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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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;

View File

@@ -1,5 +1,4 @@
export type PortraitKey = 'liuBei' | 'guanYu' | 'zhangFei';
export type VoiceRole = 'narrator' | PortraitKey;
export type StoryPage = {
id: string;
@@ -8,8 +7,6 @@ export type StoryPage = {
speaker?: string;
portrait?: PortraitKey;
bgm?: string;
voice?: string;
voiceRole?: VoiceRole;
text: string;
};
@@ -38,8 +35,6 @@ export const prologuePages: StoryPage[] = [
{
id: 'yellow-turban-chaos',
bgm: 'story-dark',
voice: 'yellow-turban-chaos',
voiceRole: 'narrator',
chapter: '난세의 시작',
background: 'story-chaos',
text: '중평 원년, 황건의 깃발이 각지에서 일어났다. 관군은 흩어지고, 마을마다 피난 행렬이 이어졌다.'
@@ -47,8 +42,6 @@ export const prologuePages: StoryPage[] = [
{
id: 'liu-bei-resolve',
bgm: 'story-dark',
voice: 'liu-bei-resolve',
voiceRole: 'liuBei',
chapter: '탁현의 방',
background: 'story-liu-bei',
speaker: '유비',
@@ -58,8 +51,6 @@ export const prologuePages: StoryPage[] = [
{
id: 'zhang-fei-joins',
bgm: 'story-dark',
voice: 'zhang-fei-joins',
voiceRole: 'zhangFei',
chapter: '뜻이 모이다',
background: 'story-three-heroes',
speaker: '장비',
@@ -69,8 +60,6 @@ export const prologuePages: StoryPage[] = [
{
id: 'guan-yu-joins',
bgm: 'story-dark',
voice: 'guan-yu-joins',
voiceRole: 'guanYu',
chapter: '뜻이 모이다',
background: 'story-three-heroes',
speaker: '관우',
@@ -80,8 +69,6 @@ export const prologuePages: StoryPage[] = [
{
id: 'oath-narration',
bgm: 'oath-theme',
voice: 'oath-narration',
voiceRole: 'narrator',
chapter: '도원결의',
background: 'title-taoyuan',
text: '복숭아꽃이 흩날리는 동산에서 세 사람은 하늘과 땅에 고했다.'
@@ -89,8 +76,6 @@ export const prologuePages: StoryPage[] = [
{
id: 'oath-liu-bei',
bgm: 'oath-theme',
voice: 'oath-liu-bei',
voiceRole: 'liuBei',
chapter: '도원결의',
background: 'title-taoyuan',
speaker: '유비',
@@ -100,8 +85,6 @@ export const prologuePages: StoryPage[] = [
{
id: 'oath-brothers',
bgm: 'oath-theme',
voice: 'oath-brothers',
voiceRole: 'guanYu',
chapter: '도원결의',
background: 'title-taoyuan',
speaker: '관우',
@@ -111,8 +94,6 @@ export const prologuePages: StoryPage[] = [
{
id: 'militia-rises',
bgm: 'militia-theme',
voice: 'militia-rises',
voiceRole: 'zhangFei',
chapter: '의용군 모집',
background: 'story-militia',
speaker: '장비',
@@ -122,8 +103,6 @@ export const prologuePages: StoryPage[] = [
{
id: 'first-sortie',
bgm: 'battle-prep',
voice: 'first-sortie',
voiceRole: 'liuBei',
chapter: '첫 출진',
background: 'story-sortie',
speaker: '유비',
@@ -133,8 +112,6 @@ export const prologuePages: StoryPage[] = [
{
id: 'battle-briefing',
bgm: 'battle-prep',
voice: 'battle-briefing',
voiceRole: 'narrator',
chapter: '황건적 토벌',
background: 'story-sortie',
text: '의용군은 마을 어귀로 향했다. 첫 싸움은 곧 시작된다.'

View File

@@ -25,7 +25,6 @@ export class BattleScene extends Phaser.Scene {
create() {
const { width, height } = this.scale;
soundDirector.stopVoice();
soundDirector.playMusic('battle-prep');
this.add.rectangle(0, 0, width, height, 0x0f1418).setOrigin(0);
this.drawMap();

View File

@@ -116,7 +116,6 @@ export class StoryScene extends Phaser.Scene {
private applyPage(page: StoryPage) {
soundDirector.playMusic(page.bgm);
soundDirector.playVoice(page.voice, page.voiceRole ?? page.portrait ?? 'narrator');
this.applyBackground(page.background);
this.chapterText?.setText(page.chapter);

View File

@@ -1,5 +1,5 @@
import Phaser from 'phaser';
import { musicTracks, voiceTracks } from './game/audio/audioAssets';
import { musicTracks } from './game/audio/audioAssets';
import { soundDirector } from './game/audio/SoundDirector';
import { BattleScene } from './game/scenes/BattleScene';
import { BootScene } from './game/scenes/BootScene';
@@ -27,7 +27,6 @@ const config: Phaser.Types.Core.GameConfig = {
};
soundDirector.registerMusicTracks(musicTracks);
soundDirector.registerVoiceTracks(voiceTracks);
const game = new Phaser.Game(config);