import { createHash } from 'crypto' import { Wcdb4Client } from './wcdb4-client' import { createDefaultAudioDecoderRegistry, type EncodedVoiceSource } from './voice-pipeline/audio-decoder' export { findSilkWasmRuntimeLocation, getSilkWasmRuntimeLocations, type SilkWasmRuntimeLocation } from './voice-pipeline/audio-decoder' export interface ResolvedPcmAudio { pcm: Buffer sampleRate: number channels: number codec: 'silk' sourceHash: string } export type ResolvePcmResult = | { success: true; audio: ResolvedPcmAudio } | { success: false; error: string } export type ResolveSourceResult = | { success: true; source: EncodedVoiceSource } | { success: false; error: string } export class VoiceService { private wcdb4Client: Wcdb4Client private voiceCache = new Map() private pcmCache = new Map() private readonly decoderRegistry = createDefaultAudioDecoderRegistry() constructor(wcdb4Client: Wcdb4Client) { this.wcdb4Client = wcdb4Client } async resolveVoice( sessionId: string, localId: number, createTime: number, svrId?: string | number ): Promise<{ success: boolean; data?: string; error?: string }> { const cacheKey = this.buildCacheKey(sessionId, localId, createTime) const cached = this.voiceCache.get(cacheKey) if (cached) { console.log('[VoiceService] cache hit for', cacheKey) return { success: true, data: cached } } const pcmResult = await this.resolvePcm(sessionId, localId, createTime, svrId) if (!pcmResult.success) return pcmResult const pcmData = pcmResult.audio.pcm const wavData = this.createWavBuffer(pcmData, 24000) console.log( '[VoiceService] wavData length:', wavData.length, 'base64 length:', wavData.toString('base64').length ) const base64Data = wavData.toString('base64') this.voiceCache.set(cacheKey, base64Data) return { success: true, data: base64Data } } async resolvePcm( sessionId: string, localId: number, createTime: number, svrId?: string | number ): Promise { const cacheKey = this.buildCacheKey(sessionId, localId, createTime) const cached = this.pcmCache.get(cacheKey) if (cached) return { success: true, audio: cached } const sourceResult = await this.resolveSource(sessionId, localId, createTime, svrId) if (!sourceResult.success) return sourceResult try { const decoded = await this.decoderRegistry.decode(sourceResult.source) const audio: ResolvedPcmAudio = { ...decoded, codec: 'silk' } this.pcmCache.set(cacheKey, audio) return { success: true, audio } } catch (error) { console.error('[VoiceService] audio decode failed:', error) return { success: false, error: error instanceof Error ? error.message : 'Silk 解码失败' } } } async resolveSource( sessionId: string, localId: number, createTime: number, svrId?: string | number ): Promise { const candidates = this.buildCandidates(sessionId) const voiceResult = await this.wcdb4Client.getVoiceData( sessionId, createTime, candidates, localId, svrId || 0 ) if (!voiceResult.success || !voiceResult.hex) { return { success: false, error: voiceResult.error || '获取语音数据失败' } } const silkData = this.decodeVoiceBlob(voiceResult.hex) if (!silkData?.length) return { success: false, error: '语音数据为空' } return { success: true, source: { data: silkData, codec: 'silk', sourceHash: createHash('sha256').update(silkData).digest('hex') } } } private buildCacheKey(sessionId: string, localId: number, createTime: number): string { return `${sessionId}-${localId}-${createTime}` } private buildCandidates(sessionId: string): string[] { const candidates: string[] = [sessionId] if (sessionId.endsWith('@chatroom')) { candidates.push(sessionId.replace('@chatroom', '')) } return candidates } private decodeVoiceBlob(hex: string): Buffer | null { try { const hexClean = hex.replace(/\s+/g, '') if (!/^[0-9a-fA-F]+$/.test(hexClean)) { return null } return Buffer.from(hexClean, 'hex') } catch { return null } } private createWavBuffer( pcmData: Buffer, sampleRate: number = 24000, channels: number = 1 ): Buffer { const pcmLength = pcmData.length const header = Buffer.alloc(44) header.write('RIFF', 0) header.writeUInt32LE(36 + pcmLength, 4) header.write('WAVE', 8) header.write('fmt ', 12) header.writeUInt32LE(16, 16) header.writeUInt16LE(1, 20) header.writeUInt16LE(channels, 22) header.writeUInt32LE(sampleRate, 24) header.writeUInt32LE(sampleRate * channels * 2, 28) header.writeUInt16LE(channels * 2, 32) header.writeUInt16LE(16, 34) header.write('data', 36) header.writeUInt32LE(pcmLength, 40) return Buffer.concat([header, pcmData]) } }