Skip to content

Commit

Permalink
Merge pull request #668 from drowe67/ms-high-sample-rate
Browse files Browse the repository at this point in the history
Better handle high sample rate audio devices.
  • Loading branch information
tmiw authored Jan 25, 2024
2 parents b8488f5 + f33d5d2 commit b7bb816
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 29 deletions.
3 changes: 3 additions & 0 deletions USER_MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion src/audio/AudioDeviceSpecification.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
#endif // AUDIO_DEVICE_SPECIFICATION_H
3 changes: 3 additions & 0 deletions src/audio/IAudioEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ int IAudioEngine::StandardSampleRates[] =
16000, 22050,
24000, 32000,
44100, 48000,
88200, 96000,
176400, 192000,
352800, 384000,
-1 // negative terminated list
};

Expand Down
8 changes: 7 additions & 1 deletion src/audio/PortAudioDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
56 changes: 54 additions & 2 deletions src/audio/PortAudioEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,39 @@ std::vector<AudioDeviceSpecification> 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;
Expand All @@ -113,7 +142,7 @@ std::vector<int> 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;
Expand Down Expand Up @@ -163,11 +192,34 @@ std::shared_ptr<IAudioDevice> 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<IAudioDevice>(devObj);
}
}
Expand Down
30 changes: 29 additions & 1 deletion src/audio/PulseAudioEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ std::vector<AudioDeviceSpecification> 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);
Expand All @@ -189,6 +190,7 @@ std::vector<AudioDeviceSpecification> 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);
Expand All @@ -215,7 +217,11 @@ std::vector<int> 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;
Expand Down Expand Up @@ -269,10 +275,32 @@ std::shared_ptr<IAudioDevice> 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,
Expand Down
52 changes: 45 additions & 7 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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 ----------------------------------------------
Expand Down Expand Up @@ -2729,6 +2746,7 @@ void MainFrame::startRxStream()
}, nullptr);

rxInSoundDevice->setOnAudioError(errorCallback, nullptr);
rxOutSoundDevice->setOnAudioError(errorCallback, nullptr);

if (txInSoundDevice && txOutSoundDevice)
{
Expand Down Expand Up @@ -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)
{
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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")
{
Expand Down
27 changes: 10 additions & 17 deletions src/pipeline/TxRxThread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 --------------------------------------------
//
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 --------------------------------------------
//
Expand All @@ -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) ||
Expand Down

0 comments on commit b7bb816

Please sign in to comment.