Remove TTS narration and improve BGM
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: '의용군은 마을 어귀로 향했다. 첫 싸움은 곧 시작된다.'
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user