fix(tci): derive the sample width instead of trusting the format number
A real SunSDR answers format=3 where this expected 0 — and 0 was read from the documentation, which is exactly the kind of detail a memory of a document gets wrong. The stream was refused outright: 'format=3, expected float32', zero frames, silence. Swapping one magic number for another would only move the guess, so the width is now MEASURED: the header says how many samples the payload holds, and dividing gives the bytes per sample. Four is float32, two is 16-bit PCM scaled to the same -1..1 the rest of the audio path uses, and anything else is reported rather than mangled. That stays true whatever number the format field carries on the next firmware.
This commit is contained in:
@@ -83,6 +83,9 @@ type tciAudio struct {
|
||||
peakAt time.Time
|
||||
probe int
|
||||
lastErr string
|
||||
// widthLogged keeps the one-line note about the sample width to once a
|
||||
// session — it is a fact about the radio, not an event.
|
||||
widthLogged bool
|
||||
|
||||
// OnSamples receives decoded MONO samples (the two channels averaged) at
|
||||
// the negotiated rate. Mono because everything downstream — the QSO
|
||||
@@ -176,24 +179,63 @@ func (t *TCI) handleBinary(data []byte) {
|
||||
if stype != tciStreamRXAudio {
|
||||
return // IQ, TX audio echo, chrono: not this file's business yet
|
||||
}
|
||||
if format != 0 || codec != 0 {
|
||||
t.audioErr(fmt.Sprintf("stream is format=%d codec=%d, expected float32 uncompressed", format, codec))
|
||||
if codec != 0 {
|
||||
t.audioErr(fmt.Sprintf("stream is codec=%d, and nothing here decodes a compressed stream", codec))
|
||||
return
|
||||
}
|
||||
|
||||
// The FORMAT number is decided by measurement, not by the number itself.
|
||||
//
|
||||
// A real SunSDR answered format=3, where the code expected 0 — and 0 was a
|
||||
// guess from reading the documentation, which is exactly the kind of detail
|
||||
// a memory of a document gets wrong. Rather than swap one magic number for
|
||||
// another, the sample width is derived from what arrived: the header says
|
||||
// how many samples the payload holds, so the bytes per sample follow from
|
||||
// dividing. That is true whatever number the format field carries, on this
|
||||
// firmware and the next.
|
||||
payload := data[tciHeaderBytes:]
|
||||
n := len(payload) / 4
|
||||
if len(payload) == 0 || length <= 0 {
|
||||
return
|
||||
}
|
||||
width := len(payload) / length
|
||||
var n int
|
||||
switch width {
|
||||
case 4:
|
||||
n = len(payload) / 4 // float32
|
||||
case 2:
|
||||
n = len(payload) / 2 // 16-bit PCM
|
||||
default:
|
||||
t.audioErr(fmt.Sprintf("frame carries %d bytes for %d samples (format=%d) — not a width this reads",
|
||||
len(payload), length, format))
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
// Under the lock like the rest of the counters: the reader is the only
|
||||
// writer today, but a fact about the radio that is read from another
|
||||
// goroutine has no business being the one field left unguarded.
|
||||
t.audio.mu.Lock()
|
||||
first := !t.audio.widthLogged
|
||||
t.audio.widthLogged = true
|
||||
t.audio.mu.Unlock()
|
||||
if first {
|
||||
debugLog.Printf("TCI: audio is %d bytes per sample at %d Hz (format field says %d)", width, rate, format)
|
||||
}
|
||||
// Stereo interleaved → mono. Both channels of a receiver carry the same
|
||||
// audio, and everything downstream works on one.
|
||||
mono := make([]float32, 0, n/2+1)
|
||||
var peak float64
|
||||
sample := func(i int) float32 {
|
||||
if width == 2 {
|
||||
// 16-bit PCM, scaled to the same -1…1 the rest of the audio path
|
||||
// works in, so a change of format cannot change what a level means.
|
||||
return float32(int16(le.Uint16(payload[i*2:]))) / 32768
|
||||
}
|
||||
return math.Float32frombits(le.Uint32(payload[i*4:]))
|
||||
}
|
||||
for i := 0; i+1 < n; i += 2 {
|
||||
l := math.Float32frombits(le.Uint32(payload[i*4:]))
|
||||
r := math.Float32frombits(le.Uint32(payload[(i+1)*4:]))
|
||||
v := (l + r) / 2
|
||||
v := (sample(i) + sample(i+1)) / 2
|
||||
if a := math.Abs(float64(v)); a > peak {
|
||||
peak = a
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user