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 of4ea752fand2a3b90bwere 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>
106 lines
4.9 KiB
TypeScript
106 lines
4.9 KiB
TypeScript
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: () => {},
|
||
};
|