From fa26992b5645420069464d335fa6b2f22fedee11 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Wed, 22 Jul 2026 01:21:36 +0900 Subject: [PATCH] feat: add narrative page turn audio cue --- docs/audio-sources.md | 12 +++++++- scripts/verify-audio-asset-data.mjs | 32 +++++++++++++++++++++ scripts/verify-sound-director.mjs | 34 +++++++++++++++++++++-- src/assets/audio/sfx/story-page-turn.mp3 | Bin 0 -> 16556 bytes src/game/audio/SoundDirector.ts | 18 +++++++++--- src/game/audio/audioAssets.ts | 2 ++ 6 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 src/assets/audio/sfx/story-page-turn.mp3 diff --git a/docs/audio-sources.md b/docs/audio-sources.md index eec1cf3..f2434ea 100644 --- a/docs/audio-sources.md +++ b/docs/audio-sources.md @@ -1,6 +1,6 @@ # Audio Sources -Downloaded from Pixabay on 2026-06-22 and 2026-07-21 and used under the Pixabay Content License. +Downloaded from Pixabay on 2026-06-22, 2026-07-21, and 2026-07-22 and used under the Pixabay Content License. License reference: https://pixabay.com/service/license-summary/ @@ -45,6 +45,16 @@ The source files in this section were downloaded from their official Pixabay det All seven files were resampled to 48 kHz stereo MP3. Tactical cues were silence-trimmed, equalized by role, and balanced to approximately -17.5 to -19 LUFS. The rain and fire recordings were reduced to stable excerpts, balanced near -25 LUFS, and rebuilt with a one-second end-to-start crossfade so repeated playback does not introduce a hard seam. +## Narrative Interface Cues + +The source file in this section was downloaded from its official Pixabay detail page on 2026-07-22 and uses the Pixabay Content License linked above. + +| Project file | Role | Pixabay title | Creator | Source | +| --- | --- | --- | --- | --- | +| `src/assets/audio/sfx/story-page-turn.mp3` | Story page advance feedback | Flipping Book Page | DRAGON-STUDIO | https://pixabay.com/sound-effects/film-special-effects-flipping-book-page-499646/ | + +The narrative cue was resampled to 48 kHz stereo MP3, silence-trimmed to an approximately 1.0-second excerpt, fade-in/out processed, and high-pass and low-pass filtered for unobtrusive playback over story music. + ## Sound Effects | Project file | Pixabay title | Creator | Source | diff --git a/scripts/verify-audio-asset-data.mjs b/scripts/verify-audio-asset-data.mjs index 9ccadd5..b43a090 100644 --- a/scripts/verify-audio-asset-data.mjs +++ b/scripts/verify-audio-asset-data.mjs @@ -19,6 +19,7 @@ const requiredTacticalCueKeys = [ 'objective-success', 'objective-failure' ]; +const requiredNarrativeCueKeys = ['story-page-turn']; const requiredBattleSceneAudioMethods = [ 'playSoundscape', 'playAllyTurnCue', @@ -49,6 +50,20 @@ const battlefieldSourceRecords = [ source: 'https://pixabay.com/sound-effects/film-special-effects-failure-1-89170/' } ]; +const narrativeSourceRecord = { + projectFile: 'src/assets/audio/sfx/story-page-turn.mp3', + source: 'https://pixabay.com/sound-effects/film-special-effects-flipping-book-page-499646/', + requiredDocumentation: [ + '2026-07-22', + 'Flipping Book Page', + 'DRAGON-STUDIO', + '48 kHz stereo MP3', + '1.0-second', + 'silence-trimmed', + 'fade-in/out', + 'high-pass and low-pass filtered' + ] +}; const server = await createServer({ logLevel: 'error', @@ -69,7 +84,9 @@ try { validateTrackMap(errors, 'effect', effectTracks, 'sfx'); validateRequiredTrackKeys(errors, 'battlefield ambience', ambienceTracks, requiredBattlefieldAmbienceKeys); validateRequiredTrackKeys(errors, 'tactical cue', effectTracks, requiredTacticalCueKeys); + validateRequiredTrackKeys(errors, 'narrative cue', effectTracks, requiredNarrativeCueKeys); validateBattlefieldSourceDocumentation(errors); + validateNarrativeSourceDocumentation(errors); validateBattleSceneAudioIntegration(errors); validateEffectPools(errors, effectPools, effectTracks); validateEffectPoolRegistration(errors, effectPools); @@ -145,6 +162,21 @@ function validateBattlefieldSourceDocumentation(errors) { }); } +function validateNarrativeSourceDocumentation(errors) { + const docs = readFileSync(join('docs', 'audio-sources.md'), 'utf8'); + if (!docs.includes(narrativeSourceRecord.projectFile)) { + errors.push(`docs/audio-sources.md is missing narrative audio file "${narrativeSourceRecord.projectFile}"`); + } + if (!docs.includes(narrativeSourceRecord.source)) { + errors.push(`docs/audio-sources.md is missing Pixabay source "${narrativeSourceRecord.source}"`); + } + narrativeSourceRecord.requiredDocumentation.forEach((value) => { + if (!docs.includes(value)) { + errors.push(`docs/audio-sources.md is missing narrative audio detail "${value}"`); + } + }); +} + function validateBattleSceneAudioIntegration(errors) { const path = join('src', 'game', 'scenes', 'BattleScene.ts'); const source = readFileSync(path, 'utf8'); diff --git a/scripts/verify-sound-director.mjs b/scripts/verify-sound-director.mjs index 3f5a8ea..0a09fc4 100644 --- a/scripts/verify-sound-director.mjs +++ b/scripts/verify-sound-director.mjs @@ -102,6 +102,7 @@ try { 'operation-alert': '/audio/operation-alert.ogg', 'objective-success': '/audio/objective-success.ogg', 'objective-failure': '/audio/objective-failure.ogg', + 'story-page-turn': '/audio/story-page-turn.ogg', 'ui-select': '/audio/ui-select.ogg' }); director.registerEffectPools({ @@ -387,7 +388,7 @@ try { debug = director.getDebugState(); assert.deepEqual( debug.semanticCues.cooldownMs, - { turn: 900, operation: 650, objective: 1200 }, + { turn: 900, operation: 650, objective: 1200, narrative: 220 }, 'semantic cue cooldowns should be visible in debug state' ); assert.deepEqual( @@ -417,6 +418,35 @@ try { assert.equal(debug.activeEffectCount, 0, 'tactical cues should retire after their playback windows'); assert.equal(audioHarness.activeInstances().length, 1, 'tactical cue cleanup should leave only music active'); + const narrativeCueStart = audioHarness.instances.length; + const musicDuckRevisionBeforeNarrativeCue = debug.musicDuck.revision; + assert.equal(director.playStoryAdvanceCue(), true, 'the first story advance cue should play'); + assertClose(audioHarness.byOriginalSrc('/audio/story-page-turn.ogg').volume, 0.15, 'story advance cue volume'); + debug = director.getDebugState(); + assert.equal( + debug.musicDuck.revision, + musicDuckRevisionBeforeNarrativeCue, + 'story advance cues must not duck music' + ); + assert.deepEqual( + debug.semanticCues.lastPlayed, + { group: 'narrative', key: 'story-page-turn', at: clock.now }, + 'debug state should expose a played story advance cue' + ); + assert.equal(director.playStoryAdvanceCue(), false, 'rapid story advance cues should be coalesced'); + debug = director.getDebugState(); + assert.deepEqual( + debug.semanticCues.lastSuppressed, + { group: 'narrative', key: 'story-page-turn', at: clock.now, remainingMs: 220 }, + 'debug state should expose a coalesced story advance cue' + ); + assert.equal(audioHarness.instances.length, narrativeCueStart + 1, 'coalesced story cues must not construct Audio'); + clock.advance(220); + assert.equal(director.playStoryAdvanceCue(), true, 'story advance cues should replay after the narrative cooldown'); + assert.equal(audioHarness.instances.length, narrativeCueStart + 2, 'expired narrative cooldown should permit new Audio'); + clock.advance(1100); + assert.equal(director.getDebugState().activeEffectCount, 0, 'story advance cues should retire after playback'); + const semanticEffectStart = audioHarness.instances.length; director.playMeleeSwing(true, false); assert.equal(director.getDebugState().lastEffect?.key, 'melee-swing', 'melee API should use the swing pool'); @@ -455,7 +485,7 @@ try { console.log( `Verified dual SoundDirector channels across ${debug.music.elementRevision} music and ` + `${debug.ambience.elementRevision} ambience element revisions, no-repeat effect pools, ` + - 'coalesced tactical cues, semantic combat effects, category multipliers, and deterministic music ducking.' + 'coalesced tactical and narrative cues, semantic combat effects, category multipliers, and deterministic music ducking.' ); } finally { restoreGlobals(); diff --git a/src/assets/audio/sfx/story-page-turn.mp3 b/src/assets/audio/sfx/story-page-turn.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..4bed8819962b4e935da91346d2bfff0b0ce77464 GIT binary patch literal 16556 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 literal 0 HcmV?d00001 diff --git a/src/game/audio/SoundDirector.ts b/src/game/audio/SoundDirector.ts index c24623e..8b376c0 100644 --- a/src/game/audio/SoundDirector.ts +++ b/src/game/audio/SoundDirector.ts @@ -57,7 +57,7 @@ export type MusicDuckOptions = { releaseMs?: number; }; -type SemanticCueGroup = 'turn' | 'operation' | 'objective'; +type SemanticCueGroup = 'turn' | 'operation' | 'objective' | 'narrative'; type MusicDuckState = { multiplier: number; @@ -102,7 +102,8 @@ export class SoundDirector { private readonly semanticCueCooldownMs: Record = { turn: 900, operation: 650, - objective: 1200 + objective: 1200, + narrative: 220 }; private readonly semanticCuePlayedAt = new Map(); private lastPlayedSemanticCue?: { group: SemanticCueGroup; key: string; at: number }; @@ -434,6 +435,13 @@ export class SoundDirector { }); } + playStoryAdvanceCue() { + return this.playSemanticCue('narrative', 'story-page-turn', { + volume: 0.15, + stopAfterMs: 1100 + }); + } + playStrategyPulse() { this.playEffect('strategy-cast', { volume: 0.44, rate: 0.96, stopAfterMs: 960 }); this.playTone(460, 0.08, 0.012, 'sine'); @@ -500,7 +508,7 @@ export class SoundDirector { private playSemanticCue( group: SemanticCueGroup, key: string, - options: PlayEffectOptions & { duck: MusicDuckOptions } + options: PlayEffectOptions & { duck?: MusicDuckOptions } ) { if (!this.started || this.muted || !this.effectTracks[key]) { return false; @@ -519,7 +527,9 @@ export class SoundDirector { return false; } - this.duckMusic(options.duck); + if (options.duck) { + this.duckMusic(options.duck); + } const played = this.playEffect(key, options); if (played) { this.semanticCuePlayedAt.set(group, now); diff --git a/src/game/audio/audioAssets.ts b/src/game/audio/audioAssets.ts index d7f5538..9cc03b2 100644 --- a/src/game/audio/audioAssets.ts +++ b/src/game/audio/audioAssets.ts @@ -29,6 +29,7 @@ import objectiveSuccessUrl from '../../assets/audio/sfx/objective-success.mp3'; import operationAlertUrl from '../../assets/audio/sfx/operation-alert.mp3'; import shieldBlockUrl from '../../assets/audio/sfx/shield-block.mp3'; import strategyCastUrl from '../../assets/audio/sfx/strategy-cast.mp3'; +import storyPageTurnUrl from '../../assets/audio/sfx/story-page-turn.mp3'; import swordSlashUrl from '../../assets/audio/sfx/sword-slash.mp3'; import swordSwingVariantUrl from '../../assets/audio/sfx/sword-swing-variant.mp3'; import uiSelectUrl from '../../assets/audio/sfx/ui-select.mp3'; @@ -80,6 +81,7 @@ export const effectTracks = { 'critical-impact': criticalImpactUrl, 'critical-boom': criticalBoomUrl, 'strategy-cast': strategyCastUrl, + 'story-page-turn': storyPageTurnUrl, 'cart-roll': cartRollUrl, 'exp-gain': expGainUrl, 'level-up': levelUpUrl,