From 3ef21257539b3c930bb1efd93de28204d6882df1 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Mon, 22 Jan 2024 23:34:46 -0800 Subject: [PATCH 01/10] Better handle high sample rate audio devices. --- src/audio/IAudioEngine.cpp | 3 +++ src/pipeline/TxRxThread.cpp | 27 ++++++++++----------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/audio/IAudioEngine.cpp b/src/audio/IAudioEngine.cpp index 65d4178e2..1723a6f2e 100644 --- a/src/audio/IAudioEngine.cpp +++ b/src/audio/IAudioEngine.cpp @@ -29,6 +29,9 @@ int IAudioEngine::StandardSampleRates[] = 16000, 22050, 24000, 32000, 44100, 48000, + 88200, 96000, + 176400, 192000, + 352800, 384000, -1 // negative terminated list }; diff --git a/src/pipeline/TxRxThread.cpp b/src/pipeline/TxRxThread.cpp index c15b177fb..f8ce03397 100644 --- a/src/pipeline/TxRxThread.cpp +++ b/src/pipeline/TxRxThread.cpp @@ -557,13 +557,6 @@ void TxRxThread::txProcessing_() // sample rate. Typically the sound card is running at 48 or 44.1 // kHz, and the modem at 8kHz - // allocate enough room for 20ms processing buffers at maximum - // sample rate of 48 kHz. Note these buffer are used by rx and tx - // side processing - - short insound_card[10*N48]; - int nout; - // // TX side processing -------------------------------------------- // @@ -597,7 +590,11 @@ void TxRxThread::txProcessing_() } int nsam_in_48 = freedvInterface.getTxNumSpeechSamples() * ((float)inputSampleRate_ / (float)freedvInterface.getTxSpeechSampleRate()); - assert(nsam_in_48 > 0 && nsam_in_48 < 10*N48); + assert(nsam_in_48 > 0); + + short insound_card[nsam_in_48]; + int nout; + while((unsigned)codec2_fifo_free(cbData->outfifo1) >= nsam_one_modem_frame) { // OK to generate a frame of modem output samples we need @@ -657,13 +654,6 @@ void TxRxThread::rxProcessing_() // sample rate. Typically the sound card is running at 48 or 44.1 // kHz, and the modem at 8kHz. - // allocate enough room for 20ms processing buffers at maximum - // sample rate of 48 kHz. Note these buffer are used by rx and tx - // side processing - - short insound_card[10*N48]; - int nout; - // // RX side processing -------------------------------------------- // @@ -679,8 +669,11 @@ void TxRxThread::rxProcessing_() // Attempt to read one processing frame (about 20ms) of receive samples, we // keep this frame duration constant across modes and sound card sample rates int nsam = (int)(inputSampleRate_ * FRAME_DURATION); - assert(nsam <= 10*N48); - assert(nsam != 0); + assert(nsam > 0); + + short insound_card[nsam]; + int nout; + bool processInputFifo = (g_voice_keyer_tx && wxGetApp().appConfiguration.monitorVoiceKeyerAudio) || From c6ae452e2c8abc78994c0022def160231f6096d1 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Mon, 22 Jan 2024 23:39:40 -0800 Subject: [PATCH 02/10] Add PR #668 to changelog. --- USER_MANUAL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/USER_MANUAL.md b/USER_MANUAL.md index 5f1210b36..cf0200dba 100644 --- a/USER_MANUAL.md +++ b/USER_MANUAL.md @@ -893,6 +893,7 @@ LDPC | Low Density Parity Check Codes - a family of powerful FEC codes 1. Bugfixes: * Prevent unnecessary recreation of resamplers in analog mode. (PR #661) + * Better handle high sample rate audio devices. (PR #668) 2. Enhancements: * Add Frequency column to RX drop-down. (PR #663) 3. Code cleanup: From 74092f75712a5b730b16a00a6788b1767ee63bce Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Tue, 23 Jan 2024 00:03:22 -0800 Subject: [PATCH 03/10] Use default sample rate if somehow incorrect. --- src/audio/PortAudioDevice.cpp | 8 +++++++- src/audio/PortAudioEngine.cpp | 18 ++++++++++++++++++ src/audio/PulseAudioEngine.cpp | 23 ++++++++++++++++++++++- src/main.cpp | 17 +++++++++++++++++ 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/audio/PortAudioDevice.cpp b/src/audio/PortAudioDevice.cpp index 1b47e0883..9caa0170f 100644 --- a/src/audio/PortAudioDevice.cpp +++ b/src/audio/PortAudioDevice.cpp @@ -43,7 +43,13 @@ PortAudioDevice::PortAudioDevice(int deviceId, IAudioEngine::AudioDirection dire // of erroring out, let's just use the device's default sample rate // instead to prevent users from needing to reconfigure as much as // possible. - if (hostApiName.find("Windows WASAPI") != std::string::npos) + // + // Additionally, if we somehow get a sample rate of 0 (which normally + // wouldn't happen, but just in case), use the default sample rate + // as well. The correct sample rate will eventually be retrieved by + // higher level code and re-saved. + if (hostApiName.find("Windows WASAPI") != std::string::npos || + sampleRate == 0) { sampleRate_ = deviceInfo->defaultSampleRate; } diff --git a/src/audio/PortAudioEngine.cpp b/src/audio/PortAudioEngine.cpp index a9fbca2fc..dace2c77c 100644 --- a/src/audio/PortAudioEngine.cpp +++ b/src/audio/PortAudioEngine.cpp @@ -163,6 +163,24 @@ std::shared_ptr PortAudioEngine::getAudioDevice(wxString deviceNam { auto deviceList = getAudioDeviceList(direction); + auto supportedSampleRates = getSupportedSampleRates(deviceName, direction); + bool found = false; + for (auto& rate : supportedSampleRates) + { + if (rate == sampleRate) + { + found = true; + break; + } + } + + if (!found) + { + // Zero out the input sample rate. The device object will use the default sample rate + // instead. + sampleRate = 0; + } + for (auto& dev : deviceList) { if (dev.name.Find(deviceName) == 0) diff --git a/src/audio/PulseAudioEngine.cpp b/src/audio/PulseAudioEngine.cpp index 4c9afe7db..1fb7dee5e 100644 --- a/src/audio/PulseAudioEngine.cpp +++ b/src/audio/PulseAudioEngine.cpp @@ -215,7 +215,11 @@ std::vector PulseAudioEngine::getSupportedSampleRates(wxString deviceName, int index = 0; while (IAudioEngine::StandardSampleRates[index] != -1) { - result.push_back(IAudioEngine::StandardSampleRates[index++]); + if (IAudioEngine::StandardSampleRates[index] <= 192000) + { + result.push_back(IAudioEngine::StandardSampleRates[index]); + } + index++; } return result; @@ -269,10 +273,27 @@ std::shared_ptr PulseAudioEngine::getAudioDevice(wxString deviceNa { auto deviceList = getAudioDeviceList(direction); + auto supportedSampleRates = getSupportedSampleRates(deviceName, direction); + bool found = false; + for (auto& rate : supportedSampleRates) + { + if (rate == sampleRate) + { + found = true; + break; + } + } + for (auto& dev : deviceList) { if (dev.name == deviceName) { + if (!found) + { + // Use device's default sample rate if we somehow got an unsupported one. + sampleRate = dev.defaultSampleRate; + } + auto devObj = new PulseAudioDevice( mainloop_, context_, deviceName, direction, sampleRate, diff --git a/src/main.cpp b/src/main.cpp index 293544186..8a4cc4303 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2499,6 +2499,13 @@ void MainFrame::startRxStream() return; } + else + { + // Re-save sample rates in case they were somehow invalid before + // device creation. + wxGetApp().appConfiguration.audioConfiguration.soundCard1In.sampleRate = rxInSoundDevice->getSampleRate(); + wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate = rxOutSoundDevice->getSampleRate(); + } } else { @@ -2602,6 +2609,16 @@ void MainFrame::startRxStream() return; } + else + { + // Re-save sample rates in case they were somehow invalid before + // device creation. + wxGetApp().appConfiguration.audioConfiguration.soundCard1In.sampleRate = rxInSoundDevice->getSampleRate(); + wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.sampleRate = rxOutSoundDevice->getSampleRate(); + + wxGetApp().appConfiguration.audioConfiguration.soundCard2In.sampleRate = txInSoundDevice->getSampleRate(); + wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate = txOutSoundDevice->getSampleRate(); + } } // Init call back data structure ---------------------------------------------- From 52f94b68dff7b64efc3f2c7ca6e7b2fb909f4759 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Tue, 23 Jan 2024 23:59:03 -0800 Subject: [PATCH 04/10] Add logic to detect minimum number of audio channels available. --- src/audio/AudioDeviceSpecification.h | 3 ++- src/audio/PortAudioEngine.cpp | 40 ++++++++++++++++++++++++++-- src/audio/PulseAudioEngine.cpp | 7 +++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/audio/AudioDeviceSpecification.h b/src/audio/AudioDeviceSpecification.h index 303de1ba1..2e3428c0f 100644 --- a/src/audio/AudioDeviceSpecification.h +++ b/src/audio/AudioDeviceSpecification.h @@ -33,9 +33,10 @@ struct AudioDeviceSpecification wxString apiName; int defaultSampleRate; int maxChannels; + int minChannels; bool isValid() const; static AudioDeviceSpecification GetInvalidDevice(); }; -#endif // AUDIO_DEVICE_SPECIFICATION_H \ No newline at end of file +#endif // AUDIO_DEVICE_SPECIFICATION_H diff --git a/src/audio/PortAudioEngine.cpp b/src/audio/PortAudioEngine.cpp index dace2c77c..a0394bb0c 100644 --- a/src/audio/PortAudioEngine.cpp +++ b/src/audio/PortAudioEngine.cpp @@ -86,10 +86,41 @@ std::vector PortAudioEngine::getAudioDeviceList(AudioD if ((direction == AUDIO_ENGINE_IN && deviceInfo->maxInputChannels > 0) || (direction == AUDIO_ENGINE_OUT && deviceInfo->maxOutputChannels > 0)) { + // Detect the minimum number of channels available as PortAudio doesn't + // provide this info. This should in theory be 1 but at least one device + // (Focusrite Scarlett) will not accept anything less than 4 channels + // on Windows. + PaStreamParameters streamParameters; + streamParameters.device = index; + streamParameters.channelCount = + direction == AUDIO_ENGINE_IN ? deviceInfo->maxInputChannels : deviceInfo->maxOutputChannels; + streamParameters.sampleFormat = paInt16; + streamParameters.suggestedLatency = Pa_GetDeviceInfo(index)->defaultHighInputLatency; + streamParameters.hostApiSpecificStreamInfo = NULL; + + while (streamParameters.channelCount >= 1) + { + PaError err = Pa_IsFormatSupported( + direction == AUDIO_ENGINE_IN ? &streamParameters : NULL, + direction == AUDIO_ENGINE_OUT ? &streamParameters : NULL, + deviceInfo->defaultSampleRate); + + if (err == paFormatIsSupported) + { + streamParameters.channelCount--; + } + else + { + break; + } + } + + // Add information about this device to the result array. AudioDeviceSpecification device; device.deviceId = index; device.name = wxString::FromUTF8(deviceInfo->name); device.apiName = hostApiName; + device.minChannels = streamParameters.channelCount + 1; device.maxChannels = direction == AUDIO_ENGINE_IN ? deviceInfo->maxInputChannels : deviceInfo->maxOutputChannels; device.defaultSampleRate = deviceInfo->defaultSampleRate; @@ -113,7 +144,7 @@ std::vector PortAudioEngine::getSupportedSampleRates(wxString deviceName, A PaStreamParameters streamParameters; streamParameters.device = device.deviceId; - streamParameters.channelCount = 1; + streamParameters.channelCount = device.minChannels; streamParameters.sampleFormat = paInt16; streamParameters.suggestedLatency = Pa_GetDeviceInfo(device.deviceId)->defaultHighInputLatency; streamParameters.hostApiSpecificStreamInfo = NULL; @@ -185,7 +216,12 @@ std::shared_ptr PortAudioEngine::getAudioDevice(wxString deviceNam { if (dev.name.Find(deviceName) == 0) { - auto devObj = new PortAudioDevice(dev.deviceId, direction, sampleRate, dev.maxChannels >= numChannels ? numChannels : dev.maxChannels); + // Ensure that the passed-in number of channels is within the allowed range. + numChannels = std::max(numChannels, dev.minChannels); + numChannels = std::min(numChannels, dev.maxChannels); + + // Create device object. + auto devObj = new PortAudioDevice(dev.deviceId, direction, sampleRate, numChannels); return std::shared_ptr(devObj); } } diff --git a/src/audio/PulseAudioEngine.cpp b/src/audio/PulseAudioEngine.cpp index 1fb7dee5e..e09e00fff 100644 --- a/src/audio/PulseAudioEngine.cpp +++ b/src/audio/PulseAudioEngine.cpp @@ -167,6 +167,7 @@ std::vector PulseAudioEngine::getAudioDeviceList(Audio device.name = i->name; device.apiName = "PulseAudio"; device.maxChannels = i->sample_spec.channels; + device.minChannels = 1; // TBD: can minimum be >1 on PulseAudio or pipewire? device.defaultSampleRate = i->sample_spec.rate; tempObj->result.push_back(device); @@ -189,6 +190,7 @@ std::vector PulseAudioEngine::getAudioDeviceList(Audio device.name = i->name; device.apiName = "PulseAudio"; device.maxChannels = i->sample_spec.channels; + device.minChannels = 1; // TBD: can minimum be >1 on PulseAudio or pipewire? device.defaultSampleRate = i->sample_spec.rate; tempObj->result.push_back(device); @@ -294,6 +296,11 @@ std::shared_ptr PulseAudioEngine::getAudioDevice(wxString deviceNa sampleRate = dev.defaultSampleRate; } + // Cap number of channels to allowed range. + numChannels = std::max(numChannels, dev.minChannels); + numChannels = std::min(numChannels, dev.maxChannels); + + // Create device object. auto devObj = new PulseAudioDevice( mainloop_, context_, deviceName, direction, sampleRate, From 67675eb9b6e152104a9264767225c4136b26a609 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Wed, 24 Jan 2024 00:04:36 -0800 Subject: [PATCH 05/10] Optimize for most common case (min channels = 1). --- src/audio/PortAudioEngine.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/audio/PortAudioEngine.cpp b/src/audio/PortAudioEngine.cpp index a0394bb0c..58778561f 100644 --- a/src/audio/PortAudioEngine.cpp +++ b/src/audio/PortAudioEngine.cpp @@ -92,13 +92,13 @@ std::vector PortAudioEngine::getAudioDeviceList(AudioD // on Windows. PaStreamParameters streamParameters; streamParameters.device = index; - streamParameters.channelCount = - direction == AUDIO_ENGINE_IN ? deviceInfo->maxInputChannels : deviceInfo->maxOutputChannels; + streamParameters.channelCount = 1; streamParameters.sampleFormat = paInt16; streamParameters.suggestedLatency = Pa_GetDeviceInfo(index)->defaultHighInputLatency; streamParameters.hostApiSpecificStreamInfo = NULL; - while (streamParameters.channelCount >= 1) + int maxChannels = direction == AUDIO_ENGINE_IN ? deviceInfo->maxInputChannels : deviceInfo->maxOutputChannels; + while (streamParameters.channelCount < maxChannels) { PaError err = Pa_IsFormatSupported( direction == AUDIO_ENGINE_IN ? &streamParameters : NULL, @@ -106,13 +106,11 @@ std::vector PortAudioEngine::getAudioDeviceList(AudioD deviceInfo->defaultSampleRate); if (err == paFormatIsSupported) - { - streamParameters.channelCount--; - } - else { break; } + + streamParameters.channelCount++; } // Add information about this device to the result array. @@ -120,7 +118,7 @@ std::vector PortAudioEngine::getAudioDeviceList(AudioD device.deviceId = index; device.name = wxString::FromUTF8(deviceInfo->name); device.apiName = hostApiName; - device.minChannels = streamParameters.channelCount + 1; + device.minChannels = streamParameters.channelCount; device.maxChannels = direction == AUDIO_ENGINE_IN ? deviceInfo->maxInputChannels : deviceInfo->maxOutputChannels; device.defaultSampleRate = deviceInfo->defaultSampleRate; From df5498d5071d7532d31e44bece4eb85a33abc480 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Wed, 24 Jan 2024 00:16:46 -0800 Subject: [PATCH 06/10] Update audio handler to properly handle >2 channels. --- src/main.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 8a4cc4303..5fe766b37 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2820,12 +2820,12 @@ void MainFrame::startRxStream() int result = codec2_fifo_read(cbData->outfifo1, outdata, size); if (result == 0) { - // write signal to both channels if the device can support two channels. + // write signal to all channels if the device can support 2+ channels. // Otherwise, we assume we're only dealing with one channel and write // only to that channel. - if (dev.getNumChannels() == 2) + if (dev.getNumChannels() >= 2) { - for(size_t i = 0; i < size; i++, audioData += 2) + for(size_t i = 0; i < size; i++, audioData += dev.getNumChannels()) { if (cbData->leftChannelVoxTone) { @@ -2836,7 +2836,10 @@ void MainFrame::startRxStream() else audioData[0] = outdata[i]; - audioData[1] = outdata[i]; + for (auto j = 1; j < dev.getNumChannels(); j++) + { + audioData[j] = outdata[i]; + } } } else From 7105ee862deb7e17871e760e9a92e411072fc1d6 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Wed, 24 Jan 2024 00:51:31 -0800 Subject: [PATCH 07/10] Add missed error handler for the RX output sound device. --- src/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.cpp b/src/main.cpp index 5fe766b37..a6bef82ae 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2746,6 +2746,7 @@ void MainFrame::startRxStream() }, nullptr); rxInSoundDevice->setOnAudioError(errorCallback, nullptr); + rxOutSoundDevice->setOnAudioError(errorCallback, nullptr); if (txInSoundDevice && txOutSoundDevice) { From 710e5bcbaebb443e4b7665b48001446b328ebe30 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Wed, 24 Jan 2024 01:14:11 -0800 Subject: [PATCH 08/10] Changelog update based on recent bugs. --- USER_MANUAL.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/USER_MANUAL.md b/USER_MANUAL.md index cf0200dba..8c02d0841 100644 --- a/USER_MANUAL.md +++ b/USER_MANUAL.md @@ -893,7 +893,8 @@ LDPC | Low Density Parity Check Codes - a family of powerful FEC codes 1. Bugfixes: * Prevent unnecessary recreation of resamplers in analog mode. (PR #661) - * Better handle high sample rate audio devices. (PR #668) + * Better handle high sample rate audio devices and those with >2 channels. (PR #668) + * Fix issue preventing errors from being displayed for issues involving the FreeDV->Speaker sound device. (PR #668) 2. Enhancements: * Add Frequency column to RX drop-down. (PR #663) 3. Code cleanup: From 72bdf93fddfffcb45d4925608464343b151b0880 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Wed, 24 Jan 2024 21:02:26 -0800 Subject: [PATCH 09/10] Check for default device validity before overwriting configuration after failed device check. --- src/main.cpp | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index a6bef82ae..5fe8bc37e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3077,15 +3077,20 @@ bool MainFrame::validateSoundCardSetup() { if (g_nSoundCards == 1) { - if (!soundCard1OutDevice) + if (!soundCard1OutDevice && defaultOutputDevice.isValid()) { wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName = defaultOutputDevice.name; wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate = defaultOutputDevice.defaultSampleRate; } + else + { + wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName = "none"; + wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate = 0; + } } else if (g_nSoundCards == 2) { - if (!soundCard2InDevice) + if (!soundCard2InDevice && defaultInputDevice.isValid()) { // If we're not already using the default input device as the radio input device, use that instead. if (defaultInputDevice.name != wxGetApp().appConfiguration.audioConfiguration.soundCard1In.deviceName) @@ -3096,10 +3101,16 @@ bool MainFrame::validateSoundCardSetup() else { wxGetApp().appConfiguration.audioConfiguration.soundCard2In.deviceName = "none"; + wxGetApp().appConfiguration.audioConfiguration.soundCard2In.sampleRate = 0; } } + else + { + wxGetApp().appConfiguration.audioConfiguration.soundCard2In.deviceName = "none"; + wxGetApp().appConfiguration.audioConfiguration.soundCard2In.sampleRate = 0; + } - if (!soundCard2OutDevice) + if (!soundCard2OutDevice && defaultOutputDevice.isValid()) { // If we're not already using the default output device as the radio input device, use that instead. if (defaultOutputDevice.name != wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName) @@ -3110,8 +3121,14 @@ bool MainFrame::validateSoundCardSetup() else { wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.deviceName = "none"; + wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.sampleRate = 0; } } + else + { + wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.deviceName = "none"; + wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.sampleRate = 0; + } if (wxGetApp().appConfiguration.audioConfiguration.soundCard2In.deviceName == "none" && wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.deviceName == "none") { From f33d5d22c1c58ddd22c2f7405954b40883bc7b67 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Wed, 24 Jan 2024 21:03:37 -0800 Subject: [PATCH 10/10] Add additional fixed bug to changelog. --- USER_MANUAL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/USER_MANUAL.md b/USER_MANUAL.md index 06ec2ea76..e8911d86b 100644 --- a/USER_MANUAL.md +++ b/USER_MANUAL.md @@ -895,6 +895,7 @@ LDPC | Low Density Parity Check Codes - a family of powerful FEC codes * Prevent unnecessary recreation of resamplers in analog mode. (PR #661) * Better handle high sample rate audio devices and those with >2 channels. (PR #668) * Fix issue preventing errors from being displayed for issues involving the FreeDV->Speaker sound device. (PR #668) + * Fix issue resulting in incorrect audio device usage after validation failure if no valid default exists. (PR #668) 2. Enhancements: * Add Frequency column to RX drop-down. (PR #663) * Update tooltip for the free form text field to indicate that it's not covered by FEC. (PR #665)