section_loop 0.1.0
section_loop: ^0.1.0 copied to clipboard
Player-agnostic A-B section looping for audio apps. Loop a named part of a track on any player, with the seek-storm and queue-stall bugs handled.
๐ section_loop #
Player-agnostic A-B section looping for Dart and Flutter audio apps.
Try the live demo #
๐ Overview #
Pick a part of a track and play it over and over. A chorus, four bars of a drill, one phrase of a language recording. That is the whole job, and it is the kind of thing that looks like five lines of code until you write it.
section_loop holds no player of its own. You feed it position updates and it
tells your player when to seek or pause, so it works with just_audio,
audioplayers, a platform player behind a method channel, or a fake in a test.
The package is pure Dart and depends on no audio library.
The live demo is this package
compiled to JavaScript, driving a plain <audio> element. Every second of the
demo track plays a different pitch, so you can hear where playback jumps back.
โจ Features #
- Any player - supply a seek, a pause, and an optional play callback. That is the whole integration surface.
- Two end behaviours - loop back to the section start, or play the section once and hold at the end.
- Named, serialisable sections -
LoopSectionround-trips through JSON, so sections can be saved per track. - Handles the seek storm - positions arriving while a seek is in flight are dropped. See below.
- Leaves whole-track ranges alone - so your queue keeps auto-advancing. See below.
- No audio dependency - pure Dart, testable without a device or a player.
๐ฆ Install #
dependencies:
section_loop: ^0.1.0
๐ Usage #
import 'package:section_loop/section_loop.dart';
final engine = SectionLoopEngine(
onSeek: player.seek,
onPause: player.pause,
onPlay: player.play, // optional, resumes a paused player when looping
);
engine.section = LoopSection(
name: 'Chorus',
start: const Duration(seconds: 48),
end: const Duration(seconds: 72),
);
player.positionStream.listen((position) {
engine.handlePosition(position, trackDuration: player.duration);
});
Set engine.section = null, or call clear(), to let the track play through.
Play the section once instead of repeating:
engine.behavior = SectionEndBehavior.pauseAtEnd;
Save a section between runs:
final stored = jsonEncode(section.toJson());
final restored = LoopSection.fromJson(jsonDecode(stored) as Map<String, Object?>);
๐ Two bugs this handles for you #
Both took a while to track down in a real app. Both are covered by tests here so you do not have to meet them.
A range covering the whole track is not treated as a section.
Loop one and you seek at the final moment of playback, which suppresses the
player's completion event. The track never reports that it finished, so a queue
sitting above it silently stops advancing. Ranges that reach both ends of the
track are ignored, and the margin is configurable through edgeTolerance.
Positions arriving during an in-flight seek are dropped.
Players keep emitting the pre-seek position for a few frames after seek() is
called. Act on those and you queue up another seek, then another, and playback
never escapes the boundary. The engine ignores incoming positions until its own
seek completes.
๐ง Where it came from #
This was pulled out of AudioMark, a practice player for dancers with about 5,000 users on Google Play. Dancers loop four counts, slow them down, and run them until the choreography sticks, so the looping had to be exact and had to survive the screen going off.
The logic ended up tangled with two quirks that had nothing to do with looping, which is what made it worth extracting. It is easier to reason about, and easier to test, when it is not sitting inside a player.
๐งช Tests #
dart test
23 tests cover the section model, both end behaviours, the seek guard, and the whole-track case.
๐ License #
MIT. See LICENSE.