Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions daemon/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ public class ControlConfig
/// Config for motorola SB9600
/// </summary>
public MotoSb9600Config Sb9600 = new MotoSb9600Config();
/// <summary>
/// Config for VOX audio-level based control
/// </summary>
public VoxConfig Vox = new VoxConfig();
}

/// <summary>
Expand Down
43 changes: 39 additions & 4 deletions daemon/LocalAudio.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ internal class LocalAudio
// RX Audio Objects
private SDL2AudioSource rxSource;
private AudioEncoder rxEncoder;
private AudioEncoder rxMonitorEncoder;
private AudioFormat rxAudioFormat = AudioFormat.Empty;
private long lastRawRxSampleMs = long.MinValue;
private bool started = false;

// TX Audio Objects
private SDL2AudioEndPoint txEndpoint;
Expand All @@ -60,6 +64,9 @@ internal class LocalAudio

// RX audio callback action
public Action<uint, byte[]> RxEncodedSampleCallback;
// Optional raw PCM monitor used by VOX mode to detect RX activity without
// changing the encoded audio path used by normal radios.
public Action<AudioSamplingRatesEnum, uint, short[]> RxRawSampleCallback;

public LocalAudio(string rxDevice, string txDevice, rc2_core.Radio radio, bool rxOnly = false)
{
Expand All @@ -76,14 +83,28 @@ public LocalAudio(string rxDevice, string txDevice, rc2_core.Radio radio, bool r

// Setup RX audio devices
rxEncoder = new AudioEncoder();
rxMonitorEncoder = new AudioEncoder();
rxSource = new SDL2AudioSource(rxDevice, rxEncoder);
rxSource.OnAudioSourceError += (e) => {
Log.Logger.Error("Got RX audio error: {error}", e);
};
// Setup RX sample callback
rxSource.OnAudioSourceEncodedSample += (uint durationRtpUnits, byte[] samples) => {
//Log.Logger.Verbose("Got {count} encoded RX samples", samples.Length);
RxEncodedSampleCallback(durationRtpUnits, samples);
RxEncodedSampleCallback?.Invoke(durationRtpUnits, samples);
if (RxRawSampleCallback != null && Environment.TickCount64 - lastRawRxSampleMs > 1000 && rxAudioFormat.ClockRate > 0)
{
// Some SDL backends only provide encoded callbacks. Decode a monitor
// copy so VOX still receives audio levels, but rate-limit it behind
// direct raw callbacks to avoid duplicate gate updates.
short[] pcmSamples = rxMonitorEncoder.DecodeAudio(samples, rxAudioFormat);
uint durationMilliseconds = (uint)(pcmSamples.Length * 1000 / rxAudioFormat.ClockRate);
RxRawSampleCallback(AudioSamplingRatesEnum.Rate16KHz, durationMilliseconds, pcmSamples);
}
};
rxSource.OnAudioSourceRawSample += (AudioSamplingRatesEnum samplingRate, uint durationMilliseconds, short[] samples) => {
lastRawRxSampleMs = Environment.TickCount64;
RxRawSampleCallback?.Invoke(samplingRate, durationMilliseconds, samples);
};
Log.Logger.Information(" RX: {rxDevice}", rxDevice);
// Setup TX audio devices if we aren't rx-only
Expand All @@ -101,6 +122,16 @@ public LocalAudio(string rxDevice, string txDevice, rc2_core.Radio radio, bool r

public void Start(AudioFormat audioFormat)
{
if (started)
{
// VOX can request Start from both initial setup and later WebRTC format
// negotiation. Keep the first active device session instead of reopening
// SDL devices mid-call.
Log.Logger.Debug("Audio device(s) already started, keeping existing format {format}/{rate}/{chans}", rxAudioFormat.FormatName, rxAudioFormat.ClockRate, rxAudioFormat.ChannelCount);
return;
}

rxAudioFormat = audioFormat;
// Set audio formats
rxSource.SetAudioSourceFormat(audioFormat);
if (!rxOnly)
Expand All @@ -114,14 +145,18 @@ public void Start(AudioFormat audioFormat)
txEndpoint.StartAudioSink();
}
Log.Logger.Debug("Audio device(s) started using format {format}/{rate}/{chans}", audioFormat.FormatName, audioFormat.ClockRate, audioFormat.ChannelCount);
started = true;
}

public async Task Stop()
{
await rxSource.CloseAudio();
if (!rxOnly)
if (started)
{
await txEndpoint.CloseAudioSink();
await rxSource.CloseAudio();
if (!rxOnly)
{
await txEndpoint.CloseAudioSink();
}
}
// De-init SDL2
SDL2Helper.QuitSDL();
Expand Down
67 changes: 65 additions & 2 deletions daemon/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,45 @@ static async Task Startup(FileInfo configFile, bool debug, bool verbose, bool no
// Switch based on control mode
switch(Config.Control.ControlMode)
{
case RadioControlMode.VOX:
{
VoxRadio voxRadio = null;
// WebRTC TX audio enters through the base RC2 radio callback. Capture
// the VoxRadio after construction so the callback can feed the TX gate.
Action<short[]> txAudioCallback = (samples) =>
{
voxRadio?.HandleTxAudioSamples(samples);
};
Action<AudioFormat> rtcFormatCallback = (audioFormat) =>
{
// Start local SDL audio using the negotiated WebRTC format, then
// restart VOX warmup so device-open noise is ignored.
localAudio.Start(audioFormat);
voxRadio?.ReArmStartupDelay("WebRTC audio negotiation");
};

voxRadio = new VoxRadio(
Config.Daemon.Name,
Config.Daemon.Desc,
Config.Control.RxOnly,
Config.Daemon.ListenAddress,
Config.Daemon.ListenPort,
Config.Daemon.AllowedNetworks,
Config.Control.Vox,
txAudioCallback,
localAudio.TxAudioCallback,
16000,
rtcFormatCallback,
Config.Softkeys,
Config.TextLookups.Zone,
Config.TextLookups.Channel
);
radio = voxRadio;
// Raw RX samples drive VOX state; encoded RX samples are forwarded
// separately only while VoxRadio reports Receiving.
localAudio.RxRawSampleCallback += voxRadio.HandleRxAudioSamples;
}
break;
case RadioControlMode.SB9600:
{
radio = new MotoSb9600Radio(
Expand Down Expand Up @@ -230,7 +269,17 @@ static async Task Startup(FileInfo configFile, bool debug, bool verbose, bool no
}

// Setup RX audio callback
localAudio.RxEncodedSampleCallback += radio.RxSendEncodedSamples;
if (Config.Control.ControlMode == RadioControlMode.VOX)
{
// VOX needs a default format before a peer negotiates so local RX can
// start and begin detecting audio. Re-negotiation may update this later.
localAudio.RxEncodedSampleCallback += ((VoxRadio)radio).HandleRxEncodedSamples;
localAudio.Start(GetDefaultAudioFormat());
}
else
{
localAudio.RxEncodedSampleCallback += radio.RxSendEncodedSamples;
}

// Start radio
radio.Start(noreset);
Expand Down Expand Up @@ -332,5 +381,19 @@ static void GetAudioDeviceInfo(string devName)
}
SDL2Helper.QuitSDL();
}

static AudioFormat GetDefaultAudioFormat()
{
AudioEncoder audioEncoder = new AudioEncoder();
AudioFormat g722 = audioEncoder.SupportedFormats.Find(f => f.FormatName == "G722");

if (g722.ClockRate == 0)
{
var audioFormatManager = new MediaFormatManager<AudioFormat>(audioEncoder.SupportedFormats);
return audioFormatManager.SelectedFormat;
}

return g722;
}
}
}
}
Loading
Loading