package main

// Feeding the QSO recorder from the TCI stream.
//
// The recorder works in 16 kHz mono, which is what its files and its mixing are
// built around; TCI delivers 48 kHz stereo float32. The conversion is the whole
// of this file, and it happens here rather than in internal/cat because the
// radio's job is to hand over what it sent, not to know what the recorder wants.
//
// Confirmed on a SunSDR (ExpertSDR3 1.5): 2048 samples a frame, 8192 bytes,
// four bytes per sample — and a test recording that plays back clean.

import (
	"encoding/binary"

	"hamlog/internal/applog"
	"hamlog/internal/audio"
	"hamlog/internal/cat"
)

// tciRecordSink pushes the receive stream into the QSO recorder.
//
// Installed whenever the TCI backend starts, and harmless when nothing is
// recording: PushRX drops what arrives unless a QSO is being captured, so the
// cost while idle is a decimation and a function call.
func (a *App) tciRecordSink(rate int, samples []float32) {
	if a.qsoRec == nil || len(samples) == 0 {
		return
	}
	a.qsoRec.PushRX(tciToRecorderPCM(rate, samples))
}

// tciToRecorderPCM converts the stream's mono float samples to the recorder's
// 16-bit PCM at its own rate.
//
// Averaging rather than picking every third sample: dropping samples aliases
// everything above 8 kHz back down into the voice band, and on a receiver that
// is hiss — the one thing a QSO recording has plenty of. A three-tap mean is a
// crude low-pass, but it is a low-pass, and it costs two additions.
func tciToRecorderPCM(rate int, samples []float32) []byte {
	if rate <= 0 {
		rate = 48000
	}
	step := rate / audio.RecorderSampleRate
	if step < 1 {
		step = 1
	}
	out := make([]byte, 0, (len(samples)/step)*2)
	for i := 0; i+step <= len(samples); i += step {
		var sum float32
		for j := 0; j < step; j++ {
			sum += samples[i+j]
		}
		v := sum / float32(step)
		if v > 1 {
			v = 1
		}
		if v < -1 {
			v = -1
		}
		var b [2]byte
		binary.LittleEndian.PutUint16(b[:], uint16(int16(v*32767)))
		out = append(out, b[0], b[1])
	}
	return out
}

// startTCIRecording opens the receive stream and routes it to the recorder.
//
// Called when the TCI backend comes up, and only when the operator has asked
// for it by choosing the radio as their receive device: opening a 384 kB/s
// stream on a station that records nothing is work the radio does for nobody.
func (a *App) startTCIRecording() {
	if a.cat == nil {
		return
	}
	// One switch, and it is the one an operator is already looking at: the
	// "From radio" device. There used to be a tick box here as well, from when
	// this was an experiment with no device to choose — two controls for one
	// question, and the second was where nobody would look.
	cfg, _ := a.GetAudioSettings()
	if cfg.FromRadio != audio.NetworkDeviceID {
		return
	}
	err := a.cat.TCIAudioDo(func(t cat.TCIAudioController) error {
		if s, ok := t.(interface {
			SetTCIAudioSink(func(int, []float32))
		}); ok {
			s.SetTCIAudioSink(a.tciRecordSink)
		}
		return t.StartTCIAudio(0, 48000)
	})
	if err != nil {
		applog.Printf("tci: could not open the receive stream for recording: %v", err)
		return
	}
	applog.Printf("tci: recording the receive audio over TCI — no virtual cable needed")
}
