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