diff --git a/CMakeLists.txt b/CMakeLists.txt index 2934b14..415175a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,16 @@ else(Threads_FOUND) set(STATUS_THREADS "not found") endif(Threads_FOUND) +find_package(JACK) +if(JACK_FOUND) + set(STATUS_JACK "OK") + set(SOUNDIO_HAVE_JACK true) + include_directories(${JACK_INCLUDE_DIR}) +else() + set(STATUS_JACK "not found") + set(SOUNDIO_HAVE_JACK false) + set(JACK_LIBRARY "") +endif() find_package(PulseAudio) if(PULSEAUDIO_FOUND) @@ -71,6 +81,14 @@ set(TEST_SOURCES "${CMAKE_SOURCE_DIR}/src/ring_buffer.cpp" ) +if(SOUNDIO_HAVE_JACK) + set(LIBSOUNDIO_SOURCES ${LIBSOUNDIO_SOURCES} + "${CMAKE_SOURCE_DIR}/src/jack.cpp" + ) + set(TEST_SOURCES ${TEST_SOURCES} + "${CMAKE_SOURCE_DIR}/src/jack.cpp" + ) +endif() if(SOUNDIO_HAVE_PULSEAUDIO) set(LIBSOUNDIO_SOURCES ${LIBSOUNDIO_SOURCES} "${CMAKE_SOURCE_DIR}/src/pulseaudio.cpp" @@ -132,6 +150,7 @@ include_directories( ${ALSA_INCLUDE_DIRS} ) target_link_libraries(libsoundio_shared LINK_PUBLIC + ${JACK_LIBRARY} ${PULSEAUDIO_LIBRARY} ${ALSA_LIBRARIES} m @@ -182,6 +201,7 @@ enable_testing() add_executable(unit_tests ${TEST_SOURCES}) target_link_libraries(unit_tests LINK_PUBLIC ${CMAKE_THREAD_LIBS_INIT} + ${JACK_LIBRARY} ${PULSEAUDIO_LIBRARY} ${ALSA_LIBRARIES} m @@ -218,6 +238,7 @@ message( "System Dependencies\n" "-------------------\n" "* threads : ${STATUS_THREADS}\n" - "* pulseaudio : ${STATUS_PULSEAUDIO}\n" - "* ALSA : ${STATUS_ALSA}\n" + "* JACK (optional) : ${STATUS_JACK}\n" + "* PulseAudio (optional) : ${STATUS_PULSEAUDIO}\n" + "* ALSA (optional) : ${STATUS_ALSA}\n" ) diff --git a/README.md b/README.md index 1d42cbe..7734ceb 100644 --- a/README.md +++ b/README.md @@ -232,25 +232,37 @@ view `coverage/index.html` in a browser. ## Roadmap - 0. pipe record to playback example working with ALSA linux + 0. implement JACK backend, get examples working 0. why does pulseaudio microphone use up all the CPU? 0. merge in/out stream structures and functions? - 0. implement JACK backend, get examples working 0. implement CoreAudio (OSX) backend, get examples working 0. implement WASAPI (Windows) backend, get examples working + 0. implement ASIO (Windows) backend, get examples working 0. Avoid calling `panic` in PulseAudio. 0. Figure out a way to test prebuf. I suspect prebuf not working for ALSA which is why we have to pre-fill the ring buffer with silence for the microphone example. + 0. PulseAudio: when prebuf gets set to 0 need to pass `PA_STREAM_START_CORKED`. 0. In ALSA do we need to wake up the poll when destroying the in or out stream? 0. Create a test for clearing the playback buffer. 0. Create a test for pausing and resuming input and output streams. - 0. implement ASIO (Windows) backend, get examples working + 0. Create a test for the latency / synchronization API. + - Input is an audio file and some events indexed at particular frame - when + listening the events should line up exactly with a beat or visual + indicator, even when the latency is large. + - 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. 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. + 0. Should pause/resume be callable from outside the callbacks? 0. clean up API and improve documentation - make sure every function which can return an error documents which errors it can return 0. use a documentation generator and host the docs somewhere - 0. -fvisibility=hidden and then explicitly export stuff + 0. -fvisibility=hidden and then explicitly export stuff, or + explicitly make the unexported stuff private 0. Integrate into libgroove and test with Groove Basin 0. look at microphone example and determine if fewer memcpys can be done with the audio data diff --git a/cmake/FindJACK.cmake b/cmake/FindJACK.cmake new file mode 100644 index 0000000..3a1d9de --- /dev/null +++ b/cmake/FindJACK.cmake @@ -0,0 +1,16 @@ +# Copyright (c) 2015 Andrew Kelley +# This file is MIT licensed. +# See http://opensource.org/licenses/MIT + +# JACK_FOUND +# JACK_INCLUDE_DIR +# JACK_LIBRARY + +find_path(JACK_INCLUDE_DIR NAMES jack/jack.h) + +find_library(JACK_LIBRARY NAMES jack) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(JACK DEFAULT_MSG JACK_LIBRARY JACK_INCLUDE_DIR) + +mark_as_advanced(JACK_INCLUDE_DIR JACK_LIBRARY) diff --git a/example/microphone.c b/example/microphone.c index 2336187..f4c6ead 100644 --- a/example/microphone.c +++ b/example/microphone.c @@ -49,15 +49,22 @@ static void panic(const char *format, ...) { struct SoundIoRingBuffer *ring_buffer = NULL; +static int min_int(int a, int b) { + return (a < b) ? a : b; +} + static void read_callback(struct SoundIoInStream *instream, int available_frame_count) { int err; - struct SoundIoChannelArea *areas; char *write_ptr = soundio_ring_buffer_write_ptr(ring_buffer); - int frames_left = available_frame_count; + int free_bytes = soundio_ring_buffer_free_count(ring_buffer); + int free_count = free_bytes / instream->bytes_per_frame; + int write_count = min_int(available_frame_count, free_count); + int frames_left = write_count; for (;;) { int frame_count = frames_left; + struct SoundIoChannelArea *areas; if ((err = soundio_instream_begin_read(instream, &areas, &frame_count))) panic("begin read error: %s", soundio_strerror(err)); @@ -68,6 +75,7 @@ static void read_callback(struct SoundIoInStream *instream, int available_frame_ // Due to an overflow there is a hole. Fill the ring buffer with // silence for the size of the hole. memset(write_ptr, 0, frame_count * instream->bytes_per_frame); + fprintf(stderr, "Dropped %d frames due to internal overflow\n", frame_count); } else { for (int frame = 0; frame < frame_count; frame += 1) { for (int ch = 0; ch < instream->layout.channel_count; ch += 1) { @@ -87,12 +95,12 @@ static void read_callback(struct SoundIoInStream *instream, int available_frame_ break; } - int advance_bytes = available_frame_count * instream->bytes_per_frame; + int advance_bytes = write_count * instream->bytes_per_frame; soundio_ring_buffer_advance_write_ptr(ring_buffer, advance_bytes); -} -static int min_int(int a, int b) { - return (a < b) ? a : b; + int dropped_frames = available_frame_count - write_count; + if (dropped_frames > 0) + fprintf(stderr, "Dropped %d frames due to overflow\n", dropped_frames); } static void write_callback(struct SoundIoOutStream *outstream, int requested_frame_count) { @@ -134,10 +142,8 @@ static void write_callback(struct SoundIoOutStream *outstream, int requested_fra } static void underflow_callback(struct SoundIoOutStream *outstream) { - char *write_ptr = soundio_ring_buffer_write_ptr(ring_buffer); - int bytes_to_fill = outstream->buffer_duration * outstream->sample_rate * outstream->bytes_per_frame; - memset(write_ptr, 0, bytes_to_fill); - soundio_ring_buffer_advance_write_ptr(ring_buffer, bytes_to_fill); + static int count = 0; + fprintf(stderr, "underflow %d\n", ++count); } static int usage(char *exe) { @@ -279,8 +285,8 @@ int main(int argc, char **argv) { outstream->format = *fmt; outstream->sample_rate = sample_rate; outstream->layout = *layout; - outstream->buffer_duration = 0.1; - outstream->period_duration = 0.25; + outstream->buffer_duration = 0.2; + outstream->period_duration = 0.1; outstream->write_callback = write_callback; outstream->underflow_callback = underflow_callback; diff --git a/src/alsa.cpp b/src/alsa.cpp index 8e76caf..15e91a1 100644 --- a/src/alsa.cpp +++ b/src/alsa.cpp @@ -1013,11 +1013,13 @@ void outstream_thread_run(void *arg) { } continue; case SND_PCM_STATE_PREPARED: + { if ((err = snd_pcm_start(osa->handle)) < 0) { outstream->error_callback(outstream, SoundIoErrorStreaming); return; } continue; + } case SND_PCM_STATE_RUNNING: { if ((err = wait_for_poll(osa)) < 0) { diff --git a/src/config.h.in b/src/config.h.in index 364039b..5fc8912 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -16,7 +16,8 @@ #cmakedefine SOUNDIO_OS_BIG_ENDIAN #cmakedefine SOUNDIO_OS_LITTLE_ENDIAN -#cmakedefine SOUNDIO_HAVE_ALSA +#cmakedefine SOUNDIO_HAVE_JACK #cmakedefine SOUNDIO_HAVE_PULSEAUDIO +#cmakedefine SOUNDIO_HAVE_ALSA #endif diff --git a/src/jack.cpp b/src/jack.cpp new file mode 100644 index 0000000..48db9b5 --- /dev/null +++ b/src/jack.cpp @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2015 Andrew Kelley + * + * This file is part of libsoundio, which is MIT licensed. + * See http://opensource.org/licenses/MIT + */ + +#include "jack.hpp" +#include "soundio.hpp" + +#include +#include + +struct SoundIoJack { + jack_client_t *client; +}; + +static void flush_events_jack(struct SoundIoPrivate *) { + soundio_panic("TODO"); +} + +static void wait_events_jack(struct SoundIoPrivate *) { + soundio_panic("TODO"); +} + +static void wakeup_jack(struct SoundIoPrivate *) { + soundio_panic("TODO"); +} + + +static int outstream_open_jack(struct SoundIoPrivate *, struct SoundIoOutStreamPrivate *) { + soundio_panic("TODO"); +} + +static void outstream_destroy_jack(struct SoundIoPrivate *, struct SoundIoOutStreamPrivate *) { + soundio_panic("TODO"); +} + +static int outstream_start_jack(struct SoundIoPrivate *, struct SoundIoOutStreamPrivate *) { + soundio_panic("TODO"); +} + +static int outstream_begin_write_jack(struct SoundIoPrivate *, struct SoundIoOutStreamPrivate *, + SoundIoChannelArea **out_areas, int *frame_count) +{ + soundio_panic("TODO"); +} + +static int outstream_end_write_jack(struct SoundIoPrivate *, struct SoundIoOutStreamPrivate *, int frame_count) { + soundio_panic("TODO"); +} + +static int outstream_clear_buffer_jack(struct SoundIoPrivate *, struct SoundIoOutStreamPrivate *) { + soundio_panic("TODO"); +} + +static int outstream_pause_jack(struct SoundIoPrivate *, struct SoundIoOutStreamPrivate *, bool pause) { + soundio_panic("TODO"); +} + + + +static int instream_open_jack(struct SoundIoPrivate *, struct SoundIoInStreamPrivate *) { + soundio_panic("TODO"); +} + +static void instream_destroy_jack(struct SoundIoPrivate *, struct SoundIoInStreamPrivate *) { + soundio_panic("TODO"); +} + +static int instream_start_jack(struct SoundIoPrivate *, struct SoundIoInStreamPrivate *) { + soundio_panic("TODO"); +} + +static int instream_begin_read_jack(struct SoundIoPrivate *, struct SoundIoInStreamPrivate *, + SoundIoChannelArea **out_areas, int *frame_count) +{ + soundio_panic("TODO"); +} + +static int instream_end_read_jack(struct SoundIoPrivate *, struct SoundIoInStreamPrivate *) { + soundio_panic("TODO"); +} + +static int instream_pause_jack(struct SoundIoPrivate *, struct SoundIoInStreamPrivate *, bool pause) { + soundio_panic("TODO"); +} + + +static void destroy_jack(SoundIoPrivate *si) { + SoundIoJack *sij = (SoundIoJack *)si->backend_data; + if (!sij) + return; + + destroy(sij); + si->backend_data = nullptr; +} + +static void error_callback(const char *msg) { + //fprintf(stderr, "JACK error: %s\n", msg); +} + +static void info_callback(const char *msg) { + //fprintf(stderr, "JACK info: %s\n", msg); +} + +int soundio_jack_init(struct SoundIoPrivate *si) { + SoundIo *soundio = &si->pub; + assert(!si->backend_data); + SoundIoJack *sij = create(); + if (!sij) { + destroy_jack(si); + return SoundIoErrorNoMem; + } + si->backend_data = sij; + + jack_set_error_function(error_callback); + jack_set_info_function(info_callback); + + jack_status_t status; + sij->client = jack_client_open(soundio->app_name, JackNoStartServer, &status); + if (!sij->client) { + destroy_jack(si); + assert(!(status & JackInvalidOption)); + if (status & JackNameNotUnique) + return SoundIoErrorNameNotUnique; + if (status & JackShmFailure) + return SoundIoErrorSystemResources; + if (status & JackNoSuchClient) + return SoundIoErrorNoSuchClient; + + return SoundIoErrorInitAudioBackend; + } + + + si->destroy = destroy_jack; + si->flush_events = flush_events_jack; + si->wait_events = wait_events_jack; + si->wakeup = wakeup_jack; + + si->outstream_open = outstream_open_jack; + si->outstream_destroy = outstream_destroy_jack; + si->outstream_start = outstream_start_jack; + si->outstream_begin_write = outstream_begin_write_jack; + si->outstream_end_write = outstream_end_write_jack; + si->outstream_clear_buffer = outstream_clear_buffer_jack; + si->outstream_pause = outstream_pause_jack; + + si->instream_open = instream_open_jack; + si->instream_destroy = instream_destroy_jack; + si->instream_start = instream_start_jack; + si->instream_begin_read = instream_begin_read_jack; + si->instream_end_read = instream_end_read_jack; + si->instream_pause = instream_pause_jack; + + return 0; +} diff --git a/src/jack.hpp b/src/jack.hpp new file mode 100644 index 0000000..7d48696 --- /dev/null +++ b/src/jack.hpp @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2015 Andrew Kelley + * + * This file is part of libsoundio, which is MIT licensed. + * See http://opensource.org/licenses/MIT + */ + +#ifndef SOUNDIO_JACK_HPP +#define SOUNDIO_JACK_HPP + +int soundio_jack_init(struct SoundIoPrivate *si); + +#endif + diff --git a/src/soundio.cpp b/src/soundio.cpp index 200ab3b..d97229b 100644 --- a/src/soundio.cpp +++ b/src/soundio.cpp @@ -11,6 +11,10 @@ #include "os.hpp" #include "config.h" +#ifdef SOUNDIO_HAVE_JACK +#include "jack.hpp" +#endif + #ifdef SOUNDIO_HAVE_PULSEAUDIO #include "pulseaudio.hpp" #endif @@ -23,6 +27,9 @@ #include static const SoundIoBackend available_backends[] = { +#ifdef SOUNDIO_HAVE_JACK + SoundIoBackendJack, +#endif #ifdef SOUNDIO_HAVE_PULSEAUDIO SoundIoBackendPulseAudio, #endif @@ -32,6 +39,26 @@ static const SoundIoBackend available_backends[] = { SoundIoBackendDummy, }; +static int (*backend_init_fns[])(SoundIoPrivate *) = { + nullptr, // SoundIoBackendNone +#ifdef SOUNDIO_HAVE_JACK + soundio_jack_init, +#else + nullptr, +#endif +#ifdef SOUNDIO_HAVE_PULSEAUDIO + soundio_pulseaudio_init, +#else + nullptr, +#endif +#ifdef SOUNDIO_HAVE_ALSA + soundio_alsa_init, +#else + nullptr, +#endif + soundio_dummy_init, +}; + const char *soundio_strerror(int error) { switch ((enum SoundIoError)error) { case SoundIoErrorNone: return "(no error)"; @@ -43,6 +70,8 @@ const char *soundio_strerror(int error) { case SoundIoErrorBackendUnavailable: return "backend unavailable"; case SoundIoErrorStreaming: return "unrecoverable streaming failure"; case SoundIoErrorIncompatibleDevice: return "incompatible device"; + case SoundIoErrorNameNotUnique: return "name not unique"; + case SoundIoErrorNoSuchClient: return "no such client"; } soundio_panic("invalid error enum value: %d", error); } @@ -105,6 +134,7 @@ const char * soundio_format_string(enum SoundIoFormat format) { const char *soundio_backend_name(enum SoundIoBackend backend) { switch (backend) { case SoundIoBackendNone: return "(none)"; + case SoundIoBackendJack: return "JACK"; case SoundIoBackendPulseAudio: return "PulseAudio"; case SoundIoBackendAlsa: return "ALSA"; case SoundIoBackendDummy: return "Dummy"; @@ -158,41 +188,20 @@ int soundio_connect_backend(SoundIo *soundio, SoundIoBackend backend) { if (si->current_backend) return SoundIoErrorInvalid; - int err; - switch (backend) { - case SoundIoBackendPulseAudio: -#ifdef SOUNDIO_HAVE_PULSEAUDIO - si->current_backend = SoundIoBackendPulseAudio; - if ((err = soundio_pulseaudio_init(si))) { - soundio_disconnect(soundio); - return err; - } - return 0; -#else - return SoundIoErrorBackendUnavailable; -#endif - case SoundIoBackendAlsa: -#ifdef SOUNDIO_HAVE_ALSA - si->current_backend = SoundIoBackendAlsa; - if ((err = soundio_alsa_init(si))) { - soundio_disconnect(soundio); - return err; - } - return 0; -#else - return SoundIoErrorBackendUnavailable; -#endif - case SoundIoBackendDummy: - si->current_backend = SoundIoBackendDummy; - if ((err = soundio_dummy_init(si))) { - soundio_disconnect(soundio); - return err; - } - return 0; - case SoundIoBackendNone: + if (backend <= 0 || backend > SoundIoBackendDummy) return SoundIoErrorInvalid; + + int (*fn)(SoundIoPrivate *) = backend_init_fns[backend]; + + if (!fn) + return SoundIoErrorBackendUnavailable; + + int err; + if ((err = backend_init_fns[backend](si))) { + soundio_disconnect(soundio); + return err; } - return SoundIoErrorInvalid; + return 0; } void soundio_disconnect(struct SoundIo *soundio) { @@ -528,25 +537,9 @@ void soundio_destroy_devices_info(SoundIoDevicesInfo *devices_info) { } bool soundio_have_backend(SoundIoBackend backend) { - switch (backend) { - case SoundIoBackendPulseAudio: -#ifdef SOUNDIO_HAVE_PULSEAUDIO - return true; -#else - return false; -#endif - case SoundIoBackendAlsa: -#ifdef SOUNDIO_HAVE_ALSA - return true; -#else - return false; -#endif - case SoundIoBackendDummy: - return true; - case SoundIoBackendNone: - return false; - } - return false; + assert(backend > 0); + assert(backend <= SoundIoBackendDummy); + return backend_init_fns[backend]; } int soundio_backend_count(struct SoundIo *soundio) { diff --git a/src/soundio.h b/src/soundio.h index 9e8af85..6291ba3 100644 --- a/src/soundio.h +++ b/src/soundio.h @@ -26,6 +26,8 @@ enum SoundIoError { SoundIoErrorBackendUnavailable, SoundIoErrorStreaming, SoundIoErrorIncompatibleDevice, + SoundIoErrorNameNotUnique, + SoundIoErrorNoSuchClient, }; enum SoundIoChannelId { @@ -98,6 +100,7 @@ enum SoundIoChannelLayoutId { enum SoundIoBackend { SoundIoBackendNone, + SoundIoBackendJack, SoundIoBackendPulseAudio, SoundIoBackendAlsa, SoundIoBackendDummy, @@ -205,7 +208,9 @@ struct SoundIo { // variable to wake up. Called when soundio_wait_events would be woken up. void (*on_events_signal)(struct SoundIo *); - // Optional: Application name. Used by PulseAudio. Defaults to "SoundIo". + // Optional: Application name. + // PulseAudio uses this for "application name". + // JACK uses this for `client_name`. const char *app_name; };