Files
feuerwerk/src/runtime/systems/Audio.ts
T
adminandClaude Opus 5.5 8f9c250eb6
game-maker / gate (push) Successful in 3m29s
game-maker / suite (push) Successful in 16m29s
Lay patterns and sounds out on tracks and play them as a song #334
Patterns could only be heard one at a time, in their own tab or from
a behavior. A song lays patterns and sounds out on tracks along one
time axis and plays them through the scheduler of #332, the third
piece of the music tools of #330.

- New asset type song under the heading Songs, with four empty tracks,
  120 BPM in 4/4 and a grid of a bar. Tempo, meter and grid are rows
  in the inspector. Tracks, items and the loop are the asset's data,
  held to their rules on load and in every command.
- A track has a name, volume, pan, mute and solo. Each is heard on a
  bus of its own, so a change reaches what is sounding at once.
- A pattern item repeats its pattern for as long as it is, with a
  transpose and another instrument if wanted. A sound item plays its
  sound as a clip at its own speed, round and round, through a sound
  FX if wanted. Both have a volume on top of their track's.
- Cut back at the left end or split, an item starts as far into what
  it plays as the cut is: ticks into a pattern, seconds into a sound.
- The tab shows the heads with M, S, volume and pan, the ruler of the
  Pattern tab and the items, a pattern with its notes, a sound with
  its waveform, a missing asset red. Patterns and sounds are dragged
  in from the Assets panel. Body, right and left end move, lengthen
  and cut back, Alt leaves the grid. Ctrl+C/V/D/A, Ctrl+E splits
  where playing starts, Delete, arrows. A double-click opens a
  pattern item in its tab.
- What is picked is a new selection of its own, so the inspector
  shows the item or the track, with a button to delete it.
- The scheduler plans clips beside notes. Where the playhead starts
  or jumps back inside a clip, it is taken up that far into its
  sound. A clip fades out at the end of its item or of the loop.
- The editor plays one piece at a time: a pattern started stops the
  song and the other way round.
- The rules are pure functions in core/music/song, editor/song/
  songView and songEdit.

Findings:
- The suites of 4ea752f and 2a3b90b were red at e2e 54: the Pattern
  pushed Struct below the fold of the + menu. The module now scrolls
  to it, as e2e 47 does for Vector art. It also left the menu open,
  which made e2e 55 fail behind it.
- The Assets panel only dragged assets a scene can place. Patterns
  and sounds are dragged now as well, and a scene says why it does
  not take one instead of calling every asset a scene.

Tests:
- song.test.ts (new, 14) and song-edit.test.ts (new, 9): model,
  round trip, sequence of patterns and clips, mute and solo, cuts,
  clips on the scheduler, moving, sizing, trimming, splitting,
  duplicating, hits, head parts. sound-library.test.ts +5: clip
  offset, end fade, pitched clip, track bus, peaks.
- e2e 65 (new) on melektaus-atx: ALL PASSED, 18 checks. e2e 54 and
  55 with the fix: ALL PASSED, 33 checks. e2e 4 with 30 asset
  types: ALL PASSED, 62 checks.
- Gate on melektaus-atx: CHECK EXIT 0, 250 test files, 3884 tests.
- Counter-checks: no taking up of a clip, no pattern offset, no clip
  end each fail 3, 1 and 1 tests.
- Probe on stein2: the tab with four tracks, before and while
  playing, looked at in two screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:28:25 +02:00

106 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { EaseShare } from '../../core/model/animation';
import type { SequencePlay } from '../../core/music/playbacks';
import type { FxParams, FxPlan, FxVoice } from '../../core/soundfx/nodes';
/**
* What the runtime needs of the outside world to make a noise – the counterpart of `Input` on the
* way out. The browser side of it is `viewport/SoundLibrary`; a test (and a run started before
* anything wired one in) gets `SILENT_AUDIO` and plays to nobody.
*
* Files, not asset ids: resolving the id is the `BehaviorSystem`'s job, because it is the one
* holding the project. Everything below this line only knows bytes and voices.
*/
export interface AudioSink {
/**
* Starts one voice. `volume` is final (0–1) and every call overlaps whatever is already playing.
* `owner` is the behavior the sound belongs to: when that behavior is destroyed the voice goes
* with it, because a sound is something a thing in the world is doing, not a thing of its own.
* `''` means the voice has no owner and outlives whatever started it. `fx` is the sound FX the
* voice passes on its way out (#311), with the values of its parameters; `DRY_VOICE` passes none.
* Returns the number of the voice, never used twice, or 0 where nothing is going to play.
*/
play(file: string, volume: number, loop: boolean, owner: string, fx?: FxVoice): number;
/**
* Starts one note of an instrument (#331), as `play` starts a sound: `layers` are what the note is
* made of, played side by side as one voice, each way reading the file of its own source. `name`
* stands for the instrument. `params` fill in the parameters of the plans, its velocity among them.
* A `duration` above 0 releases it that many seconds after it starts; 0 holds it.
*/
playNote(
name: string,
layers: readonly FxPlan[],
params: FxParams,
volume: number,
owner: string,
duration: number,
timing?: NoteTiming,
): number;
/**
* Plays the notes of a pattern on the audio clock, each struck through `play.strike`. `name` stands
* for the pattern as a file does for a sound, and the number it gives back is one no voice has:
* `stopVoice` and `playing` take it like a voice's, `stopOwner` and stopping everything reach it.
* Returns 0 where nothing is going to play.
*/
playSequence(name: string, play: SequencePlay, owner: string): number;
/** Stops every pattern of that name, `''` every pattern; with `letRing` its notes fall away in their release. */
stopSequence(name: string, letRing?: boolean): void;
/**
* The end of a note (#331): its ways that end with the note fade out, those that play through
* go on. Nothing happens for a voice that is no note or not there.
*/
release(voice: number): void;
/**
* Hands new values to the sound FX parameters of one voice, by the number `play` gave it (#315).
* A name its FX does not have is left out, one not given keeps its value. A voice that has ended
* is not there any more and nothing happens – a detached one is reached like any other. `time` is
* how many seconds the values take to get there, counted on the audio clock from now – or from
* the moment the voice really starts, where it still waits on its bytes, along `ease` (linear
* where it is not given). 0 is a quick glide.
*/
tune(voice: number, params: FxParams, time?: number, ease?: EaseShare): void;
/**
* How many seconds one run of that file lasts, the same with a pitch it starts on (#312); 0 while
* its bytes have not arrived.
*/
length(file: string): number;
/**
* How many seconds one voice rings on after its sound has ended (#324), at the values it has now:
* the echoes of its FX; 0 where it has none or is not there.
*/
tail(voice: number): number;
/**
* Silences every voice of that file; `''` silences all of them, patterns too. A voice is faded out in a few
* milliseconds, its echoes with it – unless `letRing`, which stops the sound and lets them fade.
*/
stop(file: string, letRing?: boolean): void;
/** Silences one voice, by the number `play` gave it, the same way. */
stopVoice(voice: number, letRing?: boolean): void;
/** Silences every voice started by that owner – what destroying a behavior does. */
stopOwner(owner: string): void;
}
/**
* When a note is heard (#332): `at` is the second of the audio clock it starts at, now where it is
* left out or already past; `tempo` is what a Delay synced to beats counts by, 120 where it is left out.
*/
export interface NoteTiming {
at?: number;
tempo?: number;
/** The track of a song it is heard on (#334); straight out where left out. */
bus?: string;
}
export const SILENT_AUDIO: AudioSink = {
play: () => 0,
playNote: () => 0,
playSequence: () => 0,
stopSequence: () => {},
release: () => {},
tune: () => {},
length: () => 0,
tail: () => 0,
stop: () => {},
stopVoice: () => {},
stopOwner: () => {},
};