Add 'l' hotkey for quickly listening into the incoming audio stream

This commit is contained in:
Nonoo 2020-10-30 15:57:33 +01:00
parent d9d6ff887b
commit eed09e0956
6 changed files with 116 additions and 19 deletions

View file

@ -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 as new console log lines. This is also the case if a Unix/VT100 terminal is
not available. 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 ## Authors
- Norbert Varga HA2NON [nonoo@nonoo.hu](mailto:nonoo@nonoo.hu) - Norbert Varga HA2NON [nonoo@nonoo.hu](mailto:nonoo@nonoo.hu)

View file

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/akosmarton/papipes" "github.com/akosmarton/papipes"
"github.com/mesilliac/pulse-simple"
) )
const audioSampleRate = 48000 const audioSampleRate = 48000
@ -19,6 +20,8 @@ const audioFrameSize = 1920 // 20ms
const maxPlayBufferSize = audioFrameSize * 5 const maxPlayBufferSize = audioFrameSize * 5
type audioStruct struct { type audioStruct struct {
devName string
source papipes.Source source papipes.Source
sink papipes.Sink sink papipes.Sink
@ -33,14 +36,58 @@ type audioStruct struct {
mutex sync.Mutex mutex sync.Mutex
playBuf *bytes.Buffer playBuf *bytes.Buffer
canPlay chan bool canPlay chan bool
togglePlaybackToDefaultSoundcardChan chan bool
defaultSoundCardStream *pulse.Stream
} }
var audio audioStruct 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) { func (a *audioStruct) playLoop(deinitNeededChan, deinitFinishedChan chan bool) {
for { for {
select { select {
case <-a.canPlay: case <-a.canPlay:
case <-a.togglePlaybackToDefaultSoundcardChan:
a.togglePlaybackToDefaultSoundcard()
case <-deinitNeededChan: case <-deinitNeededChan:
deinitFinishedChan <- true deinitFinishedChan <- true
return return
@ -65,19 +112,9 @@ func (a *audioStruct) playLoop(deinitNeededChan, deinitFinishedChan chan bool) {
break break
} }
for { a.playToVirtualSoundcard(d[:bytesToWrite])
written, err := a.source.Write(d) if a.defaultSoundCardStream != nil {
if err != nil { a.playToDefaultSoundcard(d[:bytesToWrite])
if _, ok := err.(*os.PathError); !ok {
reportError(err)
}
break
}
bytesToWrite -= written
if bytesToWrite == 0 {
break
}
d = d[written:]
} }
} }
} }
@ -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 // 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. // won't have issues with the interface going down while the app is running.
func (a *audioStruct) initIfNeeded(devName string) error { func (a *audioStruct) initIfNeeded(devName string) error {
a.devName = devName
bufferSizeInBits := (audioSampleRate * audioSampleBytes * 8) / 1000 * pulseAudioBufferLength.Milliseconds() bufferSizeInBits := (audioSampleRate * audioSampleBytes * 8) / 1000 * pulseAudioBufferLength.Milliseconds()
if !a.source.IsOpen() { if !a.source.IsOpen() {
a.source.Name = "kappanhang-" + devName a.source.Name = "kappanhang-" + a.devName
a.source.Filename = "/tmp/kappanhang-" + devName + ".source" a.source.Filename = "/tmp/kappanhang-" + a.devName + ".source"
a.source.Rate = audioSampleRate a.source.Rate = audioSampleRate
a.source.Format = "s16le" a.source.Format = "s16le"
a.source.Channels = 1 a.source.Channels = 1
a.source.SetProperty("device.buffering.buffer_size", bufferSizeInBits) 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. // Cleanup previous pipes.
sources, err := papipes.GetActiveSources() sources, err := papipes.GetActiveSources()
@ -202,14 +240,14 @@ func (a *audioStruct) initIfNeeded(devName string) error {
} }
if !a.sink.IsOpen() { if !a.sink.IsOpen() {
a.sink.Name = "kappanhang-" + devName a.sink.Name = "kappanhang-" + a.devName
a.sink.Filename = "/tmp/kappanhang-" + devName + ".sink" a.sink.Filename = "/tmp/kappanhang-" + a.devName + ".sink"
a.sink.Rate = audioSampleRate a.sink.Rate = audioSampleRate
a.sink.Format = "s16le" a.sink.Format = "s16le"
a.sink.Channels = 1 a.sink.Channels = 1
a.sink.UseSystemClockForTiming = true a.sink.UseSystemClockForTiming = true
a.sink.SetProperty("device.buffering.buffer_size", bufferSizeInBits) 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. // Cleanup previous pipes.
sinks, err := papipes.GetActiveSinks() sinks, err := papipes.GetActiveSinks()
@ -233,6 +271,7 @@ func (a *audioStruct) initIfNeeded(devName string) error {
a.play = make(chan []byte) a.play = make(chan []byte)
a.canPlay = make(chan bool) a.canPlay = make(chan bool)
a.rec = make(chan []byte) a.rec = make(chan []byte)
a.togglePlaybackToDefaultSoundcardChan = make(chan bool)
a.deinitNeededChan = make(chan bool) a.deinitNeededChan = make(chan bool)
a.deinitFinishedChan = make(chan bool) a.deinitFinishedChan = make(chan bool)
go a.loop() go a.loop()

1
go.mod
View file

@ -7,6 +7,7 @@ require (
github.com/fatih/color v1.9.0 github.com/fatih/color v1.9.0
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2
github.com/mattn/go-isatty v0.0.11 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 github.com/pborman/getopt v1.1.0
go.uber.org/multierr v1.6.0 // indirect go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.16.0 go.uber.org/zap v1.16.0

2
go.sum
View file

@ -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.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 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 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 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0=
github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk= 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= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=

44
keyboard.go Normal file
View file

@ -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()
}

View file

@ -99,6 +99,8 @@ func main() {
osSignal := make(chan os.Signal, 1) osSignal := make(chan os.Signal, 1)
signal.Notify(osSignal, os.Interrupt, syscall.SIGTERM) signal.Notify(osSignal, os.Interrupt, syscall.SIGTERM)
keyboard.init()
var shouldExit bool var shouldExit bool
var exitCode int var exitCode int
for !shouldExit { for !shouldExit {
@ -120,6 +122,7 @@ func main() {
audio.deinit() audio.deinit()
serialTCPSrv.deinit() serialTCPSrv.deinit()
serialPort.deinit() serialPort.deinit()
keyboard.deinit()
log.Print("exiting") log.Print("exiting")
os.Exit(exitCode) os.Exit(exitCode)