diff --git a/README.md b/README.md index 6eeaac9..cc458a8 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,21 @@ # libsoundio -C library which provides cross-platform audio input and output. The API is +C99 library providing cross-platform audio input and output. The API is suitable for real-time software such as digital audio workstations as well as consumer software such as music players. -This library is an abstraction; however it prioritizes performance and power -over API convenience. Features that only exist in some sound backends are -exposed. +This library is an abstraction; however in the delicate balance between +performance and power, and API convenience, the scale is tipped closer to +the former. Features that only exist in some sound backends are exposed. -**This library is a work-in-progress.** +The goal of this library is to be the only resource needed to implement top +quality audio playback and capture on desktop and laptop systems. This +includes detailed documentation explaining how audio works on each supported +backend, how they are abstracted to provide the libsoundio API, and what +assumptions you can and cannot make in order to guarantee consistent, reliable +behavior on every platform. + +**This project is a work-in-progress.** ## Features and Limitations @@ -16,18 +23,20 @@ exposed. - [PulseAudio](http://www.freedesktop.org/wiki/Software/PulseAudio/) - [ALSA](http://www.alsa-project.org/) - Dummy (silence) - - (planned) [JACK](http://jackaudio.org/) + - [JACK](http://jackaudio.org/) - (planned) [CoreAudio](https://developer.apple.com/library/mac/documentation/MusicAudio/Conceptual/CoreAudioOverview/Introduction/Introduction.html) - (planned) [WASAPI](https://msdn.microsoft.com/en-us/library/windows/desktop/dd371455%28v=vs.85%29.aspx) - (planned) [ASIO](http://www.asio4all.com/) * C library. Depends only on the respective backend API libraries and libc. Does *not* depend on libstdc++, and does *not* have exceptions, run-time type information, or [setjmp](http://latentcontent.net/2007/12/05/libpng-worst-api-ever/). - * Does not write anything to stdio. I'm looking at you, - [PortAudio](http://www.portaudio.com/). + * Errors are communicated via return codes, not logging to stdio. This is one + of my many complaints against [PortAudio](http://www.portaudio.com/). * Supports channel layouts (also known as channel maps), important for surround sound applications. * Ability to monitor devices and get an event when available devices change. + * Ability to get an event when the backend is disconnected, for example when + the JACK server or PulseAudio server shuts down. * Detects which input device is default and which output device is default. * Ability to connect to multiple backends at once. For example you could have an ALSA device open and a JACK device open at the same time. @@ -249,6 +258,7 @@ view `coverage/index.html` in a browser. If not, might need to hav xrun callback set a flag and have process callback call the underflow callback. 0. Detect PulseAudio server going offline and emit `on_backend_disconnect`. + 0. Instead fo open(), start(), pause(), open() starts it and it starts paused. 0. Create a test for clearing the playback buffer and prebuffering. 0. Create a test for pausing and resuming input and output streams. 0. Create a test for the latency / synchronization API. @@ -258,6 +268,7 @@ view `coverage/index.html` in a browser. - Play the audio file, have the user press an input right at the beat. Find out what the frame index it thinks the user pressed it at and make sure that is correct. + 0. Create a test for input stream overflow handling. 0. Expose JACK options in `jack_client_open` 0. Allow calling functions from outside the callbacks as long as they first call lock and then unlock when done. diff --git a/example/microphone.c b/example/microphone.c index f4c6ead..f228575 100644 --- a/example/microphone.c +++ b/example/microphone.c @@ -293,7 +293,7 @@ int main(int argc, char **argv) { if ((err = soundio_outstream_open(outstream))) panic("unable to open output stream: %s", soundio_strerror(err)); - int capacity = instream->buffer_duration * instream->sample_rate * instream->bytes_per_frame; + int capacity = fmax(1.0, instream->buffer_duration) * instream->sample_rate * instream->bytes_per_frame; ring_buffer = soundio_ring_buffer_create(soundio, capacity); if (!ring_buffer) panic("unable to create ring buffer: out of memory"); diff --git a/src/alsa.cpp b/src/alsa.cpp index 7391f2f..bb46011 100644 --- a/src/alsa.cpp +++ b/src/alsa.cpp @@ -888,6 +888,7 @@ static int xrun_recovery(SoundIoOutStreamPrivate *os, int err) { static int instream_xrun_recovery(SoundIoInStreamPrivate *is, int err) { SoundIoInStreamAlsa *isa = &is->backend_data.alsa; + // TODO do something with this overflow if (err == -EPIPE) { err = snd_pcm_prepare(isa->handle); } else if (err == -ESTRPIPE) { diff --git a/src/jack.cpp b/src/jack.cpp index 50b70f7..8084058 100644 --- a/src/jack.cpp +++ b/src/jack.cpp @@ -20,6 +20,7 @@ struct SoundIoJackPort { const char *name; int name_len; SoundIoChannelId channel_id; + jack_latency_range_t latency_range; }; struct SoundIoJackClient { @@ -113,11 +114,9 @@ static int outstream_xrun_callback(void *arg) { static int outstream_buffer_size_callback(jack_nframes_t nframes, void *arg) { SoundIoOutStreamPrivate *os = (SoundIoOutStreamPrivate *)arg; + SoundIoOutStreamJack *osj = &os->backend_data.jack; SoundIoOutStream *outstream = &os->pub; - SoundIo *soundio = outstream->device->soundio; - SoundIoPrivate *si = (SoundIoPrivate *)soundio; - SoundIoJack *sij = &si->backend_data.jack; - if ((jack_nframes_t)sij->buffer_size == nframes) { + if ((jack_nframes_t)osj->period_size == nframes) { return 0; } else { outstream->error_callback(outstream, SoundIoErrorStreaming); @@ -153,9 +152,10 @@ static int outstream_open_jack(struct SoundIoPrivate *si, struct SoundIoOutStrea if (sij->is_shutdown) return SoundIoErrorBackendDisconnected; - outstream->buffer_duration = device->buffer_duration_current; - outstream->period_duration = 0.0; + outstream->buffer_duration = 0.0; + outstream->period_duration = device->period_duration_current; outstream->prebuf_duration = 0.0; + osj->period_size = sij->period_size; jack_status_t status; osj->client = jack_client_open(outstream->name, JackNoStartServer, &status); @@ -269,7 +269,7 @@ static int outstream_begin_write_jack(struct SoundIoPrivate *si, struct SoundIoO SoundIoOutStream *outstream = &os->pub; SoundIoOutStreamJack *osj = &os->backend_data.jack; SoundIoJack *sij = &si->backend_data.jack; - assert(*frame_count <= sij->buffer_size); + assert(*frame_count <= sij->period_size); for (int ch = 0; ch < outstream->layout.channel_count; ch += 1) { SoundIoOutStreamJackPort *osjp = &osj->ports[ch]; @@ -284,11 +284,13 @@ static int outstream_begin_write_jack(struct SoundIoPrivate *si, struct SoundIoO return 0; } -static int outstream_end_write_jack(struct SoundIoPrivate *, struct SoundIoOutStreamPrivate *, int frame_count) { +static int outstream_end_write_jack(struct SoundIoPrivate *si, struct SoundIoOutStreamPrivate *os, + int frame_count) +{ return 0; } -static int outstream_clear_buffer_jack(struct SoundIoPrivate *, struct SoundIoOutStreamPrivate *) { +static int outstream_clear_buffer_jack(struct SoundIoPrivate *si, struct SoundIoOutStreamPrivate *os) { // JACK does not support `prebuf` which is the same as a `prebuf` value of 0, // which means that clearing the buffer is always successful and does nothing. return 0; @@ -296,44 +298,199 @@ static int outstream_clear_buffer_jack(struct SoundIoPrivate *, struct SoundIoOu -static void instream_destroy_jack(struct SoundIoPrivate *, struct SoundIoInStreamPrivate *) { - soundio_panic("TODO destroy instream"); +static void instream_destroy_jack(struct SoundIoPrivate *si, struct SoundIoInStreamPrivate *is) { + SoundIoInStreamJack *isj = &is->backend_data.jack; + + jack_client_close(isj->client); +} + +static int instream_xrun_callback(void *arg) { +// SoundIoInStreamPrivate *is = (SoundIoInStreamPrivate *)arg; +// SoundIoInStream *instream = &is->pub; +// TODO do something with this overflow + return 0; +} + +static int instream_buffer_size_callback(jack_nframes_t nframes, void *arg) { + SoundIoInStreamPrivate *is = (SoundIoInStreamPrivate *)arg; + SoundIoInStreamJack *isj = &is->backend_data.jack; + SoundIoInStream *instream = &is->pub; + + if ((jack_nframes_t)isj->period_size == nframes) { + return 0; + } else { + instream->error_callback(instream, SoundIoErrorStreaming); + return -1; + } +} + +static int instream_sample_rate_callback(jack_nframes_t nframes, void *arg) { + SoundIoInStreamPrivate *is = (SoundIoInStreamPrivate *)arg; + SoundIoInStream *instream = &is->pub; + if (nframes == (jack_nframes_t)instream->sample_rate) { + return 0; + } else { + instream->error_callback(instream, SoundIoErrorStreaming); + return -1; + } +} + +static void instream_shutdown_callback(void *arg) { + SoundIoInStreamPrivate *is = (SoundIoInStreamPrivate *)arg; + SoundIoInStream *instream = &is->pub; + instream->error_callback(instream, SoundIoErrorStreaming); +} + +static int instream_process_callback(jack_nframes_t nframes, void *arg) { + SoundIoInStreamPrivate *is = (SoundIoInStreamPrivate *)arg; + SoundIoInStream *instream = &is->pub; + instream->read_callback(instream, nframes); + return 0; } static int instream_open_jack(struct SoundIoPrivate *si, struct SoundIoInStreamPrivate *is) { - //SoundIoInStream *outstream = &is->pub; - //SoundIoInStreamJack *isj = &is->backend_data.jack; + SoundIoInStream *instream = &is->pub; + SoundIoInStreamJack *isj = &is->backend_data.jack; SoundIoJack *sij = &si->backend_data.jack; - if (sij->is_shutdown) { - instream_destroy_jack(si, is); + SoundIoDevice *device = instream->device; + SoundIoDevicePrivate *dev = (SoundIoDevicePrivate *)device; + SoundIoDeviceJack *dj = &dev->backend_data.jack; + + if (sij->is_shutdown) return SoundIoErrorBackendDisconnected; + + instream->buffer_duration = 0.0; + instream->period_duration = device->period_duration_current; + isj->period_size = sij->period_size; + + jack_status_t status; + isj->client = jack_client_open(instream->name, JackNoStartServer, &status); + if (!isj->client) { + instream_destroy_jack(si, is); + assert(!(status & JackInvalidOption)); + if (status & JackShmFailure) + return SoundIoErrorSystemResources; + if (status & JackNoSuchClient) + return SoundIoErrorNoSuchClient; + return SoundIoErrorOpeningDevice; } - soundio_panic("TODO open instream"); + + int err; + if ((err = jack_set_process_callback(isj->client, instream_process_callback, is))) { + instream_destroy_jack(si, is); + return SoundIoErrorOpeningDevice; + } + if ((err = jack_set_buffer_size_callback(isj->client, instream_buffer_size_callback, is))) { + instream_destroy_jack(si, is); + return SoundIoErrorOpeningDevice; + } + if ((err = jack_set_sample_rate_callback(isj->client, instream_sample_rate_callback, is))) { + instream_destroy_jack(si, is); + return SoundIoErrorOpeningDevice; + } + if ((err = jack_set_xrun_callback(isj->client, instream_xrun_callback, is))) { + instream_destroy_jack(si, is); + return SoundIoErrorOpeningDevice; + } + jack_on_shutdown(isj->client, instream_shutdown_callback, is); + + // register ports and map channels + int connected_count = 0; + for (int ch = 0; ch < instream->layout.channel_count; ch += 1) { + SoundIoChannelId my_channel_id = instream->layout.channels[ch]; + const char *channel_name = soundio_get_channel_name(my_channel_id); + unsigned long flags = JackPortIsInput; + if (!instream->non_terminal_hint) + flags |= JackPortIsTerminal; + jack_port_t *jport = jack_port_register(isj->client, channel_name, JACK_DEFAULT_AUDIO_TYPE, flags, 0); + if (!jport) { + instream_destroy_jack(si, is); + return SoundIoErrorOpeningDevice; + } + SoundIoInStreamJackPort *isjp = &isj->ports[ch]; + isjp->dest_port = jport; + // figure out which source port this connects to + SoundIoDeviceJackPort *djp = find_port_matching_channel(device, my_channel_id); + if (djp) { + isjp->source_port_name = djp->full_name; + isjp->source_port_name_len = djp->full_name_len; + connected_count += 1; + } + } + // If nothing got connected, channel layouts aren't working. Just send the + // data in the order of the ports. + if (connected_count == 0) { + instream->layout_error = SoundIoErrorIncompatibleDevice; + + int ch_count = min(instream->layout.channel_count, dj->port_count); + for (int ch = 0; ch < ch_count; ch += 1) { + SoundIoInStreamJackPort *isjp = &isj->ports[ch]; + SoundIoDeviceJackPort *djp = &dj->ports[ch]; + isjp->source_port_name = djp->full_name; + isjp->source_port_name_len = djp->full_name_len; + } + } + + return 0; } -static int instream_start_jack(struct SoundIoPrivate *si, struct SoundIoInStreamPrivate *is) { +static int instream_pause_jack(struct SoundIoPrivate *si, struct SoundIoInStreamPrivate *is, bool pause) { + SoundIoInStreamJack *isj = &is->backend_data.jack; + SoundIoInStream *instream = &is->pub; SoundIoJack *sij = &si->backend_data.jack; if (sij->is_shutdown) return SoundIoErrorBackendDisconnected; - soundio_panic("TODO start instream"); + int err; + if (pause) { + if ((err = jack_deactivate(isj->client))) + return SoundIoErrorStreaming; + } else { + if ((err = jack_activate(isj->client))) + return SoundIoErrorStreaming; + + for (int ch = 0; ch < instream->layout.channel_count; ch += 1) { + SoundIoInStreamJackPort *isjp = &isj->ports[ch]; + const char *source_port_name = isjp->source_port_name; + // allow unconnected ports + if (!source_port_name) + continue; + const char *dest_port_name = jack_port_name(isjp->dest_port); + if ((err = jack_connect(isj->client, source_port_name, dest_port_name))) + return SoundIoErrorStreaming; + } + } + + return 0; +} + +static int instream_start_jack(struct SoundIoPrivate *si, struct SoundIoInStreamPrivate *is) { + return instream_pause_jack(si, is, false); } static int instream_begin_read_jack(struct SoundIoPrivate *si, struct SoundIoInStreamPrivate *is, SoundIoChannelArea **out_areas, int *frame_count) { - soundio_panic("TODO begin read"); + SoundIoInStream *instream = &is->pub; + SoundIoInStreamJack *isj = &is->backend_data.jack; + SoundIoJack *sij = &si->backend_data.jack; + assert(*frame_count <= sij->period_size); + + for (int ch = 0; ch < instream->layout.channel_count; ch += 1) { + SoundIoInStreamJackPort *isjp = &isj->ports[ch]; + if (!(isj->areas[ch].ptr = (char*)jack_port_get_buffer(isjp->dest_port, *frame_count))) + return SoundIoErrorStreaming; + + isj->areas[ch].step = instream->bytes_per_sample; + } + + *out_areas = isj->areas; + + return 0; } static int instream_end_read_jack(struct SoundIoPrivate *si, struct SoundIoInStreamPrivate *is) { - soundio_panic("TODO end read"); -} - -static int instream_pause_jack(struct SoundIoPrivate *si, struct SoundIoInStreamPrivate *is, bool pause) { - SoundIoJack *sij = &si->backend_data.jack; - if (sij->is_shutdown) - return SoundIoErrorBackendDisconnected; - soundio_panic("TODO pause"); + return 0; } static void split_str(const char *input_str, int input_str_len, char c, @@ -421,6 +578,7 @@ static int refresh_devices(SoundIoPrivate *si) { jack_port_t *jport = jack_port_by_name(sij->client, client_and_port_name); int flags = jack_port_flags(jport); + const char *port_type = jack_port_type(jport); if (strcmp(port_type, JACK_DEFAULT_AUDIO_TYPE) != 0) { // we don't know how to support such a port @@ -459,6 +617,10 @@ static int refresh_devices(SoundIoPrivate *si) { port->name_len = port_name_len; port->channel_id = soundio_parse_channel_id(port_name, port_name_len); + jack_latency_callback_mode_t latency_mode = (purpose == SoundIoDevicePurposeOutput) ? + JackPlaybackLatency : JackCaptureLatency; + jack_port_get_latency_range(jport, latency_mode, &port->latency_range); + port_name_ptr += 1; } @@ -476,9 +638,13 @@ static int refresh_devices(SoundIoPrivate *si) { SoundIoDevice *device = &dev->pub; SoundIoDeviceJack *dj = &dev->backend_data.jack; int description_len = client->name_len + 3 + 2 * client->port_count; + jack_nframes_t max_buffer_frames = 0; for (int port_index = 0; port_index < client->port_count; port_index += 1) { SoundIoJackPort *port = &client->ports[port_index]; + description_len += port->name_len; + + max_buffer_frames = max(max_buffer_frames, port->latency_range.max); } dev->destruct = destruct_device; @@ -497,7 +663,10 @@ static int refresh_devices(SoundIoPrivate *si) { device->sample_rate_min = sij->sample_rate; device->sample_rate_max = sij->sample_rate; device->sample_rate_current = sij->sample_rate; - device->buffer_duration_min = sij->buffer_size / (double) sij->sample_rate; + device->period_duration_min = sij->period_size / (double) sij->sample_rate; + device->period_duration_max = device->period_duration_min; + device->period_duration_current = device->period_duration_min; + device->buffer_duration_min = max_buffer_frames / (double) sij->sample_rate; device->buffer_duration_max = device->buffer_duration_min; device->buffer_duration_current = device->buffer_duration_min; dj->port_count = client->port_count; @@ -591,7 +760,7 @@ static int refresh_devices(SoundIoPrivate *si) { static int buffer_size_callback(jack_nframes_t nframes, void *arg) { SoundIoPrivate *si = (SoundIoPrivate *)arg; SoundIoJack *sij = &si->backend_data.jack; - sij->buffer_size = nframes; + sij->period_size = nframes; if (sij->initialized) refresh_devices(si); return 0; @@ -706,7 +875,7 @@ int soundio_jack_init(struct SoundIoPrivate *si) { } jack_on_shutdown(sij->client, shutdown_callback, si); - sij->buffer_size = jack_get_buffer_size(sij->client); + sij->period_size = jack_get_buffer_size(sij->client); sij->sample_rate = jack_get_sample_rate(sij->client); if ((err = jack_activate(sij->client))) { diff --git a/src/jack.hpp b/src/jack.hpp index 8658ec7..2f2046d 100644 --- a/src/jack.hpp +++ b/src/jack.hpp @@ -34,7 +34,7 @@ struct SoundIoJack { struct SoundIoDevicesInfo *ready_devices_info; bool initialized; int sample_rate; - int buffer_size; + int period_size; bool is_shutdown; bool emitted_shutdown_cb; }; @@ -47,12 +47,22 @@ struct SoundIoOutStreamJackPort { struct SoundIoOutStreamJack { jack_client_t *client; + int period_size; SoundIoOutStreamJackPort ports[SOUNDIO_MAX_CHANNELS]; SoundIoChannelArea areas[SOUNDIO_MAX_CHANNELS]; }; -struct SoundIoInStreamJack { +struct SoundIoInStreamJackPort { + jack_port_t *dest_port; + const char *source_port_name; + int source_port_name_len; +}; +struct SoundIoInStreamJack { + jack_client_t *client; + int period_size; + SoundIoInStreamJackPort ports[SOUNDIO_MAX_CHANNELS]; + SoundIoChannelArea areas[SOUNDIO_MAX_CHANNELS]; }; #endif diff --git a/src/ring_buffer.cpp b/src/ring_buffer.cpp index f255d98..4e09891 100644 --- a/src/ring_buffer.cpp +++ b/src/ring_buffer.cpp @@ -15,6 +15,8 @@ struct SoundIoRingBuffer *soundio_ring_buffer_create(struct SoundIo *soundio, int requested_capacity) { SoundIoRingBuffer *rb = create(); + assert(requested_capacity > 0); + if (!rb) { soundio_ring_buffer_destroy(rb); return nullptr; diff --git a/src/soundio.h b/src/soundio.h index 518c5d7..84acb87 100644 --- a/src/soundio.h +++ b/src/soundio.h @@ -286,7 +286,8 @@ struct SoundIoDevice { int sample_rate_current; // Buffer duration in seconds. If any values are unknown, they are set to - // 0.0. These values are unknown for PulseAudio. + // 0.0. These values are unknown for PulseAudio. These values are sometimes + // unknown for JACK. double buffer_duration_min; double buffer_duration_max; double buffer_duration_current; @@ -448,6 +449,12 @@ struct SoundIoInStream { // Must not contain a colon (":"). const char *name; + // Optional: Hint that this input stream is nonterminal. This is used by + // JACK and it means that the data received by the stream will be + // passed on or made available to another stream. Defaults to `false`. + // stream. Defaults to `false`. + bool non_terminal_hint; + // computed automatically when you call soundio_instream_open int bytes_per_frame; int bytes_per_sample;