diff --git a/changelog.json b/changelog.json index 7b568fd..e780324 100644 --- a/changelog.json +++ b/changelog.json @@ -16,7 +16,8 @@ "Icom network audio: toggling Listening off and on no longer chops the sound — the monitor restarts as network-fed instead of also opening a USB capture.", "QSO recorder: starting a fresh manual take resets the counter for good — it used to flash 0 and jump back to the previous take’s elapsed time.", "MY_RIG follows the radio actually connected: the entry form now asks the active radio first, before the per-band default — an IC-7760 on the air no longer logs as the Flex the band plan names.", - "QSO recorder: a manual take on a network Icom records the network RX stream, not the “From radio” sound card (which captured another radio’s DAX on a mixed station)." + "QSO recorder: a manual take on a network Icom records the network RX stream, not the “From radio” sound card (which captured another radio’s DAX on a mixed station).", + "Icom network audio: duplicate and late-retransmitted packets no longer reach the recorder — recordings came out longer than the QSO, slowed and stuttering, while the speakers played fine." ], "fr": [ "Console Elecraft : le S-mètre est calibré sur un vrai K3 — S9 et les +dB correspondent désormais à l’affichage de la radio (il lisait environ deux points S trop bas).", @@ -32,7 +33,8 @@ "Audio réseau Icom : couper puis relancer Listening ne hache plus le son — le moniteur redémarre alimenté par le réseau au lieu d’ouvrir en plus une capture USB.", "Enregistreur de QSO : démarrer une nouvelle prise manuelle remet le compteur à zéro pour de bon — il affichait 0 puis resautait au temps de la prise précédente.", "MY_RIG suit la radio réellement connectée : le formulaire interroge d’abord la radio active, avant le défaut par bande — un IC-7760 à l’antenne ne se logue plus comme le Flex prévu par le plan de bande.", - "Enregistreur de QSO : une prise manuelle sur un Icom réseau enregistre le flux RX réseau, pas la carte son « From Radio » (qui capturait le DAX d’une autre radio sur une station mixte)." + "Enregistreur de QSO : une prise manuelle sur un Icom réseau enregistre le flux RX réseau, pas la carte son « From Radio » (qui capturait le DAX d’une autre radio sur une station mixte).", + "Audio réseau Icom : les paquets dupliqués ou retransmis en retard n’atteignent plus l’enregistreur — les enregistrements sortaient plus longs que le QSO, ralentis et hachés, alors que les haut-parleurs jouaient bien." ] }, { diff --git a/internal/cat/icomaudio.go b/internal/cat/icomaudio.go index 41d4f2e..4a7a713 100644 --- a/internal/cat/icomaudio.go +++ b/internal/cat/icomaudio.go @@ -134,12 +134,19 @@ func (a *icomAudio) audioPump() { case typ == 0x05: // rig-initiated disconnect debugLog.Printf("icom audio: rig sent DISCONNECT — audio stream dropped by the rig") case typ == 0x00 && k > icaAudioOffset: // audio data packet - a.trackRxSeq(icnLE.Uint16(buf[6:])) + fresh := a.trackRxSeq(icnLE.Uint16(buf[6:])) if a.dumped < icaDumpFirst { a.dumped++ debugLog.Printf("icom audio raw #%d: len=%d head=% X", a.dumped, k, buf[:min(icaAudioOffset+8, k)]) } - if a.sink != nil { + // Only a packet that ADVANCES the sequence reaches the sink. A + // duplicate or a late retransmit used to be delivered as if it + // were the next 20 ms of audio: the monitor's capped ring threw + // the surplus away (the speakers stayed clean), but the recorder + // keeps every sample it is given — the file grew longer than the + // QSO and played back slowed and stuttering, each lost-then- + // resent packet heard twice. + if fresh && a.sink != nil { payload := append([]byte(nil), buf[icaAudioOffset:k]...) a.sink(payload) } @@ -159,26 +166,36 @@ func (a *icomAudio) audioPump() { // trackRxSeq / sendRetransmitReq mirror icomNet's receive-side retransmit exactly // (audio is as loss-sensitive as the scope stream). Duplicated deliberately so // the audio stream owns its own seq state with no shared locking. -func (a *icomAudio) trackRxSeq(seq uint16) { +// +// The return value says whether this packet moves the stream FORWARD — the +// only kind the sink may hear. A duplicate is the same 20 ms again; a late +// retransmit would play old audio in the middle of new. Both are accounted +// for here and dropped by the caller. +func (a *icomAudio) trackRxSeq(seq uint16) bool { if !a.rxHaveSeq { a.rxHaveSeq = true a.rxLastSeq = seq - return + return true } switch d := int16(seq - a.rxLastSeq); { case d == 0: + return false case d < 0: delete(a.rxMissing, seq) + return false case d == 1: a.rxLastSeq = seq + return true case int(d) <= icnMaxMissing: for f := a.rxLastSeq + 1; f != seq; f++ { a.rxMissing[f] = 0 } a.rxLastSeq = seq + return true default: a.rxMissing = make(map[uint16]int) a.rxLastSeq = seq + return true } }