diff --git a/Makefile.am b/Makefile.am index ec723f8..a4f7525 100644 --- a/Makefile.am +++ b/Makefile.am @@ -53,6 +53,10 @@ if OSS cava_SOURCES += input/oss.c endif +if JACK + cava_SOURCES += input/jack.c +endif + if NCURSES cava_SOURCES += output/terminal_ncurses.c endif diff --git a/README.md b/README.md index 54d806a..d9ac72b 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ by [Karl Stavestrand](mailto:karl@stavestrand.no) - [MPD](#mpd) - [sndio](#sndio) - [OSS](#oss) + - [JACK](#jack) - [squeezelite](#squeezelite) - [macOS](#macos-1) - [Windows](#windows) @@ -77,11 +78,12 @@ The development lib of one of these audio frameworks, depending on your distro: * Pipewire * Portaudio * Sndio +* JACK Optional components: * SDL2 dev files -Only FFTW and the other build tools are actually required for CAVA to compile, but this will only give you the ability to read from fifo files. To more easly grab audio from your system pulseaudio, alsa, sndio or portaudio dev files are recommended (depending on what audio system you are using). Not sure how to get the pulseaudio dev files for other distros than debian/ubuntu or if they are bundled in pulseaudio. +Only FFTW and the other build tools are actually required for CAVA to compile, but this will only give you the ability to read from fifo files. To more easly grab audio from your system pulseaudio, alsa, sndio, jack or portaudio dev files are recommended (depending on what audio system you are using). Not sure how to get the pulseaudio dev files for other distros than debian/ubuntu or if they are bundled in pulseaudio. For better a better visual experience ncurses is also recomended. @@ -90,11 +92,17 @@ All the requirements can be installed easily in all major distros: FreeBSD - pkg install autoconf-archive autotools fftw3 iniparser pkgconf psftools sdl2 sndio + pkg install autoconf autoconf-archive automake fftw3 iniparser jackit libglvnd libtool pkgconf psftools sdl2 sndio + +Additionally, run these commands on FreeBSD before building: + + export CFLAGS="-I/usr/local/include" + export LDFLAGS="-L/usr/local/lib" + Debian/Ubuntu: - sudo apt install build-essential libfftw3-dev libasound2-dev libncursesw5-dev libpulse-dev libtool automake autoconf-archive libiniparser-dev libsdl2-2.0-0 libsdl2-dev libpipewire-0.3-dev pkgconf + sudo apt install build-essential libfftw3-dev libasound2-dev libncursesw5-dev libpulse-dev libtool automake autoconf-archive libiniparser-dev libsdl2-2.0-0 libsdl2-dev libpipewire-0.3-dev libjack-jackd2-dev pkgconf ArchLinux: @@ -340,9 +348,12 @@ $ AUDIODEVICE=snd/0.monitor cava ### OSS +Set + + method = oss + The audio system used on FreeBSD is the Open Sound System (OSS). The following example demonstrates how to setup CAVA for OSS on FreeBSD: - ```sh $ cat /dev/sndstat Installed devices: @@ -351,12 +362,11 @@ pcm1: (rec) pcm2: (play/rec) No devices installed from userspace. ``` - The system has three `pcm` sound devices, `pcm0`, `pcm1` and `pcm2`. `pcm0` corresponds to the analog output jack on the rear, in which external stereo speakers are plugged in, and the analog input jack, in which one could plug in a microphone. Because it encapsulates both, output and input, it is marked as `play/rec`. It is also set as the `default` sound device. `pcm1` corresponds to another analog input -jack for a mic on the front side and is marked `rec`. A USB headset which an integrated mic is plugged +jack for a mic on the front side and is marked `rec`. A USB headset with an integrated mic is plugged in an USB port and the system has created the `pcm2` sound device with `play/rec` capabilities for it. @@ -367,20 +377,17 @@ which acts like a symlink to the `default` audio device, in this example to `/de Now in order to visualize the mic input in CAVA, the `source` value in the configuration file must be set to the corresponding audio device, i.e. -```sh -[input] -source = /dev/dsp # or /dev/dsp0 for which /dev/dsp is a symlink in this example -``` + + source = /dev/dsp # or /dev/dsp0 for which /dev/dsp is a symlink in this example + (which is already the default for CAVA) for the `pcm0` mic on the rear, or -```sh -[input] -source = /dev/dsp1 -``` + + source = /dev/dsp1 + for the `pcm1` mic on the front, or -```sh -[input] -source = /dev/dsp2 -``` + + source = /dev/dsp2 + for the `pcm2` mic on the USB headset. OSS can't record the outgoing audio on its own, i.e. the sounds from a music player or a browser which @@ -403,6 +410,83 @@ It created a virtual device `dsp` from `/dev/dsp0`. Now the audio is visualized `source = /dev/dsp` in the configuration file. Virtual OSS can be configured and started as a service on FreeBSD. +### JACK + +Set + + method = jack + +The JACK Audio Connection Kit (JACK) is a professional sound server API which is available on several +operating systems, e.g. FreeBSD and Linux. + +CAVA is a JACK client with the base client name `cava` and adheres to the standard server start and +stop behaviour, i.e. CAVA starts a JACK server if none is already running and the environment variable +`JACK_START_SERVER` is defined, in which case the server also stops when all clients have exited. The +`source` in the CAVA configuration file specifies the name of the JACK server to which CAVA tries to +connect to. The default value is `default`, which is also the default JACK server name. The value can +be empty, in which case it implies `default`. Therefore the following three entries are equivalent: + + ; source = default + source = default + source = + +One exception is the combination of an empty `source` entry and the environment variable `JACK_DEFAULT_SERVER`. +If the environment variable is defined, e.g. `export JACK_DEFAULT_SERVER=foo`, then the following entries +are equivalent: + + source = foo + source = + +Consult the manpage `jackd(1)` for further information regarding configuration and startup of a JACK +server. + +CAVA creates terminal audio-typed (so no MIDI support) input ports. These ports can connect to output +ports of other JACK clients, e.g. connect to the output ports of a music player and CAVA will visualize +the music. Currently CAVA supports up to two input ports, i.e. it supports mono and stereo. The number +of input ports can be controlled via the `channels` option in the input section of the configuration +file: + + channels = 1 # one input port, mono + +or + + channels = 2 # two input ports, stereo + +The port's short name is simply `M` for mono, and `L` and `R` for stereo. The full name of the input +port according to the base client name is `cava:M` for mono, and `cava:L` and `cava:R` for stereo. + +Currently CAVA doesn't connect its ports to other client's ports automatically, i.e. on program start +CAVA isn't connected to any other program and won't connect during execution on its own. There are +connection management programs in order to control and manage the connection between ports of the different +client programs. Some well known connection managers with a graphical user interface include QjackCtl +and Cadence. The JACK package itself often comes with CLI tools. Depending on the operating system +they might need to get installed separately, e.g. on FreeBSD +```sh +$ doas pkg install jack-example-tools +``` +Among the tools are the programs `jack_lsp` and `jack_connect`. These two tools are enough to list +and connect ports on the commandline. The following example demonstrates how to setup connections with +these tools: +```sh +$ jack_lsp +system:capture_1 +system:capture_2 +system:playback_1 +system:playback_2 +cava:L +moc:output0 +moc:output1 +cava:R +``` +This listing shows all full port names that are currently available. These correspond to two external +JACK clients (cava and moc) and one internal JACK client (system). There are also `-p` and `-c` switches +for `jack_lsp`, with which the types and current connections between the ports are listed. Connect +the ports of cava and moc: +```sh +$ jack_connect cava:L moc:output0 +$ jack_connect cava:R moc:output1 +``` +Now CAVA visualizes the outgoing audio from MOC. ### squeezelite [squeezelite](https://en.wikipedia.org/wiki/Squeezelite) is one of several software clients available for the Logitech Media Server. Squeezelite can export its audio data as shared memory, which is what this input module uses. diff --git a/cava.c b/cava.c index 949210c..93dc478 100644 --- a/cava.c +++ b/cava.c @@ -70,6 +70,7 @@ #include "input/alsa.h" #include "input/fifo.h" +#include "input/jack.h" #include "input/oss.h" #include "input/pipewire.h" #include "input/portaudio.h" @@ -422,9 +423,17 @@ as of 0.4.0 all options are specified in config file, see in '/home/username/.co case INPUT_OSS: audio.format = p.samplebits; audio.rate = p.samplerate; + audio.channels = p.channels; audio.threadparams = 1; // OSS can adjust parameters thr_id = pthread_create(&p_thread, NULL, input_oss, (void *)&audio); break; +#endif +#ifdef JACK + case INPUT_JACK: + audio.channels = p.channels; + audio.threadparams = 1; // JACK server provides parameters + thr_id = pthread_create(&p_thread, NULL, input_jack, (void *)&audio); + break; #endif case INPUT_SHMEM: audio.format = 16; diff --git a/config.c b/config.c index f28c7de..4a9d58d 100644 --- a/config.c +++ b/config.c @@ -45,20 +45,21 @@ const char *default_shader_name[NUMBER_OF_SHADERS] = {"northern_lights.frag", "p double smoothDef[5] = {1, 1, 1, 1, 1}; enum input_method default_methods[] = { - INPUT_FIFO, INPUT_PORTAUDIO, INPUT_ALSA, INPUT_PULSE, - INPUT_PIPEWIRE, INPUT_WINSCAP, INPUT_SNDIO, INPUT_OSS, + INPUT_FIFO, INPUT_PORTAUDIO, INPUT_ALSA, INPUT_PULSE, INPUT_JACK, + INPUT_SNDIO, INPUT_PIPEWIRE, INPUT_WINSCAP, INPUT_OSS, }; char *outputMethod, *orientation, *channels, *xaxisScale, *monoOption, *fragmentShader, *vertexShader; const char *input_method_names[] = { - "fifo", "portaudio", "pipewire", "alsa", "pulse", "sndio", "oss", "shmem", "winscap", + "fifo", "portaudio", "pipewire", "alsa", "pulse", "sndio", "oss", "jack", "shmem", "winscap", }; const bool has_input_method[] = { HAS_FIFO, /** Always have at least FIFO and shmem input. */ - HAS_PORTAUDIO, HAS_PIPEWIRE, HAS_ALSA, HAS_PULSE, HAS_SNDIO, HAS_OSS, HAS_SHMEM, HAS_WINSCAP, + HAS_PORTAUDIO, HAS_PIPEWIRE, HAS_ALSA, HAS_PULSE, HAS_SNDIO, + HAS_OSS, HAS_JACK, HAS_SHMEM, HAS_WINSCAP, }; enum input_method input_method_by_name(const char *str) { @@ -328,7 +329,7 @@ bool validate_config(struct config_params *p, struct error_s *error) { if (p->stereo == -1) { write_errorf( error, - "output channels %s is not supported, supported channelss are: 'mono' and 'stereo'\n", + "output channels %s is not supported, supported channels are: 'mono' and 'stereo'\n", channels); return false; } @@ -389,6 +390,12 @@ bool validate_config(struct config_params *p, struct error_s *error) { } p->sens = p->sens / 100; + // validate: channels + if (p->channels <= 1) + p->channels = 1; + else + p->channels = 2; + return validate_colors(p, error); } @@ -546,7 +553,6 @@ bool load_config(char configPath[PATH_MAX], struct config_params *p, bool colors monoOption = malloc(sizeof(char) * 32); p->raw_target = malloc(sizeof(char) * 129); p->data_format = malloc(sizeof(char) * 32); - channels = malloc(sizeof(char) * 32); orientation = malloc(sizeof(char) * 32); vertexShader = malloc(sizeof(char) * PATH_MAX / 2); fragmentShader = malloc(sizeof(char) * PATH_MAX / 2); @@ -677,6 +683,7 @@ bool load_config(char configPath[PATH_MAX], struct config_params *p, bool colors p->samplerate = iniparser_getint(ini, "input:sample_rate", 44100); p->samplebits = iniparser_getint(ini, "input:sample_bits", 16); + p->channels = iniparser_getint(ini, "input:channels", 2); enum input_method default_input = INPUT_FIFO; for (size_t i = 0; i < ARRAY_SIZE(default_methods); i++) { @@ -716,6 +723,11 @@ bool load_config(char configPath[PATH_MAX], struct config_params *p, bool colors case INPUT_OSS: p->audio_source = strdup(iniparser_getstring(ini, "input:source", "/dev/dsp")); break; +#endif +#ifdef JACK + case INPUT_JACK: + p->audio_source = strdup(iniparser_getstring(ini, "input:source", "default")); + break; #endif case INPUT_SHMEM: p->audio_source = diff --git a/config.h b/config.h index c60bca6..165f9f5 100644 --- a/config.h +++ b/config.h @@ -43,6 +43,12 @@ #define HAS_OSS false #endif +#ifdef JACK +#define HAS_JACK true +#else +#define HAS_JACK false +#endif + #ifdef _MSC_VER #define HAS_WINSCAP true #define SDL true @@ -66,6 +72,7 @@ enum input_method { INPUT_PULSE, INPUT_SNDIO, INPUT_OSS, + INPUT_JACK, INPUT_SHMEM, INPUT_WINSCAP, INPUT_MAX, @@ -104,9 +111,9 @@ struct config_params { enum orientation orientation; int userEQ_keys, userEQ_enabled, col, bgcol, autobars, stereo, raw_format, ascii_range, bit_format, gradient, gradient_count, fixedbars, framerate, bar_width, bar_spacing, - bar_height, autosens, overshoot, waves, samplerate, samplebits, sleep_timer, sdl_width, - sdl_height, sdl_x, sdl_y, sdl_full_screen, draw_and_quit, zero_test, non_zero_test, reverse, - sync_updates, continuous_rendering, disable_blanking; + bar_height, autosens, overshoot, waves, samplerate, samplebits, channels, sleep_timer, + sdl_width, sdl_height, sdl_x, sdl_y, sdl_full_screen, draw_and_quit, zero_test, + non_zero_test, reverse, sync_updates, continuous_rendering, disable_blanking; }; struct error_s { diff --git a/configure.ac b/configure.ac index 5020225..c70a586 100644 --- a/configure.ac +++ b/configure.ac @@ -248,6 +248,29 @@ AS_IF([test "x$enable_input_oss" != "xno"], [ AM_CONDITIONAL([OSS], [test "x$have_oss" = "xyes"]) +dnl ###################### +dnl checking for jack dev +dnl ###################### +AC_ARG_ENABLE([input_jack], + AS_HELP_STRING([--disable-input-jack], + [do not include support for input from jack]) +) + +AS_IF([test "x$enable_input_jack" != "xno"], [ + PKG_CHECK_MODULES(JACK, jack, have_jack=yes, have_jack=no) + if [[ $have_jack = "yes" ]] ; then + LIBS="$LIBS $JACK_LIBS" + CPPFLAGS="$CPPFLAGS -DJACK $JACK_CFLAGS" + fi + + if [[ $have_jack = "no" ]] ; then + AC_MSG_NOTICE([WARNING: No jack dev files found building without jack support]) + fi], + [have_jack=no] +) + +AM_CONDITIONAL([JACK], [test "x$have_jack" = "xyes"]) + dnl ###################### dnl checking for math lib dnl ###################### diff --git a/example_files/config b/example_files/config index 6d92567..4c6ca32 100644 --- a/example_files/config +++ b/example_files/config @@ -52,8 +52,8 @@ [input] -# Audio capturing method. Possible methods are: 'fifo', 'portaudio', 'pipewire', 'alsa', 'pulse', 'sndio', 'oss' or 'shmem' -# Defaults to 'oss', 'sndio', 'pipewire', 'pulse', 'alsa', 'portaudio' or 'fifo', in that order, dependent on what support cava was built with. +# Audio capturing method. Possible methods are: 'fifo', 'portaudio', 'pipewire', 'alsa', 'pulse', 'sndio', 'oss', 'jack' or 'shmem' +# Defaults to 'oss', 'pipewire', 'sndio', 'jack', 'pulse', 'alsa', 'portaudio' or 'fifo', in that order, dependent on what support cava was built with. # On Mac it defaults to 'portaudio' or 'fifo' # On windows this is automatic and no input settings are needed. # @@ -76,6 +76,9 @@ # For oss 'source' will be the path to a audio device, e.g. '/dev/dsp2'. Default: '/dev/dsp', i.e. the default audio device. # README.md contains further information on how to setup CAVA for OSS on FreeBSD. # +# For jack 'source' will be the name of the JACK server to connect to, e.g. 'foobar'. Default: 'default'. +# README.md contains further information on how to setup CAVA for JACK. +# ; method = pulse ; source = auto @@ -100,17 +103,25 @@ ; method = oss ; source = /dev/dsp +; method = jack +; source = default + # The sample rate and format can be configured for some input methods. Currently # the following methods support such a configuration: 'fifo', 'pipewire' and 'oss'. # Other methods ignore these settings. # +# The channels can be configured for some input methods. Currently the following +# methods support such a configuration: 'oss' and 'jack'. +# Other methods ignore these settings. +# # For 'oss' they are only preferred values, i.e. if the values are not supported # by the chosen audio device, the device will use other supported values instead. -# Example: 48000 and 32, but the device only supports 44100 and 16, then it will -# use 44100 and 16. +# Example: 48000, 32 and 2, but the device only supports 44100, 16 and 1, then it +# will use 44100, 16 and 1. # ; sample_rate = 44100 ; sample_bits = 16 +; channels = 2 [output] diff --git a/input/common.c b/input/common.c index 9acbcb9..2f1d958 100644 --- a/input/common.c +++ b/input/common.c @@ -52,4 +52,16 @@ void reset_output_buffers(struct audio_data *data) { audio->cava_in[n] = 0; } pthread_mutex_unlock(&audio->lock); -} \ No newline at end of file +} + +void signal_threadparams(struct audio_data *audio) { + pthread_mutex_lock(&audio->lock); + audio->threadparams = 0; + pthread_mutex_unlock(&audio->lock); +} + +void signal_terminate(struct audio_data *audio) { + pthread_mutex_lock(&audio->lock); + audio->terminate = 1; + pthread_mutex_unlock(&audio->lock); +} diff --git a/input/common.h b/input/common.h index 3eaed7a..a98f97f 100644 --- a/input/common.h +++ b/input/common.h @@ -25,17 +25,19 @@ struct audio_data { unsigned int rate; unsigned int channels; int threadparams; // shared variable used to prevent main thread from cava_init before input - // threads have finalized parameters + // threads have finalized parameters (0=allow cava_init, 1=disallow) char *source; // alsa device, fifo path or pulse source int im; // input mode alsa, fifo, pulse, portaudio, shmem or sndio int terminate; // shared variable used to terminate audio thread char error_message[1024]; int samples_counter; - int IEEE_FLOAT; + int IEEE_FLOAT; // format for 32bit (0=int, 1=float) pthread_mutex_t lock; }; void reset_output_buffers(struct audio_data *data); +void signal_threadparams(struct audio_data *data); +void signal_terminate(struct audio_data *data); int write_to_cava_input_buffers(int16_t size, unsigned char *buf, void *data); diff --git a/input/jack.c b/input/jack.c new file mode 100644 index 0000000..cfa8533 --- /dev/null +++ b/input/jack.c @@ -0,0 +1,304 @@ +#include +#include + +#include + +#include "input/common.h" +#include "input/jack.h" + +// CAVA is hard-coded to a maximum of 2 channels, i.e. stereo. +#define MAX_CHANNELS 2 + +typedef jack_default_audio_sample_t sample_t; + +struct jack_data { + struct audio_data *audio; // CAVA ctx + + jack_client_t *client; // JACK client + jack_port_t *port[MAX_CHANNELS]; // input ports + jack_nframes_t nframes; // number of samples per port + sample_t *buf; // samples buffer + + int shutdown; // JACK shutdown signal (0=keep, 1=shutdown) +}; + +static bool set_rate(struct jack_data *jack) { + // Query sample rate from JACK server. If CAVA doesn't support the final value then it will + // complain later. + jack_nframes_t rate = jack_get_sample_rate(jack->client); + + if (rate <= 0) { + fprintf(stderr, __FILE__ ": jack_get_sample_rate() failed.\n"); + return false; + } + + jack->audio->rate = rate; + + return true; +} + +static bool set_format(struct jack_data *jack) { + // JACK returns 32bit float data. + jack->audio->format = 32; + jack->audio->IEEE_FLOAT = 1; + return true; +} + +static bool set_channels(struct jack_data *jack) { + // Try to create terminal audio-typed input ports for the requested number of channels. These + // ports can receive the audio data from other JACK clients. + static const char *port_name[MAX_CHANNELS][MAX_CHANNELS] = {/* mono */ {"M", NULL}, + /* stereo */ {"L", "R"}}; + static const char port_type[] = JACK_DEFAULT_AUDIO_TYPE; + static const unsigned long flags = JackPortIsInput | JackPortIsTerminal; + + int channels; + int chtype; + int ch; + + // Limit the requested channels in case CAVA becomes surround-aware and MAX_CHANNELS hasn't + // adapted yet. + channels = jack->audio->channels > MAX_CHANNELS ? MAX_CHANNELS : jack->audio->channels; + + // Determines the row in the 'port_name' table, i.e. mono, stereo, etc... + chtype = channels - 1; + + // Try to create a port for every requested channel. After the for-loop the variable 'ch' holds + // the number of actual created ports. + for (ch = 0; ch < channels; ++ch) { + if ((jack->port[ch] = jack_port_register(jack->client, port_name[chtype][ch], port_type, + flags, 0)) == NULL) + break; + } + + // If not even one port was created, then there was a deeper problem. + if (ch == 0) { + fprintf(stderr, __FILE__ ": jack_port_register('%s') failed.\n", port_name[chtype][0]); + return false; + } + + // If less ports were created than channels requested, then the JACK server is probably not + // configured for the requested channels, e.g. we requested stereo but the server only provides + // mono. In this case we rename the created ports according to the resulting table row. + if (ch < channels) { + int chtype_new; + + channels = ch; + chtype_new = channels - 1; + + for (ch = 0; ch < channels; ++ch) { + int err; + + if ((err = jack_port_rename(jack->client, jack->port[ch], port_name[chtype_new][ch])) != + 0) { + fprintf(stderr, __FILE__ ": jack_port_rename('%s', '%s') failed: 0x%x\n", + port_name[chtype][ch], port_name[chtype_new][ch], err); + return false; + } + } + } + + jack->audio->channels = channels; + + return true; +} + +static int on_buffer_size(jack_nframes_t nframes, void *arg) { + // Buffersize changes should never happen in CAVA! + struct jack_data *jack = arg; + + if ((jack->shutdown == 1) || (jack->audio->terminate == 1)) + return 0; + + if (jack->nframes != nframes) { + fprintf(stderr, __FILE__ ": Unexpected change of JACK port buffersize! Aborting!\n"); + jack->shutdown = 1; + return 1; + } + + return 0; +} + +static int on_process(jack_nframes_t nframes, void *arg) { + // Interleave samples from separate ports and feed them to CAVA. + struct jack_data *jack = arg; + sample_t *buf[MAX_CHANNELS]; + unsigned char *buf_cava; + + if ((jack->shutdown == 1) || (jack->audio->terminate == 1)) + return 0; + + for (unsigned int i = 0; i < jack->audio->channels; ++i) { + if ((buf[i] = jack_port_get_buffer(jack->port[i], nframes)) == NULL) { + fprintf(stderr, __FILE__ ": jack_port_get_buffer('%s') failed.\n", + jack_port_name(jack->port[i])); + jack->shutdown = 1; + return 1; + } + } + + switch (jack->audio->channels) { + case 1: + // If mono then no interleaving needed, feed into CAVA directly. + buf_cava = (unsigned char *)buf[0]; + break; + case 2: + // If stereo then unroll interleaving manually. + for (jack_nframes_t i = 0; i < nframes; ++i) { + jack->buf[2 * i + 0] = buf[0][i]; + jack->buf[2 * i + 1] = buf[1][i]; + } + + buf_cava = (unsigned char *)jack->buf; + break; + default: + // Else to the loops. + for (jack_nframes_t i = 0; i < nframes; ++i) { + for (unsigned int j = 0; j < jack->audio->channels; ++j) + jack->buf[jack->audio->channels * i + j] = buf[j][i]; + } + + buf_cava = (unsigned char *)jack->buf; + break; + } + + write_to_cava_input_buffers(nframes * jack->audio->channels, buf_cava, jack->audio); + + return 0; +} + +static int on_sample_rate(jack_nframes_t nframes, void *arg) { + // Sample rate changes are not supported in CAVA! + struct jack_data *jack = arg; + + if ((jack->shutdown == 1) || (jack->audio->terminate == 1)) + return 0; + + if (jack->audio->rate != nframes) { + fprintf(stderr, __FILE__ ": Unexpected change of JACK sample rate! Aborting!\n"); + jack->shutdown = 1; + return 1; + } + + return 0; +} + +static void on_shutdown(void *arg) { ((struct jack_data *)arg)->shutdown = 1; } + +void *input_jack(void *data) { + static const char client_name[] = "cava"; + static const jack_options_t options = JackNullOption | JackServerName; + static const struct timespec rqtp = {.tv_sec = 0, .tv_nsec = 1000000}; // 1ms nsleep + + struct audio_data *audio = data; + char *server_name; + jack_status_t status; + int err; + + struct jack_data jack = {0}; + + bool is_jack_activated = false; + bool success = false; + + jack.audio = audio; + + // JACK server selection by source or implicit default. + if (strlen(audio->source) == 0) + server_name = NULL; + else + server_name = audio->source; + + if ((jack.client = jack_client_open(client_name, options, &status, server_name)) == NULL) { + fprintf(stderr, __FILE__ ": Could not open JACK source '%s': 0x%x\n", server_name, status); + goto cleanup; + } + + if (!set_rate(&jack) || !set_format(&jack) || !set_channels(&jack)) + goto cleanup; + + // Parameters finalized. Signal main thread. + signal_threadparams(audio); + + // JACK returns samples per channel. Adjust its buffersize to fit within CAVA. + // Must be a power of 2. + jack.nframes = 1 << 31; + + while (jack.nframes > audio->input_buffer_size / audio->channels) + jack.nframes >>= 1; + + if ((err = jack_set_buffer_size(jack.client, jack.nframes)) != 0) { + fprintf(stderr, __FILE__ ": jack_set_buffer_size() failed: 0x%x\n", err); + goto cleanup; + } + + // Work buffer for interleaving if not mono. + if ((audio->channels > 1) && + ((jack.buf = malloc(jack.nframes * audio->channels * sizeof(sample_t))) == NULL)) { + fprintf(stderr, __FILE__ ": malloc() failed: %s\n", strerror(errno)); + goto cleanup; + } + + // Set JACK callbacks before JACK activation. + if ((err = jack_set_buffer_size_callback(jack.client, on_buffer_size, &jack)) != 0) { + fprintf(stderr, __FILE__ ": jack_set_buffer_size_callback() failed: 0x%x\n", err); + goto cleanup; + } + + if ((err = jack_set_process_callback(jack.client, on_process, &jack)) != 0) { + fprintf(stderr, __FILE__ ": jack_set_process_callback() failed: 0x%x\n", err); + goto cleanup; + } + + if ((err = jack_set_sample_rate_callback(jack.client, on_sample_rate, &jack)) != 0) { + fprintf(stderr, __FILE__ ": jack_set_sample_rate_callback() failed: 0x%x\n", err); + goto cleanup; + } + + jack_on_shutdown(jack.client, on_shutdown, &jack); + + if ((err = jack_activate(jack.client)) != 0) { + fprintf(stderr, __FILE__ ": jack_activate() failed: 0x%x\n", err); + goto cleanup; + } + + is_jack_activated = true; + + while (audio->terminate != 1) { + if (jack.shutdown == 1) + signal_terminate(audio); + + nanosleep(&rqtp, NULL); + } + + success = true; + +cleanup: + if (is_jack_activated && ((err = jack_deactivate(jack.client)) != 0)) { + fprintf(stderr, __FILE__ ": jack_deactivate() failed: 0x%x\n", err); + success = false; + } + + free(jack.buf); + + for (int i = 0; i < MAX_CHANNELS; ++i) { + if ((jack.port[i] != NULL) && + ((err = jack_port_unregister(jack.client, jack.port[i])) != 0)) { + fprintf(stderr, __FILE__ ": jack_port_unregister('%s') failed: 0x%x\n", + jack_port_name(jack.port[i]), err); + success = false; + } + } + + if ((jack.client != NULL) && ((err = jack_client_close(jack.client)) != 0)) { + fprintf(stderr, __FILE__ ": jack_client_close() failed: 0x%x\n", err); + success = false; + } + + signal_threadparams(audio); + signal_terminate(audio); + + if (!success) + exit(EXIT_FAILURE); + + return NULL; +} diff --git a/input/jack.h b/input/jack.h new file mode 100644 index 0000000..b612149 --- /dev/null +++ b/input/jack.h @@ -0,0 +1,5 @@ +// header file for jack, part of cava. + +#pragma once + +void *input_jack(void *data); diff --git a/input/oss.c b/input/oss.c index 3c2f6a5..96467a0 100644 --- a/input/oss.c +++ b/input/oss.c @@ -31,14 +31,23 @@ static bool set_format(int fd, struct audio_data *audio) { } // Determine the sle format for the requested bitlength. - if (audio->format <= 8) + switch (audio->format) { + case 8: fmt = AFMT_S8; - else if (audio->format <= 16) + break; + case 16: fmt = AFMT_S16_LE; - else if (audio->format <= 24) + break; + case 24: fmt = AFMT_S24_LE; - else + break; + case 32: fmt = AFMT_S32_LE; + break; + default: + fprintf(stderr, __FILE__ ": Invalid format: %d\n", audio->format); + return false; + } // If the requested format is not available then test for the other sle formats. if (!(fmts & fmt)) { @@ -104,25 +113,12 @@ static bool set_rate(int fd, struct audio_data *audio) { return true; } -static void signal_threadparams(struct audio_data *audio) { - pthread_mutex_lock(&audio->lock); - audio->threadparams = 0; - pthread_mutex_unlock(&audio->lock); -} - -static void signal_terminate(struct audio_data *audio) { - pthread_mutex_lock(&audio->lock); - audio->terminate = 1; - pthread_mutex_unlock(&audio->lock); -} - void *input_oss(void *data) { static const int flags = O_RDONLY; - struct audio_data *audio = (struct audio_data *)data; + struct audio_data *audio = data; int bytes; size_t buf_size; - ssize_t rd; int fd = -1; void *buf = NULL; @@ -136,7 +132,7 @@ void *input_oss(void *data) { } // For OSS it's adviced to determine format, channels and rate in this order. - if (!(set_format(fd, audio) && set_channels(fd, audio) && set_rate(fd, audio))) + if (!set_format(fd, audio) || !set_channels(fd, audio) || !set_rate(fd, audio)) goto cleanup; // Parameters finalized. Signal main thread. @@ -156,7 +152,9 @@ void *input_oss(void *data) { } while (audio->terminate != 1) { - if ((rd = read(fd, buf, buf_size)) == -1) { + ssize_t rd; + + if ((rd = read(fd, buf, buf_size)) < 0) { fprintf(stderr, __FILE__ ": read() failed: %s\n", strerror(errno)); goto cleanup; } else if (rd == 0) diff --git a/input/pulse.h b/input/pulse.h index 155da47..f93955b 100644 --- a/input/pulse.h +++ b/input/pulse.h @@ -3,4 +3,4 @@ #pragma once void *input_pulse(void *data); -void getPulseDefaultSink(); +void getPulseDefaultSink(void *data);