fix: a new playback raced the previous one for the audio device
Reported precisely: press play again and it stops the current message, but you then have to wait the whole length of the message before it will play — otherwise the radio is keyed and the PTT released at once. Play only SIGNALLED the previous playback to stop and started a new one straight away. The old goroutine still held the WASAPI render client for a moment, so the new client could not start; it returned immediately and the PTT tail was all that was left. The log now shows both halves of that: a play line, then a release 120 ms later. Stopping now waits for the goroutine to have returned — the point of stopping before starting again — bounded at 1.5 s so a wedged device cannot freeze the operator's click, and saying so in the log if it ever comes to that.
This commit is contained in:
@@ -5,6 +5,7 @@ package audio
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Manager owns the DVK record/playback lifecycle: at most one recording and
|
||||
@@ -15,6 +16,10 @@ type Manager struct {
|
||||
recStop chan struct{}
|
||||
recDone chan recResult
|
||||
playStop chan struct{}
|
||||
// playDone closes when the playback goroutine has returned and the audio
|
||||
// device is free again. Without it, the next Play raced the old one for the
|
||||
// device and lost.
|
||||
playDone chan struct{}
|
||||
monStop chan struct{} // RX monitor passthrough (capture → render)
|
||||
monRing *pcmRing // live audio hand-off, also fed by the network stream
|
||||
txStop chan struct{} // TX audio passthrough (mic → rig)
|
||||
@@ -125,10 +130,20 @@ func (m *Manager) Play(deviceID, path string, gainPct int) error {
|
||||
pcm[i], pcm[i+1] = byte(uint16(v)), byte(uint16(v)>>8)
|
||||
}
|
||||
}
|
||||
// Waits for any previous playback to have RELEASED THE DEVICE.
|
||||
//
|
||||
// It used to only signal the old one to stop and start a new one at once.
|
||||
// The old goroutine still held the WASAPI render client for a moment, so the
|
||||
// new client could not start: it returned immediately, the PTT was keyed and
|
||||
// released a tenth of a second later, and nothing came out. On the air that
|
||||
// looked like "press play again and you must wait the whole length of the
|
||||
// message before it will play at all".
|
||||
m.StopPlayback()
|
||||
stop := make(chan struct{})
|
||||
done := make(chan struct{})
|
||||
m.mu.Lock()
|
||||
m.playStop = stop
|
||||
m.playDone = done
|
||||
m.mu.Unlock()
|
||||
go func() {
|
||||
// The error was discarded. A device that refuses to start returns here
|
||||
@@ -142,7 +157,11 @@ func (m *Manager) Play(deviceID, path string, gainPct int) error {
|
||||
if m.playStop == stop {
|
||||
m.playStop = nil
|
||||
}
|
||||
if m.playDone == done {
|
||||
m.playDone = nil
|
||||
}
|
||||
m.mu.Unlock()
|
||||
close(done) // the device is free from here
|
||||
m.notify()
|
||||
}()
|
||||
m.notify()
|
||||
@@ -153,12 +172,24 @@ func (m *Manager) Play(deviceID, path string, gainPct int) error {
|
||||
func (m *Manager) StopPlayback() {
|
||||
m.mu.Lock()
|
||||
stop := m.playStop
|
||||
done := m.playDone
|
||||
m.playStop = nil
|
||||
m.mu.Unlock()
|
||||
if stop != nil {
|
||||
close(stop)
|
||||
m.notify()
|
||||
if stop == nil {
|
||||
return
|
||||
}
|
||||
close(stop)
|
||||
// Wait for the goroutine to release the device — that is the whole point of
|
||||
// stopping before starting again. Bounded: a wedged WASAPI call must not
|
||||
// freeze the caller, which here is the operator clicking a button.
|
||||
if done != nil {
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(1500 * time.Millisecond):
|
||||
LogSink("audio: previous playback did not release the device within 1.5 s")
|
||||
}
|
||||
}
|
||||
m.notify()
|
||||
}
|
||||
|
||||
// ---- RX audio monitor (Phase 2: USB codec passthrough) --------------------
|
||||
|
||||
Reference in New Issue
Block a user