105 lines
4.2 KiB
Go
105 lines
4.2 KiB
Go
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
|
||
}
|