kappanhang/audio-linux.go
Birk da Yooper 7656d4335b Added flag to set alternate controller address
Added flag to show CI-V packet contents to support debugging

At start-up show virtual soundcard info by both name & filesystem location
Updated to always present status lines at bottom of the terminal
Removed milliseconds from time shown on status lines
Reformatted status line showing packet activity to be easier to follow
Adjusted default status update frequency to 150ms to be easier to observe

Restored commented out code for working with single VFO

Added multiple functions to handle BCD <-> human readable values to improve easy of maintenance
Added function to prepare CI-V packets, abstracting this step so that each function no longer
  needs to create entire packet. This makes the code much easier to follow and maintaint.
  This will also greatly ease effort needed for extending radio features supported

Added/adjusted various comments

Updated some variable names to be less terse to facilitate maintenance
  eg "incTS" to "incTuningStep"

Some adjustments to improve readability/ease maintenance in favor of unnecessary optimizations
  EG - using formatted prints instead of prints using concatenations
       using strings.Repeat instead of loop to build space buffers
       abstracted control char strings to variables

Hooks for future functionality

Removed some commented out code blocks
   "Trust the repo, Luke!"
2024-02-24 20:53:49 -05:00

502 lines
14 KiB
Go

//go:build linux
// +build linux
package main
import (
"bytes"
"errors"
"io"
"os"
"sync"
"time"
"github.com/akosmarton/papipes"
"github.com/mesilliac/pulse-simple"
)
const audioSampleRate = 48000
const audioSampleBytes = 2
const pulseAudioBufferLength = 100 * time.Millisecond
const audioFrameLength = 20 * time.Millisecond
const audioFrameSize = int((audioSampleRate * audioSampleBytes * audioFrameLength) / time.Second)
const maxPlayBufferSize = audioFrameSize*5 + int((audioSampleRate*audioSampleBytes*audioRxSeqBufLength)/time.Second)
type audioStruct struct {
devName string
deinitNeededChan chan bool
deinitFinishedChan chan bool
// Send to this channel to play audio.
play chan []byte
// Read from this channel for audio.
rec chan []byte
virtualSoundcardStream struct {
source papipes.Source
sink papipes.Sink
mutex sync.Mutex
playBuf *bytes.Buffer
canPlay chan bool
}
defaultSoundcardStream struct {
togglePlaybackChan chan bool
playStream *pulse.Stream
recStream *pulse.Stream
recLoopDeinitNeededChan chan bool
recLoopDeinitFinishedChan chan bool
mutex sync.Mutex
playBuf *bytes.Buffer
canPlay chan bool
}
}
var audio audioStruct
func (a *audioStruct) defaultSoundCardPlayStreamDeinit() {
_ = a.defaultSoundcardStream.playStream.Drain()
a.defaultSoundcardStream.playStream.Free()
a.defaultSoundcardStream.playStream = nil
}
func (a *audioStruct) defaultSoundCardRecStreamDeinit() {
if a.defaultSoundcardStream.recStream == nil {
return
}
a.defaultSoundcardStream.recLoopDeinitNeededChan <- true
<-a.defaultSoundcardStream.recLoopDeinitFinishedChan
a.defaultSoundcardStream.recStream.Free()
a.defaultSoundcardStream.recStream = nil
}
func (a *audioStruct) togglePlaybackToDefaultSoundcard() {
if a.defaultSoundcardStream.togglePlaybackChan == nil {
return
}
// Non-blocking send to channel.
select {
case a.defaultSoundcardStream.togglePlaybackChan <- true:
default:
}
}
func (a *audioStruct) toggleRecFromDefaultSoundcard() {
if a.defaultSoundcardStream.recStream == nil {
ss := pulse.SampleSpec{Format: pulse.SAMPLE_S16LE, Rate: audioSampleRate, Channels: 1}
battr := pulse.NewBufferAttr()
battr.Fragsize = uint32(audioFrameSize)
var err error
a.defaultSoundcardStream.recStream, err = pulse.NewStream("", "kappanhang", pulse.STREAM_RECORD, "", a.devName,
&ss, nil, battr)
if err == nil {
a.defaultSoundcardStream.recLoopDeinitNeededChan = make(chan bool)
a.defaultSoundcardStream.recLoopDeinitFinishedChan = make(chan bool)
go a.recLoopFromDefaultSoundcard()
log.Print("turned on audio rec")
statusLog.reportAudioRec(true)
if setDataModeOnTx {
if err := civControl.setDataMode(true); err != nil {
log.Error("can't enable data mode: ", err)
}
}
if err := civControl.setPTT(true); err != nil {
log.Error("can't turn on ptt: ", err)
}
} else {
log.Error("can't turn on rec: ", err)
a.defaultSoundcardStream.recStream = nil
}
} else {
a.defaultSoundCardRecStreamDeinit()
statusLog.reportAudioRec(false)
log.Print("turned off audio rec")
if err := civControl.setPTT(false); err != nil {
log.Error("can't turn off ptt: ", err)
}
}
}
func (a *audioStruct) doTogglePlaybackToDefaultSoundcard() {
if a.defaultSoundcardStream.playStream == nil {
log.Print("turned on audio playback")
statusLog.reportAudioMon(true)
ss := pulse.SampleSpec{Format: pulse.SAMPLE_S16LE, Rate: audioSampleRate, Channels: 1}
a.defaultSoundcardStream.playStream, _ = pulse.Playback("kappanhang", a.devName, &ss)
} else {
a.defaultSoundCardPlayStreamDeinit()
log.Print("turned off audio playback")
statusLog.reportAudioMon(false)
}
}
func (a *audioStruct) playLoopToDefaultSoundcard(deinitNeededChan, deinitFinishedChan chan bool) {
for {
select {
case <-a.defaultSoundcardStream.canPlay:
case <-a.defaultSoundcardStream.togglePlaybackChan:
a.doTogglePlaybackToDefaultSoundcard()
case <-deinitNeededChan:
deinitFinishedChan <- true
return
}
for {
a.defaultSoundcardStream.mutex.Lock()
if a.defaultSoundcardStream.playBuf.Len() < audioFrameSize {
a.defaultSoundcardStream.mutex.Unlock()
break
}
d := make([]byte, audioFrameSize)
bytesToWrite, err := a.defaultSoundcardStream.playBuf.Read(d)
a.defaultSoundcardStream.mutex.Unlock()
if err != nil {
log.Error(err)
break
}
if bytesToWrite != len(d) {
log.Error("buffer underread")
break
}
for len(d) > 0 && a.defaultSoundcardStream.playStream != nil {
written, err := a.defaultSoundcardStream.playStream.Write(d)
if err != nil {
if _, ok := err.(*os.PathError); !ok {
reportError(err)
}
break
}
d = d[written:]
}
}
}
}
func (a *audioStruct) recLoopFromDefaultSoundcard() {
defer func() {
a.defaultSoundcardStream.recLoopDeinitFinishedChan <- true
}()
frameBuf := make([]byte, audioFrameSize)
buf := bytes.NewBuffer([]byte{})
for {
select {
case <-a.defaultSoundcardStream.recLoopDeinitNeededChan:
return
default:
}
n, err := a.defaultSoundcardStream.recStream.Read(frameBuf)
if err != nil {
if _, ok := err.(*os.PathError); !ok {
reportError(err)
}
}
// Do not send silence frames to the radio unnecessarily
if isAllZero(frameBuf[:n]) {
continue
}
buf.Write(frameBuf[:n])
for buf.Len() >= len(frameBuf) {
// We need to create a new []byte slice for each chunk to be able to send it through the rec chan.
b := make([]byte, len(frameBuf))
n, err = buf.Read(b)
if err != nil {
reportError(err)
}
if n != len(frameBuf) {
reportError(errors.New("audio buffer read error"))
}
select {
case a.rec <- b:
case <-a.defaultSoundcardStream.recLoopDeinitNeededChan:
return
}
}
}
}
func (a *audioStruct) playLoopToVirtualSoundcard(deinitNeededChan, deinitFinishedChan chan bool) {
for {
select {
case <-a.virtualSoundcardStream.canPlay:
case <-deinitNeededChan:
deinitFinishedChan <- true
return
}
for {
a.virtualSoundcardStream.mutex.Lock()
if a.virtualSoundcardStream.playBuf.Len() < audioFrameSize {
a.virtualSoundcardStream.mutex.Unlock()
break
}
d := make([]byte, audioFrameSize)
bytesToWrite, err := a.virtualSoundcardStream.playBuf.Read(d)
a.virtualSoundcardStream.mutex.Unlock()
if err != nil {
log.Error(err)
break
}
if bytesToWrite != len(d) {
log.Error("buffer underread")
break
}
for len(d) > 0 {
written, err := a.virtualSoundcardStream.source.Write(d)
if err != nil {
if _, ok := err.(*os.PathError); !ok {
reportError(err)
}
break
}
d = d[written:]
}
}
}
}
func (a *audioStruct) recLoopFromVirtualSoundcard(deinitNeededChan, deinitFinishedChan chan bool) {
defer func() {
deinitFinishedChan <- true
}()
frameBuf := make([]byte, audioFrameSize)
buf := bytes.NewBuffer([]byte{})
for {
select {
case <-deinitNeededChan:
return
default:
}
n, err := a.virtualSoundcardStream.sink.Read(frameBuf)
if err != nil {
if _, ok := err.(*os.PathError); !ok {
reportError(err)
if err == io.EOF {
<-deinitNeededChan
return
}
}
}
// Do not send silence frames to the radio unnecessarily
if isAllZero(frameBuf[:n]) {
continue
}
buf.Write(frameBuf[:n])
for buf.Len() >= len(frameBuf) {
// We need to create a new []byte slice for each chunk to be able to send it through the rec chan.
b := make([]byte, len(frameBuf))
n, err = buf.Read(b)
if err != nil {
reportError(err)
}
if n != len(frameBuf) {
reportError(errors.New("audio buffer read error"))
}
select {
case a.rec <- b:
case <-deinitNeededChan:
return
}
}
}
}
func (a *audioStruct) loop() {
playLoopToVirtualSoundcardDeinitNeededChan := make(chan bool)
playLoopToVirtualSoundcardDeinitFinishedChan := make(chan bool)
go a.playLoopToVirtualSoundcard(playLoopToVirtualSoundcardDeinitNeededChan, playLoopToVirtualSoundcardDeinitFinishedChan)
playLoopToDefaultSoundcardDeinitNeededChan := make(chan bool)
playLoopToDefaultSoundcardDeinitFinishedChan := make(chan bool)
go a.playLoopToDefaultSoundcard(playLoopToDefaultSoundcardDeinitNeededChan, playLoopToDefaultSoundcardDeinitFinishedChan)
recLoopFromVirtualSoundcardDeinitNeededChan := make(chan bool)
recLoopFromVirtualSoundcardDeinitFinishedChan := make(chan bool)
go a.recLoopFromVirtualSoundcard(recLoopFromVirtualSoundcardDeinitNeededChan, recLoopFromVirtualSoundcardDeinitFinishedChan)
var d []byte
for {
select {
case d = <-a.play:
case <-a.deinitNeededChan:
a.closeIfNeeded()
recLoopFromVirtualSoundcardDeinitNeededChan <- true
<-recLoopFromVirtualSoundcardDeinitFinishedChan
playLoopToVirtualSoundcardDeinitNeededChan <- true
<-playLoopToVirtualSoundcardDeinitFinishedChan
if a.defaultSoundcardStream.playStream != nil {
a.defaultSoundCardPlayStreamDeinit()
}
playLoopToDefaultSoundcardDeinitNeededChan <- true
<-playLoopToDefaultSoundcardDeinitFinishedChan
a.deinitFinishedChan <- true
return
}
a.virtualSoundcardStream.mutex.Lock()
free := maxPlayBufferSize - a.virtualSoundcardStream.playBuf.Len()
if free < len(d) {
b := make([]byte, len(d)-free)
_, _ = a.virtualSoundcardStream.playBuf.Read(b)
}
a.virtualSoundcardStream.playBuf.Write(d)
a.virtualSoundcardStream.mutex.Unlock()
// Non-blocking notify.
select {
case a.virtualSoundcardStream.canPlay <- true:
default:
}
if a.defaultSoundcardStream.playStream != nil {
a.defaultSoundcardStream.mutex.Lock()
free := maxPlayBufferSize - a.defaultSoundcardStream.playBuf.Len()
if free < len(d) {
b := make([]byte, len(d)-free)
_, _ = a.defaultSoundcardStream.playBuf.Read(b)
}
a.defaultSoundcardStream.playBuf.Write(d)
a.defaultSoundcardStream.mutex.Unlock()
// Non-blocking notify.
select {
case a.defaultSoundcardStream.canPlay <- true:
default:
}
}
}
}
// We only init the audio once, with the first device name we acquire, so apps using the virtual sound card
// won't have issues with the interface going down while the app is running.
//
// ad8im note: but this seems like it leaves stale virtuals around indefinitely in some cases
//
// so it may be desirable to enable force cleanup and recreate via flags
func (a *audioStruct) initIfNeeded(devName string) error {
a.devName = devName
bufferSizeInBits := (audioSampleRate * audioSampleBytes * 8) / 1000 * pulseAudioBufferLength.Milliseconds()
if !a.virtualSoundcardStream.source.IsOpen() {
a.virtualSoundcardStream.source.Name = "kappanhang-" + a.devName
a.virtualSoundcardStream.source.Filename = "/tmp/kappanhang-" + a.devName + ".source"
a.virtualSoundcardStream.source.Rate = audioSampleRate
a.virtualSoundcardStream.source.Format = "s16le"
a.virtualSoundcardStream.source.Channels = 1
a.virtualSoundcardStream.source.SetProperty("device.buffering.buffer_size", bufferSizeInBits)
a.virtualSoundcardStream.source.SetProperty("device.description", "kappanhang: "+a.devName)
// Cleanup previous pipes.
sources, err := papipes.GetActiveSources()
if err == nil {
for _, i := range sources {
if i.Filename == a.virtualSoundcardStream.source.Filename {
i.Close()
}
}
}
if err := a.virtualSoundcardStream.source.Open(); err != nil {
return err
}
}
if !a.virtualSoundcardStream.sink.IsOpen() {
a.virtualSoundcardStream.sink.Name = "kappanhang-" + a.devName
a.virtualSoundcardStream.sink.Filename = "/tmp/kappanhang-" + a.devName + ".sink"
a.virtualSoundcardStream.sink.Rate = audioSampleRate
a.virtualSoundcardStream.sink.Format = "s16le"
a.virtualSoundcardStream.sink.Channels = 1
a.virtualSoundcardStream.sink.UseSystemClockForTiming = true
a.virtualSoundcardStream.sink.SetProperty("device.buffering.buffer_size", bufferSizeInBits)
a.virtualSoundcardStream.sink.SetProperty("device.description", "kappanhang: "+a.devName)
// Cleanup previous pipes.
sinks, err := papipes.GetActiveSinks()
if err == nil {
for _, i := range sinks {
if i.Filename == a.virtualSoundcardStream.sink.Filename {
i.Close()
}
}
}
if err := a.virtualSoundcardStream.sink.Open(); err != nil {
return err
}
}
if a.virtualSoundcardStream.playBuf == nil {
//origina line// log.Print("opened device '" + a.virtualSoundcardStream.source.Name + "'")
log.Print("opened virtual sound card device")
log.Print(" name - source:'" + a.virtualSoundcardStream.source.Name + "' sink:'" + a.virtualSoundcardStream.sink.Name + "'")
log.Print(" location - source:'" + a.virtualSoundcardStream.source.Filename + "' sink:'" + a.virtualSoundcardStream.sink.Filename + "'")
a.play = make(chan []byte)
a.rec = make(chan []byte)
a.virtualSoundcardStream.playBuf = bytes.NewBuffer([]byte{})
a.defaultSoundcardStream.playBuf = bytes.NewBuffer([]byte{})
a.virtualSoundcardStream.canPlay = make(chan bool)
a.defaultSoundcardStream.canPlay = make(chan bool)
a.defaultSoundcardStream.togglePlaybackChan = make(chan bool)
a.deinitNeededChan = make(chan bool)
a.deinitFinishedChan = make(chan bool)
go a.loop()
}
return nil
}
func (a *audioStruct) closeIfNeeded() {
if a.virtualSoundcardStream.source.IsOpen() {
if err := a.virtualSoundcardStream.source.Close(); err != nil {
if _, ok := err.(*os.PathError); !ok {
log.Error(err)
}
}
}
if a.virtualSoundcardStream.sink.IsOpen() {
if err := a.virtualSoundcardStream.sink.Close(); err != nil {
if _, ok := err.(*os.PathError); !ok {
log.Error(err)
}
}
}
}
func (a *audioStruct) deinit() {
a.defaultSoundCardRecStreamDeinit()
a.closeIfNeeded()
if a.deinitNeededChan != nil {
a.deinitNeededChan <- true
<-a.deinitFinishedChan
}
}