diff --git a/README.md b/README.md index a9427cf..038760e 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,14 @@ disabled and the contents of the second line of the status bar will be written as new console log lines. This is also the case if a Unix/VT100 terminal is not available. +### Hotkeys + +Currently the only supported hotkey is `l` (listen), which toggles audio +stream playback to the default sound device. This is useful for quickly +listening into the audio stream coming from the server (the transceiver). +Note that audio will be played to the previously created virtual sound card +regardless of this setting. + ## Authors - Norbert Varga HA2NON [nonoo@nonoo.hu](mailto:nonoo@nonoo.hu) diff --git a/audio-linux.go b/audio-linux.go index b277c06..81e275d 100644 --- a/audio-linux.go +++ b/audio-linux.go @@ -10,6 +10,7 @@ import ( "time" "github.com/akosmarton/papipes" + "github.com/mesilliac/pulse-simple" ) const audioSampleRate = 48000 @@ -19,6 +20,8 @@ const audioFrameSize = 1920 // 20ms const maxPlayBufferSize = audioFrameSize * 5 type audioStruct struct { + devName string + source papipes.Source sink papipes.Sink @@ -33,14 +36,58 @@ type audioStruct struct { mutex sync.Mutex playBuf *bytes.Buffer canPlay chan bool + + togglePlaybackToDefaultSoundcardChan chan bool + defaultSoundCardStream *pulse.Stream } var audio audioStruct +func (a *audioStruct) togglePlaybackToDefaultSoundcard() { + if a.defaultSoundCardStream == nil { + log.Print("turned on audio playback") + ss := pulse.SampleSpec{Format: pulse.SAMPLE_S16LE, Rate: 48000, Channels: 1} + a.defaultSoundCardStream, _ = pulse.Playback("kappanhang", a.devName, &ss) + } else { + _ = a.defaultSoundCardStream.Drain() + a.defaultSoundCardStream.Free() + a.defaultSoundCardStream = nil + log.Print("turned off audio playback") + } +} + +func (a *audioStruct) playToVirtualSoundcard(d []byte) { + for len(d) > 0 { + written, err := a.source.Write(d) + if err != nil { + if _, ok := err.(*os.PathError); !ok { + reportError(err) + } + break + } + d = d[written:] + } +} + +func (a *audioStruct) playToDefaultSoundcard(d []byte) { + for len(d) > 0 { + written, err := a.defaultSoundCardStream.Write(d) + if err != nil { + if _, ok := err.(*os.PathError); !ok { + reportError(err) + } + break + } + d = d[written:] + } +} + func (a *audioStruct) playLoop(deinitNeededChan, deinitFinishedChan chan bool) { for { select { case <-a.canPlay: + case <-a.togglePlaybackToDefaultSoundcardChan: + a.togglePlaybackToDefaultSoundcard() case <-deinitNeededChan: deinitFinishedChan <- true return @@ -65,19 +112,9 @@ func (a *audioStruct) playLoop(deinitNeededChan, deinitFinishedChan chan bool) { break } - for { - written, err := a.source.Write(d) - if err != nil { - if _, ok := err.(*os.PathError); !ok { - reportError(err) - } - break - } - bytesToWrite -= written - if bytesToWrite == 0 { - break - } - d = d[written:] + a.playToVirtualSoundcard(d[:bytesToWrite]) + if a.defaultSoundCardStream != nil { + a.playToDefaultSoundcard(d[:bytesToWrite]) } } } @@ -175,16 +212,17 @@ func (a *audioStruct) loop() { // 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.source.IsOpen() { - a.source.Name = "kappanhang-" + devName - a.source.Filename = "/tmp/kappanhang-" + devName + ".source" + a.source.Name = "kappanhang-" + a.devName + a.source.Filename = "/tmp/kappanhang-" + a.devName + ".source" a.source.Rate = audioSampleRate a.source.Format = "s16le" a.source.Channels = 1 a.source.SetProperty("device.buffering.buffer_size", bufferSizeInBits) - a.source.SetProperty("device.description", "kappanhang: "+devName) + a.source.SetProperty("device.description", "kappanhang: "+a.devName) // Cleanup previous pipes. sources, err := papipes.GetActiveSources() @@ -202,14 +240,14 @@ func (a *audioStruct) initIfNeeded(devName string) error { } if !a.sink.IsOpen() { - a.sink.Name = "kappanhang-" + devName - a.sink.Filename = "/tmp/kappanhang-" + devName + ".sink" + a.sink.Name = "kappanhang-" + a.devName + a.sink.Filename = "/tmp/kappanhang-" + a.devName + ".sink" a.sink.Rate = audioSampleRate a.sink.Format = "s16le" a.sink.Channels = 1 a.sink.UseSystemClockForTiming = true a.sink.SetProperty("device.buffering.buffer_size", bufferSizeInBits) - a.sink.SetProperty("device.description", "kappanhang: "+devName) + a.sink.SetProperty("device.description", "kappanhang: "+a.devName) // Cleanup previous pipes. sinks, err := papipes.GetActiveSinks() @@ -233,6 +271,7 @@ func (a *audioStruct) initIfNeeded(devName string) error { a.play = make(chan []byte) a.canPlay = make(chan bool) a.rec = make(chan []byte) + a.togglePlaybackToDefaultSoundcardChan = make(chan bool) a.deinitNeededChan = make(chan bool) a.deinitFinishedChan = make(chan bool) go a.loop() diff --git a/go.mod b/go.mod index b1ab969..c23636d 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/fatih/color v1.9.0 github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 github.com/mattn/go-isatty v0.0.11 + github.com/mesilliac/pulse-simple v0.0.0-20170506101341-75ac54e19fdf github.com/pborman/getopt v1.1.0 go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.16.0 diff --git a/go.sum b/go.sum index b8b1d0e..897b0bc 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mesilliac/pulse-simple v0.0.0-20170506101341-75ac54e19fdf h1:Zkc2mThKEz8uIFMN5S9Vde4F075QqonswrYWngsjq0g= +github.com/mesilliac/pulse-simple v0.0.0-20170506101341-75ac54e19fdf/go.mod h1:w/UDU7AYzhUNZpb9TmWkrEFVu1+yA8jn++5871x9hWc= github.com/pborman/getopt v1.1.0 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0= github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= diff --git a/keyboard.go b/keyboard.go new file mode 100644 index 0000000..8bf308e --- /dev/null +++ b/keyboard.go @@ -0,0 +1,44 @@ +package main + +import ( + "os" + "os/exec" +) + +type keyboardStruct struct { +} + +var keyboard keyboardStruct + +func (s *keyboardStruct) loop() { + var b []byte = make([]byte, 1) + for { + n, err := os.Stdin.Read(b) + if n > 0 && err == nil { + if b[0] == 'l' { + if audio.togglePlaybackToDefaultSoundcardChan != nil { + // Non-blocking send to channel. + select { + case audio.togglePlaybackToDefaultSoundcardChan <- true: + default: + } + } + } + } + } +} + +func (s *keyboardStruct) init() { + if err := exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run(); err != nil { + log.Error("can't disable input buffering") + } + if err := exec.Command("stty", "-F", "/dev/tty", "-echo").Run(); err != nil { + log.Error("can't disable displaying entered characters") + } + + go s.loop() +} + +func (s *keyboardStruct) deinit() { + _ = exec.Command("stty", "-F", "/dev/tty", "echo").Run() +} diff --git a/main.go b/main.go index 3828dee..86452cf 100644 --- a/main.go +++ b/main.go @@ -99,6 +99,8 @@ func main() { osSignal := make(chan os.Signal, 1) signal.Notify(osSignal, os.Interrupt, syscall.SIGTERM) + keyboard.init() + var shouldExit bool var exitCode int for !shouldExit { @@ -120,6 +122,7 @@ func main() { audio.deinit() serialTCPSrv.deinit() serialPort.deinit() + keyboard.deinit() log.Print("exiting") os.Exit(exitCode)