fix: say when a capture device is streaming nothing
Second empty recording from the same station, and this time NEITHER source had
delivered a sample — so the fix for a silent second source could not apply, and
the log still said only "recording was empty" at the end of the QSO.
Two blind spots, both now closed:
- captureStream's error was discarded. A device that cannot be opened — renamed,
unplugged, held by another program — looked exactly like one that is merely
quiet.
- A WASAPI device can open cleanly and then produce nothing at all. A DAX
channel with no stream behind it does precisely that, and it is
indistinguishable from silence until the file turns out to be empty.
A watchdog now reports both, three seconds in, and the first goes to the OPERATOR
as a toast rather than only to the log: they are mid-QSO, and by the time they
find out at save time the audio is gone for good.
This commit is contained in:
@@ -844,7 +844,11 @@ func (a *App) startup(ctx context.Context) {
|
|||||||
// Route CAT/OmniRig debug lines into the unified app log (they used to go
|
// Route CAT/OmniRig debug lines into the unified app log (they used to go
|
||||||
// to a separate cat.log in the old HamLog folder, which users couldn't find).
|
// to a separate cat.log in the old HamLog folder, which users couldn't find).
|
||||||
cat.LogSink = applog.Printf
|
cat.LogSink = applog.Printf
|
||||||
audio.LogSink = applog.Printf // capture audio-goroutine panics in the app log
|
audio.LogSink = applog.Printf // capture audio-goroutine panics in the app log
|
||||||
|
// A recorder that captures nothing must reach the OPERATOR, not just the log:
|
||||||
|
// they are mid-QSO, and by the time they notice at save time the audio is
|
||||||
|
// gone for good.
|
||||||
|
audio.AlertSink = func(format string, args ...any) { a.toast(fmt.Sprintf(format, args...)) }
|
||||||
extsvc.LogSink = applog.Printf // log raw QRZ (and other) service responses for diagnosis
|
extsvc.LogSink = applog.Printf // log raw QRZ (and other) service responses for diagnosis
|
||||||
lookup.LogSink = applog.Printf // which call was queried, and why a portable lookup fell back
|
lookup.LogSink = applog.Printf // which call was queried, and why a portable lookup fell back
|
||||||
db.LogSink = applog.Printf // which schema migrations ran, and how long they took
|
db.LogSink = applog.Printf // which schema migrations ran, and how long they took
|
||||||
|
|||||||
+4
-2
@@ -7,14 +7,16 @@
|
|||||||
"QSO recording works on CW again. Your own sidetone is not in the file, the other station is.",
|
"QSO recording works on CW again. Your own sidetone is not in the file, the other station is.",
|
||||||
"Playing a recording on the air now says why when it cannot — most often no audio output to the radio is configured.",
|
"Playing a recording on the air now says why when it cannot — most often no audio output to the radio is configured.",
|
||||||
"The play-on-air button is hidden on CW: the transmitter builds its tone from the keyer and ignores the audio path. Recording, stopping and resuming stay available.",
|
"The play-on-air button is hidden on CW: the transmitter builds its tone from the keyer and ignores the audio path. Recording, stopping and resuming stay available.",
|
||||||
"A QSO recording no longer comes out empty when one of the two audio sources is silent — the case in CW, where the mic device delivers nothing. The live source is recorded alone."
|
"A QSO recording no longer comes out empty when one of the two audio sources is silent — the case in CW, where the mic device delivers nothing. The live source is recorded alone.",
|
||||||
|
"The recorder now warns on screen within three seconds when a capture device delivers no audio, instead of producing an empty file discovered when the QSO is logged."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"FlexRadio : en split, OpsLog reste sur la slice de réception au lieu de suivre l'émission.",
|
"FlexRadio : en split, OpsLog reste sur la slice de réception au lieu de suivre l'émission.",
|
||||||
"L'enregistrement des QSO fonctionne à nouveau en CW. Votre propre signal d'écoute n'est pas dans le fichier, celui du correspondant l'est.",
|
"L'enregistrement des QSO fonctionne à nouveau en CW. Votre propre signal d'écoute n'est pas dans le fichier, celui du correspondant l'est.",
|
||||||
"L'émission d'un enregistrement indique désormais pourquoi elle échoue — le plus souvent aucune sortie audio vers la radio n'est configurée.",
|
"L'émission d'un enregistrement indique désormais pourquoi elle échoue — le plus souvent aucune sortie audio vers la radio n'est configurée.",
|
||||||
"Le bouton d'émission de l'enregistrement est masqué en CW : l'émetteur fabrique sa tonalité à partir du manipulateur et ignore la chaîne audio. Enregistrer, arrêter et reprendre restent disponibles.",
|
"Le bouton d'émission de l'enregistrement est masqué en CW : l'émetteur fabrique sa tonalité à partir du manipulateur et ignore la chaîne audio. Enregistrer, arrêter et reprendre restent disponibles.",
|
||||||
"Un enregistrement de QSO n'est plus vide quand l'une des deux sources audio est muette — le cas en CW, où le périphérique micro ne débite rien. La source active est enregistrée seule."
|
"Un enregistrement de QSO n'est plus vide quand l'une des deux sources audio est muette — le cas en CW, où le périphérique micro ne débite rien. La source active est enregistrée seule.",
|
||||||
|
"L'enregistreur avertit désormais à l'écran en trois secondes lorsqu'un périphérique de capture ne délivre aucun audio, au lieu de produire un fichier vide découvert à l'enregistrement du QSO."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ import (
|
|||||||
// Defaults to a no-op so the package is usable without wiring.
|
// Defaults to a no-op so the package is usable without wiring.
|
||||||
var LogSink = func(string, ...any) {}
|
var LogSink = func(string, ...any) {}
|
||||||
|
|
||||||
|
// AlertSink receives the few audio problems an operator must see WHILE they are
|
||||||
|
// operating, not afterwards in a log file — a capture device that opens but
|
||||||
|
// never streams being the one that matters: the recording is silently empty and
|
||||||
|
// nothing says so until the QSO is logged and gone.
|
||||||
|
//
|
||||||
|
// Set to a toast emitter at startup; a no-op keeps the package standalone.
|
||||||
|
var AlertSink = func(string, ...any) {}
|
||||||
|
|
||||||
// recoverGoroutine turns a panic in a long-running audio goroutine into a logged
|
// recoverGoroutine turns a panic in a long-running audio goroutine into a logged
|
||||||
// event with a stack trace instead of a silent process-killing crash. (It can't
|
// event with a stack trace instead of a silent process-killing crash. (It can't
|
||||||
// catch a hard Windows access violation from the WASAPI layer — those are fatal
|
// catch a hard Windows access violation from the WASAPI layer — those are fatal
|
||||||
@@ -136,29 +144,63 @@ func (r *Recorder) Start(fromDev, micDev string, prerollSec int) error {
|
|||||||
go func() {
|
go func() {
|
||||||
defer r.wg.Done()
|
defer r.wg.Done()
|
||||||
defer recoverGoroutine("recorder capture (radio)")
|
defer recoverGoroutine("recorder capture (radio)")
|
||||||
_ = captureStream(fromDev, stop, func(chunk []byte) {
|
// The error was discarded here. A device that cannot be opened — renamed,
|
||||||
|
// unplugged, held by another program — then looked exactly like a device
|
||||||
|
// that is merely quiet, and the recording came out empty with nothing in
|
||||||
|
// the log to say why.
|
||||||
|
if err := captureStream(fromDev, stop, func(chunk []byte) {
|
||||||
s := bytesToInt16(chunk)
|
s := bytesToInt16(chunk)
|
||||||
r.srcMu.Lock()
|
r.srcMu.Lock()
|
||||||
r.bufA = append(r.bufA, s...)
|
r.bufA = append(r.bufA, s...)
|
||||||
r.lastA = time.Now()
|
r.lastA = time.Now()
|
||||||
r.srcMu.Unlock()
|
r.srcMu.Unlock()
|
||||||
})
|
}); err != nil {
|
||||||
|
LogSink("recorder: capture from %q failed: %v", fromDev, err)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
if twoSrc {
|
if twoSrc {
|
||||||
r.wg.Add(1)
|
r.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer r.wg.Done()
|
defer r.wg.Done()
|
||||||
defer recoverGoroutine("recorder capture (mic)")
|
defer recoverGoroutine("recorder capture (mic)")
|
||||||
_ = captureStream(micDev, stop, func(chunk []byte) {
|
if err := captureStream(micDev, stop, func(chunk []byte) {
|
||||||
s := bytesToInt16(chunk)
|
s := bytesToInt16(chunk)
|
||||||
r.srcMu.Lock()
|
r.srcMu.Lock()
|
||||||
r.bufB = append(r.bufB, s...)
|
r.bufB = append(r.bufB, s...)
|
||||||
r.lastB = time.Now()
|
r.lastB = time.Now()
|
||||||
r.srcMu.Unlock()
|
r.srcMu.Unlock()
|
||||||
})
|
}); err != nil {
|
||||||
|
LogSink("recorder: capture from %q failed: %v", micDev, err)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watchdog. A WASAPI device can open cleanly and then produce nothing at
|
||||||
|
// all — a DAX channel with no stream behind it does exactly that, and it is
|
||||||
|
// indistinguishable from silence until the recording turns out to be empty
|
||||||
|
// at the end of a QSO. Say it once, three seconds in, while there is still
|
||||||
|
// time to fix the setup.
|
||||||
|
r.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer r.wg.Done()
|
||||||
|
defer recoverGoroutine("recorder watchdog")
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
}
|
||||||
|
r.srcMu.Lock()
|
||||||
|
aQuiet, bQuiet := r.lastA.IsZero(), r.lastB.IsZero()
|
||||||
|
r.srcMu.Unlock()
|
||||||
|
if aQuiet {
|
||||||
|
LogSink("recorder: no audio at all from %q after 3 s — the device opened but nothing is streaming", fromDev)
|
||||||
|
AlertSink("No audio from %s — nothing is being recorded", fromDev)
|
||||||
|
}
|
||||||
|
if twoSrc && bQuiet {
|
||||||
|
LogSink("recorder: no audio at all from %q after 3 s — the device opened but nothing is streaming", micDev)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Mixer goroutine.
|
// Mixer goroutine.
|
||||||
r.wg.Add(1)
|
r.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
Reference in New Issue
Block a user