Files
AskaEth 4a2d276881
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(capability): 转码能力包(vendor 纯 Go AMR-NB 解码器 + 黄金向量逐样本对照 + 8 哨兵 + 注册面)
2026-10-05 13:02:28 +08:00

105 lines
4.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}