├── .gitignore
├── .idea
├── libraries
│ ├── Dart_SDK.xml
│ └── Flutter_for_Android.xml
├── modules.xml
├── runConfigurations
│ └── example_lib_main_dart.xml
└── workspace.xml
├── .metadata
├── CHANGELOG.md
├── LICENSE
├── LICENSE.md
├── README.md
├── android
├── .gitignore
├── build.gradle
├── gradle.properties
├── settings.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── xyz
│ └── fireslime
│ └── gapless_audio_loop
│ └── GaplessAudioLoopPlugin.java
├── example
├── .gitignore
├── .metadata
├── README.md
├── assets
│ └── Loop-Menu.wav
├── lib
│ └── main.dart
├── pubspec.lock
└── pubspec.yaml
├── gapless_audio_loop.iml
├── ios
├── .gitignore
├── Assets
│ └── .gitkeep
├── Classes
│ ├── GaplessAudioLoopPlugin.h
│ ├── GaplessAudioLoopPlugin.m
│ └── SwiftGaplessAudioLoopPlugin.swift
└── gapless_audio_loop.podspec
├── lib
└── gapless_audio_loop.dart
├── pubspec.lock
└── pubspec.yaml
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .dart_tool/
3 |
4 | .packages
5 | .pub/
6 |
7 | build/
8 |
--------------------------------------------------------------------------------
/.idea/libraries/Dart_SDK.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/.idea/libraries/Flutter_for_Android.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/example_lib_main_dart.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 2e540931f73593e35627592ca4f9a4ca3035ed31
8 | channel: stable
9 |
10 | project_type: plugin
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 1.1.1
2 |
3 | - Fix for iOS compilation
4 |
5 | ## 1.1.0
6 |
7 | - Added support for files not from the asset bundle
8 |
9 | ## 1.0.0
10 |
11 | - Added pause, resume, seek and volume control
12 |
13 | ## 0.0.1
14 |
15 | - Initial release
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | TODO: Add your license here.
2 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Fireslime
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### Deprecated
2 |
3 | This package is no longer maintained as this solution does not works well on Android 9 anymore. To get gapless loop plays, use this [library instead](https://github.com/erickzanardo/ocarina)
4 |
5 | # Gapless Audio Loop
6 |
7 | A flutter plugin to enable gapless loops on Android and iOs.
8 |
9 | Android may stills see some gaps on older versions like Android 6, newer versions of the SO seems to work fine.
10 |
11 | The Android solution is heavily inspired by this [article](https://medium.com/@viksaaskool/gappless-sound-loop-on-android-1ddeccc563de).
12 |
13 | At the moment this package is very simple and does not feature many media player functions, the focus on this package is to have a gapless loop, if you have suggestions to improve this package, please file an issue or send a PR.
14 |
15 | If you need a more full-featured audio player, I suggest that you take a look on the [audioplayers package](https://github.com/luanpotter/audioplayers).
16 |
17 | # Usage
18 |
19 | Drop it on your pubspec.yaml
20 | ```yaml
21 | gapless_audio_loop: ^1.1.0
22 | ```
23 |
24 | Import it:
25 | ```dart
26 | import 'package:gapless_audio_loop/gapless_audio_loop.dart';
27 | ```
28 |
29 | Loading and starting the loop:
30 | ```dart
31 | final player = GaplessAudioLoop();
32 | await player.loadAsset('Loop-Menu.wav'); // use loadFile instead to use a file from the device storage
33 |
34 | await player.play();
35 | ```
36 |
37 | To stop the loop just call `await player.stop()`
38 |
39 | You can also pause and resume using the `pause` and `resume` methods.
40 |
41 | ## Volume
42 |
43 | Audio volume can be changed using the the `setVolume` method, which receives a double between 0 and 1, which represents the percentage of the total volume of the device, example, if you pass `0.5` it means that the audio will play on 50% of the total volume.
44 |
45 | ## Seek
46 |
47 | Seeking can be done by the `seek` method, which receives a `Duration` object, beware that since this is a looping library, if you call seek to a value bigger than the total duration of the file, unexpected behaviour may occur, so it is highly recommend to avoid that and only use durations that are inside the total length of the file.
48 |
49 | ## Troubleshooting
50 |
51 |
52 | These are some know reasons for audio files not looping perfect:
53 |
54 | - Android 6 does not seems to loop perfectly the files for some reason.
55 | - MP3 usually have gaps due to its compress format, for more info check [this question on stackexchange](https://sound.stackexchange.com/questions/8916/mp3-gapless-looping-help).
56 | - _OGG files working only on Android:_ Unfortunately OGG is not support by iOs.
57 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | group 'xyz.fireslime.gapless_audio_loop'
2 | version '1.0-SNAPSHOT'
3 |
4 | buildscript {
5 | repositories {
6 | google()
7 | jcenter()
8 | }
9 |
10 | dependencies {
11 | classpath 'com.android.tools.build:gradle:3.2.1'
12 | }
13 | }
14 |
15 | rootProject.allprojects {
16 | repositories {
17 | google()
18 | jcenter()
19 | }
20 | }
21 |
22 | apply plugin: 'com.android.library'
23 |
24 | android {
25 | compileSdkVersion 28
26 |
27 | defaultConfig {
28 | minSdkVersion 16
29 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
30 | }
31 | lintOptions {
32 | disable 'InvalidPackage'
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 |
3 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'gapless_audio_loop'
2 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/android/src/main/java/xyz/fireslime/gapless_audio_loop/GaplessAudioLoopPlugin.java:
--------------------------------------------------------------------------------
1 | package xyz.fireslime.gapless_audio_loop;
2 |
3 | import android.media.MediaPlayer;
4 |
5 | import java.io.IOException;
6 | import java.util.HashMap;
7 | import java.util.Map;
8 |
9 | import io.flutter.plugin.common.MethodCall;
10 | import io.flutter.plugin.common.MethodChannel;
11 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
12 | import io.flutter.plugin.common.MethodChannel.Result;
13 | import io.flutter.plugin.common.PluginRegistry.Registrar;
14 |
15 | class GaplessPlayer {
16 | private MediaPlayer currentPlayer = null;
17 | private MediaPlayer nextPlayer = null;
18 |
19 | private String url;
20 | private double volume;
21 |
22 | GaplessPlayer(String url, double volume) {
23 | this.url = url;
24 | this.volume = volume;
25 |
26 | try {
27 | currentPlayer = new MediaPlayer();
28 | currentPlayer.setDataSource(url);
29 | currentPlayer.setVolume((float) volume, (float) volume);
30 | currentPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
31 | @Override
32 | public void onPrepared(MediaPlayer mediaPlayer) {
33 | currentPlayer.start();
34 | }
35 | });
36 | currentPlayer.prepareAsync();
37 | createNextMediaPlayer();
38 | } catch (IOException e) {
39 | e.printStackTrace();
40 | }
41 | }
42 |
43 | public void setVolume(double volume) {
44 | this.volume = volume;
45 | if (currentPlayer != null) {
46 | currentPlayer.setVolume((float) volume, (float) volume);
47 | }
48 | if (nextPlayer != null) {
49 | nextPlayer.setVolume((float) volume, (float) volume);
50 | }
51 | }
52 |
53 | private void createNextMediaPlayer() {
54 | nextPlayer = new MediaPlayer();
55 | try {
56 | nextPlayer.setDataSource(url);
57 | nextPlayer.setVolume((float) volume, (float) volume);
58 | nextPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
59 | @Override
60 | public void onPrepared(MediaPlayer mp) {
61 | nextPlayer.seekTo(0);
62 | currentPlayer.setNextMediaPlayer(nextPlayer);
63 | currentPlayer.setOnCompletionListener(onCompletionListener);
64 | }
65 | });
66 | nextPlayer.prepareAsync();
67 | } catch (IOException e) {
68 | e.printStackTrace();
69 | }
70 | }
71 |
72 | private final MediaPlayer.OnCompletionListener onCompletionListener =
73 | new MediaPlayer.OnCompletionListener() {
74 | @Override
75 | public void onCompletion(MediaPlayer mediaPlayer) {
76 | currentPlayer = nextPlayer;
77 | createNextMediaPlayer();
78 | mediaPlayer.release();
79 | }
80 | };
81 |
82 | public void stop() {
83 | currentPlayer.stop();
84 | if (nextPlayer != null) {
85 | nextPlayer.stop();
86 | }
87 | }
88 |
89 | public void pause() {
90 | currentPlayer.pause();
91 | }
92 |
93 | public void resume() {
94 | currentPlayer.start();
95 | }
96 |
97 | public void seek(int position) {
98 | currentPlayer.seekTo(position);
99 | }
100 | }
101 |
102 | /**
103 | * GaplessAudioLoopPlugin
104 | */
105 | public class GaplessAudioLoopPlugin implements MethodCallHandler {
106 | /**
107 | * Plugin registration.
108 | */
109 | public static void registerWith(Registrar registrar) {
110 | final MethodChannel channel = new MethodChannel(registrar.messenger(), "gapless_audio_loop");
111 | channel.setMethodCallHandler(new GaplessAudioLoopPlugin());
112 | }
113 |
114 | private static int id = 0;
115 | private Map players = new HashMap<>();
116 |
117 | GaplessPlayer getPlayer(MethodCall call) {
118 | int playerId = call.argument("playerId");
119 | return players.get(playerId);
120 | }
121 | @Override
122 | public void onMethodCall(MethodCall call, Result result) {
123 | if (call.method.equals("play")) {
124 | int id = GaplessAudioLoopPlugin.id;
125 |
126 | String url = call.argument("url");
127 | double volume = call.argument("volume");
128 |
129 | GaplessPlayer player = new GaplessPlayer(url, volume);
130 | players.put(id, player);
131 |
132 | GaplessAudioLoopPlugin.id++;
133 |
134 | result.success(id);
135 | } else if (call.method.equals("stop")) {
136 | int playerId = call.argument("playerId");
137 | GaplessPlayer player = players.get(playerId);
138 |
139 | if (player != null) {
140 | player.stop();
141 | players.remove(playerId);
142 | }
143 | } else if (call.method.equals("pause")) {
144 | GaplessPlayer player = getPlayer(call);
145 |
146 | if (player != null) {
147 | player.pause();
148 | }
149 | } else if (call.method.equals("resume")) {
150 | GaplessPlayer player = getPlayer(call);
151 |
152 | if (player != null) {
153 | player.resume();
154 | }
155 | } else if (call.method.equals("setVolume")) {
156 | GaplessPlayer player = getPlayer(call);
157 | double volume = call.argument("volume");
158 |
159 | if (player != null) {
160 | player.setVolume(volume);
161 | }
162 | } else if (call.method.equals("seek")) {
163 | GaplessPlayer player = getPlayer(call);
164 | int position = call.argument("position");
165 |
166 | player.seek(position);
167 | } else {
168 | result.notImplemented();
169 | }
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 |
12 | # IntelliJ related
13 | *.iml
14 | *.ipr
15 | *.iws
16 | .idea/
17 |
18 | # The .vscode folder contains launch configuration and tasks you configure in
19 | # VS Code which you may wish to be included in version control, so this line
20 | # is commented out by default.
21 | #.vscode/
22 |
23 | # Flutter/Dart/Pub related
24 | **/doc/api/
25 | .dart_tool/
26 | .flutter-plugins
27 | .packages
28 | .pub-cache/
29 | .pub/
30 | /build/
31 |
32 | # Android related
33 | **/android/**/gradle-wrapper.jar
34 | **/android/.gradle
35 | **/android/captures/
36 | **/android/gradlew
37 | **/android/gradlew.bat
38 | **/android/local.properties
39 | **/android/**/GeneratedPluginRegistrant.java
40 |
41 | # iOS/XCode related
42 | **/ios/**/*.mode1v3
43 | **/ios/**/*.mode2v3
44 | **/ios/**/*.moved-aside
45 | **/ios/**/*.pbxuser
46 | **/ios/**/*.perspectivev3
47 | **/ios/**/*sync/
48 | **/ios/**/.sconsign.dblite
49 | **/ios/**/.tags*
50 | **/ios/**/.vagrant/
51 | **/ios/**/DerivedData/
52 | **/ios/**/Icon?
53 | **/ios/**/Pods/
54 | **/ios/**/.symlinks/
55 | **/ios/**/profile
56 | **/ios/**/xcuserdata
57 | **/ios/.generated/
58 | **/ios/Flutter/App.framework
59 | **/ios/Flutter/Flutter.framework
60 | **/ios/Flutter/Generated.xcconfig
61 | **/ios/Flutter/app.flx
62 | **/ios/Flutter/app.zip
63 | **/ios/Flutter/flutter_assets/
64 | **/ios/ServiceDefinitions.json
65 | **/ios/Runner/GeneratedPluginRegistrant.*
66 |
67 | # Exceptions to above rules.
68 | !**/ios/**/default.mode1v3
69 | !**/ios/**/default.mode2v3
70 | !**/ios/**/default.pbxuser
71 | !**/ios/**/default.perspectivev3
72 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
73 |
74 | android
75 | ios
76 | .flutter-plugins-dependencies
77 |
--------------------------------------------------------------------------------
/example/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 2e540931f73593e35627592ca4f9a4ca3035ed31
8 | channel: stable
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # gapless_audio_loop_example
2 |
3 | Demonstrates how to use the gapless_audio_loop plugin.
4 |
5 | Audio file from: https://opengameart.org/content/menu-loop
6 |
--------------------------------------------------------------------------------
/example/assets/Loop-Menu.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bluefireteam/flutter_gapless_audio_loop/7d2bb35d1224f68d8f65a4d19aa5eaf27d68f784/example/assets/Loop-Menu.wav
--------------------------------------------------------------------------------
/example/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:path_provider/path_provider.dart';
5 | import 'package:http/http.dart';
6 |
7 | import 'package:gapless_audio_loop/gapless_audio_loop.dart';
8 |
9 | void main() => runApp(MyApp());
10 |
11 | class MyApp extends StatelessWidget {
12 | @override
13 | Widget build(BuildContext context) {
14 | return MaterialApp(
15 | title: 'Flutter Demo',
16 | theme: ThemeData(
17 | primarySwatch: Colors.blue,
18 | ),
19 | home: MyHomePage(title: 'Flutter Demo Home Page'),
20 | );
21 | }
22 | }
23 |
24 | class MyHomePage extends StatefulWidget {
25 | MyHomePage({Key key, this.title}) : super(key: key);
26 |
27 | final String title;
28 |
29 | @override
30 | _MyHomePageState createState() => _MyHomePageState();
31 | }
32 |
33 | const staticFileUrl = 'https://luan.xyz/files/audio/ambient_c_motion.mp3';
34 |
35 | class _MyHomePageState extends State {
36 | GaplessAudioLoop _player;
37 | String _localFilePath;
38 |
39 | Future _loadFile() async {
40 | final bytes = await readBytes(staticFileUrl);
41 | final dir = await getApplicationDocumentsDirectory();
42 | final file = File('${dir.path}/audio.mp3');
43 |
44 | await file.writeAsBytes(bytes);
45 | if (await file.exists()) {
46 | setState(() {
47 | _localFilePath = file.path;
48 | });
49 | }
50 | }
51 |
52 | @override
53 | Widget build(BuildContext context) {
54 | return Scaffold(
55 | appBar: AppBar(
56 | title: Text(widget.title),
57 | ),
58 | body: Center(
59 | child: Column(
60 | mainAxisAlignment: MainAxisAlignment.center,
61 | children: [
62 | RaisedButton(
63 | child: Text("Play"),
64 | onPressed: () async {
65 | final player = GaplessAudioLoop();
66 | await player.loadAsset('Loop-Menu.wav');
67 |
68 | await player.play();
69 |
70 | setState(() {
71 | _player = player;
72 | });
73 | }),
74 | RaisedButton(
75 | child: Text("Stop"),
76 | onPressed: () {
77 | if (_player != null) {
78 | _player.stop();
79 | }
80 | }),
81 | RaisedButton(
82 | child: Text("Pause"),
83 | onPressed: () {
84 | if (_player != null) {
85 | _player.pause();
86 | }
87 | }),
88 | RaisedButton(
89 | child: Text("Resume"),
90 | onPressed: () {
91 | if (_player != null) {
92 | _player.resume();
93 | }
94 | }),
95 | RaisedButton(
96 | child: Text("Seek to 5 secs"),
97 | onPressed: () {
98 | if (_player != null) {
99 | _player.seek(Duration(seconds: 5));
100 | }
101 | }),
102 | Row(mainAxisAlignment: MainAxisAlignment.center, children: [
103 | Text("Volume"),
104 | RaisedButton(
105 | child: Text("0.2"),
106 | onPressed: () {
107 | if (_player != null) {
108 | _player.setVolume(0.2);
109 | }
110 | }),
111 | RaisedButton(
112 | child: Text("0.5"),
113 | onPressed: () {
114 | if (_player != null) {
115 | _player.setVolume(0.5);
116 | }
117 | }),
118 | RaisedButton(
119 | child: Text("1.0"),
120 | onPressed: () {
121 | if (_player != null) {
122 | _player.setVolume(1.0);
123 | }
124 | }),
125 | ]),
126 | RaisedButton(
127 | child: Text("Download file to Device, and play it"),
128 | onPressed: () async {
129 | await _loadFile();
130 |
131 | final player = GaplessAudioLoop();
132 | player.loadFile(_localFilePath);
133 |
134 | await player.play();
135 |
136 | setState(() {
137 | _player = player;
138 | });
139 | }),
140 | ],
141 | ),
142 | ),
143 | );
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/example/pubspec.lock:
--------------------------------------------------------------------------------
1 | # Generated by pub
2 | # See https://dart.dev/tools/pub/glossary#lockfile
3 | packages:
4 | async:
5 | dependency: transitive
6 | description:
7 | name: async
8 | url: "https://pub.dartlang.org"
9 | source: hosted
10 | version: "2.4.1"
11 | boolean_selector:
12 | dependency: transitive
13 | description:
14 | name: boolean_selector
15 | url: "https://pub.dartlang.org"
16 | source: hosted
17 | version: "2.0.0"
18 | charcode:
19 | dependency: transitive
20 | description:
21 | name: charcode
22 | url: "https://pub.dartlang.org"
23 | source: hosted
24 | version: "1.1.3"
25 | clock:
26 | dependency: transitive
27 | description:
28 | name: clock
29 | url: "https://pub.dartlang.org"
30 | source: hosted
31 | version: "1.0.1"
32 | collection:
33 | dependency: transitive
34 | description:
35 | name: collection
36 | url: "https://pub.dartlang.org"
37 | source: hosted
38 | version: "1.14.12"
39 | fake_async:
40 | dependency: transitive
41 | description:
42 | name: fake_async
43 | url: "https://pub.dartlang.org"
44 | source: hosted
45 | version: "1.1.0"
46 | flutter:
47 | dependency: "direct main"
48 | description: flutter
49 | source: sdk
50 | version: "0.0.0"
51 | flutter_test:
52 | dependency: "direct dev"
53 | description: flutter
54 | source: sdk
55 | version: "0.0.0"
56 | gapless_audio_loop:
57 | dependency: "direct dev"
58 | description:
59 | path: ".."
60 | relative: true
61 | source: path
62 | version: "1.1.1"
63 | http:
64 | dependency: "direct main"
65 | description:
66 | name: http
67 | url: "https://pub.dartlang.org"
68 | source: hosted
69 | version: "0.12.0+2"
70 | http_parser:
71 | dependency: transitive
72 | description:
73 | name: http_parser
74 | url: "https://pub.dartlang.org"
75 | source: hosted
76 | version: "3.1.3"
77 | matcher:
78 | dependency: transitive
79 | description:
80 | name: matcher
81 | url: "https://pub.dartlang.org"
82 | source: hosted
83 | version: "0.12.6"
84 | meta:
85 | dependency: transitive
86 | description:
87 | name: meta
88 | url: "https://pub.dartlang.org"
89 | source: hosted
90 | version: "1.1.8"
91 | path:
92 | dependency: transitive
93 | description:
94 | name: path
95 | url: "https://pub.dartlang.org"
96 | source: hosted
97 | version: "1.7.0"
98 | path_provider:
99 | dependency: "direct main"
100 | description:
101 | name: path_provider
102 | url: "https://pub.dartlang.org"
103 | source: hosted
104 | version: "1.3.0"
105 | pedantic:
106 | dependency: transitive
107 | description:
108 | name: pedantic
109 | url: "https://pub.dartlang.org"
110 | source: hosted
111 | version: "1.8.0+1"
112 | platform:
113 | dependency: transitive
114 | description:
115 | name: platform
116 | url: "https://pub.dartlang.org"
117 | source: hosted
118 | version: "2.2.1"
119 | sky_engine:
120 | dependency: transitive
121 | description: flutter
122 | source: sdk
123 | version: "0.0.99"
124 | source_span:
125 | dependency: transitive
126 | description:
127 | name: source_span
128 | url: "https://pub.dartlang.org"
129 | source: hosted
130 | version: "1.7.0"
131 | stack_trace:
132 | dependency: transitive
133 | description:
134 | name: stack_trace
135 | url: "https://pub.dartlang.org"
136 | source: hosted
137 | version: "1.9.3"
138 | stream_channel:
139 | dependency: transitive
140 | description:
141 | name: stream_channel
142 | url: "https://pub.dartlang.org"
143 | source: hosted
144 | version: "2.0.0"
145 | string_scanner:
146 | dependency: transitive
147 | description:
148 | name: string_scanner
149 | url: "https://pub.dartlang.org"
150 | source: hosted
151 | version: "1.0.5"
152 | term_glyph:
153 | dependency: transitive
154 | description:
155 | name: term_glyph
156 | url: "https://pub.dartlang.org"
157 | source: hosted
158 | version: "1.1.0"
159 | test_api:
160 | dependency: transitive
161 | description:
162 | name: test_api
163 | url: "https://pub.dartlang.org"
164 | source: hosted
165 | version: "0.2.15"
166 | typed_data:
167 | dependency: transitive
168 | description:
169 | name: typed_data
170 | url: "https://pub.dartlang.org"
171 | source: hosted
172 | version: "1.1.6"
173 | vector_math:
174 | dependency: transitive
175 | description:
176 | name: vector_math
177 | url: "https://pub.dartlang.org"
178 | source: hosted
179 | version: "2.0.8"
180 | sdks:
181 | dart: ">=2.6.0 <3.0.0"
182 | flutter: ">=1.12.0 <2.0.0"
183 |
--------------------------------------------------------------------------------
/example/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: gapless_audio_loop_example
2 | description: Demonstrates how to use the gapless_audio_loop plugin.
3 | publish_to: 'none'
4 |
5 | environment:
6 | sdk: ">=2.1.0 <3.0.0"
7 |
8 | dependencies:
9 | http: 0.12.0+2
10 | path_provider: 1.3.0
11 | flutter:
12 | sdk: flutter
13 |
14 | dev_dependencies:
15 | flutter_test:
16 | sdk: flutter
17 |
18 | gapless_audio_loop:
19 | path: ../
20 |
21 | flutter:
22 | uses-material-design: true
23 | assets:
24 | - assets/Loop-Menu.wav
25 |
--------------------------------------------------------------------------------
/gapless_audio_loop.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vagrant/
3 | .sconsign.dblite
4 | .svn/
5 |
6 | .DS_Store
7 | *.swp
8 | profile
9 |
10 | DerivedData/
11 | build/
12 | GeneratedPluginRegistrant.h
13 | GeneratedPluginRegistrant.m
14 |
15 | .generated/
16 |
17 | *.pbxuser
18 | *.mode1v3
19 | *.mode2v3
20 | *.perspectivev3
21 |
22 | !default.pbxuser
23 | !default.mode1v3
24 | !default.mode2v3
25 | !default.perspectivev3
26 |
27 | xcuserdata
28 |
29 | *.moved-aside
30 |
31 | *.pyc
32 | *sync/
33 | Icon?
34 | .tags*
35 |
36 | /Flutter/Generated.xcconfig
37 |
--------------------------------------------------------------------------------
/ios/Assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bluefireteam/flutter_gapless_audio_loop/7d2bb35d1224f68d8f65a4d19aa5eaf27d68f784/ios/Assets/.gitkeep
--------------------------------------------------------------------------------
/ios/Classes/GaplessAudioLoopPlugin.h:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | @interface GaplessAudioLoopPlugin : NSObject
4 | @end
5 |
--------------------------------------------------------------------------------
/ios/Classes/GaplessAudioLoopPlugin.m:
--------------------------------------------------------------------------------
1 | #import "GaplessAudioLoopPlugin.h"
2 | #import
3 |
4 | @implementation GaplessAudioLoopPlugin
5 | + (void)registerWithRegistrar:(NSObject*)registrar {
6 | [SwiftGaplessAudioLoopPlugin registerWithRegistrar:registrar];
7 | }
8 | @end
9 |
--------------------------------------------------------------------------------
/ios/Classes/SwiftGaplessAudioLoopPlugin.swift:
--------------------------------------------------------------------------------
1 | import Flutter
2 | import UIKit
3 | import AVFoundation
4 |
5 | struct Player {
6 | var player: AVQueuePlayer?
7 | var playerLooper: AVPlayerLooper?
8 | }
9 |
10 | public class SwiftGaplessAudioLoopPlugin: NSObject, FlutterPlugin {
11 | static var players = [Int: Player]()
12 | static var id: Int = 0;
13 |
14 |
15 | public static func register(with registrar: FlutterPluginRegistrar) {
16 | let channel = FlutterMethodChannel(name: "gapless_audio_loop", binaryMessenger: registrar.messenger())
17 | let instance = SwiftGaplessAudioLoopPlugin()
18 | registrar.addMethodCallDelegate(instance, channel: channel)
19 | }
20 |
21 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
22 | if (call.method == "play") {
23 | guard let args = call.arguments else {
24 | return;
25 | }
26 |
27 | if let myArgs = args as? [String: Any],
28 | let url: String = myArgs["url"] as? String,
29 | let volume: Double = myArgs["volume"] as? Double {
30 |
31 | // Get url
32 | let asset = AVAsset(url: URL(fileURLWithPath: url ))
33 | let playerItem = AVPlayerItem(asset: asset)
34 |
35 | let player = AVQueuePlayer(items: [playerItem])
36 | let playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)
37 |
38 | player.play()
39 |
40 | player.volume = Float(volume)
41 |
42 | let id = SwiftGaplessAudioLoopPlugin.id
43 | SwiftGaplessAudioLoopPlugin.players[id] = Player(player: player, playerLooper: playerLooper)
44 |
45 | SwiftGaplessAudioLoopPlugin.id = SwiftGaplessAudioLoopPlugin.id + 1
46 |
47 | result(id)
48 | }
49 | } else if (call.method == "stop" || call.method == "pause") {
50 | guard let args = call.arguments else {
51 | return;
52 | }
53 |
54 | if let myArgs = args as? [String: Any],
55 | let playerId: Int = myArgs["playerId"] as? Int {
56 |
57 | let player = SwiftGaplessAudioLoopPlugin.players[playerId]
58 | player?.player?.pause()
59 | if (call.method == "stop") {
60 | SwiftGaplessAudioLoopPlugin.players[playerId] = nil
61 | }
62 | }
63 | } else if (call.method == "setVolume") {
64 | guard let args = call.arguments else {
65 | return;
66 | }
67 |
68 | if let myArgs = args as? [String: Any],
69 | let playerId: Int = myArgs["playerId"] as? Int,
70 | let volume: Double = myArgs["volume"] as? Double {
71 |
72 | let player = SwiftGaplessAudioLoopPlugin.players[playerId]
73 | player?.player?.volume = Float(volume)
74 | }
75 | } else if (call.method == "resume") {
76 | guard let args = call.arguments else {
77 | return;
78 | }
79 |
80 | if let myArgs = args as? [String: Any],
81 | let playerId: Int = myArgs["playerId"] as? Int {
82 |
83 | let player = SwiftGaplessAudioLoopPlugin.players[playerId]
84 | player?.player?.play()
85 | }
86 | } else if (call.method == "seek") {
87 | guard let args = call.arguments else {
88 | return;
89 | }
90 |
91 | if let myArgs = args as? [String: Any],
92 | let playerId: Int = myArgs["playerId"] as? Int,
93 | let positionInMillis: Int = myArgs["position"] as? Int {
94 |
95 | let player = SwiftGaplessAudioLoopPlugin.players[playerId]
96 | player?.player?.seek(to: CMTimeMakeWithSeconds(Float64(positionInMillis / 1000), preferredTimescale: Int32(NSEC_PER_SEC)))
97 | }
98 | }
99 | }
100 | }
101 |
102 |
--------------------------------------------------------------------------------
/ios/gapless_audio_loop.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
3 | #
4 | Pod::Spec.new do |s|
5 | s.name = 'gapless_audio_loop'
6 | s.version = '0.0.1'
7 | s.summary = 'A new flutter plugin project.'
8 | s.description = <<-DESC
9 | A new flutter plugin project.
10 | DESC
11 | s.homepage = 'http://example.com'
12 | s.license = { :file => '../LICENSE' }
13 | s.author = { 'Your Company' => 'email@example.com' }
14 | s.source = { :path => '.' }
15 | s.source_files = 'Classes/**/*'
16 | s.public_header_files = 'Classes/**/*.h'
17 | s.dependency 'Flutter'
18 |
19 | s.ios.deployment_target = '10.0'
20 | end
21 |
22 |
--------------------------------------------------------------------------------
/lib/gapless_audio_loop.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:io';
3 | import 'package:path_provider/path_provider.dart';
4 | import 'package:flutter/services.dart' show rootBundle;
5 |
6 | import 'package:flutter/services.dart';
7 |
8 | class GaplessAudioLoop {
9 | static const MethodChannel _channel =
10 | const MethodChannel('gapless_audio_loop');
11 |
12 | /// A reference to the loaded file.
13 | String _loadedFile;
14 | int _id;
15 | double _volume = 1.0;
16 |
17 | double get volume => _volume;
18 |
19 | void setVolume(double volume) async {
20 | _volume = volume;
21 |
22 | if (_id != null) {
23 | await _channel
24 | .invokeMethod("setVolume", {'playerId': _id, "volume": _volume});
25 | }
26 | }
27 |
28 | Future _fetchAsset(String fileName) async {
29 | return await rootBundle.load('assets/$fileName');
30 | }
31 |
32 | Future _fetchToMemory(String fileName) async {
33 | final file = File('${(await getTemporaryDirectory()).path}/$fileName');
34 | await file.create(recursive: true);
35 | return await file
36 | .writeAsBytes((await _fetchAsset(fileName)).buffer.asUint8List());
37 | }
38 |
39 | /// Load the [fileName] for playing
40 | ///
41 | Future loadAsset(String fileName) async {
42 | if (_loadedFile != null) {
43 | return _loadedFile;
44 | }
45 |
46 | final result = await _fetchToMemory(fileName);
47 | _loadedFile = result.path;
48 | }
49 |
50 | void loadFile(String path) {
51 | _loadedFile = path;
52 | }
53 |
54 | Future play() async {
55 | assert(_loadedFile != null, 'File is not loaded');
56 |
57 | // Do nothing when it is already playing
58 | if (_id == null) {
59 | _id = await _channel
60 | .invokeMethod("play", {'url': _loadedFile, 'volume': _volume});
61 | }
62 | }
63 |
64 | Future pause() async {
65 | assert(_id != null, 'Loop is not playing');
66 |
67 | await _channel.invokeMethod("pause", {'playerId': _id});
68 | }
69 |
70 | Future resume() async {
71 | assert(_loadedFile != null, 'File is not loaded');
72 | assert(_id != null, 'Loop is not playing');
73 |
74 | await _channel.invokeMethod("resume", {'playerId': _id});
75 | }
76 |
77 | Future stop() async {
78 | assert(_loadedFile != null, 'File is not loaded');
79 | assert(_id != null, 'Loop is not playing');
80 |
81 | await _channel.invokeMethod("stop", {'playerId': _id});
82 | }
83 |
84 | Future seek(Duration duration) async {
85 | assert(_loadedFile != null, 'File is not loaded');
86 | assert(_id != null, 'Loop is not playing');
87 |
88 | await _channel.invokeMethod(
89 | "seek", {'playerId': _id, "position": duration.inMilliseconds});
90 | }
91 |
92 | bool isAssetLoaded() {
93 | return _loadedFile != null;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/pubspec.lock:
--------------------------------------------------------------------------------
1 | # Generated by pub
2 | # See https://dart.dev/tools/pub/glossary#lockfile
3 | packages:
4 | async:
5 | dependency: transitive
6 | description:
7 | name: async
8 | url: "https://pub.dartlang.org"
9 | source: hosted
10 | version: "2.3.0"
11 | boolean_selector:
12 | dependency: transitive
13 | description:
14 | name: boolean_selector
15 | url: "https://pub.dartlang.org"
16 | source: hosted
17 | version: "1.0.5"
18 | charcode:
19 | dependency: transitive
20 | description:
21 | name: charcode
22 | url: "https://pub.dartlang.org"
23 | source: hosted
24 | version: "1.1.2"
25 | collection:
26 | dependency: transitive
27 | description:
28 | name: collection
29 | url: "https://pub.dartlang.org"
30 | source: hosted
31 | version: "1.14.11"
32 | flutter:
33 | dependency: "direct main"
34 | description: flutter
35 | source: sdk
36 | version: "0.0.0"
37 | flutter_test:
38 | dependency: "direct dev"
39 | description: flutter
40 | source: sdk
41 | version: "0.0.0"
42 | matcher:
43 | dependency: transitive
44 | description:
45 | name: matcher
46 | url: "https://pub.dartlang.org"
47 | source: hosted
48 | version: "0.12.5"
49 | meta:
50 | dependency: transitive
51 | description:
52 | name: meta
53 | url: "https://pub.dartlang.org"
54 | source: hosted
55 | version: "1.1.7"
56 | path:
57 | dependency: transitive
58 | description:
59 | name: path
60 | url: "https://pub.dartlang.org"
61 | source: hosted
62 | version: "1.6.4"
63 | path_provider:
64 | dependency: "direct main"
65 | description:
66 | name: path_provider
67 | url: "https://pub.dartlang.org"
68 | source: hosted
69 | version: "1.1.2"
70 | pedantic:
71 | dependency: transitive
72 | description:
73 | name: pedantic
74 | url: "https://pub.dartlang.org"
75 | source: hosted
76 | version: "1.8.0+1"
77 | quiver:
78 | dependency: transitive
79 | description:
80 | name: quiver
81 | url: "https://pub.dartlang.org"
82 | source: hosted
83 | version: "2.0.5"
84 | sky_engine:
85 | dependency: transitive
86 | description: flutter
87 | source: sdk
88 | version: "0.0.99"
89 | source_span:
90 | dependency: transitive
91 | description:
92 | name: source_span
93 | url: "https://pub.dartlang.org"
94 | source: hosted
95 | version: "1.5.5"
96 | stack_trace:
97 | dependency: transitive
98 | description:
99 | name: stack_trace
100 | url: "https://pub.dartlang.org"
101 | source: hosted
102 | version: "1.9.3"
103 | stream_channel:
104 | dependency: transitive
105 | description:
106 | name: stream_channel
107 | url: "https://pub.dartlang.org"
108 | source: hosted
109 | version: "2.0.0"
110 | string_scanner:
111 | dependency: transitive
112 | description:
113 | name: string_scanner
114 | url: "https://pub.dartlang.org"
115 | source: hosted
116 | version: "1.0.5"
117 | term_glyph:
118 | dependency: transitive
119 | description:
120 | name: term_glyph
121 | url: "https://pub.dartlang.org"
122 | source: hosted
123 | version: "1.1.0"
124 | test_api:
125 | dependency: transitive
126 | description:
127 | name: test_api
128 | url: "https://pub.dartlang.org"
129 | source: hosted
130 | version: "0.2.5"
131 | typed_data:
132 | dependency: transitive
133 | description:
134 | name: typed_data
135 | url: "https://pub.dartlang.org"
136 | source: hosted
137 | version: "1.1.6"
138 | vector_math:
139 | dependency: transitive
140 | description:
141 | name: vector_math
142 | url: "https://pub.dartlang.org"
143 | source: hosted
144 | version: "2.0.8"
145 | sdks:
146 | dart: ">=2.2.2 <3.0.0"
147 | flutter: ">=0.1.4 <2.0.0"
148 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: gapless_audio_loop
2 | description: A plugin specialized on playing audio files on a gapless loop
3 | version: 1.1.1
4 | homepage: https://github.com/fireslime/flutter_gapless_audio_loop
5 |
6 | environment:
7 | sdk: ">=2.1.0 <3.0.0"
8 | flutter: ">=1.12.0 <2.0.0"
9 |
10 | dependencies:
11 | flutter:
12 | sdk: flutter
13 | path_provider: ^1.1.2
14 |
15 | dev_dependencies:
16 | flutter_test:
17 | sdk: flutter
18 | http: 0.12.0+2
19 |
20 | flutter:
21 | plugin:
22 | platforms:
23 | android:
24 | package: xyz.fireslime.gapless_audio_loop
25 | pluginClass: GaplessAudioLoopPlugin
26 | ios:
27 | pluginClass: GaplessAudioLoopPlugin
28 |
--------------------------------------------------------------------------------