diff --git a/README.md b/README.md index 9a31d23..35ab531 100644 --- a/README.md +++ b/README.md @@ -242,7 +242,23 @@ cmake .. -DCMAKE_TOOLCHAIN_FILE=/path/to/mxe/usr/x86_64-w64-mingw32.static/share make ``` -#### Running the Tests +### Testing + +For each backend, do the following: + + 0. Run the unit tests: `./unit_tests`. To see test coverage, install lcov, run + `make coverage`, and then view `coverage/index.html` in a browser. + 0. Run the example `./sio_list_devices` and make sure it does not crash, and + the output looks good. If valgrind is available, use it. + 0. Run `./sio_list_devices --watch` and make sure it detects when you plug and + unplug a USB microphone. + 0. Run `./sio_sine` and make sure you hear a sine wave. For backends with raw + devices, run `./sio_sine --device id` (where 'id' is a device id you got + from `sio_list_devices` and make sure you hear a sine wave. + 0. Run `./underflow` and read the testing instructions that it prints. + 0. Run `./sio_microphone` and ensure that it is both recording and playing + back correctly. If possible use the `--in-device` and `--out-device` + parameters to test a USB microphone in raw mode. ``` make test diff --git a/example/sio_sine.c b/example/sio_sine.c index 9f1777e..3e21b12 100644 --- a/example/sio_sine.c +++ b/example/sio_sine.c @@ -11,6 +11,7 @@ #include #include #include +#include #include static void panic(const char *format, ...) { @@ -28,11 +29,36 @@ static int usage(char *exe) { return EXIT_FAILURE; } -static const float PI = 3.1415926535f; -static float seconds_offset = 0.0f; +static void write_sample_s16ne(char *ptr, double sample) { + int16_t *buf = (int16_t *)ptr; + double range = (double)INT16_MAX - (double)INT16_MIN; + double val = sample * range / 2.0; + *buf = val; +} + +static void write_sample_s32ne(char *ptr, double sample) { + int32_t *buf = (int32_t *)ptr; + double range = (double)INT32_MAX - (double)INT32_MIN; + double val = sample * range / 2.0; + *buf = val; +} + +static void write_sample_float32ne(char *ptr, double sample) { + float *buf = (float *)ptr; + *buf = sample; +} + +static void write_sample_float64ne(char *ptr, double sample) { + double *buf = (double *)ptr; + *buf = sample; +} + +static void (*write_sample)(char *ptr, double sample); +static const double PI = 3.14159265358979323846264338328; +static double seconds_offset = 0.0; static void write_callback(struct SoundIoOutStream *outstream, int frame_count_min, int frame_count_max) { - float float_sample_rate = outstream->sample_rate; - float seconds_per_frame = 1.0f / float_sample_rate; + double float_sample_rate = outstream->sample_rate; + double seconds_per_frame = 1.0f / float_sample_rate; struct SoundIoChannelArea *areas; int err; @@ -48,13 +74,13 @@ static void write_callback(struct SoundIoOutStream *outstream, int frame_count_m const struct SoundIoChannelLayout *layout = &outstream->layout; - float pitch = 440.0f; - float radians_per_second = pitch * 2.0f * PI; + double pitch = 440.0; + double radians_per_second = pitch * 2.0 * PI; for (int frame = 0; frame < frame_count; frame += 1) { - float sample = sinf((seconds_offset + frame * seconds_per_frame) * radians_per_second); + double sample = sinf((seconds_offset + frame * seconds_per_frame) * radians_per_second); for (int channel = 0; channel < layout->channel_count; channel += 1) { - float *ptr = (float*)(areas[channel].ptr + areas[channel].step * frame); - *ptr = sample; + write_sample(areas[channel].ptr, sample); + areas[channel].ptr += areas[channel].step; } } seconds_offset += seconds_per_frame * frame_count; @@ -158,15 +184,32 @@ int main(int argc, char **argv) { fprintf(stderr, "Output device: %s\n", device->name); struct SoundIoOutStream *outstream = soundio_outstream_create(device); - outstream->format = SoundIoFormatFloat32NE; outstream->write_callback = write_callback; outstream->underflow_callback = underflow_callback; + if (soundio_device_supports_format(device, SoundIoFormatFloat32NE)) { + outstream->format = SoundIoFormatFloat32NE; + write_sample = write_sample_float32ne; + } else if (soundio_device_supports_format(device, SoundIoFormatFloat64NE)) { + outstream->format = SoundIoFormatFloat64NE; + write_sample = write_sample_float64ne; + } else if (soundio_device_supports_format(device, SoundIoFormatS32NE)) { + outstream->format = SoundIoFormatS32NE; + write_sample = write_sample_s32ne; + } else if (soundio_device_supports_format(device, SoundIoFormatS16NE)) { + outstream->format = SoundIoFormatS16NE; + write_sample = write_sample_s16ne; + } else { + fprintf(stderr, "No suitable device format available.\n"); + return EXIT_FAILURE; + } + if ((err = soundio_outstream_open(outstream))) { fprintf(stderr, "unable to open device: %s", soundio_strerror(err)); return EXIT_FAILURE; } + if (outstream->layout_error) fprintf(stderr, "unable to set channel layout: %s\n", soundio_strerror(outstream->layout_error)); diff --git a/src/soundio.cpp b/src/soundio.cpp index cafab58..a0ea2db 100644 --- a/src/soundio.cpp +++ b/src/soundio.cpp @@ -454,9 +454,6 @@ int soundio_outstream_open(struct SoundIoOutStream *outstream) { if (device->aim != SoundIoDeviceAimOutput) return SoundIoErrorInvalid; - if (outstream->format <= SoundIoFormatInvalid) - return SoundIoErrorInvalid; - if (device->probe_error) return device->probe_error; @@ -465,6 +462,9 @@ int soundio_outstream_open(struct SoundIoOutStream *outstream) { SoundIoFormatFloat32NE : device->formats[0]; } + if (outstream->format <= SoundIoFormatInvalid) + return SoundIoErrorInvalid; + if (!outstream->layout.channel_count) { const SoundIoChannelLayout *stereo = soundio_channel_layout_get_builtin(SoundIoChannelLayoutIdStereo); outstream->layout = soundio_device_supports_layout(device, stereo) ? *stereo : device->layouts[0]; diff --git a/src/wasapi.cpp b/src/wasapi.cpp index f389477..437df77 100644 --- a/src/wasapi.cpp +++ b/src/wasapi.cpp @@ -18,8 +18,6 @@ const static GUID SOUNDIO_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT = { const static GUID SOUNDIO_KSDATAFORMAT_SUBTYPE_PCM = { 0x00000001,0x0000,0x0010, {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}}; -const static DWORD SOUNDIO_AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM = 0x80000000; - // Adding more common sample rates helps the heuristics; feel free to do that. static int test_sample_rates[] = { 8000, @@ -64,6 +62,27 @@ static SoundIoChannelLayoutId test_layouts[] = { SoundIoChannelLayoutId5Point1Back, }; +/* +static void print_possible_err_codes() { + fprintf(stderr, "AUDCLNT_E_ALREADY_INITIALIZED: %ld\n", AUDCLNT_E_ALREADY_INITIALIZED); + fprintf(stderr, "AUDCLNT_E_WRONG_ENDPOINT_TYPE: %ld\n", AUDCLNT_E_WRONG_ENDPOINT_TYPE); + fprintf(stderr, "AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED: %ld\n", AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED); + fprintf(stderr, "AUDCLNT_E_BUFFER_SIZE_ERROR: %ld\n", AUDCLNT_E_BUFFER_SIZE_ERROR); + fprintf(stderr, "AUDCLNT_E_CPUUSAGE_EXCEEDED: %ld\n", AUDCLNT_E_CPUUSAGE_EXCEEDED); + fprintf(stderr, "AUDCLNT_E_DEVICE_INVALIDATED: %ld\n", AUDCLNT_E_DEVICE_INVALIDATED); + fprintf(stderr, "AUDCLNT_E_DEVICE_IN_USE: %ld\n", AUDCLNT_E_DEVICE_IN_USE); + fprintf(stderr, "AUDCLNT_E_ENDPOINT_CREATE_FAILED: %ld\n", AUDCLNT_E_ENDPOINT_CREATE_FAILED); + fprintf(stderr, "AUDCLNT_E_INVALID_DEVICE_PERIOD: %ld\n", AUDCLNT_E_INVALID_DEVICE_PERIOD); + fprintf(stderr, "AUDCLNT_E_UNSUPPORTED_FORMAT: %ld\n", AUDCLNT_E_UNSUPPORTED_FORMAT); + fprintf(stderr, "AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED: %ld\n", AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED); + fprintf(stderr, "AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL: %ld\n", AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL); + fprintf(stderr, "AUDCLNT_E_SERVICE_NOT_RUNNING: %ld\n", AUDCLNT_E_SERVICE_NOT_RUNNING); + fprintf(stderr, "E_POINTER: %ld\n", E_POINTER); + fprintf(stderr, "E_INVALIDARG: %ld\n", E_INVALIDARG); + fprintf(stderr, "E_OUTOFMEMORY: %ld\n", E_OUTOFMEMORY); +} +*/ + // converts a windows wide string to a UTF-8 encoded char * // Possible errors: // * SoundIoErrorNoMem @@ -611,6 +630,7 @@ static int refresh_devices(SoundIoPrivate *si) { rd.device_shared->ref_count = 1; rd.device_shared->soundio = soundio; rd.device_shared->is_raw = false; + rd.device_shared->software_latency_max = 2.0; SoundIoDevicePrivate *dev_raw = allocate(1); if (!dev_raw) { @@ -624,6 +644,7 @@ static int refresh_devices(SoundIoPrivate *si) { rd.device_raw->ref_count = 1; rd.device_raw->soundio = soundio; rd.device_raw->is_raw = true; + rd.device_raw->software_latency_max = 0.5; int device_id_len; if ((err = from_lpwstr(rd.lpwstr, &rd.device_shared->id, &device_id_len))) { @@ -657,7 +678,10 @@ static int refresh_devices(SoundIoPrivate *si) { return SoundIoErrorOpeningDevice; } dev_w_shared->period_duration = from_reference_time(default_device_period); + rd.device_shared->software_latency_current = dev_w_shared->period_duration; + dev_w_raw->period_duration = from_reference_time(min_device_period); + rd.device_raw->software_latency_min = dev_w_raw->period_duration * 2; if (rd.endpoint) { @@ -954,9 +978,14 @@ static void outstream_destroy_wasapi(struct SoundIoPrivate *si, struct SoundIoOu if (osw->thread) { osw->thread_exit_flag.clear(); - soundio_os_mutex_lock(osw->mutex); - soundio_os_cond_signal(osw->cond, osw->mutex); - soundio_os_mutex_unlock(osw->mutex); + if (osw->is_raw) { + if (osw->h_event) + SetEvent(osw->h_event); + } else { + soundio_os_mutex_lock(osw->mutex); + soundio_os_cond_signal(osw->cond, osw->mutex); + soundio_os_mutex_unlock(osw->mutex); + } soundio_os_thread_destroy(osw->thread); } @@ -966,6 +995,8 @@ static void outstream_destroy_wasapi(struct SoundIoPrivate *si, struct SoundIoOu IUnknown_Release(osw->audio_clock_adjustment); if (osw->audio_client) IUnknown_Release(osw->audio_client); + if (osw->h_event) + CloseHandle(osw->h_event); soundio_os_cond_destroy(osw->cond); soundio_os_mutex_destroy(osw->mutex); @@ -1029,7 +1060,6 @@ static int outstream_open_wasapi(struct SoundIoPrivate *si, struct SoundIoOutStr CoTaskMemFree(mix_format); mix_format = nullptr; osw->need_resample = (wave_format.Format.nSamplesPerSec != (DWORD)outstream->sample_rate); - // TODO do we need SOUNDIO_AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM ? flags = osw->need_resample ? AUDCLNT_STREAMFLAGS_RATEADJUST : 0; share_mode = AUDCLNT_SHAREMODE_SHARED; periodicity = 0; @@ -1065,22 +1095,35 @@ static int outstream_open_wasapi(struct SoundIoPrivate *si, struct SoundIoOutStr CoTaskMemFree(mix_format); mix_format = nullptr; osw->need_resample = (wave_format.Format.nSamplesPerSec != (DWORD)outstream->sample_rate); - // TODO do we need SOUNDIO_AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM ? flags = osw->need_resample ? AUDCLNT_STREAMFLAGS_RATEADJUST : 0; to_wave_format_layout(&outstream->layout, &wave_format); to_wave_format_format(outstream->format, &wave_format); complete_wave_format_data(&wave_format); } - buffer_duration = to_reference_time(osw->buffer_frame_count / outstream->sample_rate); + buffer_duration = to_reference_time(osw->buffer_frame_count / (double)outstream->sample_rate); if (osw->is_raw) periodicity = buffer_duration; if (FAILED(hr = IAudioClient_Initialize(osw->audio_client, share_mode, flags, buffer_duration, periodicity, (WAVEFORMATEX*)&wave_format, nullptr))) { - outstream_destroy_wasapi(si, os); - return SoundIoErrorOpeningDevice; + if (hr == AUDCLNT_E_UNSUPPORTED_FORMAT) { + outstream_destroy_wasapi(si, os); + return SoundIoErrorIncompatibleDevice; + } else if (hr == E_OUTOFMEMORY) { + outstream_destroy_wasapi(si, os); + return SoundIoErrorNoMem; + } else { + outstream_destroy_wasapi(si, os); + return SoundIoErrorOpeningDevice; + } } + } else if (hr == AUDCLNT_E_UNSUPPORTED_FORMAT) { + outstream_destroy_wasapi(si, os); + return SoundIoErrorIncompatibleDevice; + } else if (hr == E_OUTOFMEMORY) { + outstream_destroy_wasapi(si, os); + return SoundIoErrorNoMem; } else { outstream_destroy_wasapi(si, os); return SoundIoErrorOpeningDevice; @@ -1093,7 +1136,15 @@ static int outstream_open_wasapi(struct SoundIoPrivate *si, struct SoundIoOutStr outstream->software_latency = osw->buffer_frame_count / (double)outstream->sample_rate; if (osw->is_raw) { - soundio_panic("TODO event callback"); + osw->h_event = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (!osw->h_event) { + outstream_destroy_wasapi(si, os); + return SoundIoErrorOpeningDevice; + } + if (FAILED(hr = IAudioClient_SetEventHandle(osw->audio_client, osw->h_event))) { + outstream_destroy_wasapi(si, os); + return SoundIoErrorOpeningDevice; + } } else if (osw->need_resample) { if (FAILED(hr = IAudioClient_GetService(osw->audio_client, IID_IAudioClockAdjustment, (void**)&osw->audio_clock_adjustment))) @@ -1181,16 +1232,40 @@ void outstream_shared_run(void *arg) { } } +void outstream_raw_run(void *arg) { + SoundIoOutStreamPrivate *os = (SoundIoOutStreamPrivate *) arg; + SoundIoOutStreamWasapi *osw = &os->backend_data.wasapi; + SoundIoOutStream *outstream = &os->pub; + + HRESULT hr; + + outstream->write_callback(outstream, osw->buffer_frame_count, osw->buffer_frame_count); + + if (FAILED(hr = IAudioClient_Start(osw->audio_client))) { + outstream->error_callback(outstream, SoundIoErrorStreaming); + return; + } + + for (;;) { + WaitForSingleObject(osw->h_event, INFINITE); + if (!osw->thread_exit_flag.test_and_set()) + return; + + outstream->write_callback(outstream, osw->buffer_frame_count, osw->buffer_frame_count); + } +} + static int outstream_start_wasapi(struct SoundIoPrivate *si, struct SoundIoOutStreamPrivate *os) { SoundIoOutStreamWasapi *osw = &os->backend_data.wasapi; int err; - if (osw->is_raw) { - soundio_panic("TODO start raw"); - } else { - assert(!osw->thread); + assert(!osw->thread); + osw->thread_exit_flag.test_and_set(); - osw->thread_exit_flag.test_and_set(); + if (osw->is_raw) { + if ((err = soundio_os_thread_create(outstream_raw_run, os, true, &osw->thread))) + return err; + } else { if ((err = soundio_os_thread_create(outstream_shared_run, os, true, &osw->thread))) return err; } diff --git a/src/wasapi.hpp b/src/wasapi.hpp index 910ed19..348cb49 100644 --- a/src/wasapi.hpp +++ b/src/wasapi.hpp @@ -62,6 +62,7 @@ struct SoundIoOutStreamWasapi { int writable_frame_count; UINT32 buffer_frame_count; int write_frame_count; + HANDLE h_event; SoundIoChannelArea areas[SOUNDIO_MAX_CHANNELS]; }; diff --git a/test/underflow.c b/test/underflow.c index e469ccf..a074309 100644 --- a/test/underflow.c +++ b/test/underflow.c @@ -113,7 +113,8 @@ int main(int argc, char **argv) { fprintf(stderr, "You should hear a sine wave for 3 seconds, then some period of silence or glitches,\n" "then you should see at least one buffer underflow message, then hear a sine\n" - "wave for 3 seconds, then the program should exit successfully.\n"); + "wave for 3 seconds, then the program should exit successfully.\n" + "WASAPI does not report buffer underflows.\n"); if (!(soundio = soundio_create())) panic("out of memory");