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:
adminandClaude Opus 5.5 committed 2026-10-09 13:09:36 +02:00
1 parent 1f13794b0c
commit af2312a5e7
7 files changed
+30 -25

No files matched your search

+4 -2
View File
@@ -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<InstrumentAsset, GraphCanva
struck,
PREVIEW_OWNER,
{ ...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),
);
if (voice) this.voices.push(voice);
+3 -3
View File
@@ -24,7 +24,7 @@ import { DEFAULT_TEMPO } from '../../core/music/time';
import { instrumentFx, playNote } from '../../runtime/systems/behaviorContext';
import { Button, Dropdown, Label, Menu, 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 { AssetTabEditor } from '../AssetTabEditor';
import { claimTransport, releaseTransport } from '../music/transport';
@@ -54,10 +54,10 @@ export class PatternEditor extends AssetTabEditor<PatternAsset, PianoRoll> {
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));
+3 -3
View File
@@ -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<SongAsset, SongTimeline> {
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));
+3 -4
View File
@@ -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<SoundAsset, WaveCanvas> {
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();
}
+3 -4
View File
@@ -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<SoundFxAsset, GraphCanvas> {
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;
}
+2 -2
View File
@@ -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(