Remove TTS narration and improve BGM

This commit is contained in:
2026-06-22 00:26:28 +09:00
parent e45ad9e5ea
commit cc554ccf21
25 changed files with 347 additions and 414 deletions

View File

@@ -1,18 +1,4 @@
type AudioTrackMap = Record<string, string>;
export type VoiceRole = 'narrator' | 'liuBei' | 'guanYu' | 'zhangFei';
type PitchSafeAudio = HTMLAudioElement & {
preservesPitch?: boolean;
mozPreservesPitch?: boolean;
webkitPreservesPitch?: boolean;
};
const rolePlayback: Record<VoiceRole, { rate: number; volume: number }> = {
narrator: { rate: 0.98, volume: 0.9 },
liuBei: { rate: 0.96, volume: 0.92 },
guanYu: { rate: 0.84, volume: 0.95 },
zhangFei: { rate: 1.05, volume: 1 }
};
class SoundDirector {
private context?: AudioContext;
@@ -20,24 +6,16 @@ class SoundDirector {
private started = false;
private muted = false;
private musicTracks: AudioTrackMap = {};
private voiceTracks: AudioTrackMap = {};
private music?: HTMLAudioElement;
private voice?: HTMLAudioElement;
private currentMusicKey?: string;
private pendingMusicKey?: string;
private musicFadeTimer?: number;
private musicRampTimer?: number;
private readonly musicVolume = 0.46;
private readonly duckedMusicVolume = 0.18;
private readonly musicVolume = 0.56;
registerMusicTracks(tracks: AudioTrackMap) {
this.musicTracks = tracks;
}
registerVoiceTracks(tracks: AudioTrackMap) {
this.voiceTracks = tracks;
}
start() {
if (this.started) {
this.resume();
@@ -74,9 +52,6 @@ class SoundDirector {
if (this.music) {
this.music.muted = muted;
}
if (this.voice) {
this.voice.muted = muted;
}
if (!this.context || !this.master) {
return;
@@ -130,64 +105,7 @@ class SoundDirector {
this.music = next;
this.currentMusicKey = key;
void next.play().catch(() => undefined);
this.fadeMusic(previous, next, this.voice ? this.duckedMusicVolume : this.musicVolume);
}
playVoice(key?: string, role: VoiceRole = 'narrator') {
if (!key || !this.started) {
this.stopVoice();
return;
}
this.stopVoice(false);
const src = this.voiceTracks[key];
if (!src) {
return;
}
const profile = rolePlayback[role] ?? rolePlayback.narrator;
const voice = new Audio(src) as PitchSafeAudio;
voice.preload = 'auto';
voice.muted = this.muted;
voice.volume = profile.volume;
voice.playbackRate = profile.rate;
voice.preservesPitch = false;
voice.mozPreservesPitch = false;
voice.webkitPreservesPitch = false;
voice.addEventListener('ended', () => {
if (this.voice === voice) {
this.voice = undefined;
this.duckMusic(false);
}
});
voice.addEventListener('error', () => {
if (this.voice === voice) {
this.voice = undefined;
this.duckMusic(false);
}
});
this.voice = voice;
this.duckMusic(true);
void voice.play().catch(() => {
if (this.voice === voice) {
this.voice = undefined;
this.duckMusic(false);
}
});
}
stopVoice(restoreMusic = true) {
if (!this.voice) {
return;
}
this.voice.pause();
this.voice.currentTime = 0;
this.voice = undefined;
if (restoreMusic) {
this.duckMusic(false);
}
this.fadeMusic(previous, next, this.musicVolume);
}
private playTone(
@@ -246,38 +164,6 @@ class SoundDirector {
}, 32);
}
private duckMusic(ducked: boolean) {
if (!this.music) {
return;
}
if (ducked && this.musicFadeTimer) {
window.clearInterval(this.musicFadeTimer);
this.musicFadeTimer = undefined;
}
if (this.musicRampTimer) {
window.clearInterval(this.musicRampTimer);
}
const from = this.music.volume;
const to = ducked ? this.duckedMusicVolume : this.musicVolume;
const duration = 260;
const startedAt = performance.now();
this.musicRampTimer = window.setInterval(() => {
if (!this.music) {
return;
}
const progress = Math.min(1, (performance.now() - startedAt) / duration);
this.music.volume = from + (to - from) * progress;
if (progress >= 1 && this.musicRampTimer) {
window.clearInterval(this.musicRampTimer);
this.musicRampTimer = undefined;
}
}, 32);
}
}
declare global {

View File

@@ -3,16 +3,6 @@ import militiaThemeUrl from '../../assets/audio/bgm/militia-theme.wav';
import oathThemeUrl from '../../assets/audio/bgm/oath-theme.wav';
import storyDarkUrl from '../../assets/audio/bgm/story-dark.wav';
import titleThemeUrl from '../../assets/audio/bgm/title-theme.wav';
import battleBriefingVoiceUrl from '../../assets/audio/voice/prologue/battle-briefing.wav';
import firstSortieVoiceUrl from '../../assets/audio/voice/prologue/first-sortie.wav';
import guanYuJoinsVoiceUrl from '../../assets/audio/voice/prologue/guan-yu-joins.wav';
import liuBeiResolveVoiceUrl from '../../assets/audio/voice/prologue/liu-bei-resolve.wav';
import militiaRisesVoiceUrl from '../../assets/audio/voice/prologue/militia-rises.wav';
import oathBrothersVoiceUrl from '../../assets/audio/voice/prologue/oath-brothers.wav';
import oathLiuBeiVoiceUrl from '../../assets/audio/voice/prologue/oath-liu-bei.wav';
import oathNarrationVoiceUrl from '../../assets/audio/voice/prologue/oath-narration.wav';
import yellowTurbanChaosVoiceUrl from '../../assets/audio/voice/prologue/yellow-turban-chaos.wav';
import zhangFeiJoinsVoiceUrl from '../../assets/audio/voice/prologue/zhang-fei-joins.wav';
export const musicTracks = {
'title-theme': titleThemeUrl,
@@ -22,18 +12,4 @@ export const musicTracks = {
'battle-prep': battlePrepUrl
} as const;
export const voiceTracks = {
'yellow-turban-chaos': yellowTurbanChaosVoiceUrl,
'liu-bei-resolve': liuBeiResolveVoiceUrl,
'zhang-fei-joins': zhangFeiJoinsVoiceUrl,
'guan-yu-joins': guanYuJoinsVoiceUrl,
'oath-narration': oathNarrationVoiceUrl,
'oath-liu-bei': oathLiuBeiVoiceUrl,
'oath-brothers': oathBrothersVoiceUrl,
'militia-rises': militiaRisesVoiceUrl,
'first-sortie': firstSortieVoiceUrl,
'battle-briefing': battleBriefingVoiceUrl
} as const;
export type MusicTrackKey = keyof typeof musicTracks;
export type VoiceTrackKey = keyof typeof voiceTracks;