๐Ÿ” section_loop

Player-agnostic A-B section looping for Dart and Flutter audio apps.

CI License: MIT Dart

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 - LoopSection round-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.

Libraries

section_loop
Player-agnostic A-B section looping for audio apps.