├── .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 | 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 | 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 | --------------------------------------------------------------------------------