6.6 KiB
OpenSpec Change: enterprise-audio-transport
- Change ID: enterprise-audio-transport
- Package:
@suaveplan/audio-transport - Category: universal
- Target path:
packages/audio/audio-transport - Status: PROPOSED
- Date: 2026-05-14
- Design:
./design.md - Spec delta:
./specs/audio-transport/spec.md
Summary
Deliver @suaveplan/audio-transport on the genesis branch from scratch at enterprise quality. The package is a musical clock and transport providing sample-accurate scheduling with PPQ (pulses per quarter), BPM automation, swing, time signatures, and loop regions. It is built on @suaveplan/timing for the lookahead scheduler and @suaveplan/fsm for playback state. Inspired by Tone.js Transport, but universal and injectable.
Current-state audit (genesis)
The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data.
Available workspace deps on genesis
| Package | Role |
|---|---|
@suaveplan/timing (workspace:^) |
createScheduler, Signal, FakeClock for lookahead and tempo curves |
@suaveplan/events (workspace:^) |
Typed event emitter for TransportEventMap |
@suaveplan/fsm (workspace:^) |
Playback state machine (stopped → playing → paused) |
@suaveplan/types (workspace:^) |
LoggerLike, Clock |
@suaveplan/error (workspace:^) |
Base class for TransportError; AUDIO_TRANSPORT.* codes |
zod (catalog:forms) |
Zod schemas for all public option types |
Dev deps: vitest, @vitest/coverage-istanbul, fast-check (all catalog:testing).
Public API
Core types
interface TransportOptions {
bpm?: number
ppq?: number
swing?: number
swingSubdivision?: Subdivision
timeSignature?: number
loop?: boolean
loopStart?: TransportTime
loopEnd?: TransportTime
logger?: LoggerLike
}
type TransportTime = string | number | Ticks
type Ticks = number
type BarsBeatsSixteenths = string
type BPM = number
type Subdivision = '4n' | '8n' | '16n' | '8t' | '16t'
type TransportCallback = (time: number) => void
interface TransportEventMap {
start: (time: number) => void
stop: (time: number) => void
pause: (time: number) => void
loop: (time: number) => void
bpm: (bpm: number, time: number) => void
}
class Transport {
constructor(options?: TransportOptions)
get state(): 'stopped' | 'playing' | 'paused'
get position(): BarsBeatsSixteenths
get seconds(): number
get ticks(): number
start(time?: number, offset?: Ticks): void
stop(time?: number): void
pause(time?: number): void
schedule(callback: TransportCallback, time: TransportTime): number
scheduleRepeat(callback: TransportCallback, interval: TransportTime, start?: TransportTime): number
scheduleOnce(callback: TransportCallback, time: TransportTime): number
cancel(eventId: number): void
cancelAfter(time: number): void
bpm: Signal<BPM>
timeSignature: number
ppq: number
loop: boolean
loopStart: TransportTime
loopEnd: TransportTime
swing: number
swingSubdivision: Subdivision
toSeconds(time: TransportTime): number
toTicks(time: TransportTime): Ticks
toBarsBeatsSixteenths(ticks: Ticks): BarsBeatsSixteenths
on<K extends keyof TransportEventMap>(event: K, listener: TransportEventMap[K]): void
off<K extends keyof TransportEventMap>(event: K, listener: TransportEventMap[K]): void
dispose(): void
}
class TransportScheduler {
constructor(transport: Transport, clock: Clock)
start(): void
stop(): void
}
class TickSource {
frequency: Signal<'bpm'>
getTicksAtTime(time: number): Ticks
getTimeOfTick(tick: Ticks, before?: number): number
setTicksAtTime(ticks: Ticks, time: number): void
start(time: number, offset?: Ticks): void
stop(time: number): void
pause(time: number): void
forEachTickBetween(start: number, end: number, callback: (time: number, ticks: Ticks) => void): void
}
Error codes
type TransportErrorCode =
| 'AUDIO_TRANSPORT_INVALID_TIME'
| 'AUDIO_TRANSPORT_INVALID_BPM'
| 'AUDIO_TRANSPORT_SCHEDULER_NOT_STARTED'
| 'AUDIO_TRANSPORT_EVENT_NOT_FOUND'
| 'AUDIO_TRANSPORT_ALREADY_DISPOSED'
const AUDIO_TRANSPORT_ERROR_METADATA: Readonly<Record<TransportErrorCode, CodeMetadata>>
function registerTransportErrorCodes(): void
Zod schemas
TransportOptionsSchema
TransportTimeSchema
TicksSchema
BarsBeatsSixteenthsSchema
File layout
packages/audio/audio-transport/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts
error-codes.ts
transport/
transport.ts
transport.md
transport.types.ts
transport.test.ts
index.ts
scheduler/
scheduler.ts
scheduler.md
scheduler.types.ts
scheduler.test.ts
index.ts
tick-source/
tick-source.ts
tick-source.md
tick-source.types.ts
tick-source.test.ts
index.ts
signal/
signal.ts
signal.md
signal.types.ts
signal.test.ts
index.ts
time/
time.ts
time.md
time.types.ts
time.test.ts
index.ts
__benchmarks__/
transport.bench.ts
tests/
integration.test.ts
Impact
Breaking changes: none — fresh build, package does not yet exist on genesis.
Prerequisites: @suaveplan/timing, @suaveplan/events, @suaveplan/fsm, @suaveplan/types, @suaveplan/error published to Verdaccio.
Affected dependents
@suaveplan/audio-transport is consumed by the timeline, both rendering engines, and the
controller packages. At the time of this change none of the dependents exist on genesis yet — they
land in later changes in this same batch — so there is no existing dependent surface to test or
migrate. Each dependent is created against this package's spec delta, not retrofitted.
@suaveplan/audio-timeline— [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created inenterprise-audio-timeline)@suaveplan/audio-engine-browser— [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created inenterprise-audio-engine-browser)@suaveplan/audio-engine-server— [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created inenterprise-audio-engine-server)@suaveplan/audio-controllers— [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created inenterprise-audio-controllers)
Once landed, any subsequent change to this package's public surface MUST run
bunx turbo test --filter=...@suaveplan/audio-transport per openspec/AGENTS.md §13 step 10.