Files

202 lines
4.2 KiB
Go

package silk
import (
"bytes"
"compress/gzip"
"compress/zlib"
"encoding/binary"
"fmt"
"io"
"github.com/rs/zerolog/log"
"github.com/sjzar/go-lame"
"github.com/sjzar/go-silk"
"github.com/sjzar/chatlog/pkg/util/zstd"
)
const (
decodedSampleRate = 24000
minSilkHeaderLength = 9
)
// Silk2PCM16 解码 Silk 数据,返回 16-bit PCM 采样数据及采样率。
func Silk2PCM16(data []byte) ([]int16, int, error) {
samples, rate, err := decodeSilk(data)
if err == nil {
return samples, rate, nil
}
log.Debug().Err(err).Msg("silk decode failed, retry with normalized payload")
normalized := normalizeSilkPayload(data)
if bytes.Equal(normalized, data) {
log.Error().Err(err).Msg("silk decode failed; payload unchanged after normalization")
return nil, 0, err
}
samples, rate, err = decodeSilk(normalized)
if err != nil {
log.Error().Err(err).Msg("silk decode failed after normalization")
}
return samples, rate, err
}
func decodeSilk(data []byte) ([]int16, int, error) {
payload, err := prepareSilkPayload(data)
if err != nil {
return nil, 0, err
}
sd := silk.SilkInit()
defer sd.Close()
pcmBytes := sd.Decode(payload)
if len(pcmBytes) == 0 {
return nil, 0, fmt.Errorf("silk decode failed")
}
if len(pcmBytes)%2 != 0 {
return nil, 0, fmt.Errorf("invalid pcm length: %d", len(pcmBytes))
}
samples := make([]int16, len(pcmBytes)/2)
for i := range samples {
samples[i] = int16(binary.LittleEndian.Uint16(pcmBytes[2*i:]))
}
return samples, decodedSampleRate, nil
}
var (
silkMagic = []byte("#!SILK")
zstdMagic = []byte{0x28, 0xb5, 0x2f, 0xfd}
)
func prepareSilkPayload(data []byte) ([]byte, error) {
trimmed := bytes.TrimLeft(data, "\x00\xff")
if len(trimmed) < minSilkHeaderLength {
return nil, fmt.Errorf("silk payload too short: %d bytes", len(trimmed))
}
if !bytes.HasPrefix(trimmed, silkMagic) {
return nil, fmt.Errorf("silk header missing")
}
return trimmed, nil
}
func normalizeSilkPayload(data []byte) []byte {
current := data
for i := 0; i < 3; i++ {
trimmed := bytes.TrimLeft(current, "\x00\xff")
if idx := bytes.Index(trimmed, silkMagic); idx >= 0 {
return trimmed[idx:]
}
if next, ok := tryDecompress(trimmed); ok {
current = next
continue
}
if !bytes.Equal(trimmed, current) {
current = trimmed
continue
}
break
}
return current
}
type decompressor struct {
name string
match func([]byte) bool
fn func([]byte) ([]byte, error)
}
var decompressors = []decompressor{
{
name: "zstd",
match: func(b []byte) bool {
return len(b) >= 4 && bytes.Equal(b[:4], zstdMagic)
},
fn: func(b []byte) ([]byte, error) {
return zstd.Decompress(b)
},
},
{
name: "gzip",
match: func(b []byte) bool {
return len(b) >= 2 && b[0] == 0x1f && b[1] == 0x8b
},
fn: func(b []byte) ([]byte, error) {
reader, err := gzip.NewReader(bytes.NewReader(b))
if err != nil {
return nil, err
}
defer reader.Close()
return io.ReadAll(reader)
},
},
{
name: "zlib",
match: func(b []byte) bool {
return len(b) >= 2 && b[0] == 0x78
},
fn: func(b []byte) ([]byte, error) {
reader, err := zlib.NewReader(bytes.NewReader(b))
if err != nil {
return nil, err
}
defer reader.Close()
return io.ReadAll(reader)
},
},
}
func tryDecompress(data []byte) ([]byte, bool) {
for _, dc := range decompressors {
if !dc.match(data) {
continue
}
out, err := dc.fn(data)
if err != nil {
log.Debug().Str("codec", dc.name).Err(err).Msg("silk payload decompress failed")
continue
}
log.Debug().Str("codec", dc.name).Msg("silk payload decompressed")
return out, true
}
return nil, false
}
func Silk2MP3(data []byte) ([]byte, error) {
sd := silk.SilkInit()
defer sd.Close()
pcmdata, _, err := Silk2PCM16(data)
if err != nil {
return nil, err
}
le := lame.Init()
defer le.Close()
le.SetInSamplerate(24000)
le.SetOutSamplerate(24000)
le.SetNumChannels(1)
le.SetBitrate(16)
// IMPORTANT!
le.InitParams()
// go-lame 期望的是小端 PCM 字节序列
pcmBytes := make([]byte, len(pcmdata)*2)
for i, sample := range pcmdata {
binary.LittleEndian.PutUint16(pcmBytes[i*2:], uint16(sample))
}
mp3data := le.Encode(pcmBytes)
if len(mp3data) == 0 {
return nil, fmt.Errorf("mp3 encode failed")
}
return mp3data, nil
}