diff --git a/README.md b/README.md index 8aa1b05..f082796 100644 --- a/README.md +++ b/README.md @@ -249,16 +249,10 @@ view `coverage/index.html` in a browser. ## Roadmap - 0. ALSA backend for microphone example is broken - 0. Add some builtin channel layouts from - https://developer.apple.com/library/mac/documentation/MusicAudio/Reference/CoreAudioDataTypesRef/#//apple_ref/doc/constant_group/Audio_Channel_Layout_Tags - 0. Make sure sending bogus device id results in "SoundIoErrorNoSuchDevice" on each backend - 0. Make sure PulseAudio can handle refresh devices crashing before - block_until_have_devices - 0. CoreAudio exposes a list of min/max pairs of supported sample rates. libsoundio - should do the same. 0. implement WASAPI (Windows) backend, get examples working 0. implement ASIO (Windows) backend, get examples working + 0. Make sure PulseAudio can handle refresh devices crashing before + block_until_have_devices 0. Do we really want `period_duration` in the API? 0. Integrate into libgroove and test with Groove Basin 0. clear buffer maybe could take an argument to say how many frames to not clear diff --git a/example/sio_list_devices.c b/example/sio_list_devices.c index 0ee3546..4dc0fc8 100644 --- a/example/sio_list_devices.c +++ b/example/sio_list_devices.c @@ -50,8 +50,12 @@ static void print_device(struct SoundIoDevice *device, bool is_default) { fprintf(stderr, "\n"); } - fprintf(stderr, " min sample rate: %d\n", device->sample_rate_min); - fprintf(stderr, " max sample rate: %d\n", device->sample_rate_max); + fprintf(stderr, " sample rates:\n"); + for (int i = 0; i < device->sample_rate_count; i += 1) { + struct SoundIoSampleRateRange *range = &device->sample_rates[i]; + fprintf(stderr, " %d - %d\n", range->min, range->max); + + } if (device->sample_rate_current) fprintf(stderr, " current sample rate: %d\n", device->sample_rate_current); fprintf(stderr, " formats: "); diff --git a/example/sio_microphone.c b/example/sio_microphone.c index 6fc4c4e..88a57c9 100644 --- a/example/sio_microphone.c +++ b/example/sio_microphone.c @@ -39,6 +39,14 @@ static enum SoundIoFormat prioritized_formats[] = { SoundIoFormatInvalid, }; +static int prioritized_sample_rates[] = { + 48000, + 44100, + 96000, + 24000, + 0, +}; + __attribute__ ((cold)) __attribute__ ((noreturn)) @@ -274,11 +282,15 @@ int main(int argc, char **argv) { if (!layout) panic("channel layouts not compatible"); - - int sample_rate = 48000; - if (in_device->sample_rate_max < sample_rate) sample_rate = in_device->sample_rate_max; - if (out_device->sample_rate_max < sample_rate) sample_rate = out_device->sample_rate_max; - if (in_device->sample_rate_min > sample_rate || out_device->sample_rate_min > sample_rate) + int *sample_rate; + for (sample_rate = prioritized_sample_rates; *sample_rate; sample_rate += 1) { + if (soundio_device_supports_sample_rate(in_device, *sample_rate) && + soundio_device_supports_sample_rate(out_device, *sample_rate)) + { + break; + } + } + if (!*sample_rate) panic("incompatible sample rates"); enum SoundIoFormat *fmt; @@ -296,7 +308,7 @@ int main(int argc, char **argv) { if (!instream) panic("out of memory"); instream->format = *fmt; - instream->sample_rate = sample_rate; + instream->sample_rate = *sample_rate; instream->layout = *layout; instream->period_duration = microphone_latency / 4.0; instream->read_callback = read_callback; @@ -308,7 +320,7 @@ int main(int argc, char **argv) { if (!outstream) panic("out of memory"); outstream->format = *fmt; - outstream->sample_rate = sample_rate; + outstream->sample_rate = *sample_rate; outstream->layout = *layout; outstream->buffer_duration = microphone_latency; outstream->write_callback = write_callback; diff --git a/soundio/soundio.h b/soundio/soundio.h index 89473f9..b84258d 100644 --- a/soundio/soundio.h +++ b/soundio/soundio.h @@ -238,7 +238,13 @@ struct SoundIoChannelLayout { enum SoundIoChannelId channels[SOUNDIO_MAX_CHANNELS]; }; -// The size of this struct is not part of the API or ABI. +// The size of this struct is OK to use. +struct SoundIoSampleRateRange { + int min; + int max; +}; + +// The size of this struct is OK to use. struct SoundIoChannelArea { // Base address of buffer. char *ptr; @@ -336,10 +342,11 @@ struct SoundIoDevice { // Sample rate is the number of frames per second. // Sample rate is handled very similar to sample format; see those docs. // If sample rate information is missing due to a probe error, the field - // will be set to zero. - // Devices are guaranteed to have at least 1 sample rate available. - int sample_rate_min; - int sample_rate_max; + // will be set to 0 or NULL. + // Devices which have `probe_error` set to `SoundIoErrorNone` are + // guaranteed to have at least 1 sample rate available. + struct SoundIoSampleRateRange *sample_rates; + int sample_rate_count; int sample_rate_current; // Buffer duration in seconds. If `buffer_duration_current` is unknown or @@ -705,6 +712,16 @@ bool soundio_device_supports_format(struct SoundIoDevice *device, bool soundio_device_supports_layout(struct SoundIoDevice *device, const struct SoundIoChannelLayout *layout); +// Convenience function. Returns whether `sample_rate` is included in the +// device's supported sample rates. +bool soundio_device_supports_sample_rate(struct SoundIoDevice *device, + int sample_rate); + +// Convenience function. Returns the available sample rate nearest to +// `sample_rate`, rounding up. +int soundio_device_nearest_sample_rate(struct SoundIoDevice *device, + int sample_rate); + // Output Streams diff --git a/src/alsa.cpp b/src/alsa.cpp index 576ed6d..6d786a8 100644 --- a/src/alsa.cpp +++ b/src/alsa.cpp @@ -252,6 +252,7 @@ static int set_access(snd_pcm_t *handle, snd_pcm_hw_params_t *hwparams, snd_pcm_ static int probe_open_device(SoundIoDevice *device, snd_pcm_t *handle, int resample, int *out_channels_min, int *out_channels_max) { + SoundIoDevicePrivate *dev = (SoundIoDevicePrivate *)device; int err; snd_pcm_hw_params_t *hwparams; @@ -286,8 +287,10 @@ static int probe_open_device(SoundIoDevice *device, snd_pcm_t *handle, int resam if ((err = snd_pcm_hw_params_set_rate_last(handle, hwparams, &rate_max, nullptr)) < 0) return SoundIoErrorOpeningDevice; - device->sample_rate_min = rate_min; - device->sample_rate_max = rate_max; + device->sample_rate_count = 1; + device->sample_rates = &dev->prealloc_sample_rate_range; + device->sample_rates[0].min = rate_min; + device->sample_rates[0].max = rate_max; double one_over_actual_rate = 1.0 / (double)rate_max; @@ -437,8 +440,8 @@ static int probe_device(SoundIoDevice *device, snd_pcm_chmap_query_t **maps) { maps = nullptr; if (!device->is_raw) { - if (device->sample_rate_min == device->sample_rate_max) - device->sample_rate_current = device->sample_rate_min; + if (device->sample_rates[0].min == device->sample_rates[0].max) + device->sample_rate_current = device->sample_rates[0].min; if (device->buffer_duration_min == device->buffer_duration_max) device->buffer_duration_current = device->buffer_duration_min; diff --git a/src/coreaudio.cpp b/src/coreaudio.cpp index 92834ec..cf7521a 100644 --- a/src/coreaudio.cpp +++ b/src/coreaudio.cpp @@ -642,44 +642,58 @@ static int refresh_devices(struct SoundIoPrivate *si) { } rd.device->sample_rate_current = (int)floored_value; - prop_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; - prop_address.mScope = aim_to_scope(aim); - prop_address.mElement = kAudioObjectPropertyElementMaster; - if ((os_err = AudioObjectGetPropertyDataSize(device_id, &prop_address, 0, nullptr, - &io_size))) - { - deinit_refresh_devices(&rd); - return SoundIoErrorOpeningDevice; - } - int avr_array_len = io_size / sizeof(AudioValueRange); - rd.avr_array = (AudioValueRange*)allocate(io_size); - - if (!rd.avr_array) { - deinit_refresh_devices(&rd); - return SoundIoErrorNoMem; - } - - if ((os_err = AudioObjectGetPropertyData(device_id, &prop_address, 0, nullptr, - &io_size, rd.avr_array))) - { - deinit_refresh_devices(&rd); - return SoundIoErrorOpeningDevice; - } - - for (int i = 0; i < avr_array_len; i += 1) { - AudioValueRange *avr = &rd.avr_array[i]; - int min_val = ceil(avr->mMinimum); - int max_val = floor(avr->mMaximum); - if (rd.device->sample_rate_min == 0 || min_val < rd.device->sample_rate_min) - rd.device->sample_rate_min = min_val; - if (rd.device->sample_rate_max == 0 || max_val > rd.device->sample_rate_max) - rd.device->sample_rate_max = max_val; - } // If you try to open an input stream with anything but the current // nominal sample rate, AudioUnitRender returns an error. if (aim == SoundIoDeviceAimInput) { - rd.device->sample_rate_min = rd.device->sample_rate_current; - rd.device->sample_rate_max = rd.device->sample_rate_current; + device->sample_rate_count = 1; + device->sample_rates = &dev->prealloc_sample_rate_range; + device->sample_rates[0].min = rd.device->sample_rate_current; + device->sample_rates[0].max = rd.device->sample_rate_current; + } else { + prop_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; + prop_address.mScope = aim_to_scope(aim); + prop_address.mElement = kAudioObjectPropertyElementMaster; + if ((os_err = AudioObjectGetPropertyDataSize(device_id, &prop_address, 0, nullptr, + &io_size))) + { + deinit_refresh_devices(&rd); + return SoundIoErrorOpeningDevice; + } + int avr_array_len = io_size / sizeof(AudioValueRange); + rd.avr_array = (AudioValueRange*)allocate(io_size); + + if (!rd.avr_array) { + deinit_refresh_devices(&rd); + return SoundIoErrorNoMem; + } + + if ((os_err = AudioObjectGetPropertyData(device_id, &prop_address, 0, nullptr, + &io_size, rd.avr_array))) + { + deinit_refresh_devices(&rd); + return SoundIoErrorOpeningDevice; + } + + if (avr_array_len == 1) { + device->sample_rate_count = 1; + device->sample_rates = &dev->prealloc_sample_rate_range; + device->sample_rates[0].min = ceil(avr->mMinimum); + device->sample_rates[0].max = floor(avr->mMaximum); + } else { + device->sample_rate_count = avr_array_len; + device->sample_rates = allocate(avr_array_len); + if (!device->sample_rates) { + deinit_refresh_devices(&rd); + return SoundIoErrorNoMem; + } + for (int i = 0; i < avr_array_len; i += 1) { + AudioValueRange *avr = &rd.avr_array[i]; + int min_val = ceil(avr->mMinimum); + int max_val = floor(avr->mMaximum); + device->sample_rates[i].min = min_val; + device->sample_rates[i].max = max_val; + } + } } prop_address.mSelector = kAudioDevicePropertyBufferFrameSize; diff --git a/src/dummy.cpp b/src/dummy.cpp index 0534664..51a25ac 100644 --- a/src/dummy.cpp +++ b/src/dummy.cpp @@ -363,6 +363,14 @@ static int set_all_device_formats(SoundIoDevice *device) { return 0; } +static void set_all_device_sample_rates(SoundIoDevice *device) { + SoundIoDevicePrivate *dev = (SoundIoDevicePrivate *)device; + device->sample_rate_count = 1; + device->sample_rates = &dev->prealloc_sample_rate_range; + device->sample_rates[0].min = 8000; + device->sample_rates[0].max = 5644800; +} + static int set_all_device_channel_layouts(SoundIoDevice *device) { device->layout_count = soundio_channel_layout_builtin_count(); device->layouts = allocate(device->layout_count); @@ -429,12 +437,11 @@ int soundio_dummy_init(SoundIoPrivate *si) { destroy_dummy(si); return err; } + set_all_device_sample_rates(device); device->buffer_duration_min = 0.01; device->buffer_duration_max = 4; device->buffer_duration_current = 0.1; - device->sample_rate_min = 2; - device->sample_rate_max = 5644800; device->sample_rate_current = 48000; device->period_duration_min = 0.01; device->period_duration_max = 2; @@ -479,11 +486,10 @@ int soundio_dummy_init(SoundIoPrivate *si) { destroy_dummy(si); return err; } + set_all_device_sample_rates(device); device->buffer_duration_min = 0.01; device->buffer_duration_max = 4; device->buffer_duration_current = 0.1; - device->sample_rate_min = 2; - device->sample_rate_max = 5644800; device->sample_rate_current = 48000; device->period_duration_min = 0.01; device->period_duration_max = 2; diff --git a/src/jack.cpp b/src/jack.cpp index 8f51750..217a36b 100644 --- a/src/jack.cpp +++ b/src/jack.cpp @@ -192,8 +192,10 @@ static int refresh_devices_bare(SoundIoPrivate *si) { device->format_count = 1; device->formats = allocate(1); device->current_format = SoundIoFormatFloat32NE; - device->sample_rate_min = sij->sample_rate; - device->sample_rate_max = sij->sample_rate; + device->sample_rate_count = 1; + device->sample_rates = &dev->prealloc_sample_rate_range; + device->sample_rates[0].min = sij->sample_rate; + device->sample_rates[0].max = sij->sample_rate; device->sample_rate_current = sij->sample_rate; device->period_duration_min = sij->period_size / (double) sij->sample_rate; device->period_duration_max = device->period_duration_min; diff --git a/src/pulseaudio.cpp b/src/pulseaudio.cpp index 34601e3..371bbc4 100644 --- a/src/pulseaudio.cpp +++ b/src/pulseaudio.cpp @@ -294,8 +294,10 @@ static void sink_info_callback(pa_context *pulse_context, const pa_sink_info *in device->sample_rate_current = info->sample_spec.rate; // PulseAudio performs resampling, so any value is valid. Let's pick // some reasonable min and max values. - device->sample_rate_min = min(8000, device->sample_rate_current); - device->sample_rate_max = max(5644800, device->sample_rate_current); + device->sample_rate_count = 1; + device->sample_rates = &dev->prealloc_sample_rate_range; + device->sample_rates[0].min = min(8000, device->sample_rate_current); + device->sample_rates[0].max = max(5644800, device->sample_rate_current); device->current_format = from_pulseaudio_format(info->sample_spec); // PulseAudio performs sample format conversion, so any PulseAudio @@ -364,8 +366,10 @@ static void source_info_callback(pa_context *pulse_context, const pa_source_info device->sample_rate_current = info->sample_spec.rate; // PulseAudio performs resampling, so any value is valid. Let's pick // some reasonable min and max values. - device->sample_rate_min = min(8000, device->sample_rate_current); - device->sample_rate_max = max(5644800, device->sample_rate_current); + device->sample_rate_count = 1; + device->sample_rates = &dev->prealloc_sample_rate_range; + device->sample_rates[0].min = min(8000, device->sample_rate_current); + device->sample_rates[0].max = max(5644800, device->sample_rate_current); device->current_format = from_pulseaudio_format(info->sample_spec); // PulseAudio performs sample format conversion, so any PulseAudio diff --git a/src/soundio.cpp b/src/soundio.cpp index ac01d17..84e86bc 100644 --- a/src/soundio.cpp +++ b/src/soundio.cpp @@ -368,6 +368,9 @@ void soundio_device_unref(struct SoundIoDevice *device) { free(device->formats); free(device->layouts); + if (device->sample_rates != &dev->prealloc_sample_rate_range) + free(device->sample_rates); + free(dev); } } @@ -451,7 +454,7 @@ int soundio_outstream_open(struct SoundIoOutStream *outstream) { } if (!outstream->sample_rate) - outstream->sample_rate = clamp(device->sample_rate_min, 48000, device->sample_rate_max); + outstream->sample_rate = soundio_device_nearest_sample_rate(device, 48000); if (!outstream->name) outstream->name = "SoundIoOutStream"; @@ -537,7 +540,7 @@ int soundio_instream_open(struct SoundIoInStream *instream) { } if (!instream->sample_rate) - instream->sample_rate = clamp(device->sample_rate_min, 48000, device->sample_rate_max); + instream->sample_rate = soundio_device_nearest_sample_rate(device, 48000); if (!instream->name) instream->name = "SoundIoInStream"; @@ -687,3 +690,32 @@ bool soundio_device_supports_layout(struct SoundIoDevice *device, } return false; } + +bool soundio_device_supports_sample_rate(struct SoundIoDevice *device, int sample_rate) { + for (int i = 0; i < device->sample_rate_count; i += 1) { + SoundIoSampleRateRange *range = &device->sample_rates[i]; + if (sample_rate >= range->min && sample_rate <= range->max) + return true; + } + return false; +} + +int soundio_device_nearest_sample_rate(struct SoundIoDevice *device, int sample_rate) { + int best_rate = -1; + int best_delta = -1; + for (int i = 0; i < device->sample_rate_count; i += 1) { + SoundIoSampleRateRange *range = &device->sample_rates[i]; + if (sample_rate < range->min) { + int delta = range->min - sample_rate; + if (best_delta == -1 || delta < best_delta) { + best_delta = delta; + best_rate = range->min; + } + } else if (best_rate == -1 && sample_rate > range->max) { + best_rate = range->max; + } else { + return sample_rate; + } + } + return best_rate; +} diff --git a/src/soundio.hpp b/src/soundio.hpp index 8ef6ab0..9adc9d6 100644 --- a/src/soundio.hpp +++ b/src/soundio.hpp @@ -147,6 +147,7 @@ struct SoundIoDevicePrivate { SoundIoDevice pub; SoundIoDeviceBackendData backend_data; void (*destruct)(SoundIoDevicePrivate *); + SoundIoSampleRateRange prealloc_sample_rate_range; }; void soundio_destroy_devices_info(struct SoundIoDevicesInfo *devices_info);