Files
suaveplan/openspec/archive/2026-05-15-enterprise-audio-transport/proposal.md
2026-05-27 23:29:53 +01:00

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 in enterprise-audio-timeline)
  • @suaveplan/audio-engine-browser — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created in enterprise-audio-engine-browser)
  • @suaveplan/audio-engine-server — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created in enterprise-audio-engine-server)
  • @suaveplan/audio-controllers — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created in enterprise-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.