Files
WechatExplorer/src/main/voice-service.ts
T
2026-08-05 09:45:59 +08:00

175 lines
5.0 KiB
TypeScript

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<string, string>()
private pcmCache = new Map<string, ResolvedPcmAudio>()
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<ResolvePcmResult> {
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<ResolveSourceResult> {
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])
}
}