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

This commit is contained in:
AskaEth committed 2026-10-05 13:02:28 +08:00
1 parent 89715d1813
commit 4a2d276881
100 files changed
+11319

No files matched your search

+104
View File
@@ -0,0 +1,104 @@
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
}