diff --git a/src/audio/create-audio-controller.ts b/src/audio/create-audio-controller.ts index 9fb814a..00cfb38 100644 --- a/src/audio/create-audio-controller.ts +++ b/src/audio/create-audio-controller.ts @@ -2,10 +2,11 @@ import type { AudioController, AudioControllerOptions, AudioGraph, - AudioJourneyState, + ChapterIntent, + DriveEnergySample, AudioUiState, } from '@/audio/types'; -import { DEFAULT_AUDIO_JOURNEY_STATE } from '@/audio/journey-music-state'; +import { DEFAULT_AUDIO_INITIAL_STATE } from '@/audio/journey-music-state'; type ActualState = AudioUiState['kind']; type DesiredState = 'idle' | 'running' | 'suspended' | 'closed'; @@ -21,7 +22,12 @@ export function createAudioController(options: AudioControllerOptions): AudioCon let visible = options.initiallyVisible ?? true; let muted = false; let volume = clamp(options.initialVolume ?? DEFAULT_VOLUME); - let journeyState = normalizeJourneyState(options.initialJourneyState ?? DEFAULT_AUDIO_JOURNEY_STATE); + let chapterIntent = normalizeChapterIntent( + options.initialState?.chapter ?? DEFAULT_AUDIO_INITIAL_STATE.chapter, + ); + let driveEnergy = normalizeDriveEnergy( + options.initialState?.drive ?? DEFAULT_AUDIO_INITIAL_STATE.drive, + ); let graph: AudioGraph | null = null; let graphClosed = false; let removeStateListener = (): void => {}; @@ -96,7 +102,8 @@ export function createAudioController(options: AudioControllerOptions): AudioCon if (!graph || graphClosed) return; graph.connectOutput(); graph.setOutputGain(muted ? 0 : volume); - graph.setJourneyState(journeyState); + graph.setChapterIntent(chapterIntent); + graph.setDriveEnergy(driveEnergy); resumeIntent = 'none'; setActual('running'); }; @@ -152,12 +159,13 @@ export function createAudioController(options: AudioControllerOptions): AudioCon try { // This factory call intentionally remains synchronous in the activating event stack. - graph = options.createGraph(journeyState); + graph = options.createGraph({ chapter: chapterIntent, drive: driveEnergy }); removeStateListener = graph.onContextStateChange((state) => { if (state === 'interrupted') void noteInterruption(); }); graph.setOutputGain(0); - graph.setJourneyState(journeyState); + graph.setChapterIntent(chapterIntent); + graph.setDriveEnergy(driveEnergy); } catch { startOperation = enqueue(fail); return startOperation; @@ -265,10 +273,15 @@ export function createAudioController(options: AudioControllerOptions): AudioCon graph?.setOutputGain(muted ? 0 : volume); if (actual === 'running') publish(); }, - setJourneyState(nextState) { + setChapterIntent(nextIntent) { if (actual === 'disposed') return; - journeyState = normalizeJourneyState(nextState); - graph?.setJourneyState(journeyState); + chapterIntent = normalizeChapterIntent(nextIntent); + graph?.setChapterIntent(chapterIntent); + }, + setDriveEnergy(nextSample) { + if (actual === 'disposed') return; + driveEnergy = normalizeDriveEnergy(nextSample); + graph?.setDriveEnergy(driveEnergy); }, setVisible, noteInterruption, @@ -290,12 +303,22 @@ function clamp(value: number): number { return Math.min(1, Math.max(0, value)); } -function normalizeJourneyState(state: AudioJourneyState): AudioJourneyState { - const entryPhrase = Number.isFinite(state.entryPhrase) ? Math.trunc(state.entryPhrase) : 0; +function normalizeChapterIntent(intent: ChapterIntent): ChapterIntent { + const entryPhrase = Number.isFinite(intent.entryPhrase) + ? Math.trunc(intent.entryPhrase) + : 0; + const generation = Number.isFinite(intent.generation) + ? Math.trunc(intent.generation) + : 0; return Object.freeze({ - atStart: state.atStart === true, - bias: state.bias, + sectionId: intent.sectionId, + mood: intent.mood, + atStart: intent.atStart === true, entryPhrase: Math.min(7, Math.max(0, entryPhrase)), - intensity: clamp(state.intensity), + generation: Math.max(0, generation), }); } + +function normalizeDriveEnergy(sample: DriveEnergySample): DriveEnergySample { + return Object.freeze({ energy: clamp(sample.energy), source: sample.source }); +} diff --git a/src/audio/journey-music-state.ts b/src/audio/journey-music-state.ts index 1ab1acc..13f9ccd 100644 --- a/src/audio/journey-music-state.ts +++ b/src/audio/journey-music-state.ts @@ -1,6 +1,6 @@ import type { SectionId } from '@/content/types'; import type { - AudioJourneyState, + AudioInitialState, ChapterIntent, DriveEnergySample, MusicMood, @@ -40,18 +40,7 @@ export const DEFAULT_DRIVE_ENERGY = Object.freeze({ source: 'idle', } as const satisfies DriveEnergySample); -export function resolveJourneyMusicState(sectionId: string, intensity = 0): AudioJourneyState { - const canonicalId = Object.hasOwn(JOURNEY_MUSIC, sectionId) - ? sectionId as SectionId - : 'start'; - const chapter = resolveChapterIntent(canonicalId); - const finiteIntensity = Number.isFinite(intensity) ? intensity : 0; - return Object.freeze({ - atStart: sectionId === 'start', - intensity: Math.min(1, Math.max(0, finiteIntensity)), - bias: chapter.mood, - entryPhrase: chapter.entryPhrase, - }); -} - -export const DEFAULT_AUDIO_JOURNEY_STATE = resolveJourneyMusicState('start'); +export const DEFAULT_AUDIO_INITIAL_STATE = Object.freeze({ + chapter: DEFAULT_CHAPTER_INTENT, + drive: DEFAULT_DRIVE_ENERGY, +} as const satisfies AudioInitialState); diff --git a/src/audio/soundtrack-engine.ts b/src/audio/soundtrack-engine.ts index 289907f..0286add 100644 --- a/src/audio/soundtrack-engine.ts +++ b/src/audio/soundtrack-engine.ts @@ -13,8 +13,10 @@ import { import type { AudioContextLifecycleState, AudioGraph, - AudioJourneyState, - MusicChapterBias, + AudioInitialState, + ChapterIntent, + DriveEnergySample, + MusicMood, SoundtrackSources, } from '@/audio/types'; @@ -35,7 +37,7 @@ interface MixProfile { readonly highShelfDb: number; } -const MIX_PROFILES: Readonly> = Object.freeze({ +const MIX_PROFILES: Readonly> = Object.freeze({ open: { gain: 0.985, highShelfDb: -0.35 }, shadow: { gain: 0.97, highShelfDb: -0.9 }, lift: { gain: 1, highShelfDb: 0.35 }, @@ -44,7 +46,7 @@ const MIX_PROFILES: Readonly> = Object.free export function createBrowserSoundtrackEngine( sources: SoundtrackSources, - initialState: AudioJourneyState, + initialState: AudioInitialState, ): AudioGraph { const BrowserAudioContext = window.AudioContext; if (BrowserAudioContext === undefined) throw new Error('Web Audio is unavailable.'); @@ -58,10 +60,10 @@ export function createBrowserSoundtrackEngine( const outputGain = context.createGain(); const sourceNodes = new Set(); const stateListeners = new Set<(state: AudioContextLifecycleState) => void>(); - let latestState = initialState; + let latestChapter = initialState.chapter; let connected = false; let closed = false; - const activatedFromStart = initialState.atStart; + const activatedFromStart = initialState.chapter.atStart; let transportStarted = false; let introStarted = false; let mainCommitted = false; @@ -106,11 +108,10 @@ export function createBrowserSoundtrackEngine( parameter.linearRampToValueAtTime(value, now + duration); }; - const applyMix = (state: AudioJourneyState): void => { - const profile = MIX_PROFILES[state.bias]; - const intensity = clamp(state.intensity); - ramp(profileGain.gain, profile.gain + intensity * 0.012, PROFILE_RAMP_SECONDS); - ramp(tone.gain, profile.highShelfDb + intensity * 0.25, PROFILE_RAMP_SECONDS); + const applyMix = (chapter: ChapterIntent): void => { + const profile = MIX_PROFILES[chapter.mood]; + ramp(profileGain.gain, profile.gain, PROFILE_RAMP_SECONDS); + ramp(tone.gain, profile.highShelfDb, PROFILE_RAMP_SECONDS); }; const startMain = ( @@ -207,7 +208,7 @@ export function createBrowserSoundtrackEngine( if (activatedFromStart) { loadedIntroBuffer = await loadAudioBuffer(context, sources.intro, signal); if (closed || signal.aborted) throw abortError(); - if (latestState.atStart) { + if (latestChapter.atStart) { transportStarted = true; startIntro(loadedIntroBuffer, context.currentTime + SOURCE_LEAD_SECONDS); return; @@ -216,7 +217,7 @@ export function createBrowserSoundtrackEngine( const mainBuffer = await mainBufferPromise; if (closed || signal.aborted) throw abortError(); - if (activatedFromStart && latestState.atStart && loadedIntroBuffer !== null) { + if (activatedFromStart && latestChapter.atStart && loadedIntroBuffer !== null) { transportStarted = true; startIntro(loadedIntroBuffer, context.currentTime + SOURCE_LEAD_SECONDS); return; @@ -225,7 +226,7 @@ export function createBrowserSoundtrackEngine( startMain( mainBuffer, context.currentTime + SOURCE_LEAD_SECONDS, - mainPhraseOffsetSeconds(latestState.entryPhrase), + mainPhraseOffsetSeconds(latestChapter.entryPhrase), START_FADE_SECONDS, ); }; @@ -234,24 +235,30 @@ export function createBrowserSoundtrackEngine( if (!introStarted || mainCommitted || exitTimer !== undefined || closed) return; exitTimer = window.setTimeout(() => { exitTimer = undefined; - if (closed || latestState.atStart || mainCommitted) return; + if (closed || latestChapter.atStart || mainCommitted) return; const introBuffer = loadedIntroBuffer; const pendingMain = mainBufferPromise; if (introBuffer === null || pendingMain === null) return; void pendingMain.then((mainBuffer) => { - if (!closed && !latestState.atStart && !mainCommitted) { + if (!closed && !latestChapter.atStart && !mainCommitted) { scheduleIntroExit(introBuffer, mainBuffer); } }).catch(() => undefined); }, EXIT_COALESCE_MS); }; + const cancelUnscheduledIntroExit = (): void => { + if (exitTimer === undefined) return; + window.clearTimeout(exitTimer); + exitTimer = undefined; + }; + const onStateChange = (): void => { const state = context.state as AudioContextLifecycleState; for (const listener of stateListeners) listener(state); }; context.addEventListener('statechange', onStateChange); - applyMix(initialState); + applyMix(initialState.chapter); return { async resume() { @@ -291,15 +298,15 @@ export function createBrowserSoundtrackEngine( setOutputGain(value) { ramp(outputGain.gain, clamp(value), 0.08); }, - setJourneyState(state) { + setChapterIntent(intent) { + if (closed) return; + latestChapter = intent; + applyMix(intent); + if (!intent.atStart) requestIntroExit(); + else cancelUnscheduledIntroExit(); + }, + setDriveEnergy(_sample: DriveEnergySample) { if (closed) return; - latestState = state; - applyMix(state); - if (!state.atStart) requestIntroExit(); - else if (exitTimer !== undefined) { - window.clearTimeout(exitTimer); - exitTimer = undefined; - } }, cancelPending() { loadController?.abort(); diff --git a/src/audio/soundtrack-transport.ts b/src/audio/soundtrack-transport.ts index 9e41a9e..b3f654e 100644 --- a/src/audio/soundtrack-transport.ts +++ b/src/audio/soundtrack-transport.ts @@ -1,4 +1,4 @@ -import type { AudioJourneyState } from '@/audio/types'; +import type { ChapterIntent } from '@/audio/types'; export const SOUNDTRACK_SAMPLE_RATE = 48_000; export const MUSIC_BPM = 86; @@ -25,7 +25,7 @@ export type InitialPlayback = | { readonly kind: 'intro' } | { readonly kind: 'main'; readonly offsetSeconds: number }; -export function resolveInitialPlayback(state: AudioJourneyState): InitialPlayback { +export function resolveInitialPlayback(state: ChapterIntent): InitialPlayback { return state.atStart ? { kind: 'intro' } : { kind: 'main', offsetSeconds: mainPhraseOffsetSeconds(state.entryPhrase) }; diff --git a/src/audio/types.ts b/src/audio/types.ts index 50cd0d1..006772d 100644 --- a/src/audio/types.ts +++ b/src/audio/types.ts @@ -11,7 +11,6 @@ export type AudioUiState = export type AudioContextLifecycleState = AudioContextState | 'interrupted'; export type MusicMood = 'open' | 'shadow' | 'lift' | 'resolve'; -export type MusicChapterBias = MusicMood; export type DriveEnergySource = 'physical' | 'navigation' | 'idle'; export interface ChapterIntent { @@ -32,13 +31,6 @@ export interface AudioInitialState { readonly drive: DriveEnergySample; } -export interface AudioJourneyState { - readonly atStart: boolean; - readonly intensity: number; - readonly bias: MusicChapterBias; - readonly entryPhrase: number; -} - export interface AudioSourceVariant { readonly url: string; readonly mediaType: 'audio/ogg' | 'audio/mpeg'; @@ -47,6 +39,8 @@ export interface AudioSourceVariant { export interface SoundtrackSources { readonly intro: readonly AudioSourceVariant[]; readonly main: readonly AudioSourceVariant[]; + readonly afterglow: readonly AudioSourceVariant[]; + readonly driveLift: readonly AudioSourceVariant[]; } export interface AudioGraph { @@ -56,18 +50,19 @@ export interface AudioGraph { connectOutput(): void; disconnectOutput(): void; setOutputGain(value: number): void; - setJourneyState(state: AudioJourneyState): void; + setChapterIntent(intent: ChapterIntent): void; + setDriveEnergy(sample: DriveEnergySample): void; cancelPending(): void; onContextStateChange(listener: (state: AudioContextLifecycleState) => void): () => void; } -export type AudioGraphFactory = (initialState: AudioJourneyState) => AudioGraph; +export type AudioGraphFactory = (initialState: AudioInitialState) => AudioGraph; export interface AudioControllerOptions { - createGraph: AudioGraphFactory; - initialVolume?: number; - initiallyVisible?: boolean; - initialJourneyState?: AudioJourneyState; + readonly createGraph: AudioGraphFactory; + readonly initialVolume?: number; + readonly initiallyVisible?: boolean; + readonly initialState?: AudioInitialState; } export interface AudioController { @@ -75,7 +70,8 @@ export interface AudioController { resumeFromGesture(): Promise; setMuted(muted: boolean): void; setVolume(volume: number): void; - setJourneyState(state: AudioJourneyState): void; + setChapterIntent(intent: ChapterIntent): void; + setDriveEnergy(sample: DriveEnergySample): void; setVisible(visible: boolean): Promise; noteInterruption(): Promise; pageHide(persisted: boolean): Promise; diff --git a/src/components/AppShell.vue b/src/components/AppShell.vue index 7c78d22..265aab5 100644 --- a/src/components/AppShell.vue +++ b/src/components/AppShell.vue @@ -14,6 +14,7 @@ import SiteNavigation from './SiteNavigation.vue'; import ToolkitGrid from './ToolkitGrid.vue'; import { useAdaptiveQuality } from '@/composables/useAdaptiveQuality'; import { useAudioEngine } from '@/composables/useAudioEngine'; +import { useAudioIntent } from '@/composables/useAudioIntent'; import { useJourneySections } from '@/composables/useJourneySections'; import { useKeyboardNavigation } from '@/composables/useKeyboardNavigation'; import { useMotionPreferences } from '@/composables/useMotionPreferences'; @@ -21,7 +22,7 @@ import { assetUrl } from '@/content/asset-url'; import type { PortfolioContent, PublicAssetManifest, SectionId, SectionLayout } from '@/content/types'; import type { SceneHostState } from '@/composables/useSceneController'; import type { MotionPolicy } from '@/journey/motion-policy'; -import type { SoundtrackSources } from '@/audio/types'; +import type { AudioSourceVariant, SoundtrackSources } from '@/audio/types'; declare const __PORTFOLIO_TEST_BUILD__: boolean; const TEST_BUILD = typeof __PORTFOLIO_TEST_BUILD__ !== 'undefined' && __PORTFOLIO_TEST_BUILD__; @@ -65,43 +66,30 @@ const journeyBinding = useJourneySections( computed(() => motion.policy.value.allowSceneRequest), ); const journey = journeyBinding.snapshot; -const pendingAudioDestination = shallowRef(null); -let audioNavigationEpoch = -1; -watch( - [navigation.state, () => journey.value.activeSectionId], - ([navigationState, activeSectionId]) => { - if (navigationState.epoch !== audioNavigationEpoch) { - audioNavigationEpoch = navigationState.epoch; - pendingAudioDestination.value = navigationState.source === 'passive-scroll' - ? null - : navigationState.destinationId; - } - if (pendingAudioDestination.value === activeSectionId) { - pendingAudioDestination.value = null; - } - }, - { flush: 'sync', immediate: true }, -); -const audioSectionId = computed(() => ( - pendingAudioDestination.value ?? journey.value.activeSectionId -)); +const audioIntent = useAudioIntent({ + canonicalIds: canonicalSectionIds, + journey: journeyBinding, + navigation: navigation.state, +}); +const variants = ( + sources: { readonly opusAssetId: string; readonly mp3AssetId: string }, +): readonly AudioSourceVariant[] => [ + { url: assetUrl(props.manifest, sources.opusAssetId), mediaType: 'audio/ogg' }, + { url: assetUrl(props.manifest, sources.mp3AssetId), mediaType: 'audio/mpeg' }, +]; const soundtrackSources = computed(() => { const soundtrack = props.content.soundtrack; if (soundtrack === undefined) return null; return { - intro: [ - { url: assetUrl(props.manifest, soundtrack.intro.opusAssetId), mediaType: 'audio/ogg' }, - { url: assetUrl(props.manifest, soundtrack.intro.mp3AssetId), mediaType: 'audio/mpeg' }, - ], - main: [ - { url: assetUrl(props.manifest, soundtrack.main.opusAssetId), mediaType: 'audio/ogg' }, - { url: assetUrl(props.manifest, soundtrack.main.mp3AssetId), mediaType: 'audio/mpeg' }, - ], + intro: variants(soundtrack.intro), + main: variants(soundtrack.main), + afterglow: variants(soundtrack.afterglow), + driveLift: variants(soundtrack.driveLift), }; }); const audio = useAudioEngine({ - activeSectionId: audioSectionId, - intensity: computed(() => journey.value.acceleration), + chapter: audioIntent.chapter, + drive: audioIntent.drive, soundtrack: soundtrackSources, volume: motion.volume, setVolume: motion.setVolume, diff --git a/src/composables/useAudioEngine.ts b/src/composables/useAudioEngine.ts index f8d09c4..f6abce3 100644 --- a/src/composables/useAudioEngine.ts +++ b/src/composables/useAudioEngine.ts @@ -1,5 +1,4 @@ import { - computed, onMounted, onUnmounted, readonly, @@ -9,17 +8,22 @@ import { } from 'vue'; import { createAudioController } from '@/audio/create-audio-controller'; import { createBrowserSoundtrackEngine } from '@/audio/soundtrack-engine'; -import { resolveJourneyMusicState } from '@/audio/journey-music-state'; -import type { AudioController, AudioUiState, SoundtrackSources } from '@/audio/types'; +import type { + AudioController, + AudioUiState, + ChapterIntent, + DriveEnergySample, + SoundtrackSources, +} from '@/audio/types'; declare const __PORTFOLIO_TEST_BUILD__: boolean; const TEST_BUILD = typeof __PORTFOLIO_TEST_BUILD__ !== 'undefined' && __PORTFOLIO_TEST_BUILD__; export interface AudioEngineSources { - activeSectionId: Readonly>; - intensity: Readonly>; - soundtrack: Readonly>; - volume: Readonly>; + readonly chapter: Readonly>; + readonly drive: Readonly>; + readonly soundtrack: Readonly>; + readonly volume: Readonly>; setVolume(value: number): void; } @@ -38,10 +42,6 @@ export function useAudioEngine(sources: AudioEngineSources): AudioEngineBinding let controller: AudioController | null = null; let removeSubscription = (): void => {}; let removePageListeners = (): void => {}; - const journeyState = computed(() => resolveJourneyMusicState( - sources.activeSectionId.value, - sources.intensity.value, - )); const ensureController = (): AudioController => { if (controller) return controller; @@ -53,13 +53,17 @@ export function useAudioEngine(sources: AudioEngineSources): AudioEngineBinding }, initialVolume: sources.volume.value, initiallyVisible: document.visibilityState !== 'hidden', - initialJourneyState: journeyState.value, + initialState: { + chapter: sources.chapter.value, + drive: sources.drive.value, + }, }); removeSubscription = controller.subscribe((next) => { state.value = next; }); return controller; }; - watch(journeyState, (state) => controller?.setJourneyState(state), { flush: 'sync' }); + watch(sources.chapter, (intent) => controller?.setChapterIntent(intent), { flush: 'sync' }); + watch(sources.drive, (sample) => controller?.setDriveEnergy(sample), { flush: 'sync' }); watch(sources.volume, (value) => controller?.setVolume(value)); onMounted(() => { diff --git a/tests/unit/audio-lifecycle.test.ts b/tests/unit/audio-lifecycle.test.ts index 2e98593..6b2ea10 100644 --- a/tests/unit/audio-lifecycle.test.ts +++ b/tests/unit/audio-lifecycle.test.ts @@ -3,7 +3,7 @@ import { createAudioController } from '@/audio/create-audio-controller'; import type { AudioGraph, AudioGraphFactory, - AudioJourneyState, + AudioInitialState, } from '@/audio/types'; describe('audio controller lifecycle', () => { @@ -196,25 +196,49 @@ describe('audio controller lifecycle', () => { expect(controller.snapshot().kind).toBe('failed'); }); - it('stores the latest semantic state before activation and forwards raw clamped updates', async () => { + it('stores and normalizes chapter and drive independently before activation', async () => { const harness = createHarness(); const controller = createAudioController({ createGraph: harness.createGraph }); + controller.setChapterIntent({ + sectionId: 'contact', + mood: 'resolve', + atStart: false, + entryPhrase: 99, + generation: -3, + }); + controller.setDriveEnergy({ energy: 2, source: 'navigation' }); - controller.setJourneyState({ atStart: false, bias: 'resolve', entryPhrase: 7, intensity: 2 }); expect(harness.constructed).toBe(0); await controller.startFromGesture(); + expect(harness.initialStates).toEqual([{ + chapter: { + sectionId: 'contact', + mood: 'resolve', + atStart: false, + entryPhrase: 7, + generation: 0, + }, + drive: { energy: 1, source: 'navigation' }, + }]); - expect(harness.initialStates).toEqual([ - { atStart: false, bias: 'resolve', entryPhrase: 7, intensity: 1 }, - ]); - - controller.setJourneyState({ atStart: false, bias: 'shadow', entryPhrase: 2, intensity: 0.73 }); - - expect(harness.graph.setJourneyState).toHaveBeenLastCalledWith({ + controller.setChapterIntent({ + sectionId: 'experience', + mood: 'shadow', atStart: false, - bias: 'shadow', entryPhrase: 2, - intensity: 0.73, + generation: 4, + }); + controller.setDriveEnergy({ energy: 0.73, source: 'physical' }); + expect(harness.graph.setChapterIntent).toHaveBeenLastCalledWith({ + sectionId: 'experience', + mood: 'shadow', + atStart: false, + entryPhrase: 2, + generation: 4, + }); + expect(harness.graph.setDriveEnergy).toHaveBeenLastCalledWith({ + energy: 0.73, + source: 'physical', }); }); }); @@ -245,7 +269,8 @@ function createHarness(options: HarnessOptions = {}) { connectOutput: vi.fn(), disconnectOutput: vi.fn(), setOutputGain: vi.fn(), - setJourneyState: vi.fn(), + setChapterIntent: vi.fn(), + setDriveEnergy: vi.fn(), cancelPending: vi.fn(), onContextStateChange: vi.fn((listener) => { stateListeners.add(listener); @@ -253,10 +278,10 @@ function createHarness(options: HarnessOptions = {}) { }), } satisfies AudioGraph; let constructed = 0; - const initialStates: AudioJourneyState[] = []; - const createGraph: AudioGraphFactory = (initialState?: AudioJourneyState) => { + const initialStates: AudioInitialState[] = []; + const createGraph: AudioGraphFactory = (initialState: AudioInitialState) => { constructed += 1; - if (initialState) initialStates.push(initialState); + initialStates.push(initialState); return graph; }; return { diff --git a/tests/unit/soundtrack-transport.test.ts b/tests/unit/soundtrack-transport.test.ts index 04b77f9..d41f548 100644 --- a/tests/unit/soundtrack-transport.test.ts +++ b/tests/unit/soundtrack-transport.test.ts @@ -12,17 +12,19 @@ import { describe('soundtrack transport decisions', () => { it('starts at the intro only on Start and otherwise chooses the matching main phrase', () => { expect(resolveInitialPlayback({ + sectionId: 'start', + mood: 'open', atStart: true, - bias: 'open', entryPhrase: 0, - intensity: 0, + generation: 0, })).toEqual({ kind: 'intro' }); expect(resolveInitialPlayback({ + sectionId: 'infrastructure', + mood: 'shadow', atStart: false, - bias: 'shadow', entryPhrase: 3, - intensity: 0.5, + generation: 1, })).toEqual({ kind: 'main', offsetSeconds: mainPhraseOffsetSeconds(3) }); expect(mainPhraseOffsetSeconds(Number.NaN)).toBe(0);