A song that had been played a few times in a long session stuttered at regular intervals until the page was reloaded. A quick run up the keys of a sampled instrument made every sound drop out for a second or two. Both came from work the audio thread should not have had. - Every worklet node of a voice listened on its port. Chrome never collects a port that is listened to, and through its listener not the voice either. Every node of the voice stayed on its bus and was worked on in every block. A way or stage on the worklet now closes its port once it ends or is stopped, and the processor closes its own side when it is told to stop. - A voice without echoes now leaves its bus as soon as its sound and what its stages hold back are through. Before, only a voice with echoes did. - At most 12 ways play through the pitch worklet at once. One more cuts the oldest in 10 ms, a note already let go before one still held. Each runs a phase vocoder, whose first block alone took 2.7 ms of the 2.67 ms a block has on stein2. - Play for the game ends everything the tabs play: the piece holding the transport and every key or preview (`stopPreviews`). - Closing a Sound, Sound FX, Instrument, Pattern or Song tab stops what it plays of that asset (`AssetTabEditor.closed`). - A preset in the Instrument tab plays nothing any more. With Audition on it is heard like any other change. - Test hooks: `__gm.instrument.play`, `__gm.sound.playing`, and `vocoders` in the stats of the sound library. Measured: - A copy of the project, its Song 2 played for 12 s and stopped, six times (Chrome on stein2, CDP WebAudio): before, 746 nodes were left after the first time and 4469 after the sixth, the load at rest grew from 0.01 to 0.06. After, 7 nodes are left every time (the master and one bus) and the load at rest stays at 0.01 to 0.02. - Pitch-player voices live in Chrome on melektaus-atx: 12 take 0.50 of the audio thread, 16 take 0.71, 24 take 0.99. 30 struck 40 ms apart held it at 0.99 from the first second on. Tests: - Unit sound-library: 110 green (3 new), 4 counter-probes each red - e2e 34 (melektaus-atx): ALL PASSED, 86 checks (3 new, 2 changed) - e2e counter-probe with game start and preset undone: their 3 checks red, and the old check of Play note in the game as well, which now counts on the preview being gone - e2e counter-probe with tab close undone: exactly its check red. A first form of it held a key and stayed green, since the keys let go of a held key by themselves once the tab shows no asset - Gate (melektaus-atx): 270 files, 4181 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
5.0 KiB
TypeScript
151 lines
5.0 KiB
TypeScript
import type { Asset } from '../core/model/Asset';
|
||
import { findAsset } from '../core/model/Project';
|
||
import { Button, Label, Spacer, VStack, type Widget } from '../ui';
|
||
import type { EditorState } from './state';
|
||
import { TabHeader } from './TabHeader';
|
||
|
||
/**
|
||
* The canvas each tab wraps: the header's Fit button and the tab's own plumbing need this much.
|
||
* A canvas without `fit` belongs to a tab whose Fit frames something else (see `AssetTabEditor.fit`).
|
||
* `saveView`/`restoreView` carry zoom and scroll across a switch to another document and back.
|
||
*/
|
||
export interface TabCanvas {
|
||
readonly currentAssetId: string;
|
||
show(assetId: string): void;
|
||
fit?(): void;
|
||
saveView?(): unknown;
|
||
restoreView?(view: unknown): void;
|
||
}
|
||
|
||
export interface AssetTabOptions {
|
||
/** Width reserved for the right-hand readout; a stack measures the label once, while empty. */
|
||
infoWidth: number;
|
||
}
|
||
|
||
/**
|
||
* The Behavior, State machine, Texture, Container … tabs. One editor per kind serves every open
|
||
* document of that kind (#171): the tab row tells it which one to show, and it keeps each one's
|
||
* view for when it comes back. A subclass must call `mount()`: header and canvas depend on each
|
||
* other, so the base cannot build them.
|
||
*/
|
||
export abstract class AssetTabEditor<
|
||
A extends Asset,
|
||
C extends TabCanvas & Widget = TabCanvas & Widget,
|
||
> extends VStack {
|
||
readonly fitButton: Button;
|
||
protected readonly info: Label;
|
||
/** Assigned by `mount`. Public because the check hooks and the play loop reach for it. */
|
||
canvas!: C;
|
||
private readonly views = new Map<string, unknown>();
|
||
|
||
constructor(
|
||
protected readonly state: EditorState,
|
||
options: AssetTabOptions,
|
||
) {
|
||
super();
|
||
this.fitButton = new Button('Fit', { onClick: () => this.fit() });
|
||
this.info = new Label('', { style: 'small', align: 'right' });
|
||
this.info.minWidth = options.infoWidth;
|
||
}
|
||
|
||
/** Builds the header (with `strip` below it) around `canvas` and wires the state events. */
|
||
protected mount(canvas: C, extras: readonly Widget[] = [], strip?: Widget): void {
|
||
this.canvas = canvas;
|
||
const header = new TabHeader();
|
||
for (const widget of extras) header.add(widget);
|
||
header.add(this.fitButton);
|
||
header.add(new Spacer(), 1);
|
||
header.add(this.info);
|
||
|
||
this.add(header);
|
||
if (strip) this.add(strip);
|
||
this.add(this.body(canvas), 1);
|
||
|
||
this.state.on('project', () => this.refresh());
|
||
this.state.on('load', () => {
|
||
this.views.clear();
|
||
this.canvas.show('');
|
||
this.refresh();
|
||
});
|
||
this.refresh();
|
||
}
|
||
|
||
/** The asset currently shown, `''` for none – the check scripts read this. */
|
||
get currentAssetId(): string {
|
||
return this.canvas.currentAssetId;
|
||
}
|
||
|
||
/** The right-hand readout as shown – the check scripts read this. */
|
||
get shownInfo(): string {
|
||
return this.info.text;
|
||
}
|
||
|
||
/** Shows a document, with the view it had when it was last on screen. */
|
||
showAsset(assetId: string): void {
|
||
const from = this.canvas.currentAssetId;
|
||
if (from !== assetId) {
|
||
if (from) this.views.set(from, this.saveView());
|
||
this.canvas.show(assetId);
|
||
const view = this.views.get(assetId);
|
||
if (view !== undefined) this.restoreView(view);
|
||
}
|
||
this.refresh();
|
||
}
|
||
|
||
/** A closed tab: opened again, the document starts from a fitted view. What it played stops. */
|
||
forgetView(assetId: string): void {
|
||
this.views.delete(assetId);
|
||
this.closed(assetId);
|
||
}
|
||
|
||
/** Stops what the tab plays of that asset; a tab that plays nothing has nothing to stop. */
|
||
protected closed(_assetId: string): void {}
|
||
|
||
/** The selection's id for this kind of asset, `''` when the selection is about something else. */
|
||
abstract selectedId(): string;
|
||
|
||
abstract isMine(asset: Asset): asset is A;
|
||
|
||
/** The right-hand readout; `null` is the empty state. */
|
||
protected abstract infoText(asset: A | null): string;
|
||
|
||
/** What fills the tab below the header. Override to put something beside the canvas. */
|
||
protected body(canvas: C): Widget {
|
||
return canvas;
|
||
}
|
||
|
||
/** What the header's Fit button frames. Override when the tab has more than one view. */
|
||
protected fit(): void {
|
||
this.canvas.fit?.();
|
||
}
|
||
|
||
/** The view to come back to; override when the tab has more than one. */
|
||
protected saveView(): unknown {
|
||
return this.canvas.saveView?.();
|
||
}
|
||
|
||
protected restoreView(view: unknown): void {
|
||
this.canvas.restoreView?.(view);
|
||
}
|
||
|
||
/** The canvas's area on screen, without the header – so a check can ask what lies outside it. */
|
||
canvasRect(): { x: number; y: number; w: number; h: number } {
|
||
const p = this.canvas.getGlobalPosition();
|
||
return { x: p.x, y: p.y, w: this.canvas.w, h: this.canvas.h };
|
||
}
|
||
|
||
/** Enable/disable whatever `extras` the subclass passed to `mount`. */
|
||
protected syncExtras(_asset: A | null): void {}
|
||
|
||
protected asset(): A | null {
|
||
const asset = findAsset(this.state.project, this.canvas.currentAssetId);
|
||
return asset && this.isMine(asset) ? asset : null;
|
||
}
|
||
|
||
protected refresh(): void {
|
||
const asset = this.asset();
|
||
this.info.setText(asset ? this.infoText(asset) : '');
|
||
this.syncExtras(asset);
|
||
}
|
||
}
|