package transcode import ( "encoding/binary" "fmt" "math" ) // WAV/RIFF 的 44 字节规范头(design/25 §0.5 登记的 WAV/RIFF 规范)。 // 四块:RIFF + WAVE + fmt + data。本期只发 PCM16 一种载荷,且**不引入音频库** // ——头是手写的,只用 encoding/binary。 // // 之所以目标容器选 WAV 而不是 mp3/ogg:WAV 是「只要解码器、不要编码器」的一条路 // (PCM + 44 字节头),而纯 Go 的 mp3 编码器生态远不如解码器成熟(design/25 §3.1)。 const ( // wavHeaderBytes 是规范头的字节数。 wavHeaderBytes = 44 // wavFormatPCM 是 fmt 块里的音频格式号:1 = PCM(线性量化)。 wavFormatPCM = 1 // wavBitsPerSample 是每样本位数:AMR-NB 解码出来就是 16 位定点。 wavBitsPerSample = 16 // wavFmtChunkBytes 是 fmt 子块的长度(PCM 的规范值 16)。 wavFmtChunkBytes = 16 // wavPCMBytesPerSample 是单个样本占的字节数。 wavPCMBytesPerSample = wavBitsPerSample / 8 // riffChunkSizeBase 是 RIFF 长度字段里除 data 之外的部分: // chunkSize = riffChunkSizeBase + dataBytes。 riffChunkSizeBase = wavHeaderBytes - 8 // 四块的标识与头内各字段的偏移(规范里定死的值,具名以免裸数字)。 wavRiffID = "RIFF" wavWaveID = "WAVE" wavFmtID = "fmt " wavDataID = "data" wavOffRiffID = 0 wavOffChunkSize = 4 wavOffWaveID = 8 wavOffFmtID = 12 wavOffFmtSize = 16 wavOffAudioFormat = 20 wavOffChannels = 22 wavOffSampleRate = 24 wavOffByteRate = 28 wavOffBlockAlign = 32 wavOffBitsPerSamp = 34 wavOffDataID = 36 wavOffDataSize = 40 ) // wavMaxDataBytes 是 RIFF 的 32 位长度字段能表达的 data 上限。超出即无法用 // 44 字节头如实描述 ⇒ ErrEncodeFailed(绝不写出一个长度撒谎的头)。 const wavMaxDataBytes = math.MaxUint32 - riffChunkSizeBase // wavHeader 装配一个 44 字节的 RIFF/WAVE 头,声明 dataBytes 字节的 PCM16 载荷。 func wavHeader(dataBytes, sampleRate, channels int) ([wavHeaderBytes]byte, error) { var hdr [wavHeaderBytes]byte if sampleRate <= 0 || channels <= 0 { return hdr, fmt.Errorf("transcode: wav header: %w: sample_rate=%d channels=%d", ErrEncodeFailed, sampleRate, channels) } if dataBytes < 0 || dataBytes > wavMaxDataBytes { return hdr, fmt.Errorf("transcode: wav header: %w: %d data bytes exceed the RIFF limit", ErrEncodeFailed, dataBytes) } blockAlign := channels * wavPCMBytesPerSample copy(hdr[wavOffRiffID:], wavRiffID) binary.LittleEndian.PutUint32(hdr[wavOffChunkSize:], uint32(riffChunkSizeBase+dataBytes)) copy(hdr[wavOffWaveID:], wavWaveID) copy(hdr[wavOffFmtID:], wavFmtID) binary.LittleEndian.PutUint32(hdr[wavOffFmtSize:], wavFmtChunkBytes) binary.LittleEndian.PutUint16(hdr[wavOffAudioFormat:], wavFormatPCM) binary.LittleEndian.PutUint16(hdr[wavOffChannels:], uint16(channels)) binary.LittleEndian.PutUint32(hdr[wavOffSampleRate:], uint32(sampleRate)) binary.LittleEndian.PutUint32(hdr[wavOffByteRate:], uint32(sampleRate*blockAlign)) binary.LittleEndian.PutUint16(hdr[wavOffBlockAlign:], uint16(blockAlign)) binary.LittleEndian.PutUint16(hdr[wavOffBitsPerSamp:], wavBitsPerSample) copy(hdr[wavOffDataID:], wavDataID) binary.LittleEndian.PutUint32(hdr[wavOffDataSize:], uint32(dataBytes)) return hdr, nil } // writePCM16LE 把样本按小端 PCM16 写进 dst(dst 必须够长:2 字节/样本)。 func writePCM16LE(dst []byte, samples []int16) { for i, s := range samples { binary.LittleEndian.PutUint16(dst[i*wavPCMBytesPerSample:], uint16(s)) } } // encodePCM16WAV 把样本装成一份完整 WAV(44 字节头 + PCM16 数据)。 // 主路径(codec_amr_nb.go)不用它:那条路直接写进目标缓冲,避免「先攒一份 PCM、 // 再复制一份 WAV」把峰值内存翻倍(design/25 §2.5 的瞬时上限)。 func encodePCM16WAV(samples []int16, sampleRate, channels int) ([]byte, error) { dataBytes := len(samples) * wavPCMBytesPerSample hdr, err := wavHeader(dataBytes, sampleRate, channels) if err != nil { return nil, err } out := make([]byte, wavHeaderBytes+dataBytes) copy(out, hdr[:]) writePCM16LE(out[wavHeaderBytes:], samples) return out, nil }