// +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. 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 { log.Print("opened device " + a.virtualSoundcardStream.source.Name) 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 } }