// 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 }