mirror of
https://github.com/nonoo/kappanhang.git
synced 2025-12-06 08:02:00 +01:00
Add 'l' hotkey for quickly listening into the incoming audio stream
This commit is contained in:
parent
d9d6ff887b
commit
eed09e0956
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
1
go.mod
|
|
@ -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
2
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.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
44
keyboard.go
Normal 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()
|
||||||
|
}
|
||||||
3
main.go
3
main.go
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue