sync: migrate ai-operator to Gitea (2026-08-10)
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
package audio
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
const muLawBias = 0x84
|
||||
const muLawClip = 32635
|
||||
|
||||
func EncodeULaw(pcm []byte) []byte {
|
||||
out := make([]byte, len(pcm)/2)
|
||||
for i := range out {
|
||||
out[i] = LinearToULaw(int16(binary.LittleEndian.Uint16(pcm[i*2:])))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func DecodeULaw(data []byte) []byte {
|
||||
out := make([]byte, len(data)*2)
|
||||
for i, sample := range data {
|
||||
binary.LittleEndian.PutUint16(out[i*2:], uint16(ULawToLinear(sample)))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func LinearToULaw(sample int16) byte {
|
||||
pcm := int(sample)
|
||||
sign := byte(0)
|
||||
if pcm < 0 {
|
||||
pcm = -pcm
|
||||
sign = 0x80
|
||||
}
|
||||
if pcm > muLawClip {
|
||||
pcm = muLawClip
|
||||
}
|
||||
pcm += muLawBias
|
||||
exponent := 7
|
||||
for mask := 0x4000; (pcm&mask) == 0 && exponent > 0; mask >>= 1 {
|
||||
exponent--
|
||||
}
|
||||
mantissa := (pcm >> (exponent + 3)) & 0x0f
|
||||
return ^(sign | byte(exponent<<4) | byte(mantissa))
|
||||
}
|
||||
|
||||
func ULawToLinear(sample byte) int16 {
|
||||
u := ^sample
|
||||
sign := u & 0x80
|
||||
exponent := (u >> 4) & 0x07
|
||||
mantissa := u & 0x0f
|
||||
pcm := ((int(mantissa) << 3) + muLawBias) << exponent
|
||||
pcm -= muLawBias
|
||||
if sign != 0 {
|
||||
pcm = -pcm
|
||||
}
|
||||
return int16(pcm)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
func IsPCM16Aligned(data []byte) bool { return len(data)%2 == 0 }
|
||||
func Base64Encode(data []byte) string { return base64.StdEncoding.EncodeToString(data) }
|
||||
func Base64Decode(s string) ([]byte, error) { return base64.StdEncoding.DecodeString(s) }
|
||||
func ChunkBytes(data []byte, max int) [][]byte {
|
||||
if max <= 0 || len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := [][]byte{}
|
||||
for len(data) > 0 {
|
||||
n := max
|
||||
if len(data) < n {
|
||||
n = len(data)
|
||||
}
|
||||
cp := append([]byte(nil), data[:n]...)
|
||||
out = append(out, cp)
|
||||
data = data[n:]
|
||||
}
|
||||
return out
|
||||
}
|
||||
func ChunkPCM16ByDuration(data []byte, rate int, dur time.Duration) [][]byte {
|
||||
if rate <= 0 || dur <= 0 {
|
||||
return nil
|
||||
}
|
||||
bytes := int(dur.Seconds()*float64(rate)) * 2
|
||||
if bytes < 2 {
|
||||
bytes = 2
|
||||
}
|
||||
return ChunkBytes(data, bytes)
|
||||
}
|
||||
func ResamplePCM16MonoLinear(data []byte, fromRate, toRate int) ([]byte, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if len(data)%2 != 0 {
|
||||
return nil, errors.New("pcm16 data is not 2-byte aligned")
|
||||
}
|
||||
if fromRate <= 0 || toRate <= 0 {
|
||||
return nil, errors.New("sample rates must be positive")
|
||||
}
|
||||
if fromRate == toRate {
|
||||
return append([]byte(nil), data...), nil
|
||||
}
|
||||
inN := len(data) / 2
|
||||
outN := int(math.Round(float64(inN) * float64(toRate) / float64(fromRate)))
|
||||
if outN < 1 {
|
||||
outN = 1
|
||||
}
|
||||
out := make([]byte, outN*2)
|
||||
for i := 0; i < outN; i++ {
|
||||
pos := float64(i) * float64(fromRate) / float64(toRate)
|
||||
idx := int(math.Floor(pos))
|
||||
frac := pos - float64(idx)
|
||||
if idx >= inN-1 {
|
||||
binary.LittleEndian.PutUint16(out[i*2:], binary.LittleEndian.Uint16(data[(inN-1)*2:]))
|
||||
continue
|
||||
}
|
||||
a := int16(binary.LittleEndian.Uint16(data[idx*2:]))
|
||||
b := int16(binary.LittleEndian.Uint16(data[(idx+1)*2:]))
|
||||
v := float64(a) + (float64(b)-float64(a))*frac
|
||||
if v > math.MaxInt16 {
|
||||
v = math.MaxInt16
|
||||
}
|
||||
if v < math.MinInt16 {
|
||||
v = math.MinInt16
|
||||
}
|
||||
binary.LittleEndian.PutUint16(out[i*2:], uint16(int16(math.Round(v))))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func pcm(samples int) []byte {
|
||||
b := make([]byte, samples*2)
|
||||
for i := 0; i < samples; i++ {
|
||||
binary.LittleEndian.PutUint16(b[i*2:], uint16(int16(i)))
|
||||
}
|
||||
return b
|
||||
}
|
||||
func TestPCMBase64ChunkResample(t *testing.T) {
|
||||
data := pcm(160)
|
||||
if !IsPCM16Aligned(data) {
|
||||
t.Fatal("aligned")
|
||||
}
|
||||
if IsPCM16Aligned([]byte{1}) {
|
||||
t.Fatal("misaligned")
|
||||
}
|
||||
enc := Base64Encode(data)
|
||||
dec, err := Base64Decode(enc)
|
||||
if err != nil || len(dec) != len(data) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
chunks := ChunkBytes(data, 100)
|
||||
if len(chunks) == 0 || len(chunks[0]) > 100 {
|
||||
t.Fatal("chunks")
|
||||
}
|
||||
dur := ChunkPCM16ByDuration(data, 16000, 10*time.Millisecond)
|
||||
if len(dur) != 1 {
|
||||
t.Fatal("duration chunks")
|
||||
}
|
||||
up, err := ResamplePCM16MonoLinear(pcm(16000), 16000, 24000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := len(up) / 2; got < 23990 || got > 24010 {
|
||||
t.Fatalf("up samples=%d", got)
|
||||
}
|
||||
down, err := ResamplePCM16MonoLinear(pcm(24000), 24000, 16000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := len(down) / 2; got < 15990 || got > 16010 {
|
||||
t.Fatalf("down samples=%d", got)
|
||||
}
|
||||
if _, err := ResamplePCM16MonoLinear([]byte{1}, 16000, 24000); err == nil {
|
||||
t.Fatal("want err")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user