flutter_gapless_loop 0.0.9
flutter_gapless_loop: ^0.0.9 copied to clipboard
True sample-accurate gapless audio looping on iOS, Android, macOS, Windows, and Linux. Zero-gap, zero-click loop playback for music production apps.
0.0.10 #
New features #
-
Play-once mode.
play()now accepts an optional named parameter:play({bool loop = true}). Passloop: falseto play through the file (or loop region) exactly once;stateStreamemitsPlayerState.stoppedwhen the end is reached. The default istrue, so all existing callers continue to loop without any changes. Implemented natively on iOS, macOS, Android, and Windows. -
3-band EQ (iOS, macOS, Android, Windows). A real-time parametric equaliser applies three biquad filters to every audio chunk in the playback pipeline:
setEq(EqSettings)— low shelf at 80 Hz, peaking at 1 kHz, high shelf at 10 kHz. Each band is ±12 dB. Settings take effect immediately without a file reload.resetEq()— restores all three bands to 0 dB.
-
Cutoff filter (iOS, macOS, Android, Windows). A single-pole biquad low-pass or high-pass filter, applied after the EQ in the signal chain:
setCutoffFilter(CutoffFilterSettings)— configureFilterType.lowPassorFilterType.highPass, cutoff frequency (20–20000 Hz), and resonance (Q factor, default 0.707 = Butterworth).resetCutoffFilter()— bypasses the filter.
-
Reverb (iOS, macOS).
setReverb(ReverbPreset, {double wetMix = 0.3})applies one of seven built-in room simulations:smallRoom,mediumRoom,largeRoom,mediumHall,largeHall,plate,cathedral.wetMixcontrols the blend from 0.0 (dry) to 1.0 (fully wet). -
Compressor (iOS, macOS).
setCompressor(CompressorSettings)applies dynamic range compression with configurable threshold (dB), makeup gain (dB), attack time (ms), and release time (ms). -
Pitch shift (iOS, macOS).
setPitch(double semitones)shifts pitch by ±24 semitones without affecting playback speed. Independent ofsetPlaybackRate. -
FFT spectrum analyser (iOS, macOS). Real-time 256-bin FFT data for visualisers and meters:
enableSpectrum()/disableSpectrum()— start or stop analysis.spectrumStream—Stream<SpectrumData>emitting normalised[0, 1]magnitude bins from low to high frequency at ~20 Hz.
-
Volume fades (iOS, macOS). Smooth gain ramps handled natively at 100 Hz:
fadeTo(double targetVolume, Duration duration)— fade to any target volume.fadeIn(Duration duration)— shorthand forfadeTo(1.0, duration).fadeOut(Duration duration)— shorthand forfadeTo(0.0, duration).
-
Export to file (iOS, macOS).
exportToFile(String outputPath, {ExportFormat format})renders the currently loaded audio (with all active DSP effects applied) to a WAV file. SupportsExportFormat.wav32bit(IEEE 32-bit float, default) andExportFormat.wav16bit(16-bit integer PCM). -
Now Playing info (iOS).
setNowPlayingInfo(NowPlayingInfo)populates the iOS lock screen and Control Center media strip (title, artist, album, artwork, duration). -
Effects preset — save and restore.
applyEffectsPreset(EffectsPreset)atomically applies a bundle of EQ, reverb, compressor, and cutoff settings. UseEffectsPresetto snapshot and restore the full DSP chain in one call. -
A-B loop points (all platforms, Dart layer). Bookmark the current playback position as a loop boundary without stopping:
saveLoopPointA()/saveLoopPointB()— capture the current position as the loop start / end.applyLoopPoints()— callsetLoopRegionwith the saved A-B positions.clearLoopPoints()— reset both points.loopPointsgetter — read the currentLoopPointsstate.
-
Count-in before playback (all platforms, Dart layer).
playAfterCountIn(MetronomePlayer metro, BpmResult bpm, {int bars = 1})subscribes to the running metronome'sbeatStreamand starts playback automatically after the specified number of complete bars.
0.0.9 #
Build system #
- Swift sources moved to SPM-standard layout.
darwin/Sources/flutter_gapless_loop/replacesdarwin/Classes/as the Swift source directory, satisfying pana's SPM directory check and fixing the pub.dev "Package does not support Swift Package Manager" score for iOS and macOS.darwin/Package.swiftand all three CocoaPods podspecs updated accordingly.
New platforms #
- Linux support. Full implementation using miniaudio v0.11.21 (PipeWire / PulseAudio / ALSA auto-selected at runtime) for audio output and decode, plus
libcurlfor URL loading. All four playback modes (full/region × with/without crossfade), BPM/time-signature detection, equal-power crossfade, metronome, real-time amplitude metering, stereo pan, volume, seek, and playback rate are supported. Minimum: Ubuntu 20.04+ / glibc 2.31+.
0.0.8 #
Bug fixes #
-
iOS: fix overlapping
inoutaccess in FFT normalisation.vDSP_vsdivwas passed&magsas both input and output, violating Swift's memory exclusivity rules (overlapping accesses to 'mags', but modification requires exclusive access). The input is now copied to a localsrcvariable before the call. -
iOS:
clamped(to:)extension added.Float.clamped(to:)is not publicly available in the Swift standard library — thepackageprotection level on the stdlib internal made calls to it fail with 'clamped' is inaccessible due to 'package' protection level. Aprivate extension Comparableprovidingclamped(to:)is now defined inLoopAudioEngine.swift. -
iOS:
trimSilencespuriousreturnremoved. TheguardintrimSilenceincorrectly usedreturn [] as Void, which the compiler rejected as unexpected non-void return value in void function. Fixed to a barereturn. -
iOS:
kDynamicsProcessorParam_OverallGainused insetCompressor.kDynamicsProcessorParam_MasterGainwas removed in iOS 7; its Swift-visible name is nowkDynamicsProcessorParam_OverallGain. Updated accordingly.
0.0.7 #
Bug fixes #
-
iOS:
loadFileruns on a background queue.AVAudioFile(forReading:)andbuffer.read(into:)are synchronous I/O calls. Previously both the'load'and'loadAsset'method channel handlers invokedloadFiledirectly on the platform channel handler thread (the main thread), blocking the Flutter UI for the full duration of audio decoding. Both handlers now dispatchloadFiletoDispatchQueue.global(qos: .userInitiated)and return the Flutter result onDispatchQueue.main. The'loadUrl'handler is unaffected — itsloadFilecall already runs inside aURLSession.dataTaskcompletion block on a background thread. -
iOS:
sessionConfiguredaccess level corrected.LoopAudioEngine.sessionConfiguredwas declaredprivate staticinLoopAudioEngine.swiftbut accessed fromFlutterGaplessLoopPlugin.swiftin the same module. In Swift,privateis file-scoped — this cross-file reference would fail to compile. Changed tointernal static(Swift's default), which is correct: the property is reset by the plugin ondetachFromEngine(hot restart) and must be visible within the module without being part of the public API. -
Web:
AudioContextauto-resumed onplay(). Browsers suspend theAudioContextuntil a user gesture occurs. Callingplay()without prior interaction would silently fail because the context remainedsuspended.play()now callsAudioContext.resume()before scheduling playback, conforming to the Web Audio API autoplay policy. -
Web:
setCrossfadeDurationthrowsUnsupportedError. The web implementation previously ignored crossfade duration changes silently. It now throwsUnsupportedError('setCrossfadeDuration is not supported on web')so callers receive explicit feedback. -
Dart/IO: temp file names include a random nonce.
loadFromBytesandloadFromUrlwrite audio data to a temporary file whose name previously used only a millisecond timestamp, creating a collision window under concurrent calls. The name now includes a 32-bit random suffix, making collisions practically impossible.
New API #
-
LoopAudioPlayer.isDisposed— synchronousboolgetter. Returnstrueafterdispose()has been called. Use this to guard cleanup code or check lifecycle state without async overhead. -
LoopAudioPlayer.lastKnownPosition— synchronousdoublegetter (seconds). Updated byseek()and reset to0.0bystop(). Use this for non-critical UI reads that can tolerate a slightly stale value; usecurrentPosition(async) when an exact native-layer position is required.
Build system #
- Swift Package Manager (SPM) support (iOS and macOS). iOS and macOS Swift sources are now unified in
darwin/Classes/and exposed as an SPM package (darwin/Package.swift). On Flutter 3.27+, SPM is the default build system for both platforms — no configuration flag required. CocoaPods remains supported as a fallback. This consolidation eliminates ~1700 lines of duplicated Swift source.
Breaking changes #
- Minimum Flutter version raised from
3.3.0to3.27.0. Apps targeting an earlier Flutter release should pin toflutter_gapless_loop: ^0.0.6.
0.0.6 #
Bug fixes #
- Multi-engine
AVAudioSessionconflict (iOS). When twoLoopAudioPlayerinstances are used concurrently (e.g. a drone pad and a loop player), everyloadFilecall on a new engine previously re-ranAVAudioSession.setCategory(.playback) + setActive(true)on the shared session. Reconfiguring the sharedAVAudioSessionwhile anotherAVAudioEngineis actively running triggers anAVAudioEngineConfigurationChangenotification that invalidates the running engine, causing the second player'sengine.start()to fail. Fixed by guardingsetCategory/setActivebehind aprivate static var sessionConfiguredflag — the session is configured exactly once per process lifetime, regardless of how many engines are created. Each engine instance still registers its owninterruptionNotificationandrouteChangeNotificationobservers independently. The static flag is reset indetachFromEngine(for:)so a hot restart correctly reconfigures the session for the next engine lifecycle.
0.0.5 #
Bug fixes #
clearAllunhandled exception on startup. On every cold start and hot restart the Dart constructor calledclearAllon the native engine map (a fire-and-forget method with noplayerId). On Android, iOS, and macOS theplayerIdguard ran unconditionally at the top ofonMethodCall/handle(_:result:)andhandleMetronomeCall, soclearAllwas rejected withPlatformException(INVALID_ARGS, 'playerId' is required)before it could be dispatched. Because the call is fire-and-forget the error surfaced as an unhandled exception: two per launch (one fromLoopAudioPlayer, one fromMetronomePlayer). Fixed by handlingclearAllas an early-return before theplayerIdguard in all three platforms (Android, iOS, macOS). The now-unreachable duplicateclearAllcases inside thewhen/switchblocks were removed.
0.0.4 #
New platforms #
- macOS support. Full implementation using
AVAudioEngine+AVAudioUnitTimePitch, matching the iOS engine. Audio session is replaced byAVAudioEngineConfigurationChangenotifications. Minimum macOS version: 11.0. - Windows support. Full implementation using XAudio2 2.9 (Windows 10+) + MediaFoundation decoding. All four playback modes (full/region × with/without crossfade) are supported. Beat-accurate metronome via XAudio2 +
std::chronotimer. BPM/time-signature detection ported in C++. Audio device changes handled viaIMMNotificationClient.
Bug fixes #
- Hot-restart guard. A
static bool _didClearAllflag firesclearAllon the native engine map the first time aLoopAudioPlayerorMetronomePlayeris constructed after a Dart hot restart. This prevents stale native engines from a previous Dart generation leaking into the new session. All four native platforms (iOS, Android, macOS, Windows) handle theclearAllcall on both the loop and metronome channels. - GC-based dispose safety net.
LoopAudioPlayerandMetronomePlayernow register aFinalizer<String>that fires a nativedisposecall if the Dart object is garbage-collected without an explicitdispose(). Instances are tracked in aSet<WeakReference<T>>so they do not prevent collection. The_forEachLivehelper inLoopAudioMaster/MetronomeMasterlazily removes stale weak references during group-bus operations.
Performance improvements #
- Android: async
MediaCodecdecode.AudioFileLoadernow usesMediaCodec.Callback(async mode) instead of a synchronous poll loop with a 10 ms dequeue timeout. Codec buffer callbacks fire immediately when the hardware is ready, eliminating hundreds of unnecessary spin cycles on longer files. Biggest win on files ≥ 10 seconds. - Android: pre-allocated PCM buffer. The decoded PCM output is now pre-allocated from the track duration estimate (+ 10% headroom for encoder padding) and written into directly, replacing the previous
ArrayList<FloatArray>collect-then-copy pattern. This cuts peak memory usage and eliminates one full-sizeFloatArraycopy per load.
New features #
-
LoopAudioPlayer.amplitudeStream. A newStream<AmplitudeEvent>that emits real-time audio level data approximately 20 times per second while the player is inPlayerState.playing. EachAmplitudeEventcarries:rms— root-mean-square level of the most recent audio buffer rendered by the native engine, in[0.0, 1.0]. Smooth signal; well-suited for VU meters.peak— peak sample magnitude of the same buffer, in[0.0, 1.0]. Reacts faster thanrms; use for peak-hold indicators.
The stream emits no events when playback is paused or stopped. Both iOS and Android compute RMS and peak in the native render thread and post events via the existing
EventChannel. -
LoopAudioMaster. A new static group-bus controller for all liveLoopAudioPlayerinstances.setVolumescales every instance multiplicatively (effectiveVolume = localVolume × masterVolume);setPanshifts every instance additively (effectivePan = clamp(localPan + masterPan, −1, 1)).reset()restores defaults and re-applies. Per-instance relative levels are preserved at the Dart layer — native engines receive only the final effective float. -
MetronomeMaster. Same group-bus pattern for all liveMetronomePlayerinstances. -
MetronomePlayer.setVolume/setPan. New per-instance volume and pan control onMetronomePlayer. Effective values are computed multiplicatively withMetronomeMasterbefore being sent to native. iOS:AVAudioEngine.mainMixerNode.volume/.pan, re-applied after everysetupAndPlayrebuild. Android:AudioTrack.setStereoVolumeviapanToGains, re-applied after everyplayBarBufferrebuild.
Breaking changes #
LoopAudioPlayer.setVolumepreviously threwArgumentErrorfor values outside[0.0, 1.0]; it now silently clamps to be consistent withsetPanand the new master API.
0.0.3 #
New features #
- Multi-instance support. Any number of
LoopAudioPlayerandMetronomePlayerinstances can run concurrently without cross-talk. Each instance receives a uniqueplayerId('loop_N'/'metro_N') injected into every method channel call. Events are tagged with the same ID so the Dart layer filters them per-instance using a shared broadcast stream. MetronomePlayer. A new class that drives a sample-accurate click track independent ofLoopAudioPlayer. Pre-generates a single-bar PCM buffer (accent on beat 0, regular clicks on beats 1…N-1) and loops it via the native hardware scheduler. Beat-tick events emitted per beat for UI synchronisation. API:start,stop,setBpm,setBeatsPerBar,beatStream,dispose.loadFromUrl(Uri). Downloads and loads audio from an HTTP/HTTPS URL using the native networking stack (URLSessionon iOS,HttpURLConnectionon Android) — no third-party packages required.loadFromBytes(Uint8List). Loads audio from in-memory bytes by writing to a temporary file, loading it, and cleaning up immediately.- Automatic time signature detection.
BpmResultnow includesbeatsPerBar(int) andbars(List<double>) in addition tobpm,confidence, andbeats. - Pitch-preserving playback rate (
setPlaybackRate) — time-stretch from 0.25× to 4×.
Native engine changes #
- iOS:
MetronomeEngineuses its ownAVAudioEngine+AVAudioPlayerNode. Bar buffer is built withbuildBarBuffer(bpm:beatsPerBar:)and looped viascheduleBuffer(.loops). Beat ticks fire viaDispatchSourceTimeron.main. Plugin bridge now holds[String: LoopAudioEngine]and[String: MetronomeEngine]registries. - Android:
MetronomeEngineusesAudioTrack MODE_STATIC+setLoopPointsfor hardware-level looping. Bar buffer is built viabuildBarBuffer()(companion object — unit-testable). Beat ticks fire viaHandler. Plugin bridge now holdsHashMap<String, LoopAudioEngine>andHashMap<String, MetronomeEngine>registries.
Breaking changes #
loadFromUrlno longer accepts anhttpClientparameter (native networking is used instead).- All method channel payloads now include a
playerIdkey. Custom native-side integrations must be updated to extract and route by this key.
Dependencies #
- Removed
http: ^1.2.0(no longer needed).
0.0.2 #
loadFromUrlnow downloads via the platform networking stack (URLSessionon iOS,HttpURLConnectionon Android) instead of Dart's HTTP client. No third-party packages required.- URL scheme is validated natively (
http/httpsonly); invalid schemes returnPlatformException(INVALID_ARGS). - Temp files for URL downloads use UUID names and are always cleaned up, including on coroutine cancellation (Android) and write failure (iOS).
0.0.1 #
- Initial release.
- Sample-accurate gapless looping on iOS (AVAudioEngine) and Android (AudioTrack).
- Configurable loop region (start/end in seconds).
- Optional equal-power crossfade between loop iterations.
- Volume control and seek support.
- Stereo pan control (
setPan). - Pitch-preserving playback rate / time-stretching (
setPlaybackRate). - Automatic BPM/tempo detection after every load (
bpmStream,BpmResult). stateStream,errorStream,routeChangeStream, andbpmStreamfor reactive UI.- Audio route change events (e.g. headphones unplugged).