package cat // Playing a recorded message to the radio over TCI — the voice keyer's path. // // The same exchange the tone probe established, with a WAV in place of the // sine: the radio asks for a frame, we answer with the next slice of the // message, and it sets the pace. What is added here is the conversion, because // a recording is whatever the microphone gave it — 16-bit, often mono, often // not 48 kHz — and the radio wants interleaved float32 at the stream's rate. // // The message is converted ONCE, up front, rather than per frame. A voice // message is a few hundred kilobytes; resampling it inside the callback would // put arithmetic on the path that has 21 ms to answer, and a late frame is a // gap in what goes out. import ( "encoding/binary" "fmt" "math" "time" ) // tciTXFirstAskTimeout is how long to wait for the radio to ask for the first // frame before giving up. // // It answers within a frame or two when it is going to answer at all, so this // is generous. When it stays quiet the cause is always the same — the transmit // audio source is the microphone rather than TCI — and a fifth of a second of // carrier is a cheap way to find that out. const tciTXFirstAskTimeout = 200 * time.Millisecond // PlayTXAudio sends one message and returns when it has all been handed over, // or when stop is closed. // // The PTT is NOT touched here. The voice keyer keys before calling and unkeys // after, exactly as it does with a sound card, so the transmission is bracketed // by the same code whichever way the audio travels. func (t *TCI) PlayTXAudio(pcm []byte, rate, ch, bits int, stop <-chan struct{}) error { t.mu.Lock() connected := t.conn != nil t.mu.Unlock() if !connected { return fmt.Errorf("not connected to the radio") } t.audio.mu.Lock() outRate := t.audio.rate t.audio.mu.Unlock() if outRate <= 0 { outRate = 48000 } mono := decodeToMono(pcm, ch, bits) if len(mono) == 0 { return fmt.Errorf("the message is empty") } if rate > 0 && rate != outRate { mono = resampleLinear(mono, rate, outRate) } // Served from here on. The callback does nothing but copy and interleave, // which is what keeps it inside the frame interval. pos := 0 done := make(chan struct{}) var closed bool t.setTXFeed(func(samples int) []byte { if samples <= 0 { samples = 2048 } pairs := samples / 2 if pos >= len(mono) { if !closed { closed = true close(done) } return nil } payload := make([]byte, samples*4) le := binary.LittleEndian for i := 0; i < pairs; i++ { var v float32 if pos < len(mono) { v = mono[pos] pos++ } bits := math.Float32bits(v) le.PutUint32(payload[(i*2)*4:], bits) // left le.PutUint32(payload[(i*2+1)*4:], bits) // right } return payload }) defer t.setTXFeed(nil) // Nothing asked for in a fifth of a second means nothing is listening. // Reported plainly: the message would otherwise go out as silence, and a // voice keyer that transmits silence is worse than one that refuses. deadline := time.Now().Add(tciTXFirstAskTimeout) for time.Now().Before(deadline) { t.audio.mu.Lock() asked := t.audio.txSent > 0 t.audio.mu.Unlock() if asked { break } select { case <-stop: return nil case <-time.After(10 * time.Millisecond): } } t.audio.mu.Lock() asked := t.audio.txSent t.audio.mu.Unlock() if asked == 0 { return fmt.Errorf("the radio did not ask for any audio — set its transmit audio source to TCI instead of the microphone") } // The radio drains the message at real time, so this waits for the feed to // run out. The cap is the message's own length with a second to spare: a // radio that stops asking mid-message must not hold the transmitter up. limit := time.Duration(float64(len(mono))/float64(outRate)*float64(time.Second)) + time.Second select { case <-done: case <-stop: case <-time.After(limit): debugLog.Printf("TCI: the radio stopped asking for audio before the message ended") } return nil } // decodeToMono turns interleaved PCM into one channel of -1…1 floats. func decodeToMono(pcm []byte, ch, bits int) []float32 { if ch <= 0 { ch = 1 } switch bits { case 16: frame := ch * 2 out := make([]float32, 0, len(pcm)/frame+1) for i := 0; i+frame <= len(pcm); i += frame { var sum float32 for c := 0; c < ch; c++ { v := int16(uint16(pcm[i+c*2]) | uint16(pcm[i+c*2+1])<<8) sum += float32(v) / 32768 } out = append(out, sum/float32(ch)) } return out case 8: // Unsigned, centred on 128 — the one format where silence is not zero. out := make([]float32, 0, len(pcm)/ch+1) for i := 0; i+ch <= len(pcm); i += ch { var sum float32 for c := 0; c < ch; c++ { sum += (float32(pcm[i+c]) - 128) / 128 } out = append(out, sum/float32(ch)) } return out } return nil } // resampleLinear moves samples from one rate to another. // // Linear interpolation, which is crude and entirely adequate here: a voice // recording at 16 kHz going to 48 kHz is being INTERPOLATED, and interpolation // invents no frequencies to alias. Going the other way would want a filter // first, but a message recorded above the radio's stream rate is not a case // that arises — the recorder works at 16 kHz and radios stream at 48. func resampleLinear(in []float32, from, to int) []float32 { if from <= 0 || to <= 0 || from == to || len(in) == 0 { return in } ratio := float64(from) / float64(to) n := int(float64(len(in)) / ratio) out := make([]float32, n) for i := 0; i < n; i++ { src := float64(i) * ratio j := int(src) frac := float32(src - float64(j)) if j+1 < len(in) { out[i] = in[j]*(1-frac) + in[j+1]*frac } else { out[i] = in[len(in)-1] } } return out }