diff --git a/src/editor/instrument/InstrumentEditor.ts b/src/editor/instrument/InstrumentEditor.ts index ac704b94..67085c00 100644 --- a/src/editor/instrument/InstrumentEditor.ts +++ b/src/editor/instrument/InstrumentEditor.ts @@ -21,8 +21,9 @@ import { import { MIDDLE_C, noteName } from '../../core/instrument/notes'; import type { Asset, InstrumentAsset } from '../../core/model/Asset'; import { fxVoiceOf } from '../../core/music/needs'; +import { MASTER_MIX } from '../../core/music/mixer'; import { DEFAULT_TEMPO } from '../../core/music/time'; -import { instrumentBus, instrumentFx, playNote } from '../../runtime/systems/behaviorContext'; +import { instrumentFx, playNote } from '../../runtime/systems/behaviorContext'; import { Button, Dropdown, @@ -168,7 +169,8 @@ export class InstrumentEditor extends AssetTabEditor { readonly ghostsButton: Button; /** Where the ghost notes come from: song, track and bar. */ 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( () => this.state.project, - () => playedRoute(this.state.project, '', this.asset()?.id ?? ''), + () => MASTER_MIX, () => { const asset = this.asset(); return asset && fxVoiceOf(this.state.project, assetFxOf(asset.props), patternTempo(asset.props)); diff --git a/src/editor/song/SongEditor.ts b/src/editor/song/SongEditor.ts index 4a7c38aa..56fb84a3 100644 --- a/src/editor/song/SongEditor.ts +++ b/src/editor/song/SongEditor.ts @@ -28,7 +28,7 @@ import { assetFxOf } from '../../core/properties/assetFx'; import type { SoundStore } from '../../core/store/SoundStore'; import { Button, Dialog, Dropdown, Label, Menu, ui, VStack, type Widget } from '../../ui'; import { Music } from '../../viewport/music'; -import { playedRoute } from '../../core/music/mixer'; +import { MASTER_MIX } from '../../core/music/mixer'; import { soundLibrary } from '../../viewport/SoundLibrary'; import { type InputDevice, @@ -93,10 +93,10 @@ export class SongEditor extends AssetTabEditor { readonly snap = new SnapToggle(() => this.routing); private shown: SongView = 'timeline'; 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( () => this.state.project, - () => playedRoute(this.state.project, '', this.asset()?.id ?? ''), + () => MASTER_MIX, () => { const asset = this.asset(); return asset && fxVoiceOf(this.state.project, assetFxOf(asset.props), songTempo(asset.props)); diff --git a/src/editor/sound/SoundEditor.ts b/src/editor/sound/SoundEditor.ts index 9b82d147..cd5c3251 100644 --- a/src/editor/sound/SoundEditor.ts +++ b/src/editor/sound/SoundEditor.ts @@ -16,7 +16,7 @@ import { } from '../../core/properties/sounds'; import { Button, Dropdown, Label, Menu, NumberField } from '../../ui'; 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 { fxVoiceOf } from '../../core/music/needs'; import { DEFAULT_TEMPO } from '../../core/music/time'; @@ -152,11 +152,10 @@ export class SoundEditor extends AssetTabEditor { this.stop(); claimTransport(this.halt); const props = resolveAssetProps(this.state.project, asset); - // Through its sound FX (#352) and along its routing (#351), as the game hears it. - const bus = playedBus(this.state.project, '', asset.id); + // Through its sound FX (#352), then straight into the master: a preview leaves its routing out (#368). const fx = fxVoiceOf(this.state.project, assetFxOf(props), DEFAULT_TEMPO) ?? DRY_VOICE; 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.syncPlayButton(); } diff --git a/src/editor/soundfx/SoundFxEditor.ts b/src/editor/soundfx/SoundFxEditor.ts index 9cc4aa0a..ccf5c2fb 100644 --- a/src/editor/soundfx/SoundFxEditor.ts +++ b/src/editor/soundfx/SoundFxEditor.ts @@ -7,7 +7,7 @@ import { volumeOf } from '../../core/properties/sounds'; import { soundFxBuildFor, soundFxParams } from '../../core/soundfx/compile'; import { Button, Dropdown, HStack, Label, Menu, NumberField, Stack, Theme } from '../../ui'; import { soundLibrary } from '../../viewport/SoundLibrary'; -import { playedBus } from '../../core/music/mixer'; +import { MASTER_MIX } from '../../core/music/mixer'; import { AssetTabEditor } from '../AssetTabEditor'; import { GraphCanvas } from '../graph/GraphCanvas'; import { SnapToggle } from '../graph/SnapToggle'; @@ -106,10 +106,9 @@ export class SoundFxEditor extends AssetTabEditor { if (!sound?.asset.file || !plan) return; const props = resolveAssetProps(this.state.project, sound.asset); const fx = { plan, params: { ...this.values } }; - // Along the routing of the sound, as the game would play it there (#351). - const bus = playedBus(this.state.project, '', sound.asset.id); + // Straight into the master: a preview leaves the routing of the sound out (#368). 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); this.playedFor = asset.id; } diff --git a/src/viewport/music.ts b/src/viewport/music.ts index 128100ff..816c4e48 100644 --- a/src/viewport/music.ts +++ b/src/viewport/music.ts @@ -52,8 +52,8 @@ export class Music { readonly clips: PlannedClip[] = []; /** - * `output` is where what it plays goes (#351): along the routing of the pattern or song in the - * tab, so it sounds as it would in the game. The master where left out. `fx` is the sound FX of + * `output` is where what it plays goes (#351): the master where left out, as for the preview 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. */ constructor( diff --git a/tools/e2e/34-sound.mjs b/tools/e2e/34-sound.mjs index 0e9d1c75..c41c6e6b 100644 --- a/tools/e2e/34-sound.mjs +++ b/tools/e2e/34-sound.mjs @@ -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 // 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 - // tab then plays it. A click on a node shows its asset, on the master the master's volume. + // there and one deleted send the sound into a bus and that bus into the other; the Sound + // 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++) { await clickText('+'); await pickAssetType(t, 'Bus'); @@ -2395,6 +2396,9 @@ export async function sound(t) { await mutate(` __gm.state.project.assets.find((a) => a.id === ${JSON.stringify(busA.id)}).props.volume = 0.5;`); 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; await click(tabPlay.x, tabPlay.y); 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 heardOn = (await stats()).last?.bus; const peak = await waitFor( - () => json(`__gm.routing.peaks([${JSON.stringify(`mix:${busB.id}`)}])`), - (peaks) => Math.max(...(peaks[`mix:${busB.id}`] ?? [0])) > 0.01, + () => json(`__gm.routing.peaks(${tapped})`), + (peaks) => Math.max(...(peaks.mix ?? [0])) > 0.01, ); await click(tabPlay.x, tabPlay.y); const levelOf = (name) => buses.find((bus) => bus.name === name); 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()) .filter((l) => l.startsWith(droneId) || l.startsWith(busB.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:${busA.id}`)?.into === 'mix' && levelOf(`mix:${busA.id}`)?.gain === 0.5 && 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 }), );