277 lines
8.0 KiB
TypeScript
277 lines
8.0 KiB
TypeScript
/**
|
|
* 对话音效角色类型
|
|
*/
|
|
export type DialogCharacterType =
|
|
| 'undertale' // 类似Undertale的方波音效
|
|
| 'zelda' // 三角波为基础的音效
|
|
| 'pokemon' // 高音调短促音效
|
|
| 'robot' // 机械音效
|
|
| 'pixie' // 精灵/仙女音效
|
|
| 'monster' // 怪物/低沉音效
|
|
| 'custom'; // 自定义音效
|
|
|
|
/**
|
|
* 音效配置接口
|
|
*/
|
|
export interface DialogSoundConfig {
|
|
oscillatorType: OscillatorType; // 振荡器类型
|
|
baseFrequency: number; // 基础频率
|
|
frequencyVariation: number; // 频率变化范围
|
|
gain: number; // 音量
|
|
attackTime: number; // 起音时间
|
|
releaseTime: number; // 释音时间
|
|
duration: number; // 持续时间
|
|
highPassFrequency?: number; // 高通滤波器频率
|
|
lowPassFrequency?: number; // 低通滤波器频率
|
|
distortion?: number; // 失真度
|
|
detune?: number; // 音高微调
|
|
}
|
|
|
|
/**
|
|
* 为每种角色类型预设的音效配置
|
|
*/
|
|
export const CHARACTER_SOUND_CONFIGS: Record<Exclude<DialogCharacterType, 'custom'>, DialogSoundConfig> = {
|
|
// Undertale风格 - 清脆的方波
|
|
undertale: {
|
|
oscillatorType: 'square',
|
|
baseFrequency: 380,
|
|
frequencyVariation: 10,
|
|
gain: 0.05,
|
|
attackTime: 0.01,
|
|
releaseTime: 0.02,
|
|
duration: 0.05,
|
|
highPassFrequency: 700
|
|
},
|
|
|
|
// 塞尔达风格 - 三角波
|
|
zelda: {
|
|
oscillatorType: 'triangle',
|
|
baseFrequency: 300,
|
|
frequencyVariation: 8,
|
|
gain: 0.08,
|
|
attackTime: 0.01,
|
|
releaseTime: 0.08,
|
|
duration: 0.12,
|
|
highPassFrequency: 400
|
|
},
|
|
|
|
// 宝可梦风格 - 高音调短促
|
|
pokemon: {
|
|
oscillatorType: 'sine',
|
|
baseFrequency: 850,
|
|
frequencyVariation: 5,
|
|
gain: 0.06,
|
|
attackTime: 0.005,
|
|
releaseTime: 0.015,
|
|
duration: 0.03
|
|
},
|
|
|
|
// 机器人风格 - 方波带失真
|
|
robot: {
|
|
oscillatorType: 'square',
|
|
baseFrequency: 200,
|
|
frequencyVariation: 12,
|
|
gain: 0.04,
|
|
attackTime: 0.005,
|
|
releaseTime: 0.03,
|
|
duration: 0.07,
|
|
distortion: 15,
|
|
detune: 5
|
|
},
|
|
|
|
// 精灵/仙女风格 - 高音调正弦波
|
|
pixie: {
|
|
oscillatorType: 'sine',
|
|
baseFrequency: 1200,
|
|
frequencyVariation: 15,
|
|
gain: 0.03,
|
|
attackTime: 0.005,
|
|
releaseTime: 0.1,
|
|
duration: 0.15,
|
|
highPassFrequency: 900
|
|
},
|
|
|
|
// 怪物/低沉风格 - 低音锯齿波
|
|
monster: {
|
|
oscillatorType: 'sawtooth',
|
|
baseFrequency: 150,
|
|
frequencyVariation: 6,
|
|
gain: 0.07,
|
|
attackTime: 0.02,
|
|
releaseTime: 0.05,
|
|
duration: 0.1,
|
|
lowPassFrequency: 300,
|
|
distortion: 8
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 用于存储AudioContext单例
|
|
*/
|
|
let audioContextInstance: AudioContext | null = null;
|
|
|
|
/**
|
|
* 获取或创建AudioContext
|
|
*/
|
|
export function getAudioContext(): AudioContext {
|
|
if (!audioContextInstance) {
|
|
audioContextInstance = new (window.AudioContext || (window as any).webkitAudioContext)();
|
|
}
|
|
return audioContextInstance;
|
|
}
|
|
|
|
/**
|
|
* 销毁AudioContext
|
|
*/
|
|
export function destroyAudioContext(): void {
|
|
if (audioContextInstance && audioContextInstance.state !== 'closed') {
|
|
audioContextInstance.close();
|
|
audioContextInstance = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 创建失真效果
|
|
*/
|
|
function createDistortionEffect(
|
|
audioContext: AudioContext,
|
|
distortionAmount: number
|
|
): WaveShaperNode {
|
|
const waveShaperNode = audioContext.createWaveShaper();
|
|
|
|
// 创建失真曲线
|
|
function makeDistortionCurve(amount: number): Float32Array {
|
|
const k = amount;
|
|
const samples = 44100;
|
|
const curve = new Float32Array(samples);
|
|
|
|
for (let i = 0; i < samples; ++i) {
|
|
const x = (i * 2) / samples - 1;
|
|
curve[i] = ((3 + k) * x * 0.5 * (1 - Math.abs(x))) / (3 + k * Math.abs(x));
|
|
}
|
|
|
|
return curve;
|
|
}
|
|
|
|
waveShaperNode.curve = makeDistortionCurve(distortionAmount);
|
|
return waveShaperNode;
|
|
}
|
|
|
|
/**
|
|
* 播放单个字符的对话音效
|
|
* @param char 要为其播放音效的字符
|
|
* @param characterType 角色类型
|
|
* @param customConfig 可选的自定义配置
|
|
* @returns 音效的持续时间(秒)
|
|
*/
|
|
export function playDialogSound(
|
|
char: string,
|
|
characterType: DialogCharacterType | number,
|
|
customConfig?: Partial<DialogSoundConfig>
|
|
): number {
|
|
// 空格不播放音效
|
|
if (char === ' ') {
|
|
return 0;
|
|
}
|
|
|
|
// 获取音频上下文
|
|
const audioContext = getAudioContext();
|
|
const now = audioContext.currentTime;
|
|
|
|
// 获取基础配置
|
|
let config: DialogSoundConfig;
|
|
|
|
if (characterType === 'custom' && customConfig) {
|
|
// 使用自定义配置
|
|
config = {
|
|
oscillatorType: customConfig.oscillatorType || 'square',
|
|
baseFrequency: customConfig.baseFrequency || 400,
|
|
frequencyVariation: customConfig.frequencyVariation || 10,
|
|
gain: customConfig.gain || 0.05,
|
|
attackTime: customConfig.attackTime || 0.01,
|
|
releaseTime: customConfig.releaseTime || 0.05,
|
|
duration: customConfig.duration || 0.08,
|
|
...customConfig
|
|
};
|
|
} else {
|
|
// 使用预设配置
|
|
if (typeof characterType === 'number') {
|
|
config = Object.values(CHARACTER_SOUND_CONFIGS)[characterType] || CHARACTER_SOUND_CONFIGS.undertale;
|
|
} else {
|
|
config = { ...CHARACTER_SOUND_CONFIGS[characterType as Exclude<DialogCharacterType, 'custom'>] };
|
|
}
|
|
// 应用自定义配置覆盖(如果有)
|
|
if (customConfig) {
|
|
config = { ...config, ...customConfig };
|
|
}
|
|
}
|
|
|
|
// 创建音频节点
|
|
const oscillator = audioContext.createOscillator();
|
|
const gainNode = audioContext.createGain();
|
|
|
|
// 设置振荡器
|
|
oscillator.type = config.oscillatorType;
|
|
|
|
// 基于字符计算频率变化
|
|
const charCode = char.charCodeAt(0);
|
|
const frequencyOffset = (charCode % config.frequencyVariation) / config.frequencyVariation;
|
|
|
|
// 设置频率和可选的失谐
|
|
oscillator.frequency.value = config.baseFrequency + (config.baseFrequency * 0.2 * frequencyOffset);
|
|
|
|
if (config.detune) {
|
|
// 添加轻微的失谐,使声音更有特色
|
|
oscillator.detune.value = (charCode % 10) * config.detune;
|
|
}
|
|
|
|
// 连接音频节点链
|
|
let currentNode: AudioNode = oscillator;
|
|
|
|
// 连接到增益节点
|
|
currentNode.connect(gainNode);
|
|
currentNode = gainNode;
|
|
|
|
// 添加失真效果(如果指定)
|
|
if (config.distortion && config.distortion > 0) {
|
|
const distortionNode = createDistortionEffect(audioContext, config.distortion);
|
|
currentNode.connect(distortionNode);
|
|
currentNode = distortionNode;
|
|
}
|
|
|
|
// 添加低通滤波器(如果指定)
|
|
if (config.lowPassFrequency) {
|
|
const lowPassFilter = audioContext.createBiquadFilter();
|
|
lowPassFilter.type = 'lowpass';
|
|
lowPassFilter.frequency.value = config.lowPassFrequency;
|
|
lowPassFilter.Q.value = 1;
|
|
|
|
currentNode.connect(lowPassFilter);
|
|
currentNode = lowPassFilter;
|
|
}
|
|
|
|
// 添加高通滤波器(如果指定)
|
|
if (config.highPassFrequency) {
|
|
const highPassFilter = audioContext.createBiquadFilter();
|
|
highPassFilter.type = 'highpass';
|
|
highPassFilter.frequency.value = config.highPassFrequency;
|
|
highPassFilter.Q.value = 1;
|
|
|
|
currentNode.connect(highPassFilter);
|
|
currentNode = highPassFilter;
|
|
}
|
|
|
|
// 连接到输出
|
|
currentNode.connect(audioContext.destination);
|
|
|
|
// 设置音量包络
|
|
gainNode.gain.setValueAtTime(0, now);
|
|
gainNode.gain.linearRampToValueAtTime(config.gain, now + config.attackTime);
|
|
gainNode.gain.linearRampToValueAtTime(0, now + config.duration);
|
|
|
|
// 播放音效
|
|
oscillator.start(now);
|
|
oscillator.stop(now + config.duration);
|
|
|
|
return config.duration;
|
|
} |