Join one FM-Funknetz talk group from a headset window.
The station reads ~/.funknetz.json, speaks protocol 2.0, and builds an unsigned funknetz.app. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+164
@@ -0,0 +1,164 @@
|
||||
// Package audio talks to the default input and output devices and presents
|
||||
// 16 kHz mono PCM to the station.
|
||||
package audio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"math"
|
||||
|
||||
"github.com/gen2brain/malgo"
|
||||
)
|
||||
|
||||
const stationRate = 16000
|
||||
|
||||
// Start opens the default input and output and converts them to and from
|
||||
// 16 kHz mono. It returns when the devices are running. They close when ctx ends.
|
||||
func Start(ctx context.Context, capture func([]int16), playback func() []int16) error {
|
||||
mctx, err := malgo.InitContext(nil, malgo.ContextConfig{}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg := malgo.DefaultDeviceConfig(malgo.Duplex)
|
||||
cfg.SampleRate = stationRate
|
||||
cfg.Capture.Format = malgo.FormatS16
|
||||
cfg.Capture.Channels = 1
|
||||
cfg.Playback.Format = malgo.FormatS16
|
||||
cfg.Playback.Channels = 1
|
||||
|
||||
var playRes, capRes converter
|
||||
var playBuf []int16
|
||||
callbacks := malgo.DeviceCallbacks{
|
||||
Data: func(output, input []byte, framecount uint32) {
|
||||
if len(input) > 0 {
|
||||
channels := 1
|
||||
if framecount > 0 {
|
||||
channels = len(input) / int(framecount) / 2
|
||||
}
|
||||
if channels < 1 {
|
||||
channels = 1
|
||||
}
|
||||
pcm := capRes.Convert(downmix(bytesToS16(input), channels))
|
||||
if len(pcm) > 0 {
|
||||
capture(pcm)
|
||||
}
|
||||
}
|
||||
if len(output) == 0 {
|
||||
return
|
||||
}
|
||||
channels := 1
|
||||
if framecount > 0 {
|
||||
channels = len(output) / int(framecount) / 2
|
||||
}
|
||||
if channels < 1 {
|
||||
channels = 1
|
||||
}
|
||||
playBuf = append(playBuf, playRes.Convert(playback())...)
|
||||
need := int(framecount)
|
||||
var frame []int16
|
||||
if len(playBuf) >= need {
|
||||
frame = playBuf[:need]
|
||||
rest := append([]int16(nil), playBuf[need:]...)
|
||||
playBuf = rest
|
||||
} else {
|
||||
frame = playBuf
|
||||
playBuf = nil
|
||||
}
|
||||
writeS16(output, frame, channels)
|
||||
},
|
||||
}
|
||||
dev, err := malgo.InitDevice(mctx.Context, cfg, callbacks)
|
||||
if err != nil {
|
||||
mctx.Free()
|
||||
return err
|
||||
}
|
||||
rate := int(dev.SampleRate())
|
||||
if rate == 0 {
|
||||
rate = stationRate
|
||||
}
|
||||
capRes = converter{from: rate, to: stationRate}
|
||||
playRes = converter{from: stationRate, to: rate}
|
||||
if err := dev.Start(); err != nil {
|
||||
dev.Uninit()
|
||||
mctx.Free()
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
dev.Stop()
|
||||
dev.Uninit()
|
||||
mctx.Free()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func bytesToS16(b []byte) []int16 {
|
||||
n := len(b) / 2
|
||||
out := make([]int16, n)
|
||||
for i := range out {
|
||||
out[i] = int16(binary.NativeEndian.Uint16(b[i*2:]))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func writeS16(dst []byte, samples []int16, channels int) {
|
||||
if channels < 1 {
|
||||
return
|
||||
}
|
||||
frames := len(dst) / (channels * 2)
|
||||
for i := 0; i < frames; i++ {
|
||||
var s int16
|
||||
if i < len(samples) {
|
||||
s = samples[i]
|
||||
}
|
||||
for ch := 0; ch < channels; ch++ {
|
||||
off := (i*channels + ch) * 2
|
||||
binary.NativeEndian.PutUint16(dst[off:], uint16(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downmix(samples []int16, channels int) []int16 {
|
||||
if channels <= 1 {
|
||||
return samples
|
||||
}
|
||||
n := len(samples) / channels
|
||||
out := make([]int16, n)
|
||||
for i := range out {
|
||||
var sum int
|
||||
for ch := 0; ch < channels; ch++ {
|
||||
sum += int(samples[i*channels+ch])
|
||||
}
|
||||
out[i] = int16(sum / channels)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type converter struct {
|
||||
from int
|
||||
to int
|
||||
pos float64
|
||||
buf []int16
|
||||
}
|
||||
|
||||
func (c *converter) Convert(in []int16) []int16 {
|
||||
if c.from == 0 || c.to == 0 || c.from == c.to {
|
||||
return in
|
||||
}
|
||||
c.buf = append(c.buf, in...)
|
||||
step := float64(c.from) / float64(c.to)
|
||||
var out []int16
|
||||
for c.pos+1 < float64(len(c.buf)) {
|
||||
i := int(c.pos)
|
||||
frac := c.pos - float64(i)
|
||||
sample := (1-frac)*float64(c.buf[i]) + frac*float64(c.buf[i+1])
|
||||
out = append(out, int16(math.Round(sample)))
|
||||
c.pos += step
|
||||
}
|
||||
drop := int(c.pos)
|
||||
if drop > 0 && drop <= len(c.buf) {
|
||||
c.buf = append([]int16(nil), c.buf[drop:]...)
|
||||
c.pos -= float64(drop)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user