first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
# Engine Clip Helpers
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `clip.ts` module contains the internal bridge between `TimelineEngine` public methods and the pure `clip-operations.ts` helpers. It wraps each operation with OTel span recording, histogram timing, and event emission so the engine class stays focused on orchestration.
## Features
- `engineAddClip` appends a clip and emits `clipChange`
- `engineRemoveClip` filters a clip and emits `clipChange`
- `engineMoveClip` applies constrained drags, skips no-ops, and records duration
- `engineTrimClip` trims a boundary and records a `timeline.clip_operation` span
- `engineSplitClip` splits a clip and records a `timeline.clip_operation` span
## Usage
These helpers are used internally by `TimelineEngine`. Consumers interact with the engine's public methods instead.
## API Reference
| Export | Signature | Description |
|--------|-----------|-------------|
| `engineAddClip` | `(deps, trackId, clip) => void` | Add clip |
| `engineRemoveClip` | `(deps, trackId, clipId) => void` | Remove clip |
| `engineMoveClip` | `(deps, trackId, clipId, delta) => number` | Move clip |
| `engineTrimClip` | `(deps, trackId, clipId, boundary, delta) => void` | Trim clip |
| `engineSplitClip` | `(deps, trackId, clipId, atSample) => void` | Split clip |
## Implementation Notes
The `ClipDeps` interface decouples the helpers from the engine class, making them testable in isolation. Histograms record `audio_timeline_clip_operation_duration_ms` for every trim and split. No-op moves (where the constrained delta is zero) skip both the undo push and the event emission, keeping the history clean. OTel spans include the operation name, track id, and clip id so that performance traces can pinpoint slow interactions.

View File

@@ -0,0 +1,52 @@
# TimelineEngine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`TimelineEngine` is the central state container for the `@suaveplan/audio-timeline` package. It owns the multi-track arrangement, playback transport, mixer state, zoom/selection view state, and a full undo/redo history. All rendering is delegated to a pluggable `TimelineAdapter`, keeping the engine headless and suitable for both web and native targets.
## Features
- Track CRUD: `setTracks`, `addTrack`, `removeTrack`, `updateTrack`, `selectTrack`
- Clip CRUD: `addClip`, `removeClip`, `moveClip`, `trimClip`, `splitClip`
- Transport: `play`, `pause`, `stop`, `seek` guarded by an internal FSM
- Mixer: `setMasterVolume`, `setTrackVolume`, `setTrackMute`, `setTrackSolo`, `setTrackPan`
- View: `zoomIn`, `zoomOut`, `setZoomLevel`, `setSelection`, `setLoopRegion`, `setLoopEnabled`
- Undo/redo with transaction support (`beginTransaction`, `commitTransaction`, `abortTransaction`)
- Serialization via `toJSON` and static `fromJSON`
- Typed events via `on` / `off`
- OTel spans and counters for clip operations and transport
## Usage
### Basic
```ts
import { TimelineEngine, NullTimelineAdapter } from "@suaveplan/audio-timeline";
const engine = new TimelineEngine({ sampleRate: 44100 });
engine.setAdapter(new NullTimelineAdapter());
engine.addTrack({ name: "Drums" });
engine.play();
```
### Advanced
```ts
engine.beginTransaction();
engine.addClip(trackId, clip);
engine.moveClip(trackId, clip.id, 500);
engine.commitTransaction();
engine.undo();
```
## API Reference
`TimelineEngine` exposes the full public surface documented in the spec. Key getters: `getState()` returns an immutable snapshot; `getCurrentTime()` reads from the adapter; `getTempo()` returns current BPM.
## Implementation Notes
The engine is deliberately headless. It does not touch the DOM or Web Audio directly; every output goes through the adapter. State mutations push `Command<Track[]>` instances onto an `UndoStack`. On undo/redo, if the track count is unchanged the engine calls `adapter.updateTrack` per changed track instead of `adapter.setTracks` to avoid playback interruption. Events are emitted through an `EventBus` so UI layers can subscribe to granular changes without polling.

View File

@@ -0,0 +1,35 @@
# Engine Mixer Helpers
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `mixer.ts` module bridges `TimelineEngine` mixer public methods to the pure `mixer.ts` helpers. It manages undo pushes and adapter forwarding for every mixer mutation, keeping the engine class from growing too large.
## Features
- `engineSetMasterVolume` updates master volume and pushes undo
- `engineSetTrackVolume` delegates to pure helper and forwards to adapter
- `engineSetTrackMute` toggles mute and emits via adapter
- `engineSetTrackSolo` toggles solo and emits via adapter
- `engineSetTrackPan` clamps pan and emits via adapter
## Usage
These helpers are used internally by `TimelineEngine`. Consumers call `engine.setTrackVolume(id, 0.8)` etc.
## API Reference
| Export | Signature | Description |
|--------|-----------|-------------|
| `engineSetMasterVolume` | `(deps, volume) => void` | Master volume |
| `engineSetTrackVolume` | `(deps, trackId, volume) => void` | Track volume |
| `engineSetTrackMute` | `(deps, trackId, muted) => void` | Track mute |
| `engineSetTrackSolo` | `(deps, trackId, soloed) => void` | Track solo |
| `engineSetTrackPan` | `(deps, trackId, pan) => void` | Track pan |
## Implementation Notes
The `MixerDeps` interface isolates the helpers from the engine class. Every track mutation goes through `updateTrackInPlace`, which pushes an undo command automatically. The master volume operation is special-cased because it does not live on a track; it pushes its own undo command directly. All other mixer state lives on individual tracks and is therefore covered by the standard track-update undo path. This symmetry makes it easy to reason about what changes are reversible.

View File

@@ -0,0 +1,28 @@
# Engine State Builder
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `state.ts` module builds the immutable `TimelineState` snapshot returned by `TimelineEngine.getState()`. It derives computed fields such as `duration`, `samplesPerPixel`, `canZoomIn`, and `canZoomOut` from the engine's internal fields so consumers always see a consistent, derived view.
## Features
- `buildTimelineState` assembles a complete `TimelineState` object from internal deps
- `computeDuration` scans all clips to find the timeline end
## Usage
Used internally by `TimelineEngine.getState()`.
## API Reference
| Export | Signature | Description |
|--------|-----------|-------------|
| `buildTimelineState` | `(deps: StateDeps) => TimelineState` | Build snapshot |
## Implementation Notes
Duration is calculated by iterating every clip in every track and taking the maximum `startSample + durationSamples`. This is `O(clips)` and is only run when `getState()` is called, not on every mutation. The `StateDeps` interface decouples the builder from the engine class, allowing tests to construct snapshots without instantiating a full engine. Because duration is derived rather than stored, it always reflects the current arrangement even after complex undo/redo sequences. Consumers can rely on `duration` being accurate without subscribing to additional events. The snapshot also includes `canZoomIn` and `canZoomOut` booleans so UI buttons can disable themselves at table bounds. All derived fields are recalculated on every `getState()` call, which keeps the internal engine state minimal and avoids stale derived data.

View File

@@ -0,0 +1,34 @@
# Playback FSM
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `transport-fsm.ts` module creates the finite state machine that guards timeline transport transitions. It prevents illegal transitions such as pausing while already stopped, ensuring that the engine and adapter never receive contradictory commands.
## Features
- Three states: `stopped`, `playing`, `paused`
- Valid transitions: `stopped --play--> playing`, `playing --pause--> paused`, `playing --stop--> stopped`, `paused --play--> playing`, `paused --stop--> stopped`, `stopped --seek--> stopped`, `playing --seek--> playing`, `paused --seek--> paused`
## Usage
```ts
import { createPlaybackFsm } from "./transport-fsm.js";
const fsm = createPlaybackFsm();
fsm.send("play");
console.log(fsm.current); // "playing"
```
## API Reference
| Export | Signature | Description |
|--------|-----------|-------------|
| `createPlaybackFsm` | `() => FSMInterpreter` | Create playback FSM |
## Implementation Notes
The FSM is created with `@suaveplan/fsm`. It uses an empty context object because the timeline engine stores playback state separately. Invalid transitions throw `FSMError`, which the engine catches and ignores. This design keeps transport state management declarative and easy to extend if new states (such as `recording`) are added later. The FSM interpreter is created fresh for each engine instance, so multiple engines can coexist with independent playback states. This is important for applications that may host multiple timeline views or sessions simultaneously. Each engine instance manages its own FSM lifecycle independently. The FSM definition is hard-coded in this module because the playback state machine is universal and does not need customisation for different adapters or rendering backends.

View File

@@ -0,0 +1,33 @@
# Engine Transport Helpers
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `transport.ts` module bridges `TimelineEngine` transport public methods (`play`, `pause`, `stop`, `seek`) to the `TimelineAdapter`. Each method is guarded by the playback FSM and records OTel spans, ensuring that illegal transitions are silently ignored and every transport change is observable.
## Features
- `enginePlay` sends `play` to FSM, calls `adapter.play`, and emits event
- `enginePause` sends `pause` to FSM, calls `adapter.pause`, and emits event
- `engineStop` sends `stop` to FSM, calls `adapter.stop`, resets time, and emits event
- `engineSeek` calls `adapter.seek` directly without FSM involvement
## Usage
Used internally by `TimelineEngine`. Consumers call `engine.play()` etc.
## API Reference
| Export | Signature | Description |
|--------|-----------|-------------|
| `enginePlay` | `(deps) => void` | Start playback |
| `enginePause` | `(deps) => void` | Pause playback |
| `engineStop` | `(deps) => void` | Stop and reset |
| `engineSeek` | `(deps, time) => void` | Seek to time |
## Implementation Notes
Every transport mutation creates a `timeline.transport` OTel span. The FSM prevents illegal transitions (e.g. `pause` while stopped) by throwing `FSMError`, which the engine silently catches and ignores. Seek does not consult the FSM because it is valid in any state. This allows users to scrub the playhead while playback is running, paused, or stopped without triggering transition errors. The adapter receives the seek command immediately so audio output can stay in sync with the UI.

View File

@@ -0,0 +1,31 @@
# Engine Undo Helpers
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `undo.ts` module provides the command builder and incremental adapter diff logic used by `TimelineEngine` for undo/redo. It converts before/after track snapshots into `Command<Track[]>` instances and detects which individual tracks changed so the engine can forward minimal updates to the adapter.
## Features
- `createTracksUndoStack` constructs an `UndoStack<Track[]>` with a 500-entry limit
- `buildTracksCommand` creates a `Command<Track[]>` from before/after snapshots
- `findChangedTrackIds` compares two track arrays to detect which tracks changed
## Usage
Used internally by `TimelineEngine`.
## API Reference
| Export | Signature | Description |
|--------|-----------|-------------|
| `createTracksUndoStack` | `(initial) => UndoStack<Track[]>` | Create undo stack |
| `buildTracksCommand` | `(label, prev, next) => TracksCommand` | Build command |
| `findChangedTrackIds` | `(prev, next) => string[]` | Diff tracks |
## Implementation Notes
`findChangedTrackIds` uses `JSON.stringify` for a fast deep-equal check. This is sufficient because tracks are plain JSON-compatible objects with no circular references. The function returns ids present in `prev` but not `next` (removed tracks) as well as ids whose contents differ. When the track count is unchanged, the engine uses these ids to call `adapter.updateTrack` individually, avoiding a full `setTracks` reset that could interrupt playback. The 500-entry limit prevents unbounded memory growth in long editing sessions. When the limit is reached, the oldest entries are discarded automatically so the stack stays bounded.

View File

@@ -0,0 +1,36 @@
# Engine Zoom Helpers
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `zoom.ts` module bridges `TimelineEngine` zoom, selection, and loop public methods to the pure `timeline-operations.ts` and `zoom.ts` helpers. It manages internal state updates and event emission so the engine class stays focused on high-level orchestration.
## Features
- `engineZoomIn` / `engineZoomOut` step through the zoom table and emit `zoomChange`
- `engineSetZoomLevel` snaps to the nearest table entry
- `engineSetSelection` normalises ranges and emits `selectionChange`
- `engineSetLoopRegion` updates loop bounds and forwards to adapter
- `engineSetLoopEnabled` toggles loop and forwards to adapter
## Usage
These helpers are used internally by `TimelineEngine`. Consumers call `engine.zoomIn()` etc.
## API Reference
| Export | Signature | Description |
|--------|-----------|-------------|
| `engineZoomIn` | `(deps) => void` | Zoom in |
| `engineZoomOut` | `(deps) => void` | Zoom out |
| `engineSetZoomLevel` | `(deps, samplesPerPixel) => void` | Set zoom |
| `engineSetSelection` | `(deps, start, end) => void` | Set selection |
| `engineSetLoopRegion` | `(deps, start, end) => void` | Set loop region |
| `engineSetLoopEnabled` | `(deps, enabled) => void` | Toggle loop |
## Implementation Notes
The `ZoomDeps` interface isolates the helpers from the engine class, making them testable in isolation. Loop changes are forwarded to the adapter immediately so the audio backend can update its loop markers. Selection changes are clamped to the current document duration so invalid ranges cannot be created. Zoom changes only affect the view and do not touch the arrangement, which is why they bypass the undo stack entirely.