Playback Components
The playback system is a small family of composable components that together turn a<ppt-coil> full of solfège tokens into scheduled audio. Each component has a single responsibility — transport control, timing resolution, mixing, or synthesis — and they communicate exclusively through a shared EventBus rather than direct DOM references. This keeps them loosely coupled and straightforward to compose in different configurations.
Four of the five components are headless: they render no visible UI and exist only to coordinate behaviour. The exception is <ppt-coil-transport>, which provides the Play/Stop/BPM/Loop controls a performer or student interacts with directly.
All event names follow a consistent kebab-case convention and are summarised in the EventBus summary table at the bottom of this page.
Transport Bar — <ppt-coil-transport>
The transport bar is the primary interactive surface of the playback system. It renders three controls inline:
- Play / Stop toggle — clicking Play publishes a
coil-playevent carrying the current BPM and loop state; clicking Stop (or clicking again while playing) publishescoil-stop. - BPM field — a numeric input clamped to 40–300, defaulting to
120. The current value is bundled into everycoil-playevent. - Loop checkbox — when checked, the scheduler loops indefinitely rather than auto-stopping at the end of the track.
The component also subscribes to coil-stop on the EventBus, so that when the scheduler auto-fires stop at the end of a non-looping track, the transport UI resets to its resting state automatically — no imperative wiring required.
No custom attributes are required. Drop the element anywhere in the page; it is self-contained.
View Source Code
<ppt-coil-transport></ppt-coil-transport>EventBus
- Publishes
coil-play { bpm, loop }— on Play button press. - Publishes
coil-stop {}— on Stop button press. - Subscribes
coil-stop {}— resets the button to its resting state when the scheduler auto-fires stop.
Mixer — <ppt-coil-mixer>
Headless layout decorator — renders no visual UI of its own. The mixer wraps a<ppt-coil> and injects per-row Mute andSolo button headers into the header slot of each<ppt-coil-row> it discovers. It uses a MutationObserverto watch the coil's DOM, so rows added after the initial render are handled automatically.
Button colouring follows the layer type of each row's parent<ppt-coil-layer> (rhythm, harmony, melody), giving performers a quick visual cue. On initialisation, any row whose parent layer carries the mutedattribute starts in the muted state without requiring an explicit event.
Placement: wrap a <ppt-coil> in<ppt-coil-mixer>. Do not place it inside the coil — the mixer must be an ancestor, not a sibling or child.
View Source Code
<ppt-coil-mixer>
<ppt-coil>
<ppt-coil-layer layer="rhythm">
<ppt-coil-row>...</ppt-coil-row>
</ppt-coil-layer>
<ppt-coil-layer layer="harmony">
<ppt-coil-row>...</ppt-coil-row>
</ppt-coil-layer>
<ppt-coil-layer layer="melody">
<ppt-coil-row>...</ppt-coil-row>
</ppt-coil-layer>
</ppt-coil>
</ppt-coil-mixer>EventBus
- Publishes
mixer-mute { layer, rowIndex, active }— when a Mute button is toggled. - Publishes
mixer-solo { layer, rowIndex, active }— when a Solo button is toggled. - Subscribes
mixer-batch-update { action: 'solo-layer' | 'reset', layer? }— applies preset mixer states from<ppt-playalong-presets>.
Scheduler — <ppt-playback-scheduler>
Headless audio engine — renders nothing visually. The scheduler is the timing brain of the playback system. When a coil-play event fires it:
- Reads the rhythm layer tokens from its parent
<ppt-coil>and resolves them into an absolute timing grid of note onsets using the Tone.js Transport. If no rhythm data is present it falls back to a uniform 16-sixteenth-note grid. - Maps melody and harmony layer tokens onto those onsets, respecting mute and solo state as last reported by the mixer.
- Schedules
play-note-{voiceId}events viaTone.Drawat the correct audio-context times, ensuring the UI and audio remain tightly synchronised.
In non-loop mode, the scheduler auto-publishes coil-stopwhen the final note onset has been scheduled, which in turn resets the transport UI.
Placement: the scheduler must live inside a<ppt-coil> so that it can traverse the coil's layer and row children to read token data. Placing it outside the coil will prevent it from finding any layers.
View Source Code
<ppt-coil>
<ppt-playback-scheduler></ppt-playback-scheduler>
<ppt-tone-voice voice-id="melody"></ppt-tone-voice>
<ppt-tone-voice voice-id="harmony"></ppt-tone-voice>
<ppt-coil-layer layer="rhythm">...</ppt-coil-layer>
<ppt-coil-layer layer="harmony">...</ppt-coil-layer>
<ppt-coil-layer layer="melody">...</ppt-coil-layer>
</ppt-coil>EventBus
- Subscribes
coil-play { bpm, loop }— starts scheduling. - Subscribes
coil-stop {}— cancels all scheduled events and tears down the Transport. - Subscribes
mixer-mute { layer, rowIndex, active }— updates internal mute state. - Subscribes
mixer-solo { layer, rowIndex, active }— updates internal solo state. - Publishes
play-note-{voiceId} { freq, duration?, time? }— fires at each note onset. - Publishes
stop-note-{voiceId} { freq?, time? }— fires to release held notes. - Publishes
coil-stop {}— auto-fires at end of track in non-loop mode.
Synth Voice — <ppt-tone-voice>
Headless synthesiser — renders nothing visually. Each<ppt-tone-voice> instance wraps a Tone.jsPolySynth and listens on its own namespaced EventBus channels (play-note-{voiceId} and stop-note-{voiceId}) to trigger and release notes. Multiple voices can coexist inside a single coil — for example, one for melody and one for harmony — each distinguished by a unique voice-id.
The underlying PolySynth is initialised lazily on the firstplay-note event, which defers the AudioContext creation until a genuine user gesture has occurred and avoids browser autoplay restrictions.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
voice-id | string | 'default' | Unique identifier for this voice. The scheduler publishesplay-note-{voiceId} and stop-note-{voiceId} events targeting this string. |
View Source Code
<!-- Two voices inside a coil: one for melody, one for harmony -->
<ppt-coil>
<ppt-tone-voice voice-id="melody"></ppt-tone-voice>
<ppt-tone-voice voice-id="harmony"></ppt-tone-voice>
<ppt-playback-scheduler></ppt-playback-scheduler>
<!-- ...layers... -->
</ppt-coil>EventBus
- Subscribes
play-note-{voiceId} { freq, duration?, time? }— triggers the PolySynth at the given frequency and time. - Subscribes
stop-note-{voiceId} { freq?, time? }— releases a specific frequency (or all held notes iffreqis omitted).
Presets Panel — <ppt-playalong-presets>
Headless preset controller — renders nothing visually beyond the preset buttons themselves. The presets panel provides four shortcut buttons designed for pedagogical use — situations where a student wants to hear just one layer in isolation, or return to the full arrangement quickly:
- Full Track — resets all mute/solo state, playing every layer.
- Solo Melody — solos the melody layer, muting rhythm and harmony.
- Solo Harmony — solos the harmony layer, muting rhythm and melody.
- Rhythm Only — solos the rhythm layer, muting melody and harmony.
Each button fires a mixer-batch-update event that the<ppt-coil-mixer> interprets to apply the corresponding state atomically. This component is designed to be used alongside, not inside,<ppt-coil-mixer>.
No custom attributes are required.
View Source Code
<!-- Presets sit alongside the mixer, outside the coil -->
<ppt-playalong-presets></ppt-playalong-presets>
<ppt-coil-mixer>
<ppt-coil>
<!-- ...layers... -->
</ppt-coil>
</ppt-coil-mixer>EventBus
- Publishes
mixer-batch-update { action: 'reset' }— on Full Track. - Publishes
mixer-batch-update { action: 'solo-layer', layer: 'melody' | 'harmony' | 'rhythm' }— on the three layer-specific presets.
Full Composition Example
The snippet below shows how all five components fit together in a typical playalong configuration. The transport and presets sit outside the mixer; the scheduler and voices live inside the coil.
View Source Code
<!-- Transport and presets: outside the coil -->
<ppt-coil-transport></ppt-coil-transport>
<ppt-playalong-presets></ppt-playalong-presets>
<!-- Mixer wraps the coil -->
<ppt-coil-mixer>
<ppt-coil>
<!-- Headless engines live inside the coil -->
<ppt-playback-scheduler></ppt-playback-scheduler>
<ppt-tone-voice voice-id="melody"></ppt-tone-voice>
<ppt-tone-voice voice-id="harmony"></ppt-tone-voice>
<!-- Three-layer content -->
<ppt-coil-layer layer="rhythm">
<ppt-coil-row>...</ppt-coil-row>
</ppt-coil-layer>
<ppt-coil-layer layer="harmony">
<ppt-coil-row>...</ppt-coil-row>
</ppt-coil-layer>
<ppt-coil-layer layer="melody">
<ppt-coil-row>...</ppt-coil-row>
</ppt-coil-layer>
</ppt-coil>
</ppt-coil-mixer>EventBus Summary
All inter-component communication uses a shared EventBus. The table below lists every event name, its payload shape, and which components publish or subscribe to it.
| Event | Payload | Publisher(s) | Subscriber(s) |
|---|---|---|---|
coil-play | { bpm, loop } | ppt-coil-transport | ppt-playback-scheduler |
coil-stop | {} | ppt-coil-transport, ppt-playback-scheduler (auto) | ppt-coil-transport, ppt-playback-scheduler |
mixer-mute | { layer, rowIndex, active } | ppt-coil-mixer | ppt-playback-scheduler |
mixer-solo | { layer, rowIndex, active } | ppt-coil-mixer | ppt-playback-scheduler |
mixer-batch-update | { action: 'reset' | 'solo-layer', layer? } | ppt-playalong-presets | ppt-coil-mixer |
play-note-{voiceId} | { freq, duration?, time? } | ppt-playback-scheduler | ppt-tone-voice |
stop-note-{voiceId} | { freq?, time? } | ppt-playback-scheduler | ppt-tone-voice |