Initial Commit
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
# 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
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
TransportOptionsSchema
|
||||
TransportTimeSchema
|
||||
TicksSchema
|
||||
BarsBeatsSixteenthsSchema
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
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.
|
||||
Reference in New Issue
Block a user