diff --git a/USER_MANUAL.md b/USER_MANUAL.md index b04e73b85..e8911d86b 100644 --- a/USER_MANUAL.md +++ b/USER_MANUAL.md @@ -893,6 +893,9 @@ 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 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) 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/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/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..58778561f 100644 --- a/src/audio/PortAudioEngine.cpp +++ b/src/audio/PortAudioEngine.cpp @@ -86,10 +86,39 @@ 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 = 1; + streamParameters.sampleFormat = paInt16; + streamParameters.suggestedLatency = Pa_GetDeviceInfo(index)->defaultHighInputLatency; + streamParameters.hostApiSpecificStreamInfo = NULL; + + int maxChannels = direction == AUDIO_ENGINE_IN ? deviceInfo->maxInputChannels : deviceInfo->maxOutputChannels; + while (streamParameters.channelCount < maxChannels) + { + PaError err = Pa_IsFormatSupported( + direction == AUDIO_ENGINE_IN ? &streamParameters : NULL, + direction == AUDIO_ENGINE_OUT ? &streamParameters : NULL, + deviceInfo->defaultSampleRate); + + if (err == paFormatIsSupported) + { + break; + } + + streamParameters.channelCount++; + } + + // 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; device.maxChannels = direction == AUDIO_ENGINE_IN ? deviceInfo->maxInputChannels : deviceInfo->maxOutputChannels; device.defaultSampleRate = deviceInfo->defaultSampleRate; @@ -113,7 +142,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; @@ -163,11 +192,34 @@ 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) { - 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 4c9afe7db..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); @@ -215,7 +217,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 +275,32 @@ 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; + } + + // 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, diff --git a/src/main.cpp b/src/main.cpp index 293544186..5fe8bc37e 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 ---------------------------------------------- @@ -2729,6 +2746,7 @@ void MainFrame::startRxStream() }, nullptr); rxInSoundDevice->setOnAudioError(errorCallback, nullptr); + rxOutSoundDevice->setOnAudioError(errorCallback, nullptr); if (txInSoundDevice && txOutSoundDevice) { @@ -2803,12 +2821,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) { @@ -2819,7 +2837,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 @@ -3056,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) @@ -3075,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) @@ -3089,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") { 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) ||