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>
142 lines
6.9 KiB
TypeScript
142 lines
6.9 KiB
TypeScript
import type { Application } from 'pixi.js';
|
||
import { physicsStatus } from '../runtime/physics/engine';
|
||
import type { SchedulerOptions } from '../core/music/Scheduler';
|
||
import type { Sequence } from '../core/music/sequence';
|
||
import type { FxEnvelope, FxStage } from '../core/soundfx/nodes';
|
||
import { Music } from '../viewport/music';
|
||
import {
|
||
bothAtCenter,
|
||
envelopeDriver,
|
||
followCurve,
|
||
releaseEnvelope,
|
||
soundLibrary,
|
||
stageNodes,
|
||
} from '../viewport/SoundLibrary';
|
||
import { curveHooks } from './debug/curveHooks';
|
||
import { patternHooks } from './debug/patternHooks';
|
||
import { songHooks } from './debug/songHooks';
|
||
import { documentHooks } from './debug/documentHooks';
|
||
import { graphHooks } from './debug/graphHooks';
|
||
import { centreOf, type HookContext } from './debug/hookHelpers';
|
||
import { pixelArtHooks } from './debug/pixelArtHooks';
|
||
import { renderHooks } from './debug/renderHooks';
|
||
import { sceneHooks } from './debug/sceneHooks';
|
||
import { uiHooks } from './debug/uiHooks';
|
||
import { vectorArtHooks } from './debug/vectorArtHooks';
|
||
import type { DocumentId, Editor, TabId } from './Editor';
|
||
import { NodeBoard } from './plugins/NodeBoard';
|
||
import type { EditorState } from './state';
|
||
import { rectOf } from './textProbe';
|
||
|
||
/**
|
||
* Installs `window.__gm`, which the check scripts in tools/ use to find widgets by their
|
||
* visible text and to read editor state. The hooks of each area live in `debug/`.
|
||
*/
|
||
export function installDebugHook(app: Application, state: EditorState, editor: Editor): void {
|
||
const context: HookContext = { app, state, editor };
|
||
const music = new Music(() => state.project);
|
||
(window as unknown as { __gm: unknown }).__gm = {
|
||
state,
|
||
/** The Pixi application, for probes that need the renderer or the stage. */
|
||
app,
|
||
...uiHooks(context),
|
||
...sceneHooks(context),
|
||
...graphHooks(context),
|
||
...documentHooks(context),
|
||
...pixelArtHooks(context),
|
||
...vectorArtHooks(context),
|
||
...curveHooks(context),
|
||
...patternHooks(context),
|
||
...songHooks(context),
|
||
...renderHooks(context),
|
||
/**
|
||
* Voices started, stopped and in flight. CI runs the browser with `--mute-audio`, so nothing
|
||
* is ever heard there: these counters are the only way a check can see that a graph played
|
||
* anything at all, the way `renderStats` is for a repaint that did not happen.
|
||
*/
|
||
sound: {
|
||
stats: () => soundLibrary.stats(),
|
||
duration: (file: string) => soundLibrary.duration(file),
|
||
/** Where the knobs of each voice of that file are now, oldest first, a pitch as its speed (#315). */
|
||
levels: (file: string) => soundLibrary.levels(file),
|
||
pitchWorkletUrl: () => soundLibrary.pitchWorkletUrl(),
|
||
/** Seconds on the audio clock: the time a voice really runs on. */
|
||
clock: () => soundLibrary.clock(),
|
||
/** Decodes ahead of a check, so a play can be asserted without racing the network. */
|
||
load: (file: string) => soundLibrary.load(file).then((buffer) => buffer?.duration ?? null),
|
||
stopAll: () => soundLibrary.stopAll(),
|
||
/** What Stop sound asks for: every voice of that file, its echoes cut or left to ring (#324). */
|
||
stop: (file: string, letRing: boolean) => soundLibrary.stop(file, letRing),
|
||
/** The patterns a game plays, with the notes last planned of each. */
|
||
patterns: () => soundLibrary.patterns(),
|
||
/** The nodes a stage of a voice becomes, in any context – an offline one renders what it does. */
|
||
stageNodes: (context: BaseAudioContext, stage: FxStage) => stageNodes(context, stage),
|
||
/** What drives the two shares of a Mix "Both at center", in any context (unstarted). */
|
||
bothAtCenter: (context: BaseAudioContext, mix: number, a: GainNode, b: GainNode) =>
|
||
bothAtCenter(context, mix, a, b),
|
||
/** The curve of an Envelope from `at` (unstarted), its release, and how it moves a number (#335). */
|
||
envelopeDriver: (context: BaseAudioContext, envelope: FxEnvelope, at: number) =>
|
||
envelopeDriver(context, envelope, at),
|
||
releaseEnvelope: (level: AudioParam, envelope: FxEnvelope, startedAt: number, at: number) =>
|
||
releaseEnvelope(level, envelope, startedAt, at),
|
||
stageWith: (context: BaseAudioContext, stage: FxStage, driver: ConstantSourceNode) =>
|
||
stageNodes(context, stage, (mod, param, ratio, map) =>
|
||
followCurve(context, driver, mod, param, ratio, map),
|
||
),
|
||
},
|
||
/**
|
||
* The scheduler (#332), which nothing in the editor plays yet: a check hands it a sequence of
|
||
* notes and reads back what was planned for which second of the audio clock.
|
||
*/
|
||
music: {
|
||
play: (sequence: Sequence, options?: SchedulerOptions & { from?: number }) =>
|
||
music.play(sequence, options),
|
||
edit: (sequence: Sequence) => music.edit(sequence),
|
||
stop: () => music.stop(),
|
||
position: () => music.current?.position() ?? null,
|
||
playing: () => music.current?.playing ?? false,
|
||
planned: () => [...music.planned],
|
||
latency: () => soundLibrary.latency(),
|
||
},
|
||
world: () => editor.worldSnapshot(),
|
||
playInfo: () => editor.playInfo(),
|
||
/** Which scene Play would start, and under which of the two rules (#183: set in the settings). */
|
||
playFrom: () => editor.playFromInfo(),
|
||
/** The two run buttons of the top bar: the picture each wears, and whether it takes a click. */
|
||
runButtons: () => editor.runButtons(),
|
||
/** Where to click one of the icon buttons: menu, undo, redo, play, pause. */
|
||
toolbarAt: (name: keyof Editor['toolbarControls']) => centreOf(editor.toolbarControls[name]),
|
||
/** Behaviors running without Play (`Run in editor`), and how long they have been at it. */
|
||
editorRun: () => editor.editorRunInfo(),
|
||
physics: () => physicsStatus(),
|
||
tab: () => editor.tab,
|
||
showTab: (tab: TabId) => editor.showTab(tab),
|
||
/** The open documents in tab order and the one showing (#171); a document is `asset:<id>` or `list`. */
|
||
documents: () => editor.openDocuments(),
|
||
openDocument: (doc: DocumentId) => editor.openDocument(doc),
|
||
closeDocument: (doc: DocumentId) => editor.closeDocument(doc),
|
||
plugins: {
|
||
/** Every file in `data/plugins/` and what became of it. */
|
||
records: () => [...(editor.plugins?.records ?? [])],
|
||
/** The plugins in the user management tab (#179): one row per file, with its switch. */
|
||
pane: () => ({ ...editor.pluginsPane.debugInfo(), visible: editor.tab === 'management' }),
|
||
/** Where to click the switch of one plugin; null when it has no row. */
|
||
switchAt: (file: string) => {
|
||
const box = editor.pluginsPane.switchOf(file);
|
||
return box ? centreOf(box) : null;
|
||
},
|
||
/** A plugin tab's node board as drawn, with its canvas and Fit button; null without one. */
|
||
board: (tabId: string) => {
|
||
const pane = editor.pluginPane(tabId);
|
||
if (!(pane instanceof NodeBoard)) return null;
|
||
return {
|
||
...pane.debugInfo(),
|
||
visible: pane.visible,
|
||
rect: rectOf(pane),
|
||
fit: centreOf(pane.fitButton),
|
||
};
|
||
},
|
||
},
|
||
};
|
||
}
|