Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sndio: configurable parameters #546

Merged
merged 2 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 77 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ by [Karl Stavestrand](mailto:karl@stavestrand.no)
- [Pipewire](#pipewire)
- [ALSA](#alsa)
- [MPD](#mpd)
- [sndio](#sndio)
- [Sndio](#sndio)
- [OSS](#oss)
- [JACK](#jack)
- [squeezelite](#squeezelite)
Expand Down Expand Up @@ -331,20 +331,69 @@ I had some trouble with sync (the visualizer was ahead of the sound). Reducing t
buffer_time "50000" # (50ms); default is 500000 microseconds (0.5s)
}

### sndio
### Sndio

sndio is the audio framework used on OpenBSD, but it's also available on
FreeBSD and Linux. So far this is only tested on FreeBSD.
Set

method = sndio

Sndio is the audio framework used on OpenBSD, but it's also available on FreeBSD, NetBSD and Linux.
So far this is only tested on FreeBSD, but it's probably very similar on other operating systems. The
following example demonstrates how to setup CAVA for sndio on FreeBSD (please consult the [OSS](#oss)
section for a deeper explanation of the various `pcmX` sound devices and the corresponding `/dev/dspX`
audio devices in this example).
```sh
$ cat /dev/sndstat
Installed devices:
pcm0: <Realtek ALC1220 (Rear Analog)> (play/rec) default
pcm1: <Realtek ALC1220 (Front Analog Mic)> (rec)
pcm2: <USB audio> (play/rec)
No devices installed from userspace.
```
Sndio operates on device descriptors. In general for every `/dev/dspX` audio device there is a corresponding
`rsnd/X` sndio raw device descriptor. In this example there are `rsnd/0`, `rsnd/1` and `rsnd/2` (they
are not listed in `/dev`, sndio uses these descriptors to access the corresponding audio devices internally).
Sndio also handles the implicit `default` device descriptor, which acts like a symlink to the raw device
descriptor corresponding to the default audio device `/dev/dsp`. In this example it acts like a symlink
to `rsnd/0` because the default audio device `/dev/dsp` symlinks to `/dev/dsp0`. Sndio also evaluates
the environment variables `AUDIODEVICE` and `AUDIORECDEVICE`. If one of these is set (`AUDIORECDEVICE`
overrides `AUDIODEVICE` if both are set) and a sndio-aware program tries to open the `default` device
descriptor or an unspecified device descriptor, then the program will use the device descriptor specified
in the environment variable.

Now in order to visualize the mic input in CAVA, the `source` value in the configuration file must
be set to the corresponding audio descriptor:

To test it
```bash
# Start sndiod with a monitor sub-device
$ sndiod -dd -s default -m mon -s monitor
source = default # default; symlink to rsnd/0 in this example; AUDIORECDEVICE and AUDIODEVICE evaluation
source = # unspecified device descriptor; same as default above
source = rsnd/0 # for the pcm0 mic on the rear
source = rsnd/1 # for the pcm1 mic on the front
source = rsnd/2 # for the pcm2 mic on the USB headset

# Set the AUDIODEVICE environment variable to override the default
# sndio device and run cava
$ AUDIODEVICE=snd/0.monitor cava
With `source = default` one can switch the visualization on the commandline without changing the configuration
file again:
```sh
$ AUDIODEVICE=rsnd/0 cava
$ AUDIODEVICE=rsnd/1 cava
$ AUDIODEVICE=rsnd/2 cava
```
Sndio can't record the played back audio with just the raw device descriptors, i.e. the sounds from
a music player or a browser which play on the external stereo speakers through `rsnd/0` are not visualized
in CAVA. For this to work the sndio server has to be started and a monitoring sub-device has to be
created. The following example shows how to start the server and create a monitoring sub-device `snd/0`
from `rsnd/0` and then start CAVA with `AUDIODEVICE` pointing to the new monitoring sub-device:
```sh
$ sndiod -f rsnd/0 -m play,mon
$ AUDIODEVICE=snd/0 cava
```
Switch between the speakers and the USB headset:
```sh
$ sndiod -f rsnd/2 -m play,mon -s usb -f rsnd/0 -m play,mon -s speakers
$ AUDIODEVICE=snd/usb cava
$ AUDIODEVICE=snd/speakers cava
```
Consult the manpage `sndiod(8)` for further information regarding configuration and startup of a sndio
server.

### OSS

Expand Down Expand Up @@ -372,43 +421,38 @@ it.

In general for every `pcmX` device there is a corresponding `/dev/dspX` audio device. In this example
there are `/dev/dsp0`, `/dev/dsp1` and `/dev/dsp2` (the system creates them when needed, they are not
listet via `ls /dev` if they are currently not in use). The system also creates an implicit `/dev/dsp`,
listed via `ls /dev` if they are currently not in use). The system also creates an implicit `/dev/dsp`,
which acts like a symlink to the `default` audio device, in this example to `/dev/dsp0`.

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.

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

source = /dev/dsp1

for the `pcm1` mic on the front, or

source = /dev/dsp2
be set to the corresponding audio device:

for the `pcm2` mic on the USB headset.
source = /dev/dsp # default; symlink to /dev/dsp0 in this example
source = /dev/dsp0 # for the pcm0 mic on the rear
source = /dev/dsp1 # for the pcm1 mic on the front
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
play on the external stereo speakers through `/dev/dsp0` are not visualized in CAVA. A solution is
to use Virtual OSS. It can create virtual audio devices from existing audio devices and from which
the played back audio can be fed into CAVA:
to use Virtual OSS. It can create virtual audio devices from existing audio devices, in particular
it can create a loopback audio device from `/dev/dsp0` and from which the played back audio can be
fed into CAVA:
```sh
$ doas pkg install virtual_oss
$ doas virtual_oss -Q0 -C2 -c2 -r48000 -b16 -s2048 -P /dev/dsp0 -R /dev/null -w vdsp.wav -t vdsp.ctl -T /dev/sndstat -l dsp
$ doas virtual_oss -r44100 -b16 -c2 -s4ms -O /dev/dsp0 -R /dev/null -T /dev/sndstat -l dsp.cava

$ cat /dev/sndstat
Installed devices:
pcm0: <Realtek ALC1220 (Rear Analog)> (play/rec) default
pcm1: <Realtek ALC1220 (Front Analog Mic)> (rec)
pcm2: <USB audio> (play/rec)
Installed devices from userspace:
dsp: <Virtual OSS> (play/rec)
dsp.cava: <Virtual OSS> (play/rec)
```
It created a virtual device `dsp` from `/dev/dsp0`. Now the audio is visualized in CAVA with the default
`source = /dev/dsp` in the configuration file. Virtual OSS can be configured and started as a service
on FreeBSD.
It created a virtual loopback device `/dev/dsp.cava` from `/dev/dsp0`. Now the audio is visualized
in CAVA with `source = /dev/dsp.cava` in the configuration file. The playback program must have a configuration
to use the `/dev/dsp.cava` device. For programs where this is not possible, e.g. which always use `/dev/dsp`,
replace `-l dsp.cava` with `-l dsp`. Virtual OSS can be configured and started as a service on FreeBSD.

### JACK

Expand Down Expand Up @@ -460,7 +504,7 @@ The option `autoconnect` controls the connection strategy for CAVA's ports to ot

The automatic connection strategies scan the physical terminal input-ports, i.e. the real audio device
which actually outputs the sound, and applies the same connections to CAVA's ports. In this way CAVA
visualizes the played audio of JACK clients by default.
visualizes the played back audio from JACK clients by default.

In order to control and manage the connection between CAVA's ports and ports of other client programs,
there are connection management programs for JACK. Some well known connection managers with a graphical
Expand All @@ -485,7 +529,7 @@ 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`. The types and current active
connections between the ports can be listed With the `-p` and `-c` switches for `jack_lsp`. In order
connections between the ports can be listed with the `-p` and `-c` switches for `jack_lsp`. In order
to connect the ports of CAVA and MOC, `jack_connect` is used:
```sh
$ jack_connect cava:L moc:output0
Expand Down
8 changes: 5 additions & 3 deletions cava.c
Original file line number Diff line number Diff line change
Expand Up @@ -415,8 +415,10 @@ as of 0.4.0 all options are specified in config file, see in '/home/username/.co
#endif
#ifdef SNDIO
case INPUT_SNDIO:
audio.format = 16;
audio.rate = 44100;
audio.format = p.samplebits;
audio.rate = p.samplerate;
audio.channels = p.channels;
audio.threadparams = 1; // Sndio can adjust parameters
thr_id = pthread_create(&p_thread, NULL, input_sndio, (void *)&audio);
break;
#endif
Expand Down Expand Up @@ -492,7 +494,7 @@ as of 0.4.0 all options are specified in config file, see in '/home/username/.co

if (p.upper_cut_off > audio.rate / 2) {
cleanup();
fprintf(stderr, "higher cuttoff frequency can't be higher than sample rate / 2");
fprintf(stderr, "higher cutoff frequency can't be higher than sample rate / 2");
exit(EXIT_FAILURE);
}

Expand Down
16 changes: 8 additions & 8 deletions example_files/config
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@
# For fifo 'source' will be the path to fifo-file.
# For shmem 'source' will be /squeezelite-AA:BB:CC:DD:EE:FF where 'AA:BB:CC:DD:EE:FF' will be squeezelite's MAC address
#
# For sndio 'source' will be a monitor sub-device, e.g. 'snd/0.monitor'. Default: 'default', in which case a device
# should be specified with the environment variable AUDIODEVICE, e.g. on the commandline: AUDIODEVICE=snd/0.monitor cava.
# For sndio 'source' will be a raw recording audio descriptor or a monitoring sub-device, e.g. 'rsnd/2' or 'snd/1'. Default: 'default'.
# README.md contains further information on how to setup CAVA for sndio.
#
# 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.
Expand Down Expand Up @@ -106,14 +106,14 @@
; method = jack
; source = default

# The options 'sample rate', 'format', 'channels' and 'autoconnect' can be configured for some input methods:
# sample rate: 'fifo', 'pipewire', 'oss'
# format: 'fifo', 'pipewire', 'oss'
# channels: 'oss', 'jack'
# autoconnect: 'jack'
# The options 'sample_rate', 'sample_bits', 'channels' and 'autoconnect' can be configured for some input methods:
# sample_rate: fifo, pipewire, sndio, oss
# sample_bits: fifo, pipewire, sndio, oss
# channels: sndio, oss, jack
# autoconnect: jack
# Other methods ignore these settings.
#
# For 'oss' they are only preferred values, i.e. if the values are not supported
# For 'sndio' and '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, 32 and 2, but the device only supports 44100, 16 and 1, then it
# will use 44100, 16 and 1.
Expand Down
5 changes: 4 additions & 1 deletion input/jack.c
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ static bool auto_connect(struct jack_data *jack) {
for (unsigned int i = 0; i < channels; ++i) {
const char **connections;
jack_port_t *port;
const char *port_name;

if ((connections = jack_port_get_all_connections(
jack->client, jack_port_by_name(jack->client, ports[i]))) == NULL)
Expand All @@ -234,9 +235,11 @@ static bool auto_connect(struct jack_data *jack) {
else
port = jack->port[i];

port_name = jack_port_name(port);

for (int j = 0; connections[j] != NULL; ++j) {
if (jack_port_connected_to(port, connections[j]) == 0)
jack_connect(jack->client, connections[j], jack_port_name(port));
jack_connect(jack->client, connections[j], port_name);
}

jack_free(connections);
Expand Down
119 changes: 93 additions & 26 deletions input/sndio.c
Original file line number Diff line number Diff line change
@@ -1,50 +1,117 @@
#include "input/sndio.h"
#include "input/common.h"
#include <stdbool.h>
#include <stddef.h>

#include <sndio.h>

#include "input/common.h"
#include "input/sndio.h"

void *input_sndio(void *data) {
struct audio_data *audio = (struct audio_data *)data;
static const unsigned int mode = SIO_REC;

struct audio_data *audio = data;
struct sio_par par;
struct sio_hdl *hdl;
unsigned char buf[audio->input_buffer_size * audio->format / 8];
int bytes;
size_t buf_size;

struct sio_hdl *hdl = NULL;
void *buf = NULL;

bool is_sio_started = false;
bool success = false;

if ((hdl = sio_open(audio->source, mode, 0)) == NULL) {
fprintf(stderr, __FILE__ ": Could not open sndio source '%s'.\n", audio->source);
goto cleanup;
}

// The recommended approach to negotiate device parameters is to try to set them with preferred
// values and check what sndio returns for actual supported values. If CAVA doesn't support the
// final values for rate and channels then it will complain later. We test the resulting format
// explicitly here.
sio_initpar(&par);
par.sig = 1;
par.bits = audio->format;
par.sig = 1;
par.le = 1;
par.rate = audio->rate;
;
par.rchan = audio->channels;
par.appbufsz = sizeof(buf) / par.rchan;
par.rate = audio->rate;
par.appbufsz = audio->input_buffer_size * SIO_BPS(audio->format) / audio->channels;

if ((hdl = sio_open(audio->source, SIO_REC, 0)) == NULL) {
fprintf(stderr, __FILE__ ": Could not open sndio source: %s\n", audio->source);
exit(EXIT_FAILURE);
if (sio_setpar(hdl, &par) == 0) {
fprintf(stderr, __FILE__ ": sio_setpar() failed.\n");
goto cleanup;
}

if (!sio_setpar(hdl, &par) || !sio_getpar(hdl, &par) || par.sig != 1 || par.le != 1 ||
par.rate != 44100 || par.rchan != audio->channels) {
fprintf(stderr, __FILE__ ": Could not set required audio parameters\n");
exit(EXIT_FAILURE);
if (sio_getpar(hdl, &par) == 0) {
fprintf(stderr, __FILE__ ": sio_getpar() failed.\n");
goto cleanup;
}

if (!sio_start(hdl)) {
fprintf(stderr, __FILE__ ": sio_start() failed\n");
exit(EXIT_FAILURE);
switch (par.bits) {
case 8:
case 16:
case 24:
case 32:
audio->format = par.bits;
break;
default:
fprintf(stderr, __FILE__ ": No support for 8, 16, 24 or 32 bits in sndio source '%s'.\n",
audio->source);
goto cleanup;
}

audio->channels = par.rchan;
audio->rate = par.rate;

// Parameters finalized. Signal main thread.
signal_threadparams(audio);

// Get the correct number of bytes per sample. Sndio uses 32 bits for 24bit, thankfully SIO_BPS
// handles this.
bytes = SIO_BPS(audio->format);
buf_size = audio->input_buffer_size * bytes;

if ((buf = malloc(buf_size)) == NULL) {
fprintf(stderr, __FILE__ ": malloc() failed: %s\n", strerror(errno));
goto cleanup;
}

if (sio_start(hdl) == 0) {
fprintf(stderr, __FILE__ ": sio_start() failed.\n");
goto cleanup;
}

is_sio_started = true;

while (audio->terminate != 1) {
if (sio_read(hdl, buf, sizeof(buf)) == 0) {
fprintf(stderr, __FILE__ ": sio_read() failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
size_t rd;

if ((rd = sio_read(hdl, buf, buf_size)) == 0) {
fprintf(stderr, __FILE__ ": sio_read() failed.\n");
goto cleanup;
}

write_to_cava_input_buffers(audio->input_buffer_size, buf, audio);
write_to_cava_input_buffers(rd / bytes, buf, audio);
}

success = true;

cleanup:
if (is_sio_started && (sio_stop(hdl) == 0)) {
fprintf(stderr, __FILE__ ": sio_stop() failed.\n");
success = false;
}

sio_stop(hdl);
sio_close(hdl);
free(buf);

if (hdl != NULL)
sio_close(hdl);

signal_threadparams(audio);
signal_terminate(audio);

if (!success)
exit(EXIT_FAILURE);

return 0;
return NULL;
}