背景
作为一个前端开发者,最近在做一个游戏编辑器项目时遇到了一个有意思的需求:让用户可以在编辑器中直接生成游戏音效和影视音效,不需要切换到外部工具、下载、再导入。
这让我开始研究如何在前端接入 AI 音效生成能力,以及用 Web Audio API 做生成后的实时预览和处理。
这篇文章记录我踩过的坑和最终落地的方案。
需求拆解
用户场景很简单:在编辑器中选中一个"按钮组件" → 点击"生成音效" → 输入描述"科幻风格按钮点击音效" → 生成多个版本 → 试听 → 选择 → 自动绑定到组件。
拆成技术点:
- 调用 AI 音效生成 API,获取生成的音频文件
- 用 Web Audio API 加载、解码、播放
- 支持多版本切换试听
- 支持简单的音频处理(裁剪、淡入淡出、音量调整)
- 最终选定的音效绑定到对应的游戏对象
技术实现
1. API 调用层
对接 AI 音效生成服务的 API。以爱声音坊(aisounds.cn)为例,核心流程:
interface SoundGenRequest {
prompt: string;
duration: number;
loop: boolean;
category?: 'ui' | 'action' | 'ambient' | 'transition';
}
interface SoundGenResponse {
variations: {
id: string;
url: string;
duration: number;
waveform: number[]; // 可选:用于波形预览
}[];
}
class AISoundGenerator {
private baseUrl: string;
constructor(baseUrl = 'https://aisounds.cn/api') {
this.baseUrl = baseUrl;
}
async generate(params: SoundGenRequest): Promise<SoundGenResponse> {
const response = await fetch(`${this.baseUrl}/sounds/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...params,
num_variations: 5 // 一次生成多个版本
})
});
if (!response.ok) {
throw new Error(`Sound generation failed: ${response.statusText}`);
}
return response.json();
}
}
2. Web Audio API 播放层
拿到生成的音频 URL 后,用 Web Audio API 做加载和播放:
class SoundPlayer {
private audioContext: AudioContext;
private currentSource: AudioBufferSourceNode | null = null;
private gainNode: GainNode;
constructor() {
this.audioContext = new AudioContext();
this.gainNode = this.audioContext.createGain();
this.gainNode.connect(this.audioContext.destination);
}
async loadAudio(url: string): Promise<AudioBuffer> {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
return this.audioContext.decodeAudioData(arrayBuffer);
}
play(buffer: AudioBuffer, options?: {
loop?: boolean;
offset?: number;
duration?: number;
fadeIn?: number;
fadeOut?: number;
}) {
// 停止当前播放
this.stop();
const source = this.audioContext.createBufferSource();
source.buffer = buffer;
source.loop = options?.loop ?? false;
// 连接:source → gain → destination
source.connect(this.gainNode);
// 淡入处理
if (options?.fadeIn && options.fadeIn > 0) {
this.gainNode.gain.setValueAtTime(0, this.audioContext.currentTime);
this.gainNode.gain.linearRampToValueAtTime(
1.0,
this.audioContext.currentTime + options.fadeIn
);
} else {
this.gainNode.gain.setValueAtTime(1.0, this.audioContext.currentTime);
}
// 淡出处理
if (options?.fadeOut && options?.duration) {
const fadeOutStart = this.audioContext.currentTime
+ (options.duration ?? buffer.duration)
- options.fadeOut;
this.gainNode.gain.setValueAtTime(1.0, fadeOutStart);
this.gainNode.gain.linearRampToValueAtTime(
0,
fadeOutStart + options.fadeOut
);
}
source.start(0, options?.offset ?? 0, options?.duration);
this.currentSource = source;
}
stop() {
if (this.currentSource) {
try { this.currentSource.stop(); } catch (e) { /* 已经停止 */ }
this.currentSource = null;
}
}
setVolume(value: number) {
this.gainNode.gain.setValueAtTime(
Math.max(0, Math.min(1, value)),
this.audioContext.currentTime
);
}
getCurrentTime(): number {
return this.audioContext.currentTime;
}
}
3. 多版本切换与波形预览
用户需要快速对比多个生成版本。我做了两个交互优化:
快速切换试听:预加载所有版本的 AudioBuffer,切换时直接播放(0 延迟)
class VariationManager {
private player: SoundPlayer;
private buffers: Map<string, AudioBuffer> = new Map();
private currentVariationId: string | null = null;
constructor(player: SoundPlayer) {
this.player = player;
}
async preloadVariations(variations: SoundGenResponse['variations']) {
const results = await Promise.all(
variations.map(async (v) => {
const buffer = await this.player.loadAudio(v.url);
return { id: v.id, buffer };
})
);
this.buffers.clear();
results.forEach(r => this.buffers.set(r.id, r.buffer));
}
preview(id: string, loop = false) {
const buffer = this.buffers.get(id);
if (!buffer) return;
this.currentVariationId = id;
this.player.play(buffer, { loop });
}
stop() {
this.player.stop();
}
switchTo(id: string) {
this.stop();
this.preview(id);
}
}
简易波形预览:如果 API 返回波形数据,可以用 Canvas 快速绘制:
function drawWaveform(
canvas: HTMLCanvasElement,
waveform: number[],
color = '#4A90D9'
) {
const ctx = canvas.getContext('2d')!;
const { width, height } = canvas;
ctx.clearRect(0, 0, width, height);
// 波形镜像绘制
const centerY = height / 2;
const step = waveform.length / width;
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = 1;
for (let x = 0; x < width; x++) {
const start = Math.floor(x * step);
const end = Math.floor((x + 1) * step);
let max = 0;
for (let i = start; i < end; i++) {
max = Math.max(max, Math.abs(waveform[i] ?? 0));
}
const y = max * centerY;
ctx.moveTo(x, centerY - y);
ctx.lineTo(x, centerY + y);
}
ctx.stroke();
}
4. 提示词模板系统
从用户体验角度,我设计了一套提示词快速填充方案。不同类别的音效有不同的描述模板:
const PROMPT_TEMPLATES: Record<string, { label: string; template: string }[]> = {
game_ui: [
{ label: '科幻按钮', template: '科幻风格按钮点击音效,{mood},{duration}秒' },
{ label: '魔法按钮', template: '魔法风格按钮点击音效,{mood},{duration}秒' },
{ label: '机械按钮', template: '机械金属按钮按下音效,{mood},{duration}秒' },
],
game_action: [
{ label: '剑击', template: '剑类武器{action}音效,{mood},{duration}秒' },
{ label: '魔法', template: '{element}元素魔法{action}音效,{mood},{duration}秒' },
{ label: '枪械', template: '{type}射击音效,{mood},{duration}秒' },
],
film_transition: [
{ label: 'Whoosh', template: '电影级whoosh转场音效,{intensity},{duration}秒' },
{ label: 'Impact', template: '影视impact冲击音效,{intensity},{duration}秒' },
{ label: 'Riser', template: '悬疑riser渐强音效,{mood},{duration}秒' },
],
film_ambient: [
{ label: '城市', template: '{time}城市{weather}环境氛围音效,{mood},{duration}秒循环' },
{ label: '自然', template: '{location}自然环境氛围音效,{mood},{duration}秒循环' },
{ label: '室内', template: '{room_type}室内环境音效,{mood},{duration}秒循环' },
],
};
实际效果
结合以上代码,最终实现的交互流程:
- 用户在编辑器中选中游戏对象/视频片段
- 打开音效生成面板,选择类别和模板
- 填写或修改提示词
- 点击生成,3-10 秒内拿到 5 个变体
- 快速切换试听,选定最佳版本
- 音效自动绑定并保存
整个流程从"选中对象"到"音效绑定"大约 20-30 秒,而传统流程(找素材网站 → 搜索 → 下载 → 导入 → 绑定)至少需要 5-10 分钟。
一些坑和注意事项
AudioContext 的浏览器限制
Chrome/Safari 要求 AudioContext 必须在用户手势内创建或恢复。解决方案:
// 全局单例 + 懒激活
let audioContext: AudioContext | null = null;
async function getAudioContext(): Promise<AudioContext> {
if (!audioContext) {
audioContext = new AudioContext();
}
if (audioContext.state === 'suspended') {
await audioContext.resume();
}
return audioContext;
}
// 在首次用户点击时激活
document.addEventListener('click', () => {
getAudioContext();
}, { once: true });
音频文件缓存
API 返回的音效 URL 可能有访问期限。如果需要本地持久化,建议在用户选择后立即下载并缓存到 IndexedDB:
async function cacheAudio(id: string, url: string): Promise<void> {
const response = await fetch(url);
const blob = await response.blob();
const db = await openDB('sound-cache', 1, {
upgrade(db) {
db.createObjectStore('sounds', { keyPath: 'id' });
}
});
await db.put('sounds', { id, blob, timestamp: Date.now() });
}
生成延迟的用户体验
AI 生成需要 3-15 秒,这个等待时间需要用 UI 反馈来填补。波形的骨架屏动画(skeleton loading)效果不错——预显示一个灰色波形区域,生成完成后填充真实波形。
结语
前端接入 AI 音效生成并不是特别复杂的事。核心就是三块:API 调用 → Web Audio 播放 → UI 交互优化。但要做好体验(快速切换、波形预览、缓存、模板系统),细节处确实需要花点心思。
我目前做的这个方案已经用在游戏编辑器项目里,反馈还不错。特别是独立游戏开发者,以前做一个 UI 按钮的音效要切换到素材网站搜半天,现在直接在编辑器里输入几个字就能生成,效率提升是肉眼可见的。
如果有读者也在做类似的音频工具或游戏编辑器,可以交流一下思路。
相关资源:
- AI 音效生成服务:aisounds.cn/sounds/generate
- 提示词模板参考:aisounds.cn/prompts
- MDN Web Audio API:developer.mozilla.org/en-US/docs/…
- GitHub:github.com/liushafeiniao/aiwave