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:
+4
-2
@@ -4,11 +4,13 @@
|
|||||||
"date": "",
|
"date": "",
|
||||||
"en": [
|
"en": [
|
||||||
"Cluster filters gain NEW POTA and NEW COUNTY, alongside the existing status chips.",
|
"Cluster filters gain NEW POTA and NEW COUNTY, alongside the existing status chips.",
|
||||||
"A playback that fails to start now says so in the log instead of ending silently after a tenth of a second."
|
"A playback that fails to start now says so in the log instead of ending silently after a tenth of a second.",
|
||||||
|
"Replaying a recording before the previous one has finished now works: the new playback waits for the audio device to be free instead of keying the radio and releasing at once."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Les filtres du cluster gagnent NEW POTA et NEW COUNTY, à côté des pastilles de statut existantes.",
|
"Les filtres du cluster gagnent NEW POTA et NEW COUNTY, à côté des pastilles de statut existantes.",
|
||||||
"Une lecture qui ne démarre pas le signale désormais dans le journal, au lieu de s'arrêter en silence au bout d'un dixième de seconde."
|
"Une lecture qui ne démarre pas le signale désormais dans le journal, au lieu de s'arrêter en silence au bout d'un dixième de seconde.",
|
||||||
|
"Relancer un enregistrement avant la fin du précédent fonctionne désormais : la nouvelle lecture attend que le périphérique audio soit libre, au lieu de passer en émission et de relâcher aussitôt."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package audio
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager owns the DVK record/playback lifecycle: at most one recording and
|
// Manager owns the DVK record/playback lifecycle: at most one recording and
|
||||||
@@ -15,6 +16,10 @@ type Manager struct {
|
|||||||
recStop chan struct{}
|
recStop chan struct{}
|
||||||
recDone chan recResult
|
recDone chan recResult
|
||||||
playStop chan struct{}
|
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)
|
monStop chan struct{} // RX monitor passthrough (capture → render)
|
||||||
monRing *pcmRing // live audio hand-off, also fed by the network stream
|
monRing *pcmRing // live audio hand-off, also fed by the network stream
|
||||||
txStop chan struct{} // TX audio passthrough (mic → rig)
|
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)
|
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()
|
m.StopPlayback()
|
||||||
stop := make(chan struct{})
|
stop := make(chan struct{})
|
||||||
|
done := make(chan struct{})
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
m.playStop = stop
|
m.playStop = stop
|
||||||
|
m.playDone = done
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
go func() {
|
go func() {
|
||||||
// The error was discarded. A device that refuses to start returns here
|
// 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 {
|
if m.playStop == stop {
|
||||||
m.playStop = nil
|
m.playStop = nil
|
||||||
}
|
}
|
||||||
|
if m.playDone == done {
|
||||||
|
m.playDone = nil
|
||||||
|
}
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
close(done) // the device is free from here
|
||||||
m.notify()
|
m.notify()
|
||||||
}()
|
}()
|
||||||
m.notify()
|
m.notify()
|
||||||
@@ -153,12 +172,24 @@ func (m *Manager) Play(deviceID, path string, gainPct int) error {
|
|||||||
func (m *Manager) StopPlayback() {
|
func (m *Manager) StopPlayback() {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
stop := m.playStop
|
stop := m.playStop
|
||||||
|
done := m.playDone
|
||||||
m.playStop = nil
|
m.playStop = nil
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
if stop != nil {
|
if stop == nil {
|
||||||
close(stop)
|
return
|
||||||
m.notify()
|
|
||||||
}
|
}
|
||||||
|
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) --------------------
|
// ---- RX audio monitor (Phase 2: USB codec passthrough) --------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user