Play every tab's preview straight into the master (#368)
Since #351 the tabs played their asset along its links in the Master routing, as the game does. A song linked into a reverb bus was heard through that bus while it was edited. A preview should sound like the asset itself, so the tabs leave the Master routing out again. - The Sound, Sound FX, Instrument, Pattern and Song tab play into the master. The sound FX of the asset still applies, and so does the master's own volume and FX. - Inside a song its own routing of tracks and sends still counts. - The game is unchanged: Play nodes go along the routing as before. Tests: - e2e 34 (melektaus-atx): ALL PASSED, 96 checks. The routing check now holds the mix of the two buses as before, and the Sound tab on the master with the bus silent. - Counter-probe e2e: the Sound tab along the routing again, the routing check red. - Gate (melektaus-atx): CHECK EXIT 0, 274 files, 4230 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f13794b0c
commit
af2312a5e7
7 files changed
+30
-25
No files matched your search
@@ -21,8 +21,9 @@ import {
|
|||||||
import { MIDDLE_C, noteName } from '../../core/instrument/notes';
|
import { MIDDLE_C, noteName } from '../../core/instrument/notes';
|
||||||
import type { Asset, InstrumentAsset } from '../../core/model/Asset';
|
import type { Asset, InstrumentAsset } from '../../core/model/Asset';
|
||||||
import { fxVoiceOf } from '../../core/music/needs';
|
import { fxVoiceOf } from '../../core/music/needs';
|
||||||
|
import { MASTER_MIX } from '../../core/music/mixer';
|
||||||
import { DEFAULT_TEMPO } from '../../core/music/time';
|
import { DEFAULT_TEMPO } from '../../core/music/time';
|
||||||
import { instrumentBus, instrumentFx, playNote } from '../../runtime/systems/behaviorContext';
|
import { instrumentFx, playNote } from '../../runtime/systems/behaviorContext';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
@@ -168,7 +169,8 @@ export class InstrumentEditor extends AssetTabEditor<InstrumentAsset, GraphCanva
|
|||||||
struck,
|
struck,
|
||||||
PREVIEW_OWNER,
|
PREVIEW_OWNER,
|
||||||
{ ...this.values },
|
{ ...this.values },
|
||||||
{ bus: instrumentBus(this.state.project, asset.id, '') },
|
// Straight into the master: a preview leaves the routing of the instrument out (#368).
|
||||||
|
{ bus: MASTER_MIX },
|
||||||
fxVoiceOf(this.state.project, instrumentFx(this.state.project, asset.id, ''), DEFAULT_TEMPO),
|
fxVoiceOf(this.state.project, instrumentFx(this.state.project, asset.id, ''), DEFAULT_TEMPO),
|
||||||
);
|
);
|
||||||
if (voice) this.voices.push(voice);
|
if (voice) this.voices.push(voice);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { DEFAULT_TEMPO } from '../../core/music/time';
|
|||||||
import { instrumentFx, playNote } from '../../runtime/systems/behaviorContext';
|
import { instrumentFx, playNote } from '../../runtime/systems/behaviorContext';
|
||||||
import { Button, Dropdown, Label, Menu, type Widget } from '../../ui';
|
import { Button, Dropdown, Label, Menu, type Widget } from '../../ui';
|
||||||
import { Music } from '../../viewport/music';
|
import { Music } from '../../viewport/music';
|
||||||
import { playedRoute } from '../../core/music/mixer';
|
import { MASTER_MIX } from '../../core/music/mixer';
|
||||||
import { soundLibrary } from '../../viewport/SoundLibrary';
|
import { soundLibrary } from '../../viewport/SoundLibrary';
|
||||||
import { AssetTabEditor } from '../AssetTabEditor';
|
import { AssetTabEditor } from '../AssetTabEditor';
|
||||||
import { claimTransport, releaseTransport } from '../music/transport';
|
import { claimTransport, releaseTransport } from '../music/transport';
|
||||||
@@ -54,10 +54,10 @@ export class PatternEditor extends AssetTabEditor<PatternAsset, PianoRoll> {
|
|||||||
readonly ghostsButton: Button;
|
readonly ghostsButton: Button;
|
||||||
/** Where the ghost notes come from: song, track and bar. */
|
/** Where the ghost notes come from: song, track and bar. */
|
||||||
readonly ghostSource = new Label('', { style: 'muted' });
|
readonly ghostSource = new Label('', { style: 'muted' });
|
||||||
/** Heard through the sound FX (#352) and along the routing of the asset (#351), as in the game. */
|
/** Heard through the sound FX (#352), then straight into the master: a preview leaves the routing out (#368). */
|
||||||
readonly music = new Music(
|
readonly music = new Music(
|
||||||
() => this.state.project,
|
() => this.state.project,
|
||||||
() => playedRoute(this.state.project, '', this.asset()?.id ?? ''),
|
() => MASTER_MIX,
|
||||||
() => {
|
() => {
|
||||||
const asset = this.asset();
|
const asset = this.asset();
|
||||||
return asset && fxVoiceOf(this.state.project, assetFxOf(asset.props), patternTempo(asset.props));
|
return asset && fxVoiceOf(this.state.project, assetFxOf(asset.props), patternTempo(asset.props));
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { assetFxOf } from '../../core/properties/assetFx';
|
|||||||
import type { SoundStore } from '../../core/store/SoundStore';
|
import type { SoundStore } from '../../core/store/SoundStore';
|
||||||
import { Button, Dialog, Dropdown, Label, Menu, ui, VStack, type Widget } from '../../ui';
|
import { Button, Dialog, Dropdown, Label, Menu, ui, VStack, type Widget } from '../../ui';
|
||||||
import { Music } from '../../viewport/music';
|
import { Music } from '../../viewport/music';
|
||||||
import { playedRoute } from '../../core/music/mixer';
|
import { MASTER_MIX } from '../../core/music/mixer';
|
||||||
import { soundLibrary } from '../../viewport/SoundLibrary';
|
import { soundLibrary } from '../../viewport/SoundLibrary';
|
||||||
import {
|
import {
|
||||||
type InputDevice,
|
type InputDevice,
|
||||||
@@ -93,10 +93,10 @@ export class SongEditor extends AssetTabEditor<SongAsset, SongTimeline> {
|
|||||||
readonly snap = new SnapToggle(() => this.routing);
|
readonly snap = new SnapToggle(() => this.routing);
|
||||||
private shown: SongView = 'timeline';
|
private shown: SongView = 'timeline';
|
||||||
private readonly pane = new VStack();
|
private readonly pane = new VStack();
|
||||||
/** Heard through the sound FX (#352) and along the routing of the asset (#351), as in the game. */
|
/** Heard through the sound FX (#352), then straight into the master: a preview leaves the routing out (#368). */
|
||||||
readonly music = new Music(
|
readonly music = new Music(
|
||||||
() => this.state.project,
|
() => this.state.project,
|
||||||
() => playedRoute(this.state.project, '', this.asset()?.id ?? ''),
|
() => MASTER_MIX,
|
||||||
() => {
|
() => {
|
||||||
const asset = this.asset();
|
const asset = this.asset();
|
||||||
return asset && fxVoiceOf(this.state.project, assetFxOf(asset.props), songTempo(asset.props));
|
return asset && fxVoiceOf(this.state.project, assetFxOf(asset.props), songTempo(asset.props));
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
} from '../../core/properties/sounds';
|
} from '../../core/properties/sounds';
|
||||||
import { Button, Dropdown, Label, Menu, NumberField } from '../../ui';
|
import { Button, Dropdown, Label, Menu, NumberField } from '../../ui';
|
||||||
import { soundLibrary } from '../../viewport/SoundLibrary';
|
import { soundLibrary } from '../../viewport/SoundLibrary';
|
||||||
import { playedBus } from '../../core/music/mixer';
|
import { MASTER_MIX } from '../../core/music/mixer';
|
||||||
import { DRY_VOICE } from '../../core/soundfx/nodes';
|
import { DRY_VOICE } from '../../core/soundfx/nodes';
|
||||||
import { fxVoiceOf } from '../../core/music/needs';
|
import { fxVoiceOf } from '../../core/music/needs';
|
||||||
import { DEFAULT_TEMPO } from '../../core/music/time';
|
import { DEFAULT_TEMPO } from '../../core/music/time';
|
||||||
@@ -152,11 +152,10 @@ export class SoundEditor extends AssetTabEditor<SoundAsset, WaveCanvas> {
|
|||||||
this.stop();
|
this.stop();
|
||||||
claimTransport(this.halt);
|
claimTransport(this.halt);
|
||||||
const props = resolveAssetProps(this.state.project, asset);
|
const props = resolveAssetProps(this.state.project, asset);
|
||||||
// Through its sound FX (#352) and along its routing (#351), as the game hears it.
|
// Through its sound FX (#352), then straight into the master: a preview leaves its routing out (#368).
|
||||||
const bus = playedBus(this.state.project, '', asset.id);
|
|
||||||
const fx = fxVoiceOf(this.state.project, assetFxOf(props), DEFAULT_TEMPO) ?? DRY_VOICE;
|
const fx = fxVoiceOf(this.state.project, assetFxOf(props), DEFAULT_TEMPO) ?? DRY_VOICE;
|
||||||
const file = playedSoundOf({ asset, clip });
|
const file = playedSoundOf({ asset, clip });
|
||||||
this.voice = soundLibrary.play(file, volumeOf(props), false, PREVIEW_OWNER, fx, bus);
|
this.voice = soundLibrary.play(file, volumeOf(props), false, PREVIEW_OWNER, fx, MASTER_MIX);
|
||||||
this.playingId = this.voice ? `${asset.id}:${clip?.id ?? ''}` : '';
|
this.playingId = this.voice ? `${asset.id}:${clip?.id ?? ''}` : '';
|
||||||
this.syncPlayButton();
|
this.syncPlayButton();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { volumeOf } from '../../core/properties/sounds';
|
|||||||
import { soundFxBuildFor, soundFxParams } from '../../core/soundfx/compile';
|
import { soundFxBuildFor, soundFxParams } from '../../core/soundfx/compile';
|
||||||
import { Button, Dropdown, HStack, Label, Menu, NumberField, Stack, Theme } from '../../ui';
|
import { Button, Dropdown, HStack, Label, Menu, NumberField, Stack, Theme } from '../../ui';
|
||||||
import { soundLibrary } from '../../viewport/SoundLibrary';
|
import { soundLibrary } from '../../viewport/SoundLibrary';
|
||||||
import { playedBus } from '../../core/music/mixer';
|
import { MASTER_MIX } from '../../core/music/mixer';
|
||||||
import { AssetTabEditor } from '../AssetTabEditor';
|
import { AssetTabEditor } from '../AssetTabEditor';
|
||||||
import { GraphCanvas } from '../graph/GraphCanvas';
|
import { GraphCanvas } from '../graph/GraphCanvas';
|
||||||
import { SnapToggle } from '../graph/SnapToggle';
|
import { SnapToggle } from '../graph/SnapToggle';
|
||||||
@@ -106,10 +106,9 @@ export class SoundFxEditor extends AssetTabEditor<SoundFxAsset, GraphCanvas> {
|
|||||||
if (!sound?.asset.file || !plan) return;
|
if (!sound?.asset.file || !plan) return;
|
||||||
const props = resolveAssetProps(this.state.project, sound.asset);
|
const props = resolveAssetProps(this.state.project, sound.asset);
|
||||||
const fx = { plan, params: { ...this.values } };
|
const fx = { plan, params: { ...this.values } };
|
||||||
// Along the routing of the sound, as the game would play it there (#351).
|
// Straight into the master: a preview leaves the routing of the sound out (#368).
|
||||||
const bus = playedBus(this.state.project, '', sound.asset.id);
|
|
||||||
const file = playedSoundOf(sound);
|
const file = playedSoundOf(sound);
|
||||||
const voice = soundLibrary.play(file, volumeOf(props), false, PREVIEW_OWNER, fx, bus);
|
const voice = soundLibrary.play(file, volumeOf(props), false, PREVIEW_OWNER, fx, MASTER_MIX);
|
||||||
if (voice) this.voices.push(voice);
|
if (voice) this.voices.push(voice);
|
||||||
this.playedFor = asset.id;
|
this.playedFor = asset.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ export class Music {
|
|||||||
readonly clips: PlannedClip[] = [];
|
readonly clips: PlannedClip[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `output` is where what it plays goes (#351): along the routing of the pattern or song in the
|
* `output` is where what it plays goes (#351): the master where left out, as for the preview of
|
||||||
* tab, so it sounds as it would in the game. The master where left out. `fx` is the sound FX of
|
* a tab, which leaves the routing of its pattern or song out (#368). `fx` is the sound FX of
|
||||||
* that pattern or song (#352), on the way there.
|
* that pattern or song (#352), on the way there.
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
+12
-7
@@ -2339,8 +2339,9 @@ export async function sound(t) {
|
|||||||
|
|
||||||
// 34h-3. Buses and the master (#325), routed in the Master routing tab (#351). "+ → Bus" makes
|
// 34h-3. Buses and the master (#325), routed in the Master routing tab (#351). "+ → Bus" makes
|
||||||
// one; the tab has a node for it and for the sound, each linked to the master. Links drawn
|
// one; the tab has a node for it and for the sound, each linked to the master. Links drawn
|
||||||
// there and one deleted send the sound into a bus and that bus into the other, as the Sound
|
// there and one deleted send the sound into a bus and that bus into the other; the Sound
|
||||||
// tab then plays it. A click on a node shows its asset, on the master the master's volume.
|
// tab still plays it on the master (#368). A click on a node shows its asset, on the
|
||||||
|
// master the master's volume.
|
||||||
for (let i = 0; i < 2; i++) {
|
for (let i = 0; i < 2; i++) {
|
||||||
await clickText('+');
|
await clickText('+');
|
||||||
await pickAssetType(t, 'Bus');
|
await pickAssetType(t, 'Bus');
|
||||||
@@ -2395,6 +2396,9 @@ export async function sound(t) {
|
|||||||
await mutate(`
|
await mutate(`
|
||||||
__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(busA.id)}).props.volume = 0.5;`);
|
__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(busA.id)}).props.volume = 0.5;`);
|
||||||
await evaluate(`__gm.state.openAsset(${JSON.stringify(droneId)})`);
|
await evaluate(`__gm.state.openAsset(${JSON.stringify(droneId)})`);
|
||||||
|
// The preview of a tab leaves the routing out (#368): heard on the master, the buses stay silent.
|
||||||
|
const tapped = JSON.stringify(['mix', `mix:${busB.id}`]);
|
||||||
|
await json(`__gm.routing.peaks(${tapped})`);
|
||||||
const startedBefore = (await stats()).started;
|
const startedBefore = (await stats()).started;
|
||||||
await click(tabPlay.x, tabPlay.y);
|
await click(tabPlay.x, tabPlay.y);
|
||||||
await waitFor(stats, (s) => s.started > startedBefore, 10);
|
await waitFor(stats, (s) => s.started > startedBefore, 10);
|
||||||
@@ -2402,22 +2406,23 @@ export async function sound(t) {
|
|||||||
const buses = await json('__gm.sound.buses()');
|
const buses = await json('__gm.sound.buses()');
|
||||||
const heardOn = (await stats()).last?.bus;
|
const heardOn = (await stats()).last?.bus;
|
||||||
const peak = await waitFor(
|
const peak = await waitFor(
|
||||||
() => json(`__gm.routing.peaks([${JSON.stringify(`mix:${busB.id}`)}])`),
|
() => json(`__gm.routing.peaks(${tapped})`),
|
||||||
(peaks) => Math.max(...(peaks[`mix:${busB.id}`] ?? [0])) > 0.01,
|
(peaks) => Math.max(...(peaks.mix ?? [0])) > 0.01,
|
||||||
);
|
);
|
||||||
await click(tabPlay.x, tabPlay.y);
|
await click(tabPlay.x, tabPlay.y);
|
||||||
const levelOf = (name) => buses.find((bus) => bus.name === name);
|
const levelOf = (name) => buses.find((bus) => bus.name === name);
|
||||||
check(
|
check(
|
||||||
'linked in the routing, the Sound tab plays the sound on its bus, into the bus that one goes to, on into the master',
|
'linked in the routing, the mix sends the sound’s bus into the other; the Sound tab plays it on the master, past both',
|
||||||
links(await routing())
|
links(await routing())
|
||||||
.filter((l) => l.startsWith(droneId) || l.startsWith(busB.id))
|
.filter((l) => l.startsWith(droneId) || l.startsWith(busB.id))
|
||||||
.join() === `${droneId}>${busB.id},${busB.id}>${busA.id}` &&
|
.join() === `${droneId}>${busB.id},${busB.id}>${busA.id}` &&
|
||||||
heardOn === `mix:${busB.id}` &&
|
heardOn === 'mix' &&
|
||||||
levelOf(`mix:${busB.id}`)?.into === `mix:${busA.id}` &&
|
levelOf(`mix:${busB.id}`)?.into === `mix:${busA.id}` &&
|
||||||
levelOf(`mix:${busA.id}`)?.into === 'mix' &&
|
levelOf(`mix:${busA.id}`)?.into === 'mix' &&
|
||||||
levelOf(`mix:${busA.id}`)?.gain === 0.5 &&
|
levelOf(`mix:${busA.id}`)?.gain === 0.5 &&
|
||||||
levelOf('mix')?.into === null &&
|
levelOf('mix')?.into === null &&
|
||||||
Math.max(...(peak?.[`mix:${busB.id}`] ?? [0])) > 0.01,
|
Math.max(...(peak?.mix ?? [0])) > 0.01 &&
|
||||||
|
Math.max(...(peak?.[`mix:${busB.id}`] ?? [0])) < 0.001,
|
||||||
JSON.stringify({ links: links(await routing()), heardOn, buses, peak }),
|
JSON.stringify({ links: links(await routing()), heardOn, buses, peak }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user