From 382c88b28a8d3ac98c127cb705dba7b3820ac530 Mon Sep 17 00:00:00 2001 From: Henrik Date: Mon, 12 Feb 2024 23:06:23 +0100 Subject: [PATCH 001/135] Filter pipeline steps may filter multiple channels --- CHANGELOG.md | 4 ++++ src/config.rs | 19 +++++++++++++++---- src/filters.rs | 27 ++++++++++++++++++--------- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6673409..1ba0f6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## v3.0.0 +Changes: +- Filter pipeline steps take a list of channels to filter instead of a single one. + ## v2.0.2 Bugfixes: - MacOS: Fix a segfault when reading clock source names for some capture devices. diff --git a/src/config.rs b/src/config.rs index 9e117f6..9342498 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1218,7 +1218,7 @@ impl PipelineStepMixer { #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct PipelineStepFilter { - pub channel: usize, + pub channels: Option>, pub names: Vec, #[serde(default)] pub description: Option, @@ -1807,9 +1807,20 @@ pub fn validate_config(conf: &mut Configuration, filename: Option<&str>) -> Res< } PipelineStep::Filter(step) => { if !step.is_bypassed() { - if step.channel >= num_channels { - let msg = format!("Use of non existing channel {}", step.channel); - return Err(ConfigError::new(&msg).into()); + if let Some(channels) = &step.channels { + for channel in channels { + if *channel >= num_channels { + let msg = format!("Use of non existing channel {}", channel); + return Err(ConfigError::new(&msg).into()); + } + } + for idx in 1..channels.len() { + if channels[idx..].contains(&channels[idx - 1]) { + let msg = + format!("Use of duplicated channel {}", &channels[idx - 1]); + return Err(ConfigError::new(&msg).into()); + } + } } for name in &step.names { if let Some(filters) = &conf.filters { diff --git a/src/filters.rs b/src/filters.rs index b87ab35..8c332ba 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -488,26 +488,35 @@ impl Pipeline { debug!("Build new pipeline"); trace!("Pipeline config {:?}", conf.pipeline); let mut steps = Vec::::new(); + let mut num_channels = conf.devices.capture.channels(); for step in conf.pipeline.unwrap_or_default() { match step { config::PipelineStep::Mixer(step) => { if !step.is_bypassed() { let mixconf = conf.mixers.as_ref().unwrap()[&step.name].clone(); + num_channels = mixconf.channels.out; let mixer = mixer::Mixer::from_config(step.name, mixconf); steps.push(PipelineStep::MixerStep(mixer)); } } config::PipelineStep::Filter(step) => { if !step.is_bypassed() { - let fltgrp = FilterGroup::from_config( - step.channel, - &step.names, - conf.filters.as_ref().unwrap().clone(), - conf.devices.chunksize, - conf.devices.samplerate, - processing_params.clone(), - ); - steps.push(PipelineStep::FilterStep(fltgrp)); + let step_channels = if let Some(channels) = &step.channels { + channels.clone() + } else { + (0..num_channels).collect() + }; + for channel in step_channels { + let fltgrp = FilterGroup::from_config( + channel, + &step.names, + conf.filters.as_ref().unwrap().clone(), + conf.devices.chunksize, + conf.devices.samplerate, + processing_params.clone(), + ); + steps.push(PipelineStep::FilterStep(fltgrp)); + } } } config::PipelineStep::Processor(step) => { From 617084b74e7945fde7f9757dd2d8a84e70f67f24 Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 13 Feb 2024 16:14:06 +0100 Subject: [PATCH 002/135] Update readmes --- README.md | 317 +++++++++++++------------ exampleconfigs/brokenconfig.yml | 7 +- exampleconfigs/ditherplay.yml | 9 +- exampleconfigs/gainconfig.yml | 2 +- exampleconfigs/lf_compressor.yml | 12 +- exampleconfigs/nomixers.yml | 6 +- exampleconfigs/pulseconfig.yml | 9 +- exampleconfigs/simpleconfig.yml | 8 +- exampleconfigs/simpleconfig_plot.yml | 15 +- exampleconfigs/tokens.yml | 8 +- stepbystep.md | 146 +++++------- testscripts/config_load_test/conf1.yml | 4 +- testscripts/config_load_test/conf2.yml | 4 +- testscripts/config_load_test/conf3.yml | 4 +- testscripts/config_load_test/conf4.yml | 4 +- testscripts/test_file.yml | 10 +- testscripts/test_file_sine.yml | 9 +- 17 files changed, 254 insertions(+), 320 deletions(-) diff --git a/README.md b/README.md index 16073c5..1857c51 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![CI test and lint](https://github.com/HEnquist/camilladsp/workflows/CI%20test%20and%20lint/badge.svg) A tool to create audio processing pipelines for applications such as active crossovers or room correction. -It is written in Rust to benefit from the safety and elegant handling of threading that this language provides. +It is written in Rust to benefit from the safety and elegant handling of threading that this language provides. Supported platforms: Linux, macOS, Windows. @@ -20,7 +20,7 @@ The full configuration is given in a YAML file. CamillaDSP is distributed under the [GNU GENERAL PUBLIC LICENSE Version 3](LICENSE.txt). -This includes the following disclaimer: +This includes the following disclaimer: > 15. Disclaimer of Warranty. > > THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY @@ -121,12 +121,12 @@ It does not matter if the damage is caused by incorrect usage or a bug in the so ## Background The purpose of CamillaDSP is to enable audio processing with combinations of FIR and IIR filters. This functionality is available in EqualizerAPO, but for Windows only. -For Linux the best known FIR filter engine is probably BruteFIR, which works very well but doesn't support IIR filters. -The goal of CamillaDSP is to provide both FIR and IIR filtering for Linux, Windows and macOS, to be stable, fast and flexible, and be easy to use and configure. +For Linux the best known FIR filter engine is probably BruteFIR, which works very well but doesn't support IIR filters. +The goal of CamillaDSP is to provide both FIR and IIR filtering for Linux, Windows and macOS, to be stable, fast and flexible, and be easy to use and configure. * BruteFIR: https://torger.se/anders/brutefir.html * EqualizerAPO: https://sourceforge.net/projects/equalizerapo/ -* The IIR filtering is heavily inspired by biquad-rs: https://github.com/korken89/biquad-rs +* The IIR filtering is heavily inspired by biquad-rs: https://github.com/korken89/biquad-rs ## How it works The audio pipeline in CamillaDSP runs in three separate threads. @@ -176,14 +176,14 @@ In general, a 64-bit CPU and OS will perform better. A few examples, done with CamillaDSP v0.5.0: -- A Raspberry Pi 4 doing FIR filtering of 8 channels, with 262k taps per channel, at 192 kHz. +- A Raspberry Pi 4 doing FIR filtering of 8 channels, with 262k taps per channel, at 192 kHz. CPU usage about 55%. -- An AMD Ryzen 7 2700u (laptop) doing FIR filtering of 96 channels, with 262k taps per channel, at 192 kHz. +- An AMD Ryzen 7 2700u (laptop) doing FIR filtering of 96 channels, with 262k taps per channel, at 192 kHz. CPU usage just under 100%. ### Linux requirements -Both 64 and 32 bit architectures are supported. All platforms supported by the Rustc compiler should work. +Both 64 and 32 bit architectures are supported. All platforms supported by the Rustc compiler should work. Pre-built binaries are provided for: - x86_64 (almost all PCs) @@ -214,7 +214,7 @@ These are the key dependencies for CamillaDSP. * https://crates.io/crates/alsa - Alsa audio backend * https://crates.io/crates/clap - Command line argument parsing * https://crates.io/crates/cpal - Jack audio backend -* https://crates.io/crates/libpulse-simple-binding - PulseAudio audio backend +* https://crates.io/crates/libpulse-simple-binding - PulseAudio audio backend * https://crates.io/crates/realfft - Wrapper for RustFFT that speeds up FFTs of real-valued data * https://crates.io/crates/rustfft - FFT used for FIR filters * https://crates.io/crates/rubato - Sample rate conversion @@ -239,7 +239,7 @@ Binaries for each release are available for the most common systems. See the ["Releases"](https://github.com/HEnquist/camilladsp/releases) page. To see the files click "Assets". -These are compressed files containing a single executable file that is ready to run. +These are compressed files containing a single executable file that is ready to run. The following configurations are provided: @@ -255,9 +255,9 @@ The following configurations are provided: All builds include the Websocket server. -The `.tar.gz`-files can be uncompressed with the `tar` command: +The `.tar.gz`-files can be uncompressed with the `tar` command: -```sh +```sh tar -xvf camilladsp-linux-amd64.tar.gz ``` @@ -333,7 +333,7 @@ If possible, it's recommended to use a pre-built binary on these systems. - - see below for other options - The binary is now available at ./target/release/camilladsp - Optionally install with `cargo install --path .` -- - Note: the `install` command takes the same options for features as the `build` command. +- - Note: the `install` command takes the same options for features as the `build` command. ## Customized build All the available options, or "features" are: @@ -345,7 +345,7 @@ All the available options, or "features" are: - `secure-websocket`: Enable secure websocket, also enables the `websocket` feature. - `FFTW`: Use FFTW instead of RustFFT. - `32bit`: Perform all calculations with 32-bit floats (instead of 64). -- `debug`: Enable extra logging, useful for debugging. +- `debug`: Enable extra logging, useful for debugging. - `avoid-rustc-issue-116359`: Enable a workaround for [rust issue #116359](https://github.com/rust-lang/rust/issues/116359). Used to check if a performance issue is caused by this compiler bug. @@ -386,7 +386,7 @@ The `jack-backend` feature requires jack and its development files. To install: By default Cargo builds for a generic system, meaning the resulting binary might not run as fast as possible on your system. This means for example that it will not use AVX on an x86-64 CPU, or NEON on a Raspberry Pi. -To make an optimized build for your system, you can specify this in your Cargo configuration file. +To make an optimized build for your system, you can specify this in your Cargo configuration file. Or, just set the RUSTFLAGS environment variable by adding RUSTFLAGS='...' in from of the "cargo build" or "cargo install" command. Make an optimized build on x86-64: @@ -396,7 +396,7 @@ RUSTFLAGS='-C target-cpu=native' cargo build --release On a Raspberry Pi also state that NEON should be enabled: ``` -RUSTFLAGS='-C target-feature=+neon -C target-cpu=native' cargo build --release +RUSTFLAGS='-C target-feature=+neon -C target-cpu=native' cargo build --release ``` ## Building on Windows and macOS @@ -409,7 +409,7 @@ RUSTFLAGS='-C target-cpu=native' cargo build --release Windows (cmd.exe command prompt): ``` -set RUSTFLAGS=-C target-cpu=native +set RUSTFLAGS=-C target-cpu=native cargo build --release ``` @@ -499,7 +499,7 @@ By passing the verbosity flag once, `-v`, `debug` messages are enabled. If it's given twice, `-vv`, it also prints `trace` messages. The log messages are normally written to the terminal via stderr, but they can instead be written to a file by giving the `--logfile` option. -The argument should be the path to the logfile. If this file is not writable, CamillaDSP will panic and exit. +The argument should be the path to the logfile. If this file is not writable, CamillaDSP will panic and exit. ### Persistent storage of state @@ -537,7 +537,7 @@ volume: ### Websocket -To enable the websocket server, provide a port number with the `--port` option. Leave it out, or give 0 to disable. +To enable the websocket server, provide a port number with the `--port` option. Leave it out, or give 0 to disable. By default the websocket server binds to the address 127.0.0.1 which means it's only accessible locally (to clients running on the same machine). If it should be also available to remote machines, give the IP address of the interface where it should be available with the `--address` option. @@ -560,12 +560,13 @@ If then `enable_rate_adjust` is false and `capture_samplerate`=`samplerate`, the When overriding the samplerate, two other parameters are scaled as well. Firstly, the `chunksize` is multiplied or divided by integer factors to try to keep the pipeline running at a constant number of chunks per second. Secondly, the value of `extra_samples` is scaled to give the extra samples the same duration at the new samplerate. -But if the `extra_samples` override is used, the given value is used without scaling it. +But if the `extra_samples` override is used, the given value is used without scaling it. ### Initial volume -The `--gain` option can accept negative values, but this requires a little care since the minus sign can be misinterpreted as another option. +The `--gain` option can accept negative values, +but this requires a little care since the minus sign can be misinterpreted as another option. It works as long as there is no space in front of the minus sign. These work (for a gain of +/- 12.3 dB): @@ -583,7 +584,7 @@ These will __NOT__ work: ``` -g -12.3 --gain -12.3 -``` +``` ## Exit codes @@ -609,7 +610,7 @@ See the [separate readme for the websocket server](./websocket.md) # Processing audio The goal is to insert CamillaDSP between applications and the sound card. The details of how this is achieved depends on which operating system and which audio API is being used. -It is also possible to use pipes for apps that support reading or writing audio data from/to stdout. +It is also possible to use pipes for apps that support reading or writing audio data from/to stdout. ## Cross-platform These backends are supported on all platforms. @@ -619,12 +620,13 @@ Audio can be read from a file or a pipe using the `File` device type. This can read raw interleaved samples in most common formats. To instead read from stdin, use the `Stdin` type. -This makes it possible to pipe raw samples from some applications directly to CamillaDSP, without going via a virtual soundcard. +This makes it possible to pipe raw samples from some applications directly to CamillaDSP, +without going via a virtual soundcard. ### Jack Jack is most commonly used with Linux, but can also be used with both Windows and MacOS. The Jack support of CamillaDSP version should be considered experimental. -It is implemented using the CPAL library, which currently only supports Jack on Linux. +It is implemented using the CPAL library, which currently only supports Jack on Linux. #### Using Jack The Jack server must be running. @@ -632,7 +634,7 @@ The Jack server must be running. Set `device` to "default" for both capture and playback. The sample format is fixed at 32-bit float (FLOAT32LE). -The samplerate must match the samplerate configured for the Jack server. +The samplerate must match the samplerate configured for the Jack server. CamillaDSP will show up in Jack as "cpal_client_in" and "cpal_client_out". @@ -645,7 +647,8 @@ See the [separate readme for CoreAudio](./backend_coreaudio.md). ## Linux Linux offers several audio APIs that CamillaDSP can use. -### Alsa + +### Alsa See the [separate readme for ALSA](./backend_alsa.md). ### PulseAudio @@ -688,8 +691,8 @@ This device can be set as the default output in the Gnome sound settings, meanin The audio sent to this device can then be captured from the monitor output named "MySink.monitor" using the PulseAudio backend. Pipewire can also be configured to output to an ALSA Loopback. -This is done by adding an ALSA sink in the Pipewire configuration. -This sink then becomes available as an output device in the Gnome sound settings. +This is done by adding an ALSA sink in the Pipewire configuration. +This sink then becomes available as an output device in the Gnome sound settings. See the "camilladsp-config" repository under [Related projects](#related-projects) for an example Pipewire configuration. TODO test with Jack. @@ -697,14 +700,14 @@ TODO test with Jack. ### BlueALSA BlueALSA ([bluez-alsa](https://github.com/Arkq/bluez-alsa)) is a project to receive or send audio through Bluetooth A2DP. The `Bluez` source will connect to BlueALSA via D-Bus to get a file descriptor. -It will then read the audio directly from there, avoiding the need to go via ALSA. +It will then read the audio directly from there, avoiding the need to go via ALSA. Currently only capture (a2dp-sink) is supported. BlueALSA is supported on Linux only, and requires building CamillaDSP with the `bluez-backend` Cargo feature. #### Prerequisites Start by installing `bluez-alsa`. Both Pipewire and PulseAudio will interfere with BlueALSA and must be disabled. -The source device should be paired after disabling Pipewire or PulseAudio and enabling BlueALSA. +The source device should be paired after disabling Pipewire or PulseAudio and enabling BlueALSA. #### Configuration @@ -736,13 +739,13 @@ This should produce output similar to this: ({objectpath '/org/bluealsa/hci0/dev_A0_B1_C2_D3_E4_F5/a2dpsnk/source': {'org.bluealsa.PCM1': {'Device': , 'Sequence': , 'Transport': <'A2DP-sink'>, 'Mode': <'source'>, 'Format': , 'Channels': , 'Sampling': , 'Codec': <'AAC'>, 'CodecConfiguration': <[byte 0x80, 0x01, 0x04, 0x03, 0x5b, 0x60]>, 'Delay': , 'SoftVolume': , 'Volume': }}},) ``` The wanted path is the string after `objectpath`. -If the output is looking like `(@a{oa{sa{sv}}} {},)`, then no A2DP source is connected or detected. +If the output is looking like `(@a{oa{sa{sv}}} {},)`, then no A2DP source is connected or detected. Connect an A2DP device and try again. If a device is already connected, try removing and pairing the device again. The `service` property can be left out to get the default. This only needs changing if there is more than one instance of BlueALSA running. You have to specify correct capture sample rate, number of channel and sample format. -These parameters can be found with `bluealsa-aplay`: +These parameters can be found with `bluealsa-aplay`: ``` > bluealsa-aplay -L @@ -751,7 +754,7 @@ bluealsa:DEV=A0:B1:C2:D3:E4:F5,PROFILE=a2dp,SRV=org.bluealsa A2DP (AAC): S16_LE 2 channels 44100 Hz ``` -Note that Bluetooth transfers data in chunks, and the time between chunks can vary. +Note that Bluetooth transfers data in chunks, and the time between chunks can vary. To avoid underruns, use a large chunksize and a large target_level. The values in the example above are a good starting point. Rate adjust should also be enabled. @@ -785,7 +788,7 @@ filters: type: Raw < example_fir_b: parameters: <-- "parameters" can be placed before or after "type". - type: Wav + type: Wav filename: path/to/filter.wav type: Conv @@ -842,7 +845,7 @@ title: "Example" description: "Example description of a configuration" ``` -Both these properties are optional and can be set to `null` or left out. +Both these properties are optional and can be set to `null` or left out. The `title` property is intended for a short title, while `description` can be longer and more descriptive. ## Volume control @@ -892,13 +895,13 @@ A parameter marked (*) in any example is optional. If they are left out from the * `samplerate` - The `samplerate` setting decides the sample rate that everything will run at. + The `samplerate` setting decides the sample rate that everything will run at. This rate must be supported by both the capture and playback device. * `chunksize` - All processing is done in chunks of data. The `chunksize` is the number of samples each chunk will have per channel. - It's good if the number is an "easy" number like a power of two, since this speeds up the FFT in the Convolution filter. + All processing is done in chunks of data. The `chunksize` is the number of samples each chunk will have per channel. + It's good if the number is an "easy" number like a power of two, since this speeds up the FFT in the Convolution filter. Suggested starting points for different sample rates: - 44.1 or 48 kHz: 1024 - 88.2 or 96 kHz: 2048 @@ -909,7 +912,7 @@ A parameter marked (*) in any example is optional. If they are left out from the If you have long FIR filters you can reduce CPU usage by making the chunksize larger. When increasing, try increasing in factors of two, like 1024 -> 2048 or 4096 -> 8192. - + * `queuelimit` (optional, defaults to 4) @@ -938,18 +941,18 @@ A parameter marked (*) in any example is optional. If they are left out from the With Alsa capture devices, the first option is used whenever it's available. If not, and when not using an Alsa capture device, then the second option is used. - + * `target_level` (optional, defaults to the `chunksize` value) The value is the number of samples that should be left in the buffer of the playback device when the next chunk arrives. Only applies when `enable_rate_adjust` is set to `true`. - It will take some experimentation to find the right number. - If it's too small there will be buffer underruns from time to time, - and making it too large might lead to a longer input-output delay than what is acceptable. - Suitable values are in the range 1/2 to 1 times the `chunksize`. - + It will take some experimentation to find the right number. + If it's too small there will be buffer underruns from time to time, + and making it too large might lead to a longer input-output delay than what is acceptable. + Suitable values are in the range 1/2 to 1 times the `chunksize`. + * `adjust_period` (optional, defaults to 10) - + The `adjust_period` parameter is used to set the interval between corrections, in seconds. The default is 10 seconds. Only applies when `enable_rate_adjust` is set to `true`. A smaller value will make for a faster reaction time, which may be useful if there are occasional @@ -957,7 +960,7 @@ A parameter marked (*) in any example is optional. If they are left out from the * `silence_threshold` & `silence_timeout` (optional) The fields `silence_threshold` and `silence_timeout` are optional - and used to pause processing if the input is silent. + and used to pause processing if the input is silent. The threshold is the threshold level in dB, and the level is calculated as the difference between the minimum and maximum sample values for all channels in the capture buffer. 0 dB is full level. Some experimentation might be needed to find the right threshold. @@ -990,11 +993,11 @@ A parameter marked (*) in any example is optional. If they are left out from the * `volume_ramp_time` (optional, defaults to 400 ms) This setting controls the duration of this ramp when changing volume of the default volume control. The value must not be negative. If left out or set to `null`, it defaults to 400 ms. - + * `capture` and `playback` - Input and output devices are defined in the same way. + Input and output devices are defined in the same way. A device needs: - * `type`: + * `type`: The available types depend on which features that were included when compiling. All possible types are: * `File` * `Stdin` (capture only) @@ -1003,7 +1006,7 @@ A parameter marked (*) in any example is optional. If they are left out from the * `Jack` * `Wasapi` * `CoreAudio` - * `Alsa` + * `Alsa` * `Pulse` * `channels`: number of channels * `device`: device name (for Alsa, Pulse, Wasapi, CoreAudio). For CoreAudio and Wasapi, "default" will give the default device. @@ -1014,7 +1017,7 @@ A parameter marked (*) in any example is optional. If they are left out from the * S16LE - Signed 16-bit int, stored as two bytes * S24LE - Signed 24-bit int, stored as four bytes (three bytes of data, one padding byte) * S24LE3 - Signed 24-bit int, stored as three bytes (with no padding) - * S32LE - Signed 32-bit int, stored as four bytes + * S32LE - Signed 32-bit int, stored as four bytes * FLOAT32LE - 32-bit float, stored as four bytes * FLOAT64LE - 64-bit float, stored as eight bytes @@ -1030,8 +1033,8 @@ A parameter marked (*) in any example is optional. If they are left out from the | S32LE | Yes | Yes | Yes | Yes | No | Yes | | FLOAT32LE | Yes | Yes | Yes | Yes | Yes | Yes | | FLOAT64LE | Yes | No | No | No | No | Yes | - - + + ### Equivalent formats This table shows which formats in the different APIs are equivalent. @@ -1044,20 +1047,23 @@ A parameter marked (*) in any example is optional. If they are left out from the | S32LE | S32_LE | S32LE | | FLOAT32LE | FLOAT_LE | FLOAT32LE | | FLOAT64LE | FLOAT64_LE | - | - + ### File, Stdin, Stdout The `File` device type reads or writes to a file, while `Stdin` reads from stdin and `Stdout` writes to stdout. The format is raw interleaved samples, in the selected sample format. - If the capture device reaches the end of a file, the program will exit once all chunks have been played. - That delayed sound that would end up in a later chunk will be cut off. To avoid this, set the optional parameter `extra_samples` for the File capture device. - This causes the capture device to yield the given number of samples (per channel) after reaching end of file, allowing any delayed sound to be played back. + If the capture device reaches the end of a file, the program will exit once all chunks have been played. + That delayed sound that would end up in a later chunk will be cut off. + To avoid this, set the optional parameter `extra_samples` for the File capture device. + This causes the capture device to yield the given number of samples (per channel) after reaching end of file, + allowing any delayed sound to be played back. The `Stdin` capture device and `Stdout` playback device use stdin and stdout, so it's possible to easily pipe audio between applications: ``` > camilladsp stdio_capt.yml > rawfile.dat > cat rawfile.dat | camilladsp stdio_pb.yml ``` - Note: On Unix-like systems it's also possible to use the File device and set the filename to `/dev/stdin` for capture, or `/dev/stdout` for playback. + Note: On Unix-like systems it's also possible to use the File device + and set the filename to `/dev/stdin` for capture, or `/dev/stdout` for playback. Please note the `File` capture device isn't able to read wav-files directly. If you want to let CamillaDSP play wav-files, please see the [separate guide for converting wav to raw files](coefficients_from_wav.md). @@ -1078,7 +1084,7 @@ A parameter marked (*) in any example is optional. If they are left out from the filename: "/path/to/outputfile.raw" format: S32LE ``` - + Example config for Stdin/Stdout: ``` capture: @@ -1097,7 +1103,7 @@ A parameter marked (*) in any example is optional. If they are left out from the The `File` and `Stdin` capture devices support two additional optional parameters, for advanced handling of raw files and testing: * `skip_bytes`: Number of bytes to skip at the beginning of the file or stream. This can be used to skip over the header of some formats like .wav (which typically has a fixed size 44-byte header). - Leaving it out or setting to zero means no bytes are skipped. + Leaving it out or setting to zero means no bytes are skipped. * `read_bytes`: Read only up until the specified number of bytes. Leave it out or set it to zero to read until the end of the file or stream. @@ -1184,13 +1190,13 @@ and then use polynomial interpolation to get values for arbitrary times between The AsyncSinc resampler takes an additional parameter `profile`. This is used to select a pre-defined profile. The `Balanced` profile is the best choice in most cases. -It provides good resampling quality with a noise threshold in the range +It provides good resampling quality with a noise threshold in the range of -170 dB along with reasonable CPU usage. -As -170 dB is way beyond the resolution limit of even the best commercial DACs, +As -170 dB is way beyond the resolution limit of even the best commercial DACs, this preset is thus sufficient for all audio use. -The `Fast` and `VeryFast` profiles are faster but have a little more high-frequency roll-off +The `Fast` and `VeryFast` profiles are faster but have a little more high-frequency roll-off and give a bit higher resampling artefacts. -The `Accurate` profile provides the highest quality result, +The `Accurate` profile provides the highest quality result, with all resampling artefacts below -200dB, at the expense of higher CPU usage. Example: @@ -1202,7 +1208,7 @@ Example: It is also possible to specify all parameters of the resampler instead of using the pre-defined profiles. -Example: +Example: ``` resampler: type: AsyncSinc @@ -1216,7 +1222,7 @@ Note that these two ways of defining the resampler cannot be mixed. When using `profile` the other parameters must not be present and vice versa. The `f_cutoff` parameter is the relative cutoff frequency of the anti-aliasing filter. A value of 1.0 means the Nyquist limit. Useful values are in the range 0.9 - 0.99. -It can also be calculated automatically by setting `f_cutoff` to `null`. +It can also be calculated automatically by setting `f_cutoff` to `null`. Available interpolation types: @@ -1234,14 +1240,14 @@ For reference, the profiles are defined according to this table: | | VeryFast | Fast | Balanced | Accurate | |--------------------|:------------:|:----------------:|:------------------:|:------------------:| -|sinc_len | 64 | 128 | 192 | 256 | +|sinc_len | 64 | 128 | 192 | 256 | |oversampling_factor | 1024 | 1024 | 512 | 256 | |interpolation | Linear | Linear | Quadratic | Cubic | |window | Hann2 | Blackman2 | BlackmanHarris2 | BlackmanHarris2 | |f_cutoff | 0.91 (#) | 0.92 (#) | 0.93 (#) | 0.95 (#) | (#) These cutoff values are approximate. The actual values used are calculated automatically -at runtime for the combination of sinc length and window. +at runtime for the combination of sinc length and window. ### `AsyncPoly`: Asynchronous resampling without anti-aliasing @@ -1276,11 +1282,11 @@ Use the `AsyncPoly` type to save processing power, with little or no perceived q ### `Synchronous`: Synchronous resampling with anti-aliasing -For performing fixed ratio resampling, like resampling +For performing fixed ratio resampling, like resampling from 44.1kHz to 96kHz (which corresponds to a precise ratio of 147/320) choose the `Synchronous` type. -This works by transforming the waveform with FFT, modifying the spectrum, and then +This works by transforming the waveform with FFT, modifying the spectrum, and then getting the resampled waveform by inverse FFT. This is considerably faster than the asynchronous variants, but does not support rate adjust. @@ -1294,12 +1300,12 @@ The `Synchronous` type takes no additional parameters: ``` ### Rate adjust via resampling -When using the rate adjust feature to match capture and playback devices, -one of the "Async" types must be used. +When using the rate adjust feature to match capture and playback devices, +one of the "Async" types must be used. These asynchronous resamplers do not rely on a fixed resampling ratio. -When rate adjust is enabled the resampling ratio is dynamically adjusted in order to compensate -for drifts and mismatches between the input and output sample clocks. -Using the "Synchronous" type with rate adjust enabled will print warnings, +When rate adjust is enabled the resampling ratio is dynamically adjusted in order to compensate +for drifts and mismatches between the input and output sample clocks. +Using the "Synchronous" type with rate adjust enabled will print warnings, and any rate adjust request will be ignored. @@ -1377,7 +1383,7 @@ Then, setting the number of capture channels to 4 will enable both inputs. In this case we are only interested in the SPDIF input. This is then done by adding a mixer that reduces the number of channels to 2. In this mixer, input channels 0 and 1 are not mapped to anything. -This is then detected, and no format conversion, resampling or processing will be done on these two channels. +This is then detected, and no format conversion, resampling or processing will be done on these two channels. ## Filters The filters section defines the filter configurations to use in the pipeline. @@ -1397,13 +1403,15 @@ The `gain` value is given in either dB or as a linear factor, depending on the ` This can be set to `dB` or `linear`. If set to `null` or left out it defaults to dB. -When the dB scale is used (`scale: dB`), a positive gain value means the signal will be amplified while a negative values attenuates. +When the dB scale is used (`scale: dB`), a positive gain value means the signal will be amplified +while a negative values attenuates. The gain value must be in the range -150 to +150 dB. If linear gain is used (`scale: linear`), the gain value is treated as a simple multiplication factor. A factor 0.5 attenuates by a factor two (equivalent to a gain of -6.02 dB). A negative value inverts the signal. -Note that the `invert` setting also inverts, so a gain of -0.5 with invert set to true becomes inverted twice and the result is non-inverted. +Note that the `invert` setting also inverts, so a gain of -0.5 with invert set to true +becomes inverted twice and the result is non-inverted. The linear gain is limited to a range of -10.0 to +10.0. The `mute` parameter determines if the the signal should be muted. @@ -1436,7 +1444,7 @@ and it's not possible to select this fader for Volume filters. Volume filters may use the four additional faders, named `Aux1`, `Aux2`,`Aux3` and `Aux4`. -A Volume filter is configured to react to one of these faders. +A Volume filter is configured to react to one of these faders. The volume can then be changed via the websocket, by changing the corresponding fader. A request to set the volume will be applied to all Volume filters listening to the affected `fader`. @@ -1496,7 +1504,7 @@ filters: type: Loudness parameters: fader: Main (*) - reference_level: -25.0 + reference_level: -25.0 high_boost: 7.0 (*) low_boost: 7.0 (*) attenuate_mid: false (*) @@ -1507,16 +1515,16 @@ Allowed ranges: - low_boost: 0 to 20 ### Delay -The delay filter provides a delay in milliseconds, millimetres or samples. +The delay filter provides a delay in milliseconds, millimetres or samples. The `unit` can be `ms`, `mm` or `samples`, and if left out it defaults to `ms`. When giving the delay in millimetres, the speed of sound of is assumed to be 343 m/s (dry air at 20 degrees Celsius). If the `subsample` parameter is set to `true`, then it will use use an IIR filter to achieve subsample delay precision. If set to `false`, the value will instead be rounded to the nearest number of full samples. This is a little faster and should be used if subsample precision is not required. - -The delay value must be positive or zero. + +The delay value must be positive or zero. Example Delay filter: ``` @@ -1541,7 +1549,7 @@ filters: example_fir_a: type: Conv parameters: - type: Raw + type: Raw filename: path/to/filter.txt format: TEXT skip_bytes_lines: 0 (*) @@ -1549,7 +1557,7 @@ filters: example_fir_b: type: Conv parameters: - type: Wav + type: Wav filename: path/to/filter.wav channel: 0 (*) ``` @@ -1564,8 +1572,10 @@ If it's not found there, the path is assumed to be relative to the current worki Note that this only applies when the config is loaded from a file. When a config is supplied via the websocket server only the current working dir of the CamillaDSP process will be searched. -If the filename includes the tokens `$samplerate$` or `$channels$`, these will be replaced by the corresponding values from the config. -For example, if samplerate is 44100, the filename `/path/to/filter_$samplerate$.raw` will be updated to `/path/to/filter_44100.raw`. +If the filename includes the tokens `$samplerate$` or `$channels$`, +these will be replaced by the corresponding values from the config. +For example, if samplerate is 44100, +the filename `/path/to/filter_$samplerate$.raw` will be updated to `/path/to/filter_44100.raw`. #### Values directly in config file @@ -1608,15 +1618,17 @@ The sample rate of the file is ignored. To load coefficients from a raw file, use the `Raw` type. This is also used to load coefficients from text files. Raw files are often saved with a `.dbl`, `.raw`, or `.pcm` ending. The lack of a header means that the files doesn't contain any information about data format etc. -CamillaDSP supports loading coefficients from such files that contain a single channel only (stereo files are not supported), in all the most common sample formats. +CamillaDSP supports loading coefficients from such files that contain a single channel only +(stereo files are not supported), in all the most common sample formats. The `Raw` type supports two additional optional parameters, for advanced handling of raw files and text files with headers: * `skip_bytes_lines`: Number of bytes (for raw files) or lines (for text) to skip at the beginning of the file. - This can be used to skip over a header. Leaving it out or setting to zero means no bytes or lines are skipped. + This can be used to skip over a header. Leaving it out or setting to zero means no bytes or lines are skipped. * `read_bytes_lines`: Read only up until the specified number of bytes (for raw files) or lines (for text). Leave it out or set it to zero to read until the end of the file. The filter coefficients can be provided either as text, or as raw samples. Each file can only hold one channel. -The "format" parameter can be omitted, in which case it's assumed that the format is TEXT. This format is a simple text file with one value per row: +The "format" parameter can be omitted, in which case it's assumed that the format is TEXT. +This format is a simple text file with one value per row: ``` -0.000021 -0.000020 @@ -1635,7 +1647,8 @@ The other possible formats are raw data: ### IIR IIR filters are implemented as Biquad filters. -CamillaDSP can calculate the coefficients for a number of standard filters, or you can provide the coefficients directly. +CamillaDSP can calculate the coefficients for a number of standard filters, +or you can provide the coefficients directly. Examples: ``` @@ -1699,27 +1712,28 @@ Single Biquads are defined using the type "Biquad". The available filter types a * Highpass & Lowpass Second order high/lowpass filters (12dB/oct) - + Defined by cutoff frequency `freq` and Q-value `q`. * HighpassFO & LowpassFO First order high/lowpass filters (6dB/oct) - + Defined by cutoff frequency `freq`. * Highshelf & Lowshelf - + High / Low uniformly affects the high / low frequencies respectively while leaving the low / high part unaffected. In between there is a slope of variable steepness. Parameters: * `freq` is the center frequency of the sloping section. * `gain` gives the gain of the filter * `slope` is the steepness in dB/octave. Values up to around +-12 are usable. - * `q` is the Q-value and can be used instead of `slope` to define the steepness of the filter. Only one of `q` and `slope` can be given. + * `q` is the Q-value and can be used instead of `slope` to define the steepness of the filter. + Only one of `q` and `slope` can be given. * HighshelfFO & LowshelfFO - + First order (6dB/oct) versions of the shelving functions. Parameters: @@ -1727,22 +1741,26 @@ Single Biquads are defined using the type "Biquad". The available filter types a * `gain` gives the gain of the filter * Peaking - - A parametric peaking filter with selectable gain `gain` at a given frequency `freq` with a bandwidth given either by the Q-value `q` or bandwidth in octaves `bandwidth`. + + A parametric peaking filter with selectable gain `gain` at a given frequency `freq` + with a bandwidth given either by the Q-value `q` or bandwidth in octaves `bandwidth`. Note that bandwidth and Q-value are inversely related, a small bandwidth corresponds to a large Q-value etc. Use positive gain values to boost, and negative values to attenuate. * Notch - - A notch filter to attenuate a given frequency `freq` with a bandwidth given either by the Q-value `q` or bandwidth in octaves `bandwidth`. + + A notch filter to attenuate a given frequency `freq` with a bandwidth + given either by the Q-value `q` or bandwidth in octaves `bandwidth`. The notch filter is similar to a Peaking filter configured with a large negative gain. * GeneralNotch The general notch is a notch where the pole and zero can be placed at different frequencies. - It is defined by its zero frequency `freq_z`, pole frequency `freq_p`, pole Q `q_p`, and an optional parameter `normalize_at_dc`. + It is defined by its zero frequency `freq_z`, pole frequency `freq_p`, + pole Q `q_p`, and an optional parameter `normalize_at_dc`. - When pole and zero frequencies are different, the low-frequency gain is changed and the shape (peakiness) at the `freq_p` side of the notch can be controlled by `q_p`. + When pole and zero frequencies are different, the low-frequency gain is changed + and the shape (peakiness) at the `freq_p` side of the notch can be controlled by `q_p`. The response is similar to an adjustable Q 2nd order shelf between `freq_p` and `freq_z` plus a notch at `freq_z`. The highpass-notch form is obtained when `freq_p` > `freq_z`. @@ -1758,27 +1776,29 @@ Single Biquads are defined using the type "Biquad". The available filter types a Note that when the pole and zero frequencies are set to the same value the common (symmetrical) notch is obtained. * Bandpass - + A second order bandpass filter for a given frequency `freq` with a bandwidth given either by the Q-value `q` or bandwidth in octaves `bandwidth`. * Allpass - A second order allpass filter for a given frequency `freq` with a steepness given either by the Q-value `q` or bandwidth in octaves `bandwidth` + A second order allpass filter for a given frequency `freq` with a steepness given + either by the Q-value `q` or bandwidth in octaves `bandwidth` * AllpassFO A first order allpass filter for a given frequency `freq`. * LinkwitzTransform - + A normal sealed-box speaker has a second order high-pass frequency response given by a resonance frequency and a Q-value. - A [Linkwitz transform](https://linkwitzlab.com/filters.htm#9) can be used to apply a tailored filter that modifies the actual frequency response to a new target response. + A [Linkwitz transform](https://linkwitzlab.com/filters.htm#9) can be used to apply a tailored filter + that modifies the actual frequency response to a new target response. The target is also a second order high-pass function, given by the target resonance frequency and Q-value. Parameters: * `freq_act`: actual resonance frequency of the speaker. * `q_act`: actual Q-value of the speaker. - * `freq_target`: target resonance frequency. + * `freq_target`: target resonance frequency. * `q_target`: target Q-value. To build more complex filters, use the type "BiquadCombo". @@ -1809,11 +1829,12 @@ The available types are: The `gain` value is limited to +- 100 dB. * FivePointPeq - - This filter combo is mainly meant to be created by guis. Is defines a 5-point (or band) parametric equalizer by combining a Lowshelf, a Highshelf and three Peaking filters. + + This filter combo is mainly meant to be created by guis. + It defines a 5-point (or band) parametric equalizer by combining a Lowshelf, a Highshelf and three Peaking filters. Each individual filter is defined by frequency, gain and q. The parameter names are: - * Lowshelf: `fls`, `gls`, `qls` + * Lowshelf: `fls`, `gls`, `qls` * Peaking 1: `fp1`, `gp1`, `qp1` * Peaking 2: `fp2`, `gp2`, `qp2` * Peaking 3: `fp3`, `gp3`, `qp3` @@ -1822,7 +1843,8 @@ The available types are: All 15 parameters must be included in the config. -Other types such as Bessel filters can be built by combining several Biquads. [See the separate readme for more filter functions.](./filterfunctions.md) +Other types such as Bessel filters can be built by combining several Biquads. +[See the separate readme for more filter functions.](./filterfunctions.md) * GraphicEqualizer @@ -1834,11 +1856,12 @@ Other types such as Bessel filters can be built by combining several Biquads. [S The number of bands, and the gain for each band is given by the `gains` parameter. This accepts a list of gains in dB. The number of values determines the number of bands. - The gains are limited to +- 40 dB. + The gains are limited to +- 40 dB. The band frequencies are distributed evenly on the logarithmic frequency scale, and each band has the same relative bandwidth. - For example a 31-band equalizer on the default range gets a 1/3 octave bandwith, with the first three bands centered at 22.4, 27.9, 34.9 Hz, and the last two at 14.3 and 17.9 kHz. + For example a 31-band equalizer on the default range gets a 1/3 octave bandwith, + with the first three bands centered at 22.4, 27.9, 34.9 Hz, and the last two at 14.3 and 17.9 kHz. Example: ``` @@ -1857,7 +1880,7 @@ Other types such as Bessel filters can be built by combining several Biquads. [S ### Dither The "Dither" filter should only be added at the very end of the pipeline for each channel, and adds noise shaped dither to the output. -This is intended for 16-bit output, but can be used also for higher bit depth if desired. There are several subtypes: +This is intended for 16-bit output, but can be used also for higher bit depth if desired. There are several subtypes: | Subtype | kHz | Noise shaping | Comments | | ------------------- | ---- | ------------- | -------------------------------------------------------------- | @@ -1891,19 +1914,25 @@ Highpass is an exception, which is about as fast as Flat. The parameter "bits" sets the target bit depth. For most oversampling delta-sigma DACs, this should match the bit depth of the playback device for best results. -For true non-oversampling DACs, this should match the number of bits over which the DAC is linear (or the playback bit depth, whichever is lower). +For true non-oversampling DACs, this should match the number of bits over which the DAC is linear +(or the playback bit depth, whichever is lower). Setting it to a higher value is not useful since then the applied dither will be lost. For the "Flat" subtype, the parameter "amplitude" sets the number of LSB to be dithered. To linearize the samples, this should be 2. Lower amplitudes produce less noise but also linearize less; higher numbers produce more noise but do not linearize more. -Some comparisons between the noise shapers are available from [SoX](http://sox.sourceforge.net/SoX/NoiseShaping), [SSRC](https://shibatch.sourceforge.net/ssrc/) and [ReSampler](https://github.com/jniemann66/ReSampler/blob/master/ditherProfiles.md). -To test the different types, set the target bit depth to something very small like 5 or 6 bits (the minimum allowed value is 2) and try them all. -Note that on "None" this may well mean there is no or unintelligible audio -- this is to experiment with and show what the other ditherers actually do. +Some comparisons between the noise shapers are available from [SoX](http://sox.sourceforge.net/SoX/NoiseShaping), +[SSRC](https://shibatch.sourceforge.net/ssrc/) +and [ReSampler](https://github.com/jniemann66/ReSampler/blob/master/ditherProfiles.md). +To test the different types, set the target bit depth to something very small +like 5 or 6 bits (the minimum allowed value is 2) and try them all. +Note that on "None" this may well mean there is no or unintelligible audio -- this is to experiment with +and show what the other ditherers actually do. For sample rates above 48 kHz there is no need for anything more advanced than the "Highpass" subtype. -For the low sample rates there is no spare bandwidth and the dither noise must use the audible range, with shaping to makes it less audible. +For the low sample rates there is no spare bandwidth and the dither noise must use the audible range, +with shaping to makes it less audible. But at 96 or 192 kHz there is all the bandwidth from 20 kHz up to 48 or 96 kHz where the noise can be placed without issues. The Highpass ditherer will place almost all of it there. Of course, the high-resolution Shibata filters provide some icing on the cake. @@ -1911,7 +1940,8 @@ Of course, the high-resolution Shibata filters provide some icing on the cake. Selecting a noise shaping ditherer for a different sample rate than it was designed for, will cause the frequency response curve of the noise shaper to be fitted to the playback rate. This means that the curve no longer matches its design points to be minimally audible. -You may experiment which shaper still sounds good, or use the Flat or Highpass subtypes which work well at any sample rate. +You may experiment which shaper still sounds good, +or use the Flat or Highpass subtypes which work well at any sample rate. Example: ``` @@ -1923,7 +1953,7 @@ Example: ``` ### Limiter -The "Limiter" filter is used to limit the signal to a given level. It can use hard or soft clipping. +The "Limiter" filter is used to limit the signal to a given level. It can use hard or soft clipping. Note that soft clipping introduces some harmonic distortion to the signal. Example: @@ -1964,7 +1994,7 @@ This is intended for the user and is not used by CamillaDSP itself. ### Compressor The "Compressor" processor implements a standard dynamic range compressor. -It is configured using the most common parameters. +It is configured using the most common parameters. Example: ``` @@ -1986,7 +2016,7 @@ processors: pipeline: - type: Processor name: democompressor -``` +``` Parameters: * `channels`: number of channels, must match the number of channels of the pipeline where the compressor is inserted. @@ -2014,7 +2044,7 @@ A step can be a filter, a mixer or a processor. The filters, mixers and processors must be defined in the corresponding section of the configuration, and the pipeline refers to them by their name. During processing, the steps are applied in the listed order. For each mixer and for the output device the number of channels from the previous step must match the number of input channels. -For filter steps, the channel number must exist at that point of the pipeline. +For filter steps, the channel numbers must exist at that point of the pipeline. Channels are numbered starting from zero. Apart from this, there are no rules for ordering of the steps or how many are added. @@ -2028,28 +2058,15 @@ pipeline: name: to4channels bypassed: false (*) - type: Filter - description: "Left channel woofer" - channel: 0 + description: "Left and right woofer channels" + channels: [0, 1] bypassed: false (*) names: - lowpass_fir - peak1 - type: Filter - description: "Right channel woofer" - channel: 1 - bypassed: false (*) - names: - - lowpass_fir - - peak1 - - type: Filter - description: "Left channel tweeter" - channel: 2 - bypassed: false (*) - names: - - highpass_fir - - type: Filter - description: "Right channel tweeter" - channel: 3 + description: "Left and right tweeter channels" + channels: [2, 3] bypassed: false (*) names: - highpass_fir @@ -2061,15 +2078,15 @@ pipeline: In this config first a mixer is used to copy a stereo input to four channels. Before the mixer, only channels 0 and 1 exist. After the mixer, four channels are available, with numbers 0 to 3. -The mixer is followed by a filter step for each channel. +The mixer is followed by a filter step for each pair of channels. Finally a compressor is added as the last step. ### Filter step A filter step, `type: Filter`, can contain one or several filters. The filters must be defined in the `Filters` section. -In the example above, channel 0 and 1 get filtered by `lowpass_fir` and `peak1`, while 2 and 3 get filtered by just `highpass_fir`. -If several filters are to be applied to a channel, it is recommended to put then in a single filter step. -This makes the config easier to overview and gives a minor performance benefit, compared to adding each filter in a separate step, +In the example above, channels 0 and 1 get filtered by `lowpass_fir` and `peak1`, while 2 and 3 get filtered by just `highpass_fir`. +If several filters are to be applied to a channel, it is recommended to put them in a single filter step. +This makes the config easier to overview and gives a minor performance benefit, compared to adding each filter in a separate step. ### Mixer and Processor step Mixer steps, `type: Mixer`, and processor steps, `type: Processor`, are defined in a similar way. @@ -2132,7 +2149,7 @@ Other projects using CamillaDSP: Music players: * https://moodeaudio.org/ - moOde audio player, audiophile-quality music playback for Raspberry Pi. -* https://github.com/thoelf/Linux-Stream-Player - Play local files or streamed music with room EQ on Linux. +* https://github.com/thoelf/Linux-Stream-Player - Play local files or streamed music with room EQ on Linux. * https://github.com/Lykkedk/SuperPlayer-v8.0.0---SamplerateChanger-v1.0.0 - Automatic filter switching at sample rate change for squeezelite, see also [this thread at diyAudio.com](https://www.diyaudio.com/forums/pc-based/361429-superplayer-dsp_engine-camilladsp-samplerate-switching-esp32-remote-control.html). * https://github.com/JWahle/piCoreCDSP - Installs CamillaDSP and GUI on piCorePlayer * [FusionDsp](https://docs.google.com/document/d/e/2PACX-1vRhU4i830YaaUlB6-FiDAdvl69T3Iej_9oSbNTeSpiW0DlsyuTLSv5IsVSYMmkwbFvNbdAT0Tj6Yjjh/pub) a plugin based on CamillaDsp for [Volumio](https://volumio.com), the music player, with graphic equalizer, parametric equalizer, FIR filters, Loudness, AutoEq profile for headphone and more! @@ -2142,11 +2159,11 @@ Guides and example configs: * https://github.com/hughpyle/raspot - Hugh's raspotify config * https://github.com/Wang-Yue/camilladsp-crossfeed - Bauer stereophonic-to-binaural crossfeed for headphones * https://github.com/jensgk/akg_k702_camilladsp_eq - Headphone EQ and Crossfeed for the AKG K702 headphones -* https://github.com/phelluy/room_eq_mac_m1 - Room Equalization HowTo with REW and Apple Silicon +* https://github.com/phelluy/room_eq_mac_m1 - Room Equalization HowTo with REW and Apple Silicon Projects of general nature which can be useful together with CamillaDSP: * https://github.com/scripple/alsa_hook_hwparams - Alsa hooks for reacting to sample rate and format changes. -* https://github.com/HEnquist/cpal-listdevices - List audio devices with names and supported formats under Windows and macOS. +* https://github.com/HEnquist/cpal-listdevices - List audio devices with names and supported formats under Windows and macOS. # Getting help diff --git a/exampleconfigs/brokenconfig.yml b/exampleconfigs/brokenconfig.yml index 222368c..923351f 100644 --- a/exampleconfigs/brokenconfig.yml +++ b/exampleconfigs/brokenconfig.yml @@ -48,12 +48,9 @@ pipeline: - type: Mixer name: mono - type: Filter - channel: 0 - names: - - lp1 - - type: Filter - channel: 1 + channels: [0, 1] names: - lp1 + diff --git a/exampleconfigs/ditherplay.yml b/exampleconfigs/ditherplay.yml index 12b176d..ab18c92 100644 --- a/exampleconfigs/ditherplay.yml +++ b/exampleconfigs/ditherplay.yml @@ -53,14 +53,9 @@ filters: pipeline: - type: Filter - channel: 0 + channels: [0, 1] names: - atten - ditherfancy2 - - type: Filter - channel: 1 - names: - - atten - - ditherfancy2 - + diff --git a/exampleconfigs/gainconfig.yml b/exampleconfigs/gainconfig.yml index ff4ec95..438256a 100644 --- a/exampleconfigs/gainconfig.yml +++ b/exampleconfigs/gainconfig.yml @@ -46,7 +46,7 @@ pipeline: - type: Mixer name: mono - type: Filter - channel: 0 + channels: [0, 1] names: - delay1 diff --git a/exampleconfigs/lf_compressor.yml b/exampleconfigs/lf_compressor.yml index d47170b..5ecddab 100644 --- a/exampleconfigs/lf_compressor.yml +++ b/exampleconfigs/lf_compressor.yml @@ -94,19 +94,11 @@ pipeline: - type: Mixer name: to_four - type: Filter - channel: 0 + channels: [0, 1] names: - highpass - type: Filter - channel: 1 - names: - - highpass - - type: Filter - channel: 2 - names: - - lowpass - - type: Filter - channel: 3 + channels: [2, 3] names: - lowpass - type: Processor diff --git a/exampleconfigs/nomixers.yml b/exampleconfigs/nomixers.yml index 549478f..271fda7 100644 --- a/exampleconfigs/nomixers.yml +++ b/exampleconfigs/nomixers.yml @@ -22,11 +22,7 @@ filters: pipeline: - type: Filter - channel: 0 - names: - - lowpass_fir - - type: Filter - channel: 1 + channel: [0, 1] names: - lowpass_fir diff --git a/exampleconfigs/pulseconfig.yml b/exampleconfigs/pulseconfig.yml index ac22294..cd0207e 100644 --- a/exampleconfigs/pulseconfig.yml +++ b/exampleconfigs/pulseconfig.yml @@ -58,14 +58,9 @@ mixers: pipeline: - type: Filter - channel: 0 + channel: [0, 1] names: - atten - lowpass_fir - - type: Filter - channel: 1 - names: - - atten - - lowpass_fir - + diff --git a/exampleconfigs/simpleconfig.yml b/exampleconfigs/simpleconfig.yml index 7e3b0c4..d9a2acd 100644 --- a/exampleconfigs/simpleconfig.yml +++ b/exampleconfigs/simpleconfig.yml @@ -49,12 +49,8 @@ pipeline: - type: Mixer name: monomix - type: Filter - channel: 0 + channels: [0, 1] names: - lowpass_fir - - type: Filter - channel: 1 - names: - - lowpass_fir - + diff --git a/exampleconfigs/simpleconfig_plot.yml b/exampleconfigs/simpleconfig_plot.yml index 7510399..f1ff838 100644 --- a/exampleconfigs/simpleconfig_plot.yml +++ b/exampleconfigs/simpleconfig_plot.yml @@ -62,22 +62,13 @@ pipeline: - type: Mixer name: mono - type: Filter - channel: 0 + channels: [0, 1] names: - lowpass_fir - peak1 - type: Filter - channel: 1 - names: - - lowpass_fir - - peak1 - - type: Filter - channel: 2 + channels: [2, 3] names: - highpass_fir - - type: Filter - channel: 3 - names: - - highpass_fir - + diff --git a/exampleconfigs/tokens.yml b/exampleconfigs/tokens.yml index ae47c61..567bc20 100644 --- a/exampleconfigs/tokens.yml +++ b/exampleconfigs/tokens.yml @@ -34,13 +34,9 @@ filters: pipeline: - type: Filter - channel: 0 + channels: [0, 1] names: - demofilter - filter$samplerate$ - - type: Filter - channel: 1 - names: - - demofilter - + diff --git a/stepbystep.md b/stepbystep.md index fa41b4d..ec16dad 100644 --- a/stepbystep.md +++ b/stepbystep.md @@ -5,9 +5,13 @@ This will be a simple 2-way crossover with 2 channels in and 4 out. ## Devices -First we need to define the input and output devices. Here let's assume +First we need to define the input and output devices. Here let's assume we already figured out all the Loopbacks etc and already know the devices to use. -We need to decide a sample rate, let's go with 44100. For chunksize 1024 is a good values to start at with not too much delay, and low risk of buffer underruns. The best sample format this playback device supports is 32 bit integer so let's put that. The Loopback capture device supports all sample formats so let's just pick a good one. +We need to decide a sample rate, let's go with 44100. +For chunksize, 1024 is a good starting point. +This gives a fairly short delay, and low risk of buffer underruns. +The best sample format this playback device supports is 32 bit integer so let's put that. +The Loopback capture device supports all sample formats so let's just pick a good one. ```yaml --- title: "Example crossover" @@ -29,7 +33,10 @@ devices: ``` ## Mixer -We have 2 channels coming in but we need to have 4 going out. For this to work we have to add two more channels. Thus a mixer is needed. Lets name it "to4chan" and use output channels 0 & 1 for the woofers, and 2 & 3 for tweeters. Then we want to leave channels 0 & 1 as they are, and copy 0 -> 2 and 1 -> 3. +We have 2 channels coming in but we need to have 4 going out. +For this to work we have to add two more channels. Thus a mixer is needed. +Lets name it "to4chan" and use output channels 0 & 1 for the woofers, and 2 & 3 for tweeters. +Then we want to leave channels 0 & 1 as they are, and copy 0 -> 2 and 1 -> 3. Lets start with channels 0 and 1, that should just pass through. For each output channel we define a list of sources. Here it's a list of one. So for each output channel X we add a section under "mapping": @@ -42,7 +49,8 @@ So for each output channel X we add a section under "mapping": inverted: false ``` -To copy we just need to say that output channel 0 should have channel 0 as source, with gain 0. This part becomes: +To copy we just need to say that output channel 0 should have channel 0 as source, with gain 0. +This part becomes: ```yaml mixers: to4chan: @@ -63,7 +71,7 @@ mixers: inverted: false ``` -Then we add the two new channels, by copying from channels 0 and 1: +Then we add the two new channels, by copying from channels 0 and 1: ```yaml mixers: to4chan: @@ -95,7 +103,8 @@ mixers: ``` ## Pipeline -We now have all we need to build a working pipeline. It won't do any filtering yet so this is only for a quick test. +We now have all we need to build a working pipeline. +It won't do any filtering yet so this is only for a quick test. We only need a single step in the pipeline, for the "to4chan" mixer. ```yaml pipeline: @@ -106,8 +115,11 @@ Put everything together, and run it. It should work and give unfiltered output o ## Filters -The poor tweeters don't like the full range signal so we need lowpass filters for them. Left and right should be filtered with the same settings, so a single definition is enough. -Let's use a simple 2nd order Butterworth at 2 kHz and name it "highpass2k". Create a "filters" section like this: +The poor tweeters don't like the full range signal so we need lowpass filters for them. +Left and right should be filtered with the same settings, so a single definition is enough. +Let's use a simple 2nd order Butterworth at 2 kHz and name it "highpass2k". + +Create a "filters" section like this: ```yaml filters: highpass2k: @@ -117,23 +129,23 @@ filters: freq: 2000 q: 0.707 ``` -Next we need to plug this into the pipeline after the mixer. Thus we need to extend the pipeline with two "Filter" steps, one for each tweeter channel. +Next we need to plug this into the pipeline after the mixer. +Thus we need to extend the pipeline with a "Filter" step, +that acts on the two tweeter channels. ```yaml pipeline: - type: Mixer name: to4chan - type: Filter <---- here! - channel: 2 - names: - - highpass2k - - type: Filter <---- here! - channel: 3 + channels: [2, 3] names: - highpass2k ``` -When we try this we get properly filtered output for the tweeters on channels 2 and 3. Let's fix the woofers as well. Then we need a lowpass filter, so we add a definition to the filters section. +When we try this we get properly filtered output for the tweeters on channels 2 and 3. +Let's fix the woofers as well. +Then we need a lowpass filter, so we add a definition to the filters section. ```yaml filters: highpass2k: @@ -149,30 +161,26 @@ filters: freq: 2000 q: 0.707 ``` -Then we plug it into the pipeline with two new Filter blocks: + +Then we plug the woofer filter into the pipeline with a new Filter block: ```yaml pipeline: - type: Mixer name: to4chan - type: Filter - channel: 2 + channels: [2, 3] names: - highpass2k - - type: Filter - channel: 3 - names: - - highpass2k - - type: Filter <---- new! - channel: 0 - names: - - lowpass2k - type: Filter <---- new! - channel: 1 + channels: [0, 1] names: - lowpass2k ``` -We try this and it works, but the sound isn't very nice. First off, the tweeters have higher sensitivity than the woofers, so they need to be attenuated. This can be done in the mixer, or via a separate "Gain" filter. Let's do it in the mixer, and attenuate by 5 dB. +We try this and it works, but the sound isn't very nice. +First off, the tweeters have higher sensitivity than the woofers, so they need to be attenuated. +This can be done in the mixer, or via a separate "Gain" filter. +Let's do it in the mixer, and attenuate by 5 dB. Just modify the "gain" parameters in the mixer config: ```yaml @@ -204,7 +212,9 @@ mixers: gain: -5.0 <---- here! inverted: false ``` -This is far better but we need baffle step compensation as well. We can do this with a "Highshelf" filter. The measurements say we need to attenuate by 4 dB from 500 Hz and up. +This is far better but we need baffle step compensation as well. +We can do this with a "Highshelf" filter. +The measurements say we need to attenuate by 4 dB from 500 Hz and up. Add this filter definition: ```yaml @@ -216,37 +226,27 @@ Add this filter definition: slope: 6.0 gain: -4.0 ``` -The baffle step correction should be applied to both woofers and tweeters, so let's add this in two new Filter steps (one per channel) before the Mixer: +The baffle step correction should be applied to both woofers and tweeters, +so let's add this in a new Filter step before the Mixer: ```yaml pipeline: - - type: Filter \ - channel: 0 | - names: | - - bafflestep | <---- new - - type: Filter | - channel: 1 | + - type: Filter \ + channels: [0, 1] | <---- new names: | - - bafflestep / + - bafflestep / - type: Mixer name: to4chan - type: Filter - channel: 2 - names: - - highpass2k - - type: Filter - channel: 3 + channels: [2, 3] names: - highpass2k - type: Filter - channel: 0 - names: - - lowpass2k - - type: Filter - channel: 1 + channels: [0, 1] names: - lowpass2k ``` -The last thing we need to do is to adjust the delay between tweeter and woofer. Measurements tell us we need to delay the tweeter by 0.5 ms. +The last thing we need to do is to adjust the delay between tweeter and woofer. +Measurements tell us we need to delay the tweeter by 0.5 ms. Add this filter definition: ```yaml @@ -261,31 +261,18 @@ Now we add this to the tweeter channels: ```yaml pipeline: - type: Filter - channel: 0 - names: - - bafflestep - - type: Filter - channel: 1 + channels: [0, 1] names: - bafflestep - type: Mixer name: to4chan - type: Filter - channel: 2 - names: - - highpass2k - - tweeterdelay <---- here! - - type: Filter - channel: 3 + channels: [2, 3] names: - highpass2k - tweeterdelay <---- here! - type: Filter - channel: 0 - names: - - lowpass2k - - type: Filter - channel: 1 + channels: [0, 1] names: - lowpass2k ``` @@ -294,7 +281,8 @@ And we are done! ## Result Now we have all the parts of the configuration. -As a final touch, let's add descriptions to all pipeline steps while we have things fresh in memory. +As a final touch, let's add descriptions to all pipeline steps +while we have things fresh in memory. ```yaml --- @@ -314,7 +302,7 @@ devices: channels: 4 device: "hw:Generic_1" format: S32LE - + mixers: to4chan: description: "Expand 2 channels to 4" @@ -375,37 +363,21 @@ filters: pipeline: - type: Filter - description: "Pre-mixer filters left" - channel: 0 - names: - - bafflestep - - type: Filter - description: "Pre-mixer filters right" - channel: 1 + description: "Pre-mixer filters" + channela: [0, 1] names: - bafflestep - type: Mixer name: to4chan - type: Filter - description: "Highpass for left tweeter" - channel: 2 + description: "Highpass for tweeters" + channels: [2, 3] names: - highpass2k - tweeterdelay - type: Filter - description: "Highpass for right tweeter" - channel: 3 - names: - - highpass2k - - tweeterdelay - - type: Filter - description: "Lowpass for left woofer" - channel: 0 - names: - - lowpass2k - - type: Filter - description: "Lowpass for right woofer" - channel: 1 + description: "Lowpass for woofers" + channels: [0, 1] names: - lowpass2k ``` diff --git a/testscripts/config_load_test/conf1.yml b/testscripts/config_load_test/conf1.yml index bb4f4e6..ad5c583 100644 --- a/testscripts/config_load_test/conf1.yml +++ b/testscripts/config_load_test/conf1.yml @@ -25,8 +25,8 @@ pipeline: - type: Filter names: - testfilter - channel: 0 + channels: [0] - type: Filter names: - testfilter - channel: 1 \ No newline at end of file + channels: [1] \ No newline at end of file diff --git a/testscripts/config_load_test/conf2.yml b/testscripts/config_load_test/conf2.yml index 0cdc73c..3b69c5c 100644 --- a/testscripts/config_load_test/conf2.yml +++ b/testscripts/config_load_test/conf2.yml @@ -25,8 +25,8 @@ pipeline: - type: Filter names: - testfilter - channel: 0 + channels: [0] - type: Filter names: - testfilter - channel: 1 \ No newline at end of file + channels: [1] \ No newline at end of file diff --git a/testscripts/config_load_test/conf3.yml b/testscripts/config_load_test/conf3.yml index 4aa45c7..81da79c 100644 --- a/testscripts/config_load_test/conf3.yml +++ b/testscripts/config_load_test/conf3.yml @@ -25,8 +25,8 @@ pipeline: - type: Filter names: - testfilter - channel: 0 + channels: [0] - type: Filter names: - testfilter - channel: 1 \ No newline at end of file + channels: [1] \ No newline at end of file diff --git a/testscripts/config_load_test/conf4.yml b/testscripts/config_load_test/conf4.yml index 00f9dc2..9824aa4 100644 --- a/testscripts/config_load_test/conf4.yml +++ b/testscripts/config_load_test/conf4.yml @@ -25,8 +25,8 @@ pipeline: - type: Filter names: - testfilter - channel: 0 + channels: [0] - type: Filter names: - testfilter - channel: 1 \ No newline at end of file + channels: [1] \ No newline at end of file diff --git a/testscripts/test_file.yml b/testscripts/test_file.yml index 81898e6..e5e9d9e 100644 --- a/testscripts/test_file.yml +++ b/testscripts/test_file.yml @@ -16,7 +16,6 @@ devices: format: S32LE extra_samples: 1 - mixers: splitter: channels: @@ -34,7 +33,6 @@ mixers: gain: 0 inverted: false - filters: testlp: type: BiquadCombo @@ -49,18 +47,14 @@ filters: freq: 1000 order: 4 - - pipeline: - type: Mixer name: splitter - type: Filter - channel: 0 + channels: [0] names: - testlp - type: Filter - channel: 1 + channels: [1] names: - testhp - - diff --git a/testscripts/test_file_sine.yml b/testscripts/test_file_sine.yml index 7e759a9..e5f8cf5 100644 --- a/testscripts/test_file_sine.yml +++ b/testscripts/test_file_sine.yml @@ -29,13 +29,6 @@ filters: pipeline: - type: Filter - channel: 0 + channels: [0, 1] names: - dummy - - type: Filter - channel: 1 - names: - - dummy - - - From a72e664571583c7396bb29cc109c0c269a940908 Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 13 Feb 2024 17:10:43 +0100 Subject: [PATCH 003/135] Make channels optional, update readme --- README.md | 16 +++++++++++++--- src/config.rs | 1 + 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1857c51..1e6e1a0 100644 --- a/README.md +++ b/README.md @@ -2059,7 +2059,7 @@ pipeline: bypassed: false (*) - type: Filter description: "Left and right woofer channels" - channels: [0, 1] + channels: [0, 1] (*) bypassed: false (*) names: - lowpass_fir @@ -2083,10 +2083,20 @@ Finally a compressor is added as the last step. ### Filter step A filter step, `type: Filter`, can contain one or several filters. +The chosen filters are given in the `names` property, which is an list of filter names. The filters must be defined in the `Filters` section. -In the example above, channels 0 and 1 get filtered by `lowpass_fir` and `peak1`, while 2 and 3 get filtered by just `highpass_fir`. + +The chosen filters will be applied to the channels listed in the `channels` property. +This property is optional. If it is left out or set to `null`, +the filters are applied to all the channels at that point in the pipeline. +An empty list means the filters will not be applied to any channel. + +In the example above, channels 0 and 1 get filtered by `lowpass_fir` and `peak1`, +while 2 and 3 get filtered by just `highpass_fir`. + If several filters are to be applied to a channel, it is recommended to put them in a single filter step. -This makes the config easier to overview and gives a minor performance benefit, compared to adding each filter in a separate step. +This makes the config easier to overview and gives a minor performance benefit, +compared to adding each filter in a separate step. ### Mixer and Processor step Mixer steps, `type: Mixer`, and processor steps, `type: Processor`, are defined in a similar way. diff --git a/src/config.rs b/src/config.rs index 9342498..1ddd7af 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1218,6 +1218,7 @@ impl PipelineStepMixer { #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct PipelineStepFilter { + #[serde(default)] pub channels: Option>, pub names: Vec, #[serde(default)] From 7ea2c0496538669a917232977a3f3dae60dd9f63 Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 13 Feb 2024 21:15:01 +0100 Subject: [PATCH 004/135] Update last few example configs --- exampleconfigs/nomixers.yml | 2 +- exampleconfigs/pulseconfig.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/exampleconfigs/nomixers.yml b/exampleconfigs/nomixers.yml index 271fda7..4a51b63 100644 --- a/exampleconfigs/nomixers.yml +++ b/exampleconfigs/nomixers.yml @@ -22,7 +22,7 @@ filters: pipeline: - type: Filter - channel: [0, 1] + channels: [0, 1] names: - lowpass_fir diff --git a/exampleconfigs/pulseconfig.yml b/exampleconfigs/pulseconfig.yml index cd0207e..10fae71 100644 --- a/exampleconfigs/pulseconfig.yml +++ b/exampleconfigs/pulseconfig.yml @@ -58,7 +58,7 @@ mixers: pipeline: - type: Filter - channel: [0, 1] + channels: [0, 1] names: - atten - lowpass_fir From 30fe9b04f1e3f019fac70c0baae5f3d03eb51031 Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 13 Feb 2024 21:15:52 +0100 Subject: [PATCH 005/135] Add logging, simplify filter step creation --- src/filters.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/filters.rs b/src/filters.rs index 8c332ba..ea7c5e0 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -495,18 +495,32 @@ impl Pipeline { if !step.is_bypassed() { let mixconf = conf.mixers.as_ref().unwrap()[&step.name].clone(); num_channels = mixconf.channels.out; + debug!( + "Add Mixer step with mixer {}, pipeline becomes {} channels wide", + step.name, mixconf.channels.out + ); let mixer = mixer::Mixer::from_config(step.name, mixconf); steps.push(PipelineStep::MixerStep(mixer)); } } config::PipelineStep::Filter(step) => { if !step.is_bypassed() { - let step_channels = if let Some(channels) = &step.channels { - channels.clone() + let channels_iter: Box> = if let Some(channels) = + &step.channels + { + debug!( + "Add Filter step with filters {:?} to channels {:?}", + step.names, channels + ); + Box::new(channels.iter().copied()) as Box> } else { - (0..num_channels).collect() + debug!( + "Add Filter step with filters {:?} to all {} channels", + step.names, num_channels + ); + Box::new(0..num_channels) as Box> }; - for channel in step_channels { + for channel in channels_iter { let fltgrp = FilterGroup::from_config( channel, &step.names, @@ -521,6 +535,7 @@ impl Pipeline { } config::PipelineStep::Processor(step) => { if !step.is_bypassed() { + debug!("Add Processor step with processor {}", step.name); let procconf = conf.processors.as_ref().unwrap()[&step.name].clone(); let proc = match procconf { config::Processor::Compressor { parameters, .. } => { From 6fdd4dcf7ecb02ff730aa6fb4a9c8b41daf1f276 Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 13 Feb 2024 23:01:44 +0100 Subject: [PATCH 006/135] Option to Write wav header for File and Stdout --- CHANGELOG.md | 4 +++ README.md | 8 +++++- src/audiodevice.rs | 8 +++++- src/config.rs | 4 +++ src/filedevice.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6673409..0cec618 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.0.0 +New features: +- Optionally write wav header when outputting to file or stdout. + ## v2.0.2 Bugfixes: - MacOS: Fix a segfault when reading clock source names for some capture devices. diff --git a/README.md b/README.md index 16073c5..79d3b58 100644 --- a/README.md +++ b/README.md @@ -1059,7 +1059,13 @@ A parameter marked (*) in any example is optional. If they are left out from the ``` Note: On Unix-like systems it's also possible to use the File device and set the filename to `/dev/stdin` for capture, or `/dev/stdout` for playback. - Please note the `File` capture device isn't able to read wav-files directly. + The `File` and `Stdout` playback devices can write a wav-header to the output. + Enable this by setting `wav_header` to `true`. + Setting it to `false`, `null`, or leaving it out disables the wav header. + This is a _streaming_ header, meaning it contains a dummy value for the file length. + Most applications ignore this and calculate the correct length from the file size. + + Please note that the `File` capture device isn't able to read wav-files directly. If you want to let CamillaDSP play wav-files, please see the [separate guide for converting wav to raw files](coefficients_from_wav.md). Example config for File: diff --git a/src/audiodevice.rs b/src/audiodevice.rs index e94bd70..52e15f8 100644 --- a/src/audiodevice.rs +++ b/src/audiodevice.rs @@ -269,6 +269,7 @@ pub fn new_playback_device(conf: config::Devices) -> Box { channels, filename, format, + wav_header, .. } => Box::new(filedevice::FilePlaybackDevice { destination: filedevice::PlaybackDest::Filename(filename), @@ -276,15 +277,20 @@ pub fn new_playback_device(conf: config::Devices) -> Box { chunksize: conf.chunksize, channels, sample_format: format, + wav_header: wav_header.unwrap_or(false), }), config::PlaybackDevice::Stdout { - channels, format, .. + channels, + format, + wav_header, + .. } => Box::new(filedevice::FilePlaybackDevice { destination: filedevice::PlaybackDest::Stdout, samplerate: conf.samplerate, chunksize: conf.chunksize, channels, sample_format: format, + wav_header: wav_header.unwrap_or(false), }), #[cfg(target_os = "macos")] config::PlaybackDevice::CoreAudio(ref dev) => { diff --git a/src/config.rs b/src/config.rs index 9e117f6..a65179e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -336,12 +336,16 @@ pub enum PlaybackDevice { channels: usize, filename: String, format: SampleFormat, + #[serde(default)] + wav_header: Option, }, #[serde(alias = "STDOUT", alias = "stdout")] Stdout { #[serde(deserialize_with = "validate_nonzero_usize")] channels: usize, format: SampleFormat, + #[serde(default)] + wav_header: Option, }, #[cfg(target_os = "macos")] #[serde(alias = "COREAUDIO", alias = "coreaudio")] diff --git a/src/filedevice.rs b/src/filedevice.rs index 770e527..cf1bf60 100644 --- a/src/filedevice.rs +++ b/src/filedevice.rs @@ -38,6 +38,7 @@ pub struct FilePlaybackDevice { pub samplerate: usize, pub channels: usize, pub sample_format: SampleFormat, + pub wav_header: bool, } #[derive(Clone)] @@ -105,6 +106,54 @@ pub trait Reader { fn read(&mut self, data: &mut [u8]) -> Result>; } +// Write a wav header. We don't know the final length so we set the file size and data length to u32::MAX. +fn write_wav_header( + dest: &mut Box, + channels: usize, + sample_format: SampleFormat, + samplerate: usize, +) -> std::io::Result<()> { + // Header + dest.write_all("RIFF".as_bytes())?; + // file size, 4 bytes, unknown so set to max + dest.write_all(&u32::MAX.to_le_bytes())?; + dest.write_all("WAVE".as_bytes())?; + + let (formatcode, bits_per_sample, bytes_per_sample) = match sample_format { + SampleFormat::S16LE => (1, 16, 2), + SampleFormat::S24LE3 => (1, 24, 3), + SampleFormat::S24LE => (1, 24, 4), + SampleFormat::S32LE => (1, 32, 4), + SampleFormat::FLOAT32LE => (3, 32, 4), + SampleFormat::FLOAT64LE => (3, 64, 8), + }; + + // format block + dest.write_all("fmt ".as_bytes())?; + // size of fmt block, 4 bytes + dest.write_all(&16_u32.to_le_bytes())?; + // format code, 2 bytes + dest.write_all(&(formatcode as u16).to_le_bytes())?; + // number of channels, 2 bytes + dest.write_all(&(channels as u16).to_le_bytes())?; + // samplerate, 4 bytes + dest.write_all(&(samplerate as u32).to_le_bytes())?; + // bytes per second sec, 4 bytes + dest.write_all(&((channels * samplerate * bytes_per_sample) as u32).to_le_bytes())?; + // block alignment, 2 bytes + dest.write_all(&((channels * bytes_per_sample) as u16).to_le_bytes())?; + // bits per sample, 2 bytes + dest.write_all(&(bits_per_sample as u16).to_le_bytes())?; + + // data block + dest.write_all("data".as_bytes())?; + // data length, 4 bytes, unknown so set to max + dest.write_all(&u32::MAX.to_le_bytes())?; + + // audio data starts from here + Ok(()) +} + /// Start a playback thread listening for AudioMessages via a channel. impl PlaybackDevice for FilePlaybackDevice { fn start( @@ -119,6 +168,8 @@ impl PlaybackDevice for FilePlaybackDevice { let channels = self.channels; let store_bytes_per_sample = self.sample_format.bytes_per_sample(); let sample_format = self.sample_format; + let samplerate = self.samplerate; + let wav_header = self.wav_header; let handle = thread::Builder::new() .name("FilePlayback".to_string()) .spawn(move || { @@ -140,6 +191,16 @@ impl PlaybackDevice for FilePlaybackDevice { }; barrier.wait(); debug!("starting playback loop"); + if wav_header { + match write_wav_header(&mut file, channels, sample_format, samplerate) { + Ok(()) => debug!("Wrote Wav header"), + Err(err) => { + status_channel + .send(StatusMessage::PlaybackError(err.to_string())) + .unwrap_or(()); + } + } + } let mut buffer = vec![0u8; chunksize * channels * store_bytes_per_sample]; loop { match channel.recv() { From 609311a6b86d3713bd95baa56d960e6066ace1e4 Mon Sep 17 00:00:00 2001 From: Henrik Date: Sat, 17 Feb 2024 22:37:10 +0100 Subject: [PATCH 007/135] Add a basic signal generator --- README.md | 26 ++++++++++++++++++++++++++ src/audiodevice.rs | 12 ++++++++++++ src/config.rs | 20 +++++++++++++++++++- src/lib.rs | 7 ++++++- 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 16073c5..78ced73 100644 --- a/README.md +++ b/README.md @@ -997,6 +997,7 @@ A parameter marked (*) in any example is optional. If they are left out from the * `type`: The available types depend on which features that were included when compiling. All possible types are: * `File` + * `SignalGenerator` (capture only) * `Stdin` (capture only) * `Stdout` (playback only) * `Bluez` (capture only) @@ -1107,6 +1108,31 @@ A parameter marked (*) in any example is optional. If they are left out from the read_bytes: 200 ``` + The `SignalGenerator` capture device is intended for testing. + It requires the parameters `signal` for signal shape, the number of `channels`, and the signal `level` in dB. + It can generate sine wave, square wave and white noise. + When using the `SignalGenerator`, the resampler config and capture samplerate are ignored. + The same signal is generated on every channel. + + Example config for sine wave at 440 Hz and -20 dB: + ``` + capture: + type: SignalGenerator + channels: 2 + signal: + Sine: 440 + level: -20.0 + ``` + + Example config for white noise ad -10 dB: + ``` + capture: + type: SignalGenerator + channels: 2 + signal: WhiteNoise + level: -10.0 + ``` + ### Wasapi See the [separate readme for Wasapi](./backend_wasapi.md#configuration-of-devices). diff --git a/src/audiodevice.rs b/src/audiodevice.rs index e94bd70..dcee189 100644 --- a/src/audiodevice.rs +++ b/src/audiodevice.rs @@ -16,6 +16,7 @@ use crate::coreaudiodevice; ))] use crate::cpaldevice; use crate::filedevice; +use crate::generatordevice; #[cfg(feature = "pulse-backend")] use crate::pulsedevice; #[cfg(target_os = "windows")] @@ -603,6 +604,17 @@ pub fn new_capture_device(conf: config::Devices) -> Box { stop_on_rate_change: conf.stop_on_rate_change(), rate_measure_interval: conf.rate_measure_interval(), }), + config::CaptureDevice::SignalGenerator { + signal, + channels, + level, + } => Box::new(generatordevice::GeneratorDevice { + signal, + samplerate: conf.samplerate, + channels, + chunksize: conf.chunksize, + level, + }), #[cfg(all(target_os = "linux", feature = "bluez-backend"))] config::CaptureDevice::Bluez(ref dev) => Box::new(filedevice::FileCaptureDevice { source: filedevice::CaptureSource::BluezDBus(dev.service(), dev.dbus_path.clone()), diff --git a/src/config.rs b/src/config.rs index 9e117f6..13e8cab 100644 --- a/src/config.rs +++ b/src/config.rs @@ -119,7 +119,14 @@ impl fmt::Display for SampleFormat { } } -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] +pub enum Signal { + Sine(f64), + Square(f64), + WhiteNoise, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] #[serde(tag = "type")] pub enum CaptureDevice { @@ -168,6 +175,12 @@ pub enum CaptureDevice { channels: usize, device: String, }, + SignalGenerator { + #[serde(deserialize_with = "validate_nonzero_usize")] + channels: usize, + signal: Signal, + level: PrcFmt, + }, } impl CaptureDevice { @@ -196,6 +209,7 @@ impl CaptureDevice { ) ))] CaptureDevice::Jack { channels, .. } => *channels, + CaptureDevice::SignalGenerator { channels, .. } => *channels, } } } @@ -1415,6 +1429,9 @@ fn apply_overrides(configuration: &mut Configuration) { CaptureDevice::Jack { channels, .. } => { *channels = chans; } + CaptureDevice::SignalGenerator { channels, .. } => { + *channels = chans; + } } } if let Some(fmt) = overrides.sample_format { @@ -1459,6 +1476,7 @@ fn apply_overrides(configuration: &mut Configuration) { CaptureDevice::Jack { .. } => { error!("Not possible to override capture format for Jack, ignoring"); } + CaptureDevice::SignalGenerator { .. } => {} } } } diff --git a/src/lib.rs b/src/lib.rs index f297cb9..2f35040 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,6 +102,7 @@ pub mod filereader; #[cfg(target_os = "linux")] pub mod filereader_nonblock; pub mod filters; +pub mod generatordevice; pub mod helpers; pub mod limiter; pub mod loudness; @@ -347,7 +348,11 @@ impl fmt::Display for ProcessingState { pub fn list_supported_devices() -> (Vec, Vec) { let mut playbacktypes = vec!["File".to_owned(), "Stdout".to_owned()]; - let mut capturetypes = vec!["File".to_owned(), "Stdin".to_owned()]; + let mut capturetypes = vec![ + "File".to_owned(), + "Stdin".to_owned(), + "SignalGenerator".to_owned(), + ]; if cfg!(target_os = "linux") { playbacktypes.push("Alsa".to_owned()); From 07091daf5a9651e6526af39b1d3ef3a475954944 Mon Sep 17 00:00:00 2001 From: Henrik Date: Sat, 17 Feb 2024 22:41:05 +0100 Subject: [PATCH 008/135] Add the new module --- src/generatordevice.rs | 238 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 src/generatordevice.rs diff --git a/src/generatordevice.rs b/src/generatordevice.rs new file mode 100644 index 0000000..b09fe4d --- /dev/null +++ b/src/generatordevice.rs @@ -0,0 +1,238 @@ +use crate::audiodevice::*; +use crate::config; + +use std::f64::consts::PI; +use std::sync::mpsc; +use std::sync::{Arc, Barrier}; +use std::thread; + +use parking_lot::RwLock; + +use rand::{rngs::SmallRng, SeedableRng}; +use rand_distr::{Distribution, Uniform}; + +use crate::CaptureStatus; +use crate::CommandMessage; +use crate::PrcFmt; +use crate::ProcessingState; +use crate::Res; +use crate::StatusMessage; + +struct SineGenerator { + time: f64, + freq: f64, + delta_t: f64, + amplitude: PrcFmt, +} + +impl SineGenerator { + fn new(freq: f64, fs: usize, amplitude: PrcFmt) -> Self { + SineGenerator { + time: 0.0, + freq, + delta_t: 1.0 / fs as f64, + amplitude, + } + } +} + +impl Iterator for SineGenerator { + type Item = PrcFmt; + fn next(&mut self) -> Option { + let output = (self.freq * self.time * PI * 2.).sin() as PrcFmt * self.amplitude; + self.time += self.delta_t; + Some(output) + } +} + +struct SquareGenerator { + time: f64, + freq: f64, + delta_t: f64, + amplitude: PrcFmt, +} + +impl SquareGenerator { + fn new(freq: f64, fs: usize, amplitude: PrcFmt) -> Self { + SquareGenerator { + time: 0.0, + freq, + delta_t: 1.0 / fs as f64, + amplitude, + } + } +} + +impl Iterator for SquareGenerator { + type Item = PrcFmt; + fn next(&mut self) -> Option { + let output = (self.freq * self.time * PI * 2.).sin().signum() as PrcFmt * self.amplitude; + self.time += self.delta_t; + Some(output) + } +} + +struct NoiseGenerator { + rng: SmallRng, + distribution: Uniform, +} + +impl NoiseGenerator { + fn new(amplitude: PrcFmt) -> Self { + let rng = SmallRng::from_entropy(); + let distribution = Uniform::new_inclusive(-amplitude, amplitude); + NoiseGenerator { rng, distribution } + } +} + +impl Iterator for NoiseGenerator { + type Item = PrcFmt; + fn next(&mut self) -> Option { + Some(self.distribution.sample(&mut self.rng)) + } +} + +pub struct GeneratorDevice { + pub chunksize: usize, + pub samplerate: usize, + pub channels: usize, + pub signal: config::Signal, + pub level: PrcFmt, +} + +struct CaptureChannels { + audio: mpsc::SyncSender, + status: crossbeam_channel::Sender, + command: mpsc::Receiver, +} + +struct GeneratorParams { + channels: usize, + chunksize: usize, + capture_status: Arc>, + signal: config::Signal, + samplerate: usize, + amplitude: PrcFmt, +} + +fn capture_loop(params: GeneratorParams, msg_channels: CaptureChannels) { + debug!("starting generator loop"); + let mut chunk_stats = ChunkStats { + rms: vec![0.0; params.channels], + peak: vec![0.0; params.channels], + }; + let mut sine_gen; + let mut square_gen; + let mut noise_gen; + + let mut generator: &mut dyn Iterator = match params.signal { + config::Signal::Sine(freq) => { + sine_gen = SineGenerator::new(freq, params.samplerate, params.amplitude); + &mut sine_gen as &mut dyn Iterator + } + config::Signal::Square(freq) => { + square_gen = SquareGenerator::new(freq, params.samplerate, params.amplitude); + &mut square_gen as &mut dyn Iterator + } + config::Signal::WhiteNoise => { + noise_gen = NoiseGenerator::new(params.amplitude); + &mut noise_gen as &mut dyn Iterator + } + }; + + loop { + match msg_channels.command.try_recv() { + Ok(CommandMessage::Exit) => { + debug!("Exit message received, sending EndOfStream"); + let msg = AudioMessage::EndOfStream; + msg_channels.audio.send(msg).unwrap_or(()); + msg_channels + .status + .send(StatusMessage::CaptureDone) + .unwrap_or(()); + break; + } + Ok(CommandMessage::SetSpeed { .. }) => { + warn!("Signal generator does not support rate adjust. Ignoring request."); + } + Err(mpsc::TryRecvError::Empty) => {} + Err(mpsc::TryRecvError::Disconnected) => { + error!("Command channel was closed"); + break; + } + }; + let mut waveform = vec![0.0; params.chunksize]; + for (sample, value) in waveform.iter_mut().zip(&mut generator) { + *sample = value; + } + let mut waveforms = Vec::with_capacity(params.channels); + waveforms.push(waveform); + for _ in 1..params.channels { + waveforms.push(waveforms[0].clone()); + } + + let chunk = AudioChunk::new(waveforms, 1.0, -1.0, params.chunksize, params.chunksize); + + chunk.update_stats(&mut chunk_stats); + { + let mut capture_status = params.capture_status.write(); + capture_status + .signal_rms + .add_record_squared(chunk_stats.rms_linear()); + capture_status + .signal_peak + .add_record(chunk_stats.peak_linear()); + } + let msg = AudioMessage::Audio(chunk); + if msg_channels.audio.send(msg).is_err() { + info!("Processing thread has already stopped."); + break; + } + } + params.capture_status.write().state = ProcessingState::Inactive; +} + +/// Start a capture thread providing AudioMessages via a channel +impl CaptureDevice for GeneratorDevice { + fn start( + &mut self, + channel: mpsc::SyncSender, + barrier: Arc, + status_channel: crossbeam_channel::Sender, + command_channel: mpsc::Receiver, + capture_status: Arc>, + ) -> Res>> { + let samplerate = self.samplerate; + let chunksize = self.chunksize; + let channels = self.channels; + let signal = self.signal; + let amplitude = (10.0 as PrcFmt).powf(self.level / 20.0); + + let handle = thread::Builder::new() + .name("SignalGenerator".to_string()) + .spawn(move || { + let params = GeneratorParams { + signal, + samplerate, + channels, + chunksize, + capture_status, + amplitude, + }; + match status_channel.send(StatusMessage::CaptureReady) { + Ok(()) => {} + Err(_err) => {} + } + barrier.wait(); + let msg_channels = CaptureChannels { + audio: channel, + status: status_channel, + command: command_channel, + }; + debug!("starting captureloop"); + capture_loop(params, msg_channels); + }) + .unwrap(); + Ok(Box::new(handle)) + } +} From f15460cb050c0a50fef07ee7aedf1e3511b55809 Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 20 Feb 2024 20:44:03 +0100 Subject: [PATCH 009/135] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 534268d..1e2b324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ ## v3.0.0 +New features: +- Add a signal generator capture device. Changes: - Filter pipeline steps take a list of channels to filter instead of a single one. From ea38d89f32ff53f59a82031420f034aa624cf5e8 Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 20 Feb 2024 21:26:58 +0100 Subject: [PATCH 010/135] Simplify signal generator config --- README.md | 19 +++++++++++++------ src/audiodevice.rs | 19 ++++++++----------- src/config.rs | 9 +++++---- src/generatordevice.rs | 20 ++++++++++---------- 4 files changed, 36 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index c8f7028..face709 100644 --- a/README.md +++ b/README.md @@ -1121,8 +1121,13 @@ A parameter marked (*) in any example is optional. If they are left out from the ``` The `SignalGenerator` capture device is intended for testing. - It requires the parameters `signal` for signal shape, the number of `channels`, and the signal `level` in dB. - It can generate sine wave, square wave and white noise. + It accepts the number of channels as `channels`. + It also requires a block defining the signal properties, called `signal`. + + The signal shape is give by `type`, which accepts `Sine`, `Square` and `WhiteNoise`. + All types require the signal level, which is given in dB in the `level` parameter. + `Sine` and `Square` also require a frequency, defined by the `freq` parameter. + When using the `SignalGenerator`, the resampler config and capture samplerate are ignored. The same signal is generated on every channel. @@ -1132,8 +1137,9 @@ A parameter marked (*) in any example is optional. If they are left out from the type: SignalGenerator channels: 2 signal: - Sine: 440 - level: -20.0 + type: Sine + freq: 440 + level: -20.0 ``` Example config for white noise ad -10 dB: @@ -1141,8 +1147,9 @@ A parameter marked (*) in any example is optional. If they are left out from the capture: type: SignalGenerator channels: 2 - signal: WhiteNoise - level: -10.0 + signal: + type: WhiteNoise + level: -10.0 ``` ### Wasapi diff --git a/src/audiodevice.rs b/src/audiodevice.rs index c7a0023..5c63be8 100644 --- a/src/audiodevice.rs +++ b/src/audiodevice.rs @@ -610,17 +610,14 @@ pub fn new_capture_device(conf: config::Devices) -> Box { stop_on_rate_change: conf.stop_on_rate_change(), rate_measure_interval: conf.rate_measure_interval(), }), - config::CaptureDevice::SignalGenerator { - signal, - channels, - level, - } => Box::new(generatordevice::GeneratorDevice { - signal, - samplerate: conf.samplerate, - channels, - chunksize: conf.chunksize, - level, - }), + config::CaptureDevice::SignalGenerator { signal, channels } => { + Box::new(generatordevice::GeneratorDevice { + signal, + samplerate: conf.samplerate, + channels, + chunksize: conf.chunksize, + }) + } #[cfg(all(target_os = "linux", feature = "bluez-backend"))] config::CaptureDevice::Bluez(ref dev) => Box::new(filedevice::FileCaptureDevice { source: filedevice::CaptureSource::BluezDBus(dev.service(), dev.dbus_path.clone()), diff --git a/src/config.rs b/src/config.rs index b7de08f..eb1ba12 100644 --- a/src/config.rs +++ b/src/config.rs @@ -120,10 +120,12 @@ impl fmt::Display for SampleFormat { } #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +#[serde(tag = "type")] pub enum Signal { - Sine(f64), - Square(f64), - WhiteNoise, + Sine { freq: f64, level: PrcFmt }, + Square { freq: f64, level: PrcFmt }, + WhiteNoise { level: PrcFmt }, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] @@ -179,7 +181,6 @@ pub enum CaptureDevice { #[serde(deserialize_with = "validate_nonzero_usize")] channels: usize, signal: Signal, - level: PrcFmt, }, } diff --git a/src/generatordevice.rs b/src/generatordevice.rs index b09fe4d..6a58d28 100644 --- a/src/generatordevice.rs +++ b/src/generatordevice.rs @@ -97,7 +97,6 @@ pub struct GeneratorDevice { pub samplerate: usize, pub channels: usize, pub signal: config::Signal, - pub level: PrcFmt, } struct CaptureChannels { @@ -112,7 +111,10 @@ struct GeneratorParams { capture_status: Arc>, signal: config::Signal, samplerate: usize, - amplitude: PrcFmt, +} + +fn decibel_to_amplitude(level: PrcFmt) -> PrcFmt { + (10.0 as PrcFmt).powf(level / 20.0) } fn capture_loop(params: GeneratorParams, msg_channels: CaptureChannels) { @@ -126,16 +128,16 @@ fn capture_loop(params: GeneratorParams, msg_channels: CaptureChannels) { let mut noise_gen; let mut generator: &mut dyn Iterator = match params.signal { - config::Signal::Sine(freq) => { - sine_gen = SineGenerator::new(freq, params.samplerate, params.amplitude); + config::Signal::Sine { freq, level } => { + sine_gen = SineGenerator::new(freq, params.samplerate, decibel_to_amplitude(level)); &mut sine_gen as &mut dyn Iterator } - config::Signal::Square(freq) => { - square_gen = SquareGenerator::new(freq, params.samplerate, params.amplitude); + config::Signal::Square { freq, level } => { + square_gen = SquareGenerator::new(freq, params.samplerate, decibel_to_amplitude(level)); &mut square_gen as &mut dyn Iterator } - config::Signal::WhiteNoise => { - noise_gen = NoiseGenerator::new(params.amplitude); + config::Signal::WhiteNoise { level } => { + noise_gen = NoiseGenerator::new(decibel_to_amplitude(level)); &mut noise_gen as &mut dyn Iterator } }; @@ -206,7 +208,6 @@ impl CaptureDevice for GeneratorDevice { let chunksize = self.chunksize; let channels = self.channels; let signal = self.signal; - let amplitude = (10.0 as PrcFmt).powf(self.level / 20.0); let handle = thread::Builder::new() .name("SignalGenerator".to_string()) @@ -217,7 +218,6 @@ impl CaptureDevice for GeneratorDevice { channels, chunksize, capture_status, - amplitude, }; match status_channel.send(StatusMessage::CaptureReady) { Ok(()) => {} From 493ac0bb33aa404298900d320b871860820c0a02 Mon Sep 17 00:00:00 2001 From: Henrik Date: Thu, 22 Feb 2024 00:10:10 +0100 Subject: [PATCH 011/135] Move wav reading and writing to separate file --- src/filedevice.rs | 49 +-------- src/filters.rs | 180 +----------------------------- src/lib.rs | 1 + src/wavtools.rs | 271 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 275 insertions(+), 226 deletions(-) create mode 100644 src/wavtools.rs diff --git a/src/filedevice.rs b/src/filedevice.rs index cf1bf60..2b7819f 100644 --- a/src/filedevice.rs +++ b/src/filedevice.rs @@ -25,6 +25,7 @@ use crate::filedevice_bluez; use crate::filereader::BlockingReader; #[cfg(target_os = "linux")] use crate::filereader_nonblock::NonBlockingReader; +use crate::wavtools::write_wav_header; use crate::CommandMessage; use crate::PrcFmt; use crate::ProcessingState; @@ -106,54 +107,6 @@ pub trait Reader { fn read(&mut self, data: &mut [u8]) -> Result>; } -// Write a wav header. We don't know the final length so we set the file size and data length to u32::MAX. -fn write_wav_header( - dest: &mut Box, - channels: usize, - sample_format: SampleFormat, - samplerate: usize, -) -> std::io::Result<()> { - // Header - dest.write_all("RIFF".as_bytes())?; - // file size, 4 bytes, unknown so set to max - dest.write_all(&u32::MAX.to_le_bytes())?; - dest.write_all("WAVE".as_bytes())?; - - let (formatcode, bits_per_sample, bytes_per_sample) = match sample_format { - SampleFormat::S16LE => (1, 16, 2), - SampleFormat::S24LE3 => (1, 24, 3), - SampleFormat::S24LE => (1, 24, 4), - SampleFormat::S32LE => (1, 32, 4), - SampleFormat::FLOAT32LE => (3, 32, 4), - SampleFormat::FLOAT64LE => (3, 64, 8), - }; - - // format block - dest.write_all("fmt ".as_bytes())?; - // size of fmt block, 4 bytes - dest.write_all(&16_u32.to_le_bytes())?; - // format code, 2 bytes - dest.write_all(&(formatcode as u16).to_le_bytes())?; - // number of channels, 2 bytes - dest.write_all(&(channels as u16).to_le_bytes())?; - // samplerate, 4 bytes - dest.write_all(&(samplerate as u32).to_le_bytes())?; - // bytes per second sec, 4 bytes - dest.write_all(&((channels * samplerate * bytes_per_sample) as u32).to_le_bytes())?; - // block alignment, 2 bytes - dest.write_all(&((channels * bytes_per_sample) as u16).to_le_bytes())?; - // bits per sample, 2 bytes - dest.write_all(&(bits_per_sample as u16).to_le_bytes())?; - - // data block - dest.write_all("data".as_bytes())?; - // data length, 4 bytes, unknown so set to max - dest.write_all(&u32::MAX.to_le_bytes())?; - - // audio data starts from here - Ok(()) -} - /// Start a playback thread listening for AudioMessages via a channel. impl PlaybackDevice for FilePlaybackDevice { fn start( diff --git a/src/filters.rs b/src/filters.rs index ea7c5e0..83a6fb4 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -16,10 +16,9 @@ use crate::loudness; use crate::mixer; use rawsample::SampleReader; use std::collections::HashMap; -use std::convert::TryInto; use std::fs::File; use std::io::BufReader; -use std::io::{BufRead, Read, Seek, SeekFrom}; +use std::io::{BufRead, Seek, SeekFrom}; use std::sync::Arc; use std::time::Instant; @@ -27,55 +26,7 @@ use crate::PrcFmt; use crate::ProcessingParameters; use crate::Res; -/// Windows Guid -/// Used to give sample format in the extended WAVEFORMATEXTENSIBLE wav header -#[derive(Debug, PartialEq, Eq)] -struct Guid { - data1: u32, - data2: u16, - data3: u16, - data4: [u8; 8], -} - -impl Guid { - fn from_slice(data: &[u8; 16]) -> Guid { - let data1 = u32::from_le_bytes(data[0..4].try_into().unwrap()); - let data2 = u16::from_le_bytes(data[4..6].try_into().unwrap()); - let data3 = u16::from_le_bytes(data[6..8].try_into().unwrap()); - let data4 = data[8..16].try_into().unwrap(); - Guid { - data1, - data2, - data3, - data4, - } - } -} - -/// KSDATAFORMAT_SUBTYPE_IEEE_FLOAT -const SUBTYPE_FLOAT: Guid = Guid { - data1: 3, - data2: 0, - data3: 16, - data4: [128, 0, 0, 170, 0, 56, 155, 113], -}; - -/// KSDATAFORMAT_SUBTYPE_PCM -const SUBTYPE_PCM: Guid = Guid { - data1: 1, - data2: 0, - data3: 16, - data4: [128, 0, 0, 170, 0, 56, 155, 113], -}; - -#[derive(Debug)] -pub struct WavParams { - sample_format: config::FileFormat, - sample_rate: usize, - data_offset: usize, - data_length: usize, - channels: usize, -} +use crate::wavtools::find_data_in_wav; pub trait Filter { // Filter a Vec @@ -184,133 +135,6 @@ pub fn read_coeff_file( Ok(coefficients) } -pub fn find_data_in_wav(filename: &str) -> Res { - let f = File::open(filename)?; - let filesize = f.metadata()?.len(); - let mut file = BufReader::new(&f); - let mut header = [0; 12]; - let _ = file.read(&mut header)?; - - let riff_b = "RIFF".as_bytes(); - let wave_b = "WAVE".as_bytes(); - let data_b = "data".as_bytes(); - let fmt_b = "fmt ".as_bytes(); - let riff_err = header.iter().take(4).zip(riff_b).any(|(a, b)| *a != *b); - let wave_err = header - .iter() - .skip(8) - .take(4) - .zip(wave_b) - .any(|(a, b)| *a != *b); - if riff_err || wave_err { - let msg = format!("Invalid wav header in file '{filename}'"); - return Err(config::ConfigError::new(&msg).into()); - } - let mut next_chunk_location = 12; - let mut found_fmt = false; - let mut found_data = false; - let mut buffer = [0; 8]; - - let mut sample_format = config::FileFormat::S16LE; - let mut sample_rate = 0; - let mut channels = 0; - let mut data_offset = 0; - let mut data_length = 0; - - while (!found_fmt || !found_data) && next_chunk_location < filesize { - file.seek(SeekFrom::Start(next_chunk_location))?; - let _ = file.read(&mut buffer)?; - let chunk_length = u32::from_le_bytes(buffer[4..8].try_into().unwrap()); - trace!("Analyzing wav chunk of length: {}", chunk_length); - let is_data = buffer.iter().take(4).zip(data_b).all(|(a, b)| *a == *b); - let is_fmt = buffer.iter().take(4).zip(fmt_b).all(|(a, b)| *a == *b); - if is_fmt && (chunk_length == 16 || chunk_length == 18 || chunk_length == 40) { - found_fmt = true; - let mut data = vec![0; chunk_length as usize]; - let _ = file.read(&mut data).unwrap(); - let formatcode = u16::from_le_bytes(data[0..2].try_into().unwrap()); - channels = u16::from_le_bytes(data[2..4].try_into().unwrap()); - sample_rate = u32::from_le_bytes(data[4..8].try_into().unwrap()); - let bytes_per_frame = u16::from_le_bytes(data[12..14].try_into().unwrap()); - let bits = u16::from_le_bytes(data[14..16].try_into().unwrap()); - let bytes_per_sample = bytes_per_frame / channels; - sample_format = match (formatcode, bits, bytes_per_sample) { - (1, 16, 2) => config::FileFormat::S16LE, - (1, 24, 3) => config::FileFormat::S24LE3, - (1, 24, 4) => config::FileFormat::S24LE, - (1, 32, 4) => config::FileFormat::S32LE, - (3, 32, 4) => config::FileFormat::FLOAT32LE, - (3, 64, 8) => config::FileFormat::FLOAT64LE, - (0xFFFE, _, _) => { - // waveformatex - if chunk_length != 40 { - let msg = format!("Invalid extended header of wav file '{filename}'"); - return Err(config::ConfigError::new(&msg).into()); - } - let cb_size = u16::from_le_bytes(data[16..18].try_into().unwrap()); - let valid_bits_per_sample = - u16::from_le_bytes(data[18..20].try_into().unwrap()); - let channel_mask = u32::from_le_bytes(data[20..24].try_into().unwrap()); - let subformat = &data[24..40]; - let subformat_guid = Guid::from_slice(subformat.try_into().unwrap()); - trace!( - "Found extended wav fmt chunk: subformatcode: {:?}, cb_size: {}, channel_mask: {}, valid bits per sample: {}", - subformat_guid, cb_size, channel_mask, valid_bits_per_sample - ); - match ( - subformat_guid, - bits, - bytes_per_sample, - valid_bits_per_sample, - ) { - (SUBTYPE_PCM, 16, 2, 16) => config::FileFormat::S16LE, - (SUBTYPE_PCM, 24, 3, 24) => config::FileFormat::S24LE3, - (SUBTYPE_PCM, 24, 4, 24) => config::FileFormat::S24LE, - (SUBTYPE_PCM, 32, 4, 32) => config::FileFormat::S32LE, - (SUBTYPE_FLOAT, 32, 4, 32) => config::FileFormat::FLOAT32LE, - (SUBTYPE_FLOAT, 64, 8, 64) => config::FileFormat::FLOAT64LE, - (_, _, _, _) => { - let msg = - format!("Unsupported extended wav format of file '{filename}'"); - return Err(config::ConfigError::new(&msg).into()); - } - } - } - (_, _, _) => { - let msg = format!("Unsupported wav format of file '{filename}'"); - return Err(config::ConfigError::new(&msg).into()); - } - }; - trace!( - "Found wav fmt chunk: formatcode: {}, channels: {}, samplerate: {}, bits: {}, bytes_per_frame: {}", - formatcode, channels, sample_rate, bits, bytes_per_frame - ); - } else if is_data { - found_data = true; - data_offset = next_chunk_location + 8; - data_length = chunk_length; - trace!( - "Found wav data chunk, start: {}, length: {}", - data_offset, - data_length - ) - } - next_chunk_location += 8 + chunk_length as u64; - } - if found_data && found_fmt { - trace!("Wav file with parameters: format: {:?}, samplerate: {}, channels: {}, data_length: {}, data_offset: {}", sample_format, sample_rate, channels, data_length, data_offset); - return Ok(WavParams { - sample_format, - sample_rate: sample_rate as usize, - channels: channels as usize, - data_length: data_length as usize, - data_offset: data_offset as usize, - }); - } - let msg = format!("Unable to parse wav file '{filename}'"); - Err(config::ConfigError::new(&msg).into()) -} - pub fn read_wav(filename: &str, channel: usize) -> Res> { let params = find_data_in_wav(filename)?; if channel >= params.channels { diff --git a/src/lib.rs b/src/lib.rs index 2f35040..6f16697 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,6 +115,7 @@ pub mod socketserver; pub mod statefile; #[cfg(target_os = "windows")] pub mod wasapidevice; +pub mod wavtools; pub enum StatusMessage { PlaybackReady, diff --git a/src/wavtools.rs b/src/wavtools.rs new file mode 100644 index 0000000..f1e7393 --- /dev/null +++ b/src/wavtools.rs @@ -0,0 +1,271 @@ +use std::convert::TryInto; +use std::fs::File; +use std::io::BufReader; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::mem; + +use crate::config::{ConfigError, FileFormat, SampleFormat}; +use crate::Res; + +const RIFF: &[u8] = "RIFF".as_bytes(); +const WAVE: &[u8] = "WAVE".as_bytes(); +const DATA: &[u8] = "data".as_bytes(); +const FMT: &[u8] = "fmt ".as_bytes(); + +/// Windows Guid +/// Used to give sample format in the extended WAVEFORMATEXTENSIBLE wav header +#[derive(Debug, PartialEq, Eq)] +struct Guid { + data1: u32, + data2: u16, + data3: u16, + data4: [u8; 8], +} + +impl Guid { + fn from_slice(data: &[u8; 16]) -> Guid { + let data1 = read_u32(data, 0); + let data2 = read_u16(data, 4); + let data3 = read_u16(data, 6); + let data4 = data[8..16].try_into().unwrap_or([0; 8]); + Guid { + data1, + data2, + data3, + data4, + } + } +} + +/// KSDATAFORMAT_SUBTYPE_IEEE_FLOAT +const SUBTYPE_FLOAT: Guid = Guid { + data1: 3, + data2: 0, + data3: 16, + data4: [128, 0, 0, 170, 0, 56, 155, 113], +}; + +/// KSDATAFORMAT_SUBTYPE_PCM +const SUBTYPE_PCM: Guid = Guid { + data1: 1, + data2: 0, + data3: 16, + data4: [128, 0, 0, 170, 0, 56, 155, 113], +}; + +#[derive(Debug)] +pub struct WavParams { + pub sample_format: FileFormat, + pub sample_rate: usize, + pub data_offset: usize, + pub data_length: usize, + pub channels: usize, +} + +fn read_u32(buffer: &[u8], start_index: usize) -> u32 { + u32::from_le_bytes( + buffer[start_index..start_index + mem::size_of::()] + .try_into() + .unwrap_or_default(), + ) +} + +fn read_u16(buffer: &[u8], start_index: usize) -> u16 { + u16::from_le_bytes( + buffer[start_index..start_index + mem::size_of::()] + .try_into() + .unwrap_or_default(), + ) +} + +fn compare_4cc(buffer: &[u8], bytes: &[u8]) -> bool { + buffer.iter().take(4).zip(bytes).all(|(a, b)| *a == *b) +} + +fn look_up_format( + data: &[u8], + formatcode: u16, + bits: u16, + bytes_per_sample: u16, + chunk_length: u32, +) -> Res { + match (formatcode, bits, bytes_per_sample) { + (1, 16, 2) => Ok(FileFormat::S16LE), + (1, 24, 3) => Ok(FileFormat::S24LE3), + (1, 24, 4) => Ok(FileFormat::S24LE), + (1, 32, 4) => Ok(FileFormat::S32LE), + (3, 32, 4) => Ok(FileFormat::FLOAT32LE), + (3, 64, 8) => Ok(FileFormat::FLOAT64LE), + (0xFFFE, _, _) => look_up_extended_format(data, bits, bytes_per_sample, chunk_length), + (_, _, _) => Err(ConfigError::new("Unsupported wav format").into()), + } +} + +fn look_up_extended_format( + data: &[u8], + bits: u16, + bytes_per_sample: u16, + chunk_length: u32, +) -> Res { + if chunk_length != 40 { + return Err(ConfigError::new("Invalid extended header").into()); + } + let cb_size = read_u16(data, 16); + let valid_bits_per_sample = read_u16(data, 18); + let channel_mask = read_u32(data, 20); + let subformat = &data[24..40]; + let subformat_guid = Guid::from_slice(subformat.try_into().unwrap()); + trace!( + "Found extended wav fmt chunk: subformatcode: {:?}, cb_size: {}, channel_mask: {}, valid bits per sample: {}", + subformat_guid, cb_size, channel_mask, valid_bits_per_sample + ); + match ( + subformat_guid, + bits, + bytes_per_sample, + valid_bits_per_sample, + ) { + (SUBTYPE_PCM, 16, 2, 16) => Ok(FileFormat::S16LE), + (SUBTYPE_PCM, 24, 3, 24) => Ok(FileFormat::S24LE3), + (SUBTYPE_PCM, 24, 4, 24) => Ok(FileFormat::S24LE), + (SUBTYPE_PCM, 32, 4, 32) => Ok(FileFormat::S32LE), + (SUBTYPE_FLOAT, 32, 4, 32) => Ok(FileFormat::FLOAT32LE), + (SUBTYPE_FLOAT, 64, 8, 64) => Ok(FileFormat::FLOAT64LE), + (_, _, _, _) => Err(ConfigError::new("Unsupported extended wav format").into()), + } +} + +pub fn find_data_in_wav(filename: &str) -> Res { + let f = File::open(filename)?; + find_data_in_wav_stream(f).map_err(|err| { + ConfigError::new(&format!( + "Unable to parse wav file '{}', error: {}", + filename, err + )) + .into() + }) +} + +pub fn find_data_in_wav_stream(mut f: impl Read + Seek) -> Res { + let filesize = f.seek(SeekFrom::End(0))?; + f.seek(SeekFrom::Start(0))?; + let mut file = BufReader::new(f); + let mut header = [0; 12]; + file.read_exact(&mut header)?; + + // The file must start with RIFF + let riff_err = !compare_4cc(&header, RIFF); + // Bytes 8 to 12 must be WAVE + let wave_err = !compare_4cc(&header[8..], WAVE); + if riff_err || wave_err { + return Err(ConfigError::new("Invalid header").into()); + } + + let mut next_chunk_location = 12; + let mut found_fmt = false; + let mut found_data = false; + let mut buffer = [0; 8]; + + // Dummy values until we have found the real ones + let mut sample_format = FileFormat::S16LE; + let mut sample_rate = 0; + let mut channels = 0; + let mut data_offset = 0; + let mut data_length = 0; + + // Analyze each chunk to find format and data + while (!found_fmt || !found_data) && next_chunk_location < filesize { + file.seek(SeekFrom::Start(next_chunk_location))?; + file.read_exact(&mut buffer)?; + let chunk_length = read_u32(&buffer, 4); + trace!("Analyzing wav chunk of length: {}", chunk_length); + let is_data = compare_4cc(&buffer, DATA); + let is_fmt = compare_4cc(&buffer, FMT); + if is_fmt && (chunk_length == 16 || chunk_length == 18 || chunk_length == 40) { + found_fmt = true; + let mut data = vec![0; chunk_length as usize]; + file.read_exact(&mut data).unwrap(); + let formatcode: u16 = read_u16(&data, 0); + channels = read_u16(&data, 2); + sample_rate = read_u32(&data, 4); + let bytes_per_frame = read_u16(&data, 12); + let bits = read_u16(&data, 14); + let bytes_per_sample = bytes_per_frame / channels; + sample_format = + look_up_format(&data, formatcode, bits, bytes_per_sample, chunk_length)?; + trace!( + "Found wav fmt chunk: formatcode: {}, channels: {}, samplerate: {}, bits: {}, bytes_per_frame: {}", + formatcode, channels, sample_rate, bits, bytes_per_frame + ); + } else if is_data { + found_data = true; + data_offset = next_chunk_location + 8; + data_length = chunk_length; + trace!( + "Found wav data chunk, start: {}, length: {}", + data_offset, + data_length + ) + } + next_chunk_location += 8 + chunk_length as u64; + } + if found_data && found_fmt { + trace!("Wav file with parameters: format: {:?}, samplerate: {}, channels: {}, data_length: {}, data_offset: {}", sample_format, sample_rate, channels, data_length, data_offset); + return Ok(WavParams { + sample_format, + sample_rate: sample_rate as usize, + channels: channels as usize, + data_length: data_length as usize, + data_offset: data_offset as usize, + }); + } + Err(ConfigError::new("Unable to parse as wav").into()) +} + +// Write a wav header. We don't know the final length so we set the file size and data length to u32::MAX. +pub fn write_wav_header( + dest: &mut impl Write, + channels: usize, + sample_format: SampleFormat, + samplerate: usize, +) -> std::io::Result<()> { + // Header + dest.write_all(RIFF)?; + // file size, 4 bytes, unknown so set to max + dest.write_all(&u32::MAX.to_le_bytes())?; + dest.write_all(WAVE)?; + + let (formatcode, bits_per_sample, bytes_per_sample) = match sample_format { + SampleFormat::S16LE => (1, 16, 2), + SampleFormat::S24LE3 => (1, 24, 3), + SampleFormat::S24LE => (1, 24, 4), + SampleFormat::S32LE => (1, 32, 4), + SampleFormat::FLOAT32LE => (3, 32, 4), + SampleFormat::FLOAT64LE => (3, 64, 8), + }; + + // format block + dest.write_all(FMT)?; + // size of fmt block, 4 bytes + dest.write_all(&16_u32.to_le_bytes())?; + // format code, 2 bytes + dest.write_all(&(formatcode as u16).to_le_bytes())?; + // number of channels, 2 bytes + dest.write_all(&(channels as u16).to_le_bytes())?; + // samplerate, 4 bytes + dest.write_all(&(samplerate as u32).to_le_bytes())?; + // bytes per second sec, 4 bytes + dest.write_all(&((channels * samplerate * bytes_per_sample) as u32).to_le_bytes())?; + // block alignment, 2 bytes + dest.write_all(&((channels * bytes_per_sample) as u16).to_le_bytes())?; + // bits per sample, 2 bytes + dest.write_all(&(bits_per_sample as u16).to_le_bytes())?; + + // data block + dest.write_all(DATA)?; + // data length, 4 bytes, unknown so set to max + dest.write_all(&u32::MAX.to_le_bytes())?; + + // audio data starts from here + Ok(()) +} From 3f31ec38f5b1593499e51b085855052dfb255ebe Mon Sep 17 00:00:00 2001 From: Henrik Date: Thu, 22 Feb 2024 21:00:08 +0100 Subject: [PATCH 012/135] Tests for wav read and write --- src/filters.rs | 12 +----------- src/wavtools.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ testdata/f32_ex.wav | Bin 0 -> 124 bytes 3 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 testdata/f32_ex.wav diff --git a/src/filters.rs b/src/filters.rs index 83a6fb4..13bcd99 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -475,7 +475,7 @@ pub fn validate_filter(fs: usize, filter_config: &config::Filter) -> Res<()> { #[cfg(test)] mod tests { use crate::config::FileFormat; - use crate::filters::{find_data_in_wav, read_wav}; + use crate::filters::read_wav; use crate::filters::{pad_vector, read_coeff_file}; use crate::PrcFmt; @@ -620,16 +620,6 @@ mod tests { assert!(compare_waveforms(&values_padded, &values_5, 1e-15)); } - #[test] - pub fn test_analyze_wav() { - let info = find_data_in_wav("testdata/int32.wav").unwrap(); - println!("{info:?}"); - assert_eq!(info.sample_format, FileFormat::S32LE); - assert_eq!(info.data_offset, 44); - assert_eq!(info.data_length, 20); - assert_eq!(info.channels, 1); - } - #[test] pub fn test_read_wav() { let values = read_wav("testdata/int32.wav", 0).unwrap(); diff --git a/src/wavtools.rs b/src/wavtools.rs index f1e7393..696500a 100644 --- a/src/wavtools.rs +++ b/src/wavtools.rs @@ -269,3 +269,47 @@ pub fn write_wav_header( // audio data starts from here Ok(()) } + +#[cfg(test)] +mod tests { + use super::find_data_in_wav; + use super::find_data_in_wav_stream; + use super::write_wav_header; + use crate::config::{FileFormat, SampleFormat}; + use std::io::Cursor; + + #[test] + pub fn test_analyze_wav() { + let info = find_data_in_wav("testdata/int32.wav").unwrap(); + println!("{info:?}"); + assert_eq!(info.sample_format, FileFormat::S32LE); + assert_eq!(info.data_offset, 44); + assert_eq!(info.data_length, 20); + assert_eq!(info.channels, 1); + assert_eq!(info.sample_rate, 44100); + } + + #[test] + pub fn test_analyze_wavex() { + let info = find_data_in_wav("testdata/int32_ex.wav").unwrap(); + println!("{info:?}"); + assert_eq!(info.sample_format, FileFormat::FLOAT32LE); + assert_eq!(info.data_offset, 104); + assert_eq!(info.data_length, 20); + assert_eq!(info.channels, 1); + assert_eq!(info.sample_rate, 44100); + } + + #[test] + fn write_and_read_wav() { + let bytes = vec![0_u8; 1000]; + let mut buffer = Cursor::new(bytes); + write_wav_header(&mut buffer, 2, SampleFormat::S32LE, 44100).unwrap(); + let info = find_data_in_wav_stream(buffer).unwrap(); + assert_eq!(info.sample_format, FileFormat::S32LE); + assert_eq!(info.data_offset, 44); + assert_eq!(info.channels, 2); + assert_eq!(info.sample_rate, 44100); + assert_eq!(info.data_length, u32::MAX as usize); + } +} diff --git a/testdata/f32_ex.wav b/testdata/f32_ex.wav new file mode 100644 index 0000000000000000000000000000000000000000..9bc1eefaab4e52c926296e1cc0ff298018241354 GIT binary patch literal 124 zcmWIYbaN|VU|OWN=x-z#y=ZiGhVdfk6z2S%BiqKnxURXkcJi z#b7bJFfB2;1SHN1!~w33-U2|H5r{dLUQcCUXs`z|8B!8U5=B4^hKBt>0)#;TDA!;Q E0E|Et8UO$Q literal 0 HcmV?d00001 From ac9b09abebf440098aca32d0078da32599cd51de Mon Sep 17 00:00:00 2001 From: Henrik Date: Thu, 22 Feb 2024 21:05:41 +0100 Subject: [PATCH 013/135] Typos --- src/wavtools.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/wavtools.rs b/src/wavtools.rs index 696500a..38c8e27 100644 --- a/src/wavtools.rs +++ b/src/wavtools.rs @@ -222,7 +222,8 @@ pub fn find_data_in_wav_stream(mut f: impl Read + Seek) -> Res { Err(ConfigError::new("Unable to parse as wav").into()) } -// Write a wav header. We don't know the final length so we set the file size and data length to u32::MAX. +// Write a wav header. +// We don't know the final length so we set the file size and data length to u32::MAX. pub fn write_wav_header( dest: &mut impl Write, channels: usize, @@ -254,7 +255,7 @@ pub fn write_wav_header( dest.write_all(&(channels as u16).to_le_bytes())?; // samplerate, 4 bytes dest.write_all(&(samplerate as u32).to_le_bytes())?; - // bytes per second sec, 4 bytes + // bytes per second, 4 bytes dest.write_all(&((channels * samplerate * bytes_per_sample) as u32).to_le_bytes())?; // block alignment, 2 bytes dest.write_all(&((channels * bytes_per_sample) as u16).to_le_bytes())?; @@ -291,7 +292,7 @@ mod tests { #[test] pub fn test_analyze_wavex() { - let info = find_data_in_wav("testdata/int32_ex.wav").unwrap(); + let info = find_data_in_wav("testdata/f32_ex.wav").unwrap(); println!("{info:?}"); assert_eq!(info.sample_format, FileFormat::FLOAT32LE); assert_eq!(info.data_offset, 104); From e8c01fa22b4a944c3697a31ce95ba52a65c7c433 Mon Sep 17 00:00:00 2001 From: Henrik Date: Thu, 22 Feb 2024 22:00:54 +0100 Subject: [PATCH 014/135] Clean up test scripts --- testscripts/analyze_wav.py | 85 ---------------------------------- testscripts/makesineraw.py | 16 +++++-- testscripts/play_wav.py | 52 --------------------- testscripts/test_file.yml | 4 +- testscripts/test_file_sine.yml | 9 +--- 5 files changed, 16 insertions(+), 150 deletions(-) delete mode 100644 testscripts/analyze_wav.py delete mode 100644 testscripts/play_wav.py diff --git a/testscripts/analyze_wav.py b/testscripts/analyze_wav.py deleted file mode 100644 index e82ff4f..0000000 --- a/testscripts/analyze_wav.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -import struct -import logging - -sampleformats = {1: "int", - 3: "float", - } - -def analyze_chunk(type, start, length, file, wav_info): - if type == "fmt ": - data = file.read(length) - wav_info['SampleFormat'] = sampleformats[struct.unpack('= input_filesize: - break - file_in.close() - return wav_info - -if __name__ == "__main__": - import sys - info = read_wav_header(sys.argv[1]) - print("Wav properties:") - for name, val in info.items(): - print("{} : {}".format(name, val)) diff --git a/testscripts/makesineraw.py b/testscripts/makesineraw.py index 0d9bd93..04c1528 100644 --- a/testscripts/makesineraw.py +++ b/testscripts/makesineraw.py @@ -1,17 +1,25 @@ -# Make a simple sine for testing purposes +# Make simple sines for testing purposes +# Example: 20 seconds of 1kHz + 2 kHz at 44.1 kHz +# > python testscripts/makesineraw.py 44100 20 1000 2000 import numpy as np import sys -f = float(sys.argv[2]) +f = float(sys.argv[3]) fs = float(sys.argv[1]) -length = int(sys.argv[3]) +length = int(sys.argv[2]) t = np.linspace(0, 20, num=int(20*fs), endpoint=False) wave = 0.5*np.sin(f*2*np.pi*t) +f_label = "{:.0f}".format(f) +for f2 in sys.argv[4:]: + f2f = float(f2) + wave += 0.5*np.sin(f2f*2*np.pi*t) + f_label = "{}-{:.0f}".format(f_label, f2f) + wave= np.reshape(wave,(-1,1)) wave = np.concatenate((wave, wave), axis=1) wave64 = wave.astype('float64') -name = "sine_{:.0f}_{:.0f}_{}s_f64_2ch.raw".format(f, fs, length) +name = "sine_{}_{:.0f}_{}s_f64_2ch.raw".format(f_label, fs, length) #print(wave64) wave64.tofile(name) diff --git a/testscripts/play_wav.py b/testscripts/play_wav.py deleted file mode 100644 index 0421224..0000000 --- a/testscripts/play_wav.py +++ /dev/null @@ -1,52 +0,0 @@ -#play wav -import yaml -from websocket import create_connection -import sys -import os -import json -from analyze_wav import read_wav_header - -try: - port = int(sys.argv[1]) - template_file = os.path.abspath(sys.argv[2]) - wav_file = os.path.abspath(sys.argv[3]) -except: - print("Usage: start CamillaDSP with the websocket server enabled, and wait mode:") - print("> camilladsp -p4321 -w") - print("Then play a wav file:") - print("> python play_wav.py 4321 path/to/some/template/config.yml path/to/file.wav") - sys.exit() -# read the config to a Python dict -with open(template_file) as f: - cfg=yaml.safe_load(f) - -wav_info = read_wav_header(wav_file) -if wav_info["SampleFormat"] == "unknown": - print("Unknown wav sample format!") - -# template -capt_device = { - "type": "File", - "filename": wav_file, - "format": wav_info["SampleFormat"], - "channels": wav_info["NumChannels"], - "skip_bytes": wav_info["DataStart"], - "read_bytes": wav_info["DataLength"], -} -# Modify config -cfg["devices"]["capture_samplerate"] = wav_info["SampleRate"] -cfg["devices"]["enable_rate_adjust"] = False -if cfg["devices"]["samplerate"] != cfg["devices"]["capture_samplerate"]: - cfg["devices"]["enable_resampling"] = True - cfg["devices"]["resampler_type"] = "Synchronous" -else: - cfg["devices"]["enable_resampling"] = False -cfg["devices"]["capture"] = capt_device - -# Serialize to yaml string -modded = yaml.dump(cfg) - -# Send the modded config -ws = create_connection("ws://127.0.0.1:{}".format(port)) -ws.send(json.dumps({"SetConfig": modded)) -ws.recv() \ No newline at end of file diff --git a/testscripts/test_file.yml b/testscripts/test_file.yml index e5e9d9e..20e9d3a 100644 --- a/testscripts/test_file.yml +++ b/testscripts/test_file.yml @@ -5,12 +5,12 @@ devices: target_level: 512 adjust_period: 10 playback: - type: Raw + type: File channels: 2 filename: "result_i32.raw" format: S32LE capture: - type: Raw + type: File channels: 1 filename: "spike_i32.raw" format: S32LE diff --git a/testscripts/test_file_sine.yml b/testscripts/test_file_sine.yml index e5f8cf5..8866dee 100644 --- a/testscripts/test_file_sine.yml +++ b/testscripts/test_file_sine.yml @@ -3,20 +3,17 @@ devices: samplerate: 44100 chunksize: 1024 playback: - type: Raw + type: File channels: 2 filename: "result_f64.raw" format: FLOAT64LE capture: - type: Raw + type: File channels: 2 filename: "sine_1000_44100_20s_f64_2ch.raw" format: FLOAT64LE extra_samples: 0 - - - filters: dummy: type: Conv @@ -25,8 +22,6 @@ filters: filename: testscripts/spike_f64_65k.raw format: FLOAT64LE - - pipeline: - type: Filter channels: [0, 1] From 727eb2aa487a5862668e0fc2833449e4aa10a91c Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Sun, 3 Mar 2024 22:12:02 +0100 Subject: [PATCH 015/135] Try to find supported multichannel channel mask in exclusive mode --- src/wasapidevice.rs | 95 +++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 42 deletions(-) diff --git a/src/wasapidevice.rs b/src/wasapidevice.rs index f661fc5..2747a6c 100644 --- a/src/wasapidevice.rs +++ b/src/wasapidevice.rs @@ -120,6 +120,43 @@ fn wave_format( } } +fn get_supported_wave_format( + audio_client: &wasapi::AudioClient, + sample_format: &SampleFormat, + samplerate: usize, + channels: usize, + sharemode: &wasapi::ShareMode, +) -> Res { + let wave_format = wave_format(sample_format, samplerate, channels); + match sharemode { + wasapi::ShareMode::Exclusive => { + return audio_client.is_supported_exclusive_with_quirks(&wave_format); + } + wasapi::ShareMode::Shared => { + match audio_client.is_supported(&wave_format, &sharemode) { + Ok(None) => { + debug!("Device supports format {:?}", wave_format); + return Ok(wave_format); + } + Ok(Some(modified)) => { + let msg = format!( + "Device doesn't support format:\n{:#?}\nClosest match is:\n{:#?}", + wave_format, modified + ); + return Err(ConfigError::new(&msg).into()); + } + Err(err) => { + let msg = format!( + "Device doesn't support format:\n{:#?}\nError: {}", + wave_format, err + ); + return Err(ConfigError::new(&msg).into()); + } + }; + } + } +} + fn open_playback( devname: &Option, samplerate: usize, @@ -150,26 +187,13 @@ fn open_playback( trace!("Found playback device {:?}", devname); let mut audio_client = device.get_iaudioclient()?; trace!("Got playback iaudioclient"); - let wave_format = wave_format(sample_format, samplerate, channels); - match audio_client.is_supported(&wave_format, &sharemode) { - Ok(None) => { - debug!("Playback device supports format {:?}", wave_format) - } - Ok(Some(modified)) => { - let msg = format!( - "Playback device doesn't support format:\n{:#?}\nClosest match is:\n{:#?}", - wave_format, modified - ); - return Err(ConfigError::new(&msg).into()); - } - Err(err) => { - let msg = format!( - "Playback device doesn't support format:\n{:#?}\nError: {}", - wave_format, err - ); - return Err(ConfigError::new(&msg).into()); - } - }; + let wave_format = get_supported_wave_format( + &audio_client, + sample_format, + samplerate, + channels, + &sharemode, + )?; let (def_time, min_time) = audio_client.get_periods()?; debug!( "playback default period {}, min period {}", @@ -182,7 +206,7 @@ fn open_playback( &sharemode, false, )?; - debug!("initialized capture"); + debug!("initialized playback audio client"); let handle = audio_client.set_get_eventhandle()?; let render_client = audio_client.get_audiorenderclient()?; debug!("Opened Wasapi playback device {:?}", devname); @@ -232,26 +256,13 @@ fn open_capture( trace!("Found capture device {:?}", devname); let mut audio_client = device.get_iaudioclient()?; trace!("Got capture iaudioclient"); - let wave_format = wave_format(sample_format, samplerate, channels); - match audio_client.is_supported(&wave_format, &sharemode) { - Ok(None) => { - debug!("Capture device supports format {:?}", wave_format) - } - Ok(Some(modified)) => { - let msg = format!( - "Capture device doesn't support format:\n{:#?}\nClosest match is:\n{:#?}", - wave_format, modified - ); - return Err(ConfigError::new(&msg).into()); - } - Err(err) => { - let msg = format!( - "Capture device doesn't support format:\n{:#?}\nError: {}", - wave_format, err - ); - return Err(ConfigError::new(&msg).into()); - } - }; + let wave_format = get_supported_wave_format( + &audio_client, + sample_format, + samplerate, + channels, + &sharemode, + )?; let (def_time, min_time) = audio_client.get_periods()?; debug!( "capture default period {}, min period {}", @@ -264,7 +275,7 @@ fn open_capture( &sharemode, loopback, )?; - debug!("initialized capture"); + debug!("initialized capture audio client"); let handle = audio_client.set_get_eventhandle()?; trace!("capture got event handle"); let capture_client = audio_client.get_audiocaptureclient()?; From b69a549af2c4f2966912a660ec13b22f63dcc786 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Sun, 3 Mar 2024 22:36:26 +0100 Subject: [PATCH 016/135] Update wasapi crate --- Cargo.toml | 4 ++-- src/wasapidevice.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1fa06e9..7c4a804 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,8 +42,8 @@ dispatch = "0.2.0" [target.'cfg(target_os="windows")'.dependencies] #wasapi = { path = "../../rust/wasapi" } #wasapi = { git = "https://github.com/HEnquist/wasapi-rs", branch = "win041" } -wasapi = "0.13.0" -windows = {version = "0.48.0", features = ["Win32_System_Threading", "Win32_Foundation"] } +wasapi = "0.14.0" +windows = {version = "0.51.0", features = ["Win32_System_Threading", "Win32_Foundation"] } [dependencies] serde = { version = "1.0", features = ["derive"] } diff --git a/src/wasapidevice.rs b/src/wasapidevice.rs index 2747a6c..a271dbb 100644 --- a/src/wasapidevice.rs +++ b/src/wasapidevice.rs @@ -15,7 +15,7 @@ use std::thread; use std::time::Duration; use wasapi; use wasapi::DeviceCollection; -use windows::w; +use windows::core::w; use windows::Win32::System::Threading::AvSetMmThreadCharacteristicsW; use crate::CommandMessage; From 45e243f5fb623c111aaca8cf049f6e8e5f1e3adb Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 5 Mar 2024 23:24:33 +0100 Subject: [PATCH 017/135] Update rubato to latest --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1fa06e9..bb6630f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,8 +67,8 @@ tungstenite = { version = "0.21.0", optional = true } native-tls = { version = "0.2.7", optional = true } libpulse-binding = { version = "2.0", optional = true } libpulse-simple-binding = { version = "2.0", optional = true } -rubato = "0.14.1" -#rubato = { git = "https://github.com/HEnquist/rubato", branch = "next-0.13" } +rubato = "0.15.0" +#rubato = { git = "https://github.com/HEnquist/rubato", branch = "optional_fft" } cpal = { version = "0.13.3", optional = true } #rawsample = { path = "../../rust/rawsample" } #rawsample = { git = "https://github.com/HEnquist/rawsample", branch = "main" } From 20b8ccc0165b08a6a20c09aaa1dd0310f5bb34c5 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Wed, 6 Mar 2024 21:08:28 +0100 Subject: [PATCH 018/135] Use aligned period size --- src/wasapidevice.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/wasapidevice.rs b/src/wasapidevice.rs index a271dbb..909b08e 100644 --- a/src/wasapidevice.rs +++ b/src/wasapidevice.rs @@ -195,17 +195,19 @@ fn open_playback( &sharemode, )?; let (def_time, min_time) = audio_client.get_periods()?; - debug!( - "playback default period {}, min period {}", - def_time, min_time - ); + let aligned_time = + audio_client.calculate_aligned_period_near(def_time, Some(128), &wave_format)?; audio_client.initialize_client( &wave_format, - def_time, + aligned_time, &wasapi::Direction::Render, &sharemode, false, )?; + debug!( + "playback default period {}, min period {}, aligned period {}", + def_time, min_time, aligned_time + ); debug!("initialized playback audio client"); let handle = audio_client.set_get_eventhandle()?; let render_client = audio_client.get_audiorenderclient()?; From 83f927ccb9860d79cb9909ddaddd7c2f1a3c63aa Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Wed, 6 Mar 2024 21:15:04 +0100 Subject: [PATCH 019/135] Round off numbers in logging --- src/wasapidevice.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wasapidevice.rs b/src/wasapidevice.rs index 909b08e..49bd9b9 100644 --- a/src/wasapidevice.rs +++ b/src/wasapidevice.rs @@ -364,7 +364,7 @@ fn playback_loop( device_time = device_prevtime + buffer_free_frame_count as f64 / samplerate; } trace!( - "Device time counted up by {} s", + "Device time counted up by {:.4} s", device_time - device_prevtime ); if buffer_free_frame_count > 0 @@ -372,7 +372,7 @@ fn playback_loop( > 1.75 * (buffer_free_frame_count as f64 / samplerate) { warn!( - "Missing event! Resetting stream. Interval {} s, expected {} s", + "Missing event! Resetting stream. Interval {:.4} s, expected {:.4} s", device_time - device_prevtime, buffer_free_frame_count as f64 / samplerate ); From c56cea3c4a9e5c79704c230dfede9d14a64db092 Mon Sep 17 00:00:00 2001 From: Henrik Date: Sun, 10 Mar 2024 22:51:36 +0100 Subject: [PATCH 020/135] Add preliminary support for capture from wav --- src/audiodevice.rs | 2 +- src/config.rs | 121 ++++++++++++++++++++++++++++++--------------- src/filedevice.rs | 63 +++++++++++++---------- src/filters.rs | 2 +- src/wavtools.rs | 42 ++++++++-------- 5 files changed, 140 insertions(+), 90 deletions(-) diff --git a/src/audiodevice.rs b/src/audiodevice.rs index 5c63be8..a6b0eb8 100644 --- a/src/audiodevice.rs +++ b/src/audiodevice.rs @@ -601,7 +601,7 @@ pub fn new_capture_device(conf: config::Devices) -> Box { resampler_config: conf.resampler, chunksize: conf.chunksize, channels: dev.channels, - sample_format: dev.format, + sample_format: Some(dev.format), extra_samples: dev.extra_samples(), silence_threshold: conf.silence_threshold(), silence_timeout: conf.silence_timeout(), diff --git a/src/config.rs b/src/config.rs index eb1ba12..b88e15c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,7 @@ use crate::compressor; use crate::filters; use crate::mixer; +use crate::wavtools::find_data_in_wav_stream; use parking_lot::RwLock; use serde::{de, Deserialize, Serialize}; //use serde_with; @@ -119,6 +120,57 @@ impl fmt::Display for SampleFormat { } } +#[allow(clippy::upper_case_acronyms)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +// Similar to SampleFormat, but also includes TEXT +pub enum FileFormat { + TEXT, + S16LE, + S24LE, + S24LE3, + S32LE, + FLOAT32LE, + FLOAT64LE, +} + +impl FileFormat { + pub fn bits_per_sample(&self) -> usize { + match self { + FileFormat::S16LE => 16, + FileFormat::S24LE => 24, + FileFormat::S24LE3 => 24, + FileFormat::S32LE => 32, + FileFormat::FLOAT32LE => 32, + FileFormat::FLOAT64LE => 64, + FileFormat::TEXT => 0, + } + } + + pub fn bytes_per_sample(&self) -> usize { + match self { + FileFormat::S16LE => 2, + FileFormat::S24LE => 4, + FileFormat::S24LE3 => 3, + FileFormat::S32LE => 4, + FileFormat::FLOAT32LE => 4, + FileFormat::FLOAT64LE => 8, + FileFormat::TEXT => 0, + } + } + + pub fn from_sample_format(sample_format: &SampleFormat) -> Self { + match sample_format { + SampleFormat::S16LE => FileFormat::S16LE, + SampleFormat::S24LE => FileFormat::S24LE, + SampleFormat::S24LE3 => FileFormat::S24LE3, + SampleFormat::S32LE => FileFormat::S32LE, + SampleFormat::FLOAT32LE => FileFormat::FLOAT32LE, + SampleFormat::FLOAT64LE => FileFormat::FLOAT64LE, + } + } +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] #[serde(tag = "type")] @@ -221,7 +273,8 @@ pub struct CaptureDeviceFile { #[serde(deserialize_with = "validate_nonzero_usize")] pub channels: usize, pub filename: String, - pub format: SampleFormat, + #[serde(default)] + pub format: Option, #[serde(default)] pub extra_samples: Option, #[serde(default)] @@ -645,45 +698,6 @@ pub enum Filter { }, } -#[allow(clippy::upper_case_acronyms)] -#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)] -#[serde(deny_unknown_fields)] -pub enum FileFormat { - TEXT, - S16LE, - S24LE, - S24LE3, - S32LE, - FLOAT32LE, - FLOAT64LE, -} - -impl FileFormat { - pub fn bits_per_sample(&self) -> usize { - match self { - FileFormat::S16LE => 16, - FileFormat::S24LE => 24, - FileFormat::S24LE3 => 24, - FileFormat::S32LE => 32, - FileFormat::FLOAT32LE => 32, - FileFormat::FLOAT64LE => 64, - FileFormat::TEXT => 0, - } - } - - pub fn bytes_per_sample(&self) -> usize { - match self { - FileFormat::S16LE => 2, - FileFormat::S24LE => 4, - FileFormat::S24LE3 => 3, - FileFormat::S32LE => 4, - FileFormat::FLOAT32LE => 4, - FileFormat::FLOAT64LE => 8, - FileFormat::TEXT => 0, - } - } -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(tag = "type")] #[serde(deny_unknown_fields)] @@ -1331,6 +1345,7 @@ pub fn load_config(filename: &str) -> Res { } fn apply_overrides(configuration: &mut Configuration) { + // TODO update to get overrides from wav capture file let overrides = OVERRIDES.read(); if let Some(rate) = overrides.samplerate { let cfg_rate = configuration.devices.samplerate; @@ -1444,7 +1459,8 @@ fn apply_overrides(configuration: &mut Configuration) { debug!("Apply override for capture sample format: {}", fmt); match &mut configuration.devices.capture { CaptureDevice::File(dev) => { - dev.format = fmt; + // TODO dont update if null! + dev.format = Some(fmt); } CaptureDevice::Stdin(dev) => { dev.format = fmt; @@ -1791,6 +1807,29 @@ pub fn validate_config(conf: &mut Configuration, filename: Option<&str>) -> Res< .into()); } } + if let CaptureDevice::File(dev) = &conf.devices.capture { + let fname = &dev.filename; + let f = match File::open(fname) { + Ok(f) => f, + Err(err) => { + let msg = format!("Could not open input file '{fname}'. Error: {err}"); + return Err(ConfigError::new(&msg).into()); + } + }; + if dev.format.is_none() { + let file = BufReader::new(&f); + let wav_info = find_data_in_wav_stream(file)?; + // TODO warn if samplerate is wrong + // TODO 2, next step: update overrides for channels and sample rate + if wav_info.channels != dev.channels { + let msg = format!( + "Wav file '{}' has wrong number of channels, expected: {}, found: {}", + fname, dev.channels, wav_info.channels + ); + return Err(ConfigError::new(&msg).into()); + } + } + } let mut num_channels = conf.devices.capture.channels(); let fs = conf.devices.samplerate; if let Some(pipeline) = &conf.pipeline { diff --git a/src/filedevice.rs b/src/filedevice.rs index 2b7819f..f78e14e 100644 --- a/src/filedevice.rs +++ b/src/filedevice.rs @@ -25,7 +25,7 @@ use crate::filedevice_bluez; use crate::filereader::BlockingReader; #[cfg(target_os = "linux")] use crate::filereader_nonblock::NonBlockingReader; -use crate::wavtools::write_wav_header; +use crate::wavtools::{find_data_in_wav, write_wav_header}; use crate::CommandMessage; use crate::PrcFmt; use crate::ProcessingState; @@ -63,7 +63,7 @@ pub struct FileCaptureDevice { pub capture_samplerate: usize, pub resampler_config: Option, pub channels: usize, - pub sample_format: SampleFormat, + pub sample_format: Option, pub silence_threshold: PrcFmt, pub silence_timeout: PrcFmt, pub extra_samples: usize, @@ -232,38 +232,32 @@ fn nbr_capture_bytes( store_bytes_per_sample: usize, ) -> usize { if let Some(resampl) = &resampler { - //let new_capture_bytes = resampl.input_frames_next() * channels * store_bytes_per_sample; - //trace!( - // "Resampler needs {} frames, will read {} bytes", - // resampl.input_frames_next(), - // new_capture_bytes - //); - //new_capture_bytes resampl.input_frames_next() * channels * store_bytes_per_sample } else { capture_bytes } } -fn capture_bytes( +// Adjust buffer size if needed, and check if capture is done +fn limit_capture_bytes( bytes_to_read: usize, nbr_bytes_read: usize, capture_bytes: usize, buf: &mut Vec, -) -> usize { - let capture_bytes = if bytes_to_read == 0 +) -> (usize, bool) { + let (capture_bytes, done) = if bytes_to_read == 0 || (bytes_to_read > 0 && (nbr_bytes_read + capture_bytes) <= bytes_to_read) { - capture_bytes + (capture_bytes, false) } else { debug!("Stopping capture, reached read_bytes limit"); - bytes_to_read - nbr_bytes_read + (bytes_to_read - nbr_bytes_read, true) }; if capture_bytes > buf.len() { debug!("Capture buffer too small, extending"); buf.append(&mut vec![0u8; capture_bytes - buf.len()]); } - capture_bytes + (capture_bytes, done) } fn capture_loop( @@ -279,6 +273,7 @@ fn capture_loop( let mut bytes_read = 0; let mut bytes_to_capture = chunksize_bytes; let mut bytes_to_capture_tmp; + let mut capture_done: bool; let mut extra_bytes_left = params.extra_bytes; let mut nbr_bytes_read = 0; let rate_measure_interval_ms = (1000.0 * params.rate_measure_interval) as u64; @@ -342,7 +337,7 @@ fn capture_loop( params.channels, params.store_bytes_per_sample, ); - bytes_to_capture_tmp = capture_bytes( + (bytes_to_capture_tmp, capture_done) = limit_capture_bytes( params.read_bytes, nbr_bytes_read, bytes_to_capture, @@ -350,8 +345,8 @@ fn capture_loop( ); //let read_res = read_retry(&mut file, &mut buf[0..capture_bytes_temp]); let read_res = file.read(&mut buf[0..bytes_to_capture_tmp]); - match read_res { - Ok(ReadResult::EndOfFile(bytes)) => { + match (read_res, capture_done) { + (Ok(ReadResult::EndOfFile(bytes)), _) | (Ok(ReadResult::Complete(bytes)), true) => { bytes_read = bytes; nbr_bytes_read += bytes; if bytes > 0 { @@ -391,7 +386,7 @@ fn capture_loop( break; } } - Ok(ReadResult::Timeout(bytes)) => { + (Ok(ReadResult::Timeout(bytes)), _) => { bytes_read = bytes; nbr_bytes_read += bytes; if bytes > 0 { @@ -426,7 +421,8 @@ fn capture_loop( continue; } } - Ok(ReadResult::Complete(bytes)) => { + (Ok(ReadResult::Complete(bytes)), false) => { + trace!("successfully read {} bytes", bytes); if stalled { debug!("Leaving stalled state, resuming processing"); stalled = false; @@ -477,7 +473,7 @@ fn capture_loop( trace!("Measured sample rate is {:.1} Hz", measured_rate_f); } } - Err(err) => { + (Err(err), _) => { debug!("Encountered a read error"); msg_channels .status @@ -558,7 +554,26 @@ impl CaptureDevice for FileCaptureDevice { let chunksize = self.chunksize; let capture_samplerate = self.capture_samplerate; let channels = self.channels; - let store_bytes_per_sample = self.sample_format.bytes_per_sample(); + let mut skip_bytes = self.skip_bytes; + let mut read_bytes = self.read_bytes; + let sample_format = match &self.source { + CaptureSource::Filename(fname) => { + if self.sample_format.is_none() { + // No format was given, try to get from the file. + // Only works if the file is in wav format. + let wav_info = find_data_in_wav(fname)?; + skip_bytes = wav_info.data_offset; + read_bytes = wav_info.data_length; + // TODO check channels? + wav_info.sample_format + } else { + self.sample_format.unwrap() + } + } + _ => self.sample_format.unwrap(), + }; + let store_bytes_per_sample = sample_format.bytes_per_sample(); + let extra_bytes = self.extra_samples * store_bytes_per_sample * channels; let buffer_bytes = 2.0f32.powf( (capture_samplerate as f32 / samplerate as f32 * chunksize as f32) .log2() @@ -567,12 +582,8 @@ impl CaptureDevice for FileCaptureDevice { * 2 * channels * store_bytes_per_sample; - let sample_format = self.sample_format; let resampler_config = self.resampler_config; let async_src = resampler_is_async(&resampler_config); - let extra_bytes = self.extra_samples * store_bytes_per_sample * channels; - let skip_bytes = self.skip_bytes; - let read_bytes = self.read_bytes; let silence_timeout = self.silence_timeout; let silence_threshold = self.silence_threshold; let stop_on_rate_change = self.stop_on_rate_change; diff --git a/src/filters.rs b/src/filters.rs index 13bcd99..4284185 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -147,7 +147,7 @@ pub fn read_wav(filename: &str, channel: usize) -> Res> { let alldata = read_coeff_file( filename, - ¶ms.sample_format, + &config::FileFormat::from_sample_format(¶ms.sample_format), params.data_length, params.data_offset, )?; diff --git a/src/wavtools.rs b/src/wavtools.rs index 38c8e27..81e49cd 100644 --- a/src/wavtools.rs +++ b/src/wavtools.rs @@ -4,7 +4,7 @@ use std::io::BufReader; use std::io::{Read, Seek, SeekFrom, Write}; use std::mem; -use crate::config::{ConfigError, FileFormat, SampleFormat}; +use crate::config::{ConfigError, SampleFormat}; use crate::Res; const RIFF: &[u8] = "RIFF".as_bytes(); @@ -55,7 +55,7 @@ const SUBTYPE_PCM: Guid = Guid { #[derive(Debug)] pub struct WavParams { - pub sample_format: FileFormat, + pub sample_format: SampleFormat, pub sample_rate: usize, pub data_offset: usize, pub data_length: usize, @@ -88,14 +88,14 @@ fn look_up_format( bits: u16, bytes_per_sample: u16, chunk_length: u32, -) -> Res { +) -> Res { match (formatcode, bits, bytes_per_sample) { - (1, 16, 2) => Ok(FileFormat::S16LE), - (1, 24, 3) => Ok(FileFormat::S24LE3), - (1, 24, 4) => Ok(FileFormat::S24LE), - (1, 32, 4) => Ok(FileFormat::S32LE), - (3, 32, 4) => Ok(FileFormat::FLOAT32LE), - (3, 64, 8) => Ok(FileFormat::FLOAT64LE), + (1, 16, 2) => Ok(SampleFormat::S16LE), + (1, 24, 3) => Ok(SampleFormat::S24LE3), + (1, 24, 4) => Ok(SampleFormat::S24LE), + (1, 32, 4) => Ok(SampleFormat::S32LE), + (3, 32, 4) => Ok(SampleFormat::FLOAT32LE), + (3, 64, 8) => Ok(SampleFormat::FLOAT64LE), (0xFFFE, _, _) => look_up_extended_format(data, bits, bytes_per_sample, chunk_length), (_, _, _) => Err(ConfigError::new("Unsupported wav format").into()), } @@ -106,7 +106,7 @@ fn look_up_extended_format( bits: u16, bytes_per_sample: u16, chunk_length: u32, -) -> Res { +) -> Res { if chunk_length != 40 { return Err(ConfigError::new("Invalid extended header").into()); } @@ -125,12 +125,12 @@ fn look_up_extended_format( bytes_per_sample, valid_bits_per_sample, ) { - (SUBTYPE_PCM, 16, 2, 16) => Ok(FileFormat::S16LE), - (SUBTYPE_PCM, 24, 3, 24) => Ok(FileFormat::S24LE3), - (SUBTYPE_PCM, 24, 4, 24) => Ok(FileFormat::S24LE), - (SUBTYPE_PCM, 32, 4, 32) => Ok(FileFormat::S32LE), - (SUBTYPE_FLOAT, 32, 4, 32) => Ok(FileFormat::FLOAT32LE), - (SUBTYPE_FLOAT, 64, 8, 64) => Ok(FileFormat::FLOAT64LE), + (SUBTYPE_PCM, 16, 2, 16) => Ok(SampleFormat::S16LE), + (SUBTYPE_PCM, 24, 3, 24) => Ok(SampleFormat::S24LE3), + (SUBTYPE_PCM, 24, 4, 24) => Ok(SampleFormat::S24LE), + (SUBTYPE_PCM, 32, 4, 32) => Ok(SampleFormat::S32LE), + (SUBTYPE_FLOAT, 32, 4, 32) => Ok(SampleFormat::FLOAT32LE), + (SUBTYPE_FLOAT, 64, 8, 64) => Ok(SampleFormat::FLOAT64LE), (_, _, _, _) => Err(ConfigError::new("Unsupported extended wav format").into()), } } @@ -167,7 +167,7 @@ pub fn find_data_in_wav_stream(mut f: impl Read + Seek) -> Res { let mut buffer = [0; 8]; // Dummy values until we have found the real ones - let mut sample_format = FileFormat::S16LE; + let mut sample_format = SampleFormat::S16LE; let mut sample_rate = 0; let mut channels = 0; let mut data_offset = 0; @@ -276,14 +276,14 @@ mod tests { use super::find_data_in_wav; use super::find_data_in_wav_stream; use super::write_wav_header; - use crate::config::{FileFormat, SampleFormat}; + use crate::config::SampleFormat; use std::io::Cursor; #[test] pub fn test_analyze_wav() { let info = find_data_in_wav("testdata/int32.wav").unwrap(); println!("{info:?}"); - assert_eq!(info.sample_format, FileFormat::S32LE); + assert_eq!(info.sample_format, SampleFormat::S32LE); assert_eq!(info.data_offset, 44); assert_eq!(info.data_length, 20); assert_eq!(info.channels, 1); @@ -294,7 +294,7 @@ mod tests { pub fn test_analyze_wavex() { let info = find_data_in_wav("testdata/f32_ex.wav").unwrap(); println!("{info:?}"); - assert_eq!(info.sample_format, FileFormat::FLOAT32LE); + assert_eq!(info.sample_format, SampleFormat::FLOAT32LE); assert_eq!(info.data_offset, 104); assert_eq!(info.data_length, 20); assert_eq!(info.channels, 1); @@ -307,7 +307,7 @@ mod tests { let mut buffer = Cursor::new(bytes); write_wav_header(&mut buffer, 2, SampleFormat::S32LE, 44100).unwrap(); let info = find_data_in_wav_stream(buffer).unwrap(); - assert_eq!(info.sample_format, FileFormat::S32LE); + assert_eq!(info.sample_format, SampleFormat::S32LE); assert_eq!(info.data_offset, 44); assert_eq!(info.channels, 2); assert_eq!(info.sample_rate, 44100); From b6318e234203910669b6f2da7ef3d7e450bc7f80 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Tue, 12 Mar 2024 22:46:09 +0100 Subject: [PATCH 021/135] Raise inner thread priority --- src/wasapidevice.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/wasapidevice.rs b/src/wasapidevice.rs index 49bd9b9..75a8fd2 100644 --- a/src/wasapidevice.rs +++ b/src/wasapidevice.rs @@ -16,7 +16,9 @@ use std::time::Duration; use wasapi; use wasapi::DeviceCollection; use windows::core::w; -use windows::Win32::System::Threading::AvSetMmThreadCharacteristicsW; +use windows::Win32::System::Threading::{ + AvSetMmThreadCharacteristicsW, AvSetMmThreadPriority, AVRT_PRIORITY_HIGH, +}; use crate::CommandMessage; use crate::PrcFmt; @@ -337,11 +339,12 @@ fn playback_loop( // Raise priority let mut task_idx = 0; - unsafe { - let _ = AvSetMmThreadCharacteristicsW(w!("Pro Audio"), &mut task_idx); - } + let task_handle = unsafe { AvSetMmThreadCharacteristicsW(w!("Pro Audio"), &mut task_idx)? }; if task_idx > 0 { debug!("Playback thread raised priority, task index: {}", task_idx); + unsafe { + AvSetMmThreadPriority(task_handle, AVRT_PRIORITY_HIGH)?; + } } else { warn!("Failed to raise playback thread priority"); } @@ -476,11 +479,12 @@ fn capture_loop( // Raise priority let mut task_idx = 0; - unsafe { - let _ = AvSetMmThreadCharacteristicsW(w!("Pro Audio"), &mut task_idx); - } + let task_handle = unsafe { AvSetMmThreadCharacteristicsW(w!("Pro Audio"), &mut task_idx)? }; if task_idx > 0 { debug!("Capture thread raised priority, task index: {}", task_idx); + unsafe { + AvSetMmThreadPriority(task_handle, AVRT_PRIORITY_HIGH)?; + } } else { warn!("Failed to raise capture thread priority"); } From 6eafef98edf1cff9ec7035a6abce15fd9ce5f460 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Wed, 13 Mar 2024 21:02:44 +0100 Subject: [PATCH 022/135] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03cf315..a207f08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ New features: - Optionally write wav header when outputting to file or stdout. Changes: - Filter pipeline steps take a list of channels to filter instead of a single one. +Bugfixes: +- Fix compatibility issues for some WASAPI devices. ## v2.0.3 Bugfixes: From 2af22eb9ed5155c9cf6f0e6386fc942387e19acc Mon Sep 17 00:00:00 2001 From: Henrik Date: Thu, 14 Mar 2024 22:38:06 +0100 Subject: [PATCH 023/135] Get override values from wav, update readme --- CHANGELOG.md | 2 + README.md | 56 +++++++---- exampleconfigs/all_biquads.yml | 2 +- exampleconfigs/brokenconfig.yml | 2 +- exampleconfigs/ditherplay.yml | 2 +- exampleconfigs/file_pb.yml | 2 +- exampleconfigs/gainconfig.yml | 2 +- exampleconfigs/lf_compressor.yml | 2 +- exampleconfigs/nofilters.yml | 2 +- exampleconfigs/nomixers.yml | 2 +- exampleconfigs/resample_file.yml | 2 +- exampleconfigs/simpleconfig_plot.yml | 2 +- exampleconfigs/simpleconfig_resample.yml | 2 +- exampleconfigs/tokens.yml | 2 +- src/audiodevice.rs | 20 +++- src/config.rs | 113 +++++++++++++++-------- src/filedevice.rs | 5 +- src/filters.rs | 6 +- src/lib.rs | 3 +- src/wavtools.rs | 2 +- testscripts/test_file.yml | 2 +- testscripts/test_file_sine.yml | 2 +- troubleshooting.md | 4 +- 23 files changed, 162 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03cf315..1deb094 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ New features: - Add a signal generator capture device. - Optionally write wav header when outputting to file or stdout. +- Add `WavFile` capture device type for reading wav files. Changes: +- Rename `File` capture device to `RawFile`. - Filter pipeline steps take a list of channels to filter instead of a single one. ## v2.0.3 diff --git a/README.md b/README.md index face709..a84af91 100644 --- a/README.md +++ b/README.md @@ -452,7 +452,7 @@ A flexible tool for processing audio Built with features: websocket Supported device types: -Capture: File, Stdin, Wasapi +Capture: RawFile, WavFile, Stdin, Wasapi Playback: File, Stdout, Wasapi USAGE: @@ -616,13 +616,16 @@ It is also possible to use pipes for apps that support reading or writing audio These backends are supported on all platforms. ### File or pipe -Audio can be read from a file or a pipe using the `File` device type. +Audio can be read from a file or a pipe using the `RawFile` device type. This can read raw interleaved samples in most common formats. To instead read from stdin, use the `Stdin` type. This makes it possible to pipe raw samples from some applications directly to CamillaDSP, without going via a virtual soundcard. +Wav files can be read using the `WavFile` device type. +See [the capture device section](#file-rawfile-wavfile-stdin-stdout) for more details. + ### Jack Jack is most commonly used with Linux, but can also be used with both Windows and MacOS. The Jack support of CamillaDSP version should be considered experimental. @@ -999,7 +1002,9 @@ A parameter marked (*) in any example is optional. If they are left out from the A device needs: * `type`: The available types depend on which features that were included when compiling. All possible types are: - * `File` + * `RawFile` (capture only) + * `WavFile` (capture only) + * `File` (playback only) * `SignalGenerator` (capture only) * `Stdin` (capture only) * `Stdout` (playback only) @@ -1010,9 +1015,9 @@ A parameter marked (*) in any example is optional. If they are left out from the * `Alsa` * `Pulse` * `channels`: number of channels - * `device`: device name (for Alsa, Pulse, Wasapi, CoreAudio). For CoreAudio and Wasapi, "default" will give the default device. - * `filename` path to the file (for File) - * `format`: sample format (for all except Jack). + * `device`: device name (for `Alsa`, `Pulse`, `Wasapi`, `CoreAudio`). For `CoreAudio` and `Wasapi`, `null` will give the default device. + * `filename` path to the file (for `File`, `RawFile` and `WavFile`) + * `format`: sample format (for all except `Jack`). Currently supported sample formats are signed little-endian integers of 16, 24 and 32 bits as well as floats of 32 and 64 bits: * S16LE - Signed 16-bit int, stored as two bytes @@ -1049,14 +1054,17 @@ A parameter marked (*) in any example is optional. If they are left out from the | FLOAT32LE | FLOAT_LE | FLOAT32LE | | FLOAT64LE | FLOAT64_LE | - | - ### File, Stdin, Stdout - The `File` device type reads or writes to a file, while `Stdin` reads from stdin and `Stdout` writes to stdout. + ### File, RawFile, WavFile, Stdin, Stdout + The `RawFile` device type reads from a file, while `Stdin` reads from stdin. + `File` and `Stdout` writes to a file and stdout, respectively. The format is raw interleaved samples, in the selected sample format. + If the capture device reaches the end of a file, the program will exit once all chunks have been played. That delayed sound that would end up in a later chunk will be cut off. To avoid this, set the optional parameter `extra_samples` for the File capture device. This causes the capture device to yield the given number of samples (per channel) after reaching end of file, allowing any delayed sound to be played back. + The `Stdin` capture device and `Stdout` playback device use stdin and stdout, so it's possible to easily pipe audio between applications: ``` @@ -1072,13 +1080,14 @@ A parameter marked (*) in any example is optional. If they are left out from the This is a _streaming_ header, meaning it contains a dummy value for the file length. Most applications ignore this and calculate the correct length from the file size. - Please note that the `File` capture device isn't able to read wav-files directly. - If you want to let CamillaDSP play wav-files, please see the [separate guide for converting wav to raw files](coefficients_from_wav.md). + To read from a wav file, use the `WavFile` capture device. + The samplerate and numnber of channels of the file is used to override the values in the config, + similar to how these values can be [overriden from the command line](#overriding-config-values). - Example config for File: - ``` + Example config for raw files: + ```yaml capture: - type: File + type: RawFile channels: 2 filename: "/path/to/inputfile.raw" format: S16LE @@ -1093,7 +1102,7 @@ A parameter marked (*) in any example is optional. If they are left out from the ``` Example config for Stdin/Stdout: - ``` + ```yaml capture: type: Stdin channels: 2 @@ -1107,9 +1116,24 @@ A parameter marked (*) in any example is optional. If they are left out from the format: S32LE ``` - The `File` and `Stdin` capture devices support two additional optional parameters, for advanced handling of raw files and testing: + Example config for wav input and output: + ```yaml + capture: + type: WavFile + filename: "/path/to/inputfile.wav" + playback: + type: File + channels: 2 + format: S32LE + wav_header: true + filename: "/path/to/outputfile.wav" + ``` + + + The `RawFile` and `Stdin` capture devices support two additional optional parameters, for advanced handling of raw files and testing: * `skip_bytes`: Number of bytes to skip at the beginning of the file or stream. - This can be used to skip over the header of some formats like .wav (which typically has a fixed size 44-byte header). + This can be used to skip over the header of some formats like .wav + (which often has a 44-byte header). Leaving it out or setting to zero means no bytes are skipped. * `read_bytes`: Read only up until the specified number of bytes. Leave it out or set it to zero to read until the end of the file or stream. diff --git a/exampleconfigs/all_biquads.yml b/exampleconfigs/all_biquads.yml index 5da91e5..053802c 100644 --- a/exampleconfigs/all_biquads.yml +++ b/exampleconfigs/all_biquads.yml @@ -3,7 +3,7 @@ devices: samplerate: 44100 chunksize: 1024 capture: - type: File + type: RawFile filename: "dummy" channels: 2 format: S16LE diff --git a/exampleconfigs/brokenconfig.yml b/exampleconfigs/brokenconfig.yml index 923351f..e5e69be 100644 --- a/exampleconfigs/brokenconfig.yml +++ b/exampleconfigs/brokenconfig.yml @@ -3,7 +3,7 @@ devices: samplerate: 44100 chunksize: 1024 capture: - type: File + type: RawFile filename: "dummy" channels: 2 format: S16LE diff --git a/exampleconfigs/ditherplay.yml b/exampleconfigs/ditherplay.yml index ab18c92..722c42c 100644 --- a/exampleconfigs/ditherplay.yml +++ b/exampleconfigs/ditherplay.yml @@ -3,7 +3,7 @@ devices: samplerate: 44100 chunksize: 4096 capture: - type: File + type: RawFile filename: "dummy" channels: 2 format: S16LE diff --git a/exampleconfigs/file_pb.yml b/exampleconfigs/file_pb.yml index cd43a66..296505c 100644 --- a/exampleconfigs/file_pb.yml +++ b/exampleconfigs/file_pb.yml @@ -5,7 +5,7 @@ devices: silence_threshold: -60 silence_timeout: 3.0 capture: - type: File + type: RawFile channels: 2 filename: "/home/henrik/test.raw" format: S16LE diff --git a/exampleconfigs/gainconfig.yml b/exampleconfigs/gainconfig.yml index 438256a..3067007 100644 --- a/exampleconfigs/gainconfig.yml +++ b/exampleconfigs/gainconfig.yml @@ -3,7 +3,7 @@ devices: samplerate: 44100 chunksize: 1024 capture: - type: File + type: RawFile filename: "dummy" channels: 2 format: S16LE diff --git a/exampleconfigs/lf_compressor.yml b/exampleconfigs/lf_compressor.yml index 5ecddab..c5adce0 100644 --- a/exampleconfigs/lf_compressor.yml +++ b/exampleconfigs/lf_compressor.yml @@ -4,7 +4,7 @@ devices: chunksize: 4096 resampler: null capture: - type: File + type: RawFile filename: "dummy" channels: 2 format: S16LE diff --git a/exampleconfigs/nofilters.yml b/exampleconfigs/nofilters.yml index 5532ba7..3ebdd37 100644 --- a/exampleconfigs/nofilters.yml +++ b/exampleconfigs/nofilters.yml @@ -3,7 +3,7 @@ devices: samplerate: 44100 chunksize: 1024 capture: - type: File + type: RawFile filename: "dummy" channels: 2 format: S16LE diff --git a/exampleconfigs/nomixers.yml b/exampleconfigs/nomixers.yml index 4a51b63..3aa1af2 100644 --- a/exampleconfigs/nomixers.yml +++ b/exampleconfigs/nomixers.yml @@ -3,7 +3,7 @@ devices: samplerate: 44100 chunksize: 1024 capture: - type: File + type: RawFile filename: "dummy" channels: 2 format: S16LE diff --git a/exampleconfigs/resample_file.yml b/exampleconfigs/resample_file.yml index 6c3a4a2..55b870d 100644 --- a/exampleconfigs/resample_file.yml +++ b/exampleconfigs/resample_file.yml @@ -7,7 +7,7 @@ devices: profile: Fast capture_samplerate: 44100 playback: - type: File + type: RawFile channels: 2 filename: "result_f64.raw" format: FLOAT64LE diff --git a/exampleconfigs/simpleconfig_plot.yml b/exampleconfigs/simpleconfig_plot.yml index f1ff838..fe6ac3f 100644 --- a/exampleconfigs/simpleconfig_plot.yml +++ b/exampleconfigs/simpleconfig_plot.yml @@ -3,7 +3,7 @@ devices: samplerate: 44100 chunksize: 1024 capture: - type: File + type: RawFile filename: "dummy" channels: 2 format: S16LE diff --git a/exampleconfigs/simpleconfig_resample.yml b/exampleconfigs/simpleconfig_resample.yml index 57a5196..8270a4b 100644 --- a/exampleconfigs/simpleconfig_resample.yml +++ b/exampleconfigs/simpleconfig_resample.yml @@ -7,7 +7,7 @@ devices: profile: Balanced capture_samplerate: 44100 capture: - type: File + type: RawFile filename: "dummy" channels: 2 format: S16LE diff --git a/exampleconfigs/tokens.yml b/exampleconfigs/tokens.yml index 567bc20..e92b5f4 100644 --- a/exampleconfigs/tokens.yml +++ b/exampleconfigs/tokens.yml @@ -3,7 +3,7 @@ devices: samplerate: 44100 chunksize: 1024 capture: - type: File + type: RawFile filename: "dummy" channels: 2 format: S16LE diff --git a/src/audiodevice.rs b/src/audiodevice.rs index a6b0eb8..a99b3aa 100644 --- a/src/audiodevice.rs +++ b/src/audiodevice.rs @@ -578,14 +578,14 @@ pub fn new_capture_device(conf: config::Devices) -> Box { silence_threshold: conf.silence_threshold(), silence_timeout: conf.silence_timeout(), }), - config::CaptureDevice::File(ref dev) => Box::new(filedevice::FileCaptureDevice { + config::CaptureDevice::RawFile(ref dev) => Box::new(filedevice::FileCaptureDevice { source: filedevice::CaptureSource::Filename(dev.filename.clone()), samplerate: conf.samplerate, capture_samplerate, resampler_config: conf.resampler, chunksize: conf.chunksize, channels: dev.channels, - sample_format: dev.format, + sample_format: Some(dev.format), extra_samples: dev.extra_samples(), silence_threshold: conf.silence_threshold(), silence_timeout: conf.silence_timeout(), @@ -594,6 +594,22 @@ pub fn new_capture_device(conf: config::Devices) -> Box { stop_on_rate_change: conf.stop_on_rate_change(), rate_measure_interval: conf.rate_measure_interval(), }), + config::CaptureDevice::WavFile(ref dev) => Box::new(filedevice::FileCaptureDevice { + source: filedevice::CaptureSource::Filename(dev.filename.clone()), + samplerate: conf.samplerate, + capture_samplerate, + resampler_config: conf.resampler, + chunksize: conf.chunksize, + channels: 0, + sample_format: None, + extra_samples: dev.extra_samples(), + silence_threshold: conf.silence_threshold(), + silence_timeout: conf.silence_timeout(), + skip_bytes: 0, + read_bytes: 0, + stop_on_rate_change: conf.stop_on_rate_change(), + rate_measure_interval: conf.rate_measure_interval(), + }), config::CaptureDevice::Stdin(ref dev) => Box::new(filedevice::FileCaptureDevice { source: filedevice::CaptureSource::Stdin, samplerate: conf.samplerate, diff --git a/src/config.rs b/src/config.rs index b88e15c..6a0471b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ use crate::compressor; use crate::filters; use crate::mixer; -use crate::wavtools::find_data_in_wav_stream; +use crate::wavtools::{find_data_in_wav_stream, WavParams}; use parking_lot::RwLock; use serde::{de, Deserialize, Serialize}; //use serde_with; @@ -17,6 +17,7 @@ use std::path::{Path, PathBuf}; use crate::PrcFmt; type Res = Result>; +#[derive(Clone)] pub struct Overrides { pub samplerate: Option, pub sample_format: Option, @@ -203,8 +204,8 @@ pub enum CaptureDevice { device: String, format: SampleFormat, }, - #[serde(alias = "FILE", alias = "file")] - File(CaptureDeviceFile), + RawFile(CaptureDeviceRawFile), + WavFile(CaptureDeviceWavFile), #[serde(alias = "STDIN", alias = "stdin")] Stdin(CaptureDeviceStdin), #[cfg(target_os = "macos")] @@ -245,7 +246,10 @@ impl CaptureDevice { CaptureDevice::Bluez(dev) => dev.channels, #[cfg(feature = "pulse-backend")] CaptureDevice::Pulse { channels, .. } => *channels, - CaptureDevice::File(dev) => dev.channels, + CaptureDevice::RawFile(dev) => dev.channels, + CaptureDevice::WavFile(dev) => { + dev.wav_info().map(|info| info.channels).unwrap_or_default() + } CaptureDevice::Stdin(dev) => dev.channels, #[cfg(target_os = "macos")] CaptureDevice::CoreAudio(dev) => dev.channels, @@ -269,12 +273,11 @@ impl CaptureDevice { #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(deny_unknown_fields)] -pub struct CaptureDeviceFile { +pub struct CaptureDeviceRawFile { #[serde(deserialize_with = "validate_nonzero_usize")] pub channels: usize, pub filename: String, - #[serde(default)] - pub format: Option, + pub format: SampleFormat, #[serde(default)] pub extra_samples: Option, #[serde(default)] @@ -283,7 +286,7 @@ pub struct CaptureDeviceFile { pub read_bytes: Option, } -impl CaptureDeviceFile { +impl CaptureDeviceRawFile { pub fn extra_samples(&self) -> usize { self.extra_samples.unwrap_or_default() } @@ -295,6 +298,33 @@ impl CaptureDeviceFile { } } +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct CaptureDeviceWavFile { + pub filename: String, + #[serde(default)] + pub extra_samples: Option, +} + +impl CaptureDeviceWavFile { + pub fn extra_samples(&self) -> usize { + self.extra_samples.unwrap_or_default() + } + + pub fn wav_info(&self) -> Res { + let fname = &self.filename; + let f = match File::open(fname) { + Ok(f) => f, + Err(err) => { + let msg = format!("Could not open input file '{fname}'. Reason: {err}"); + return Err(ConfigError::new(&msg).into()); + } + }; + let file = BufReader::new(&f); + find_data_in_wav_stream(file) + } +} + #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(deny_unknown_fields)] pub struct CaptureDeviceStdin { @@ -1317,7 +1347,7 @@ pub fn load_config(filename: &str) -> Res { let file = match File::open(filename) { Ok(f) => f, Err(err) => { - let msg = format!("Could not open config file '{filename}'. Error: {err}"); + let msg = format!("Could not open config file '{filename}'. Reason: {err}"); return Err(ConfigError::new(&msg).into()); } }; @@ -1326,7 +1356,7 @@ pub fn load_config(filename: &str) -> Res { let _number_of_bytes: usize = match buffered_reader.read_to_string(&mut contents) { Ok(number_of_bytes) => number_of_bytes, Err(err) => { - let msg = format!("Could not read config file '{filename}'. Error: {err}"); + let msg = format!("Could not read config file '{filename}'. Reason: {err}"); return Err(ConfigError::new(&msg).into()); } }; @@ -1337,16 +1367,24 @@ pub fn load_config(filename: &str) -> Res { return Err(ConfigError::new(&msg).into()); } }; - //Ok(configuration) - //apply_overrides(&mut configuration); - //replace_tokens_in_config(&mut configuration); - //replace_relative_paths_in_config(&mut configuration, filename); Ok(configuration) } fn apply_overrides(configuration: &mut Configuration) { - // TODO update to get overrides from wav capture file - let overrides = OVERRIDES.read(); + let mut overrides = OVERRIDES.read().clone(); + // Only one match arm for now, might be more later. + #[allow(clippy::single_match)] + match &configuration.devices.capture { + CaptureDevice::WavFile(dev) => { + if let Ok(wav_info) = dev.wav_info() { + overrides.channels = Some(wav_info.channels); + overrides.sample_format = Some(wav_info.sample_format); + overrides.samplerate = Some(wav_info.sample_rate); + debug!("Updating overrides with values from wav input file, rate {}, format: {}, channels: {}", wav_info.sample_rate, wav_info.sample_format, wav_info.channels); + } + } + _ => {} + } if let Some(rate) = overrides.samplerate { let cfg_rate = configuration.devices.samplerate; let cfg_chunksize = configuration.devices.chunksize; @@ -1366,7 +1404,7 @@ fn apply_overrides(configuration: &mut Configuration) { configuration.devices.chunksize = scaled_chunksize; #[allow(unreachable_patterns)] match &mut configuration.devices.capture { - CaptureDevice::File(dev) => { + CaptureDevice::RawFile(dev) => { let new_extra = dev.extra_samples() * rate / cfg_rate; debug!( "Scale extra samples: {} -> {}", @@ -1399,7 +1437,7 @@ fn apply_overrides(configuration: &mut Configuration) { debug!("Apply override for extra_samples: {}", extra); #[allow(unreachable_patterns)] match &mut configuration.devices.capture { - CaptureDevice::File(dev) => { + CaptureDevice::RawFile(dev) => { dev.extra_samples = Some(extra); } CaptureDevice::Stdin(dev) => { @@ -1411,9 +1449,10 @@ fn apply_overrides(configuration: &mut Configuration) { if let Some(chans) = overrides.channels { debug!("Apply override for capture channels: {}", chans); match &mut configuration.devices.capture { - CaptureDevice::File(dev) => { + CaptureDevice::RawFile(dev) => { dev.channels = chans; } + CaptureDevice::WavFile(_dev) => {} CaptureDevice::Stdin(dev) => { dev.channels = chans; } @@ -1458,10 +1497,10 @@ fn apply_overrides(configuration: &mut Configuration) { if let Some(fmt) = overrides.sample_format { debug!("Apply override for capture sample format: {}", fmt); match &mut configuration.devices.capture { - CaptureDevice::File(dev) => { - // TODO dont update if null! - dev.format = Some(fmt); + CaptureDevice::RawFile(dev) => { + dev.format = fmt; } + CaptureDevice::WavFile(_dev) => {} CaptureDevice::Stdin(dev) => { dev.format = fmt; } @@ -1807,28 +1846,30 @@ pub fn validate_config(conf: &mut Configuration, filename: Option<&str>) -> Res< .into()); } } - if let CaptureDevice::File(dev) = &conf.devices.capture { + if let CaptureDevice::RawFile(dev) = &conf.devices.capture { let fname = &dev.filename; - let f = match File::open(fname) { + match File::open(fname) { Ok(f) => f, Err(err) => { - let msg = format!("Could not open input file '{fname}'. Error: {err}"); + let msg = format!("Could not open input file '{fname}'. Reason: {err}"); return Err(ConfigError::new(&msg).into()); } }; - if dev.format.is_none() { - let file = BufReader::new(&f); - let wav_info = find_data_in_wav_stream(file)?; - // TODO warn if samplerate is wrong - // TODO 2, next step: update overrides for channels and sample rate - if wav_info.channels != dev.channels { - let msg = format!( - "Wav file '{}' has wrong number of channels, expected: {}, found: {}", - fname, dev.channels, wav_info.channels - ); + } + if let CaptureDevice::WavFile(dev) = &conf.devices.capture { + let fname = &dev.filename; + let f = match File::open(fname) { + Ok(f) => f, + Err(err) => { + let msg = format!("Could not open input file '{fname}'. Reason: {err}"); return Err(ConfigError::new(&msg).into()); } - } + }; + let file = BufReader::new(&f); + let _wav_info = find_data_in_wav_stream(file).map_err(|err| { + let msg = format!("Error reading wav file '{fname}'. Reason: {err}"); + ConfigError::new(&msg) + })?; } let mut num_channels = conf.devices.capture.channels(); let fs = conf.devices.samplerate; diff --git a/src/filedevice.rs b/src/filedevice.rs index f78e14e..718a627 100644 --- a/src/filedevice.rs +++ b/src/filedevice.rs @@ -553,7 +553,7 @@ impl CaptureDevice for FileCaptureDevice { let samplerate = self.samplerate; let chunksize = self.chunksize; let capture_samplerate = self.capture_samplerate; - let channels = self.channels; + let mut channels = self.channels; let mut skip_bytes = self.skip_bytes; let mut read_bytes = self.read_bytes; let sample_format = match &self.source { @@ -561,10 +561,11 @@ impl CaptureDevice for FileCaptureDevice { if self.sample_format.is_none() { // No format was given, try to get from the file. // Only works if the file is in wav format. + // Also update channels and read & skip bytes. let wav_info = find_data_in_wav(fname)?; skip_bytes = wav_info.data_offset; read_bytes = wav_info.data_length; - // TODO check channels? + channels = wav_info.channels; wav_info.sample_format } else { self.sample_format.unwrap() diff --git a/src/filters.rs b/src/filters.rs index 4284185..e08666b 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -67,7 +67,7 @@ pub fn read_coeff_file( let f = match File::open(filename) { Ok(f) => f, Err(err) => { - let msg = format!("Could not open coefficient file '{filename}'. Error: {err}"); + let msg = format!("Could not open coefficient file '{filename}'. Reason: {err}"); return Err(config::ConfigError::new(&msg).into()); } }; @@ -90,7 +90,7 @@ pub fn read_coeff_file( match line { Err(err) => { let msg = format!( - "Can't read line {} of file '{}'. Error: {}", + "Can't read line {} of file '{}'. Reason: {}", nbr + 1 + skip_bytes_lines, filename, err @@ -101,7 +101,7 @@ pub fn read_coeff_file( Ok(val) => coefficients.push(val), Err(err) => { let msg = format!( - "Can't parse value on line {} of file '{}'. Error: {}", + "Can't parse value on line {} of file '{}'. Reason: {}", nbr + 1 + skip_bytes_lines, filename, err diff --git a/src/lib.rs b/src/lib.rs index 6f16697..75e0d28 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -350,7 +350,8 @@ impl fmt::Display for ProcessingState { pub fn list_supported_devices() -> (Vec, Vec) { let mut playbacktypes = vec!["File".to_owned(), "Stdout".to_owned()]; let mut capturetypes = vec![ - "File".to_owned(), + "RawFile".to_owned(), + "WavFile".to_owned(), "Stdin".to_owned(), "SignalGenerator".to_owned(), ]; diff --git a/src/wavtools.rs b/src/wavtools.rs index 81e49cd..60e7fbe 100644 --- a/src/wavtools.rs +++ b/src/wavtools.rs @@ -158,7 +158,7 @@ pub fn find_data_in_wav_stream(mut f: impl Read + Seek) -> Res { // Bytes 8 to 12 must be WAVE let wave_err = !compare_4cc(&header[8..], WAVE); if riff_err || wave_err { - return Err(ConfigError::new("Invalid header").into()); + return Err(ConfigError::new("Invalid wav header").into()); } let mut next_chunk_location = 12; diff --git a/testscripts/test_file.yml b/testscripts/test_file.yml index 20e9d3a..356b749 100644 --- a/testscripts/test_file.yml +++ b/testscripts/test_file.yml @@ -5,7 +5,7 @@ devices: target_level: 512 adjust_period: 10 playback: - type: File + type: RawFile channels: 2 filename: "result_i32.raw" format: S32LE diff --git a/testscripts/test_file_sine.yml b/testscripts/test_file_sine.yml index 8866dee..e2f05de 100644 --- a/testscripts/test_file_sine.yml +++ b/testscripts/test_file_sine.yml @@ -3,7 +3,7 @@ devices: samplerate: 44100 chunksize: 1024 playback: - type: File + type: RawFile channels: 2 filename: "result_f64.raw" format: FLOAT64LE diff --git a/troubleshooting.md b/troubleshooting.md index c97fcc6..9c3a52f 100644 --- a/troubleshooting.md +++ b/troubleshooting.md @@ -58,11 +58,11 @@ The coefficient file for a filter was found to be empty. -- Could not open coefficient file '*examplefile.raw*'. Error: *description from OS* +- Could not open coefficient file '*examplefile.raw*'. Reason: *description from OS* The specified file could not be opened. The description from the OS may give more info. -- Can't parse value on line *X* of file '*examplefile.txt*'. Error: *description from parser* +- Can't parse value on line *X* of file '*examplefile.txt*'. Reason: *description from parser* The value on the specified line could not be parsed as a number. Check that the file only contains numbers. From 3a5afeadb87630fc9702a1b262b155470ceea050 Mon Sep 17 00:00:00 2001 From: Henrik Date: Thu, 14 Mar 2024 22:47:37 +0100 Subject: [PATCH 024/135] Change sample format type also for bluez --- src/audiodevice.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/audiodevice.rs b/src/audiodevice.rs index a99b3aa..250aae4 100644 --- a/src/audiodevice.rs +++ b/src/audiodevice.rs @@ -642,7 +642,7 @@ pub fn new_capture_device(conf: config::Devices) -> Box { resampler_config: conf.resampler, chunksize: conf.chunksize, channels: dev.channels, - sample_format: dev.format, + sample_format: Some(dev.format), extra_samples: 0, silence_threshold: conf.silence_threshold(), silence_timeout: conf.silence_timeout(), From 387c1b298cf8e79a89658e5885eaa631a3efcbf2 Mon Sep 17 00:00:00 2001 From: Henrik Date: Sun, 17 Mar 2024 22:18:31 +0100 Subject: [PATCH 025/135] Mention no wav from pipe --- README.md | 1 + src/filedevice.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a84af91..64a2288 100644 --- a/README.md +++ b/README.md @@ -1083,6 +1083,7 @@ A parameter marked (*) in any example is optional. If they are left out from the To read from a wav file, use the `WavFile` capture device. The samplerate and numnber of channels of the file is used to override the values in the config, similar to how these values can be [overriden from the command line](#overriding-config-values). + Note that `WavFile` only supports reading from files. Reading from a pipe is not supported. Example config for raw files: ```yaml diff --git a/src/filedevice.rs b/src/filedevice.rs index 718a627..8e09d3e 100644 --- a/src/filedevice.rs +++ b/src/filedevice.rs @@ -266,7 +266,7 @@ fn capture_loop( msg_channels: CaptureChannels, mut resampler: Option>>, ) { - debug!("starting captureloop"); + debug!("preparing captureloop"); let chunksize_bytes = params.channels * params.chunksize * params.store_bytes_per_sample; let bytes_per_frame = params.channels * params.store_bytes_per_sample; let mut buf = vec![0u8; params.buffer_bytes]; From 57ba34b59d729cea244aca9df8d9e9cc175a17d8 Mon Sep 17 00:00:00 2001 From: Henrik Date: Mon, 1 Apr 2024 22:59:47 +0200 Subject: [PATCH 026/135] Add limit for volume controls --- CHANGELOG.md | 1 + Cargo.toml | 2 +- README.md | 13 +++++++++++-- src/basicfilters.rs | 32 ++++++++++++++++++++++++++------ src/config.rs | 17 +++++++++++++++++ src/filters.rs | 1 + 6 files changed, 57 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1deb094..cdfc665 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ New features: - Add a signal generator capture device. - Optionally write wav header when outputting to file or stdout. - Add `WavFile` capture device type for reading wav files. +- Optional limit for volume controls. Changes: - Rename `File` capture device to `RawFile`. - Filter pipeline steps take a list of channels to filter instead of a single one. diff --git a/Cargo.toml b/Cargo.toml index bb6630f..f075203 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "camilladsp" -version = "2.0.3" +version = "3.0.0" authors = ["Henrik Enquist "] edition = "2021" description = "A flexible tool for processing audio" diff --git a/README.md b/README.md index 64a2288..c91e6f8 100644 --- a/README.md +++ b/README.md @@ -854,13 +854,16 @@ The `title` property is intended for a short title, while `description` can be l ## Volume control There is a volume control that is enabled regardless of what configuration file is loaded. -CamillaDSP defines a total of five control "channels" for volume. -The default volume control reacts to the `Main` control channel. +CamillaDSP defines a total of five control "channels" for volume called "faders". +The default volume control reacts to the `Main` fader. When the volume or mute setting is changed, the gain is smoothly ramped to the new setting. The duration of this ramp can be customized via the `volume_ramp_time` parameter in the `devices` section. The value must not be negative. If left out or set to `null`, it defaults to 400 ms. +The range of the volume control can be limited. +Set the `volume_limit` to the desired maximum volume value. +This setting is optional. If left out or set to `null`, it defaults to +50 dB. In addition to this, there are four additional control channels, named `Aux1` to `Aux4`. These can be used to implement for example a separate volume control for a headphone output, @@ -883,6 +886,7 @@ devices: stop_on_rate_change: false (*) rate_measure_interval: 1.0 (*) volume_ramp_time: 400.0 (*) + volume_limit: -12.0 (*) capture: type: Pulse channels: 2 @@ -1517,6 +1521,10 @@ The duration of this ramp is set by the `ramp_time` parameter (unit milliseconds The value must not be negative. If left out or set to `null`, it defaults to 400 ms. The value will be rounded to the nearest number of chunks. +The range of the volume control can be limited via the optional `limit` parameter. +This sets a limit for the maximum value of the volume. +If left out or set to `null`, it defaults to +50 dB. + Example Volume filter: ``` filters: @@ -1524,6 +1532,7 @@ filters: type: Volume parameters: ramp_time: 200 (*) + limit: 10.0 (*) fader: Aux1 ``` diff --git a/src/basicfilters.rs b/src/basicfilters.rs index 67e8325..f1fc2b0 100644 --- a/src/basicfilters.rs +++ b/src/basicfilters.rs @@ -38,6 +38,7 @@ pub struct Volume { chunksize: usize, processing_params: Arc, fader: usize, + volume_limit: f32, } impl Volume { @@ -45,6 +46,7 @@ impl Volume { pub fn new( name: &str, ramp_time_ms: f32, + limit: f32, current_volume: f32, mute: bool, chunksize: usize, @@ -75,6 +77,7 @@ impl Volume { chunksize, processing_params, fader, + volume_limit: limit } } @@ -91,6 +94,7 @@ impl Volume { Self::new( name, conf.ramp_time(), + conf.limit(), current_volume, mute, chunksize, @@ -126,13 +130,16 @@ impl Volume { let shared_vol = self.processing_params.target_volume(self.fader); let shared_mute = self.processing_params.is_mute(self.fader); + // are we above the set limit? + let target_volume = shared_vol.min(self.volume_limit); + // Volume setting changed - if (shared_vol - self.target_volume).abs() > 0.01 || self.mute != shared_mute { + if (target_volume - self.target_volume).abs() > 0.01 || self.mute != shared_mute { if self.ramptime_in_chunks > 0 { trace!( "starting ramp: {} -> {}, mute: {}", self.current_volume, - shared_vol, + target_volume, shared_mute ); self.ramp_start = self.current_volume; @@ -141,22 +148,22 @@ impl Volume { trace!( "switch volume without ramp: {} -> {}, mute: {}", self.current_volume, - shared_vol, + target_volume, shared_mute ); self.current_volume = if shared_mute { 0.0 } else { - shared_vol as PrcFmt + target_volume as PrcFmt }; self.ramp_step = 0; } - self.target_volume = shared_vol; + self.target_volume = target_volume; self.target_linear_gain = if shared_mute { 0.0 } else { let tempgain: PrcFmt = 10.0; - tempgain.powf(shared_vol as PrcFmt / 20.0) + tempgain.powf(target_volume as PrcFmt / 20.0) }; self.mute = shared_mute; } @@ -240,6 +247,19 @@ impl Filter for Volume { / (1000.0 * self.chunksize as f32 / self.samplerate as f32)) .round() as usize; self.fader = conf.fader as usize; + self.volume_limit = conf.limit(); + if (self.volume_limit as PrcFmt) < self.current_volume { + self.current_volume = self.volume_limit as PrcFmt; + } + if self.volume_limit < self.target_volume { + self.target_volume = self.volume_limit; + self.target_linear_gain = if self.mute { + 0.0 + } else { + let tempgain: PrcFmt = 10.0; + tempgain.powf(self.target_volume as PrcFmt / 20.0) + }; + } } else { // This should never happen unless there is a bug somewhere else panic!("Invalid config change!"); diff --git a/src/config.rs b/src/config.rs index 6a0471b..86e5dd6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -565,6 +565,8 @@ pub struct Devices { pub rate_measure_interval: Option, #[serde(default)] pub volume_ramp_time: Option, + #[serde(default)] + pub volume_limit: Option, } // Getters for all the defaults @@ -608,6 +610,10 @@ impl Devices { pub fn ramp_time(&self) -> f32 { self.volume_ramp_time.unwrap_or(400.0) } + + pub fn volume_limit(&self) -> f32 { + self.volume_limit.unwrap_or(50.0) + } } #[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)] @@ -965,12 +971,17 @@ pub struct VolumeParameters { #[serde(default)] pub ramp_time: Option, pub fader: VolumeFader, + pub limit: Option, } impl VolumeParameters { pub fn ramp_time(&self) -> f32 { self.ramp_time.unwrap_or(400.0) } + + pub fn limit(&self) -> f32 { + self.limit.unwrap_or(50.0) + } } #[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)] @@ -1765,6 +1776,12 @@ pub fn validate_config(conf: &mut Configuration, filename: Option<&str>) -> Res< if conf.devices.ramp_time() < 0.0 { return Err(ConfigError::new("Volume ramp time cannot be negative").into()); } + if conf.devices.volume_limit() > 50.0 { + return Err(ConfigError::new("Volume limit cannot be above +50 dB").into()); + } + if conf.devices.volume_limit() < -150.0 { + return Err(ConfigError::new("Volume limit cannot be less than -150 dB").into()); + } #[cfg(target_os = "windows")] if let CaptureDevice::Wasapi(dev) = &conf.devices.capture { if dev.format == SampleFormat::FLOAT64LE { diff --git a/src/filters.rs b/src/filters.rs index e08666b..633e4ac 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -382,6 +382,7 @@ impl Pipeline { let volume = basicfilters::Volume::new( "default", conf.devices.ramp_time(), + conf.devices.volume_limit(), current_volume, mute, conf.devices.chunksize, From 9a644aa7c60bf16f4a7ad258c1e5fe577c86c726 Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 2 Apr 2024 20:38:47 +0200 Subject: [PATCH 027/135] Simplify volume limit update --- src/basicfilters.rs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/basicfilters.rs b/src/basicfilters.rs index f1fc2b0..7a65363 100644 --- a/src/basicfilters.rs +++ b/src/basicfilters.rs @@ -77,7 +77,7 @@ impl Volume { chunksize, processing_params, fader, - volume_limit: limit + volume_limit: limit, } } @@ -131,7 +131,7 @@ impl Volume { let shared_mute = self.processing_params.is_mute(self.fader); // are we above the set limit? - let target_volume = shared_vol.min(self.volume_limit); + let target_volume = shared_vol.min(self.volume_limit); // Volume setting changed if (target_volume - self.target_volume).abs() > 0.01 || self.mute != shared_mute { @@ -251,15 +251,6 @@ impl Filter for Volume { if (self.volume_limit as PrcFmt) < self.current_volume { self.current_volume = self.volume_limit as PrcFmt; } - if self.volume_limit < self.target_volume { - self.target_volume = self.volume_limit; - self.target_linear_gain = if self.mute { - 0.0 - } else { - let tempgain: PrcFmt = 10.0; - tempgain.powf(self.target_volume as PrcFmt / 20.0) - }; - } } else { // This should never happen unless there is a bug somewhere else panic!("Invalid config change!"); From 9636b71819d6c9fee09e08b305267bdd442f6dee Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 2 Apr 2024 21:22:09 +0200 Subject: [PATCH 028/135] Look up device id by name and scope --- CHANGELOG.md | 2 ++ src/coreaudiodevice.rs | 30 +++++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdfc665..6bac197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ New features: Changes: - Rename `File` capture device to `RawFile`. - Filter pipeline steps take a list of channels to filter instead of a single one. +Bugfixes: +- MacOS: Support devices appearing as separate capture and playback devices. ## v2.0.3 Bugfixes: diff --git a/src/coreaudiodevice.rs b/src/coreaudiodevice.rs index 8e093fa..ee2ef37 100644 --- a/src/coreaudiodevice.rs +++ b/src/coreaudiodevice.rs @@ -21,7 +21,7 @@ use std::time::Duration; use coreaudio::audio_unit::audio_format::LinearPcmFlags; use coreaudio::audio_unit::macos_helpers::{ audio_unit_from_device_id, find_matching_physical_format, get_audio_device_ids, - get_default_device_id, get_device_id_from_name, get_device_name, get_hogging_pid, + get_audio_device_supports_scope, get_default_device_id, get_device_name, get_hogging_pid, get_supported_physical_stream_formats, set_device_physical_stream_format, set_device_sample_rate, toggle_hog_mode, AliveListener, RateListener, }; @@ -62,6 +62,7 @@ fn take_ownership(device_id: AudioDeviceID) -> Res { } fn release_ownership(device_id: AudioDeviceID) -> Res<()> { + trace!("Releasing any device ownership for device id {}", device_id); let device_owner_pid = match get_hogging_pid(device_id) { Ok(pid) => pid, Err(CoreAudioError::AudioCodec(AudioCodecError::UnknownProperty)) => return Ok(()), @@ -84,6 +85,26 @@ fn release_ownership(device_id: AudioDeviceID) -> Res<()> { Ok(()) } +/// Find the device id for a device name and scope (input or output). +/// Some devices are listed as two device ids with the same name, +/// where one supports playback and the other capture. +pub fn get_device_id_from_name_and_scope(name: &str, input: bool) -> Option { + let scope = match input { + false => Scope::Output, + true => Scope::Input, + }; + if let Ok(all_ids) = get_audio_device_ids() { + return all_ids + .iter() + .find(|id| { + get_device_name(**id).unwrap_or_default() == name + && get_audio_device_supports_scope(**id, scope).unwrap_or_default() + }) + .copied(); + } + None +} + #[derive(Clone, Debug)] pub struct CoreaudioPlaybackDevice { pub devname: Option, @@ -171,7 +192,7 @@ fn open_coreaudio_playback( ) -> Res<(AudioUnit, AudioDeviceID)> { let device_id = if let Some(name) = devname { trace!("Available playback devices: {:?}", list_device_names(false)); - match get_device_id_from_name(name) { + match get_device_id_from_name_and_scope(name, false) { Some(dev) => dev, None => { let msg = format!("Could not find playback device '{name}'"); @@ -187,10 +208,12 @@ fn open_coreaudio_playback( } } }; + trace!("Playback device id: {}", device_id); let mut audio_unit = audio_unit_from_device_id(device_id, false) .map_err(|e| ConfigError::new(&format!("{e}")))?; + trace!("Created playback audio unit"); if exclusive { take_ownership(device_id)?; } else { @@ -230,6 +253,7 @@ fn open_coreaudio_playback( return Err(ConfigError::new(msg).into()); } } else { + trace!("Set playback device sample rate"); set_device_sample_rate(device_id, samplerate as f64) .map_err(|e| ConfigError::new(&format!("{e}")))?; } @@ -259,7 +283,7 @@ fn open_coreaudio_capture( ) -> Res<(AudioUnit, AudioDeviceID)> { let device_id = if let Some(name) = devname { debug!("Available capture devices: {:?}", list_device_names(true)); - match get_device_id_from_name(name) { + match get_device_id_from_name_and_scope(name, true) { Some(dev) => dev, None => { let msg = format!("Could not find capture device '{name}'"); From 3460087823110d8902ab44353abeb041e6ab6f3e Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 2 Apr 2024 22:29:07 +0200 Subject: [PATCH 029/135] Fix readme for default devices, clean up --- README.md | 24 -- backend_coreaudio.md | 51 ++-- backend_wasapi.md | 57 +++-- show_config.py | 564 ------------------------------------------- 4 files changed, 75 insertions(+), 621 deletions(-) delete mode 100644 show_config.py diff --git a/README.md b/README.md index c91e6f8..1c05db4 100644 --- a/README.md +++ b/README.md @@ -2199,30 +2199,6 @@ REW V5.20.14 and later is able to export the filters in the CamillaDSP YAML form Note that the generated YAML file is not a complete CamillaDSP configuration. It contains only filter definitions and pipeline steps, that can be pasted into a CamillaDSP config file. -## Visualizing the config -__Please note that the `show_config.py` script mentioned here is deprecated, and has been replaced by the `plotcamillaconf` tool from the pycamilladsp-plot library.__ -The new tool provides the same functionality as well as many improvements. -The `show_config.py` does not support any of the newer config options, and the script will be removed in a future version. - -A Python script is included to view the configuration. -This plots the transfer functions of all included filters, as well as plots a flowchart of the entire processing pipeline. Run it with: -``` -python show_config.py /path/to/config.yml -``` - -Example flowchart: - -![Example](pipeline.png) - -Note that the script assumes a valid configuration file and will not give any helpful error messages if it's not, -so it's a good idea to first use CamillaDSP to validate the file. -The script requires the following: -* Python 3 -* Numpy -* Matplotlib -* PyYAML - - # Related projects Other projects using CamillaDSP: * https://github.com/scripple/alsa_cdsp - ALSA CamillaDSP "I/O" plugin, automatic config updates at changes of samplerate, sample format or number of channels. diff --git a/backend_coreaudio.md b/backend_coreaudio.md index e916071..8d7a8c4 100644 --- a/backend_coreaudio.md +++ b/backend_coreaudio.md @@ -1,33 +1,46 @@ # CoreAudio (macOS) ## Introduction -CoreAudio is the standard audio API of macOS. -The CoreAudio support of CamillaDSP is provided by [an updated and extended fork](https://github.com/HEnquist/coreaudio-rs) of the [coreaudio-rs library](https://github.com/RustAudio/coreaudio-rs). +CoreAudio is the standard audio API of macOS. +The CoreAudio support of CamillaDSP is provided via the [coreaudio-rs library](https://github.com/RustAudio/coreaudio-rs). -CoreAudio is a large API that offers several ways to accomplish most common tasks. CamillaDSP uses the low-level AudioUnits for playback and capture. An AudioUnit that represents a hardware device has two stream formats. One format is used for communicating with the application. This is typically 32-bit float, the same format that CoreAudio uses internally. The other format (called the physical format) is the one used to send or receive data to/from the sound card driver. +CoreAudio is a large API that offers several ways to accomplish most common tasks. +CamillaDSP uses the low-level AudioUnits for playback and capture. +An AudioUnit that represents a hardware device has two stream formats. +One format is used for communicating with the application. +This is typically 32-bit float, the same format that CoreAudio uses internally. +The other format (called the physical format) is the one used to send or receive data to/from the sound card driver. ## Capturing audio from other applications To capture audio from applications a virtual sound card is needed. It is recommended to use [BlackHole](https://github.com/ExistentialAudio/BlackHole). This works on both Intel and Apple Silicon macs. -The latest (currently unreleased) version of BlachHole supports adjusting the rate of the virtual clock. +Since version 0.5.0 Blackhole supports adjusting the rate of the virtual clock. This makes it possible to sync the virtual device with a real device, and avoid the need for asynchronous resampling. CamillaDSP supports and will use this functionality when it is available. An alternative is [Soundflower](https://github.com/mattingalls/Soundflower), which is older and only supports Intel macs. -Some player applications can use hog mode to get exclusive access to the playback device. Using this with a virtual soundcard like BlackHole causes problems, and is therefore not recommended. +Some player applications can use hog mode to get exclusive access to the playback device. +Using this with a virtual soundcard like BlackHole causes problems, and is therefore not recommended. ### Sending all audio to the virtual card -Set the virtual sound card as the default playback device in the Sound preferences. This will work for all applications that respect this setting, which in practice is nearly all. The exceptions are the ones that provide their own way of selecting playback device. +Set the virtual sound card as the default playback device in the Sound preferences. +This will work for all applications that respect this setting, which in practice is nearly all. +The exceptions are the ones that provide their own way of selecting playback device. ### Capturing the audio When applications output their audio to the playback side of the virtual soundcard, then this audio can be captured from the capture side. This is done by giving the virtual soundcard as the capture device in the CamillaDSP configuration. ### Sample rate change notifications -CamillaDSP will listen for notifications from CoreAudio. If the sample rate of the capture device changes, then CoreAudio will stop providing new samples to any client currently capturing from it. To continue from this state, the capture device needs to be closed and reopened. For CamillaDSP this means that the configuration must be reloaded. If the capture device sample rate changes, then CamillaDSP will stop. Reading the "StopReason" via the websocket server tells that this was due to a sample rate change, and give the value for the new sample rate. +CamillaDSP will listen for notifications from CoreAudio. +If the sample rate of the capture device changes, then CoreAudio will stop providing new samples to any client currently capturing from it. +To continue from this state, the capture device needs to be closed and reopened. +For CamillaDSP this means that the configuration must be reloaded. +If the capture device sample rate changes, then CamillaDSP will stop. +Reading the "StopReason" via the websocket server tells that this was due to a sample rate change, and give the value for the new sample rate. ## Configuration of devices @@ -36,38 +49,40 @@ This example configuration will be used to explain the various options specific capture: type: CoreAudio channels: 2 - device: "Soundflower (2ch)" + device: "Soundflower (2ch)" (*) format: S32LE (*) playback: type: CoreAudio channels: 2 - device: "Built-in Output" + device: "Built-in Output" (*) format: S24LE (*) exclusive: false (*) ``` The parameters marked (*) are optional. ### Device names -The device names that are used for `device:` for both playback and capture are entered as shown in the "Audio MIDI Setup" that can be found under "Other" in Launchpad. +The device names that are used for `device` for both playback and capture are entered as shown in the "Audio MIDI Setup" that can be found under "Other" in Launchpad. The name for the 2-channel interface of Soundflower is "Soundflower (2ch)", and the built in audio in a MacBook Pro is called "Built-in Output". -Specifying "default" will give the default capture or playback device. +Specifying `null` or leaving out `device` will give the default capture or playback device. To help with finding the name of playback and capture devices, use the macOS version of "cpal-listdevices" program from here: https://github.com/HEnquist/cpal-listdevices/releases Just download the binary and run it in a terminal. It will list all devices with the names. ### Sample format -CamillaDSP always uses 32-bit float uses when transferring data to and from CoreAudio. The conversion from 32-bit float to the sample format used by the actual DAC (the physical format) is performed by CoreAudio. +CamillaDSP always uses 32-bit float uses when transferring data to and from CoreAudio. +The conversion from 32-bit float to the sample format used by the actual DAC (the physical format) is performed by CoreAudio. The physical format can be set using the "Audio MIDI Setup" app. -The optional `format` parameter determines whether CamillaDSP should change the physical format or not. If a value is given, then CamillaDSP will change the setting to match the selected `format`. -To do this, it fetches a list of the supported stream formats for the device. +The optional `format` parameter determines whether CamillaDSP should change the physical format or not. +If a value is given, then CamillaDSP will change the setting to match the selected `format`. +To do this, it fetches a list of the supported stream formats for the device. It then searches the list until it finds a suitable one. -The criteria is that it must have the right sample rate, the right number of bits, -and the right number type (float or integer). -There exact representation of the given format isn't used. -This means that S24LE and S24LE3 are equivalent, and the "LE" ending that means +The criteria is that it must have the right sample rate, the right number of bits, +and the right number type (float or integer). +There exact representation of the given format isn't used. +This means that S24LE and S24LE3 are equivalent, and the "LE" ending that means little-endian for other backends is ignored. This table shows the mapping between the format setting in "Audio MIDI Setup" and the CamillaDSP `format`: diff --git a/backend_wasapi.md b/backend_wasapi.md index c5c088a..b3836c9 100644 --- a/backend_wasapi.md +++ b/backend_wasapi.md @@ -8,7 +8,10 @@ It offers two modes, "shared" and "exclusive", that offer different features and ### Shared mode This is the mode that most applications use. As the name suggests, this mode allows an audio device to be shared by several applications. -In shared mode the audio device then operates at a fixed sample rate and sample format. Every stream sent to it (or recorded from it) is resampled to/from the shared rate and format. The sample rate and output sample format of the device are called the "Default format" of the device and can be set in the Sound control panel. Internally, the Windows audio stack uses 32-bit float as the sample format. +In shared mode the audio device then operates at a fixed sample rate and sample format. +Every stream sent to it (or recorded from it) is resampled to/from the shared rate and format. +The sample rate and output sample format of the device are called the "Default format" of the device and can be set in the Sound control panel. +Internally, the Windows audio stack uses 32-bit float as the sample format. The audio passes through the Windows mixer and volume control. In shared mode, these points apply for the CamillaDSP configuration: @@ -23,7 +26,10 @@ In shared mode, these points apply for the CamillaDSP configuration: ### Exclusive mode This mode is often used for high quality music playback. -In this mode one application takes full control over an audio device. Only one application at a time can use the device. The sample rate and sample format can be changed, and the audio does not pass through the Windows mixer and volume control. This allows bit-perfect playback at any sample rate and sample format the hardware supports. While an application holds the device in exclusive mode, other apps will not be able to play for example notification sounds. +In this mode one application takes full control over an audio device. Only one application at a time can use the device. +The sample rate and sample format can be changed, and the audio does not pass through the Windows mixer and volume control. +This allows bit-perfect playback at any sample rate and sample format the hardware supports. +While an application holds the device in exclusive mode, other apps will not be able to play for example notification sounds. In exclusive mode, these points apply for the CamillaDSP configuration: - CamillaDSP is able to control the sample rate of the devices. @@ -41,22 +47,31 @@ CamillaDSP must capture audio from a capture device. This can either be a virtua ### Virtual sound card -When using a virtual sound card (sometimes called loopback device), all applications output their audio to the playback side of this virtual sound card. Then this audio signal can be captured from the capture side of the virtual card. [VB-CABLE from VB-AUDIO](https://www.vb-audio.com/Cable/) works well. +When using a virtual sound card (sometimes called loopback device), all applications output their audio to the playback side of this virtual sound card. +Then this audio signal can be captured from the capture side of the virtual card. [VB-CABLE from VB-AUDIO](https://www.vb-audio.com/Cable/) works well. #### Sending all audio to the virtual card -Set VB-CABLE as the default playback device in the Windows sound control panel. Open "Sound" in the Control Panel, then in the "Playback" tab select "CABLE Input" and click the "Set Default" button. This will work for all applications that respect this setting, which in practice is nearly all. The exceptions are the ones that provide their own way of selecting playback device. +Set VB-CABLE as the default playback device in the Windows sound control panel. +Open "Sound" in the Control Panel, then in the "Playback" tab select "CABLE Input" and click the "Set Default" button. +This will work for all applications that respect this setting, which in practice is nearly all. +The exceptions are the ones that provide their own way of selecting playback device. #### Capturing the audio The next step is to figure out the device name to enter in the CamillaDSP configuration. -Again open "Sound" in the Control Panel, and switch to the Recording tab. There should be a device listed as "CABLE Output". Unless the default names have been changed, the device name to enter in the CamillaDSP config is "CABLE Output (VB-Audio Virtual Cable)". +Again open "Sound" in the Control Panel, and switch to the Recording tab. +There should be a device listed as "CABLE Output". +Unless the default names have been changed, the device name to enter in the CamillaDSP config is "CABLE Output (VB-Audio Virtual Cable)". See also [Device names](#device-names) for more details on how to build the device names. ### Loopback capture -In loopback mode the audio is captured from a Playback device. This allows capturing the sound that a card is playing. In this mode, a spare unused sound card is used (note that this card can be either real or virtual). -The built in audio of the computer should work. The quality of the card doesn't matter, +In loopback mode the audio is captured from a Playback device. +This allows capturing the sound that a card is playing. +In this mode, a spare unused sound card is used (note that this card can be either real or virtual). +The built in audio of the computer should work. The quality of the card doesn't matter, since the audio data will not be routed through it. This requires using [Shared mode](#shared-mode). -Open the Sound Control Panel app, and locate the unused card in the "Playback" tab. Set it as default device. See [Device names](#device-names) for how to write the device name to enter in the CamillaDSP configuration. +Open the Sound Control Panel app, and locate the unused card in the "Playback" tab. +Set it as default device. See [Device names](#device-names) for how to write the device name to enter in the CamillaDSP configuration. ## Configuration of devices @@ -65,29 +80,41 @@ This example configuration will be used to explain the various options specific capture: type: Wasapi channels: 2 - device: "CABLE Output (VB-Audio Virtual Cable)" + device: "CABLE Output (VB-Audio Virtual Cable)" (*) format: FLOAT32LE exclusive: false (*) loopback: false (*) playback: type: Wasapi channels: 2 - device: "SPDIF Interface (FX-AUDIO-DAC-X6)" + device: "SPDIF Interface (FX-AUDIO-DAC-X6)" (*) format: S24LE3 exclusive: true (*) ``` +The parameters marked (*) are optional. ### Device names -The device names that are used for `device:` for both playback and capture are entered as shown in the Windows volume control. Click the speaker icon in the notification area, and then click the small up-arrow in the upper right corner of the volume control pop-up. This displays a list of all playback devices, with their names in the right format. The names can also be seen in the "Sound" control panel app. Look at either the "Playback" or "Recording" tab. The device name is built from the input/output name and card name, and the format is "{input/output name} ({card name})". For example, the VB-CABLE device name is "CABLE Output (VB-Audio Virtual Cable)", and the built in audio of a desktop computer can be "Speakers (Realtek(R) Audio)". +The device names that are used for `device` for both playback and capture are entered as shown in the Windows volume control. +Click the speaker icon in the notification area, and then click the small up-arrow in the upper right corner of the volume control pop-up. +This displays a list of all playback devices, with their names in the right format. +The names can also be seen in the "Sound" control panel app. Look at either the "Playback" or "Recording" tab. +The device name is built from the input/output name and card name, and the format is "{input/output name} ({card name})". +For example, the VB-CABLE device name is "CABLE Output (VB-Audio Virtual Cable)", +and the built in audio of a desktop computer can be "Speakers (Realtek(R) Audio)". -Specifying "default" will give the default capture or playback device. +Specifying `null` or leaving out `device` will give the default capture or playback device. To help with finding the name of playback and capture devices, use the Windows version of "cpal-listdevices" program from here: https://github.com/HEnquist/cpal-listdevices/releases -Just download the binary and run it in a terminal. It will list all devices with the names. The parameters shown are for shared mode, more sample rates and sample formats will likely be available in exclusive mode. +Just download the binary and run it in a terminal. It will list all devices with the names. +The parameters shown are for shared mode, more sample rates and sample formats will likely be available in exclusive mode. ### Shared or exclusive mode -Set `exclusive` to `true` to enable exclusive mode. Setting it to `false` or leaving it out means that shared mode will be used. Playback and capture are independent, they do not need to use the same mode. +Set `exclusive` to `true` to enable exclusive mode. +Setting it to `false` or leaving it out means that shared mode will be used. +Playback and capture are independent, they do not need to use the same mode. ### Loopback capture -Setting `loopback` to `true` enables loopback capture. This requires using shared mode for the capture device. See [Loopback capture](#loopback-capture) for more details. \ No newline at end of file +Setting `loopback` to `true` enables loopback capture. +This requires using shared mode for the capture device. +See [Loopback capture](#loopback-capture) for more details. \ No newline at end of file diff --git a/show_config.py b/show_config.py deleted file mode 100644 index 73dbdc3..0000000 --- a/show_config.py +++ /dev/null @@ -1,564 +0,0 @@ -# show_config.py - -import numpy as np -import numpy.fft as fft -import csv -import yaml -import sys -from matplotlib import pyplot as plt -from matplotlib.patches import Rectangle -import math - -class Conv(object): - def __init__(self, conf, fs): - if not conf: - conf = {values: [1.0]} - if 'filename' in conf: - fname = conf['filename'] - values = [] - if 'format' not in conf: - conf['format'] = "text" - if conf['format'] == "text": - with open(fname) as f: - values = [float(row[0]) for row in csv.reader(f)] - elif conf['format'] == "FLOAT64LE": - values = np.fromfile(fname, dtype=float) - elif conf['format'] == "FLOAT32LE": - values = np.fromfile(fname, dtype=np.float32) - elif conf['format'] == "S16LE": - values = np.fromfile(fname, dtype=np.int16)/(2**15-1) - elif conf['format'] == "S24LE": - values = np.fromfile(fname, dtype=np.int32)/(2**23-1) - elif conf['format'] == "S32LE": - values = np.fromfile(fname, dtype=np.int32)/(2**31-1) - else: - values = conf['values'] - self.impulse = values - self.fs = fs - - def gain_and_phase(self): - impulselen = len(self.impulse) - npoints = impulselen - if npoints < 300: - npoints = 300 - impulse = np.zeros(npoints*2) - impulse[0:impulselen] = self.impulse - impfft = fft.fft(impulse) - cut = impfft[0:npoints] - f = np.linspace(0, self.fs/2.0, npoints) - gain = 20*np.log10(np.abs(cut)) - phase = 180/np.pi*np.angle(cut) - return f, gain, phase - - def get_impulse(self): - t = np.linspace(0, len(self.impulse)/self.fs, len(self.impulse), endpoint=False) - return t, self.impulse - -class DiffEq(object): - def __init__(self, conf, fs): - self.fs = fs - self.a = conf['a'] - self.b = conf['b'] - if len(self.a)==0: - self.a=[1.0] - if len(self.b)==0: - self.b=[1.0] - - def gain_and_phase(self, f): - z = np.exp(1j*2*np.pi*f/self.fs); - A1=np.zeros(z.shape) - for n, bn in enumerate(self.b): - A1 = A1 + bn*z**(-n) - A2=np.zeros(z.shape) - for n, an in enumerate(self.a): - A2 = A2 + an*z**(-n) - A = A1/A2 - gain = 20*np.log10(np.abs(A)) - phase = 180/np.pi*np.angle(A) - return gain, phase - - def is_stable(self): - # TODO - return None - -class BiquadCombo(object): - - def Butterw_q(self, order): - odd = order%2 > 0 - n_so = math.floor(order/2.0) - qvalues = [] - for n in range(0, n_so): - q = 1/(2.0*math.sin((math.pi/order)*(n + 1/2))) - qvalues.append(q) - if odd: - qvalues.append(-1.0) - return qvalues - - def __init__(self, conf, fs): - self.ftype = conf['type'] - self.order = conf['order'] - self.freq = conf['freq'] - self.fs = fs - if self.ftype == "LinkwitzRileyHighpass": - #qvalues = self.LRtable[self.order] - q_temp = self.Butterw_q(self.order/2) - if (self.order/2)%2 > 0: - q_temp = q_temp[0:-1] - qvalues = q_temp + q_temp + [0.5] - else: - qvalues = q_temp + q_temp - type_so = "Highpass" - type_fo = "HighpassFO" - - elif self.ftype == "LinkwitzRileyLowpass": - q_temp = self.Butterw_q(self.order/2) - if (self.order/2)%2 > 0: - q_temp = q_temp[0:-1] - qvalues = q_temp + q_temp + [0.5] - else: - qvalues = q_temp + q_temp - type_so = "Lowpass" - type_fo = "LowpassFO" - elif self.ftype == "ButterworthHighpass": - qvalues = self.Butterw_q(self.order) - type_so = "Highpass" - type_fo = "HighpassFO" - elif self.ftype == "ButterworthLowpass": - qvalues = self.Butterw_q(self.order) - type_so = "Lowpass" - type_fo = "LowpassFO" - self.biquads = [] - print(qvalues) - for q in qvalues: - if q >= 0: - bqconf = {'freq': self.freq, 'q': q, 'type': type_so} - else: - bqconf = {'freq': self.freq, 'type': type_fo} - self.biquads.append(Biquad(bqconf, self.fs)) - - def is_stable(self): - # TODO - return None - - def gain_and_phase(self, f): - A = np.ones(f.shape) - for bq in self.biquads: - A = A * bq.complex_gain(f) - gain = 20*np.log10(np.abs(A)) - phase = 180/np.pi*np.angle(A) - return gain, phase - -class Biquad(object): - def __init__(self, conf, fs): - ftype = conf['type'] - if ftype == "Free": - a0 = 1.0 - a1 = conf['a1'] - a2 = conf['a1'] - b0 = conf['b0'] - b1 = conf['b1'] - b2 = conf['b2'] - if ftype == "Highpass": - freq = conf['freq'] - q = conf['q'] - omega = 2.0 * np.pi * freq / fs - sn = np.sin(omega) - cs = np.cos(omega) - alpha = sn / (2.0 * q) - b0 = (1.0 + cs) / 2.0 - b1 = -(1.0 + cs) - b2 = (1.0 + cs) / 2.0 - a0 = 1.0 + alpha - a1 = -2.0 * cs - a2 = 1.0 - alpha - elif ftype == "Lowpass": - freq = conf['freq'] - q = conf['q'] - omega = 2.0 * np.pi * freq / fs - sn = np.sin(omega) - cs = np.cos(omega) - alpha = sn / (2.0 * q) - b0 = (1.0 - cs) / 2.0 - b1 = 1.0 - cs - b2 = (1.0 - cs) / 2.0 - a0 = 1.0 + alpha - a1 = -2.0 * cs - a2 = 1.0 - alpha - elif ftype == "Peaking": - freq = conf['freq'] - q = conf['q'] - gain = conf['gain'] - omega = 2.0 * np.pi * freq / fs - sn = np.sin(omega) - cs = np.cos(omega) - ampl = 10.0**(gain / 40.0) - alpha = sn / (2.0 * q) - b0 = 1.0 + (alpha * ampl) - b1 = -2.0 * cs - b2 = 1.0 - (alpha * ampl) - a0 = 1.0 + (alpha / ampl) - a1 = -2.0 * cs - a2 = 1.0 - (alpha / ampl) - elif ftype == "HighshelfFO": - freq = conf['freq'] - gain = conf['gain'] - omega = 2.0 * np.pi * freq / fs - ampl = 10.0**(gain / 40.0) - tn = np.tan(omega/2) - b0 = ampl*tn + ampl**2 - b1 = ampl*tn - ampl**2 - b2 = 0.0 - a0 = ampl*tn + 1 - a1 = ampl*tn - 1 - a2 = 0.0 - elif ftype == "Highshelf": - freq = conf['freq'] - slope = conf['slope'] - gain = conf['gain'] - omega = 2.0 * np.pi * freq / fs - ampl = 10.0**(gain / 40.0) - sn = np.sin(omega) - cs = np.cos(omega) - alpha = sn / 2.0 * np.sqrt((ampl + 1.0 / ampl) * (1.0 / (slope/12.0) - 1.0) + 2.0) - beta = 2.0 * np.sqrt(ampl) * alpha - b0 = ampl * ((ampl + 1.0) + (ampl - 1.0) * cs + beta) - b1 = -2.0 * ampl * ((ampl - 1.0) + (ampl + 1.0) * cs) - b2 = ampl * ((ampl + 1.0) + (ampl - 1.0) * cs - beta) - a0 = (ampl + 1.0) - (ampl - 1.0) * cs + beta - a1 = 2.0 * ((ampl - 1.0) - (ampl + 1.0) * cs) - a2 = (ampl + 1.0) - (ampl - 1.0) * cs - beta - elif ftype == "LowshelfFO": - freq = conf['freq'] - gain = conf['gain'] - omega = 2.0 * np.pi * freq / fs - ampl = 10.0**(gain / 40.0) - tn = np.tan(omega/2) - b0 = ampl**2*tn + ampl - b1 = ampl**2*tn - ampl - b2 = 0.0 - a0 = tn + ampl - a1 = tn - ampl - a2 = 0.0 - elif ftype == "Lowshelf": - freq = conf['freq'] - slope = conf['slope'] - gain = conf['gain'] - omega = 2.0 * np.pi * freq / fs - ampl = 10.0**(gain / 40.0) - sn = np.sin(omega) - cs = np.cos(omega) - alpha = sn / 2.0 * np.sqrt((ampl + 1.0 / ampl) * (1.0 / (slope/12.0) - 1.0) + 2.0) - beta = 2.0 * np.sqrt(ampl) * alpha - b0 = ampl * ((ampl + 1.0) - (ampl - 1.0) * cs + beta) - b1 = 2.0 * ampl * ((ampl - 1.0) - (ampl + 1.0) * cs) - b2 = ampl * ((ampl + 1.0) - (ampl - 1.0) * cs - beta) - a0 = (ampl + 1.0) + (ampl - 1.0) * cs + beta - a1 = -2.0 * ((ampl - 1.0) + (ampl + 1.0) * cs) - a2 = (ampl + 1.0) + (ampl - 1.0) * cs - beta - elif ftype == "LowpassFO": - freq = conf['freq'] - omega = 2.0 * np.pi * freq / fs - k = np.tan(omega/2.0) - alpha = 1 + k - a0 = 1.0 - a1 = -((1 - k)/alpha) - a2 = 0.0 - b0 = k/alpha - b1 = k/alpha - b2 = 0 - elif ftype == "HighpassFO": - freq = conf['freq'] - omega = 2.0 * np.pi * freq / fs - k = np.tan(omega/2.0) - alpha = 1 + k - a0 = 1.0 - a1 = -((1 - k)/alpha) - a2 = 0.0 - b0 = 1.0/alpha - b1 = -1.0/alpha - b2 = 0 - elif ftype == "Notch": - freq = conf['freq'] - q = conf['q'] - omega = 2.0 * np.pi * freq / fs - sn = np.sin(omega) - cs = np.cos(omega) - alpha = sn / (2.0 * q) - b0 = 1.0 - b1 = -2.0 * cs - b2 = 1.0 - a0 = 1.0 + alpha - a1 = -2.0 * cs - a2 = 1.0 - alpha - elif ftype == "Bandpass": - freq = conf['freq'] - q = conf['q'] - omega = 2.0 * np.pi * freq / fs - sn = np.sin(omega) - cs = np.cos(omega) - alpha = sn / (2.0 * q) - b0 = alpha - b1 = 0.0 - b2 = -alpha - a0 = 1.0 + alpha - a1 = -2.0 * cs - a2 = 1.0 - alpha - elif ftype == "Allpass": - freq = conf['freq'] - q = conf['q'] - omega = 2.0 * np.pi * freq / fs - sn = np.sin(omega) - cs = np.cos(omega) - alpha = sn / (2.0 * q) - b0 = 1.0 - alpha - b1 = -2.0 * cs - b2 = 1.0 + alpha - a0 = 1.0 + alpha - a1 = -2.0 * cs - a2 = 1.0 - alpha - elif ftype == "AllpassFO": - freq = conf['freq'] - omega = 2.0 * np.pi * freq / fs - tn = np.tan(omega/2.0) - alpha = (tn + 1.0)/(tn - 1.0) - b0 = 1.0 - b1 = alpha - b2 = 0.0 - a0 = alpha - a1 = 1.0 - a2 = 0.0 - elif ftype == "LinkwitzTransform": - f0 = conf['freq_act'] - q0 = conf['q_act'] - qt = conf['q_target'] - ft = conf['freq_target'] - - d0i = (2.0 * np.pi * f0)**2 - d1i = (2.0 * np.pi * f0)/q0 - c0i = (2.0 * np.pi * ft)**2 - c1i = (2.0 * np.pi * ft)/qt - fc = (ft+f0)/2.0 - - gn = 2 * np.pi * fc/math.tan(np.pi*fc/fs) - cci = c0i + gn * c1i + gn**2 - - b0 = (d0i+gn*d1i + gn**2)/cci - b1 = 2*(d0i-gn**2)/cci - b2 = (d0i - gn*d1i + gn**2)/cci - a0 = 1.0 - a1 = 2.0 * (c0i-gn**2)/cci - a2 = ((c0i-gn*c1i + gn**2)/cci) - - - self.fs = fs - self.a1 = a1 / a0 - self.a2 = a2 / a0 - self.b0 = b0 / a0 - self.b1 = b1 / a0 - self.b2 = b2 / a0 - - def complex_gain(self, f): - z = np.exp(1j*2*np.pi*f/self.fs); - A = (self.b0 + self.b1*z**(-1) + self.b2*z**(-2))/(1.0 + self.a1*z**(-1) + self.a2*z**(-2)) - return A - - def gain_and_phase(self, f): - A = self.complex_gain(f) - gain = 20*np.log10(np.abs(A)) - phase = 180/np.pi*np.angle(A) - return gain, phase - - def is_stable(self): - return abs(self.a2)<1.0 and abs(self.a1) < (self.a2+1.0) - -class Block(object): - def __init__(self, label): - self.label = label - self.x = None - self.y = None - - def place(self, x, y): - self.x = x - self.y = y - - def draw(self, ax): - rect = Rectangle((self.x-0.5, self.y-0.25), 1.0, 0.5, linewidth=1,edgecolor='r',facecolor='none') - ax.add_patch(rect) - ax.text(self.x, self.y, self.label, horizontalalignment='center', verticalalignment='center') - - - def input_point(self): - return self.x-0.5, self.y - - def output_point(self): - return self.x+0.5, self.y - -def draw_arrow(ax, p0, p1, label=None): - x0, y0 = p0 - x1, y1 = p1 - ax.arrow(x0, y0, x1-x0, y1-y0, width=0.01, length_includes_head=True, head_width=0.1) - if y1 > y0: - hal = 'right' - val = 'bottom' - else: - hal = 'right' - val = 'top' - if label is not None: - ax.text(x0+(x1-x0)*2/3, y0+(y1-y0)*2/3, label, horizontalalignment=hal, verticalalignment=val) - -def draw_box(ax, level, size, label=None): - x0 = 2*level-0.75 - y0 = -size/2 - rect = Rectangle((x0, y0), 1.5, size, linewidth=1,edgecolor='g',facecolor='none', linestyle='--') - ax.add_patch(rect) - if label is not None: - ax.text(2*level, size/2, label, horizontalalignment='center', verticalalignment='bottom') - -def main(): - print('This script is deprecated. Please use the "plotcamillaconf" tool\nfrom the pycamilladsp-plot library instead.') - fname = sys.argv[1] - - conffile = open(fname) - - conf = yaml.safe_load(conffile) - print(conf) - - srate = conf['devices']['samplerate'] - #if "chunksize" in conf['devices']: - # buflen = conf['devices']['chunksize'] - #else: - # buflen = conf['devices']['buffersize'] - #print (srate) - fignbr = 1 - - if 'filters' in conf: - fvect = np.linspace(1, (srate*0.95)/2.0, 10000) - for filter, fconf in conf['filters'].items(): - if fconf['type'] in ('Biquad', 'DiffEq', 'BiquadCombo'): - if fconf['type'] == 'DiffEq': - kladd = DiffEq(fconf['parameters'], srate) - elif fconf['type'] == 'BiquadCombo': - kladd = BiquadCombo(fconf['parameters'], srate) - else: - kladd = Biquad(fconf['parameters'], srate) - plt.figure(num=filter) - magn, phase = kladd.gain_and_phase(fvect) - stable = kladd.is_stable() - plt.subplot(2,1,1) - plt.semilogx(fvect, magn) - plt.title("{}, stable: {}\nMagnitude".format(filter, stable)) - plt.subplot(2,1,2) - plt.semilogx(fvect, phase) - plt.title("Phase") - fignbr += 1 - elif fconf['type'] == 'Conv': - if 'parameters' in fconf: - kladd = Conv(fconf['parameters'], srate) - else: - kladd = Conv(None, srate) - plt.figure(num=filter) - ftemp, magn, phase = kladd.gain_and_phase() - plt.subplot(2,1,1) - plt.semilogx(ftemp, magn) - plt.title("FFT of {}".format(filter)) - plt.gca().set(xlim=(10, srate/2.0)) - #fignbr += 1 - #plt.figure(fignbr) - t, imp = kladd.get_impulse() - plt.subplot(2,1,2) - plt.plot(t, imp) - plt.title("Impulse response of {}".format(filter)) - fignbr += 1 - - stages = [] - fig = plt.figure(fignbr) - - ax = fig.add_subplot(111, aspect='equal') - # add input - channels = [] - active_channels = int(conf['devices']['capture']['channels']) - for n in range(active_channels): - label = "ch {}".format(n) - b = Block(label) - b.place(0, -active_channels/2 + 0.5 + n) - b.draw(ax) - channels.append([b]) - if 'device' in conf['devices']['capture']: - capturename = conf['devices']['capture']['device'] - else: - capturename = conf['devices']['capture']['filename'] - draw_box(ax, 0, active_channels, label=capturename) - stages.append(channels) - - # loop through pipeline - - total_length = 0 - stage_start = 0 - if 'pipeline' in conf: - for step in conf['pipeline']: - stage = len(stages) - if step['type'] == 'Mixer': - total_length += 1 - name = step['name'] - mixconf = conf['mixers'][name] - active_channels = int(mixconf['channels']['out']) - channels = [[]]*active_channels - for n in range(active_channels): - label = "ch {}".format(n) - b = Block(label) - b.place(total_length*2, -active_channels/2 + 0.5 + n) - b.draw(ax) - channels[n] = [b] - for mapping in mixconf['mapping']: - dest_ch = int(mapping['dest']) - for src in mapping['sources']: - src_ch = int(src['channel']) - label = "{} dB".format(src['gain']) - if src['inverted'] == 'False': - label = label + '\ninv.' - src_p = stages[-1][src_ch][-1].output_point() - dest_p = channels[dest_ch][0].input_point() - draw_arrow(ax, src_p, dest_p, label=label) - draw_box(ax, total_length, active_channels, label=name) - stages.append(channels) - stage_start = total_length - elif step['type'] == 'Filter': - ch_nbr = step['channel'] - for name in step['names']: - b = Block(name) - ch_step = stage_start + len(stages[-1][ch_nbr]) - total_length = max((total_length, ch_step)) - b.place(ch_step*2, -active_channels/2 + 0.5 + ch_nbr) - b.draw(ax) - src_p = stages[-1][ch_nbr][-1].output_point() - dest_p = b.input_point() - draw_arrow(ax, src_p, dest_p) - stages[-1][ch_nbr].append(b) - - - total_length += 1 - channels = [] - for n in range(active_channels): - label = "ch {}".format(n) - b = Block(label) - b.place(2*total_length, -active_channels/2 + 0.5 + n) - b.draw(ax) - src_p = stages[-1][n][-1].output_point() - dest_p = b.input_point() - draw_arrow(ax, src_p, dest_p) - channels.append([b]) - if 'device' in conf['devices']['playback']: - playname = conf['devices']['playback']['device'] - else: - playname = conf['devices']['playback']['filename'] - draw_box(ax, total_length, active_channels, label=playname) - stages.append(channels) - - nbr_chan = [len(s) for s in stages] - ylim = math.ceil(max(nbr_chan)/2.0) + 0.5 - ax.set(xlim=(-1, 2*total_length+1), ylim=(-ylim, ylim)) - plt.axis('off') - plt.show() - -if __name__ == "__main__": - main() \ No newline at end of file From 5f0a8960c0be944ccddc7bd759a496b401ccae8d Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 2 Apr 2024 22:40:27 +0200 Subject: [PATCH 030/135] Remove unused image --- pipeline.png | Bin 40069 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 pipeline.png diff --git a/pipeline.png b/pipeline.png deleted file mode 100644 index 6709dec137a49b0ece98701c3f168ab4e5ef06e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40069 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 F{|n>w<2(QW From 05a37cef46dd306c3f8f764f498181cb6fa37158 Mon Sep 17 00:00:00 2001 From: Henrik Date: Thu, 4 Apr 2024 22:57:05 +0200 Subject: [PATCH 031/135] Update clap and fftw --- Cargo.toml | 4 +- src/bin.rs | 279 +++++++++++++++++++---------------------- src/coreaudiodevice.rs | 2 +- 3 files changed, 135 insertions(+), 150 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 32693e5..f5bade4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,13 +52,13 @@ serde_json = "1.0" serde_with = "1.11" realfft = "3.0.0" #realfft = { git = "https://github.com/HEnquist/realfft", branch = "better_errors" } -fftw = { version = "0.7.0", optional = true } +fftw = { version = "0.8.0", optional = true } num-complex = "0.4" num-traits = "0.2" signal-hook = "0.3.8" rand = { version = "0.8.3", default_features = false, features = ["small_rng", "std"] } rand_distr = "0.4.3" -clap = "2.33.0" +clap = { version = "4.5.4", features = ["cargo"] } lazy_static = "1.4.0" log = "0.4.14" flexi_logger = { version = "0.27.2", features = ["async", "colors"] } diff --git a/src/bin.rs b/src/bin.rs index ac4b3ef..3cdea98 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -26,7 +26,7 @@ extern crate flexi_logger; #[macro_use] extern crate log; -use clap::{crate_authors, crate_description, crate_version, App, AppSettings, Arg}; +use clap::{crate_authors, crate_description, crate_version, Arg, ArgAction, Command}; use crossbeam_channel::select; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use std::env; @@ -448,215 +448,204 @@ fn main_process() -> i32 { playback_types ); - let clapapp = App::new("CamillaDSP") + let clapapp = Command::new("CamillaDSP") .version(crate_version!()) - .about(longabout.as_str()) + .about(longabout) .author(crate_authors!()) - .setting(AppSettings::ArgRequiredElseHelp) + //.setting(AppSettings::ArgRequiredElseHelp) .arg( - Arg::with_name("configfile") + Arg::new("configfile") .help("The configuration file to use") .index(1) - //.required(true), - .required_unless_one(&["wait", "statefile"]), + .value_name("CONFIGFILE") + .action(ArgAction::Set) + .value_parser(clap::builder::NonEmptyStringValueParser::new()) + .required_unless_present_any(["wait", "statefile"]), ) .arg( - Arg::with_name("statefile") + Arg::new("statefile") .help("Use the given file to persist the state") - .short("s") + .short('s') .long("statefile") - .takes_value(true) - .display_order(2), + .value_name("STATEFILE") + .action(ArgAction::Set) + .display_order(2) + .value_parser(clap::builder::NonEmptyStringValueParser::new()), ) .arg( - Arg::with_name("check") + Arg::new("check") .help("Check config file and exit") - .short("c") + .short('c') .long("check") - .requires("configfile"), + .requires("configfile") + .action(ArgAction::SetTrue), ) .arg( - Arg::with_name("verbosity") - .short("v") - .multiple(true) - .help("Increase message verbosity"), + Arg::new("verbosity") + .help("Increase message verbosity") + .short('v') + .action(ArgAction::Count), ) .arg( - Arg::with_name("loglevel") - .short("l") + Arg::new("loglevel") + .help("Set log level") + .short('l') .long("loglevel") + .value_name("LOGLEVEL") .display_order(100) - .takes_value(true) - .possible_value("trace") - .possible_value("debug") - .possible_value("info") - .possible_value("warn") - .possible_value("error") - .possible_value("off") - .help("Set log level") - .conflicts_with("verbosity"), + .conflicts_with("verbosity") + .action(ArgAction::Set) + .value_parser(["trace", "debug", "info", "warn", "error", "off"]), ) .arg( - Arg::with_name("logfile") - .short("o") + Arg::new("logfile") + .help("Write logs to file") + .short('o') .long("logfile") + .value_name("LOGFILE") .display_order(100) - .takes_value(true) - .help("Write logs to file"), + .action(ArgAction::Set), ) .arg( - Arg::with_name("gain") + Arg::new("gain") .help("Set initial gain in dB for Volume and Loudness filters") - .short("g") + .short('g') .long("gain") + .value_name("GAIN") .display_order(200) - .takes_value(true) - .validator(|v: String| -> Result<(), String> { + .action(ArgAction::Set) + .value_parser(|v: &str| -> Result { if let Ok(gain) = v.parse::() { if (-120.0..=20.0).contains(&gain) { - return Ok(()); + return Ok(gain); } } Err(String::from("Must be a number between -120 and +20")) }), ) .arg( - Arg::with_name("mute") + Arg::new("mute") .help("Start with Volume and Loudness filters muted") - .short("m") + .short('m') .long("mute") - .display_order(200), + .display_order(200) + .action(ArgAction::SetTrue), ) .arg( - Arg::with_name("samplerate") + Arg::new("samplerate") .help("Override samplerate in config") - .short("r") + .short('r') .long("samplerate") + .value_name("SAMPLERATE") .display_order(300) - .takes_value(true) - .validator(|v: String| -> Result<(), String> { - if let Ok(rate) = v.parse::() { - if rate > 0 { - return Ok(()); - } - } - Err(String::from("Must be an integer > 0")) - }), + .action(ArgAction::Set) + .value_parser(clap::builder::RangedU64ValueParser::::new().range(1..)), ) .arg( - Arg::with_name("channels") + Arg::new("channels") .help("Override number of channels of capture device in config") - .short("n") + .short('n') .long("channels") + .value_name("CHANNELS") .display_order(300) - .takes_value(true) - .validator(|v: String| -> Result<(), String> { - if let Ok(rate) = v.parse::() { - if rate > 0 { - return Ok(()); - } - } - Err(String::from("Must be an integer > 0")) - }), + .action(ArgAction::Set) + .value_parser(clap::builder::RangedU64ValueParser::::new().range(1..)), ) .arg( - Arg::with_name("extra_samples") + Arg::new("extra_samples") .help("Override number of extra samples in config") - .short("e") + .short('e') .long("extra_samples") + .value_name("EXTRA_SAMPLES") .display_order(300) - .takes_value(true) - .validator(|v: String| -> Result<(), String> { - if let Ok(_samples) = v.parse::() { - return Ok(()); - } - Err(String::from("Must be an integer > 0")) - }), + .action(ArgAction::Set) + .value_parser(clap::builder::RangedU64ValueParser::::new().range(1..)), ) .arg( - Arg::with_name("format") - .short("f") + Arg::new("format") + .short('f') .long("format") + .value_name("FORMAT") .display_order(310) - .takes_value(true) - .possible_value("S16LE") - .possible_value("S24LE") - .possible_value("S24LE3") - .possible_value("S32LE") - .possible_value("FLOAT32LE") - .possible_value("FLOAT64LE") + .action(ArgAction::Set) + .value_parser([ + "S16LE", + "S24LE", + "S24LE3", + "S32LE", + "FLOAT32LE", + "FLOAT64LE", + ]) .help("Override sample format of capture device in config"), ); #[cfg(feature = "websocket")] let clapapp = clapapp .arg( - Arg::with_name("port") + Arg::new("port") .help("Port for websocket server") - .short("p") + .short('p') .long("port") + .value_name("PORT") .display_order(200) - .takes_value(true) - .validator(|v: String| -> Result<(), String> { - if let Ok(port) = v.parse::() { - if port > 0 && port < 65535 { - return Ok(()); - } - } - Err(String::from("Must be an integer between 0 and 65535")) - }), + .action(ArgAction::Set) + .value_parser(clap::builder::RangedU64ValueParser::::new().range(0..65535)), ) .arg( - Arg::with_name("address") + Arg::new("address") .help("IP address to bind websocket server to") - .short("a") + .short('a') .long("address") + .value_name("ADDRESS") .display_order(200) - .takes_value(true) + .action(ArgAction::Set) .requires("port") - .validator(|val: String| -> Result<(), String> { + .value_parser(|val: &str| -> Result { if val.parse::().is_ok() { - return Ok(()); + return Ok(val.to_string()); } Err(String::from("Must be a valid IP address")) }), ) .arg( - Arg::with_name("wait") - .short("w") + Arg::new("wait") + .short('w') .long("wait") .help("Wait for config from websocket") - .requires("port"), + .requires("port") + .action(ArgAction::SetTrue), ); #[cfg(feature = "secure-websocket")] let clapapp = clapapp .arg( - Arg::with_name("cert") - .long("cert") - .takes_value(true) + Arg::new("cert") .help("Path to .pfx/.p12 certificate file") + .long("cert") + .value_name("CERT") + .action(ArgAction::Set) .requires("port"), ) .arg( - Arg::with_name("pass") - .long("pass") - .takes_value(true) + Arg::new("pass") .help("Password for .pfx/.p12 certificate file") + .long("pass") + .value_name("PASS") + .action(ArgAction::Set) .requires("port"), ); let matches = clapapp.get_matches(); - let mut loglevel = match matches.occurrences_of("verbosity") { + let mut loglevel = match matches.get_count("verbosity") { 0 => "info", 1 => "debug", 2 => "trace", _ => "trace", }; - if let Some(level) = matches.value_of("loglevel") { + if let Some(level) = matches.get_one::("loglevel") { loglevel = level; } - let logger = if let Some(logfile) = matches.value_of("logfile") { + let logger = if let Some(logfile) = matches.get_one::("logfile") { let mut path = PathBuf::from(logfile); if !path.is_absolute() { let mut fullpath = std::env::current_dir().unwrap(); @@ -701,25 +690,19 @@ fn main_process() -> i32 { #[cfg(target_os = "windows")] wasapi::initialize_mta().unwrap(); - let mut configname = matches.value_of("configfile").map(|path| path.to_string()); + let mut configname = matches.get_one::("configfile").cloned(); { let mut overrides = config::OVERRIDES.write(); - overrides.samplerate = matches - .value_of("samplerate") - .map(|s| s.parse::().unwrap()); - overrides.extra_samples = matches - .value_of("extra_samples") - .map(|s| s.parse::().unwrap()); - overrides.channels = matches - .value_of("channels") - .map(|s| s.parse::().unwrap()); + overrides.samplerate = matches.get_one::("samplerate").copied(); + overrides.extra_samples = matches.get_one::("extra_samples").copied(); + overrides.channels = matches.get_one::("channels").copied(); overrides.sample_format = matches - .value_of("format") + .get_one::("format") .map(|s| config::SampleFormat::from_name(s).unwrap()); } - let statefilename = matches.value_of("statefile").map(|path| path.to_string()); + let statefilename: Option = matches.get_one::("statefile").cloned(); let state = if let Some(filename) = &statefilename { statefile::load_state(filename) } else { @@ -727,25 +710,24 @@ fn main_process() -> i32 { }; debug!("Loaded state: {state:?}"); - let initial_volumes = - if let Some(v) = matches.value_of("gain").map(|s| s.parse::().unwrap()) { - debug!("Using command line argument for initial volume"); - [v, v, v, v, v] - } else if let Some(s) = &state { - debug!("Using statefile for initial volume"); - s.volume - } else { - debug!("Using default initial volume"); - [ - ProcessingParameters::DEFAULT_VOLUME, - ProcessingParameters::DEFAULT_VOLUME, - ProcessingParameters::DEFAULT_VOLUME, - ProcessingParameters::DEFAULT_VOLUME, - ProcessingParameters::DEFAULT_VOLUME, - ] - }; + let initial_volumes = if let Some(v) = matches.get_one::("gain") { + debug!("Using command line argument for initial volume"); + [*v, *v, *v, *v, *v] + } else if let Some(s) = &state { + debug!("Using statefile for initial volume"); + s.volume + } else { + debug!("Using default initial volume"); + [ + ProcessingParameters::DEFAULT_VOLUME, + ProcessingParameters::DEFAULT_VOLUME, + ProcessingParameters::DEFAULT_VOLUME, + ProcessingParameters::DEFAULT_VOLUME, + ProcessingParameters::DEFAULT_VOLUME, + ] + }; - let initial_mutes = if matches.is_present("mute") { + let initial_mutes = if matches.get_flag("mute") { debug!("Using command line argument for initial mute"); [true, true, true, true, true] } else if let Some(s) = &state { @@ -767,7 +749,7 @@ fn main_process() -> i32 { debug!("Read config file {:?}", configname); - if matches.is_present("check") { + if matches.get_flag("check") { match config::load_validate_config(&configname.unwrap()) { Ok(_) => { println!("Config is valid"); @@ -900,7 +882,7 @@ fn main_process() -> i32 { } }); - let wait = matches.is_present("wait"); + let wait = matches.get_flag("wait"); let capture_status = Arc::new(RwLock::new(CaptureStatus { measured_samplerate: 0, @@ -941,9 +923,12 @@ fn main_process() -> i32 { let active_config_path_clone = active_config_path.clone(); let unsaved_state_changes = Arc::new(AtomicBool::new(false)); - if let Some(port_str) = matches.value_of("port") { - let serveraddress = matches.value_of("address").unwrap_or("127.0.0.1"); - let serverport = port_str.parse::().unwrap(); + if let Some(port) = matches.get_one::("port") { + let serveraddress = matches + .get_one::("address") + .cloned() + .unwrap_or("127.0.0.1".to_string()); + let serverport = *port; let shared_data = socketserver::SharedData { active_config: active_config.clone(), @@ -960,11 +945,11 @@ fn main_process() -> i32 { }; let server_params = socketserver::ServerParameters { port: serverport, - address: serveraddress, + address: &serveraddress, #[cfg(feature = "secure-websocket")] - cert_file: matches.value_of("cert"), + cert_file: matches.get_one::("cert").map(|x| x.as_str()), #[cfg(feature = "secure-websocket")] - cert_pass: matches.value_of("pass"), + cert_pass: matches.get_one::("pass").map(|x| x.as_str()), }; socketserver::start_server(server_params, shared_data); } diff --git a/src/coreaudiodevice.rs b/src/coreaudiodevice.rs index ee2ef37..86f5795 100644 --- a/src/coreaudiodevice.rs +++ b/src/coreaudiodevice.rs @@ -608,7 +608,7 @@ fn nbr_capture_frames( ) -> usize { if let Some(resampl) = &resampler { #[cfg(feature = "debug")] - trace!("Resampler needs {resampl.input_frames_next()} frames"); + trace!("Resampler needs {} frames", resampl.input_frames_next()); resampl.input_frames_next() } else { capture_frames From d21bc73fc8f71928dcaaf6c4e7266c5834b2a572 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Sat, 6 Apr 2024 22:32:55 +0200 Subject: [PATCH 032/135] Update Alsa and Nix crates --- Cargo.toml | 6 +++--- src/alsadevice.rs | 13 +++++++------ src/filereader_nonblock.rs | 15 ++++++++------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f5bade4..10cb386 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "3.0.0" authors = ["Henrik Enquist "] edition = "2021" description = "A flexible tool for processing audio" -rust-version = "1.61" +rust-version = "1.74" [features] default = ["websocket"] @@ -28,9 +28,9 @@ name = "camilladsp" path = "src/bin.rs" [target.'cfg(target_os="linux")'.dependencies] -alsa = "0.8.1" +alsa = "0.9.0" alsa-sys = "0.3.1" -nix = "0.23" +nix = { version = "0.28", features = ["poll", "signal"] } zbus = { version = "3.0.0", optional = true } [target.'cfg(target_os="macos")'.dependencies] diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 8456fba..f2bf184 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -11,6 +11,7 @@ use alsa::hctl::{Elem, HCtl}; use alsa::pcm::{Access, Format, Frames, HwParams}; use alsa::{Direction, ValueOr, PCM}; use alsa_sys; +use nix::errno::Errno; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use rubato::VecResampler; use std::ffi::CString; @@ -128,7 +129,7 @@ fn play_buffer( if playback_state < 0 { // This should never happen but sometimes does anyway, // for example if a USB device is unplugged. - let nixerr = alsa::nix::errno::from_i32(-playback_state); + let nixerr = Errno::from_raw(-playback_state); error!( "PB: Alsa snd_pcm_state() of playback device returned an unexpected error: {}", nixerr @@ -210,7 +211,7 @@ fn play_buffer( } } Err(err) => { - if err.nix_error() == alsa::nix::errno::Errno::EAGAIN { + if Errno::from_raw(err.errno()) == Errno::EAGAIN { trace!("PB: encountered EAGAIN error on write, trying again"); } else { warn!("PB: write error, trying to recover. Error: {}", err); @@ -243,7 +244,7 @@ fn capture_buffer( } else if capture_state < 0 { // This should never happen but sometimes does anyway, // for example if a USB device is unplugged. - let nixerr = alsa::nix::errno::from_i32(-capture_state); + let nixerr = Errno::from_raw(-capture_state); error!( "Alsa snd_pcm_state() of capture device returned an unexpected error: {}", capture_state @@ -301,14 +302,14 @@ fn capture_buffer( continue; } } - Err(err) => match err.nix_error() { - alsa::nix::errno::Errno::EIO => { + Err(err) => match Errno::from_raw(err.errno()) { + Errno::EIO => { warn!("Capture failed with error: {}", err); return Err(Box::new(err)); } // TODO: do we need separate handling of xruns that happen in the tiny // window between state() and readi()? - alsa::nix::errno::Errno::EPIPE => { + Errno::EPIPE => { warn!("Capture failed, error: {}", err); return Err(Box::new(err)); } diff --git a/src/filereader_nonblock.rs b/src/filereader_nonblock.rs index 58bed5c..0994879 100644 --- a/src/filereader_nonblock.rs +++ b/src/filereader_nonblock.rs @@ -3,24 +3,25 @@ use nix; use std::error::Error; use std::io::ErrorKind; use std::io::Read; -use std::os::unix::io::AsRawFd; +use std::os::unix::io::{AsRawFd, BorrowedFd}; use std::time; use std::time::Duration; use crate::filedevice::{ReadResult, Reader}; -pub struct NonBlockingReader { - poll: nix::poll::PollFd, +pub struct NonBlockingReader<'a, R: 'a> { + poll: nix::poll::PollFd<'a>, signals: nix::sys::signal::SigSet, timeout: Option, timelimit: time::Duration, inner: R, } -impl NonBlockingReader { +impl<'a, R: Read + AsRawFd + 'a> NonBlockingReader<'a, R> { pub fn new(inner: R, timeout_millis: u64) -> Self { let flags = nix::poll::PollFlags::POLLIN; - let poll = nix::poll::PollFd::new(inner.as_raw_fd(), flags); + let poll: nix::poll::PollFd<'_> = + nix::poll::PollFd::new(unsafe { BorrowedFd::borrow_raw(inner.as_raw_fd()) }, flags); let mut signals = nix::sys::signal::SigSet::empty(); signals.add(nix::sys::signal::Signal::SIGIO); let timelimit = time::Duration::from_millis(timeout_millis); @@ -35,13 +36,13 @@ impl NonBlockingReader { } } -impl Reader for NonBlockingReader { +impl<'a, R: Read + AsRawFd + 'a> Reader for NonBlockingReader<'a, R> { fn read(&mut self, data: &mut [u8]) -> Result> { let mut buf = &mut *data; let mut bytes_read = 0; let start = time::Instant::now(); loop { - let res = nix::poll::ppoll(&mut [self.poll], self.timeout, self.signals)?; + let res = nix::poll::ppoll(&mut [self.poll], self.timeout, Some(self.signals))?; //println!("loop..."); if res == 0 { return Ok(ReadResult::Timeout(bytes_read)); From 99595962a776ed5398ede1f4bbbbb612021d6c5d Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Sun, 7 Apr 2024 20:58:10 +0200 Subject: [PATCH 033/135] Improved optional logging --- src/alsadevice.rs | 9 ++++----- src/audiodevice.rs | 5 +++++ src/basicfilters.rs | 3 ++- src/conversions.rs | 1 + src/coreaudiodevice.rs | 1 - src/cpaldevice.rs | 1 - src/filedevice.rs | 10 ---------- src/lib.rs | 33 +++++++++++++++++++++++++++++++++ src/pulsedevice.rs | 15 +-------------- src/wasapidevice.rs | 1 - 10 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 8456fba..cb2fa95 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -124,7 +124,7 @@ fn play_buffer( buf_manager: &mut PlaybackBufferManager, ) -> Res { let playback_state = pcmdevice.state_raw(); - //trace!("Playback state {:?}", playback_state); + xtrace!("Playback state {:?}", playback_state); if playback_state < 0 { // This should never happen but sometimes does anyway, // for example if a USB device is unplugged. @@ -214,7 +214,6 @@ fn play_buffer( trace!("PB: encountered EAGAIN error on write, trying again"); } else { warn!("PB: write error, trying to recover. Error: {}", err); - trace!("snd_pcm_prepare"); // Would recover() be better than prepare()? pcmdevice.prepare()?; buf_manager.sleep_for_target_delay(millis_per_frame); @@ -573,7 +572,7 @@ fn playback_loop_bytes( debug!( "PB: buffer level: {:.1}, signal rms: {:?}", avg_delay, - playback_status.signal_rms.last() + playback_status.signal_rms.last_sqrt() ); } } @@ -750,7 +749,7 @@ fn capture_loop_bytes( ); match capture_res { Ok(CaptureResult::Normal) => { - //trace!("Captured {} bytes", capture_bytes); + xtrace!("Captured {} bytes", capture_bytes); averager.add_value(capture_bytes); { let capture_status = params.capture_status.upgradable_read(); @@ -890,7 +889,7 @@ fn nbr_capture_bytes_and_frames( buf: &mut Vec, ) -> (usize, Frames) { let (capture_bytes_new, capture_frames_new) = if let Some(resampl) = &resampler { - //trace!("Resampler needs {} frames", resampl.input_frames_next()); + xtrace!("Resampler needs {} frames", resampl.input_frames_next()); let frames = resampl.input_frames_next(); ( frames * params.channels * params.store_bytes_per_sample, diff --git a/src/audiodevice.rs b/src/audiodevice.rs index 250aae4..dc9211f 100644 --- a/src/audiodevice.rs +++ b/src/audiodevice.rs @@ -187,6 +187,11 @@ impl AudioChunk { *peakval = peak; *rmsval = rms; } + xtrace!( + "Stats: rms {:?}, peak {:?}", + stats.rms_db(), + stats.peak_db() + ); } pub fn update_channel_mask(&self, mask: &mut [bool]) { diff --git a/src/basicfilters.rs b/src/basicfilters.rs index 7a65363..369bc67 100644 --- a/src/basicfilters.rs +++ b/src/basicfilters.rs @@ -174,6 +174,7 @@ impl Volume { // Not in a ramp if self.ramp_step == 0 { + xtrace!("Vol: applying linear gain {}", self.target_linear_gain); for waveform in chunk.waveforms.iter_mut() { for item in waveform.iter_mut() { *item *= self.target_linear_gain; @@ -182,7 +183,7 @@ impl Volume { } // Ramping else if self.ramp_step <= self.ramptime_in_chunks { - trace!("ramp step {}", self.ramp_step); + trace!("Vol: ramp step {}", self.ramp_step); let ramp = self.make_ramp(); self.ramp_step += 1; if self.ramp_step > self.ramptime_in_chunks { diff --git a/src/conversions.rs b/src/conversions.rs index ec08cc2..559b575 100644 --- a/src/conversions.rs +++ b/src/conversions.rs @@ -61,6 +61,7 @@ pub fn chunk_to_buffer_rawbytes( } clipped += PrcFmt::write_samples(&nextframe, &mut cursor, &rawformat).unwrap(); } + xtrace!("Convert, nbr clipped: {}, peak: {}", clipped, peak); if clipped > 0 { warn!( "Clipping detected, {} samples clipped, peak +{:.2} dB ({:.1}%)", diff --git a/src/coreaudiodevice.rs b/src/coreaudiodevice.rs index 86f5795..0e2f281 100644 --- a/src/coreaudiodevice.rs +++ b/src/coreaudiodevice.rs @@ -941,7 +941,6 @@ impl CaptureDevice for CoreaudioCaptureDevice { } prev_len = data_queue.len(); chunk.update_stats(&mut chunk_stats); - //trace!("Capture rms {:?}, peak {:?}", chunk_stats.rms_db(), chunk_stats.peak_db()); { let mut capture_status = capture_status.write(); capture_status.signal_rms.add_record_squared(chunk_stats.rms_linear()); diff --git a/src/cpaldevice.rs b/src/cpaldevice.rs index 9a69bbc..0969151 100644 --- a/src/cpaldevice.rs +++ b/src/cpaldevice.rs @@ -691,7 +691,6 @@ impl CaptureDevice for CpalCaptureDevice { trace!("Measured sample rate is {:.1} Hz", measured_rate_f); } chunk.update_stats(&mut chunk_stats); - //trace!("Capture rms {:?}, peak {:?}", chunk_stats.rms_db(), chunk_stats.peak_db()); { let mut capture_status = capture_status.write(); capture_status.signal_rms.add_record_squared(chunk_stats.rms_linear()); diff --git a/src/filedevice.rs b/src/filedevice.rs index 8e09d3e..4c29e9d 100644 --- a/src/filedevice.rs +++ b/src/filedevice.rs @@ -185,11 +185,6 @@ impl PlaybackDevice for FilePlaybackDevice { .signal_peak .add_record(chunk_stats.peak_linear()); } - trace!( - "Playback signal RMS: {:?}, peak: {:?}", - chunk_stats.rms_db(), - chunk_stats.peak_db() - ); } Ok(AudioMessage::Pause) => { trace!("Pause message received"); @@ -489,11 +484,6 @@ fn capture_loop( ¶ms.capture_status.read().used_channels, ); chunk.update_stats(&mut chunk_stats); - //trace!( - // "Capture rms {:?}, peak {:?}", - // chunk_stats.rms_db(), - // chunk_stats.peak_db() - //); { let mut capture_status = params.capture_status.write(); capture_status diff --git a/src/lib.rs b/src/lib.rs index 75e0d28..c830c4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,6 +52,39 @@ use std::sync::{ Arc, }; +// Logging macros to give extra logs +// when the "debug" feature is enabled. +#[allow(unused)] +macro_rules! xtrace { ($($x:tt)*) => ( + #[cfg(feature = "debug")] { + log::trace!($($x)*) + } +) } +#[allow(unused)] +macro_rules! xdebug { ($($x:tt)*) => ( + #[cfg(feature = "debug")] { + log::debug!($($x)*) + } +) } +#[allow(unused)] +macro_rules! xinfo { ($($x:tt)*) => ( + #[cfg(feature = "debug")] { + log::info!($($x)*) + } +) } +#[allow(unused)] +macro_rules! xwarn { ($($x:tt)*) => ( + #[cfg(feature = "debug")] { + log::warn!($($x)*) + } +) } +#[allow(unused)] +macro_rules! xerror { ($($x:tt)*) => ( + #[cfg(feature = "debug")] { + log::error!($($x)*) + } +) } + // Sample format #[cfg(feature = "32bit")] pub type PrcFmt = f32; diff --git a/src/pulsedevice.rs b/src/pulsedevice.rs index fb36077..78c37b4 100644 --- a/src/pulsedevice.rs +++ b/src/pulsedevice.rs @@ -208,11 +208,6 @@ impl PlaybackDevice for PulsePlaybackDevice { .signal_peak .add_record(chunk_stats.peak_linear()); } - //trace!( - // "Playback signal RMS: {:?}, peak: {:?}", - // chunk_stats.rms_db(), - // chunk_stats.peak_db() - //); } Ok(AudioMessage::Pause) => { trace!("Pause message received"); @@ -251,13 +246,6 @@ fn nbr_capture_bytes( store_bytes_per_sample: usize, ) -> usize { if let Some(resampl) = &resampler { - //let new_capture_bytes = resampl.input_frames_next() * channels * store_bytes_per_sample; - //trace!( - // "Resampler needs {} frames, will read {} bytes", - // resampl.input_frames_next(), - // new_capture_bytes - //); - //new_capture_bytes resampl.input_frames_next() * channels * store_bytes_per_sample } else { capture_bytes @@ -381,7 +369,7 @@ impl CaptureDevice for PulseCaptureDevice { trace!( "Measured sample rate is {:.1} Hz, signal RMS is {:?}", measured_rate_f, - capture_status.signal_rms.last(), + capture_status.signal_rms.last_sqrt(), ); let mut capture_status = RwLockUpgradableReadGuard::upgrade(capture_status); // to write lock capture_status.measured_samplerate = measured_rate_f as usize; @@ -403,7 +391,6 @@ impl CaptureDevice for PulseCaptureDevice { capture_status.signal_rms.add_record_squared(chunk_stats.rms_linear()); capture_status.signal_peak.add_record(chunk_stats.peak_linear()); } - //trace!("Capture signal rms {:?}, peak {:?}", chunk_stats.rms_db(), chunk_stats.peak_db()); value_range = chunk.maxval - chunk.minval; state = silence_counter.update(value_range); if state == ProcessingState::Running { diff --git a/src/wasapidevice.rs b/src/wasapidevice.rs index 75a8fd2..8f1110f 100644 --- a/src/wasapidevice.rs +++ b/src/wasapidevice.rs @@ -1156,7 +1156,6 @@ impl CaptureDevice for WasapiCaptureDevice { &capture_status.read().used_channels, ); chunk.update_stats(&mut chunk_stats); - //trace!("Capture rms {:?}, peak {:?}", chunk_stats.rms_db(), chunk_stats.peak_db()); { let mut capture_status = capture_status.write(); capture_status.signal_rms.add_record_squared(chunk_stats.rms_linear()); From cacecbc17953fb1140bd38bc39c614a5bc0e46fb Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Sun, 7 Apr 2024 21:05:56 +0200 Subject: [PATCH 034/135] Update bluez backend for new nix --- src/filedevice_bluez.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/filedevice_bluez.rs b/src/filedevice_bluez.rs index 4fde2d9..49ce50a 100644 --- a/src/filedevice_bluez.rs +++ b/src/filedevice_bluez.rs @@ -17,32 +17,32 @@ pub struct WrappedBluezFd { impl WrappedBluezFd { fn new_from_open_message(r: Arc) -> WrappedBluezFd { let (pipe_fd, ctrl_fd): (OwnedFd, OwnedFd) = r.body().unwrap(); - return WrappedBluezFd { - pipe_fd: pipe_fd, + WrappedBluezFd { + pipe_fd, _ctrl_fd: ctrl_fd, _msg: r, - }; + } } } impl Read for WrappedBluezFd { fn read(&mut self, buf: &mut [u8]) -> io::Result { - nix::unistd::read(self.pipe_fd.as_raw_fd(), buf).map_err(|e| io::Error::from(e)) + nix::unistd::read(self.pipe_fd.as_raw_fd(), buf).map_err(io::Error::from) } } impl AsRawFd for WrappedBluezFd { fn as_raw_fd(&self) -> RawFd { - return self.pipe_fd.as_raw_fd(); + self.pipe_fd.as_raw_fd() } } -pub fn open_bluez_dbus_fd( +pub fn open_bluez_dbus_fd<'a>( service: String, path: String, chunksize: usize, samplerate: usize, -) -> Result>, zbus::Error> { +) -> Result>, zbus::Error> { let conn1 = Connection::system()?; let res = conn1.call_method(Some(service), path, Some("org.bluealsa.PCM1"), "Open", &())?; @@ -50,5 +50,5 @@ pub fn open_bluez_dbus_fd( WrappedBluezFd::new_from_open_message(res), 2 * 1000 * chunksize as u64 / samplerate as u64, )); - return Ok(reader); + Ok(reader) } From b1823f1c47129331e8cc3c7d24af6dcdc6380d98 Mon Sep 17 00:00:00 2001 From: Pavel Hofman Date: Thu, 4 Apr 2024 16:19:57 +0200 Subject: [PATCH 035/135] ALSA: Handle EPIPE errors as xruns Since EPIPE from snd_pcm_wait and snd_pcm_readi means buffer under/overrun, these errors are handled as xruns instead of exiting. Also handle EAGAIN on capture. --- src/alsadevice.rs | 66 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 600eddf..40eaced 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -180,11 +180,18 @@ fn play_buffer( return Ok(PlaybackResult::Stalled); } Err(err) => { - warn!( - "PB: device failed while waiting for available buffer space, error: {}", - err - ); - return Err(Box::new(err)); + if Errno::from_raw(err.errno()) == Errno::EPIPE { + warn!("PB: wait underrun, trying to recover. Error: {}", err); + trace!("snd_pcm_prepare"); + // Would recover() be better than prepare()? + pcmdevice.prepare()?; + } else { + warn!( + "PB: device failed while waiting for available buffer space, error: {}", + err + ); + return Err(Box::new(err)); + } } } @@ -210,18 +217,25 @@ fn play_buffer( continue; } } - Err(err) => { - if Errno::from_raw(err.errno()) == Errno::EAGAIN { + Err(err) => match Errno::from_raw(err.errno()) { + Errno::EAGAIN => { trace!("PB: encountered EAGAIN error on write, trying again"); - } else { - warn!("PB: write error, trying to recover. Error: {}", err); + continue; + } + Errno::EPIPE => { + warn!("PB: write underrun, trying to recover. Error: {}", err); + trace!("snd_pcm_prepare"); // Would recover() be better than prepare()? pcmdevice.prepare()?; buf_manager.sleep_for_target_delay(millis_per_frame); io.writei(buffer)?; break; } - } + _ => { + warn!("PB: write failed, error: {}", err); + return Err(Box::new(err)); + } + }, }; } Ok(PlaybackResult::Normal) @@ -278,11 +292,18 @@ fn capture_buffer( return Ok(CaptureResult::Stalled); } Err(err) => { - warn!( - "Capture device failed while waiting for available frames, error: {}", - err - ); - return Err(Box::new(err)); + if Errno::from_raw(err.errno()) == Errno::EPIPE { + warn!("Capture: wait overrun, trying to recover. Error: {}", err); + trace!("snd_pcm_prepare"); + // Would recover() be better than prepare()? + pcmdevice.prepare()?; + } else { + warn!( + "Capture: device failed while waiting for available frames, error: {}", + err + ); + return Err(Box::new(err)); + } } } match io.readi(buffer) { @@ -303,14 +324,19 @@ fn capture_buffer( } Err(err) => match Errno::from_raw(err.errno()) { Errno::EIO => { - warn!("Capture failed with error: {}", err); + warn!("Capture: read failed with error: {}", err); return Err(Box::new(err)); } - // TODO: do we need separate handling of xruns that happen in the tiny - // window between state() and readi()? + Errno::EAGAIN => { + trace!("Capture: encountered EAGAIN error on read, trying again"); + continue; + } Errno::EPIPE => { - warn!("Capture failed, error: {}", err); - return Err(Box::new(err)); + warn!("Capture: read overrun, trying to recover. Error: {}", err); + trace!("snd_pcm_prepare"); + // Would recover() be better than prepare()? + pcmdevice.prepare()?; + continue; } _ => { warn!("Capture failed, error: {}", err); From 60500a5fdbf53f23969b64069252b09a58efd9f2 Mon Sep 17 00:00:00 2001 From: Pavel Hofman Date: Thu, 4 Apr 2024 16:32:48 +0200 Subject: [PATCH 036/135] ALSA: Use immutable DeviceBufferManager Methods frames_to_stall() and sleep_for_target_delay() of DeviceBufferManager do not require mutable self reference. Removing mutable reference to buf_manager in the caller too. --- src/alsadevice.rs | 6 +++--- src/alsadevice_buffermanager.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 40eaced..1485c3b 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -122,7 +122,7 @@ fn play_buffer( io: &alsa::pcm::IO, millis_per_frame: f32, bytes_per_frame: usize, - buf_manager: &mut PlaybackBufferManager, + buf_manager: &PlaybackBufferManager, ) -> Res { let playback_state = pcmdevice.state_raw(); xtrace!("Playback state {:?}", playback_state); @@ -424,7 +424,7 @@ fn playback_loop_bytes( channels: PlaybackChannels, pcmdevice: &alsa::PCM, params: PlaybackParams, - buf_manager: &mut PlaybackBufferManager, + buf_manager: &PlaybackBufferManager, ) { let mut timer = countertimer::Stopwatch::new(); let mut chunk_stats = ChunkStats { @@ -990,7 +990,7 @@ impl PlaybackDevice for AlsaPlaybackDevice { audio: channel, status: status_channel, }; - playback_loop_bytes(pb_channels, &pcmdevice, pb_params, &mut buf_manager); + playback_loop_bytes(pb_channels, &pcmdevice, pb_params, &buf_manager); } Err(err) => { let send_result = diff --git a/src/alsadevice_buffermanager.rs b/src/alsadevice_buffermanager.rs index 33258e2..943898b 100644 --- a/src/alsadevice_buffermanager.rs +++ b/src/alsadevice_buffermanager.rs @@ -124,8 +124,8 @@ pub trait DeviceBufferManager { Ok(()) } - fn frames_to_stall(&mut self) -> Frames { - let data = self.data_mut(); + fn frames_to_stall(&self) -> Frames { + let data = self.data(); // +1 to make sure the device really stalls data.bufsize - data.avail_min + 1 } @@ -210,7 +210,7 @@ impl PlaybackBufferManager { } } - pub fn sleep_for_target_delay(&mut self, millis_per_frame: f32) { + pub fn sleep_for_target_delay(&self, millis_per_frame: f32) { let sleep_millis = (self.target_level as f32 * millis_per_frame) as u64; trace!( "Sleeping for {} frames = {} ms", From 095b06e84af3fc82a5aa98b536bb7fe8b1125c04 Mon Sep 17 00:00:00 2001 From: Pavel Hofman Date: Thu, 4 Apr 2024 16:35:21 +0200 Subject: [PATCH 037/135] ALSA: Move buf_manager trace Because buf_manager is immutable in method playback_loop_bytes(), tracing its contents can be moved up from the loop. --- src/alsadevice.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 1485c3b..73ad2cc 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -461,6 +461,7 @@ fn playback_loop_bytes( } let mut capture_speed: f64 = 1.0; let mut prev_delay_diff: Option = None; + trace!("PB: {:?}", buf_manager); loop { let eos_in_drain = if device_stalled { drain_check_eos(&channels.audio) @@ -490,7 +491,6 @@ fn playback_loop_bytes( conversion_result = chunk_to_buffer_rawbytes(&chunk, &mut buffer, ¶ms.sample_format); - trace!("PB: {:?}", buf_manager); let playback_res = play_buffer( &buffer, pcmdevice, From fd19ffa83c054debff1f875c2f5e322ad3c862c6 Mon Sep 17 00:00:00 2001 From: Pavel Hofman Date: Thu, 4 Apr 2024 16:38:23 +0200 Subject: [PATCH 038/135] ALSA: Log the actual period size used by the device --- src/alsadevice_buffermanager.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/alsadevice_buffermanager.rs b/src/alsadevice_buffermanager.rs index 943898b..6850868 100644 --- a/src/alsadevice_buffermanager.rs +++ b/src/alsadevice_buffermanager.rs @@ -91,6 +91,7 @@ pub trait DeviceBufferManager { hwp.set_period_size_near(alt_period_frames, alsa::ValueOr::Nearest)?; } } + debug!("Device is using a period size of {} frames", data.period); Ok(()) } From 52af5c3333553d51753555acc4bc4ed3b93d6bd3 Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 9 Apr 2024 22:53:20 +0200 Subject: [PATCH 039/135] Add command for getting all faders --- src/socketserver.rs | 27 +++++++++++++++++++++++++++ websocket.md | 4 ++++ 2 files changed, 31 insertions(+) diff --git a/src/socketserver.rs b/src/socketserver.rs index 145661d..fe923a0 100644 --- a/src/socketserver.rs +++ b/src/socketserver.rs @@ -104,6 +104,7 @@ enum WsCommand { GetMute, SetMute(bool), ToggleMute, + GetFaders, GetFaderVolume(usize), SetFaderVolume(usize, f32), SetFaderExternalVolume(usize, f32), @@ -147,6 +148,12 @@ struct PbCapLevels { capture: Vec, } +#[derive(Debug, PartialEq, Serialize)] +struct Fader { + volume: f32, + mute: bool, +} + #[derive(Debug, PartialEq, Serialize)] enum WsReply { SetConfigFilePath { @@ -315,6 +322,10 @@ enum WsReply { SetFaderExternalVolume { result: WsResult, }, + GetFaders { + result: WsResult, + value: Vec, + }, GetFaderVolume { result: WsResult, value: (usize, f32), @@ -890,6 +901,22 @@ fn handle_command( value: !tempmute, }) } + WsCommand::GetFaders => { + let volumes = shared_data_inst.processing_params.volumes(); + let mutes = shared_data_inst.processing_params.mutes(); + let faders = volumes + .iter() + .zip(mutes) + .map(|(v, m)| Fader { + volume: *v, + mute: m, + }) + .collect(); + Some(WsReply::GetFaders { + result: WsResult::Ok, + value: faders, + }) + } WsCommand::GetFaderVolume(ctrl) => { if ctrl > ProcessingParameters::NUM_FADERS - 1 { return Some(WsReply::GetFaderVolume { diff --git a/websocket.md b/websocket.md index 9fdd9a8..7a84ded 100644 --- a/websocket.md +++ b/websocket.md @@ -176,6 +176,10 @@ All commands take the fader number as the first parameter. - `ToggleFaderMute` : Toggle muting. * Returns a struct with the fader as an integer and the new muting status as a boolean. +There is also a command for getting the volume and mute settings for all faders with a single query. +- `GetFaders` : Read all faders. + * Returns a list of objects, each containing a `volume` and a `mute` property. + ### Config management Commands for reading and changing the active configuration. From e976cfa87e08d527aba1d332497e98883101eebc Mon Sep 17 00:00:00 2001 From: Henrik Date: Wed, 17 Apr 2024 22:46:19 +0200 Subject: [PATCH 040/135] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c43139..b33d43f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,14 @@ New features: - Optionally write wav header when outputting to file or stdout. - Add `WavFile` capture device type for reading wav files. - Optional limit for volume controls. +- Add websocket command for reading all faders with a single call. Changes: - Rename `File` capture device to `RawFile`. - Filter pipeline steps take a list of channels to filter instead of a single one. Bugfixes: - Windows: Fix compatibility issues for some WASAPI devices. - MacOS: Support devices appearing as separate capture and playback devices. +- Linux: Improved Alsa error handling. ## v2.0.3 Bugfixes: From 06c66f41a3854a8c9950d8fa943ed1be5c5afc44 Mon Sep 17 00:00:00 2001 From: Henrik Date: Wed, 17 Apr 2024 23:32:37 +0200 Subject: [PATCH 041/135] Update github actions --- .github/workflows/ci_test.yml | 14 +++++++------- .github/workflows/publish.yml | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci_test.yml b/.github/workflows/ci_test.yml index 202c024..bdc7d52 100644 --- a/.github/workflows/ci_test.yml +++ b/.github/workflows/ci_test.yml @@ -9,7 +9,7 @@ jobs: #container: ubuntu:20.04 steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Update package list run: sudo apt-get update @@ -57,7 +57,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable @@ -81,7 +81,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable @@ -105,7 +105,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable @@ -131,7 +131,7 @@ jobs: runs-on: windows-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable @@ -147,7 +147,7 @@ jobs: runs-on: macos-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable @@ -163,7 +163,7 @@ jobs: runs-on: macos-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e8fa04a..27bf859 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,7 +12,7 @@ jobs: #container: ubuntu:20.04 steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Update package list run: sudo apt-get update @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable @@ -85,7 +85,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable @@ -117,7 +117,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable @@ -149,7 +149,7 @@ jobs: runs-on: windows-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable @@ -173,7 +173,7 @@ jobs: runs-on: macos-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable @@ -198,7 +198,7 @@ jobs: runs-on: macos-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable From ca25bdaaadf6f9e95425101edec622c4206e05ad Mon Sep 17 00:00:00 2001 From: Henrik Date: Thu, 18 Apr 2024 20:46:13 +0200 Subject: [PATCH 042/135] Raise target_level limit for Alsa playback --- src/config.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/config.rs b/src/config.rs index 86e5dd6..1bdc40f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1748,12 +1748,17 @@ pub fn validate_config(conf: &mut Configuration, filename: Option<&str>) -> Res< if let Some(fname) = filename { replace_relative_paths_in_config(conf, fname); } + #[cfg(target_os = "linux")] + let target_level_limit = if matches!(conf.devices.playback, PlaybackDevice::Alsa { .. }) { + 4 * conf.devices.chunksize + } else { + 2 * conf.devices.chunksize + }; + #[cfg(not(target_os = "linux"))] + let target_level_limit = 2 * conf.devices.chunksize; - if conf.devices.target_level() >= 2 * conf.devices.chunksize { - let msg = format!( - "target_level can't be larger than {}", - 2 * conf.devices.chunksize - ); + if conf.devices.target_level() >= target_level_limit { + let msg = format!("target_level can't be larger than {}", target_level_limit); return Err(ConfigError::new(&msg).into()); } if let Some(period) = conf.devices.adjust_period { From ed6ef2b2ecf29a1ac8a959d827f56f5d186db1da Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Sat, 27 Apr 2024 11:15:01 +0200 Subject: [PATCH 043/135] WIP wait for pollfds --- src/alsadevice.rs | 181 ++++++++++++++++++++++++++++++++++++++-- src/alsadevice_utils.rs | 20 ++++- 2 files changed, 194 insertions(+), 7 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 73ad2cc..fb84640 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -5,22 +5,25 @@ use crate::config; use crate::config::SampleFormat; use crate::conversions::{buffer_to_chunk_rawbytes, chunk_to_buffer_rawbytes}; use crate::countertimer; -use alsa::ctl::{ElemId, ElemIface}; +use alsa::ctl::{Ctl, ElemId, ElemIface}; use alsa::ctl::{ElemType, ElemValue}; use alsa::hctl::{Elem, HCtl}; use alsa::pcm::{Access, Format, Frames, HwParams}; use alsa::{Direction, ValueOr, PCM}; +use alsa::poll::Descriptors; use alsa_sys; use nix::errno::Errno; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use rubato::VecResampler; use std::ffi::CString; use std::fmt::Debug; +use std::fs::File; use std::sync::mpsc; use std::sync::mpsc::Receiver; use std::sync::{Arc, Barrier}; use std::thread; use std::time::Instant; +use std::collections::HashSet; use crate::alsadevice_buffermanager::{ CaptureBufferManager, DeviceBufferManager, PlaybackBufferManager, @@ -40,6 +43,119 @@ lazy_static! { static ref ALSA_MUTEX: Mutex<()> = Mutex::new(()); } +struct ElemData<'a> { + element: Elem<'a>, + numid: u32, +} + +#[derive(Default)] +struct CaptureElements<'a> { + loopback_active: Option>, + loopback_rate: Option>, + loopback_format: Option>, + loopback_channels: Option>, + loopback_volume: Option>, + gadget_rate: Option>, + gadget_vol: Option>, +} + +struct FileDescriptors { + fds: Vec, + nbr_pcm_fds: usize, +} + +#[derive(Debug)] +struct PollResult { + poll_res: usize, + pcm: Option, + ctl: Option>, +} + +impl FileDescriptors { + fn wait(&mut self, ctl: &Option, timeout: i32) -> alsa::Result { + let nbr_ready = alsa::poll::poll(&mut self.fds, timeout)?; + let mut nbr_found = 0; + let mut pcm_res = None; + for fd in self.fds.iter().take(self.nbr_pcm_fds) { + if fd.revents > 0 { + pcm_res = Some(fd.revents); + nbr_found += 1; + if nbr_found == nbr_ready { + // We are done, let's return early + return Ok(PollResult{poll_res: nbr_ready, pcm: pcm_res, ctl: None}); + } + } + } + let mut ctl_res = None; + if let Some(c) = ctl { + for fd in self.fds.iter().skip(self.nbr_pcm_fds) { + if fd.revents > 0 { + nbr_found += 1; + if ctl_res.is_none() { + ctl_res = Some(HashSet::new()); + } + while let Some(ev) = c.read().unwrap() { + let nid = ev.get_id().get_numid(); + if let Some(hs) = &mut ctl_res { + hs.insert(nid); + } + } + if nbr_found == nbr_ready { + // We are done, skip the remaining + break; + } + } + } + } + Ok(PollResult{poll_res: nbr_ready, pcm: pcm_res, ctl: ctl_res}) + } +} + +fn handle_events(events: &HashSet, elems: &CaptureElements) { + for numid in events { + println!("Event from numid {}", numid); + if let Some(eldata) = &elems.loopback_active { + if eldata.numid == *numid { + println!("Active"); + } + } + else if let Some(eldata) = &elems.loopback_rate { + if eldata.numid == *numid { + println!("Rate"); + } + } + + } +} + + + +impl<'a> CaptureElements<'a> { + fn find_elements(&mut self, h:&'a HCtl, device: u32, subdevice: u32) { + self.loopback_active = find_elem(h, device, subdevice, "PCM Slave Active"); + self.loopback_rate = find_elem(h, device, subdevice, "PCM Slave Rate"); + self.loopback_format = find_elem(h, device, subdevice, "PCM Slave Format"); + self.loopback_channels = find_elem(h, device, subdevice, "PCM Slave Channels"); + self.loopback_volume = find_elem(h, device, subdevice, "PCM Playback Volume"); + self.gadget_rate = find_elem(h, device, subdevice, "Capture Rate"); + self.gadget_vol = find_elem(h, device, subdevice, "PCM Capture Volume"); + //also "PCM Playback Volume" and "Playback Rate" + } +} + + +fn find_elem<'a>(hctl: &'a HCtl, device: u32, subdevice: u32, name: &str) -> Option> { + let mut elem_id = ElemId::new(ElemIface::PCM); + elem_id.set_device(device); + elem_id.set_subdevice(subdevice); + elem_id.set_name(&CString::new(name).unwrap()); + let element = hctl.find_elem(&elem_id); + element.map(|e| { + let numid = e.get_id().map(|id| id.get_numid()).unwrap_or_default(); + ElemData {element: e, numid } + }) +} + pub struct AlsaPlaybackDevice { pub devname: String, pub samplerate: usize, @@ -249,6 +365,9 @@ fn capture_buffer( samplerate: usize, frames_to_read: usize, bytes_per_frame: usize, + fds: &mut FileDescriptors, + ctl: &Option, + hctl: &Option, ) -> Res { let capture_state = pcmdevice.state_raw(); if capture_state == alsa_sys::SND_PCM_STATE_XRUN as i32 { @@ -277,13 +396,13 @@ fn capture_buffer( if timeout_millis < 10 { timeout_millis = 10; } - let start = if log_enabled!(log::Level::Trace) { + let start = if log_enabled!(log::Level::Debug) { Some(Instant::now()) } else { None }; trace!("Capture pcmdevice.wait with timeout {} ms", timeout_millis); - match pcmdevice.wait(Some(timeout_millis)) { + /*match pcmdevice.wait(Some(timeout_millis)) { Ok(true) => { trace!("Capture waited for {:?}, ready", start.map(|s| s.elapsed())); } @@ -305,6 +424,34 @@ fn capture_buffer( return Err(Box::new(err)); } } + }*/ + match fds.wait(ctl, timeout_millis as i32) { + Ok(pollresult) => { + if pollresult.poll_res == 0 { + trace!("Wait timed out, capture device takes too long to capture frames"); + return Ok(CaptureResult::Stalled); + } + if let Some(revents) = pollresult.pcm { + debug!("Capture waited for {:?}, {}", start.map(|s| s.elapsed()), revents); + } + if let Some(ids) = pollresult.ctl { + debug!("Other events {:?}", ids); + } + } + Err(err) => { + if Errno::from_raw(err.errno()) == Errno::EPIPE { + warn!("Capture: wait overrun, trying to recover. Error: {}", err); + trace!("snd_pcm_prepare"); + // Would recover() be better than prepare()? + pcmdevice.prepare()?; + } else { + warn!( + "Capture: device failed while waiting for available frames, error: {}", + err + ); + return Err(Box::new(err)); + } + } } match io.readi(buffer) { Ok(frames_read) => { @@ -650,12 +797,29 @@ fn capture_loop_bytes( let device = pcminfo.get_device(); let subdevice = pcminfo.get_subdevice(); + let mut fds = pcmdevice.get().unwrap(); + println!("{:?}", fds); + let nbr_pcm_fds = fds.len(); + let mut file_descriptors = FileDescriptors {fds, nbr_pcm_fds}; + + let mut element_loopback: Option = None; let mut element_uac2_gadget: Option = None; + + let mut capture_elements = CaptureElements::default(); + // Virtual devices such as pcm plugins don't have a hw card ID // Only try to create the HCtl when the device has an ID - let h = (card >= 0).then(|| HCtl::new(&format!("hw:{}", card), false).unwrap()); - if let Some(h) = &h { + let hctl = (card >= 0).then(|| HCtl::new(&format!("hw:{}", card), false).unwrap()); + let ctl = (card >= 0).then(|| Ctl::new(&format!("hw:{}", card), true).unwrap()); + + if let Some(c) = &ctl { + c.subscribe_events(true).unwrap(); + } + if let Some(h) = &hctl { + let ctl_fds = h.get().unwrap(); + file_descriptors.fds.extend(ctl_fds.iter()); + println!("{:?}", file_descriptors.fds); h.load().unwrap(); let mut elid_loopback = ElemId::new(ElemIface::PCM); elid_loopback.set_device(device); @@ -668,8 +832,9 @@ fn capture_loop_bytes( elid_uac2_gadget.set_subdevice(subdevice); elid_uac2_gadget.set_name(&CString::new("Capture Pitch 1000000").unwrap()); element_uac2_gadget = h.find_elem(&elid_uac2_gadget); - } + capture_elements.find_elements(h, device, subdevice); + } if element_loopback.is_some() || element_uac2_gadget.is_some() { info!("Capture device supports rate adjust"); if params.samplerate == params.capture_samplerate && resampler.is_some() { @@ -773,6 +938,10 @@ fn capture_loop_bytes( params.capture_samplerate, capture_frames as usize, params.bytes_per_frame, + &mut file_descriptors, + &ctl, + &hctl + ); match capture_res { Ok(CaptureResult::Normal) => { diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index ea4c1b0..76d8bd0 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -3,9 +3,11 @@ use crate::Res; use alsa::card::Iter; use alsa::ctl::{Ctl, DeviceIter}; use alsa::device_name::HintIter; -use alsa::pcm::{Format, HwParams}; +use alsa::pcm::{Format, HwParams, PCM}; +use alsa::poll; use alsa::Card; use alsa::Direction; +use alsa::poll::Descriptors; use alsa_sys; const STANDARD_RATES: [u32; 17] = [ @@ -264,3 +266,19 @@ pub fn adjust_speed( pub fn is_within(value: f64, target: f64, equality_range: f64) -> bool { value <= (target + equality_range) && value >= (target - equality_range) } + +//pub fn snd_pcm_wait(pcm: &PCM, timeout: isize) +//{ + //if pcm.avail()? >= pcm.avail_min() { + // pcm.state(); + //} +// return snd_pcm_wait_nocheck(pcm, timeout); +//} + +pub fn snd_pcm_wait_nocheck(pcm: &PCM, timeout: i32) -> Res<()> +{ + let mut fds = pcm.get()?; + poll::poll(&mut fds, timeout)?; + Ok(()) +} + From b197c3466114e7de05e90219c4034bd4e5d1dd8e Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Sat, 27 Apr 2024 21:59:23 +0200 Subject: [PATCH 044/135] New poll works --- src/alsadevice.rs | 66 ++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index fb84640..66485e8 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -17,7 +17,6 @@ use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use rubato::VecResampler; use std::ffi::CString; use std::fmt::Debug; -use std::fs::File; use std::sync::mpsc; use std::sync::mpsc::Receiver; use std::sync::{Arc, Barrier}; @@ -67,50 +66,40 @@ struct FileDescriptors { #[derive(Debug)] struct PollResult { poll_res: usize, - pcm: Option, - ctl: Option>, + pcm: bool, + ctl: bool, } impl FileDescriptors { - fn wait(&mut self, ctl: &Option, timeout: i32) -> alsa::Result { + fn wait(&mut self, timeout: i32) -> alsa::Result { let nbr_ready = alsa::poll::poll(&mut self.fds, timeout)?; + debug!("Got {} ready fds", nbr_ready); let mut nbr_found = 0; - let mut pcm_res = None; + let mut pcm_res = false; for fd in self.fds.iter().take(self.nbr_pcm_fds) { if fd.revents > 0 { - pcm_res = Some(fd.revents); + pcm_res = true; nbr_found += 1; if nbr_found == nbr_ready { // We are done, let's return early - return Ok(PollResult{poll_res: nbr_ready, pcm: pcm_res, ctl: None}); + + return Ok(PollResult{poll_res: nbr_ready, pcm: pcm_res, ctl: false}); } } } - let mut ctl_res = None; - if let Some(c) = ctl { - for fd in self.fds.iter().skip(self.nbr_pcm_fds) { - if fd.revents > 0 { - nbr_found += 1; - if ctl_res.is_none() { - ctl_res = Some(HashSet::new()); - } - while let Some(ev) = c.read().unwrap() { - let nid = ev.get_id().get_numid(); - if let Some(hs) = &mut ctl_res { - hs.insert(nid); - } - } - if nbr_found == nbr_ready { - // We are done, skip the remaining - break; - } - } - } - } - Ok(PollResult{poll_res: nbr_ready, pcm: pcm_res, ctl: ctl_res}) + // There were more ready file descriptors than PCM, must be ctl + Ok(PollResult{poll_res: nbr_ready, pcm: pcm_res, ctl: true}) } } +fn read_events(ctl: &Ctl) { + while let Ok(Some(ev)) = ctl.read() { + let nid = ev.get_id().get_numid(); + debug!("Event from numid {}", nid); + } +} + + fn handle_events(events: &HashSet, elems: &CaptureElements) { for numid in events { println!("Event from numid {}", numid); @@ -425,17 +414,24 @@ fn capture_buffer( } } }*/ - match fds.wait(ctl, timeout_millis as i32) { + match fds.wait(timeout_millis as i32) { Ok(pollresult) => { if pollresult.poll_res == 0 { trace!("Wait timed out, capture device takes too long to capture frames"); return Ok(CaptureResult::Stalled); } - if let Some(revents) = pollresult.pcm { - debug!("Capture waited for {:?}, {}", start.map(|s| s.elapsed()), revents); + if pollresult.pcm { + debug!("Capture waited for {:?}", start.map(|s| s.elapsed())); } - if let Some(ids) = pollresult.ctl { - debug!("Other events {:?}", ids); + if pollresult.ctl { + debug!("Other events"); + if let Some(h) = hctl { + let ev = h.handle_events().unwrap(); + debug!("hctl handle events {}", ev); + } + if let Some(c) = ctl { + read_events(c); + } } } Err(err) => { @@ -810,7 +806,7 @@ fn capture_loop_bytes( // Virtual devices such as pcm plugins don't have a hw card ID // Only try to create the HCtl when the device has an ID - let hctl = (card >= 0).then(|| HCtl::new(&format!("hw:{}", card), false).unwrap()); + let hctl = (card >= 0).then(|| HCtl::new(&format!("hw:{}", card), true).unwrap()); let ctl = (card >= 0).then(|| Ctl::new(&format!("hw:{}", card), true).unwrap()); if let Some(c) = &ctl { From bfe2a371e3a0f8a8c0bfb4082a318369edf55285 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Sat, 27 Apr 2024 22:29:23 +0200 Subject: [PATCH 045/135] WIP handling of events --- src/alsadevice.rs | 182 +++++++++++++++++++++++++++------------- src/alsadevice_utils.rs | 12 ++- 2 files changed, 129 insertions(+), 65 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 66485e8..0a48935 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -9,8 +9,8 @@ use alsa::ctl::{Ctl, ElemId, ElemIface}; use alsa::ctl::{ElemType, ElemValue}; use alsa::hctl::{Elem, HCtl}; use alsa::pcm::{Access, Format, Frames, HwParams}; -use alsa::{Direction, ValueOr, PCM}; use alsa::poll::Descriptors; +use alsa::{Direction, ValueOr, PCM}; use alsa_sys; use nix::errno::Errno; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; @@ -22,7 +22,6 @@ use std::sync::mpsc::Receiver; use std::sync::{Arc, Barrier}; use std::thread; use std::time::Instant; -use std::collections::HashSet; use crate::alsadevice_buffermanager::{ CaptureBufferManager, DeviceBufferManager, PlaybackBufferManager, @@ -82,45 +81,109 @@ impl FileDescriptors { nbr_found += 1; if nbr_found == nbr_ready { // We are done, let's return early - - return Ok(PollResult{poll_res: nbr_ready, pcm: pcm_res, ctl: false}); + + return Ok(PollResult { + poll_res: nbr_ready, + pcm: pcm_res, + ctl: false, + }); } } } // There were more ready file descriptors than PCM, must be ctl - Ok(PollResult{poll_res: nbr_ready, pcm: pcm_res, ctl: true}) + Ok(PollResult { + poll_res: nbr_ready, + pcm: pcm_res, + ctl: true, + }) } } -fn read_events(ctl: &Ctl) { +fn read_events(ctl: &Ctl, elems: &CaptureElements) { while let Ok(Some(ev)) = ctl.read() { let nid = ev.get_id().get_numid(); debug!("Event from numid {}", nid); + handle_event(nid, elems); } } - -fn handle_events(events: &HashSet, elems: &CaptureElements) { - for numid in events { - println!("Event from numid {}", numid); - if let Some(eldata) = &elems.loopback_active { - if eldata.numid == *numid { - println!("Active"); - } +fn handle_event(numid: u32, elems: &CaptureElements) { + if let Some(eldata) = &elems.loopback_active { + if eldata.numid == numid { + let value = eldata + .element + .read() + .map(|v| v.get_boolean(0).unwrap()) + .unwrap(); + debug!("Loopback active: {}", value); + return; } - else if let Some(eldata) = &elems.loopback_rate { - if eldata.numid == *numid { - println!("Rate"); - } + } else if let Some(eldata) = &elems.loopback_rate { + if eldata.numid == numid { + let value = eldata + .element + .read() + .map(|v| v.get_integer(0).unwrap()) + .unwrap(); + debug!("Loopback rate: {}", value); + return; + } + } else if let Some(eldata) = &elems.loopback_format { + if eldata.numid == numid { + let value = eldata + .element + .read() + .map(|v| v.get_integer(0).unwrap()) + .unwrap(); + debug!("Loopback format: {}", value); + return; + } + } else if let Some(eldata) = &elems.loopback_channels { + if eldata.numid == numid { + let value = eldata + .element + .read() + .map(|v| v.get_integer(0).unwrap()) + .unwrap(); + debug!("Loopback channels: {}", value); + return; + } + } else if let Some(eldata) = &elems.loopback_volume { + if eldata.numid == numid { + let value = eldata + .element + .read() + .map(|v| v.get_integer(0).unwrap()) + .unwrap(); + debug!("Loopback volume: {}", value); + return; + } + } else if let Some(eldata) = &elems.gadget_vol { + if eldata.numid == numid { + let value = eldata + .element + .read() + .map(|v| v.get_integer(0).unwrap()) + .unwrap(); + debug!("Gadget volume: {}", value); + return; + } + } else if let Some(eldata) = &elems.gadget_rate { + if eldata.numid == numid { + let value = eldata + .element + .read() + .map(|v| v.get_integer(0).unwrap()) + .unwrap(); + debug!("Gadget rate: {}", value); + return; } - } + debug!("Ignoring event from unknown numid {}", numid); } - - impl<'a> CaptureElements<'a> { - fn find_elements(&mut self, h:&'a HCtl, device: u32, subdevice: u32) { + fn find_elements(&mut self, h: &'a HCtl, device: u32, subdevice: u32) { self.loopback_active = find_elem(h, device, subdevice, "PCM Slave Active"); self.loopback_rate = find_elem(h, device, subdevice, "PCM Slave Rate"); self.loopback_format = find_elem(h, device, subdevice, "PCM Slave Format"); @@ -132,7 +195,6 @@ impl<'a> CaptureElements<'a> { } } - fn find_elem<'a>(hctl: &'a HCtl, device: u32, subdevice: u32, name: &str) -> Option> { let mut elem_id = ElemId::new(ElemIface::PCM); elem_id.set_device(device); @@ -141,7 +203,7 @@ fn find_elem<'a>(hctl: &'a HCtl, device: u32, subdevice: u32, name: &str) -> Opt let element = hctl.find_elem(&elem_id); element.map(|e| { let numid = e.get_id().map(|id| id.get_numid()).unwrap_or_default(); - ElemData {element: e, numid } + ElemData { element: e, numid } }) } @@ -357,6 +419,7 @@ fn capture_buffer( fds: &mut FileDescriptors, ctl: &Option, hctl: &Option, + elems: &CaptureElements, ) -> Res { let capture_state = pcmdevice.state_raw(); if capture_state == alsa_sys::SND_PCM_STATE_XRUN as i32 { @@ -414,38 +477,42 @@ fn capture_buffer( } } }*/ - match fds.wait(timeout_millis as i32) { - Ok(pollresult) => { - if pollresult.poll_res == 0 { - trace!("Wait timed out, capture device takes too long to capture frames"); - return Ok(CaptureResult::Stalled); - } - if pollresult.pcm { - debug!("Capture waited for {:?}", start.map(|s| s.elapsed())); - } - if pollresult.ctl { - debug!("Other events"); - if let Some(h) = hctl { - let ev = h.handle_events().unwrap(); - debug!("hctl handle events {}", ev); + loop { + match fds.wait(timeout_millis as i32) { + Ok(pollresult) => { + if pollresult.poll_res == 0 { + trace!("Wait timed out, capture device takes too long to capture frames"); + return Ok(CaptureResult::Stalled); } - if let Some(c) = ctl { - read_events(c); + if pollresult.ctl { + debug!("Other events"); + if let Some(c) = ctl { + read_events(c, elems); + } + if let Some(h) = hctl { + let ev = h.handle_events().unwrap(); + debug!("hctl handle events {}", ev); + } + } + if pollresult.pcm { + debug!("Capture waited for {:?}", start.map(|s| s.elapsed())); + break; } } - } - Err(err) => { - if Errno::from_raw(err.errno()) == Errno::EPIPE { - warn!("Capture: wait overrun, trying to recover. Error: {}", err); - trace!("snd_pcm_prepare"); - // Would recover() be better than prepare()? - pcmdevice.prepare()?; - } else { - warn!( - "Capture: device failed while waiting for available frames, error: {}", - err - ); - return Err(Box::new(err)); + Err(err) => { + if Errno::from_raw(err.errno()) == Errno::EPIPE { + warn!("Capture: wait overrun, trying to recover. Error: {}", err); + trace!("snd_pcm_prepare"); + // Would recover() be better than prepare()? + pcmdevice.prepare()?; + break; + } else { + warn!( + "Capture: device failed while waiting for available frames, error: {}", + err + ); + return Err(Box::new(err)); + } } } } @@ -796,8 +863,7 @@ fn capture_loop_bytes( let mut fds = pcmdevice.get().unwrap(); println!("{:?}", fds); let nbr_pcm_fds = fds.len(); - let mut file_descriptors = FileDescriptors {fds, nbr_pcm_fds}; - + let mut file_descriptors = FileDescriptors { fds, nbr_pcm_fds }; let mut element_loopback: Option = None; let mut element_uac2_gadget: Option = None; @@ -808,7 +874,7 @@ fn capture_loop_bytes( // Only try to create the HCtl when the device has an ID let hctl = (card >= 0).then(|| HCtl::new(&format!("hw:{}", card), true).unwrap()); let ctl = (card >= 0).then(|| Ctl::new(&format!("hw:{}", card), true).unwrap()); - + if let Some(c) = &ctl { c.subscribe_events(true).unwrap(); } @@ -936,8 +1002,8 @@ fn capture_loop_bytes( params.bytes_per_frame, &mut file_descriptors, &ctl, - &hctl - + &hctl, + &capture_elements, ); match capture_res { Ok(CaptureResult::Normal) => { diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index 76d8bd0..bc975ef 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -5,9 +5,9 @@ use alsa::ctl::{Ctl, DeviceIter}; use alsa::device_name::HintIter; use alsa::pcm::{Format, HwParams, PCM}; use alsa::poll; +use alsa::poll::Descriptors; use alsa::Card; use alsa::Direction; -use alsa::poll::Descriptors; use alsa_sys; const STANDARD_RATES: [u32; 17] = [ @@ -269,16 +269,14 @@ pub fn is_within(value: f64, target: f64, equality_range: f64) -> bool { //pub fn snd_pcm_wait(pcm: &PCM, timeout: isize) //{ - //if pcm.avail()? >= pcm.avail_min() { - // pcm.state(); - //} +//if pcm.avail()? >= pcm.avail_min() { +// pcm.state(); +//} // return snd_pcm_wait_nocheck(pcm, timeout); //} -pub fn snd_pcm_wait_nocheck(pcm: &PCM, timeout: i32) -> Res<()> -{ +pub fn snd_pcm_wait_nocheck(pcm: &PCM, timeout: i32) -> Res<()> { let mut fds = pcm.get()?; poll::poll(&mut fds, timeout)?; Ok(()) } - From cbecf4ea5e1aec6e8418186d298e49a7dae6a31f Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Sat, 27 Apr 2024 23:18:39 +0200 Subject: [PATCH 046/135] Fix in event handling --- src/alsadevice.rs | 96 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 29 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 0a48935..6b081e8 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -49,9 +49,9 @@ struct ElemData<'a> { #[derive(Default)] struct CaptureElements<'a> { loopback_active: Option>, - loopback_rate: Option>, - loopback_format: Option>, - loopback_channels: Option>, + // loopback_rate: Option>, + // loopback_format: Option>, + // loopback_channels: Option>, loopback_volume: Option>, gadget_rate: Option>, gadget_vol: Option>, @@ -103,11 +103,18 @@ fn read_events(ctl: &Ctl, elems: &CaptureElements) { while let Ok(Some(ev)) = ctl.read() { let nid = ev.get_id().get_numid(); debug!("Event from numid {}", nid); - handle_event(nid, elems); + let action = get_event_action(nid, elems, ctl); } } -fn handle_event(numid: u32, elems: &CaptureElements) { +enum EventAction { + None, + SetVolume(f32), + FormatChange, + SourceInactive, +} + +fn get_event_action(numid: u32, elems: &CaptureElements, ctl: &Ctl) -> EventAction { if let Some(eldata) = &elems.loopback_active { if eldata.numid == numid { let value = eldata @@ -116,9 +123,15 @@ fn handle_event(numid: u32, elems: &CaptureElements) { .map(|v| v.get_boolean(0).unwrap()) .unwrap(); debug!("Loopback active: {}", value); - return; + if value { + return EventAction::None; + } + return EventAction::SourceInactive; } - } else if let Some(eldata) = &elems.loopback_rate { + } + // Include this if the notify functionality of the loopback gets fixed + /* + if let Some(eldata) = &elems.loopback_rate { if eldata.numid == numid { let value = eldata .element @@ -126,9 +139,10 @@ fn handle_event(numid: u32, elems: &CaptureElements) { .map(|v| v.get_integer(0).unwrap()) .unwrap(); debug!("Loopback rate: {}", value); - return; + return EventAction::FormatChange; } - } else if let Some(eldata) = &elems.loopback_format { + } + if let Some(eldata) = &elems.loopback_format { if eldata.numid == numid { let value = eldata .element @@ -136,9 +150,10 @@ fn handle_event(numid: u32, elems: &CaptureElements) { .map(|v| v.get_integer(0).unwrap()) .unwrap(); debug!("Loopback format: {}", value); - return; + return EventAction::FormatChange; } - } else if let Some(eldata) = &elems.loopback_channels { + } + if let Some(eldata) = &elems.loopback_channels { if eldata.numid == numid { let value = eldata .element @@ -146,29 +161,40 @@ fn handle_event(numid: u32, elems: &CaptureElements) { .map(|v| v.get_integer(0).unwrap()) .unwrap(); debug!("Loopback channels: {}", value); - return; + return EventAction::FormatChange; } - } else if let Some(eldata) = &elems.loopback_volume { + } */ + if let Some(eldata) = &elems.loopback_volume { if eldata.numid == numid { let value = eldata .element .read() .map(|v| v.get_integer(0).unwrap()) .unwrap(); - debug!("Loopback volume: {}", value); - return; + let vol_db = ctl + .convert_to_db(&eldata.element.get_id().unwrap(), value as i64) + .unwrap() + .to_db(); + debug!("Loopback volume: {} raw, {} dB", value, vol_db); + return EventAction::SetVolume(vol_db); } - } else if let Some(eldata) = &elems.gadget_vol { + } + if let Some(eldata) = &elems.gadget_vol { if eldata.numid == numid { let value = eldata .element .read() .map(|v| v.get_integer(0).unwrap()) .unwrap(); - debug!("Gadget volume: {}", value); - return; + let vol_db = ctl + .convert_to_db(&eldata.element.get_id().unwrap(), value as i64) + .unwrap() + .to_db(); + debug!("Gadget volume: {} raw, {} dB", value, vol_db); + return EventAction::SetVolume(vol_db); } - } else if let Some(eldata) = &elems.gadget_rate { + } + if let Some(eldata) = &elems.gadget_rate { if eldata.numid == numid { let value = eldata .element @@ -176,33 +202,45 @@ fn handle_event(numid: u32, elems: &CaptureElements) { .map(|v| v.get_integer(0).unwrap()) .unwrap(); debug!("Gadget rate: {}", value); - return; + if value == 0 { + return EventAction::SourceInactive; + } + return EventAction::FormatChange; } } debug!("Ignoring event from unknown numid {}", numid); + EventAction::None } impl<'a> CaptureElements<'a> { fn find_elements(&mut self, h: &'a HCtl, device: u32, subdevice: u32) { - self.loopback_active = find_elem(h, device, subdevice, "PCM Slave Active"); - self.loopback_rate = find_elem(h, device, subdevice, "PCM Slave Rate"); - self.loopback_format = find_elem(h, device, subdevice, "PCM Slave Format"); - self.loopback_channels = find_elem(h, device, subdevice, "PCM Slave Channels"); - self.loopback_volume = find_elem(h, device, subdevice, "PCM Playback Volume"); - self.gadget_rate = find_elem(h, device, subdevice, "Capture Rate"); - self.gadget_vol = find_elem(h, device, subdevice, "PCM Capture Volume"); + self.loopback_active = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Active"); + // self.loopback_rate = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Rate"); + // self.loopback_format = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Format"); + // self.loopback_channels = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Channels"); + self.loopback_volume = find_elem(h, ElemIface::Mixer, 0, 0, "PCM Playback Volume"); + self.gadget_rate = find_elem(h, ElemIface::PCM, device, subdevice, "Capture Rate"); + self.gadget_vol = find_elem(h, ElemIface::Mixer, device, subdevice, "PCM Capture Volume"); //also "PCM Playback Volume" and "Playback Rate" } } -fn find_elem<'a>(hctl: &'a HCtl, device: u32, subdevice: u32, name: &str) -> Option> { - let mut elem_id = ElemId::new(ElemIface::PCM); +fn find_elem<'a>( + hctl: &'a HCtl, + iface: ElemIface, + device: u32, + subdevice: u32, + name: &str, +) -> Option> { + let mut elem_id = ElemId::new(iface); elem_id.set_device(device); elem_id.set_subdevice(subdevice); elem_id.set_name(&CString::new(name).unwrap()); let element = hctl.find_elem(&elem_id); + debug!("Look up element with name {}", name); element.map(|e| { let numid = e.get_id().map(|id| id.get_numid()).unwrap_or_default(); + debug!("Found element with name {} and numid {}", name, numid); ElemData { element: e, numid } }) } From d6c20f7e8cc2b7fc26dee55e6ffe4c64a0aaae5d Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Sat, 27 Apr 2024 23:50:16 +0200 Subject: [PATCH 047/135] Send SetVolume message --- src/alsadevice.rs | 29 +++++++++++++++++++++++++---- src/bin.rs | 6 +++++- src/lib.rs | 1 + 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 6b081e8..7d29301 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -90,7 +90,7 @@ impl FileDescriptors { } } } - // There were more ready file descriptors than PCM, must be ctl + // There were other ready file descriptors than PCM, must be controls Ok(PollResult { poll_res: nbr_ready, pcm: pcm_res, @@ -99,11 +99,30 @@ impl FileDescriptors { } } -fn read_events(ctl: &Ctl, elems: &CaptureElements) { +fn process_events( + ctl: &Ctl, + elems: &CaptureElements, + status_channel: &crossbeam_channel::Sender, +) { while let Ok(Some(ev)) = ctl.read() { let nid = ev.get_id().get_numid(); debug!("Event from numid {}", nid); let action = get_event_action(nid, elems, ctl); + match action { + EventAction::SourceInactive => { + panic!("TODO stop nicely"); + } + EventAction::FormatChange => { + panic!("TODO stop nicely"); + } + EventAction::SetVolume(vol) => { + debug!("Set main fader to {} dB", vol); + status_channel + .send(StatusMessage::SetVolume(vol)) + .unwrap_or_default(); + } + EventAction::None => {} + } } } @@ -221,7 +240,7 @@ impl<'a> CaptureElements<'a> { self.loopback_volume = find_elem(h, ElemIface::Mixer, 0, 0, "PCM Playback Volume"); self.gadget_rate = find_elem(h, ElemIface::PCM, device, subdevice, "Capture Rate"); self.gadget_vol = find_elem(h, ElemIface::Mixer, device, subdevice, "PCM Capture Volume"); - //also "PCM Playback Volume" and "Playback Rate" + //also "PCM Playback Volume" and "Playback Rate" for Gadget playback side } } @@ -458,6 +477,7 @@ fn capture_buffer( ctl: &Option, hctl: &Option, elems: &CaptureElements, + status_channel: &crossbeam_channel::Sender, ) -> Res { let capture_state = pcmdevice.state_raw(); if capture_state == alsa_sys::SND_PCM_STATE_XRUN as i32 { @@ -525,7 +545,7 @@ fn capture_buffer( if pollresult.ctl { debug!("Other events"); if let Some(c) = ctl { - read_events(c, elems); + process_events(c, elems, status_channel); } if let Some(h) = hctl { let ev = h.handle_events().unwrap(); @@ -1042,6 +1062,7 @@ fn capture_loop_bytes( &ctl, &hctl, &capture_elements, + &channels.status, ); match capture_res { Ok(CaptureResult::Normal) => { diff --git a/src/bin.rs b/src/bin.rs index 3cdea98..9af546c 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -144,7 +144,7 @@ fn run( tx_pb, rx_cap, rx_pipeconf, - status_structs.processing, + status_structs.processing.clone(), ); // Playback thread @@ -391,6 +391,10 @@ fn run( debug!("Capture thread has already exited"); } } + StatusMessage::SetVolume(vol) => { + debug!("SetVolume message to {} dB received", vol); + status_structs.processing.set_target_volume(0, vol); + } }, Err(err) => { warn!("Capture, Playback and Processing threads have exited: {}", err); diff --git a/src/lib.rs b/src/lib.rs index c830c4f..7b5f38f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -160,6 +160,7 @@ pub enum StatusMessage { PlaybackDone, CaptureDone, SetSpeed(f64), + SetVolume(f32), } pub enum CommandMessage { From 930a1cf8a4ad43c4c1133cecd9ebcafd3146e56d Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Sat, 27 Apr 2024 23:55:14 +0200 Subject: [PATCH 048/135] Update TODOs --- src/alsadevice.rs | 4 ++-- src/config.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 7d29301..d8d6913 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -110,10 +110,10 @@ fn process_events( let action = get_event_action(nid, elems, ctl); match action { EventAction::SourceInactive => { - panic!("TODO stop nicely"); + panic!("TODO FD stop nicely"); } EventAction::FormatChange => { - panic!("TODO stop nicely"); + panic!("TODO FD stop nicely"); } EventAction::SetVolume(vol) => { debug!("Set main fader to {} dB", vol); diff --git a/src/config.rs b/src/config.rs index 1bdc40f..816ad78 100644 --- a/src/config.rs +++ b/src/config.rs @@ -192,6 +192,7 @@ pub enum CaptureDevice { channels: usize, device: String, format: SampleFormat, + // TODO FD add option for stopping on Loopback / Gadget going inactive }, #[cfg(all(target_os = "linux", feature = "bluez-backend"))] #[serde(alias = "BLUEZ", alias = "bluez")] From 988c91fcf37a460e27d85c96d77c87f115d539c4 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Sun, 28 Apr 2024 00:19:10 +0200 Subject: [PATCH 049/135] Options for reacting to Alsa control events --- src/alsadevice.rs | 50 ++++++++++++++++++++++++++++++++-------------- src/audiodevice.rs | 4 ++++ src/config.rs | 5 ++++- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index d8d6913..da82d53 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -103,6 +103,7 @@ fn process_events( ctl: &Ctl, elems: &CaptureElements, status_channel: &crossbeam_channel::Sender, + params: &CaptureParams, ) { while let Ok(Some(ev)) = ctl.read() { let nid = ev.get_id().get_numid(); @@ -110,16 +111,28 @@ fn process_events( let action = get_event_action(nid, elems, ctl); match action { EventAction::SourceInactive => { - panic!("TODO FD stop nicely"); + if params.stop_on_inactive { + status_channel + .send(StatusMessage::CaptureDone) + .unwrap_or_default(); + panic!("TODO FD stop nicely"); + } } EventAction::FormatChange => { - panic!("TODO FD stop nicely"); + if params.stop_on_inactive { + status_channel + .send(StatusMessage::CaptureFormatChange(0)) + .unwrap_or_default(); + panic!("TODO FD stop nicely"); + } } EventAction::SetVolume(vol) => { - debug!("Set main fader to {} dB", vol); - status_channel - .send(StatusMessage::SetVolume(vol)) - .unwrap_or_default(); + if params.use_virtual_volume { + debug!("Set main fader to {} dB", vol); + status_channel + .send(StatusMessage::SetVolume(vol)) + .unwrap_or_default(); + } } EventAction::None => {} } @@ -287,6 +300,8 @@ pub struct AlsaCaptureDevice { pub silence_timeout: PrcFmt, pub stop_on_rate_change: bool, pub rate_measure_interval: f32, + pub stop_on_inactive: bool, + pub use_virtual_volume: bool, } struct CaptureChannels { @@ -314,6 +329,8 @@ struct CaptureParams { capture_status: Arc>, stop_on_rate_change: bool, rate_measure_interval: f32, + stop_on_inactive: bool, + use_virtual_volume: bool, } struct PlaybackParams { @@ -466,18 +483,18 @@ fn play_buffer( } /// Capture a buffer. +#[allow(clippy::too_many_arguments)] fn capture_buffer( mut buffer: &mut [u8], pcmdevice: &alsa::PCM, io: &alsa::pcm::IO, - samplerate: usize, frames_to_read: usize, - bytes_per_frame: usize, fds: &mut FileDescriptors, ctl: &Option, hctl: &Option, elems: &CaptureElements, status_channel: &crossbeam_channel::Sender, + params: &CaptureParams, ) -> Res { let capture_state = pcmdevice.state_raw(); if capture_state == alsa_sys::SND_PCM_STATE_XRUN as i32 { @@ -499,7 +516,7 @@ fn capture_buffer( ); pcmdevice.start()?; } - let millis_per_chunk = 1000 * frames_to_read / samplerate; + let millis_per_chunk = 1000 * frames_to_read / params.samplerate; loop { let mut timeout_millis = 4 * millis_per_chunk as u32; @@ -545,7 +562,7 @@ fn capture_buffer( if pollresult.ctl { debug!("Other events"); if let Some(c) = ctl { - process_events(c, elems, status_channel); + process_events(c, elems, status_channel, params); } if let Some(h) = hctl { let ev = h.handle_events().unwrap(); @@ -576,7 +593,7 @@ fn capture_buffer( } match io.readi(buffer) { Ok(frames_read) => { - let frames_req = buffer.len() / bytes_per_frame; + let frames_req = buffer.len() / params.bytes_per_frame; if frames_read == frames_req { trace!("Capture read {} frames as requested", frames_read); return Ok(CaptureResult::Normal); @@ -585,7 +602,7 @@ fn capture_buffer( "Capture read {} frames instead of the requested {}", frames_read, frames_req ); - buffer = &mut buffer[frames_read * bytes_per_frame..]; + buffer = &mut buffer[frames_read * params.bytes_per_frame..]; // repeat reading continue; } @@ -918,7 +935,7 @@ fn capture_loop_bytes( let device = pcminfo.get_device(); let subdevice = pcminfo.get_subdevice(); - let mut fds = pcmdevice.get().unwrap(); + let fds = pcmdevice.get().unwrap(); println!("{:?}", fds); let nbr_pcm_fds = fds.len(); let mut file_descriptors = FileDescriptors { fds, nbr_pcm_fds }; @@ -1055,14 +1072,13 @@ fn capture_loop_bytes( &mut buffer[0..capture_bytes], pcmdevice, &io, - params.capture_samplerate, capture_frames as usize, - params.bytes_per_frame, &mut file_descriptors, &ctl, &hctl, &capture_elements, &channels.status, + ¶ms, ); match capture_res { Ok(CaptureResult::Normal) => { @@ -1321,6 +1337,8 @@ impl CaptureDevice for AlsaCaptureDevice { let async_src = resampler_is_async(&resampler_config); let stop_on_rate_change = self.stop_on_rate_change; let rate_measure_interval = self.rate_measure_interval; + let stop_on_inactive = self.stop_on_inactive; + let use_virtual_volume = self.use_virtual_volume; let mut buf_manager = CaptureBufferManager::new( chunksize as Frames, samplerate as f32 / capture_samplerate as f32, @@ -1365,6 +1383,8 @@ impl CaptureDevice for AlsaCaptureDevice { capture_status, stop_on_rate_change, rate_measure_interval, + stop_on_inactive, + use_virtual_volume, }; let cap_channels = CaptureChannels { audio: channel, diff --git a/src/audiodevice.rs b/src/audiodevice.rs index dc9211f..391dbea 100644 --- a/src/audiodevice.rs +++ b/src/audiodevice.rs @@ -554,6 +554,8 @@ pub fn new_capture_device(conf: config::Devices) -> Box { channels, ref device, format, + stop_on_inactive, + use_virtual_volume, } => Box::new(alsadevice::AlsaCaptureDevice { devname: device.clone(), samplerate: conf.samplerate, @@ -566,6 +568,8 @@ pub fn new_capture_device(conf: config::Devices) -> Box { silence_timeout: conf.silence_timeout(), stop_on_rate_change: conf.stop_on_rate_change(), rate_measure_interval: conf.rate_measure_interval(), + stop_on_inactive: stop_on_inactive.unwrap_or_default(), + use_virtual_volume: use_virtual_volume.unwrap_or_default(), }), #[cfg(feature = "pulse-backend")] config::CaptureDevice::Pulse { diff --git a/src/config.rs b/src/config.rs index 816ad78..4f4e703 100644 --- a/src/config.rs +++ b/src/config.rs @@ -192,7 +192,10 @@ pub enum CaptureDevice { channels: usize, device: String, format: SampleFormat, - // TODO FD add option for stopping on Loopback / Gadget going inactive + #[serde(default)] + stop_on_inactive: Option, + #[serde(default)] + use_virtual_volume: Option, }, #[cfg(all(target_os = "linux", feature = "bluez-backend"))] #[serde(alias = "BLUEZ", alias = "bluez")] From f59a186a50c1d105f5fdc8ff83e01df244225a13 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Mon, 29 Apr 2024 18:43:36 +0200 Subject: [PATCH 050/135] Check if rate changed --- src/alsadevice.rs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index da82d53..ee77654 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -108,7 +108,7 @@ fn process_events( while let Ok(Some(ev)) = ctl.read() { let nid = ev.get_id().get_numid(); debug!("Event from numid {}", nid); - let action = get_event_action(nid, elems, ctl); + let action = get_event_action(nid, elems, ctl, params); match action { EventAction::SourceInactive => { if params.stop_on_inactive { @@ -118,10 +118,10 @@ fn process_events( panic!("TODO FD stop nicely"); } } - EventAction::FormatChange => { + EventAction::FormatChange(value) => { if params.stop_on_inactive { status_channel - .send(StatusMessage::CaptureFormatChange(0)) + .send(StatusMessage::CaptureFormatChange(value)) .unwrap_or_default(); panic!("TODO FD stop nicely"); } @@ -142,11 +142,16 @@ fn process_events( enum EventAction { None, SetVolume(f32), - FormatChange, + FormatChange(usize), SourceInactive, } -fn get_event_action(numid: u32, elems: &CaptureElements, ctl: &Ctl) -> EventAction { +fn get_event_action( + numid: u32, + elems: &CaptureElements, + ctl: &Ctl, + params: &CaptureParams, +) -> EventAction { if let Some(eldata) = &elems.loopback_active { if eldata.numid == numid { let value = eldata @@ -171,7 +176,7 @@ fn get_event_action(numid: u32, elems: &CaptureElements, ctl: &Ctl) -> EventActi .map(|v| v.get_integer(0).unwrap()) .unwrap(); debug!("Loopback rate: {}", value); - return EventAction::FormatChange; + return EventAction::FormatChange(value); } } if let Some(eldata) = &elems.loopback_format { @@ -232,12 +237,16 @@ fn get_event_action(numid: u32, elems: &CaptureElements, ctl: &Ctl) -> EventActi .element .read() .map(|v| v.get_integer(0).unwrap()) - .unwrap(); + .unwrap() as usize; debug!("Gadget rate: {}", value); if value == 0 { return EventAction::SourceInactive; } - return EventAction::FormatChange; + if value != params.capture_samplerate { + return EventAction::FormatChange(value); + } + debug!("Capture device resumed with unchanged sample rate"); + return EventAction::None; } } debug!("Ignoring event from unknown numid {}", numid); From 235c49430d47d8843204a388d7f1f8b32174481c Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Mon, 29 Apr 2024 20:52:27 +0200 Subject: [PATCH 051/135] Stop nicely on events --- src/alsadevice.rs | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index ee77654..65f1158 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -104,7 +104,7 @@ fn process_events( elems: &CaptureElements, status_channel: &crossbeam_channel::Sender, params: &CaptureParams, -) { +) -> CaptureResult { while let Ok(Some(ev)) = ctl.read() { let nid = ev.get_id().get_numid(); debug!("Event from numid {}", nid); @@ -112,23 +112,25 @@ fn process_events( match action { EventAction::SourceInactive => { if params.stop_on_inactive { + debug!( + "Stopping, capture device is inactive and stop_on_inactive is set to true" + ); status_channel .send(StatusMessage::CaptureDone) .unwrap_or_default(); - panic!("TODO FD stop nicely"); + return CaptureResult::Done; } } EventAction::FormatChange(value) => { - if params.stop_on_inactive { - status_channel - .send(StatusMessage::CaptureFormatChange(value)) - .unwrap_or_default(); - panic!("TODO FD stop nicely"); - } + debug!("Stopping, capture device sample format changed"); + status_channel + .send(StatusMessage::CaptureFormatChange(value)) + .unwrap_or_default(); + return CaptureResult::Done; } EventAction::SetVolume(vol) => { if params.use_virtual_volume { - debug!("Set main fader to {} dB", vol); + debug!("Alsa volume change event, set main fader to {} dB", vol); status_channel .send(StatusMessage::SetVolume(vol)) .unwrap_or_default(); @@ -137,6 +139,7 @@ fn process_events( EventAction::None => {} } } + CaptureResult::Normal } enum EventAction { @@ -357,6 +360,7 @@ struct PlaybackParams { enum CaptureResult { Normal, Stalled, + Done, } #[derive(Debug)] @@ -569,9 +573,14 @@ fn capture_buffer( return Ok(CaptureResult::Stalled); } if pollresult.ctl { - debug!("Other events"); + debug!("Got a control events"); if let Some(c) = ctl { - process_events(c, elems, status_channel, params); + let event_result = process_events(c, elems, status_channel, params); + match event_result { + CaptureResult::Done => return Ok(event_result), + CaptureResult::Stalled => debug!("Capture device is stalled"), + CaptureResult::Normal => {} + }; } if let Some(h) = hctl { let ev = h.handle_events().unwrap(); @@ -1149,6 +1158,13 @@ fn capture_loop_bytes( params.capture_status.write().state = ProcessingState::Stalled; } } + Ok(CaptureResult::Done) => { + info!("Capture stopped"); + let msg = AudioMessage::EndOfStream; + channels.audio.send(msg).unwrap_or(()); + params.capture_status.write().state = ProcessingState::Inactive; + return; + } Err(msg) => { channels .status From 092176e72926d518da47a9511a33b6b5d119b8dc Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Mon, 29 Apr 2024 21:29:31 +0200 Subject: [PATCH 052/135] Update changelog and readme --- CHANGELOG.md | 1 + backend_alsa.md | 24 ++++++++++++++++++++++++ src/alsadevice.rs | 1 + 3 files changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b33d43f..a1fb89c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ New features: - Add `WavFile` capture device type for reading wav files. - Optional limit for volume controls. - Add websocket command for reading all faders with a single call. +- Linux: Subscribe to capture device control events for volume, sample rate and format changes. Changes: - Rename `File` capture device to `RawFile`. - Filter pipeline steps take a list of channels to filter instead of a single one. diff --git a/backend_alsa.md b/backend_alsa.md index 3cf1fff..15875fd 100644 --- a/backend_alsa.md +++ b/backend_alsa.md @@ -124,6 +124,8 @@ This example configuration will be used to explain the various options specific channels: 2 device: "hw:0,1" format: S16LE + stop_on_inactive: false (*) + use_virtual_volume: false (*) playback: type: Alsa channels: 2 @@ -137,6 +139,28 @@ See [Find name of device](#find-name-of-device) for what to write in the `device ### Sample rate and format Please see [Find valid playback and capture parameters](#find-valid-playback-and-capture-parameters). +### Linking volume control to device volume +When capturing from the Alsa loopback or the USB Audio Gadget, +it's possible to let CamillaDSP follow the device volume control. +Both of these devices provide a "dummy" volume control that does not alter the signal. +This can be used to forward the volume setting from a player to CamillaDSP. +To enable this, set the `use_virtual_volume` setting to `true`. +Any change of the loopback or gadget volume then gets applied +to the CamillaDSP main volume control. + +### Subscribe to Alsa control events +The Alsa capture device subscribes to control events from the USB Gadget and Loopback devices. +For the loopback, it subscribes to events from the `PCM Slave Active` control, +and for the gadget it subscribes to events from `Capture Rate`. +Both of these can indicate when playback has stopped. +If CamillaDSP should stop when that happens, set `stop_on_inactive` to `true`. +For the loopback, this means that CamillaDSP releases the capture side, +making it possible for a player application to re-open at another sample rate. + +For the gadget, the control can also indicate that the sample rate changed. +When this happens, the capture can no longer continue and CamillaDSP will stop. +The new sample rate can then be read by the `GetStopReason` websocket command. + ## Links ### ALSA Documentation https://www.alsa-project.org/wiki/Documentation diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 65f1158..e595cc3 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -989,6 +989,7 @@ fn capture_loop_bytes( element_uac2_gadget = h.find_elem(&elid_uac2_gadget); capture_elements.find_elements(h, device, subdevice); + // TODO FD read the volume at startup } if element_loopback.is_some() || element_uac2_gadget.is_some() { info!("Capture device supports rate adjust"); From ee8d5f6edd1ba5f3a9a38bcfffe615a95eb69e7f Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Wed, 1 May 2024 20:56:18 +0200 Subject: [PATCH 053/135] Clean up control reading --- src/alsadevice.rs | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index e595cc3..6c19e4c 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -69,6 +69,26 @@ struct PollResult { ctl: bool, } +impl<'a> ElemData<'a> { + fn read_as_int(&self) -> Option { + self + .element + .read() + .map(|elval| elval.get_integer(0)) + .ok() + .flatten() + } + + fn read_volume_in_db(&self, ctl: &Ctl) -> Option { + self.read_as_int() + .and_then(|intval| { + ctl.convert_to_db(&self.element.get_id().unwrap(), intval as i64) + .ok() + .map(|v| v.to_db()) + }) + } +} + impl FileDescriptors { fn wait(&mut self, timeout: i32) -> alsa::Result { let nbr_ready = alsa::poll::poll(&mut self.fds, timeout)?; @@ -559,7 +579,7 @@ fn capture_buffer( } else { warn!( "Capture: device failed while waiting for available frames, error: {}", - err + errq ); return Err(Box::new(err)); } @@ -989,7 +1009,22 @@ fn capture_loop_bytes( element_uac2_gadget = h.find_elem(&elid_uac2_gadget); capture_elements.find_elements(h, device, subdevice); - // TODO FD read the volume at startup + if let Some(c) = &ctl { + let mut vol_db = None; + if params.use_virtual_volume { + if let Some(ref vol_elem) = capture_elements.loopback_volume { + vol_db = vol_elem.read_volume_in_db(c); + } else if let Some(ref vol_elem) = capture_elements.gadget_vol { + vol_db = vol_elem.read_volume_in_db(c); + } + info!("Using initial volume from Alsa: {:?}", vol_db); + if let Some(vol) = vol_db { + channels.status + .send(StatusMessage::SetVolume(vol)) + .unwrap_or_default(); + } + } + } } if element_loopback.is_some() || element_uac2_gadget.is_some() { info!("Capture device supports rate adjust"); From 905568b5e2b100626b850e9bcbcffe2e3beb3248 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Wed, 1 May 2024 20:58:31 +0200 Subject: [PATCH 054/135] Simplify safe unwrapping --- src/alsadevice.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 6c19e4c..cd4c729 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -71,17 +71,14 @@ struct PollResult { impl<'a> ElemData<'a> { fn read_as_int(&self) -> Option { - self - .element + self.element .read() - .map(|elval| elval.get_integer(0)) .ok() - .flatten() + .and_then(|elval| elval.get_integer(0)) } fn read_volume_in_db(&self, ctl: &Ctl) -> Option { - self.read_as_int() - .and_then(|intval| { + self.read_as_int().and_then(|intval| { ctl.convert_to_db(&self.element.get_id().unwrap(), intval as i64) .ok() .map(|v| v.to_db()) @@ -1019,7 +1016,8 @@ fn capture_loop_bytes( } info!("Using initial volume from Alsa: {:?}", vol_db); if let Some(vol) = vol_db { - channels.status + channels + .status .send(StatusMessage::SetVolume(vol)) .unwrap_or_default(); } From 6722ff247883e5ad895e23c83429245756b88f8a Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Wed, 1 May 2024 21:27:21 +0200 Subject: [PATCH 055/135] Move more things to alsa_utils --- src/alsadevice.rs | 318 +------------------------------------- src/alsadevice_utils.rs | 328 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 315 insertions(+), 331 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index cd4c729..a21f1cd 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -1,12 +1,10 @@ extern crate alsa; extern crate nix; use crate::audiodevice::*; -use crate::config; -use crate::config::SampleFormat; +use crate::config::{Resampler, SampleFormat}; use crate::conversions::{buffer_to_chunk_rawbytes, chunk_to_buffer_rawbytes}; use crate::countertimer; -use alsa::ctl::{Ctl, ElemId, ElemIface}; -use alsa::ctl::{ElemType, ElemValue}; +use alsa::ctl::{Ctl, ElemId, ElemIface, ElemType, ElemValue}; use alsa::hctl::{Elem, HCtl}; use alsa::pcm::{Access, Format, Frames, HwParams}; use alsa::poll::Descriptors; @@ -17,9 +15,7 @@ use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use rubato::VecResampler; use std::ffi::CString; use std::fmt::Debug; -use std::sync::mpsc; -use std::sync::mpsc::Receiver; -use std::sync::{Arc, Barrier}; +use std::sync::{mpsc, Arc, Barrier}; use std::thread; use std::time::Instant; @@ -28,7 +24,8 @@ use crate::alsadevice_buffermanager::{ }; use crate::alsadevice_utils::{ adjust_speed, list_channels_as_text, list_device_names, list_formats_as_text, - list_samplerates_as_text, state_desc, + list_samplerates_as_text, process_events, state_desc, CaptureElements, CaptureParams, + CaptureResult, FileDescriptors, PlaybackParams, }; use crate::CommandMessage; use crate::PrcFmt; @@ -41,271 +38,6 @@ lazy_static! { static ref ALSA_MUTEX: Mutex<()> = Mutex::new(()); } -struct ElemData<'a> { - element: Elem<'a>, - numid: u32, -} - -#[derive(Default)] -struct CaptureElements<'a> { - loopback_active: Option>, - // loopback_rate: Option>, - // loopback_format: Option>, - // loopback_channels: Option>, - loopback_volume: Option>, - gadget_rate: Option>, - gadget_vol: Option>, -} - -struct FileDescriptors { - fds: Vec, - nbr_pcm_fds: usize, -} - -#[derive(Debug)] -struct PollResult { - poll_res: usize, - pcm: bool, - ctl: bool, -} - -impl<'a> ElemData<'a> { - fn read_as_int(&self) -> Option { - self.element - .read() - .ok() - .and_then(|elval| elval.get_integer(0)) - } - - fn read_volume_in_db(&self, ctl: &Ctl) -> Option { - self.read_as_int().and_then(|intval| { - ctl.convert_to_db(&self.element.get_id().unwrap(), intval as i64) - .ok() - .map(|v| v.to_db()) - }) - } -} - -impl FileDescriptors { - fn wait(&mut self, timeout: i32) -> alsa::Result { - let nbr_ready = alsa::poll::poll(&mut self.fds, timeout)?; - debug!("Got {} ready fds", nbr_ready); - let mut nbr_found = 0; - let mut pcm_res = false; - for fd in self.fds.iter().take(self.nbr_pcm_fds) { - if fd.revents > 0 { - pcm_res = true; - nbr_found += 1; - if nbr_found == nbr_ready { - // We are done, let's return early - - return Ok(PollResult { - poll_res: nbr_ready, - pcm: pcm_res, - ctl: false, - }); - } - } - } - // There were other ready file descriptors than PCM, must be controls - Ok(PollResult { - poll_res: nbr_ready, - pcm: pcm_res, - ctl: true, - }) - } -} - -fn process_events( - ctl: &Ctl, - elems: &CaptureElements, - status_channel: &crossbeam_channel::Sender, - params: &CaptureParams, -) -> CaptureResult { - while let Ok(Some(ev)) = ctl.read() { - let nid = ev.get_id().get_numid(); - debug!("Event from numid {}", nid); - let action = get_event_action(nid, elems, ctl, params); - match action { - EventAction::SourceInactive => { - if params.stop_on_inactive { - debug!( - "Stopping, capture device is inactive and stop_on_inactive is set to true" - ); - status_channel - .send(StatusMessage::CaptureDone) - .unwrap_or_default(); - return CaptureResult::Done; - } - } - EventAction::FormatChange(value) => { - debug!("Stopping, capture device sample format changed"); - status_channel - .send(StatusMessage::CaptureFormatChange(value)) - .unwrap_or_default(); - return CaptureResult::Done; - } - EventAction::SetVolume(vol) => { - if params.use_virtual_volume { - debug!("Alsa volume change event, set main fader to {} dB", vol); - status_channel - .send(StatusMessage::SetVolume(vol)) - .unwrap_or_default(); - } - } - EventAction::None => {} - } - } - CaptureResult::Normal -} - -enum EventAction { - None, - SetVolume(f32), - FormatChange(usize), - SourceInactive, -} - -fn get_event_action( - numid: u32, - elems: &CaptureElements, - ctl: &Ctl, - params: &CaptureParams, -) -> EventAction { - if let Some(eldata) = &elems.loopback_active { - if eldata.numid == numid { - let value = eldata - .element - .read() - .map(|v| v.get_boolean(0).unwrap()) - .unwrap(); - debug!("Loopback active: {}", value); - if value { - return EventAction::None; - } - return EventAction::SourceInactive; - } - } - // Include this if the notify functionality of the loopback gets fixed - /* - if let Some(eldata) = &elems.loopback_rate { - if eldata.numid == numid { - let value = eldata - .element - .read() - .map(|v| v.get_integer(0).unwrap()) - .unwrap(); - debug!("Loopback rate: {}", value); - return EventAction::FormatChange(value); - } - } - if let Some(eldata) = &elems.loopback_format { - if eldata.numid == numid { - let value = eldata - .element - .read() - .map(|v| v.get_integer(0).unwrap()) - .unwrap(); - debug!("Loopback format: {}", value); - return EventAction::FormatChange; - } - } - if let Some(eldata) = &elems.loopback_channels { - if eldata.numid == numid { - let value = eldata - .element - .read() - .map(|v| v.get_integer(0).unwrap()) - .unwrap(); - debug!("Loopback channels: {}", value); - return EventAction::FormatChange; - } - } */ - if let Some(eldata) = &elems.loopback_volume { - if eldata.numid == numid { - let value = eldata - .element - .read() - .map(|v| v.get_integer(0).unwrap()) - .unwrap(); - let vol_db = ctl - .convert_to_db(&eldata.element.get_id().unwrap(), value as i64) - .unwrap() - .to_db(); - debug!("Loopback volume: {} raw, {} dB", value, vol_db); - return EventAction::SetVolume(vol_db); - } - } - if let Some(eldata) = &elems.gadget_vol { - if eldata.numid == numid { - let value = eldata - .element - .read() - .map(|v| v.get_integer(0).unwrap()) - .unwrap(); - let vol_db = ctl - .convert_to_db(&eldata.element.get_id().unwrap(), value as i64) - .unwrap() - .to_db(); - debug!("Gadget volume: {} raw, {} dB", value, vol_db); - return EventAction::SetVolume(vol_db); - } - } - if let Some(eldata) = &elems.gadget_rate { - if eldata.numid == numid { - let value = eldata - .element - .read() - .map(|v| v.get_integer(0).unwrap()) - .unwrap() as usize; - debug!("Gadget rate: {}", value); - if value == 0 { - return EventAction::SourceInactive; - } - if value != params.capture_samplerate { - return EventAction::FormatChange(value); - } - debug!("Capture device resumed with unchanged sample rate"); - return EventAction::None; - } - } - debug!("Ignoring event from unknown numid {}", numid); - EventAction::None -} - -impl<'a> CaptureElements<'a> { - fn find_elements(&mut self, h: &'a HCtl, device: u32, subdevice: u32) { - self.loopback_active = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Active"); - // self.loopback_rate = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Rate"); - // self.loopback_format = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Format"); - // self.loopback_channels = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Channels"); - self.loopback_volume = find_elem(h, ElemIface::Mixer, 0, 0, "PCM Playback Volume"); - self.gadget_rate = find_elem(h, ElemIface::PCM, device, subdevice, "Capture Rate"); - self.gadget_vol = find_elem(h, ElemIface::Mixer, device, subdevice, "PCM Capture Volume"); - //also "PCM Playback Volume" and "Playback Rate" for Gadget playback side - } -} - -fn find_elem<'a>( - hctl: &'a HCtl, - iface: ElemIface, - device: u32, - subdevice: u32, - name: &str, -) -> Option> { - let mut elem_id = ElemId::new(iface); - elem_id.set_device(device); - elem_id.set_subdevice(subdevice); - elem_id.set_name(&CString::new(name).unwrap()); - let element = hctl.find_elem(&elem_id); - debug!("Look up element with name {}", name); - element.map(|e| { - let numid = e.get_id().map(|id| id.get_numid()).unwrap_or_default(); - debug!("Found element with name {} and numid {}", name, numid); - ElemData { element: e, numid } - }) -} - pub struct AlsaPlaybackDevice { pub devname: String, pub samplerate: usize, @@ -321,7 +53,7 @@ pub struct AlsaCaptureDevice { pub devname: String, pub samplerate: usize, pub capture_samplerate: usize, - pub resampler_config: Option, + pub resampler_config: Option, pub chunksize: usize, pub channels: usize, pub sample_format: SampleFormat, @@ -344,42 +76,6 @@ struct PlaybackChannels { status: crossbeam_channel::Sender, } -struct CaptureParams { - channels: usize, - sample_format: SampleFormat, - silence_timeout: PrcFmt, - silence_threshold: PrcFmt, - chunksize: usize, - store_bytes_per_sample: usize, - bytes_per_frame: usize, - samplerate: usize, - capture_samplerate: usize, - async_src: bool, - capture_status: Arc>, - stop_on_rate_change: bool, - rate_measure_interval: f32, - stop_on_inactive: bool, - use_virtual_volume: bool, -} - -struct PlaybackParams { - channels: usize, - target_level: usize, - adjust_period: f32, - adjust_enabled: bool, - sample_format: SampleFormat, - playback_status: Arc>, - bytes_per_frame: usize, - samplerate: usize, - chunksize: usize, -} - -enum CaptureResult { - Normal, - Stalled, - Done, -} - #[derive(Debug)] enum PlaybackResult { Normal, @@ -947,7 +643,7 @@ fn playback_loop_bytes( } } -fn drain_check_eos(audio: &Receiver) -> Option { +fn drain_check_eos(audio: &mpsc::Receiver) -> Option { let mut eos: Option = None; while let Some(msg) = audio.try_iter().next() { if let AudioMessage::EndOfStream = msg { diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index bc975ef..20bf428 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -1,14 +1,15 @@ use crate::config::SampleFormat; -use crate::Res; +use crate::{CaptureStatus, PlaybackStatus, PrcFmt, Res, StatusMessage}; use alsa::card::Iter; -use alsa::ctl::{Ctl, DeviceIter}; +use alsa::ctl::{Ctl, DeviceIter, ElemId, ElemIface}; use alsa::device_name::HintIter; -use alsa::pcm::{Format, HwParams, PCM}; -use alsa::poll; -use alsa::poll::Descriptors; -use alsa::Card; -use alsa::Direction; +use alsa::hctl::{Elem, HCtl}; +use alsa::pcm::{Format, HwParams}; +use alsa::{Card, Direction}; use alsa_sys; +use parking_lot::RwLock; +use std::ffi::CString; +use std::sync::Arc; const STANDARD_RATES: [u32; 17] = [ 5512, 8000, 11025, 16000, 22050, 32000, 44100, 48000, 64000, 88200, 96000, 176400, 192000, @@ -21,7 +22,43 @@ pub enum SupportedValues { Discrete(Vec), } -fn get_card_names(card: &Card, input: bool, names: &mut Vec<(String, String)>) -> Res<()> { +pub struct CaptureParams { + pub channels: usize, + pub sample_format: SampleFormat, + pub silence_timeout: PrcFmt, + pub silence_threshold: PrcFmt, + pub chunksize: usize, + pub store_bytes_per_sample: usize, + pub bytes_per_frame: usize, + pub samplerate: usize, + pub capture_samplerate: usize, + pub async_src: bool, + pub capture_status: Arc>, + pub stop_on_rate_change: bool, + pub rate_measure_interval: f32, + pub stop_on_inactive: bool, + pub use_virtual_volume: bool, +} + +pub struct PlaybackParams { + pub channels: usize, + pub target_level: usize, + pub adjust_period: f32, + pub adjust_enabled: bool, + pub sample_format: SampleFormat, + pub playback_status: Arc>, + pub bytes_per_frame: usize, + pub samplerate: usize, + pub chunksize: usize, +} + +pub enum CaptureResult { + Normal, + Stalled, + Done, +} + +pub fn get_card_names(card: &Card, input: bool, names: &mut Vec<(String, String)>) -> Res<()> { let dir = if input { Direction::Capture } else { @@ -267,16 +304,267 @@ pub fn is_within(value: f64, target: f64, equality_range: f64) -> bool { value <= (target + equality_range) && value >= (target - equality_range) } -//pub fn snd_pcm_wait(pcm: &PCM, timeout: isize) -//{ -//if pcm.avail()? >= pcm.avail_min() { -// pcm.state(); -//} -// return snd_pcm_wait_nocheck(pcm, timeout); -//} - -pub fn snd_pcm_wait_nocheck(pcm: &PCM, timeout: i32) -> Res<()> { - let mut fds = pcm.get()?; - poll::poll(&mut fds, timeout)?; - Ok(()) +pub struct ElemData<'a> { + element: Elem<'a>, + numid: u32, +} + +impl<'a> ElemData<'a> { + pub fn read_as_int(&self) -> Option { + self.element + .read() + .ok() + .and_then(|elval| elval.get_integer(0)) + } + + pub fn read_volume_in_db(&self, ctl: &Ctl) -> Option { + self.read_as_int().and_then(|intval| { + ctl.convert_to_db(&self.element.get_id().unwrap(), intval as i64) + .ok() + .map(|v| v.to_db()) + }) + } +} + +#[derive(Default)] +pub struct CaptureElements<'a> { + pub loopback_active: Option>, + // pub loopback_rate: Option>, + // pub loopback_format: Option>, + // pub loopback_channels: Option>, + pub loopback_volume: Option>, + pub gadget_rate: Option>, + pub gadget_vol: Option>, +} + +pub struct FileDescriptors { + pub fds: Vec, + pub nbr_pcm_fds: usize, +} + +#[derive(Debug)] +pub struct PollResult { + pub poll_res: usize, + pub pcm: bool, + pub ctl: bool, +} + +impl FileDescriptors { + pub fn wait(&mut self, timeout: i32) -> alsa::Result { + let nbr_ready = alsa::poll::poll(&mut self.fds, timeout)?; + debug!("Got {} ready fds", nbr_ready); + let mut nbr_found = 0; + let mut pcm_res = false; + for fd in self.fds.iter().take(self.nbr_pcm_fds) { + if fd.revents > 0 { + pcm_res = true; + nbr_found += 1; + if nbr_found == nbr_ready { + // We are done, let's return early + + return Ok(PollResult { + poll_res: nbr_ready, + pcm: pcm_res, + ctl: false, + }); + } + } + } + // There were other ready file descriptors than PCM, must be controls + Ok(PollResult { + poll_res: nbr_ready, + pcm: pcm_res, + ctl: true, + }) + } +} + +pub fn process_events( + ctl: &Ctl, + elems: &CaptureElements, + status_channel: &crossbeam_channel::Sender, + params: &CaptureParams, +) -> CaptureResult { + while let Ok(Some(ev)) = ctl.read() { + let nid = ev.get_id().get_numid(); + debug!("Event from numid {}", nid); + let action = get_event_action(nid, elems, ctl, params); + match action { + EventAction::SourceInactive => { + if params.stop_on_inactive { + debug!( + "Stopping, capture device is inactive and stop_on_inactive is set to true" + ); + status_channel + .send(StatusMessage::CaptureDone) + .unwrap_or_default(); + return CaptureResult::Done; + } + } + EventAction::FormatChange(value) => { + debug!("Stopping, capture device sample format changed"); + status_channel + .send(StatusMessage::CaptureFormatChange(value)) + .unwrap_or_default(); + return CaptureResult::Done; + } + EventAction::SetVolume(vol) => { + if params.use_virtual_volume { + debug!("Alsa volume change event, set main fader to {} dB", vol); + status_channel + .send(StatusMessage::SetVolume(vol)) + .unwrap_or_default(); + } + } + EventAction::None => {} + } + } + CaptureResult::Normal +} + +pub enum EventAction { + None, + SetVolume(f32), + FormatChange(usize), + SourceInactive, +} + +pub fn get_event_action( + numid: u32, + elems: &CaptureElements, + ctl: &Ctl, + params: &CaptureParams, +) -> EventAction { + if let Some(eldata) = &elems.loopback_active { + if eldata.numid == numid { + let value = eldata + .element + .read() + .map(|v| v.get_boolean(0).unwrap()) + .unwrap(); + debug!("Loopback active: {}", value); + if value { + return EventAction::None; + } + return EventAction::SourceInactive; + } + } + // Include this if the notify functionality of the loopback gets fixed + /* + if let Some(eldata) = &elems.loopback_rate { + if eldata.numid == numid { + let value = eldata + .element + .read() + .map(|v| v.get_integer(0).unwrap()) + .unwrap(); + debug!("Loopback rate: {}", value); + return EventAction::FormatChange(value); + } + } + if let Some(eldata) = &elems.loopback_format { + if eldata.numid == numid { + let value = eldata + .element + .read() + .map(|v| v.get_integer(0).unwrap()) + .unwrap(); + debug!("Loopback format: {}", value); + return EventAction::FormatChange; + } + } + if let Some(eldata) = &elems.loopback_channels { + if eldata.numid == numid { + let value = eldata + .element + .read() + .map(|v| v.get_integer(0).unwrap()) + .unwrap(); + debug!("Loopback channels: {}", value); + return EventAction::FormatChange; + } + } */ + if let Some(eldata) = &elems.loopback_volume { + if eldata.numid == numid { + let value = eldata + .element + .read() + .map(|v| v.get_integer(0).unwrap()) + .unwrap(); + let vol_db = ctl + .convert_to_db(&eldata.element.get_id().unwrap(), value as i64) + .unwrap() + .to_db(); + debug!("Loopback volume: {} raw, {} dB", value, vol_db); + return EventAction::SetVolume(vol_db); + } + } + if let Some(eldata) = &elems.gadget_vol { + if eldata.numid == numid { + let value = eldata + .element + .read() + .map(|v| v.get_integer(0).unwrap()) + .unwrap(); + let vol_db = ctl + .convert_to_db(&eldata.element.get_id().unwrap(), value as i64) + .unwrap() + .to_db(); + debug!("Gadget volume: {} raw, {} dB", value, vol_db); + return EventAction::SetVolume(vol_db); + } + } + if let Some(eldata) = &elems.gadget_rate { + if eldata.numid == numid { + let value = eldata + .element + .read() + .map(|v| v.get_integer(0).unwrap()) + .unwrap() as usize; + debug!("Gadget rate: {}", value); + if value == 0 { + return EventAction::SourceInactive; + } + if value != params.capture_samplerate { + return EventAction::FormatChange(value); + } + debug!("Capture device resumed with unchanged sample rate"); + return EventAction::None; + } + } + debug!("Ignoring event from unknown numid {}", numid); + EventAction::None +} + +impl<'a> CaptureElements<'a> { + pub fn find_elements(&mut self, h: &'a HCtl, device: u32, subdevice: u32) { + self.loopback_active = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Active"); + // self.loopback_rate = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Rate"); + // self.loopback_format = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Format"); + // self.loopback_channels = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Channels"); + self.loopback_volume = find_elem(h, ElemIface::Mixer, 0, 0, "PCM Playback Volume"); + self.gadget_rate = find_elem(h, ElemIface::PCM, device, subdevice, "Capture Rate"); + self.gadget_vol = find_elem(h, ElemIface::Mixer, device, subdevice, "PCM Capture Volume"); + //also "PCM Playback Volume" and "Playback Rate" for Gadget playback side + } +} + +pub fn find_elem<'a>( + hctl: &'a HCtl, + iface: ElemIface, + device: u32, + subdevice: u32, + name: &str, +) -> Option> { + let mut elem_id = ElemId::new(iface); + elem_id.set_device(device); + elem_id.set_subdevice(subdevice); + elem_id.set_name(&CString::new(name).unwrap()); + let element = hctl.find_elem(&elem_id); + debug!("Look up element with name {}", name); + element.map(|e| { + let numid = e.get_id().map(|id| id.get_numid()).unwrap_or_default(); + debug!("Found element with name {} and numid {}", name, numid); + ElemData { element: e, numid } + }) } From 312eac27b7ac4043cd6a264741ee11fc0e689152 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Wed, 1 May 2024 21:41:23 +0200 Subject: [PATCH 056/135] Use new read methods on events --- src/alsadevice_utils.rs | 117 ++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 65 deletions(-) diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index 20bf428..41f23d7 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -317,6 +317,13 @@ impl<'a> ElemData<'a> { .and_then(|elval| elval.get_integer(0)) } + pub fn read_as_bool(&self) -> Option { + self.element + .read() + .ok() + .and_then(|elval| elval.get_boolean(0)) + } + pub fn read_volume_in_db(&self, ctl: &Ctl) -> Option { self.read_as_int().and_then(|intval| { ctl.convert_to_db(&self.element.get_id().unwrap(), intval as i64) @@ -437,99 +444,79 @@ pub fn get_event_action( ) -> EventAction { if let Some(eldata) = &elems.loopback_active { if eldata.numid == numid { - let value = eldata - .element - .read() - .map(|v| v.get_boolean(0).unwrap()) - .unwrap(); - debug!("Loopback active: {}", value); - if value { - return EventAction::None; + let value = eldata.read_as_bool(); + debug!("Loopback active: {:?}", value); + if let Some(active) = value { + if active { + return EventAction::None; + } + return EventAction::SourceInactive; } - return EventAction::SourceInactive; } } // Include this if the notify functionality of the loopback gets fixed /* if let Some(eldata) = &elems.loopback_rate { if eldata.numid == numid { - let value = eldata - .element - .read() - .map(|v| v.get_integer(0).unwrap()) - .unwrap(); - debug!("Loopback rate: {}", value); - return EventAction::FormatChange(value); + let value = eldata.read_as_int(); + debug!("Gadget rate: {:?}", value); + if let Some(rate) = value { + debug!("Loopback rate: {}", rate); + return EventAction::FormatChange(rate); + } } } if let Some(eldata) = &elems.loopback_format { if eldata.numid == numid { - let value = eldata - .element - .read() - .map(|v| v.get_integer(0).unwrap()) - .unwrap(); - debug!("Loopback format: {}", value); - return EventAction::FormatChange; + let value = eldata.read_as_int(); + debug!("Gadget rate: {:?}", value); + if let Some(format) = value { + debug!("Loopback format: {}", format); + return EventAction::FormatChange(TODO add sample format!); + } } } if let Some(eldata) = &elems.loopback_channels { if eldata.numid == numid { - let value = eldata - .element - .read() - .map(|v| v.get_integer(0).unwrap()) - .unwrap(); - debug!("Loopback channels: {}", value); - return EventAction::FormatChange; + debug!("Gadget rate: {:?}", value); + if let Some(chans) = value { + debug!("Loopback channels: {}", chans); + return EventAction::FormatChange(TODO add channels!); + } } } */ if let Some(eldata) = &elems.loopback_volume { if eldata.numid == numid { - let value = eldata - .element - .read() - .map(|v| v.get_integer(0).unwrap()) - .unwrap(); - let vol_db = ctl - .convert_to_db(&eldata.element.get_id().unwrap(), value as i64) - .unwrap() - .to_db(); - debug!("Loopback volume: {} raw, {} dB", value, vol_db); - return EventAction::SetVolume(vol_db); + let vol_db = eldata.read_volume_in_db(ctl); + debug!("Loopback volume: {:?} dB", vol_db); + if let Some(vol) = vol_db { + return EventAction::SetVolume(vol); + } } } if let Some(eldata) = &elems.gadget_vol { if eldata.numid == numid { - let value = eldata - .element - .read() - .map(|v| v.get_integer(0).unwrap()) - .unwrap(); - let vol_db = ctl - .convert_to_db(&eldata.element.get_id().unwrap(), value as i64) - .unwrap() - .to_db(); - debug!("Gadget volume: {} raw, {} dB", value, vol_db); - return EventAction::SetVolume(vol_db); + let vol_db = eldata.read_volume_in_db(ctl); + debug!("Gadget volume: {:?} dB", vol_db); + if let Some(vol) = vol_db { + return EventAction::SetVolume(vol); + } } } if let Some(eldata) = &elems.gadget_rate { if eldata.numid == numid { - let value = eldata - .element - .read() - .map(|v| v.get_integer(0).unwrap()) - .unwrap() as usize; - debug!("Gadget rate: {}", value); - if value == 0 { - return EventAction::SourceInactive; - } - if value != params.capture_samplerate { - return EventAction::FormatChange(value); + let value = eldata.read_as_int(); + debug!("Gadget rate: {:?}", value); + if let Some(rate) = value { + if rate == 0 { + return EventAction::SourceInactive; + } + if rate as usize != params.capture_samplerate { + return EventAction::FormatChange(rate as usize); + } + debug!("Capture device resumed with unchanged sample rate"); + return EventAction::None; } - debug!("Capture device resumed with unchanged sample rate"); - return EventAction::None; } } debug!("Ignoring event from unknown numid {}", numid); From 769c9f73b19a6193a93bef2f5caeeabe00c066be Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Wed, 1 May 2024 22:08:04 +0200 Subject: [PATCH 057/135] Refactor elemet writing --- src/alsadevice.rs | 40 ++++++++++++++++++++-------------------- src/alsadevice_utils.rs | 9 ++++++++- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index a21f1cd..a299208 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -23,9 +23,9 @@ use crate::alsadevice_buffermanager::{ CaptureBufferManager, DeviceBufferManager, PlaybackBufferManager, }; use crate::alsadevice_utils::{ - adjust_speed, list_channels_as_text, list_device_names, list_formats_as_text, + adjust_speed, find_elem, list_channels_as_text, list_device_names, list_formats_as_text, list_samplerates_as_text, process_events, state_desc, CaptureElements, CaptureParams, - CaptureResult, FileDescriptors, PlaybackParams, + CaptureResult, ElemData, FileDescriptors, PlaybackParams, }; use crate::CommandMessage; use crate::PrcFmt; @@ -671,8 +671,8 @@ fn capture_loop_bytes( let nbr_pcm_fds = fds.len(); let mut file_descriptors = FileDescriptors { fds, nbr_pcm_fds }; - let mut element_loopback: Option = None; - let mut element_uac2_gadget: Option = None; + let mut element_loopback: Option = None; + let mut element_uac2_gadget: Option = None; let mut capture_elements = CaptureElements::default(); @@ -689,17 +689,20 @@ fn capture_loop_bytes( file_descriptors.fds.extend(ctl_fds.iter()); println!("{:?}", file_descriptors.fds); h.load().unwrap(); - let mut elid_loopback = ElemId::new(ElemIface::PCM); - elid_loopback.set_device(device); - elid_loopback.set_subdevice(subdevice); - elid_loopback.set_name(&CString::new("PCM Rate Shift 100000").unwrap()); - element_loopback = h.find_elem(&elid_loopback); - - let mut elid_uac2_gadget = ElemId::new(ElemIface::PCM); - elid_uac2_gadget.set_device(device); - elid_uac2_gadget.set_subdevice(subdevice); - elid_uac2_gadget.set_name(&CString::new("Capture Pitch 1000000").unwrap()); - element_uac2_gadget = h.find_elem(&elid_uac2_gadget); + element_loopback = find_elem( + h, + ElemIface::PCM, + device, + subdevice, + "PCM Rate Shift 100000", + ); + element_uac2_gadget = find_elem( + h, + ElemIface::PCM, + device, + subdevice, + "Capture Pitch 1000000", + ); capture_elements.find_elements(h, device, subdevice); if let Some(c) = &ctl { @@ -771,14 +774,11 @@ fn capture_loop_bytes( break; } Ok(CommandMessage::SetSpeed { speed }) => { - let mut elval = ElemValue::new(ElemType::Integer).unwrap(); rate_adjust = speed; if let Some(elem_loopback) = &element_loopback { - elval.set_integer(0, (100_000.0 / speed) as i32).unwrap(); - elem_loopback.write(&elval).unwrap(); + elem_loopback.write_as_int((100_000.0 / speed) as i32); } else if let Some(elem_uac2_gadget) = &element_uac2_gadget { - elval.set_integer(0, (speed * 1_000_000.0) as i32).unwrap(); - elem_uac2_gadget.write(&elval).unwrap(); + elem_uac2_gadget.write_as_int((speed * 1_000_000.0) as i32); } else if let Some(resampl) = &mut resampler { if params.async_src { if resampl.set_resample_ratio_relative(speed, true).is_err() { diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index 41f23d7..9a1fbd0 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -1,7 +1,7 @@ use crate::config::SampleFormat; use crate::{CaptureStatus, PlaybackStatus, PrcFmt, Res, StatusMessage}; use alsa::card::Iter; -use alsa::ctl::{Ctl, DeviceIter, ElemId, ElemIface}; +use alsa::ctl::{Ctl, DeviceIter, ElemId, ElemIface, ElemType, ElemValue}; use alsa::device_name::HintIter; use alsa::hctl::{Elem, HCtl}; use alsa::pcm::{Format, HwParams}; @@ -331,6 +331,13 @@ impl<'a> ElemData<'a> { .map(|v| v.to_db()) }) } + + pub fn write_as_int(&self, value: i32) { + let mut elval = ElemValue::new(ElemType::Integer).unwrap(); + if elval.set_integer(0, value).is_some() { + self.element.write(&elval).unwrap_or_default(); + } + } } #[derive(Default)] From 0b9298bfeba9be02a7a0a641816e00a6ffdc7634 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Wed, 1 May 2024 22:20:58 +0200 Subject: [PATCH 058/135] WIP make alsa sample format optional --- src/alsadevice.rs | 16 ++++++++-------- src/alsadevice_utils.rs | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index a299208..ab8c1e6 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -371,7 +371,7 @@ fn open_pcm( sample_format: &SampleFormat, buf_manager: &mut dyn DeviceBufferManager, capture: bool, -) -> Res { +) -> Res<(alsa::PCM, SampleFormat)> { let direction = if capture { "Capture" } else { "Playback" }; debug!( "Available {} devices: {:?}", @@ -433,7 +433,7 @@ fn open_pcm( pcmdev.sw_params(&swp)?; debug!("{} device \"{}\" successfully opened", direction, devname); } - Ok(pcmdev) + Ok((pcmdev, *sample_format)) } fn playback_loop_bytes( @@ -1014,7 +1014,7 @@ impl PlaybackDevice for AlsaPlaybackDevice { let chunksize = self.chunksize; let channels = self.channels; let bytes_per_sample = self.sample_format.bytes_per_sample(); - let sample_format = self.sample_format; + let conf_sample_format = self.sample_format; let mut buf_manager = PlaybackBufferManager::new(chunksize as Frames, target_level as Frames); let handle = thread::Builder::new() @@ -1024,11 +1024,11 @@ impl PlaybackDevice for AlsaPlaybackDevice { devname, samplerate as u32, channels as u32, - &sample_format, + &conf_sample_format, &mut buf_manager, false, ) { - Ok(pcmdevice) => { + Ok((pcmdevice, sample_format)) => { match status_channel.send(StatusMessage::PlaybackReady) { Ok(()) => {} Err(_err) => {} @@ -1087,7 +1087,7 @@ impl CaptureDevice for AlsaCaptureDevice { let store_bytes_per_sample = self.sample_format.bytes_per_sample(); let silence_timeout = self.silence_timeout; let silence_threshold = self.silence_threshold; - let sample_format = self.sample_format; + let conf_sample_format = self.sample_format; let resampler_config = self.resampler_config; let async_src = resampler_is_async(&resampler_config); let stop_on_rate_change = self.stop_on_rate_change; @@ -1113,11 +1113,11 @@ impl CaptureDevice for AlsaCaptureDevice { devname, capture_samplerate as u32, channels as u32, - &sample_format, + &conf_sample_format, &mut buf_manager, true, ) { - Ok(pcmdevice) => { + Ok((pcmdevice, sample_format)) => { match status_channel.send(StatusMessage::CaptureReady) { Ok(()) => {} Err(_err) => {} diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index 9a1fbd0..22f0386 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -248,6 +248,31 @@ pub fn list_formats(hwp: &HwParams) -> Res> { Ok(formats) } +pub fn pick_preferred_format(hwp: &HwParams) -> Option { + // Start with integer formats, in descending quality + if hwp.test_format(Format::s32()).is_ok() { + return Some(SampleFormat::S32LE); + } + // The two 24-bit formats are equivalent, the order does not matter + if hwp.test_format(Format::S243LE).is_ok() { + return Some(SampleFormat::S24LE3); + } + if hwp.test_format(Format::s24()).is_ok() { + return Some(SampleFormat::S24LE); + } + if hwp.test_format(Format::s16()).is_ok() { + return Some(SampleFormat::S16LE); + } + // float formats are unusual, try these last + if hwp.test_format(Format::float()).is_ok() { + return Some(SampleFormat::FLOAT32LE); + } + if hwp.test_format(Format::float64()).is_ok() { + return Some(SampleFormat::FLOAT64LE); + } + None +} + pub fn list_formats_as_text(hwp: &HwParams) -> String { let supported_formats_res = list_formats(hwp); if let Ok(formats) = supported_formats_res { From 090e126505ac43d487224dad5fd6ef5d6eb98f52 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Wed, 1 May 2024 23:17:38 +0200 Subject: [PATCH 059/135] Make Alsa sample format optional --- CHANGELOG.md | 1 + backend_alsa.md | 74 ++++++++++++++++++++++++++++++++++++----------- src/alsadevice.rs | 31 +++++++++++++------- src/config.rs | 8 +++-- 4 files changed, 83 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1fb89c..b01d6b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ New features: - Optional limit for volume controls. - Add websocket command for reading all faders with a single call. - Linux: Subscribe to capture device control events for volume, sample rate and format changes. +- Linux: Optionally select Alsa sample format automatically. Changes: - Rename `File` capture device to `RawFile`. - Filter pipeline steps take a list of channels to filter instead of a single one. diff --git a/backend_alsa.md b/backend_alsa.md index 15875fd..485ff0c 100644 --- a/backend_alsa.md +++ b/backend_alsa.md @@ -2,17 +2,27 @@ ## Introduction -ALSA is the low level audio API that is used in the Linux kernel. The ALSA project also maintains various user-space tools and utilities that are installed by default in most Linux distributions. +ALSA is the low level audio API that is used in the Linux kernel. +The ALSA project also maintains various user-space tools and utilities +that are installed by default in most Linux distributions. -This readme only covers some basics of ALSA. For more details, see for example the [ALSA Documentation](#alsa-documentation) and [A close look at ALSA](#a-close-look-at-alsa) +This readme only covers some basics of ALSA. For more details, +see for example the [ALSA Documentation](#alsa-documentation) and [A close look at ALSA](#a-close-look-at-alsa) ### Hardware devices -In the ALSA scheme, a soundcard or dac corresponds to a "card". A card can have one or several inputs and/or outputs, denoted "devices". Finally each device can support one or several streams, called "subdevices". It depends on the driver implementation how the different physical ports of a card is exposed in terms of devices. For example a 4-channel unit may present a single 4-channel device, or two separate 2-channel devices. +In the ALSA scheme, a soundcard or dac corresponds to a "card". +A card can have one or several inputs and/or outputs, denoted "devices". +Finally each device can support one or several streams, called "subdevices". +It depends on the driver implementation how the different physical ports of a card is exposed in terms of devices. +For example a 4-channel unit may present a single 4-channel device, or two separate 2-channel devices. ### PCM devices -An alsa PCM device can be many different things, like a simple alias for a hardware device, or any of the many plugins supported by ALSA. PCM devices are normally defined in the ALSA configuration file see the [ALSA Plugin Documentation](#alsa-plugin-documentation) for a list of the available plugins. +An alsa PCM device can be many different things, like a simple alias for a hardware device, +or any of the many plugins supported by ALSA. +PCM devices are normally defined in the ALSA configuration file. +See the [ALSA Plugin Documentation](#alsa-plugin-documentation) for a list of the available plugins. ### Find name of device To list all hardware playback devices use the `aplay` command with the `-l` option: @@ -33,7 +43,9 @@ hdmi:CARD=Generic,DEV=0 ``` Capture devices can be found in the same way with `arecord -l` and `arecord -L`. -A hardware device is accessed via the "hw" plugin. The device name is then prefixed by `hw:`. To use the ALC236 hardware device from above, put either `hw:Generic` (to use the name, recommended) or `hw:0` (to use the index) in the CamillaDSP config. +A hardware device is accessed via the "hw" plugin. The device name is then prefixed by `hw:`. +To use the ALC236 hardware device from above, +put either `hw:Generic` (to use the name, recommended) or `hw:0` (to use the index) in the CamillaDSP config. To instead use the "hdmi" PCM device, it's enough to give the name `hdmi`. @@ -66,12 +78,23 @@ Available formats: - S16_LE - S32_LE ``` -Ignore the error message at the end. The interesting fields are FORMAT, RATE and CHANNELS. In this example the sample formats this device can use are S16_LE and S32_LE (corresponding to S16LE and S32LE in CamillaDSP, see the [table of equivalent formats in the main README](./README.md#equivalent-formats) for the complete list). The sample rate can be either 44.1 or 48 kHz. And it supports only stereo playback (2 channels). +Ignore the error message at the end. The interesting fields are FORMAT, RATE and CHANNELS. +In this example the sample formats this device can use are S16_LE and S32_LE (corresponding to S16LE and S32LE in CamillaDSP, +see the [table of equivalent formats in the main README](./README.md#equivalent-formats) for the complete list). +The sample rate can be either 44.1 or 48 kHz. And it supports only stereo playback (2 channels). ### Combinations of parameter values -Note that all possible combinations of the shown parameters may not be supported by the device. For example many USB DACS only support 24-bit samples up to 96 kHz, so that only 16-bit samples are supported at 192 kHz. For other devices, the number of channels depends on the sample rate. This is common on studio interfaces that support [ADAT](#adat). +Note that all possible combinations of the shown parameters may not be supported by the device. +For example many USB DACS only support 24-bit samples up to 96 kHz, +so that only 16-bit samples are supported at 192 kHz. +For other devices, the number of channels depends on the sample rate. +This is common on studio interfaces that support [ADAT](#adat). -CamillaDSP sets first the number of channels. Then it sets sample rate, and finally sample format. Setting a value for a parameter may restrict the allowed values for the ones that have not yet been set. For the USB DAC just mentioned, setting the sample rate to 192 kHz means that only the S16LE sample format is allowed. If the CamillaDSP configuration is set to 192 kHz and S24LE3, then there will be an error when setting the format. +CamillaDSP sets first the number of channels. +Then it sets sample rate, and finally sample format. +Setting a value for a parameter may restrict the allowed values for the ones that have not yet been set. +For the USB DAC just mentioned, setting the sample rate to 192 kHz means that only the S16LE sample format is allowed. +If the CamillaDSP configuration is set to 192 kHz and S24LE3, then there will be an error when setting the format. Capture parameters are determined in the same way with `arecord`: @@ -82,10 +105,13 @@ This outputs the same table as for the aplay example above, but for a capture de ## Routing all audio through CamillaDSP -To route all audio through CamillaDSP using ALSA, the audio output from any application must be redirected. This can be acheived either by using an [ALSA Loopback device](#alsa-loopback), or the [ALSA CamillaDSP "I/O" plugin](#alsa-camilladsp-"io"-plugin). +To route all audio through CamillaDSP using ALSA, the audio output from any application must be redirected. +This can be acheived either by using an [ALSA Loopback device](#alsa-loopback), +or the [ALSA CamillaDSP "I/O" plugin](#alsa-camilladsp-"io"-plugin). ### ALSA Loopback -An ALSA Loopback card can be used. This behaves like a sound card that presents two devices. The sound being send to the playback side on one device can then be captured from the capture side on the other device. +An ALSA Loopback card can be used. This behaves like a sound card that presents two devices. +The sound being send to the playback side on one device can then be captured from the capture side on the other device. To load the kernel module type: ``` sudo modprobe snd-aloop @@ -103,7 +129,8 @@ The audio can then be captured from card "Loopback", device 0, subdevice 0, by r ``` arecord -D hw:Loopback,0,0 sometrack_copy.wav ``` -The first application that opens either side of a Loopback decides the sample rate and format. If `aplay` is started first in this example, this means that `arecord` must use the same sample rate and format. +The first application that opens either side of a Loopback decides the sample rate and format. +If `aplay` is started first in this example, this means that `arecord` must use the same sample rate and format. To change format or rate, both sides of the loopback must first be closed. When using the ALSA Loopback approach, see the separate repository [camilladsp-config](#camilladsp-config). @@ -111,9 +138,13 @@ This contains example configuration files for setting up the entire system, and ### ALSA CamillaDSP "I/O" plugin -ALSA can be extended by plugins in user-space. One such plugin that is intended specifically for CamillaDSP is the [ALSA CamillaDSP "I/O" plugin](#alsa-camilladsp-plugin) by scripple. +ALSA can be extended by plugins in user-space. +One such plugin that is intended specifically for CamillaDSP +is the [ALSA CamillaDSP "I/O" plugin](#alsa-camilladsp-plugin) by scripple. -The plugin starts CamillaDSP whenever an application opens the CamillaDSP plugin PCM device. This makes it possible to support automatic switching of the sample rate. See the plugin readme for how to install and configure it. +The plugin starts CamillaDSP whenever an application opens the CamillaDSP plugin PCM device. +This makes it possible to support automatic switching of the sample rate. +See the plugin readme for how to install and configure it. ## Configuration of devices @@ -123,21 +154,29 @@ This example configuration will be used to explain the various options specific type: Alsa channels: 2 device: "hw:0,1" - format: S16LE + format: S16LE (*) stop_on_inactive: false (*) use_virtual_volume: false (*) playback: type: Alsa channels: 2 device: "hw:Generic_1" - format: S32LE + format: S32LE (*) ``` ### Device names See [Find name of device](#find-name-of-device) for what to write in the `device` field. ### Sample rate and format -Please see [Find valid playback and capture parameters](#find-valid-playback-and-capture-parameters). +The sample format is optional. If set to `null` or left out, +the highest quality available format is chosen automatically. + +When the format is set automatically, 32-bit integer (`S32LE`) is considered the best, +followed by 24-bit (`S24LE3` and `S24LE`) and 16-bit integer (`S16LE`). +The 32-bit (`FLOAT32LE`) and 64-bit (`FLOAT64LE`) float formats are high quality, +but are supported by very few devices. Therefore these are checked last. + +Please also see [Find valid playback and capture parameters](#find-valid-playback-and-capture-parameters). ### Linking volume control to device volume When capturing from the Alsa loopback or the USB Audio Gadget, @@ -175,4 +214,5 @@ https://github.com/scripple/alsa_cdsp/ ## Notes ### ADAT -ADAT achieves higher sampling rates by multiplexing two or four 44.1/48kHz audio streams into a single one. A device implementing 8 channels over ADAT at 48kHz will therefore provide 4 channels over ADAT at 96kHz and 2 channels over ADAT at 192kHz. \ No newline at end of file +ADAT achieves higher sampling rates by multiplexing two or four 44.1/48kHz audio streams into a single one. +A device implementing 8 channels over ADAT at 48kHz will therefore provide 4 channels over ADAT at 96kHz and 2 channels over ADAT at 192kHz. \ No newline at end of file diff --git a/src/alsadevice.rs b/src/alsadevice.rs index ab8c1e6..c6758c8 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -24,8 +24,8 @@ use crate::alsadevice_buffermanager::{ }; use crate::alsadevice_utils::{ adjust_speed, find_elem, list_channels_as_text, list_device_names, list_formats_as_text, - list_samplerates_as_text, process_events, state_desc, CaptureElements, CaptureParams, - CaptureResult, ElemData, FileDescriptors, PlaybackParams, + list_samplerates_as_text, pick_preferred_format, process_events, state_desc, CaptureElements, + CaptureParams, CaptureResult, ElemData, FileDescriptors, PlaybackParams, }; use crate::CommandMessage; use crate::PrcFmt; @@ -43,7 +43,7 @@ pub struct AlsaPlaybackDevice { pub samplerate: usize, pub chunksize: usize, pub channels: usize, - pub sample_format: SampleFormat, + pub sample_format: Option, pub target_level: usize, pub adjust_period: f32, pub enable_rate_adjust: bool, @@ -56,7 +56,7 @@ pub struct AlsaCaptureDevice { pub resampler_config: Option, pub chunksize: usize, pub channels: usize, - pub sample_format: SampleFormat, + pub sample_format: Option, pub silence_threshold: PrcFmt, pub silence_timeout: PrcFmt, pub stop_on_rate_change: bool, @@ -368,7 +368,7 @@ fn open_pcm( devname: String, samplerate: u32, channels: u32, - sample_format: &SampleFormat, + sample_format: &Option, buf_manager: &mut dyn DeviceBufferManager, capture: bool, ) -> Res<(alsa::PCM, SampleFormat)> { @@ -387,6 +387,7 @@ fn open_pcm( alsa::PCM::new(&devname, Direction::Playback, true)? }; // Set hardware parameters + let chosen_format; { let hwp = HwParams::any(&pcmdev)?; @@ -402,8 +403,17 @@ fn open_pcm( // Set sample format debug!("{}: {}", direction, list_formats_as_text(&hwp)); - debug!("{}: setting format to {}", direction, sample_format); - match sample_format { + chosen_format = match sample_format { + Some(sfmt) => *sfmt, + None => { + let preferred = pick_preferred_format(&hwp) + .ok_or(DeviceError::new("Unable to find a supported sample format"))?; + debug!("{}: Picked sample format {}", direction, preferred); + preferred + } + }; + debug!("{}: setting format to {}", direction, chosen_format); + match chosen_format { SampleFormat::S16LE => hwp.set_format(Format::s16())?, SampleFormat::S24LE => hwp.set_format(Format::s24())?, SampleFormat::S24LE3 => hwp.set_format(Format::s24_3())?, @@ -433,7 +443,7 @@ fn open_pcm( pcmdev.sw_params(&swp)?; debug!("{} device \"{}\" successfully opened", direction, devname); } - Ok((pcmdev, *sample_format)) + Ok((pcmdev, chosen_format)) } fn playback_loop_bytes( @@ -1013,7 +1023,6 @@ impl PlaybackDevice for AlsaPlaybackDevice { let samplerate = self.samplerate; let chunksize = self.chunksize; let channels = self.channels; - let bytes_per_sample = self.sample_format.bytes_per_sample(); let conf_sample_format = self.sample_format; let mut buf_manager = PlaybackBufferManager::new(chunksize as Frames, target_level as Frames); @@ -1033,7 +1042,7 @@ impl PlaybackDevice for AlsaPlaybackDevice { Ok(()) => {} Err(_err) => {} } - + let bytes_per_sample = sample_format.bytes_per_sample(); barrier.wait(); debug!("Starting playback loop"); let pb_params = PlaybackParams { @@ -1084,7 +1093,6 @@ impl CaptureDevice for AlsaCaptureDevice { let chunksize = self.chunksize; let channels = self.channels; - let store_bytes_per_sample = self.sample_format.bytes_per_sample(); let silence_timeout = self.silence_timeout; let silence_threshold = self.silence_threshold; let conf_sample_format = self.sample_format; @@ -1122,6 +1130,7 @@ impl CaptureDevice for AlsaCaptureDevice { Ok(()) => {} Err(_err) => {} } + let store_bytes_per_sample = sample_format.bytes_per_sample(); barrier.wait(); debug!("Starting captureloop"); let cap_params = CaptureParams { diff --git a/src/config.rs b/src/config.rs index 4f4e703..ac08b98 100644 --- a/src/config.rs +++ b/src/config.rs @@ -191,7 +191,8 @@ pub enum CaptureDevice { #[serde(deserialize_with = "validate_nonzero_usize")] channels: usize, device: String, - format: SampleFormat, + #[serde(default)] + format: Option, #[serde(default)] stop_on_inactive: Option, #[serde(default)] @@ -422,7 +423,8 @@ pub enum PlaybackDevice { #[serde(deserialize_with = "validate_nonzero_usize")] channels: usize, device: String, - format: SampleFormat, + #[serde(default)] + format: Option, }, #[cfg(feature = "pulse-backend")] #[serde(alias = "PULSE", alias = "pulse")] @@ -1521,7 +1523,7 @@ fn apply_overrides(configuration: &mut Configuration) { } #[cfg(target_os = "linux")] CaptureDevice::Alsa { format, .. } => { - *format = fmt; + *format = Some(fmt); } #[cfg(all(target_os = "linux", feature = "bluez-backend"))] CaptureDevice::Bluez(dev) => { From 9890316739c7bfd176f262d12ff19ad76004fd3a Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Fri, 3 May 2024 18:12:02 +0200 Subject: [PATCH 060/135] Redo config for following a volume cotnrol --- src/alsadevice.rs | 48 +++++++++-------------------------------- src/alsadevice_utils.rs | 44 ++++++++++++++++--------------------- src/audiodevice.rs | 4 ++-- src/config.rs | 2 +- 4 files changed, 32 insertions(+), 66 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index c6758c8..e930a17 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -62,7 +62,7 @@ pub struct AlsaCaptureDevice { pub stop_on_rate_change: bool, pub rate_measure_interval: f32, pub stop_on_inactive: bool, - pub use_virtual_volume: bool, + pub follow_volume_control: Option, } struct CaptureChannels { @@ -249,35 +249,12 @@ fn capture_buffer( if timeout_millis < 10 { timeout_millis = 10; } - let start = if log_enabled!(log::Level::Debug) { + let start = if log_enabled!(log::Level::Trace) { Some(Instant::now()) } else { None }; trace!("Capture pcmdevice.wait with timeout {} ms", timeout_millis); - /*match pcmdevice.wait(Some(timeout_millis)) { - Ok(true) => { - trace!("Capture waited for {:?}, ready", start.map(|s| s.elapsed())); - } - Ok(false) => { - trace!("Wait timed out, capture device takes too long to capture frames"); - return Ok(CaptureResult::Stalled); - } - Err(err) => { - if Errno::from_raw(err.errno()) == Errno::EPIPE { - warn!("Capture: wait overrun, trying to recover. Error: {}", err); - trace!("snd_pcm_prepare"); - // Would recover() be better than prepare()? - pcmdevice.prepare()?; - } else { - warn!( - "Capture: device failed while waiting for available frames, error: {}", - errq - ); - return Err(Box::new(err)); - } - } - }*/ loop { match fds.wait(timeout_millis as i32) { Ok(pollresult) => { @@ -286,7 +263,7 @@ fn capture_buffer( return Ok(CaptureResult::Stalled); } if pollresult.ctl { - debug!("Got a control events"); + trace!("Got a control events"); if let Some(c) = ctl { let event_result = process_events(c, elems, status_channel, params); match event_result { @@ -297,11 +274,11 @@ fn capture_buffer( } if let Some(h) = hctl { let ev = h.handle_events().unwrap(); - debug!("hctl handle events {}", ev); + trace!("hctl handle events {}", ev); } } if pollresult.pcm { - debug!("Capture waited for {:?}", start.map(|s| s.elapsed())); + trace!("Capture waited for {:?}", start.map(|s| s.elapsed())); break; } } @@ -714,15 +691,10 @@ fn capture_loop_bytes( "Capture Pitch 1000000", ); - capture_elements.find_elements(h, device, subdevice); + capture_elements.find_elements(h, device, subdevice, ¶ms.follow_volume_control); if let Some(c) = &ctl { - let mut vol_db = None; - if params.use_virtual_volume { - if let Some(ref vol_elem) = capture_elements.loopback_volume { - vol_db = vol_elem.read_volume_in_db(c); - } else if let Some(ref vol_elem) = capture_elements.gadget_vol { - vol_db = vol_elem.read_volume_in_db(c); - } + if let Some(ref vol_elem) = capture_elements.volume { + let vol_db = vol_elem.read_volume_in_db(c); info!("Using initial volume from Alsa: {:?}", vol_db); if let Some(vol) = vol_db { channels @@ -1101,7 +1073,7 @@ impl CaptureDevice for AlsaCaptureDevice { let stop_on_rate_change = self.stop_on_rate_change; let rate_measure_interval = self.rate_measure_interval; let stop_on_inactive = self.stop_on_inactive; - let use_virtual_volume = self.use_virtual_volume; + let follow_volume_control = self.follow_volume_control.clone(); let mut buf_manager = CaptureBufferManager::new( chunksize as Frames, samplerate as f32 / capture_samplerate as f32, @@ -1148,7 +1120,7 @@ impl CaptureDevice for AlsaCaptureDevice { stop_on_rate_change, rate_measure_interval, stop_on_inactive, - use_virtual_volume, + follow_volume_control, }; let cap_channels = CaptureChannels { audio: channel, diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index 22f0386..74a9056 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -37,7 +37,7 @@ pub struct CaptureParams { pub stop_on_rate_change: bool, pub rate_measure_interval: f32, pub stop_on_inactive: bool, - pub use_virtual_volume: bool, + pub follow_volume_control: Option, } pub struct PlaybackParams { @@ -371,9 +371,8 @@ pub struct CaptureElements<'a> { // pub loopback_rate: Option>, // pub loopback_format: Option>, // pub loopback_channels: Option>, - pub loopback_volume: Option>, pub gadget_rate: Option>, - pub gadget_vol: Option>, + pub volume: Option>, } pub struct FileDescriptors { @@ -391,7 +390,7 @@ pub struct PollResult { impl FileDescriptors { pub fn wait(&mut self, timeout: i32) -> alsa::Result { let nbr_ready = alsa::poll::poll(&mut self.fds, timeout)?; - debug!("Got {} ready fds", nbr_ready); + trace!("Got {} ready fds", nbr_ready); let mut nbr_found = 0; let mut pcm_res = false; for fd in self.fds.iter().take(self.nbr_pcm_fds) { @@ -448,12 +447,10 @@ pub fn process_events( return CaptureResult::Done; } EventAction::SetVolume(vol) => { - if params.use_virtual_volume { - debug!("Alsa volume change event, set main fader to {} dB", vol); - status_channel - .send(StatusMessage::SetVolume(vol)) - .unwrap_or_default(); - } + debug!("Alsa volume change event, set main fader to {} dB", vol); + status_channel + .send(StatusMessage::SetVolume(vol)) + .unwrap_or_default(); } EventAction::None => {} } @@ -517,19 +514,10 @@ pub fn get_event_action( } } } */ - if let Some(eldata) = &elems.loopback_volume { - if eldata.numid == numid { - let vol_db = eldata.read_volume_in_db(ctl); - debug!("Loopback volume: {:?} dB", vol_db); - if let Some(vol) = vol_db { - return EventAction::SetVolume(vol); - } - } - } - if let Some(eldata) = &elems.gadget_vol { + if let Some(eldata) = &elems.volume { if eldata.numid == numid { let vol_db = eldata.read_volume_in_db(ctl); - debug!("Gadget volume: {:?} dB", vol_db); + debug!("Mixer volume control: {:?} dB", vol_db); if let Some(vol) = vol_db { return EventAction::SetVolume(vol); } @@ -556,15 +544,21 @@ pub fn get_event_action( } impl<'a> CaptureElements<'a> { - pub fn find_elements(&mut self, h: &'a HCtl, device: u32, subdevice: u32) { + pub fn find_elements( + &mut self, + h: &'a HCtl, + device: u32, + subdevice: u32, + volume_name: &Option, + ) { self.loopback_active = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Active"); // self.loopback_rate = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Rate"); // self.loopback_format = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Format"); // self.loopback_channels = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Channels"); - self.loopback_volume = find_elem(h, ElemIface::Mixer, 0, 0, "PCM Playback Volume"); self.gadget_rate = find_elem(h, ElemIface::PCM, device, subdevice, "Capture Rate"); - self.gadget_vol = find_elem(h, ElemIface::Mixer, device, subdevice, "PCM Capture Volume"); - //also "PCM Playback Volume" and "Playback Rate" for Gadget playback side + self.volume = volume_name + .as_ref() + .and_then(|name| find_elem(h, ElemIface::Mixer, 0, 0, name)); } } diff --git a/src/audiodevice.rs b/src/audiodevice.rs index 391dbea..f21e10b 100644 --- a/src/audiodevice.rs +++ b/src/audiodevice.rs @@ -555,7 +555,7 @@ pub fn new_capture_device(conf: config::Devices) -> Box { ref device, format, stop_on_inactive, - use_virtual_volume, + ref follow_volume_control, } => Box::new(alsadevice::AlsaCaptureDevice { devname: device.clone(), samplerate: conf.samplerate, @@ -569,7 +569,7 @@ pub fn new_capture_device(conf: config::Devices) -> Box { stop_on_rate_change: conf.stop_on_rate_change(), rate_measure_interval: conf.rate_measure_interval(), stop_on_inactive: stop_on_inactive.unwrap_or_default(), - use_virtual_volume: use_virtual_volume.unwrap_or_default(), + follow_volume_control: follow_volume_control.clone(), }), #[cfg(feature = "pulse-backend")] config::CaptureDevice::Pulse { diff --git a/src/config.rs b/src/config.rs index ac08b98..0dd5d79 100644 --- a/src/config.rs +++ b/src/config.rs @@ -196,7 +196,7 @@ pub enum CaptureDevice { #[serde(default)] stop_on_inactive: Option, #[serde(default)] - use_virtual_volume: Option, + follow_volume_control: Option, }, #[cfg(all(target_os = "linux", feature = "bluez-backend"))] #[serde(alias = "BLUEZ", alias = "bluez")] From b2d8a4b6c206fef79d533d4c6ccee7bc9e8b04bd Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Fri, 3 May 2024 18:18:33 +0200 Subject: [PATCH 061/135] Improved search for vol ctl --- src/alsadevice.rs | 8 ++++---- src/alsadevice_utils.rs | 30 +++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index e930a17..b3ff0e8 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -679,15 +679,15 @@ fn capture_loop_bytes( element_loopback = find_elem( h, ElemIface::PCM, - device, - subdevice, + Some(device), + Some(subdevice), "PCM Rate Shift 100000", ); element_uac2_gadget = find_elem( h, ElemIface::PCM, - device, - subdevice, + Some(device), + Some(subdevice), "Capture Pitch 1000000", ); diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index 74a9056..0869b38 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -551,27 +551,43 @@ impl<'a> CaptureElements<'a> { subdevice: u32, volume_name: &Option, ) { - self.loopback_active = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Active"); + self.loopback_active = find_elem( + h, + ElemIface::PCM, + Some(device), + Some(subdevice), + "PCM Slave Active", + ); // self.loopback_rate = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Rate"); // self.loopback_format = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Format"); // self.loopback_channels = find_elem(h, ElemIface::PCM, device, subdevice, "PCM Slave Channels"); - self.gadget_rate = find_elem(h, ElemIface::PCM, device, subdevice, "Capture Rate"); + self.gadget_rate = find_elem( + h, + ElemIface::PCM, + Some(device), + Some(subdevice), + "Capture Rate", + ); self.volume = volume_name .as_ref() - .and_then(|name| find_elem(h, ElemIface::Mixer, 0, 0, name)); + .and_then(|name| find_elem(h, ElemIface::Mixer, None, None, name)); } } pub fn find_elem<'a>( hctl: &'a HCtl, iface: ElemIface, - device: u32, - subdevice: u32, + device: Option, + subdevice: Option, name: &str, ) -> Option> { let mut elem_id = ElemId::new(iface); - elem_id.set_device(device); - elem_id.set_subdevice(subdevice); + if let Some(dev) = device { + elem_id.set_device(dev); + } + if let Some(subdev) = subdevice { + elem_id.set_subdevice(subdev); + } elem_id.set_name(&CString::new(name).unwrap()); let element = hctl.find_elem(&elem_id); debug!("Look up element with name {}", name); From d9fdab4fa186104654121e2afcc19878b089afcd Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Fri, 3 May 2024 18:31:28 +0200 Subject: [PATCH 062/135] Update alsa readme --- backend_alsa.md | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/backend_alsa.md b/backend_alsa.md index 485ff0c..e11dc20 100644 --- a/backend_alsa.md +++ b/backend_alsa.md @@ -156,7 +156,7 @@ This example configuration will be used to explain the various options specific device: "hw:0,1" format: S16LE (*) stop_on_inactive: false (*) - use_virtual_volume: false (*) + follow_volume_control: "PCM Playback Volume" (*) playback: type: Alsa channels: 2 @@ -179,13 +179,35 @@ but are supported by very few devices. Therefore these are checked last. Please also see [Find valid playback and capture parameters](#find-valid-playback-and-capture-parameters). ### Linking volume control to device volume -When capturing from the Alsa loopback or the USB Audio Gadget, -it's possible to let CamillaDSP follow the device volume control. -Both of these devices provide a "dummy" volume control that does not alter the signal. -This can be used to forward the volume setting from a player to CamillaDSP. -To enable this, set the `use_virtual_volume` setting to `true`. -Any change of the loopback or gadget volume then gets applied -to the CamillaDSP main volume control. +It is possible to let CamillaDSP follow the a volume control of the capture device. +This is mostly useful when capturing from the USB Audio Gadget, +which provides a control named `PCM Capture Volume` that is controlled by the USB host. + +This does not alter the signal, and can be used to forward the volume setting from a player to CamillaDSP. +To enable this, set the `follow_volume_control` setting to the name of the volume control. +Any change of the volume then gets applied to the CamillaDSP main volume control. + +The available controls for a device can be listed with `amixer`. +List controls for card 1: +```sh +amixer -c 1 controls +``` + +List controls with values and more details: +```sh +amixer -c 1 contents +``` + +The chosen control should be one that does not affect the signal volume, +otherwise the volume gets applied twice. +It must also have a scale in decibel like in this example: +``` +numid=15,iface=MIXER,name='Master Playback Volume' + ; type=INTEGER,access=rw---R--,values=1,min=0,max=87,step=0 + : values=52 + | dBscale-min=-65.25dB,step=0.75dB,mute=0 +``` + ### Subscribe to Alsa control events The Alsa capture device subscribes to control events from the USB Gadget and Loopback devices. From 91320776d745bfe21225e39bddd1c3dcff697135 Mon Sep 17 00:00:00 2001 From: Pavel Hofman Date: Sun, 5 May 2024 11:22:53 +0200 Subject: [PATCH 063/135] ALSA: Add a comment explaining how initial target delay is generated --- src/alsadevice.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 73ad2cc..104d34c 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -141,6 +141,8 @@ fn play_buffer( buf_manager.sleep_for_target_delay(millis_per_frame); } else if playback_state == alsa_sys::SND_PCM_STATE_PREPARED as i32 { info!("PB: Starting playback from Prepared state"); + // This sleep applies for the first chunk and in combination with the threshold=1 (i.e. start at first write) + // and the next chunk generates the initial target delay. buf_manager.sleep_for_target_delay(millis_per_frame); } else if playback_state != alsa_sys::SND_PCM_STATE_RUNNING as i32 { warn!( From 128e7d8e34a04e46b971f7db0058f45f5eea1d65 Mon Sep 17 00:00:00 2001 From: Henrik Date: Mon, 6 May 2024 21:02:59 +0200 Subject: [PATCH 064/135] WIP add a PI controller for better rate adjust --- src/config.rs | 2 +- src/coreaudiodevice.rs | 38 +++++++++++++++++++++++-------------- src/helpers.rs | 43 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/src/config.rs b/src/config.rs index 1bdc40f..3b12bfe 100644 --- a/src/config.rs +++ b/src/config.rs @@ -576,7 +576,7 @@ impl Devices { } pub fn adjust_period(&self) -> f32 { - self.adjust_period.unwrap_or(10.0) + self.adjust_period.unwrap_or(5.0) } pub fn rate_measure_interval(&self) -> f32 { diff --git a/src/coreaudiodevice.rs b/src/coreaudiodevice.rs index 0e2f281..de28cbd 100644 --- a/src/coreaudiodevice.rs +++ b/src/coreaudiodevice.rs @@ -3,6 +3,7 @@ use crate::config; use crate::config::{ConfigError, SampleFormat}; use crate::conversions::{buffer_to_chunk_rawbytes, chunk_to_buffer_rawbytes}; use crate::countertimer; +use crate::helpers::PIRateController; use crossbeam_channel::{bounded, TryRecvError, TrySendError}; use dispatch::Semaphore; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; @@ -404,6 +405,8 @@ impl PlaybackDevice for CoreaudioPlaybackDevice { // TODO check if always 512! //trace!("Estimated playback callback period to {} frames", callback_frames); + let mut rate_controller = PIRateController::new_with_default_gains(samplerate, adjust_period as f64, target_level); + let mut rate_adjust_value = 1.0; trace!("Build output stream"); let mut conversion_result; let mut sample_queue: VecDeque = @@ -524,22 +527,29 @@ impl PlaybackDevice for CoreaudioPlaybackDevice { buffer_avg.add_value(buffer_fill.load(Ordering::Relaxed) as f64); if adjust && timer.larger_than_millis((1000.0 * adjust_period) as u64) { if let Some(av_delay) = buffer_avg.average() { - let speed = calculate_speed( - av_delay, - target_level, - adjust_period, - samplerate as u32, - ); + let speed = rate_controller.next(av_delay); + let changed = (speed - rate_adjust_value).abs() > 0.000_001; + timer.restart(); buffer_avg.restart(); - debug!( - "Current buffer level {:.1}, set capture rate to {:.4}%", - av_delay, - 100.0 * speed - ); - status_channel - .send(StatusMessage::SetSpeed(speed)) - .unwrap_or(()); + if changed { + debug!( + "Current buffer level {:.1}, set capture rate to {:.4}%", + av_delay, + 100.0 * speed + ); + status_channel + .send(StatusMessage::SetSpeed(speed)) + .unwrap_or(()); + rate_adjust_value = speed; + } + else { + debug!( + "Current buffer level {:.1}, leaving capture rate at {:.4}%", + av_delay, + 100.0 * rate_adjust_value + ); + } playback_status.write().buffer_level = av_delay as usize; } } diff --git a/src/helpers.rs b/src/helpers.rs index 9932b60..1cb0d82 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -79,3 +79,46 @@ pub fn linear_to_db(values: &mut [f32]) { } }); } + +// A simple PI controller for rate adjustments +pub struct PIRateController { + target_level: f64, + interval: f64, + k_p: f64, + k_i: f64, + frames_per_interval: f64, + accumulated: f64, +} + +impl PIRateController { + /// Create a new controller with default gains + pub fn new_with_default_gains(fs: usize, interval: f64, target_level: usize) -> Self { + let k_p = 0.2; + let k_i = 0.004; + Self::new(fs, interval, target_level, k_p, k_i) + } + + pub fn new(fs: usize, interval: f64, target_level: usize, k_p: f64, k_i: f64) -> Self { + let frames_per_interval = interval * fs as f64; + Self { + target_level: target_level as f64, + interval, + k_p, + k_i, + frames_per_interval, + accumulated: 0.0, + } + } + + /// Calculate the control output for the next measured value + pub fn next(&mut self, level: f64) -> f64 { + let err = level - self.target_level; + let rel_diff = err / self.frames_per_interval; + self.accumulated += rel_diff * self.interval; + let proportional = self.k_p * rel_diff; + let integral = self.k_i * self.accumulated; + let mut rate_diff = proportional + integral; + rate_diff = rate_diff.clamp(-0.005, 0.005); + 1.0 - rate_diff + } +} From e216ee3cc232c814211551e3fbbe04015dbfd978 Mon Sep 17 00:00:00 2001 From: Henrik Date: Mon, 6 May 2024 21:05:13 +0200 Subject: [PATCH 065/135] Fix clippy warning --- src/bin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin.rs b/src/bin.rs index 3cdea98..f7dd411 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -765,7 +765,7 @@ fn main_process() -> i32 { if configname.is_none() { if let Some(s) = &state { - configname = s.config_path.clone(); + configname.clone_from(&s.config_path) } } From 6fcae2d171c7e2645c59c4786723de09debe098a Mon Sep 17 00:00:00 2001 From: Henrik Date: Wed, 8 May 2024 22:02:16 +0200 Subject: [PATCH 066/135] ramp buffer level slowly to avoid large changes --- Cargo.toml | 2 +- src/config.rs | 2 +- src/helpers.rs | 33 ++++++++++++++++++++++++++++++--- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 10cb386..94ddc07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ fftw = { version = "0.8.0", optional = true } num-complex = "0.4" num-traits = "0.2" signal-hook = "0.3.8" -rand = { version = "0.8.3", default_features = false, features = ["small_rng", "std"] } +rand = { version = "0.8.3", default-features = false, features = ["small_rng", "std"] } rand_distr = "0.4.3" clap = { version = "4.5.4", features = ["cargo"] } lazy_static = "1.4.0" diff --git a/src/config.rs b/src/config.rs index 269358a..0dd5d79 100644 --- a/src/config.rs +++ b/src/config.rs @@ -582,7 +582,7 @@ impl Devices { } pub fn adjust_period(&self) -> f32 { - self.adjust_period.unwrap_or(5.0) + self.adjust_period.unwrap_or(10.0) } pub fn rate_measure_interval(&self) -> f32 { diff --git a/src/helpers.rs b/src/helpers.rs index 1cb0d82..8661251 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -88,6 +88,10 @@ pub struct PIRateController { k_i: f64, frames_per_interval: f64, accumulated: f64, + ramp_steps: usize, + ramp_trigger_limit: f64, + ramp_start: f64, + ramp_step: usize, } impl PIRateController { @@ -95,10 +99,12 @@ impl PIRateController { pub fn new_with_default_gains(fs: usize, interval: f64, target_level: usize) -> Self { let k_p = 0.2; let k_i = 0.004; - Self::new(fs, interval, target_level, k_p, k_i) + let ramp_steps = 20; + let ramp_trigger_limit = 0.33; + Self::new(fs, interval, target_level, k_p, k_i, ramp_steps, ramp_trigger_limit) } - pub fn new(fs: usize, interval: f64, target_level: usize, k_p: f64, k_i: f64) -> Self { + pub fn new(fs: usize, interval: f64, target_level: usize, k_p: f64, k_i: f64, ramp_steps: usize, ramp_trigger_limit: f64) -> Self { let frames_per_interval = interval * fs as f64; Self { target_level: target_level as f64, @@ -107,12 +113,33 @@ impl PIRateController { k_i, frames_per_interval, accumulated: 0.0, + ramp_steps, + ramp_trigger_limit, + ramp_start: target_level as f64, + ramp_step: 0 } } /// Calculate the control output for the next measured value pub fn next(&mut self, level: f64) -> f64 { - let err = level - self.target_level; + if self.ramp_step >= self.ramp_steps && ((self.target_level - level)/self.target_level).abs() > self.ramp_trigger_limit { + self.ramp_start = level; + self.ramp_step = 0; + debug!("Rate controller, buffer level is {}, starting to adjust back towards target of {}", level, self.target_level); + } + if self.ramp_step == 0 { + self.ramp_start = level; + } + let current_target =if self.ramp_step < self.ramp_steps { + self.ramp_step += 1; + let tgt = self.ramp_start + (self.target_level - self.ramp_start) * (1.0 - ((self.ramp_steps as f64 - self.ramp_step as f64) / self.ramp_steps as f64).powi(4)); + debug!("Rate controller, ramp step {}/{}, current target {}", self.ramp_step, self.ramp_steps, tgt); + tgt + } + else { + self.target_level + }; + let err = level - current_target; let rel_diff = err / self.frames_per_interval; self.accumulated += rel_diff * self.interval; let proportional = self.k_p * rel_diff; From 2246eed82b49e474998f1493944eea644f47ee94 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Wed, 8 May 2024 22:31:16 +0200 Subject: [PATCH 067/135] New rate controller in Alsa backend --- src/alsadevice.rs | 52 ++++++++++++++++++++--------------------- src/alsadevice_utils.rs | 47 ------------------------------------- src/helpers.rs | 48 +++++++++++++++++++++++++++++-------- 3 files changed, 64 insertions(+), 83 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index a8e2bcb..9f348fa 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -23,10 +23,11 @@ use crate::alsadevice_buffermanager::{ CaptureBufferManager, DeviceBufferManager, PlaybackBufferManager, }; use crate::alsadevice_utils::{ - adjust_speed, find_elem, list_channels_as_text, list_device_names, list_formats_as_text, + find_elem, list_channels_as_text, list_device_names, list_formats_as_text, list_samplerates_as_text, pick_preferred_format, process_events, state_desc, CaptureElements, CaptureParams, CaptureResult, ElemData, FileDescriptors, PlaybackParams, }; +use crate::helpers::PIRateController; use crate::CommandMessage; use crate::PrcFmt; use crate::ProcessingState; @@ -464,8 +465,12 @@ fn playback_loop_bytes( if element_uac2_gadget.is_some() { info!("Playback device supports rate adjust"); } - let mut capture_speed: f64 = 1.0; - let mut prev_delay_diff: Option = None; + + let mut rate_controller = PIRateController::new_with_default_gains( + params.samplerate, + params.adjust_period as f64, + params.target_level, + ); trace!("PB: {:?}", buf_manager); loop { let eos_in_drain = if device_stalled { @@ -574,30 +579,22 @@ fn playback_loop_bytes( timer.restart(); buffer_avg.restart(); if adjust { - let (new_capture_speed, new_delay_diff) = adjust_speed( - avg_delay, - params.target_level, - prev_delay_diff, - capture_speed, - ); - if prev_delay_diff.is_some() { - // not first cycle - capture_speed = new_capture_speed; - if let Some(elem_uac2_gadget) = &element_uac2_gadget { - let mut elval = ElemValue::new(ElemType::Integer).unwrap(); - // speed is reciprocal on playback side - elval - .set_integer(0, (1_000_000.0 / capture_speed) as i32) - .unwrap(); - elem_uac2_gadget.write(&elval).unwrap(); - } else { - channels - .status - .send(StatusMessage::SetSpeed(capture_speed)) - .unwrap_or(()); - } + let capture_speed = rate_controller.next(avg_delay); + if let Some(elem_uac2_gadget) = &element_uac2_gadget { + let mut elval = ElemValue::new(ElemType::Integer).unwrap(); + // speed is reciprocal on playback side + elval + .set_integer(0, (1_000_000.0 / capture_speed) as i32) + .unwrap(); + elem_uac2_gadget.write(&elval).unwrap(); + debug!("Set gadget playback speed to {}", capture_speed); + } else { + debug!("Send SetSpeed message for speed {}", capture_speed); + channels + .status + .send(StatusMessage::SetSpeed(capture_speed)) + .unwrap_or(()); } - prev_delay_diff = Some(new_delay_diff); } let mut playback_status = params.playback_status.write(); playback_status.buffer_level = avg_delay as usize; @@ -760,11 +757,14 @@ fn capture_loop_bytes( Ok(CommandMessage::SetSpeed { speed }) => { rate_adjust = speed; if let Some(elem_loopback) = &element_loopback { + debug!("Setting capture loopback speed to {}", speed); elem_loopback.write_as_int((100_000.0 / speed) as i32); } else if let Some(elem_uac2_gadget) = &element_uac2_gadget { + debug!("Setting capture gadget speed to {}", speed); elem_uac2_gadget.write_as_int((speed * 1_000_000.0) as i32); } else if let Some(resampl) = &mut resampler { if params.async_src { + debug!("Setting async resampler speed to {}", speed); if resampl.set_resample_ratio_relative(speed, true).is_err() { debug!("Failed to set resampling speed to {}", speed); } diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index 0869b38..c21e203 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -282,53 +282,6 @@ pub fn list_formats_as_text(hwp: &HwParams) -> String { } } -pub fn adjust_speed( - avg_delay: f64, - target_delay: usize, - prev_diff: Option, - mut capture_speed: f64, -) -> (f64, f64) { - let latency = avg_delay * capture_speed; - let diff = latency - target_delay as f64; - match prev_diff { - None => (1.0, diff), - Some(prev_diff) => { - let equality_range = target_delay as f64 / 100.0; // in frames - let speed_delta = 1e-5; - if diff > 0.0 { - if diff > (prev_diff + equality_range) { - // playback latency grows, need to slow down capture more - capture_speed -= 3.0 * speed_delta; - } else if is_within(diff, prev_diff, equality_range) { - // positive, not changed from last cycle, need to slow down capture a bit - capture_speed -= speed_delta; - } - } else if diff < 0.0 { - if diff < (prev_diff - equality_range) { - // playback latency sinks, need to speed up capture more - capture_speed += 3.0 * speed_delta; - } else if is_within(diff, prev_diff, equality_range) { - // negative, not changed from last cycle, need to speed up capture a bit - capture_speed += speed_delta - } - } - debug!( - "Avg. buffer delay: {:.1}, target delay: {:.1}, diff: {}, prev_div: {}, corrected capture rate: {:.4}%", - avg_delay, - target_delay, - diff, - prev_diff, - 100.0 * capture_speed - ); - (capture_speed, diff) - } - } -} - -pub fn is_within(value: f64, target: f64, equality_range: f64) -> bool { - value <= (target + equality_range) && value >= (target - equality_range) -} - pub struct ElemData<'a> { element: Elem<'a>, numid: u32, diff --git a/src/helpers.rs b/src/helpers.rs index 8661251..10f99c7 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -101,10 +101,26 @@ impl PIRateController { let k_i = 0.004; let ramp_steps = 20; let ramp_trigger_limit = 0.33; - Self::new(fs, interval, target_level, k_p, k_i, ramp_steps, ramp_trigger_limit) + Self::new( + fs, + interval, + target_level, + k_p, + k_i, + ramp_steps, + ramp_trigger_limit, + ) } - pub fn new(fs: usize, interval: f64, target_level: usize, k_p: f64, k_i: f64, ramp_steps: usize, ramp_trigger_limit: f64) -> Self { + pub fn new( + fs: usize, + interval: f64, + target_level: usize, + k_p: f64, + k_i: f64, + ramp_steps: usize, + ramp_trigger_limit: f64, + ) -> Self { let frames_per_interval = interval * fs as f64; Self { target_level: target_level as f64, @@ -116,27 +132,39 @@ impl PIRateController { ramp_steps, ramp_trigger_limit, ramp_start: target_level as f64, - ramp_step: 0 + ramp_step: 0, } } /// Calculate the control output for the next measured value pub fn next(&mut self, level: f64) -> f64 { - if self.ramp_step >= self.ramp_steps && ((self.target_level - level)/self.target_level).abs() > self.ramp_trigger_limit { + if self.ramp_step >= self.ramp_steps + && ((self.target_level - level) / self.target_level).abs() > self.ramp_trigger_limit + { self.ramp_start = level; self.ramp_step = 0; - debug!("Rate controller, buffer level is {}, starting to adjust back towards target of {}", level, self.target_level); + debug!( + "Rate controller, buffer level is {}, starting to adjust back towards target of {}", + level, self.target_level + ); } if self.ramp_step == 0 { self.ramp_start = level; } - let current_target =if self.ramp_step < self.ramp_steps { + let current_target = if self.ramp_step < self.ramp_steps { self.ramp_step += 1; - let tgt = self.ramp_start + (self.target_level - self.ramp_start) * (1.0 - ((self.ramp_steps as f64 - self.ramp_step as f64) / self.ramp_steps as f64).powi(4)); - debug!("Rate controller, ramp step {}/{}, current target {}", self.ramp_step, self.ramp_steps, tgt); + let tgt = self.ramp_start + + (self.target_level - self.ramp_start) + * (1.0 + - ((self.ramp_steps as f64 - self.ramp_step as f64) + / self.ramp_steps as f64) + .powi(4)); + debug!( + "Rate controller, ramp step {}/{}, current target {}", + self.ramp_step, self.ramp_steps, tgt + ); tgt - } - else { + } else { self.target_level }; let err = level - current_target; From ef53c7beb8de9d971be6a703dca432e91c6ff22d Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Thu, 9 May 2024 23:09:38 +0200 Subject: [PATCH 068/135] New pi rate controller used for wasapi --- src/helpers.rs | 19 ++++++++++----- src/wasapidevice.rs | 58 +++++++++++++++++++-------------------------- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/src/helpers.rs b/src/helpers.rs index 10f99c7..7a8863e 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -168,12 +168,19 @@ impl PIRateController { self.target_level }; let err = level - current_target; - let rel_diff = err / self.frames_per_interval; - self.accumulated += rel_diff * self.interval; - let proportional = self.k_p * rel_diff; + let rel_err = err / self.frames_per_interval; + self.accumulated += rel_err * self.interval; + let proportional = self.k_p * rel_err; let integral = self.k_i * self.accumulated; - let mut rate_diff = proportional + integral; - rate_diff = rate_diff.clamp(-0.005, 0.005); - 1.0 - rate_diff + let mut output = proportional + integral; + trace!( + "Rate controller, error: {}, output: {}, P: {}, I: {}", + err, + output, + proportional, + integral + ); + output = output.clamp(-0.005, 0.005); + 1.0 - output } } diff --git a/src/wasapidevice.rs b/src/wasapidevice.rs index 8f1110f..ccd30a3 100644 --- a/src/wasapidevice.rs +++ b/src/wasapidevice.rs @@ -3,6 +3,7 @@ use crate::config; use crate::config::{ConfigError, SampleFormat}; use crate::conversions::{buffer_to_chunk_rawbytes, chunk_to_buffer_rawbytes}; use crate::countertimer; +use crate::helpers::PIRateController; use crossbeam_channel::{bounded, unbounded, Receiver, Sender, TryRecvError, TrySendError}; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; use rubato::VecResampler; @@ -132,30 +133,28 @@ fn get_supported_wave_format( let wave_format = wave_format(sample_format, samplerate, channels); match sharemode { wasapi::ShareMode::Exclusive => { - return audio_client.is_supported_exclusive_with_quirks(&wave_format); - } - wasapi::ShareMode::Shared => { - match audio_client.is_supported(&wave_format, &sharemode) { - Ok(None) => { - debug!("Device supports format {:?}", wave_format); - return Ok(wave_format); - } - Ok(Some(modified)) => { - let msg = format!( - "Device doesn't support format:\n{:#?}\nClosest match is:\n{:#?}", - wave_format, modified - ); - return Err(ConfigError::new(&msg).into()); - } - Err(err) => { - let msg = format!( - "Device doesn't support format:\n{:#?}\nError: {}", - wave_format, err - ); - return Err(ConfigError::new(&msg).into()); - } - }; + audio_client.is_supported_exclusive_with_quirks(&wave_format) } + wasapi::ShareMode::Shared => match audio_client.is_supported(&wave_format, sharemode) { + Ok(None) => { + debug!("Device supports format {:?}", wave_format); + Ok(wave_format) + } + Ok(Some(modified)) => { + let msg = format!( + "Device doesn't support format:\n{:#?}\nClosest match is:\n{:#?}", + wave_format, modified + ); + Err(ConfigError::new(&msg).into()) + } + Err(err) => { + let msg = format!( + "Device doesn't support format:\n{:#?}\nError: {}", + wave_format, err + ); + Err(ConfigError::new(&msg).into()) + } + }, } } @@ -654,6 +653,8 @@ impl PlaybackDevice for WasapiPlaybackDevice { peak: vec![0.0; channels], }; + let mut rate_controller = PIRateController::new_with_default_gains(samplerate, adjust_period as f64, target_level); + trace!("Build output stream"); let mut conversion_result; @@ -757,12 +758,7 @@ impl PlaybackDevice for WasapiPlaybackDevice { { if adjust && timer.larger_than_millis((1000.0 * adjust_period) as u64) { if let Some(av_delay) = buffer_avg.average() { - let speed = calculate_speed( - av_delay, - target_level, - adjust_period, - samplerate as u32, - ); + let speed = rate_controller.next(av_delay); timer.restart(); buffer_avg.restart(); debug!( @@ -1116,10 +1112,6 @@ impl CaptureDevice for WasapiCaptureDevice { let samples_per_sec = averager.average(); averager.restart(); let measured_rate_f = samples_per_sec; - debug!( - "Measured sample rate is {:.1} Hz", - measured_rate_f - ); let mut capture_status = RwLockUpgradableReadGuard::upgrade(capture_status); // to write lock capture_status.measured_samplerate = measured_rate_f as usize; capture_status.signal_range = value_range as f32; From 61c1e88c2c5af3c73a3a01cafc8030dfff3bbec2 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Fri, 10 May 2024 00:37:21 +0200 Subject: [PATCH 069/135] New rate controller in cpal backend --- src/audiodevice.rs | 15 --------------- src/cpaldevice.rs | 10 ++++------ 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/audiodevice.rs b/src/audiodevice.rs index f21e10b..1fcfa7d 100644 --- a/src/audiodevice.rs +++ b/src/audiodevice.rs @@ -722,21 +722,6 @@ pub fn new_capture_device(conf: config::Devices) -> Box { } } -pub fn calculate_speed(avg_level: f64, target_level: usize, adjust_period: f32, srate: u32) -> f64 { - let diff = avg_level as isize - target_level as isize; - let rel_diff = (diff as f64) / (srate as f64); - let speed = 1.0 - 0.5 * rel_diff / adjust_period as f64; - debug!( - "Avg. buffer level: {:.1}, target level: {:.1}, corrected capture rate: {:.4}%, ({:+.1}Hz at {}Hz)", - avg_level, - target_level, - 100.0 * speed, - srate as f64 * (speed-1.0), - srate - ); - speed -} - #[cfg(test)] mod tests { use crate::audiodevice::{rms_and_peak, AudioChunk, ChunkStats}; diff --git a/src/cpaldevice.rs b/src/cpaldevice.rs index 0969151..ea4c05c 100644 --- a/src/cpaldevice.rs +++ b/src/cpaldevice.rs @@ -5,6 +5,7 @@ use crate::conversions::{ chunk_to_queue_float, chunk_to_queue_int, queue_to_chunk_float, queue_to_chunk_int, }; use crate::countertimer; +use crate::helpers::PIRateController; use cpal; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::Device; @@ -240,6 +241,8 @@ impl PlaybackDevice for CpalPlaybackDevice { let mut timer = countertimer::Stopwatch::new(); let mut chunk_stats = ChunkStats{rms: vec![0.0; channels], peak: vec![0.0; channels]}; + let mut rate_controller = PIRateController::new_with_default_gains(samplerate, adjust_period as f64, target_level); + let stream = match sample_format { SampleFormat::S16LE => { trace!("Build i16 output stream"); @@ -385,12 +388,7 @@ impl PlaybackDevice for CpalPlaybackDevice { && timer.larger_than_millis((1000.0 * adjust_period) as u64) { if let Some(av_delay) = buffer_avg.average() { - let speed = calculate_speed( - av_delay, - target_level, - adjust_period, - samplerate as u32, - ); + let speed = rate_controller.next(av_delay); timer.restart(); buffer_avg.restart(); debug!( From 7f1eaa18d51d1270422cb115b5f010770682904a Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Fri, 10 May 2024 00:40:18 +0200 Subject: [PATCH 070/135] Update changelog [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b01d6b8..a87037b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ New features: - Add websocket command for reading all faders with a single call. - Linux: Subscribe to capture device control events for volume, sample rate and format changes. - Linux: Optionally select Alsa sample format automatically. +- Improved controller for rate adjustment. Changes: - Rename `File` capture device to `RawFile`. - Filter pipeline steps take a list of channels to filter instead of a single one. From 4f200e8ea106224f72e05a2b6a805afd1611a433 Mon Sep 17 00:00:00 2001 From: Henrik Date: Wed, 12 Jun 2024 22:23:56 +0200 Subject: [PATCH 071/135] Remove needless warning --- src/alsadevice_buffermanager.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/alsadevice_buffermanager.rs b/src/alsadevice_buffermanager.rs index 6850868..4534b18 100644 --- a/src/alsadevice_buffermanager.rs +++ b/src/alsadevice_buffermanager.rs @@ -99,12 +99,7 @@ pub trait DeviceBufferManager { fn apply_avail_min(&mut self, swp: &SwParams) -> Res<()> { let data = self.data_mut(); // maximum timing safety - headroom for one io_size only - if data.io_size < data.period { - warn!( - "Trying to set avail_min to {}, must be larger than or equal to period of {}", - data.io_size, data.period - ); - } else if data.io_size > data.bufsize { + if data.io_size > data.bufsize { let msg = format!("Trying to set avail_min to {}, must be smaller than or equal to device buffer size of {}", data.io_size, data.bufsize); error!("{}", msg); From 492b467ddc8bfb4663b347d7fa8a87fa05b092a2 Mon Sep 17 00:00:00 2001 From: Henrik Date: Fri, 14 Jun 2024 22:10:56 +0200 Subject: [PATCH 072/135] Add CLI options for setting initial aux fader values --- CHANGELOG.md | 1 + Cargo.toml | 2 +- README.md | 83 ++++++++++++++------------- src/bin.rs | 158 ++++++++++++++++++++++++++++++++++++++++++--------- 4 files changed, 179 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a87037b..a69ed56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ New features: - Linux: Subscribe to capture device control events for volume, sample rate and format changes. - Linux: Optionally select Alsa sample format automatically. - Improved controller for rate adjustment. +- Command line options fo setting aux volume and mute. Changes: - Rename `File` capture device to `RawFile`. - Filter pipeline steps take a list of channels to filter instead of a single one. diff --git a/Cargo.toml b/Cargo.toml index 94ddc07..a5b8fae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "camilladsp" +name = "CamillaDSP" version = "3.0.0" authors = ["Henrik Enquist "] edition = "2021" diff --git a/README.md b/README.md index 1c05db4..a575a56 100644 --- a/README.md +++ b/README.md @@ -444,44 +444,47 @@ See more about the configuration file below. ## Command line options Starting with the --help flag prints a short help message: ``` -> camilladsp.exe --help -CamillaDSP 2.0.0 +> camilladsp --help +CamillaDSP v3.0.0 Henrik Enquist A flexible tool for processing audio Built with features: websocket Supported device types: -Capture: RawFile, WavFile, Stdin, Wasapi -Playback: File, Stdout, Wasapi - -USAGE: - camilladsp.exe [FLAGS] [OPTIONS] - -FLAGS: - -m, --mute Start with the volume control muted - -c, --check Check config file and exit - -h, --help Prints help information - -V, --version Prints version information - -v Increase message verbosity - -w, --wait Wait for config from websocket - -OPTIONS: - -s, --statefile Use the given file to persist the state - -o, --logfile Write logs to file - -l, --loglevel Set log level [possible values: trace, debug, info, warn, error, off] - -a, --address
IP address to bind websocket server to - -g, --gain Set initial gain in dB for the volume control - -p, --port Port for websocket server - -n, --channels Override number of channels of capture device in config - -e, --extra_samples Override number of extra samples in config - -r, --samplerate Override samplerate in config - -f, --format Override sample format of capture device in config [possible values: S16LE, - S24LE, S24LE3, S32LE, FLOAT32LE, FLOAT64LE] - -ARGS: - The configuration file to use - +Capture: RawFile, WavFile, Stdin, SignalGenerator, CoreAudio +Playback: File, Stdout, CoreAudio + +Usage: camilladsp [OPTIONS] [CONFIGFILE] + +Arguments: + [CONFIGFILE] The configuration file to use + +Options: + -c, --check Check config file and exit + -s, --statefile Use the given file to persist the state + -v... Increase message verbosity + -l, --loglevel Set log level [possible values: trace, debug, info, warn, error, off] + -o, --logfile Write logs to file + -a, --address
IP address to bind websocket server to + -p, --port Port for websocket server + -w, --wait Wait for config from websocket + -g, --gain Initial gain in dB for main volume control + --gain1 Initial gain in dB for Aux1 fader + --gain2 Initial gain in dB for Aux2 fader + --gain3 Initial gain in dB for Aux3 fader + --gain4 Initial gain in dB for Aux4 fader + -m, --mute Start with main volume control muted + --mute1 Start with Aux1 fader muted + --mute2 Start with Aux2 fader muted + --mute3 Start with Aux3 fader muted + --mute4 Start with Aux4 fader muted + -e, --extra_samples Override number of extra samples in config + -n, --channels Override number of channels of capture device in config + -r, --samplerate Override samplerate in config + -f, --format Override sample format of capture device in config [possible values: S16LE, S24LE, S24LE3, S32LE, FLOAT32LE, FLOAT64LE] + -h, --help Print help + -V, --version Print version ``` Most flags and options have a long and a short form. For example `--port 1234` and `-p1234` are equivalent. @@ -509,11 +512,12 @@ The values in the file will then be kept updated whenever they change. If the file doesn't exist, it will be created on the first write. If the `configfile` argument is given, then this will be used instead of the value from the statefile. -Similarly, the `--gain` and `--mute` options also override the values in the statefile. +Similarly, the `--gain` and `--mute` options also override the values in the statefile for the main fader +while the `--gain1` to `--gain4` and `--mute1` to `--mute4` do the same for the Aux faders. **Use this feature with caution! The volume setting given in the statefile will be applied immediately when CamillaDSP starts processing.** In systems that have a gain structure such that a too high volume setting can damage equipment or ears, -it is recommended to always use the `--gain` option to set the volume to start at a safe value. +it is recommended to always use the `--gain` and `--gain1..4` options to set the volume to start at a safe value. #### Example statefile The statefile is a small YAML file that holds the path to the active config file, @@ -565,22 +569,25 @@ But if the `extra_samples` override is used, the given value is used without sca ### Initial volume -The `--gain` option can accept negative values, +The `--gain` and `--gain1..4` options can accept negative values, but this requires a little care since the minus sign can be misinterpreted as another option. It works as long as there is no space in front of the minus sign. -These work (for a gain of +/- 12.3 dB): +These all work for positive values (with 12.3 dB used as an example): ``` -g12.3 +--gain=12.3 -g 12.3 --gain 12.3 ---gain=12.3 +``` +These work for negative values (note that there is no space before the minus sign): +``` -g-12.3 --gain=-12.3 ``` -These will __NOT__ work: +These have a space before the minus sign and do __NOT__ work: ``` -g -12.3 --gain -12.3 diff --git a/src/bin.rs b/src/bin.rs index 4ed0f3b..3960deb 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -26,7 +26,7 @@ extern crate flexi_logger; #[macro_use] extern crate log; -use clap::{crate_authors, crate_description, crate_version, Arg, ArgAction, Command}; +use clap::{crate_authors, crate_description, crate_name, crate_version, Arg, ArgAction, Command}; use crossbeam_channel::select; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use std::env; @@ -105,6 +105,15 @@ pub fn custom_logger_format( ) } +fn parse_gain_value(v: &str) -> Result { + if let Ok(gain) = v.parse::() { + if (-120.0..=20.0).contains(&gain) { + return Ok(gain); + } + } + Err(String::from("Must be a number between -120 and +20")) +} + fn run( shared_configs: SharedConfigs, status_structs: StatusStructs, @@ -445,7 +454,10 @@ fn main_process() -> i32 { let capture_types = format!("Capture: {}", cap_types.join(", ")); let longabout = format!( - "{}\n\n{}\n\nSupported device types:\n{}\n{}", + "{} v{}\n{}\n{}\n\n{}\n\nSupported device types:\n{}\n{}", + crate_name!(), + crate_version!(), + crate_authors!(), crate_description!(), featurelist, capture_types, @@ -512,27 +524,84 @@ fn main_process() -> i32 { ) .arg( Arg::new("gain") - .help("Set initial gain in dB for Volume and Loudness filters") + .help("Initial gain in dB for main volume control") .short('g') .long("gain") .value_name("GAIN") - .display_order(200) + .display_order(300) .action(ArgAction::Set) - .value_parser(|v: &str| -> Result { - if let Ok(gain) = v.parse::() { - if (-120.0..=20.0).contains(&gain) { - return Ok(gain); - } - } - Err(String::from("Must be a number between -120 and +20")) - }), + .value_parser(parse_gain_value), + ) + .arg( + Arg::new("gain1") + .help("Initial gain in dB for Aux1 fader") + .long("gain1") + .value_name("GAIN1") + .display_order(301) + .action(ArgAction::Set) + .value_parser(parse_gain_value), + ) + .arg( + Arg::new("gain2") + .help("Initial gain in dB for Aux2 fader") + .long("gain2") + .value_name("GAIN2") + .display_order(302) + .action(ArgAction::Set) + .value_parser(parse_gain_value), + ) + .arg( + Arg::new("gain3") + .help("Initial gain in dB for Aux3 fader") + .long("gain3") + .value_name("GAIN3") + .display_order(303) + .action(ArgAction::Set) + .value_parser(parse_gain_value), + ) + .arg( + Arg::new("gain4") + .help("Initial gain in dB for Aux4 fader") + .long("gain4") + .value_name("GAIN4") + .display_order(304) + .action(ArgAction::Set) + .value_parser(parse_gain_value), ) .arg( Arg::new("mute") - .help("Start with Volume and Loudness filters muted") + .help("Start with main volume control muted") .short('m') .long("mute") - .display_order(200) + .display_order(310) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("mute1") + .help("Start with Aux1 fader muted") + .long("mute1") + .display_order(311) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("mute2") + .help("Start with Aux2 fader muted") + .long("mute2") + .display_order(312) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("mute3") + .help("Start with Aux3 fader muted") + .long("mute3") + .display_order(313) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("mute4") + .help("Start with Aux4 fader muted") + .long("mute4") + .display_order(314) .action(ArgAction::SetTrue), ) .arg( @@ -541,7 +610,7 @@ fn main_process() -> i32 { .short('r') .long("samplerate") .value_name("SAMPLERATE") - .display_order(300) + .display_order(400) .action(ArgAction::Set) .value_parser(clap::builder::RangedU64ValueParser::::new().range(1..)), ) @@ -551,7 +620,7 @@ fn main_process() -> i32 { .short('n') .long("channels") .value_name("CHANNELS") - .display_order(300) + .display_order(400) .action(ArgAction::Set) .value_parser(clap::builder::RangedU64ValueParser::::new().range(1..)), ) @@ -561,7 +630,7 @@ fn main_process() -> i32 { .short('e') .long("extra_samples") .value_name("EXTRA_SAMPLES") - .display_order(300) + .display_order(400) .action(ArgAction::Set) .value_parser(clap::builder::RangedU64ValueParser::::new().range(1..)), ) @@ -570,7 +639,7 @@ fn main_process() -> i32 { .short('f') .long("format") .value_name("FORMAT") - .display_order(310) + .display_order(410) .action(ArgAction::Set) .value_parser([ "S16LE", @@ -614,6 +683,7 @@ fn main_process() -> i32 { Arg::new("wait") .short('w') .long("wait") + .display_order(200) .help("Wait for config from websocket") .requires("port") .action(ArgAction::SetTrue), @@ -625,6 +695,7 @@ fn main_process() -> i32 { .help("Path to .pfx/.p12 certificate file") .long("cert") .value_name("CERT") + .display_order(220) .action(ArgAction::Set) .requires("port"), ) @@ -633,6 +704,7 @@ fn main_process() -> i32 { .help("Password for .pfx/.p12 certificate file") .long("pass") .value_name("PASS") + .display_order(220) .action(ArgAction::Set) .requires("port"), ); @@ -714,10 +786,7 @@ fn main_process() -> i32 { }; debug!("Loaded state: {state:?}"); - let initial_volumes = if let Some(v) = matches.get_one::("gain") { - debug!("Using command line argument for initial volume"); - [*v, *v, *v, *v, *v] - } else if let Some(s) = &state { + let mut initial_volumes = if let Some(s) = &state { debug!("Using statefile for initial volume"); s.volume } else { @@ -730,11 +799,28 @@ fn main_process() -> i32 { ProcessingParameters::DEFAULT_VOLUME, ] }; + if let Some(v) = matches.get_one::("gain") { + debug!("Using command line argument for initial main volume"); + initial_volumes[0] = *v; + } + if let Some(v) = matches.get_one::("gain1") { + debug!("Using command line argument for initial Aux1 volume"); + initial_volumes[1] = *v; + } + if let Some(v) = matches.get_one::("gain2") { + debug!("Using command line argument for initial Aux2 volume"); + initial_volumes[2] = *v; + } + if let Some(v) = matches.get_one::("gain3") { + debug!("Using command line argument for initial Aux3 volume"); + initial_volumes[3] = *v; + } + if let Some(v) = matches.get_one::("gain4") { + debug!("Using command line argument for initial Aux4 volume"); + initial_volumes[4] = *v; + } - let initial_mutes = if matches.get_flag("mute") { - debug!("Using command line argument for initial mute"); - [true, true, true, true, true] - } else if let Some(s) = &state { + let mut initial_mutes = if let Some(s) = &state { debug!("Using statefile for initial mute"); s.mute } else { @@ -747,6 +833,26 @@ fn main_process() -> i32 { ProcessingParameters::DEFAULT_MUTE, ] }; + if matches.get_flag("mute") { + debug!("Using command line argument for initial main mute"); + initial_mutes[0] = true; + } + if matches.get_flag("mute1") { + debug!("Using command line argument for initial Aux1 mute"); + initial_mutes[1] = true; + } + if matches.get_flag("mute2") { + debug!("Using command line argument for initial Aux2 mute"); + initial_mutes[2] = true; + } + if matches.get_flag("mute3") { + debug!("Using command line argument for initial Aux3 mute"); + initial_mutes[3] = true; + } + if matches.get_flag("mute4") { + debug!("Using command line argument for initial Aux4 mute"); + initial_mutes[4] = true; + } debug!("Initial mute: {initial_mutes:?}"); debug!("Initial volume: {initial_volumes:?}"); From 805ed67037d7552f982777698a728413d3ab8b81 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Sat, 15 Jun 2024 21:46:19 +0200 Subject: [PATCH 073/135] Nicer shutdown of Alsa playback device --- src/alsadevice.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 9f348fa..58ba328 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -442,6 +442,7 @@ fn playback_loop_bytes( let adjust = params.adjust_period > 0.0 && params.adjust_enabled; let millis_per_frame: f32 = 1000.0 / params.samplerate as f32; let mut device_stalled = false; + let mut pcm_paused = false; let io = pcmdevice.io_bytes(); debug!("Playback loop uses a buffer of {} frames", params.chunksize); @@ -509,6 +510,7 @@ fn playback_loop_bytes( params.bytes_per_frame, buf_manager, ); + pcm_paused = false; device_stalled = match playback_res { Ok(PlaybackResult::Normal) => { if device_stalled { @@ -609,12 +611,25 @@ fn playback_loop_bytes( } Ok(AudioMessage::Pause) => { trace!("PB: Pause message received"); + if !pcm_paused { + let pause_res = pcmdevice.pause(true); + trace!("pcm_pause result {:?}", pause_res); + if pause_res.is_ok() { + pcm_paused = true + } + } } Ok(AudioMessage::EndOfStream) => { channels .status .send(StatusMessage::PlaybackDone) .unwrap_or(()); + // Only drain if the device isn't paused + if !pcm_paused { + let drain_res = pcmdevice.drain(); + // Draining isn't strictly needed, ignore any error and don't retry + trace!("pcm_drain result {:?}", drain_res); + } break; } Err(err) => { @@ -623,6 +638,12 @@ fn playback_loop_bytes( .status .send(StatusMessage::PlaybackError(err.to_string())) .unwrap_or(()); + // Only drain if the device isn't paused + if !pcm_paused { + let drain_res = pcmdevice.drain(); + // Draining isn't strictly needed, ignore any error and don't retry + trace!("pcm_drain result {:?}", drain_res); + } break; } } @@ -653,7 +674,7 @@ fn capture_loop_bytes( let subdevice = pcminfo.get_subdevice(); let fds = pcmdevice.get().unwrap(); - println!("{:?}", fds); + trace!("File descriptors: {:?}", fds); let nbr_pcm_fds = fds.len(); let mut file_descriptors = FileDescriptors { fds, nbr_pcm_fds }; @@ -673,7 +694,7 @@ fn capture_loop_bytes( if let Some(h) = &hctl { let ctl_fds = h.get().unwrap(); file_descriptors.fds.extend(ctl_fds.iter()); - println!("{:?}", file_descriptors.fds); + //println!("{:?}", file_descriptors.fds); h.load().unwrap(); element_loopback = find_elem( h, @@ -907,6 +928,7 @@ fn capture_loop_bytes( .add_record(chunk_stats.peak_linear()); } value_range = chunk.maxval - chunk.minval; + trace!("Captured chunk with value range {}", value_range); if device_stalled { state = ProcessingState::Stalled; } else { From 521cdff6ca8e17a198abfa2cc6252b647387ea86 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Mon, 17 Jun 2024 20:54:06 +0200 Subject: [PATCH 074/135] Check if playback device supports pause --- src/alsadevice.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 58ba328..bfebea7 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -443,7 +443,13 @@ fn playback_loop_bytes( let millis_per_frame: f32 = 1000.0 / params.samplerate as f32; let mut device_stalled = false; let mut pcm_paused = false; - + let can_pause = pcmdevice + .hw_params_current() + .map(|p| p.can_pause()) + .unwrap_or_default(); + if can_pause { + debug!("Playback device supports pausing the stream") + } let io = pcmdevice.io_bytes(); debug!("Playback loop uses a buffer of {} frames", params.chunksize); let mut buffer = vec![0u8; params.chunksize * params.bytes_per_frame]; @@ -611,7 +617,7 @@ fn playback_loop_bytes( } Ok(AudioMessage::Pause) => { trace!("PB: Pause message received"); - if !pcm_paused { + if can_pause && !pcm_paused { let pause_res = pcmdevice.pause(true); trace!("pcm_pause result {:?}", pause_res); if pause_res.is_ok() { From f7bead5c9c7b1ce5c4be36bb0e97e583d4c47118 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Tue, 18 Jun 2024 17:29:19 +0200 Subject: [PATCH 075/135] Update wasapi crate --- Cargo.toml | 4 ++-- src/wasapidevice.rs | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a5b8fae..56e2d32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,8 +42,8 @@ dispatch = "0.2.0" [target.'cfg(target_os="windows")'.dependencies] #wasapi = { path = "../../rust/wasapi" } #wasapi = { git = "https://github.com/HEnquist/wasapi-rs", branch = "win041" } -wasapi = "0.14.0" -windows = {version = "0.51.0", features = ["Win32_System_Threading", "Win32_Foundation"] } +wasapi = "0.15.0" +windows = {version = "0.57.0", features = ["Win32_System_Threading", "Win32_Foundation"] } [dependencies] serde = { version = "1.0", features = ["derive"] } diff --git a/src/wasapidevice.rs b/src/wasapidevice.rs index ccd30a3..4c01a22 100644 --- a/src/wasapidevice.rs +++ b/src/wasapidevice.rs @@ -421,7 +421,6 @@ fn playback_loop( } render_client.write_to_device_from_deque( buffer_free_frame_count as usize, - blockalign, &mut sample_queue, None, )?; @@ -543,7 +542,7 @@ fn capture_loop( data.resize(nbr_bytes, 0); } let (nbr_frames_read, flags) = - capture_client.read_from_device(blockalign, &mut data[0..nbr_bytes])?; + capture_client.read_from_device(&mut data[0..nbr_bytes])?; if nbr_frames_read != available_frames { warn!( "Expected {} frames, got {}", @@ -571,10 +570,8 @@ fn capture_loop( if data.len() < (nbr_bytes + nbr_bytes_extra) { data.resize(nbr_bytes + nbr_bytes_extra, 0); } - let (nbr_frames_read, flags) = capture_client.read_from_device( - blockalign, - &mut data[nbr_bytes..(nbr_bytes + nbr_bytes_extra)], - )?; + let (nbr_frames_read, flags) = capture_client + .read_from_device(&mut data[nbr_bytes..(nbr_bytes + nbr_bytes_extra)])?; if nbr_frames_read != extra_frames { warn!("Expected {} frames, got {}", extra_frames, nbr_frames_read); } From 80e16f912272b8f9d376c8179069983ce8c36673 Mon Sep 17 00:00:00 2001 From: Henrik Date: Mon, 24 Jun 2024 21:26:16 +0200 Subject: [PATCH 076/135] Add noise gate processor --- CHANGELOG.md | 1 + README.md | 39 +++++++++++++++++++++++++++++++++-- src/compressor.rs | 2 -- src/config.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++- src/filters.rs | 12 ++++++++++- src/lib.rs | 1 + 6 files changed, 101 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a69ed56..deb5852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ New features: - Linux: Optionally select Alsa sample format automatically. - Improved controller for rate adjustment. - Command line options fo setting aux volume and mute. +- Add noise gate. Changes: - Rename `File` capture device to `RawFile`. - Filter pipeline steps take a list of channels to filter instead of a single one. diff --git a/README.md b/README.md index a575a56..c310e98 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ It does not matter if the damage is caused by incorrect usage or a bug in the so - **[Difference equation](#difference-equation)** - **[Processors](#processors)** - **[Compressor](#compressor)** + - **[NoiseGate](#noise-gate)** - **[Pipeline](#pipeline)** - **[Filter step](#filter-step)** - **[Mixer and Processor step](#mixer-and-processor-step)** @@ -2067,7 +2068,6 @@ Both a and b are optional. If left out, they default to [1.0]. ## Processors The `processors` section contains the definitions for the Processors. These are special "filters" that work on several channels at the same time. -At present only one type of processor, "Compressor", has been implemented. Processors take an optional `description` property. This is intended for the user and is not used by CamillaDSP itself. @@ -2111,8 +2111,43 @@ pipeline: Note that soft clipping introduces some harmonic distortion to the signal. This setting is ignored if `enable_clip = false`. Optional, defaults to `false`. * `monitor_channels`: a list of channels used when estimating the loudness. Optional, defaults to all channels. - * `process_channels`: a list of channels that should be compressed. Optional, defaults to all channels. + * `process_channels`: a list of channels to be compressed. Optional, defaults to all channels. +### Noise Gate +The "NoiseGate" processor implements a simple noise gate. +It monitors the given channels to estimate the current loudness, +using the same algorithm as the compressor. +When the loudness is above the threshold, +the gate "opens" and the sound is passed through unaltered. +When it is below, the gate "closes" and attenuates the selected channels by the given amount. + +Example: +``` +processors: + demogate: + type: NoiseGate + parameters: + channels: 2 + attack: 0.025 + release: 1.0 + threshold: -25 + attenuation: 50.0 + monitor_channels: [0, 1] (*) + process_channels: [0, 1] (*) + +pipeline: + - type: Processor + name: demogate +``` + + Parameters: + * `channels`: number of channels, must match the number of channels of the pipeline where the compressor is inserted. + * `attack`: time constant in seconds for attack, how fast the gate reacts to an increase of the loudness. + * `release`: time constant in seconds for release, how fast the gate reacts when the loudness decreases. + * `threshold`: the loudness threshold in dB where gate "opens". + * `attenuation`: the amount of attenuation in dB to apply when the gate is "closed". + * `monitor_channels`: a list of channels used when estimating the loudness. Optional, defaults to all channels. + * `process_channels`: a list of channels to be gated. Optional, defaults to all channels. ## Pipeline diff --git a/src/compressor.rs b/src/compressor.rs index b3744cf..bec6d97 100644 --- a/src/compressor.rs +++ b/src/compressor.rs @@ -151,8 +151,6 @@ impl Processor for Compressor { } fn update_parameters(&mut self, config: config::Processor) { - // TODO remove when there is more than one type of Processor. - #[allow(irrefutable_let_patterns)] if let config::Processor::Compressor { parameters: config, .. } = config diff --git a/src/config.rs b/src/config.rs index 0dd5d79..01b6d14 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,7 @@ use crate::compressor; use crate::filters; use crate::mixer; +use crate::noisegate; use crate::wavtools::{find_data_in_wav_stream, WavParams}; use parking_lot::RwLock; use serde::{de, Deserialize, Serialize}; @@ -1217,6 +1218,11 @@ pub enum Processor { description: Option, parameters: CompressorParameters, }, + NoiseGate { + #[serde(default)] + description: Option, + parameters: NoiseGateParameters, + }, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] @@ -1257,6 +1263,30 @@ impl CompressorParameters { } } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct NoiseGateParameters { + pub channels: usize, + #[serde(default)] + pub monitor_channels: Option>, + #[serde(default)] + pub process_channels: Option>, + pub attack: PrcFmt, + pub release: PrcFmt, + pub threshold: PrcFmt, + pub attenuation: PrcFmt, +} + +impl NoiseGateParameters { + pub fn monitor_channels(&self) -> Vec { + self.monitor_channels.clone().unwrap_or_default() + } + + pub fn process_channels(&self) -> Vec { + self.process_channels.clone().unwrap_or_default() + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct LimiterParameters { @@ -1731,7 +1761,7 @@ pub fn config_diff(currentconf: &Configuration, newconf: &Configuration) -> Conf } if let (Some(newprocs), Some(oldprocs)) = (&newconf.processors, ¤tconf.processors) { for (proc, params) in newprocs { - // The pipeline didn't change, any added compressor isn't included and can be skipped + // The pipeline didn't change, any added processor isn't included and can be skipped if let Some(current_proc) = oldprocs.get(proc) { if params != current_proc { processors.push(proc.to_string()); @@ -2003,6 +2033,26 @@ pub fn validate_config(conf: &mut Configuration, filename: Option<&str>) -> Res< } } } + Processor::NoiseGate { parameters, .. } => { + let channels = parameters.channels; + if channels != num_channels { + let msg = format!( + "NoiseGate '{}' has wrong number of channels. Expected {}, found {}.", + step.name, num_channels, channels + ); + return Err(ConfigError::new(&msg).into()); + } + match noisegate::validate_noise_gate(parameters) { + Ok(_) => {} + Err(err) => { + let msg = format!( + "Invalid noise gate '{}'. Reason: {}", + step.name, err + ); + return Err(ConfigError::new(&msg).into()); + } + } + } } } } else { diff --git a/src/filters.rs b/src/filters.rs index 633e4ac..ebe03d1 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -14,6 +14,7 @@ use crate::fftconv_fftw as fftconv; use crate::limiter; use crate::loudness; use crate::mixer; +use crate::noisegate; use rawsample::SampleReader; use std::collections::HashMap; use std::fs::File; @@ -369,7 +370,16 @@ impl Pipeline { conf.devices.samplerate, conf.devices.chunksize, ); - Box::new(comp) + Box::new(comp) as Box + } + config::Processor::NoiseGate { parameters, .. } => { + let gate = noisegate::NoiseGate::from_config( + &step.name, + parameters, + conf.devices.samplerate, + conf.devices.chunksize, + ); + Box::new(gate) as Box } }; steps.push(PipelineStep::ProcessorStep(proc)); diff --git a/src/lib.rs b/src/lib.rs index 7b5f38f..8ac260b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -140,6 +140,7 @@ pub mod helpers; pub mod limiter; pub mod loudness; pub mod mixer; +pub mod noisegate; pub mod processing; #[cfg(feature = "pulse-backend")] pub mod pulsedevice; From 065a8df323f25c83df1bc5b84e4d0c98e71b21e8 Mon Sep 17 00:00:00 2001 From: Henrik Date: Mon, 24 Jun 2024 21:28:21 +0200 Subject: [PATCH 077/135] Add missing file --- src/noisegate.rs | 198 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 src/noisegate.rs diff --git a/src/noisegate.rs b/src/noisegate.rs new file mode 100644 index 0000000..32b80f4 --- /dev/null +++ b/src/noisegate.rs @@ -0,0 +1,198 @@ +use crate::audiodevice::AudioChunk; +use crate::config; +use crate::filters::Processor; +use crate::PrcFmt; +use crate::Res; + +#[derive(Clone, Debug)] +pub struct NoiseGate { + pub name: String, + pub channels: usize, + pub monitor_channels: Vec, + pub process_channels: Vec, + pub attack: PrcFmt, + pub release: PrcFmt, + pub threshold: PrcFmt, + pub factor: PrcFmt, + pub samplerate: usize, + pub scratch: Vec, + pub prev_loudness: PrcFmt, +} + +impl NoiseGate { + /// Creates a NoiseGate from a config struct + pub fn from_config( + name: &str, + config: config::NoiseGateParameters, + samplerate: usize, + chunksize: usize, + ) -> Self { + let name = name.to_string(); + let channels = config.channels; + let srate = samplerate as PrcFmt; + let mut monitor_channels = config.monitor_channels(); + if monitor_channels.is_empty() { + for n in 0..channels { + monitor_channels.push(n); + } + } + let mut process_channels = config.process_channels(); + if process_channels.is_empty() { + for n in 0..channels { + process_channels.push(n); + } + } + let attack = (-1.0 / srate / config.attack).exp(); + let release = (-1.0 / srate / config.release).exp(); + let scratch = vec![0.0; chunksize]; + + debug!("Creating noisegate '{}', channels: {}, monitor_channels: {:?}, process_channels: {:?}, attack: {}, release: {}, threshold: {}, attenuation: {}", + name, channels, process_channels, monitor_channels, attack, release, config.threshold, config.attenuation); + + let factor = (10.0 as PrcFmt).powf(-config.attenuation / 20.0); + + NoiseGate { + name, + channels, + monitor_channels, + process_channels, + attack, + release, + threshold: config.threshold, + factor, + samplerate, + scratch, + prev_loudness: 0.0, + } + } + + /// Sum all channels that are included in loudness monitoring, store result in self.scratch + fn sum_monitor_channels(&mut self, input: &AudioChunk) { + let ch = self.monitor_channels[0]; + self.scratch.copy_from_slice(&input.waveforms[ch]); + for ch in self.monitor_channels.iter().skip(1) { + for (acc, val) in self.scratch.iter_mut().zip(input.waveforms[*ch].iter()) { + *acc += *val; + } + } + } + + /// Estimate loudness, store result in self.scratch + fn estimate_loudness(&mut self) { + for val in self.scratch.iter_mut() { + // convert to dB + *val = 20.0 * (val.abs() + 1.0e-9).log10(); + if *val >= self.prev_loudness { + *val = self.attack * self.prev_loudness + (1.0 - self.attack) * *val; + } else { + *val = self.release * self.prev_loudness + (1.0 - self.release) * *val; + } + self.prev_loudness = *val; + } + } + + /// Calculate linear gain, store result in self.scratch + fn calculate_linear_gain(&mut self) { + for val in self.scratch.iter_mut() { + if *val < self.threshold { + *val = self.factor; + } else { + *val = 1.0; + } + } + } + + fn apply_gain(&self, input: &mut [PrcFmt]) { + for (val, gain) in input.iter_mut().zip(self.scratch.iter()) { + *val *= gain; + } + } +} + +impl Processor for NoiseGate { + fn name(&self) -> &str { + &self.name + } + + /// Apply a NoiseGate to an AudioChunk, modifying it in-place. + fn process_chunk(&mut self, input: &mut AudioChunk) -> Res<()> { + self.sum_monitor_channels(input); + self.estimate_loudness(); + self.calculate_linear_gain(); + for ch in self.process_channels.iter() { + self.apply_gain(&mut input.waveforms[*ch]); + } + Ok(()) + } + + fn update_parameters(&mut self, config: config::Processor) { + if let config::Processor::NoiseGate { + parameters: config, .. + } = config + { + let channels = config.channels; + let srate = self.samplerate as PrcFmt; + let mut monitor_channels = config.monitor_channels(); + if monitor_channels.is_empty() { + for n in 0..channels { + monitor_channels.push(n); + } + } + let mut process_channels = config.process_channels(); + if process_channels.is_empty() { + for n in 0..channels { + process_channels.push(n); + } + } + let attack = (-1.0 / srate / config.attack).exp(); + let release = (-1.0 / srate / config.release).exp(); + + self.monitor_channels = monitor_channels; + self.process_channels = process_channels; + self.attack = attack; + self.release = release; + self.threshold = config.threshold; + self.factor = (10.0 as PrcFmt).powf(-config.attenuation / 20.0); + + debug!("Updated noise gate '{}', monitor_channels: {:?}, process_channels: {:?}, attack: {}, release: {}, threshold: {}, attenuation: {}", + self.name, self.process_channels, self.monitor_channels, attack, release, config.threshold, config.attenuation); + } else { + // This should never happen unless there is a bug somewhere else + panic!("Invalid config change!"); + } + } +} + +/// Validate the noise gate config, to give a helpful message intead of a panic. +pub fn validate_noise_gate(config: &config::NoiseGateParameters) -> Res<()> { + let channels = config.channels; + if config.attack <= 0.0 { + let msg = "Attack value must be larger than zero."; + return Err(config::ConfigError::new(msg).into()); + } + if config.release <= 0.0 { + let msg = "Release value must be larger than zero."; + return Err(config::ConfigError::new(msg).into()); + } + for ch in config.monitor_channels().iter() { + if *ch >= channels { + let msg = format!( + "Invalid monitor channel: {}, max is: {}.", + *ch, + channels - 1 + ); + return Err(config::ConfigError::new(&msg).into()); + } + } + for ch in config.process_channels().iter() { + if *ch >= channels { + let msg = format!( + "Invalid channel to process: {}, max is: {}.", + *ch, + channels - 1 + ); + return Err(config::ConfigError::new(&msg).into()); + } + } + Ok(()) +} From 8418515c4aa376dcb4a39ff6ea2155496a6be035 Mon Sep 17 00:00:00 2001 From: Henrik Date: Wed, 26 Jun 2024 17:14:48 +0200 Subject: [PATCH 078/135] Deny unknown config fields everywhere --- src/config.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/config.rs b/src/config.rs index 0dd5d79..f4a9b48 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1282,6 +1282,7 @@ pub enum PipelineStep { } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] pub struct PipelineStepMixer { pub name: String, #[serde(default)] @@ -1297,6 +1298,7 @@ impl PipelineStepMixer { } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] pub struct PipelineStepFilter { #[serde(default)] pub channels: Option>, @@ -1314,6 +1316,7 @@ impl PipelineStepFilter { } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] pub struct PipelineStepProcessor { pub name: String, #[serde(default)] From 4eaa27f4c4a932640934a745fae7d9a7c2daba72 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Thu, 27 Jun 2024 21:48:33 +0200 Subject: [PATCH 079/135] Remove FFTW --- .github/workflows/ci_test.yml | 2 +- CHANGELOG.md | 1 + Cargo.toml | 2 - README.md | 25 +-- src/bin.rs | 6 - src/fftconv_fftw.rs | 362 ---------------------------------- src/filters.rs | 3 - src/lib.rs | 6 - 8 files changed, 10 insertions(+), 397 deletions(-) delete mode 100644 src/fftconv_fftw.rs diff --git a/.github/workflows/ci_test.yml b/.github/workflows/ci_test.yml index bdc7d52..929f0dd 100644 --- a/.github/workflows/ci_test.yml +++ b/.github/workflows/ci_test.yml @@ -44,7 +44,7 @@ jobs: run: cargo test --features bluez-backend,cpal-backend,jack-backend,pulse-backend, - name: Run cargo test with all optional features - run: cargo test --features 32bit,debug,fftw,secure-websocket + run: cargo test --features 32bit,debug,secure-websocket - name: Run cargo fmt run: cargo fmt --all -- --check diff --git a/CHANGELOG.md b/CHANGELOG.md index a69ed56..52c3721 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ New features: - Improved controller for rate adjustment. - Command line options fo setting aux volume and mute. Changes: +- Remove the optional use of FFTW instead of RustFFT. - Rename `File` capture device to `RawFile`. - Filter pipeline steps take a list of channels to filter instead of a single one. Bugfixes: diff --git a/Cargo.toml b/Cargo.toml index 56e2d32..bdd08e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ bluez-backend = ["zbus"] 32bit = [] websocket = ["tungstenite"] secure-websocket = ["websocket", "native-tls", "tungstenite/native-tls"] -FFTW = ["fftw"] debug = [] avoid-rustc-issue-116359 = [] @@ -52,7 +51,6 @@ serde_json = "1.0" serde_with = "1.11" realfft = "3.0.0" #realfft = { git = "https://github.com/HEnquist/realfft", branch = "better_errors" } -fftw = { version = "0.8.0", optional = true } num-complex = "0.4" num-traits = "0.2" signal-hook = "0.3.8" diff --git a/README.md b/README.md index a575a56..f1e6e9c 100644 --- a/README.md +++ b/README.md @@ -300,12 +300,6 @@ CamillaDSP includes a Websocket server that can be used to pass commands to the This feature is enabled by default, but can be left out. The feature name is "websocket". For usage see the section "Controlling via websocket". -The default FFT library is RustFFT, but it's also possible to use FFTW. -This is enabled by the feature "FFTW". When the chunksize is a power of two, like 1024 or 4096, then FFTW and RustFFT are very similar in speed. -But if the chunksize is a "strange" number like a large prime, then FFTW can be faster. -FFTW is a much larger and more complicated library, -so using FFTW is only recommended if you for some reason can't use an "easy" chunksize and this makes RustFFT too slow. - ## Building in Linux with standard features These instructions assume that the linux distribution used is one of Fedora, Debian, Ubunty or Arch. They should also work also work on distributions closely related to one of these, such as Manjaro (Arch), @@ -343,7 +337,6 @@ All the available options, or "features" are: - `bluez-backend`: Bluetooth support via BlueALSA (Linux only). - `websocket`: Websocket server for control. - `secure-websocket`: Enable secure websocket, also enables the `websocket` feature. -- `FFTW`: Use FFTW instead of RustFFT. - `32bit`: Perform all calculations with 32-bit floats (instead of 64). - `debug`: Enable extra logging, useful for debugging. - `avoid-rustc-issue-116359`: Enable a workaround for [rust issue #116359](https://github.com/rust-lang/rust/issues/116359). @@ -356,18 +349,18 @@ Cargo doesn't allow disabling a single default feature, but you can disable the whole group with the `--no-default-features` flag. Then you have to manually add all the ones you want. -Example 1: You want `websocket`, `pulse-backend` and `FFTW`. The first one is included by default so you only need to add `FFTW` and `pulse-backend`: +Example 1: You want `websocket` and `pulse-backend`. The first one is included by default so you only need to add `pulse-backend`: ``` -cargo build --release --features FFTW --features pulse-backend +cargo build --release --features pulse-backend (or) -cargo install --path . --features FFTW --features pulse-backend +cargo install --path . --features pulse-backend ``` -Example 2: You want `32bit` and `FFTW`. Since you don't want `websocket` you have to disable the defaults: +Example 2: You want only `32bit`. Since you don't want `websocket` you have to disable the defaults: ``` -cargo build --release --no-default-features --features FFTW --features 32bit +cargo build --release --no-default-features --features 32bit (or) -cargo install --path . --no-default-features --features FFTW --features 32bit +cargo install --path . --no-default-features --features 32bit ``` ### Additional dependencies @@ -419,15 +412,13 @@ $env:RUSTFLAGS="-C target-cpu=native" cargo build --release ``` -On macOS both the PulseAudio and FFTW features can be used. The necessary dependencies can be installed with brew: +The PulseAudio backend can be used on macOS. +The necessary dependencies can be installed with brew: ``` -brew install fftw brew install pkg-config brew install pulseaudio ``` -The FFTW feature can also be used on Windows. There is no need to install anything extra. - # How to run diff --git a/src/bin.rs b/src/bin.rs index 3960deb..966ae28 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -2,8 +2,6 @@ extern crate alsa; extern crate camillalib; extern crate clap; -#[cfg(feature = "FFTW")] -extern crate fftw; extern crate lazy_static; #[cfg(feature = "pulse-backend")] extern crate libpulse_binding as pulse; @@ -12,7 +10,6 @@ extern crate libpulse_simple_binding as psimple; extern crate parking_lot; extern crate rand; extern crate rand_distr; -#[cfg(not(feature = "FFTW"))] extern crate realfft; extern crate rubato; extern crate serde; @@ -438,9 +435,6 @@ fn main_process() -> i32 { if cfg!(feature = "secure-websocket") { features.push("secure-websocket"); } - if cfg!(feature = "FFTW") { - features.push("FFTW"); - } if cfg!(feature = "32bit") { features.push("32bit"); } diff --git a/src/fftconv_fftw.rs b/src/fftconv_fftw.rs deleted file mode 100644 index fef1f31..0000000 --- a/src/fftconv_fftw.rs +++ /dev/null @@ -1,362 +0,0 @@ -use crate::config; -use crate::filters; -use crate::filters::Filter; -use fftw::array::AlignedVec; -use fftw::plan::*; -use fftw::types::*; -//use helpers::{multiply_add_elements, multiply_elements}; - -// Sample format -use crate::PrcFmt; -#[cfg(feature = "32bit")] -pub type ComplexFmt = c32; -#[cfg(not(feature = "32bit"))] -pub type ComplexFmt = c64; -use crate::Res; - -// -- Duplcated from helpers.rs, needed until fftw updates to num-complex 0.3 -pub fn multiply_elements( - result: &mut [ComplexFmt], - slice_a: &[ComplexFmt], - slice_b: &[ComplexFmt], -) { - let len = result.len(); - let mut res = &mut result[..len]; - let mut val_a = &slice_a[..len]; - let mut val_b = &slice_b[..len]; - - while res.len() >= 8 { - res[0] = val_a[0] * val_b[0]; - res[1] = val_a[1] * val_b[1]; - res[2] = val_a[2] * val_b[2]; - res[3] = val_a[3] * val_b[3]; - res[4] = val_a[4] * val_b[4]; - res[5] = val_a[5] * val_b[5]; - res[6] = val_a[6] * val_b[6]; - res[7] = val_a[7] * val_b[7]; - res = &mut res[8..]; - val_a = &val_a[8..]; - val_b = &val_b[8..]; - } - for (r, val) in res - .iter_mut() - .zip(val_a.iter().zip(val_b.iter()).map(|(a, b)| *a * *b)) - { - *r = val; - } -} - -// element-wise add product, result = result + slice_a * slice_b -pub fn multiply_add_elements( - result: &mut [ComplexFmt], - slice_a: &[ComplexFmt], - slice_b: &[ComplexFmt], -) { - let len = result.len(); - let mut res = &mut result[..len]; - let mut val_a = &slice_a[..len]; - let mut val_b = &slice_b[..len]; - - while res.len() >= 8 { - res[0] += val_a[0] * val_b[0]; - res[1] += val_a[1] * val_b[1]; - res[2] += val_a[2] * val_b[2]; - res[3] += val_a[3] * val_b[3]; - res[4] += val_a[4] * val_b[4]; - res[5] += val_a[5] * val_b[5]; - res[6] += val_a[6] * val_b[6]; - res[7] += val_a[7] * val_b[7]; - res = &mut res[8..]; - val_a = &val_a[8..]; - val_b = &val_b[8..]; - } - for (r, val) in res - .iter_mut() - .zip(val_a.iter().zip(val_b.iter()).map(|(a, b)| *a * *b)) - { - *r += val; - } -} -// -- Duplcated from helpers.rs, needed until fftw updates to num-complex 0.3 - -pub struct FftConv { - name: String, - npoints: usize, - nsegments: usize, - overlap: Vec, - coeffs_f: Vec>, - #[cfg(feature = "32bit")] - fft: R2CPlan32, - #[cfg(not(feature = "32bit"))] - fft: R2CPlan64, - #[cfg(feature = "32bit")] - ifft: C2RPlan32, - #[cfg(not(feature = "32bit"))] - ifft: C2RPlan64, - input_buf: AlignedVec, - input_f: Vec>, - temp_buf: AlignedVec, - output_buf: AlignedVec, - index: usize, -} - -impl FftConv { - /// Create a new FFT colvolution filter. - pub fn new(name: &str, data_length: usize, coeffs: &[PrcFmt]) -> Self { - let name = name.to_string(); - let input_buf = AlignedVec::::new(2 * data_length); - let temp_buf = AlignedVec::::new(data_length + 1); - let output_buf = AlignedVec::::new(2 * data_length); - #[cfg(feature = "32bit")] - let mut fft: R2CPlan32 = R2CPlan::aligned(&[2 * data_length], Flag::MEASURE).unwrap(); - #[cfg(not(feature = "32bit"))] - let mut fft: R2CPlan64 = R2CPlan::aligned(&[2 * data_length], Flag::MEASURE).unwrap(); - let ifft = C2RPlan::aligned(&[2 * data_length], Flag::MEASURE).unwrap(); - - let nsegments = ((coeffs.len() as PrcFmt) / (data_length as PrcFmt)).ceil() as usize; - - let input_f = vec![AlignedVec::::new(data_length + 1); nsegments]; - let mut coeffs_f = vec![AlignedVec::::new(data_length + 1); nsegments]; - let mut coeffs_al = vec![AlignedVec::::new(2 * data_length); nsegments]; - - debug!("Conv {} is using {} segments", name, nsegments); - - for (n, coeff) in coeffs.iter().enumerate() { - coeffs_al[n / data_length][n % data_length] = coeff / (2.0 * data_length as PrcFmt); - } - - for (segment, segment_f) in coeffs_al.iter_mut().zip(coeffs_f.iter_mut()) { - fft.r2c(segment, segment_f).unwrap(); - } - - FftConv { - name, - npoints: data_length, - nsegments, - overlap: vec![0.0; data_length], - coeffs_f, - fft, - ifft, - input_f, - input_buf, - output_buf, - temp_buf, - index: 0, - } - } - - pub fn from_config(name: &str, data_length: usize, conf: config::ConvParameters) -> Self { - let values = match conf { - config::ConvParameters::Values { values } => values, - config::ConvParameters::Raw(params) => filters::read_coeff_file( - ¶ms.filename, - ¶ms.format(), - params.read_bytes_lines(), - params.skip_bytes_lines(), - ) - .unwrap(), - config::ConvParameters::Wav(params) => { - filters::read_wav(¶ms.filename, params.channel()).unwrap() - } - config::ConvParameters::Dummy { length } => { - let mut values = vec![0.0; length]; - values[0] = 1.0; - values - } - }; - FftConv::new(name, data_length, &values) - } -} - -impl Filter for FftConv { - fn name(&self) -> &str { - &self.name - } - - /// Process a waveform by FT, then multiply transform with transform of filter, and then transform back. - fn process_waveform(&mut self, waveform: &mut [PrcFmt]) -> Res<()> { - // Copy to input buffer - self.input_buf[0..self.npoints].copy_from_slice(waveform); - - // FFT and store result in history, update index - self.index = (self.index + 1) % self.nsegments; - self.fft - .r2c(&mut self.input_buf, self.input_f[self.index].as_slice_mut()) - .unwrap(); - - // Loop through history of input FTs, multiply with filter FTs, accumulate result - let segm = 0; - let hist_idx = (self.index + self.nsegments - segm) % self.nsegments; - multiply_elements( - &mut self.temp_buf, - &self.input_f[hist_idx], - &self.coeffs_f[segm], - ); - for segm in 1..self.nsegments { - let hist_idx = (self.index + self.nsegments - segm) % self.nsegments; - multiply_add_elements( - &mut self.temp_buf, - &self.input_f[hist_idx], - &self.coeffs_f[segm], - ); - } - - // IFFT result, store result anv overlap - self.ifft - .c2r(&mut self.temp_buf, &mut self.output_buf) - .unwrap(); - for (n, item) in waveform.iter_mut().enumerate().take(self.npoints) { - *item = self.output_buf[n] + self.overlap[n]; - } - self.overlap - .copy_from_slice(&self.output_buf[self.npoints..]); - Ok(()) - } - - fn update_parameters(&mut self, conf: config::Filter) { - if let config::Filter::Conv { - parameters: conf, .. - } = conf - { - let coeffs = match conf { - config::ConvParameters::Values { values } => values, - config::ConvParameters::Raw(params) => filters::read_coeff_file( - ¶ms.filename, - ¶ms.format(), - params.read_bytes_lines(), - params.skip_bytes_lines(), - ) - .unwrap(), - config::ConvParameters::Wav(params) => { - filters::read_wav(¶ms.filename, params.channel()).unwrap() - } - config::ConvParameters::Dummy { length } => { - let mut values = vec![0.0; length]; - values[0] = 1.0; - values - } - }; - - let nsegments = ((coeffs.len() as PrcFmt) / (self.npoints as PrcFmt)).ceil() as usize; - - if nsegments == self.nsegments { - // Same length, lets keep history - } else { - // length changed, clearing history - self.nsegments = nsegments; - let input_f = vec![AlignedVec::::new(self.npoints + 1); nsegments]; - self.input_f = input_f; - } - - let mut coeffs_f = vec![AlignedVec::::new(self.npoints + 1); nsegments]; - let mut coeffs_al = vec![AlignedVec::::new(2 * self.npoints); nsegments]; - - debug!("conv using {} segments", nsegments); - - for (n, coeff) in coeffs.iter().enumerate() { - coeffs_al[n / self.npoints][n % self.npoints] = - coeff / (2.0 * self.npoints as PrcFmt); - } - - for (segment, segment_f) in coeffs_al.iter_mut().zip(coeffs_f.iter_mut()) { - self.fft.r2c(segment, segment_f).unwrap(); - } - self.coeffs_f = coeffs_f; - } else { - // This should never happen unless there is a bug somewhere else - panic!("Invalid config change!"); - } - } -} - -/// Validate a FFT convolution config. -pub fn validate_config(conf: &config::ConvParameters) -> Res<()> { - match conf { - config::ConvParameters::Values { .. } | config::ConvParameters::Dummy { .. } => Ok(()), - config::ConvParameters::Raw(params) => { - let coeffs = filters::read_coeff_file( - ¶ms.filename, - ¶ms.format(), - params.read_bytes_lines(), - params.skip_bytes_lines(), - )?; - if coeffs.is_empty() { - return Err(config::ConfigError::new("Conv coefficients are empty").into()); - } - Ok(()) - } - config::ConvParameters::Wav(params) => { - let coeffs = filters::read_wav(¶ms.filename, params.channel())?; - if coeffs.is_empty() { - return Err(config::ConfigError::new("Conv coefficients are empty").into()); - } - Ok(()) - } - } -} - -#[cfg(test)] -mod tests { - use crate::config::ConvParameters; - use crate::fftconv_fftw::FftConv; - use crate::filters::Filter; - use crate::PrcFmt; - - fn is_close(left: PrcFmt, right: PrcFmt, maxdiff: PrcFmt) -> bool { - println!("{} - {}", left, right); - (left - right).abs() < maxdiff - } - - fn compare_waveforms(left: Vec, right: Vec, maxdiff: PrcFmt) -> bool { - for (val_l, val_r) in left.iter().zip(right.iter()) { - if !is_close(*val_l, *val_r, maxdiff) { - return false; - } - } - true - } - - #[test] - fn check_result() { - let coeffs = vec![0.5, 0.5]; - let conf = ConvParameters::Values { values: coeffs }; - let mut filter = FftConv::from_config("test", 8, conf); - let mut wave1 = vec![1.0, 1.0, 1.0, 0.0, 0.0, -1.0, 0.0, 0.0]; - let expected = vec![0.5, 1.0, 1.0, 0.5, 0.0, -0.5, -0.5, 0.0]; - filter.process_waveform(&mut wave1).unwrap(); - assert!(compare_waveforms(wave1, expected, 1e-7)); - } - - #[test] - fn check_result_segmented() { - let mut coeffs = Vec::::new(); - for m in 0..32 { - coeffs.push(m as PrcFmt); - } - let mut filter = FftConv::new("test", 8, &coeffs); - let mut wave1 = vec![0.0 as PrcFmt; 8]; - let mut wave2 = vec![0.0 as PrcFmt; 8]; - let mut wave3 = vec![0.0 as PrcFmt; 8]; - let mut wave4 = vec![0.0 as PrcFmt; 8]; - let mut wave5 = vec![0.0 as PrcFmt; 8]; - - wave1[0] = 1.0; - filter.process_waveform(&mut wave1).unwrap(); - filter.process_waveform(&mut wave2).unwrap(); - filter.process_waveform(&mut wave3).unwrap(); - filter.process_waveform(&mut wave4).unwrap(); - filter.process_waveform(&mut wave5).unwrap(); - - let exp1 = Vec::from(&coeffs[0..8]); - let exp2 = Vec::from(&coeffs[8..16]); - let exp3 = Vec::from(&coeffs[16..24]); - let exp4 = Vec::from(&coeffs[24..32]); - let exp5 = vec![0.0 as PrcFmt; 8]; - - assert!(compare_waveforms(wave1, exp1, 1e-5)); - assert!(compare_waveforms(wave2, exp2, 1e-5)); - assert!(compare_waveforms(wave3, exp3, 1e-5)); - assert!(compare_waveforms(wave4, exp4, 1e-5)); - assert!(compare_waveforms(wave5, exp5, 1e-5)); - } -} diff --git a/src/filters.rs b/src/filters.rs index 633e4ac..2f72ccd 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -7,10 +7,7 @@ use crate::config; use crate::conversions; use crate::diffeq; use crate::dither; -#[cfg(not(feature = "FFTW"))] use crate::fftconv; -#[cfg(feature = "FFTW")] -use crate::fftconv_fftw as fftconv; use crate::limiter; use crate::loudness; use crate::mixer; diff --git a/src/lib.rs b/src/lib.rs index 7b5f38f..34c615a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,8 +5,6 @@ extern crate alsa_sys; extern crate clap; #[cfg(feature = "cpal-backend")] extern crate cpal; -#[cfg(feature = "FFTW")] -extern crate fftw; #[macro_use] extern crate lazy_static; #[cfg(target_os = "macos")] @@ -27,7 +25,6 @@ extern crate num_traits; extern crate rand; extern crate rand_distr; extern crate rawsample; -#[cfg(not(feature = "FFTW"))] extern crate realfft; extern crate rubato; extern crate serde; @@ -123,10 +120,7 @@ pub mod countertimer; pub mod cpaldevice; pub mod diffeq; pub mod dither; -#[cfg(not(feature = "FFTW"))] pub mod fftconv; -#[cfg(feature = "FFTW")] -pub mod fftconv_fftw; pub mod filedevice; #[cfg(all(target_os = "linux", feature = "bluez-backend"))] pub mod filedevice_bluez; From ef6a15d7454868d8fb0be974802c031a2f8dc0ff Mon Sep 17 00:00:00 2001 From: HEnquist Date: Wed, 17 Jul 2024 21:23:25 +0200 Subject: [PATCH 080/135] Optional limits for volume adjust commands --- CHANGELOG.md | 3 ++- src/socketserver.rs | 60 ++++++++++++++++++++++++++++++++++++++------- websocket.md | 37 +++++++++++++++++++++++++--- 3 files changed, 86 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52c3721..1dac525 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ New features: - Linux: Subscribe to capture device control events for volume, sample rate and format changes. - Linux: Optionally select Alsa sample format automatically. - Improved controller for rate adjustment. -- Command line options fo setting aux volume and mute. +- Command line options for setting aux volume and mute. +- Optional user-defined volume limits for volume adjust commands. Changes: - Remove the optional use of FFTW instead of RustFFT. - Rename `File` capture device to `RawFile`. diff --git a/src/socketserver.rs b/src/socketserver.rs index fe923a0..0db3419 100644 --- a/src/socketserver.rs +++ b/src/socketserver.rs @@ -60,6 +60,13 @@ pub struct ServerParameters<'a> { pub cert_pass: Option<&'a str>, } +#[derive(Debug, PartialEq, Deserialize)] +#[serde(untagged)] +enum ValueWithOptionalLimits { + Plain(f32), + Limited(f32, f32, f32), +} + #[derive(Debug, PartialEq, Deserialize)] enum WsCommand { SetConfigFilePath(String), @@ -100,7 +107,7 @@ enum WsCommand { SetUpdateInterval(usize), GetVolume, SetVolume(f32), - AdjustVolume(f32), + AdjustVolume(ValueWithOptionalLimits), GetMute, SetMute(bool), ToggleMute, @@ -108,7 +115,7 @@ enum WsCommand { GetFaderVolume(usize), SetFaderVolume(usize, f32), SetFaderExternalVolume(usize, f32), - AdjustFaderVolume(usize, f32), + AdjustFaderVolume(usize, ValueWithOptionalLimits), GetFaderMute(usize), SetFaderMute(usize, bool), ToggleFaderMute(usize), @@ -851,10 +858,28 @@ fn handle_command( result: WsResult::Ok, }) } - WsCommand::AdjustVolume(nbr) => { + WsCommand::AdjustVolume(value) => { let mut tempvol = shared_data_inst.processing_params.target_volume(0); - tempvol += nbr; - tempvol = clamped_volume(tempvol); + let (volchange, minvol, maxvol) = match value { + ValueWithOptionalLimits::Plain(vol) => (vol, -150.0, 50.0), + ValueWithOptionalLimits::Limited(vol, min, max) => (vol, min, max), + }; + if maxvol < minvol { + return Some(WsReply::AdjustVolume { + result: WsResult::Error, + value: tempvol, + }); + } + tempvol += volchange; + if tempvol < minvol { + tempvol = minvol; + warn!("Clamped volume at {} dB", minvol) + } + if tempvol > maxvol { + tempvol = maxvol; + warn!("Clamped volume at {} dB", maxvol) + } + shared_data_inst .processing_params .set_target_volume(0, tempvol); @@ -974,16 +999,33 @@ fn handle_command( result: WsResult::Ok, }) } - WsCommand::AdjustFaderVolume(ctrl, nbr) => { + WsCommand::AdjustFaderVolume(ctrl, value) => { + let (volchange, minvol, maxvol) = match value { + ValueWithOptionalLimits::Plain(vol) => (vol, -150.0, 50.0), + ValueWithOptionalLimits::Limited(vol, min, max) => (vol, min, max), + }; if ctrl > ProcessingParameters::NUM_FADERS - 1 { return Some(WsReply::AdjustFaderVolume { result: WsResult::Error, - value: (ctrl, nbr), + value: (ctrl, volchange), }); } let mut tempvol = shared_data_inst.processing_params.target_volume(ctrl); - tempvol += nbr; - tempvol = clamped_volume(tempvol); + if maxvol < minvol { + return Some(WsReply::AdjustFaderVolume { + result: WsResult::Error, + value: (ctrl, tempvol), + }); + } + tempvol += volchange; + if tempvol < minvol { + tempvol = minvol; + warn!("Clamped volume at {} dB", minvol) + } + if tempvol > maxvol { + tempvol = maxvol; + warn!("Clamped volume at {} dB", maxvol) + } shared_data_inst .processing_params .set_target_volume(ctrl, tempvol); diff --git a/websocket.md b/websocket.md index 7a84ded..c1140d2 100644 --- a/websocket.md +++ b/websocket.md @@ -63,7 +63,7 @@ ws.on('message', function message(data) { } }); ``` -*Wrapped the parse with a try/catch as that's good practice to avoid crashes with improperly formatted JSON etc.* +*Wrapping the parse with a try/catch is good practice to avoid crashes with improperly formatted JSON etc.* ## All commands The available commands are listed below. All commands return the result, and for the ones that return a value are this described here. @@ -143,33 +143,61 @@ Combined commands for reading several levels with a single request. These comman - `GetSignalLevelsSinceLast` Get the peak since start. -- `GetSignalPeaksSinceStart` : Get the playback and capture peak level since processing started. The values are returned as a json object with keys `playback` and `capture`. +- `GetSignalPeaksSinceStart` : Get the playback and capture peak level since processing started. + The values are returned as a json object with keys `playback` and `capture`. - `ResetSignalPeaksSinceStart` : Reset the peak values. Note that this resets the peak for all clients. ### Volume control Commands for setting and getting the volume and mute of the default volume control on control `Main`. + - `GetVolume` : Get the current volume setting in dB. * Returns the value as a float. + - `SetVolume` : Set the volume control to the given value in dB. Clamped to the range -150 to +50 dB. -- `AdjustVolume` : Change the volume setting by the given number of dB, positive or negative. The resulting volume is clamped to the range -150 to +50 dB. + +- `AdjustVolume` : Change the volume setting by the given number of dB, positive or negative. + The resulting volume is clamped to the range -150 to +50 dB. + The allowed range can be reduced by providing two more values, for minimum and maximum. + + Example, reduce the volume by 3 dB, with limits of -50 and +10 dB: + ```{"AdjustVolume": [-3.0, -50.0, 10.0]}``` + * Returns the new value as a float. + - `GetMute` : Get the current mute setting. * Returns the muting status as a boolean. + - `SetMute` : Set muting to the given value. + - `ToggleMute` : Toggle muting. * Returns the new muting status as a boolean. + Commands for setting and getting the volume and mute setting of a given fader. The faders are selected using an integer, 0 for `Main` and 1 to 4 for `Aux1` to `Aux4`. All commands take the fader number as the first parameter. + - `GetFaderVolume` : Get the current volume setting in dB. * Returns a struct with the fader as an integer and the volume value as a float. + - `SetFaderVolume` : Set the volume control to the given value in dB. Clamped to the range -150 to +50 dB. + - `SetFaderExternalVolume` : Special command for setting the volume when a Loudness filter is being combined with an external volume control (without a Volume filter). Clamped to the range -150 to +50 dB. -- `AdjustFaderVolume` : Change the volume setting by the given number of dB, positive or negative. The resulting volume is clamped to the range -150 to +50 dB. + +- `AdjustFaderVolume` : Change the volume setting by the given number of dB, positive or negative. + The resulting volume is clamped to the range -150 to +50 dB. + The allowed range can be reduced by providing two more values, for minimum and maximum. + + Example, reduce the volume of fader 0 by 3 dB, with default limits: + ```{"AdjustFaderVolume": [0, -3.0]}``` + + Example, reduce the volume of fader 0 by 3 dB, with limits of -50 and +10 dB: + ```{"AdjustFaderVolume": [0, [-3.0, -50.0, 10.0]]}``` + * Returns a struct with the fader as an integer and the new volume value as a float. + - `GetFaderMute` : Get the current mute setting. * Returns a struct with the fader as an integer and the muting status as a boolean. - `SetFaderMute` : Set muting to the given value. @@ -180,6 +208,7 @@ There is also a command for getting the volume and mute settings for all faders - `GetFaders` : Read all faders. * Returns a list of objects, each containing a `volume` and a `mute` property. + ### Config management Commands for reading and changing the active configuration. From ce78f55553b85a3dff4839a339059cec6b67b1db Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sun, 21 Jul 2024 11:37:30 +0200 Subject: [PATCH 081/135] Add channel labels property to mixer and capture devices --- CHANGELOG.md | 1 + src/audiodevice.rs | 18 ++++++++++-------- src/config.rs | 22 ++++++++++++++++++++++ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 513170a..c09d451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ New features: - Command line options for setting aux volume and mute. - Optional user-defined volume limits for volume adjust commands. - Add noise gate. +- Add optional channel labels for capture devices and mixers. Changes: - Remove the optional use of FFTW instead of RustFFT. - Rename `File` capture device to `RawFile`. diff --git a/src/audiodevice.rs b/src/audiodevice.rs index 1fcfa7d..6981c2d 100644 --- a/src/audiodevice.rs +++ b/src/audiodevice.rs @@ -556,6 +556,7 @@ pub fn new_capture_device(conf: config::Devices) -> Box { format, stop_on_inactive, ref follow_volume_control, + .. } => Box::new(alsadevice::AlsaCaptureDevice { devname: device.clone(), samplerate: conf.samplerate, @@ -576,6 +577,7 @@ pub fn new_capture_device(conf: config::Devices) -> Box { channels, ref device, format, + .. } => Box::new(pulsedevice::PulseCaptureDevice { devname: device.clone(), samplerate: conf.samplerate, @@ -635,14 +637,14 @@ pub fn new_capture_device(conf: config::Devices) -> Box { stop_on_rate_change: conf.stop_on_rate_change(), rate_measure_interval: conf.rate_measure_interval(), }), - config::CaptureDevice::SignalGenerator { signal, channels } => { - Box::new(generatordevice::GeneratorDevice { - signal, - samplerate: conf.samplerate, - channels, - chunksize: conf.chunksize, - }) - } + config::CaptureDevice::SignalGenerator { + signal, channels, .. + } => Box::new(generatordevice::GeneratorDevice { + signal, + samplerate: conf.samplerate, + channels, + chunksize: conf.chunksize, + }), #[cfg(all(target_os = "linux", feature = "bluez-backend"))] config::CaptureDevice::Bluez(ref dev) => Box::new(filedevice::FileCaptureDevice { source: filedevice::CaptureSource::BluezDBus(dev.service(), dev.dbus_path.clone()), diff --git a/src/config.rs b/src/config.rs index eab89d7..972042e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -198,6 +198,8 @@ pub enum CaptureDevice { stop_on_inactive: Option, #[serde(default)] follow_volume_control: Option, + #[serde(default)] + labels: Option>>, }, #[cfg(all(target_os = "linux", feature = "bluez-backend"))] #[serde(alias = "BLUEZ", alias = "bluez")] @@ -209,6 +211,8 @@ pub enum CaptureDevice { channels: usize, device: String, format: SampleFormat, + #[serde(default)] + labels: Option>>, }, RawFile(CaptureDeviceRawFile), WavFile(CaptureDeviceWavFile), @@ -235,11 +239,15 @@ pub enum CaptureDevice { #[serde(deserialize_with = "validate_nonzero_usize")] channels: usize, device: String, + #[serde(default)] + labels: Option>>, }, SignalGenerator { #[serde(deserialize_with = "validate_nonzero_usize")] channels: usize, signal: Signal, + #[serde(default)] + labels: Option>>, }, } @@ -290,6 +298,8 @@ pub struct CaptureDeviceRawFile { pub skip_bytes: Option, #[serde(default)] pub read_bytes: Option, + #[serde(default)] + pub labels: Option>>, } impl CaptureDeviceRawFile { @@ -310,6 +320,8 @@ pub struct CaptureDeviceWavFile { pub filename: String, #[serde(default)] pub extra_samples: Option, + #[serde(default)] + pub labels: Option>>, } impl CaptureDeviceWavFile { @@ -343,6 +355,8 @@ pub struct CaptureDeviceStdin { pub skip_bytes: Option, #[serde(default)] pub read_bytes: Option, + #[serde(default)] + pub labels: Option>>, } impl CaptureDeviceStdin { @@ -369,6 +383,8 @@ pub struct CaptureDeviceBluez { // from D-Bus properties pub format: SampleFormat, pub channels: usize, + #[serde(default)] + pub labels: Option>>, } #[cfg(all(target_os = "linux", feature = "bluez-backend"))] @@ -390,6 +406,8 @@ pub struct CaptureDeviceWasapi { exclusive: Option, #[serde(default)] loopback: Option, + #[serde(default)] + pub labels: Option>>, } #[cfg(target_os = "windows")] @@ -412,6 +430,8 @@ pub struct CaptureDeviceCA { pub device: Option, #[serde(default)] pub format: Option, + #[serde(default)] + pub labels: Option>>, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] @@ -1207,6 +1227,8 @@ pub struct Mixer { pub description: Option, pub channels: MixerChannels, pub mapping: Vec, + #[serde(default)] + labels: Option>>, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] From befd9c7bf3c2d34b4b4721503dfcfb1890987300 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sun, 21 Jul 2024 18:26:17 +0200 Subject: [PATCH 082/135] Fix broken tests --- src/audiodevice.rs | 1 + src/config.rs | 2 +- src/mixer.rs | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/audiodevice.rs b/src/audiodevice.rs index 6981c2d..5ce01ca 100644 --- a/src/audiodevice.rs +++ b/src/audiodevice.rs @@ -707,6 +707,7 @@ pub fn new_capture_device(conf: config::Devices) -> Box { config::CaptureDevice::Jack { channels, ref device, + .. } => Box::new(cpaldevice::CpalCaptureDevice { devname: device.clone(), host: cpaldevice::CpalHost::Jack, diff --git a/src/config.rs b/src/config.rs index 972042e..a3889e7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1228,7 +1228,7 @@ pub struct Mixer { pub channels: MixerChannels, pub mapping: Vec, #[serde(default)] - labels: Option>>, + pub labels: Option>>, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] diff --git a/src/mixer.rs b/src/mixer.rs index 16fd2e5..d7525ce 100644 --- a/src/mixer.rs +++ b/src/mixer.rs @@ -208,6 +208,7 @@ mod tests { description: None, channels: chans, mapping: vec![map0, map1, map2, map3], + labels: None, }; let used = used_input_channels(&conf); assert_eq!(used, vec![true, true]); @@ -268,6 +269,7 @@ mod tests { description: None, channels: chans, mapping: vec![map0, map1, map2, map3], + labels: None, }; let used = used_input_channels(&conf); assert_eq!(used, vec![false, true]); @@ -328,6 +330,7 @@ mod tests { description: None, channels: chans, mapping: vec![map0, map1, map2, map3], + labels: None, }; let used = used_input_channels(&conf); assert_eq!(used, vec![false, true]); @@ -388,6 +391,7 @@ mod tests { description: None, channels: chans, mapping: vec![map0, map1, map2, map3], + labels: None, }; let used = used_input_channels(&conf); assert_eq!(used, vec![false, true]); @@ -448,6 +452,7 @@ mod tests { description: None, channels: chans, mapping: vec![map0, map1, map2, map3], + labels: None, }; let mix = mixer::Mixer::from_config("dummy".to_string(), conf); assert_eq!(mix.channels_in, 2); @@ -535,6 +540,7 @@ mod tests { description: None, channels: chans, mapping: vec![map0, map1, map2, map3], + labels: None, }; let mix = mixer::Mixer::from_config("dummy".to_string(), conf); assert_eq!(mix.channels_in, 2); From 0c44795ecf88f29845570a017788ed4127c008a3 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sun, 21 Jul 2024 21:59:03 +0200 Subject: [PATCH 083/135] Mention channel labels in readme --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 4aed4e6..74191c1 100644 --- a/README.md +++ b/README.md @@ -891,6 +891,7 @@ devices: channels: 2 device: "MySink.monitor" format: S16LE + labels: ["L", "R"] (*) playback: type: Alsa channels: 2 @@ -1223,6 +1224,12 @@ A parameter marked (*) in any example is optional. If they are left out from the device: "default" ``` + ### Channel labels + All capture device types have an optional `labels` property. + This accepts a list of strings, and is meant to be used by a GUI + to display meaningful channel names. + CamillaDSP itself does not use these labels. + ## Resampling Resampling is provided by the [Rubato library.](https://github.com/HEnquist/rubato) @@ -1383,6 +1390,7 @@ Example for a mixer that copies two channels into four: mixers: ExampleMixer: description: "Example mixer to convert two channels to four" (*) + labels: ["L_LF", "R_LF", "L_HF", "R_HF"] (*) channels: in: 2 out: 4 @@ -1422,8 +1430,13 @@ Each source has a `channel` number, a `gain` value, a `scale` for the gain (`dB` A channel that has no sources will be filled with silence. The `mute` option determines if an output channel of the mixer should be muted. The `mute`, `gain`, `scale` and `inverted` parameters are optional, and defaults to not muted, a gain of 0 in dB, and not inverted. + The optional `description` property is intended for the user and is not used by CamillaDSP itself. +Similar to [capture devices](#channel-labels), the mixer also has a `labels` property. +This is meant to define labels for the output channels from the mixer. +The labels are intended to be used by GUIs and are not used by CamillaDSP. + Another example, a simple stereo to mono mixer: ``` mixers: From d1512105e83147c65a4844c4841da317f318830a Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sun, 18 Aug 2024 15:57:15 +0200 Subject: [PATCH 084/135] Improve readme on REW and other tools --- FAQ.md | 4 ++++ README.md | 36 ++++++++++++++++++++++++++++++------ backend_coreaudio.md | 11 ++++++++++- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/FAQ.md b/FAQ.md index 7cdfe71..d16f894 100644 --- a/FAQ.md +++ b/FAQ.md @@ -47,6 +47,10 @@ Note the extra padding bytes (`0x00`) in S24LE. This scheme means that the samples get an "easier" alignment in memory, while wasting some space. In practice, this format isn't used much. +- Why don't I get any sound on MacOS? + + TODO mention microphone permission + ## Filtering - I only have filters with negative gain, why do I get clipping anyway? diff --git a/README.md b/README.md index 74191c1..2cd46c8 100644 --- a/README.md +++ b/README.md @@ -1662,6 +1662,18 @@ these will be replaced by the corresponding values from the config. For example, if samplerate is 44100, the filename `/path/to/filter_$samplerate$.raw` will be updated to `/path/to/filter_44100.raw`. +#### Generating FIR coefficients +There are many ways to generate impulse responses for FIR filters. +Typically they are generated by some dedicated application. +See also [Measurement and filter generation tools](#measurement-and-filter-generation-tools). + +[rePhase](#rephase) is a popular choice that is free to use. +It allows building fully linear-phase active crossovers with arbitrary slopes. +It also supports compensating the phase shifts of loudspeakers and existing crossovers. +In the Impulse Settings box configure the rate to the same as used in CamillaDSP +and the format to 64 bits IEEE-754 (.dbl). +This corresponds to raw samples in FLOAT64LE format in CamillaDSP. + #### Values directly in config file Example for giving values: @@ -2232,8 +2244,8 @@ Take care when bypassing mixers. If a mixer is used to change the number of chan then bypassing it will make the pipeline output the wrong number of channels. In this case, the bypass may be used to switch between mixers with different settings. -## Export filters from REW -REW can automatically generate a set of filters for correcting the frequency response of a system. +## Using filters from REW +[REW](#rew) can automatically generate a set of filters for correcting the frequency response of a system. REW V5.20.14 and later is able to export the filters in the CamillaDSP YAML format. - Go to the "EQ Filters" screen. Expand the "Equalizer" section in the list on the right side. @@ -2244,32 +2256,44 @@ REW V5.20.14 and later is able to export the filters in the CamillaDSP YAML form Note that the generated YAML file is not a complete CamillaDSP configuration. It contains only filter definitions and pipeline steps, that can be pasted into a CamillaDSP config file. +If using [CamillaGUI](#gui), it is also possible to import the filters into an existing configuration. # Related projects -Other projects using CamillaDSP: +## Other projects using CamillaDSP * https://github.com/scripple/alsa_cdsp - ALSA CamillaDSP "I/O" plugin, automatic config updates at changes of samplerate, sample format or number of channels. * https://github.com/raptorlightning/I2S-Hat - An SPDIF Hat for the Raspberry Pi 2-X for SPDIF Communication, see also [this thread at diyAudio.com](https://www.diyaudio.com/forums/pc-based/375834-i2s-hat-raspberry-pi-hat-spdif-i2s-communication-dsp.html). * https://github.com/daverz/camilla-remote-control - Interface for remote control of CamillaDSP using a FLIRC USB infrared receiver or remote keyboard. * https://github.com/Wang-Yue/CamillaDSP-Monitor - A script that provides a DSP pipeline and a spectral analyzer similar to those of the RME ADI-2 DAC/Pro. -Music players: +## Music players * https://moodeaudio.org/ - moOde audio player, audiophile-quality music playback for Raspberry Pi. * https://github.com/thoelf/Linux-Stream-Player - Play local files or streamed music with room EQ on Linux. * https://github.com/Lykkedk/SuperPlayer-v8.0.0---SamplerateChanger-v1.0.0 - Automatic filter switching at sample rate change for squeezelite, see also [this thread at diyAudio.com](https://www.diyaudio.com/forums/pc-based/361429-superplayer-dsp_engine-camilladsp-samplerate-switching-esp32-remote-control.html). * https://github.com/JWahle/piCoreCDSP - Installs CamillaDSP and GUI on piCorePlayer * [FusionDsp](https://docs.google.com/document/d/e/2PACX-1vRhU4i830YaaUlB6-FiDAdvl69T3Iej_9oSbNTeSpiW0DlsyuTLSv5IsVSYMmkwbFvNbdAT0Tj6Yjjh/pub) a plugin based on CamillaDsp for [Volumio](https://volumio.com), the music player, with graphic equalizer, parametric equalizer, FIR filters, Loudness, AutoEq profile for headphone and more! -Guides and example configs: +## Guides and example configs * https://github.com/ynot123/CamillaDSP-Cfgs-FIR-Examples - Example Filter Configuration and Convolver Coefficients. * https://github.com/hughpyle/raspot - Hugh's raspotify config * https://github.com/Wang-Yue/camilladsp-crossfeed - Bauer stereophonic-to-binaural crossfeed for headphones * https://github.com/jensgk/akg_k702_camilladsp_eq - Headphone EQ and Crossfeed for the AKG K702 headphones * https://github.com/phelluy/room_eq_mac_m1 - Room Equalization HowTo with REW and Apple Silicon -Projects of general nature which can be useful together with CamillaDSP: +## Projects of general nature which can be useful together with CamillaDSP * https://github.com/scripple/alsa_hook_hwparams - Alsa hooks for reacting to sample rate and format changes. * https://github.com/HEnquist/cpal-listdevices - List audio devices with names and supported formats under Windows and macOS. +## Measurement and filter generation tools +### rePhase +https://rephase.org/ - rePhase is a free FIR generation tool for building +fully linear-phase active crossovers with arbitrary slopes. +### REW +https://www.roomeqwizard.com/ - REW is free software for room acoustic measurement, +loudspeaker measurement and audio device measurement. +### DRC +https://drc-fir.sourceforge.net/ - DRC is a program used to generate correction filters +for acoustic compensation of HiFi and audio systems in general, +including listening room compensation. # Getting help diff --git a/backend_coreaudio.md b/backend_coreaudio.md index 8d7a8c4..63f57a0 100644 --- a/backend_coreaudio.md +++ b/backend_coreaudio.md @@ -9,7 +9,16 @@ CamillaDSP uses the low-level AudioUnits for playback and capture. An AudioUnit that represents a hardware device has two stream formats. One format is used for communicating with the application. This is typically 32-bit float, the same format that CoreAudio uses internally. -The other format (called the physical format) is the one used to send or receive data to/from the sound card driver. +The other format (called the physical format) is the one used to send or receive data to/from the sound card driver. + +## Microphone permissions +In order to capture audio on macOS, the application needs the be given permission. +First time CamillaDSP is launched, there should be a pop-up asking if the Terminal app +should be allowed to use the microphone. +This may be misleading, as the "microphone" permission covers all recording of sound, +and not only the microphone. +If there is no pop-up, or if the permission was mistakenly denied, +TODO mention steps. ## Capturing audio from other applications From e6b2f27dd126d5da74c9c63f31b070b5886c2a72 Mon Sep 17 00:00:00 2001 From: Henrik Date: Sun, 18 Aug 2024 20:39:02 +0200 Subject: [PATCH 085/135] Mention microhone access on macOS --- FAQ.md | 4 +++- backend_coreaudio.md | 23 ++++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/FAQ.md b/FAQ.md index d16f894..b89ecab 100644 --- a/FAQ.md +++ b/FAQ.md @@ -49,7 +49,9 @@ - Why don't I get any sound on MacOS? - TODO mention microphone permission + Apps need to be granted access to the microphone in order to record sound from any source. + Without microphone access, things appear to be running well but only silence is recorded. + See [Microphone access](./backend_coreaudio.md#microphone-access) ## Filtering diff --git a/backend_coreaudio.md b/backend_coreaudio.md index 63f57a0..56187f5 100644 --- a/backend_coreaudio.md +++ b/backend_coreaudio.md @@ -11,14 +11,23 @@ One format is used for communicating with the application. This is typically 32-bit float, the same format that CoreAudio uses internally. The other format (called the physical format) is the one used to send or receive data to/from the sound card driver. -## Microphone permissions -In order to capture audio on macOS, the application needs the be given permission. -First time CamillaDSP is launched, there should be a pop-up asking if the Terminal app +## Microphone access +In order to capture audio on macOS, an application needs the be given access. +First time CamillaDSP is launched, the system should show a popup asking if the Terminal app should be allowed to use the microphone. -This may be misleading, as the "microphone" permission covers all recording of sound, -and not only the microphone. -If there is no pop-up, or if the permission was mistakenly denied, -TODO mention steps. +This is somewhat misleading, as the microphone access covers all recording of sound, +not only from the microphone. + +Without this access, there is no error message and CamillaDSP appears to be running ok, +but only records silence. +If this happens, open System Settings, select "Privacy & Security", and click "Microphone". +Verify that "Terminal" is listed and enabled. + +There is no way to manually add approved apps to the list. +If "Terminal" is not listed, try executing `tccutil reset Microphone` in the terminal. +This resets the microphone access for all apps, +and should make the popup appear next time CamillaDSP is started. + ## Capturing audio from other applications From 63ddc8d53ec59b5319d464c2f0b8e4856d373912 Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 20 Aug 2024 17:23:53 +0200 Subject: [PATCH 086/135] Fix rew link in toc --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2cd46c8..c6e823e 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ It does not matter if the damage is caused by incorrect usage or a bug in the so - **[Mixer and Processor step](#mixer-and-processor-step)** - **[Tokens in names](#tokens-in-names)** - **[Bypassing steps](#bypassing-steps)** -- **[Export filters from REW](#export-filters-from-rew)** +- **[Using filters from REW](#using-filters-from-rew)** - **[Visualizing the config](#visualizing-the-config)** **[Related projects](#related-projects)** From 05023bac1b366eaa3502433a7eba7f4f10620fbb Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 20 Aug 2024 17:35:44 +0200 Subject: [PATCH 087/135] Clippy --- src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index a3889e7..35bf081 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1699,7 +1699,7 @@ fn check_and_replace_relative_path(path_str: &mut String, config_path: &Path) { } else { debug!("{} is relative", path_str); let mut in_config_dir = config_path.to_path_buf(); - in_config_dir.push(&path_str); + in_config_dir.push(path_str); if in_config_dir.exists() { debug!("Using {} found relative to config file dir", path_str); *path_str = in_config_dir.to_string_lossy().into(); From 3bd5e892cad05f7855054c3843845f278ab29fee Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 20 Aug 2024 17:42:43 +0200 Subject: [PATCH 088/135] Clippy again --- src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 35bf081..a3889e7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1699,7 +1699,7 @@ fn check_and_replace_relative_path(path_str: &mut String, config_path: &Path) { } else { debug!("{} is relative", path_str); let mut in_config_dir = config_path.to_path_buf(); - in_config_dir.push(path_str); + in_config_dir.push(&path_str); if in_config_dir.exists() { debug!("Using {} found relative to config file dir", path_str); *path_str = in_config_dir.to_string_lossy().into(); From bc202f26dbd18ea1560ceeb64b4231a0ba258ee7 Mon Sep 17 00:00:00 2001 From: Henrik Date: Mon, 26 Aug 2024 22:43:57 +0200 Subject: [PATCH 089/135] Add more tests for config switch --- .../config_load_test/test_set_config.py | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/testscripts/config_load_test/test_set_config.py b/testscripts/config_load_test/test_set_config.py index 8a83297..f5b359a 100644 --- a/testscripts/config_load_test/test_set_config.py +++ b/testscripts/config_load_test/test_set_config.py @@ -5,6 +5,9 @@ import signal import shutil from subprocess import check_output +from copy import deepcopy +import yaml +import json # ---------- Constants ----------- @@ -81,6 +84,83 @@ def test_set_via_ws(camillaclient, delay, reps): time.sleep(0.5) assert_active(camillaclient, "nbr 4") +def test_only_pipeline_via_ws(camillaclient): + # Change between configs that only differ in the pipeline + print("Changing slowly") + for n in range(4): + print(f"Set config 1") + conf = yaml.safe_load(CONFIGS[0]) + # conf1 unmodified + camillaclient.config.set_active(conf) + time.sleep(1) + active = camillaclient.config.active() + assert active["pipeline"][0]["names"] == ["testfilter"] + + # conf1 with added filter in pipeline + active["pipeline"][0]["names"] = ["testfilter", "testfilter"] + camillaclient.config.set_active(active) + time.sleep(1) + active = camillaclient.config.active() + assert active["pipeline"][0]["names"] == ["testfilter", "testfilter"] + + # conf1 with empty pipeline + active["pipeline"] = [] + camillaclient.config.set_active(active) + time.sleep(1) + active = camillaclient.config.active() + assert active["pipeline"] == [] + +def test_only_filter_via_ws(camillaclient): + # Change between configs that only differ in the filter defs + print("Changing slowly") + for n in range(4): + print(f"Set config 1") + conf = yaml.safe_load(CONFIGS[0]) + # conf1 unmodified + camillaclient.config.set_active(conf) + time.sleep(1) + active = camillaclient.config.active() + assert active["filters"]["testfilter"]["parameters"]["freq"] == 5000.0 + + # conf1 with added filter in pipeline + active["filters"]["testfilter"]["parameters"]["freq"] = 6000.0 + camillaclient.config.set_active(active) + time.sleep(1) + active = camillaclient.config.active() + assert active["filters"]["testfilter"]["parameters"]["freq"] == 6000.0 + + # conf1 with empty pipeline + active["filters"]["testfilter"]["parameters"]["freq"] = 7000.0 + camillaclient.config.set_active(active) + time.sleep(1) + active = camillaclient.config.active() + assert active["filters"]["testfilter"]["parameters"]["freq"] == 7000.0 + +def test_only_pipeline_json_via_ws(camillaclient): + # Change between configs that only differ in the pipeline, sent as json + print("Changing slowly") + for n in range(4): + print(f"Set config 1") + conf = yaml.safe_load(CONFIGS[0]) + # conf1 unmodified + camillaclient.config.set_active_json(json.dumps(conf)) + time.sleep(1) + active = camillaclient.config.active() + assert active["pipeline"][0]["names"] == ["testfilter"] + + # conf1 with added filter in pipeline + active["pipeline"][0]["names"] = ["testfilter", "testfilter"] + camillaclient.config.set_active_json(json.dumps(active)) + time.sleep(1) + active = camillaclient.config.active() + assert active["pipeline"][0]["names"] == ["testfilter", "testfilter"] + + # conf1 with empty pipeline + active["pipeline"] = [] + camillaclient.config.set_active_json(json.dumps(active)) + time.sleep(1) + active = camillaclient.config.active() + assert active["pipeline"] == [] # ---------- Test changing config by changing config path and reloading ----------- From 25a57aa65d1c3e144e8d40468ef3da12ed3cd9e4 Mon Sep 17 00:00:00 2001 From: Henrik Date: Sun, 1 Sep 2024 16:07:37 +0200 Subject: [PATCH 090/135] Ensure the playback queue has more than one buffer --- src/coreaudiodevice.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreaudiodevice.rs b/src/coreaudiodevice.rs index de28cbd..64b5210 100644 --- a/src/coreaudiodevice.rs +++ b/src/coreaudiodevice.rs @@ -388,7 +388,7 @@ impl PlaybackDevice for CoreaudioPlaybackDevice { .name("CoreaudioPlayback".to_string()) .spawn(move || { // Devices typically request around 1000 frames per buffer, set a reasonable capacity for the channel - let channel_capacity = 8 * 1024 / chunksize + 1; + let channel_capacity = 8 * 1024 / chunksize + 3; debug!("Using a playback channel capacity of {channel_capacity} chunks."); let (tx_dev, rx_dev) = bounded(channel_capacity); let buffer_fill = Arc::new(AtomicUsize::new(0)); From 8a33c65c5e07c8139a22e62a6032aa3bfe359ecc Mon Sep 17 00:00:00 2001 From: Henrik Date: Sun, 1 Sep 2024 22:31:08 +0200 Subject: [PATCH 091/135] Add better buffer level estimator --- src/coreaudiodevice.rs | 25 +++++++++++-------------- src/countertimer.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/coreaudiodevice.rs b/src/coreaudiodevice.rs index 64b5210..c0f9830 100644 --- a/src/coreaudiodevice.rs +++ b/src/coreaudiodevice.rs @@ -13,9 +13,8 @@ use std::ffi::CStr; use std::mem; use std::os::raw::{c_char, c_void}; use std::ptr::{null, null_mut}; -use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::mpsc; -use std::sync::{Arc, Barrier}; +use std::sync::{Arc, Barrier, Mutex}; use std::thread; use std::time::Duration; @@ -391,7 +390,7 @@ impl PlaybackDevice for CoreaudioPlaybackDevice { let channel_capacity = 8 * 1024 / chunksize + 3; debug!("Using a playback channel capacity of {channel_capacity} chunks."); let (tx_dev, rx_dev) = bounded(channel_capacity); - let buffer_fill = Arc::new(AtomicUsize::new(0)); + let buffer_fill = Arc::new(Mutex::new(countertimer::DeviceBufferEstimator::new(samplerate))); let buffer_fill_clone = buffer_fill.clone(); let mut buffer_avg = countertimer::Averager::new(); let mut timer = countertimer::Stopwatch::new(); @@ -400,10 +399,6 @@ impl PlaybackDevice for CoreaudioPlaybackDevice { peak: vec![0.0; channels], }; let blockalign = 4 * channels; - // Rough guess of the number of frames per callback. - let callback_frames = 512; - // TODO check if always 512! - //trace!("Estimated playback callback period to {} frames", callback_frames); let mut rate_controller = PIRateController::new_with_default_gains(samplerate, adjust_period as f64, target_level); let mut rate_adjust_value = 1.0; @@ -466,16 +461,18 @@ impl PlaybackDevice for CoreaudioPlaybackDevice { let byte = sample_queue.pop_front().unwrap_or(0); *bufferbyte = byte; } - let mut curr_buffer_fill = + let curr_buffer_fill = sample_queue.len() / blockalign + rx_dev.len() * chunksize; // Reduce the measured buffer fill by approximtely one callback size // to force a larger. - if curr_buffer_fill > callback_frames { - curr_buffer_fill -= callback_frames; - } else { - curr_buffer_fill = 0; + //if curr_buffer_fill > callback_frames { + // curr_buffer_fill -= callback_frames; + //} else { + // curr_buffer_fill = 0; + //} + if let Ok(mut estimator) = buffer_fill_clone.try_lock() { + estimator.add(curr_buffer_fill) } - buffer_fill_clone.store(curr_buffer_fill, Ordering::Relaxed); Ok(()) }); match callback_res { @@ -524,7 +521,7 @@ impl PlaybackDevice for CoreaudioPlaybackDevice { } match channel.recv() { Ok(AudioMessage::Audio(chunk)) => { - buffer_avg.add_value(buffer_fill.load(Ordering::Relaxed) as f64); + buffer_avg.add_value(buffer_fill.try_lock().map(|b| b.estimate() as f64).unwrap_or_default()); if adjust && timer.larger_than_millis((1000.0 * adjust_period) as u64) { if let Some(av_delay) = buffer_avg.average() { let speed = rate_controller.next(av_delay); diff --git a/src/countertimer.rs b/src/countertimer.rs index 175e1b5..8cb4c2a 100644 --- a/src/countertimer.rs +++ b/src/countertimer.rs @@ -6,6 +6,38 @@ use std::time::{Duration, Instant}; /// A counter for watching if the signal has been silent /// for longer than a given limit. + +pub struct DeviceBufferEstimator { + update_time: Instant, + frames: usize, + sample_rate: f32, +} + +impl DeviceBufferEstimator { + pub fn new(sample_rate: usize) -> Self { + DeviceBufferEstimator { + update_time: Instant::now(), + frames: 0, + sample_rate: sample_rate as f32, + } + } + + pub fn add(&mut self, frames: usize) { + self.update_time = Instant::now(); + self.frames = frames; + } + + pub fn estimate(&self) -> usize { + let now = Instant::now(); + let time_passed = now.duration_since(self.update_time).as_secs_f32(); + let frames_consumed = (self.sample_rate * time_passed) as usize; + if frames_consumed >= self.frames { + return 0; + } + self.frames - frames_consumed + } +} + pub struct SilenceCounter { silence_threshold: PrcFmt, silence_limit_nbr: usize, From bf0c1ea1b2dd67e59501212672ebe86ede77a38a Mon Sep 17 00:00:00 2001 From: Henrik Date: Sun, 1 Sep 2024 22:33:16 +0200 Subject: [PATCH 092/135] Python script for logging load and buffer level --- testscripts/log_load_and_level.py | 60 +++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 testscripts/log_load_and_level.py diff --git a/testscripts/log_load_and_level.py b/testscripts/log_load_and_level.py new file mode 100644 index 0000000..05e9c01 --- /dev/null +++ b/testscripts/log_load_and_level.py @@ -0,0 +1,60 @@ +import csv +import time +from datetime import datetime +from camilladsp import CamillaClient +from matplotlib import pyplot + +cdsp = CamillaClient("localhost", 1234) +cdsp.connect() + +loop_delay = 0.5 + +times = [] +loads = [] +levels = [] + +start = time.time() +start_time = datetime.now().isoformat() + +pyplot.ion() +fig = pyplot.figure() +ax1 = fig.add_subplot(211) +plot1, = ax1.plot([], []) +ax2 = fig.add_subplot(212) +plot2, = ax2.plot([], []) + +running = True +try: + while running: + now = time.time() + prc_load = cdsp.status.processing_load() + buffer_level = cdsp.status.buffer_level() + times.append(now - start) + loads.append(prc_load) + levels.append(buffer_level) + #ax.plot(times, loads) + plot1.set_data(times, loads) + plot2.set_data(times, levels) + ax1.relim() + ax1.autoscale_view(True, True, True) + ax2.relim() + ax2.autoscale_view(True, True, True) + + # drawing updated values + pyplot.draw() + fig.canvas.draw() + fig.canvas.flush_events() + #pyplot.show() + time.sleep(loop_delay) + print(now) +except KeyboardInterrupt: + print("stopping") + pass + +csv_name = f"loadlog_{start_time}.csv" +with open(csv_name, 'w', newline='') as f: + writer = csv.writer(f) + writer.writerow(["time", "load", "bufferlevel"]) + writer.writerows(zip(times, loads, levels)) + +print(f"saved {len(times)} records to '{csv_name}'") \ No newline at end of file From 1a39e9ab69a867e16463903c703dec08cfa5d38b Mon Sep 17 00:00:00 2001 From: HEnquist Date: Tue, 3 Sep 2024 20:24:25 +0200 Subject: [PATCH 093/135] Better buffer estimation also for wasapi --- src/wasapidevice.rs | 14 ++++++++------ testscripts/log_load_and_level.py | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/wasapidevice.rs b/src/wasapidevice.rs index 4c01a22..9e03a53 100644 --- a/src/wasapidevice.rs +++ b/src/wasapidevice.rs @@ -9,9 +9,9 @@ use parking_lot::{RwLock, RwLockUpgradableReadGuard}; use rubato::VecResampler; use std::collections::VecDeque; use std::rc::Rc; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc; -use std::sync::{Arc, Barrier}; +use std::sync::{Arc, Barrier, Mutex}; use std::thread; use std::time::Duration; use wasapi; @@ -289,7 +289,7 @@ fn open_capture( struct PlaybackSync { rx_play: Receiver, tx_cb: Sender, - bufferfill: Arc, + bufferfill: Arc>, } enum PlaybackDeviceMessage { @@ -425,7 +425,9 @@ fn playback_loop( None, )?; let curr_buffer_fill = sample_queue.len() / blockalign + sync.rx_play.len() * chunksize; - sync.bufferfill.store(curr_buffer_fill, Ordering::Relaxed); + if let Ok(mut estimator) = sync.bufferfill.try_lock() { + estimator.add(curr_buffer_fill) + } trace!("write ok"); //println!("{} bef",prev_inst.elapsed().as_micros()); if handle.wait_for_event(1000).is_err() { @@ -641,7 +643,7 @@ impl PlaybackDevice for WasapiPlaybackDevice { let (tx_dev, rx_dev) = bounded(channel_capacity); let (tx_state_dev, rx_state_dev) = bounded(0); let (tx_disconnectreason, rx_disconnectreason) = unbounded(); - let buffer_fill = Arc::new(AtomicUsize::new(0)); + let buffer_fill = Arc::new(Mutex::new(countertimer::DeviceBufferEstimator::new(samplerate))); let buffer_fill_clone = buffer_fill.clone(); let mut buffer_avg = countertimer::Averager::new(); let mut timer = countertimer::Stopwatch::new(); @@ -751,7 +753,7 @@ impl PlaybackDevice for WasapiPlaybackDevice { 0u8; channels * chunk.frames * sample_format.bytes_per_sample() ]; - buffer_avg.add_value(buffer_fill.load(Ordering::Relaxed) as f64); + buffer_avg.add_value(buffer_fill.try_lock().map(|b| b.estimate() as f64).unwrap_or_default()); { if adjust && timer.larger_than_millis((1000.0 * adjust_period) as u64) { if let Some(av_delay) = buffer_avg.average() { diff --git a/testscripts/log_load_and_level.py b/testscripts/log_load_and_level.py index 05e9c01..9542305 100644 --- a/testscripts/log_load_and_level.py +++ b/testscripts/log_load_and_level.py @@ -14,7 +14,7 @@ levels = [] start = time.time() -start_time = datetime.now().isoformat() +start_time = datetime.now().strftime("%y.%m.%d_%H.%M.%S") pyplot.ion() fig = pyplot.figure() From 173dd0d2f68d279fde2e2a2597976bb8044146a0 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Tue, 3 Sep 2024 20:35:58 +0200 Subject: [PATCH 094/135] Clean up --- src/coreaudiodevice.rs | 7 ------- src/wasapidevice.rs | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/coreaudiodevice.rs b/src/coreaudiodevice.rs index c0f9830..2b6b72f 100644 --- a/src/coreaudiodevice.rs +++ b/src/coreaudiodevice.rs @@ -463,13 +463,6 @@ impl PlaybackDevice for CoreaudioPlaybackDevice { } let curr_buffer_fill = sample_queue.len() / blockalign + rx_dev.len() * chunksize; - // Reduce the measured buffer fill by approximtely one callback size - // to force a larger. - //if curr_buffer_fill > callback_frames { - // curr_buffer_fill -= callback_frames; - //} else { - // curr_buffer_fill = 0; - //} if let Ok(mut estimator) = buffer_fill_clone.try_lock() { estimator.add(curr_buffer_fill) } diff --git a/src/wasapidevice.rs b/src/wasapidevice.rs index 9e03a53..522cc1a 100644 --- a/src/wasapidevice.rs +++ b/src/wasapidevice.rs @@ -635,7 +635,7 @@ impl PlaybackDevice for WasapiPlaybackDevice { .name("WasapiPlayback".to_string()) .spawn(move || { // Devices typically request around 1000 frames per buffer, set a reasonable capacity for the channel - let channel_capacity = 8 * 1024 / chunksize + 1; + let channel_capacity = 8 * 1024 / chunksize + 3; debug!( "Using a playback channel capacity of {} chunks.", channel_capacity From 1990f0df0c1473b233499452ad7a37345cbcad02 Mon Sep 17 00:00:00 2001 From: Henrik Date: Sat, 7 Sep 2024 11:52:01 +0200 Subject: [PATCH 095/135] Request real-time thread prio on macos --- Cargo.toml | 1 + src/coreaudiodevice.rs | 51 +++++++++++++++++++++++++++++++ src/processing.rs | 31 +++++++++++++++++++ testscripts/log_load_and_level.py | 41 +++++++++++++++---------- 4 files changed, 108 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bdd08e0..8c02772 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ rawsample = "0.2.0" circular-queue = "0.2.6" parking_lot = { version = "0.12.1", features = ["hardware-lock-elision"] } crossbeam-channel = "0.5" +audio_thread_priority = "0.32.0" [build-dependencies] version_check = "0.9" diff --git a/src/coreaudiodevice.rs b/src/coreaudiodevice.rs index 2b6b72f..6e9a6ef 100644 --- a/src/coreaudiodevice.rs +++ b/src/coreaudiodevice.rs @@ -32,6 +32,10 @@ use coreaudio::error::Error as CoreAudioError; use coreaudio::sys::*; +use audio_thread_priority::{ + demote_current_thread_from_real_time, promote_current_thread_to_real_time, +}; + use crate::CommandMessage; use crate::PrcFmt; use crate::ProcessingState; @@ -502,6 +506,20 @@ impl PlaybackDevice for CoreaudioPlaybackDevice { return; } } + let thread_handle = + match promote_current_thread_to_real_time(chunksize as u32, samplerate as u32) { + Ok(h) => { + debug!("Playback thread has real-time priority."); + Some(h) + } + Err(err) => { + warn!( + "Playback thread could not get real time priority, error: {}", + err + ); + None + } + }; 'deviceloop: loop { if !alive_listener.is_alive() { error!("Playback device is no longer alive"); @@ -596,6 +614,16 @@ impl PlaybackDevice for CoreaudioPlaybackDevice { } } } + if let Some(h) = thread_handle { + match demote_current_thread_from_real_time(h) { + Ok(_) => { + debug!("Playback thread returned to normal priority.") + } + Err(_) => { + warn!("Could not bring the playback thread back to normal priority.") + } + }; + } release_ownership(device_id).unwrap_or(()); })?; Ok(Box::new(handle)) @@ -784,6 +812,19 @@ impl CaptureDevice for CoreaudioCaptureDevice { return; }, } + let thread_handle = match promote_current_thread_to_real_time(chunksize as u32, samplerate as u32) { + Ok(h) => { + debug!("Capture thread has real-time priority."); + Some(h) + } + Err(err) => { + warn!( + "Capture thread could not get real time priority, error: {}", + err + ); + None + } + }; 'deviceloop: loop { match command_channel.try_recv() { Ok(CommandMessage::Exit) => { @@ -974,6 +1015,16 @@ impl CaptureDevice for CoreaudioCaptureDevice { } } } + if let Some(h) = thread_handle { + match demote_current_thread_from_real_time(h) { + Ok(_) => { + debug!("Capture thread returned to normal priority.") + } + Err(_) => { + warn!("Could not bring the capture thread back to normal priority.") + } + }; + } capture_status.write().state = ProcessingState::Inactive; })?; Ok(Box::new(handle)) diff --git a/src/processing.rs b/src/processing.rs index 4aaa6a4..3b4a71d 100644 --- a/src/processing.rs +++ b/src/processing.rs @@ -2,6 +2,9 @@ use crate::audiodevice::*; use crate::config; use crate::filters; use crate::ProcessingParameters; +use audio_thread_priority::{ + demote_current_thread_from_real_time, promote_current_thread_to_real_time, +}; use std::sync::mpsc; use std::sync::{Arc, Barrier}; use std::thread; @@ -15,8 +18,26 @@ pub fn run_processing( processing_params: Arc, ) -> thread::JoinHandle<()> { thread::spawn(move || { + let chunksize = conf_proc.devices.chunksize; + let samplerate = conf_proc.devices.samplerate; let mut pipeline = filters::Pipeline::from_config(conf_proc, processing_params.clone()); debug!("build filters, waiting to start processing loop"); + + let thread_handle = + match promote_current_thread_to_real_time(chunksize as u32, samplerate as u32) { + Ok(h) => { + debug!("Processing thread has real-time priority."); + Some(h) + } + Err(err) => { + warn!( + "Processing thread could not get real time priority, error: {}", + err + ); + None + } + }; + barrier_proc.wait(); debug!("Processing loop starts now!"); loop { @@ -85,5 +106,15 @@ pub fn run_processing( }; } processing_params.set_processing_load(0.0); + if let Some(h) = thread_handle { + match demote_current_thread_from_real_time(h) { + Ok(_) => { + debug!("Procesing thread returned to normal priority.") + } + Err(_) => { + warn!("Could not bring the processing thread back to normal priority.") + } + }; + } }) } diff --git a/testscripts/log_load_and_level.py b/testscripts/log_load_and_level.py index 9542305..93a30c4 100644 --- a/testscripts/log_load_and_level.py +++ b/testscripts/log_load_and_level.py @@ -8,6 +8,7 @@ cdsp.connect() loop_delay = 0.5 +plot_interval = 10 times = [] loads = [] @@ -18,12 +19,15 @@ pyplot.ion() fig = pyplot.figure() -ax1 = fig.add_subplot(211) +ax1 = fig.add_subplot(311) plot1, = ax1.plot([], []) -ax2 = fig.add_subplot(212) -plot2, = ax2.plot([], []) +ax2 = fig.add_subplot(312) +ax3 = fig.add_subplot(313) +plot3, = ax3.plot([], []) + running = True +plot_counter = 0 try: while running: now = time.time() @@ -32,21 +36,26 @@ times.append(now - start) loads.append(prc_load) levels.append(buffer_level) - #ax.plot(times, loads) - plot1.set_data(times, loads) - plot2.set_data(times, levels) - ax1.relim() - ax1.autoscale_view(True, True, True) - ax2.relim() - ax2.autoscale_view(True, True, True) - - # drawing updated values - pyplot.draw() - fig.canvas.draw() - fig.canvas.flush_events() + plot_counter += 1 + if plot_counter > plot_interval: + plot_counter = 0 + #ax.plot(times, loads) + plot1.set_data(times, loads) + plot3.set_data(times, levels) + ax1.relim() + ax1.autoscale_view(True, True, True) + ax3.relim() + ax3.autoscale_view(True, True, True) + ax2.cla() + ax2.hist(loads) + + # drawing updated values + pyplot.draw() + fig.canvas.draw() + fig.canvas.flush_events() + print(now) #pyplot.show() time.sleep(loop_delay) - print(now) except KeyboardInterrupt: print("stopping") pass From 1c9c648bbbd96867ff4cb1fc50e3cf4a2df0acd0 Mon Sep 17 00:00:00 2001 From: Henrik Date: Sat, 7 Sep 2024 21:14:55 +0200 Subject: [PATCH 096/135] No default features for audio thread prio crate --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8c02772..8094566 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,7 @@ rawsample = "0.2.0" circular-queue = "0.2.6" parking_lot = { version = "0.12.1", features = ["hardware-lock-elision"] } crossbeam-channel = "0.5" -audio_thread_priority = "0.32.0" +audio_thread_priority = { version = "0.32.0", default-features = false } [build-dependencies] version_check = "0.9" From 76ad297aa402cd4d7181a97a7d19294aac032fb2 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Sat, 7 Sep 2024 21:40:48 +0200 Subject: [PATCH 097/135] Real time prio for Alsa --- src/alsadevice.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++++ src/processing.rs | 2 +- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index bfebea7..f22d7a3 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -19,6 +19,10 @@ use std::sync::{mpsc, Arc, Barrier}; use std::thread; use std::time::Instant; +use audio_thread_priority::{ + demote_current_thread_from_real_time, promote_current_thread_to_real_time, +}; + use crate::alsadevice_buffermanager::{ CaptureBufferManager, DeviceBufferManager, PlaybackBufferManager, }; @@ -479,6 +483,22 @@ fn playback_loop_bytes( params.target_level, ); trace!("PB: {:?}", buf_manager); + let thread_handle = match promote_current_thread_to_real_time( + params.chunksize as u32, + params.samplerate as u32, + ) { + Ok(h) => { + debug!("Playback thread has real-time priority."); + Some(h) + } + Err(err) => { + warn!( + "Playback thread could not get real time priority, error: {}", + err + ); + None + } + }; loop { let eos_in_drain = if device_stalled { drain_check_eos(&channels.audio) @@ -654,6 +674,16 @@ fn playback_loop_bytes( } } } + if let Some(h) = thread_handle { + match demote_current_thread_from_real_time(h) { + Ok(_) => { + debug!("Playback thread returned to normal priority.") + } + Err(_) => { + warn!("Could not bring the playback thread back to normal priority.") + } + }; + } } fn drain_check_eos(audio: &mpsc::Receiver) -> Option { @@ -769,6 +799,22 @@ fn capture_loop_bytes( peak: vec![0.0; params.channels], }; let mut channel_mask = vec![true; params.channels]; + let thread_handle = match promote_current_thread_to_real_time( + params.chunksize as u32, + params.samplerate as u32, + ) { + Ok(h) => { + debug!("Capture thread has real-time priority."); + Some(h) + } + Err(err) => { + warn!( + "Capture thread could not get real time priority, error: {}", + err + ); + None + } + }; loop { match channels.command.try_recv() { Ok(CommandMessage::Exit) => { @@ -967,6 +1013,16 @@ fn capture_loop_bytes( } } } + if let Some(h) = thread_handle { + match demote_current_thread_from_real_time(h) { + Ok(_) => { + debug!("Capture thread returned to normal priority.") + } + Err(_) => { + warn!("Could not bring the capture thread back to normal priority.") + } + }; + } params.capture_status.write().state = ProcessingState::Inactive; } diff --git a/src/processing.rs b/src/processing.rs index 3b4a71d..8e5c66e 100644 --- a/src/processing.rs +++ b/src/processing.rs @@ -109,7 +109,7 @@ pub fn run_processing( if let Some(h) = thread_handle { match demote_current_thread_from_real_time(h) { Ok(_) => { - debug!("Procesing thread returned to normal priority.") + debug!("Processing thread returned to normal priority.") } Err(_) => { warn!("Could not bring the processing thread back to normal priority.") From eb2dee5e8803b0657f09ed8515fa697a574c2a16 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sat, 7 Sep 2024 22:13:21 +0200 Subject: [PATCH 098/135] Raise audio thread prio on wasapi --- src/wasapidevice.rs | 94 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 21 deletions(-) diff --git a/src/wasapidevice.rs b/src/wasapidevice.rs index 522cc1a..f0c4c44 100644 --- a/src/wasapidevice.rs +++ b/src/wasapidevice.rs @@ -16,9 +16,9 @@ use std::thread; use std::time::Duration; use wasapi; use wasapi::DeviceCollection; -use windows::core::w; -use windows::Win32::System::Threading::{ - AvSetMmThreadCharacteristicsW, AvSetMmThreadPriority, AVRT_PRIORITY_HIGH, + +use audio_thread_priority::{ + demote_current_thread_from_real_time, promote_current_thread_to_real_time, }; use crate::CommandMessage; @@ -337,16 +337,19 @@ fn playback_loop( debug!("Waited for data for {} ms", waited_millis); // Raise priority - let mut task_idx = 0; - let task_handle = unsafe { AvSetMmThreadCharacteristicsW(w!("Pro Audio"), &mut task_idx)? }; - if task_idx > 0 { - debug!("Playback thread raised priority, task index: {}", task_idx); - unsafe { - AvSetMmThreadPriority(task_handle, AVRT_PRIORITY_HIGH)?; + let _thread_handle = match promote_current_thread_to_real_time(0, 1) { + Ok(h) => { + debug!("Playback inner thread has real-time priority."); + Some(h) } - } else { - warn!("Failed to raise playback thread priority"); - } + Err(err) => { + warn!( + "Playback inner thread could not get real time priority, error: {}", + err + ); + None + } + }; audio_client.start_stream()?; let mut running = true; @@ -478,16 +481,19 @@ fn capture_loop( let mut saved_buffer: Option> = None; // Raise priority - let mut task_idx = 0; - let task_handle = unsafe { AvSetMmThreadCharacteristicsW(w!("Pro Audio"), &mut task_idx)? }; - if task_idx > 0 { - debug!("Capture thread raised priority, task index: {}", task_idx); - unsafe { - AvSetMmThreadPriority(task_handle, AVRT_PRIORITY_HIGH)?; + let _thread_handle = match promote_current_thread_to_real_time(0, 1) { + Ok(h) => { + debug!("Capture inner thread has real-time priority."); + Some(h) } - } else { - warn!("Failed to raise capture thread priority"); - } + Err(err) => { + warn!( + "Capture inner thread could not get real time priority, error: {}", + err + ); + None + } + }; trace!("Starting capture stream"); audio_client.start_stream()?; trace!("Started capture stream"); @@ -724,6 +730,19 @@ impl PlaybackDevice for WasapiPlaybackDevice { } debug!("Playback device ready and waiting"); barrier.wait(); + let thread_handle = match promote_current_thread_to_real_time(0, 1) { + Ok(h) => { + debug!("Playback outer thread has real-time priority."); + Some(h) + } + Err(err) => { + warn!( + "Playback outer thread could not get real time priority, error: {}", + err + ); + None + } + }; debug!("Playback device starts now!"); loop { match rx_state_dev.try_recv() { @@ -822,6 +841,16 @@ impl PlaybackDevice for WasapiPlaybackDevice { } } } + if let Some(h) = thread_handle { + match demote_current_thread_from_real_time(h) { + Ok(_) => { + debug!("Playback outer thread returned to normal priority.") + } + Err(_) => { + warn!("Could not bring the outer playback thread back to normal priority.") + } + }; + } match tx_dev.send(PlaybackDeviceMessage::Stop) { Ok(_) => { debug!("Wait for inner playback thread to exit"); @@ -1013,6 +1042,19 @@ impl CaptureDevice for WasapiCaptureDevice { Err(_err) => {} } barrier.wait(); + let thread_handle = match promote_current_thread_to_real_time(0, 1) { + Ok(h) => { + debug!("Capture outer thread has real-time priority."); + Some(h) + } + Err(err) => { + warn!( + "Capture outer thread could not get real time priority, error: {}", + err + ); + None + } + }; debug!("Capture device starts now!"); loop { match command_channel.try_recv() { @@ -1181,6 +1223,16 @@ impl CaptureDevice for WasapiCaptureDevice { } } } + if let Some(h) = thread_handle { + match demote_current_thread_from_real_time(h) { + Ok(_) => { + debug!("Capture outer thread returned to normal priority.") + } + Err(_) => { + warn!("Could not bring the outer capture thread back to normal priority.") + } + }; + } stop_signal.store(true, Ordering::Relaxed); debug!("Wait for inner capture thread to exit"); innerhandle.join().unwrap_or(()); From 11176f83b99564dbd7c6f46a9fab38d0f5dc0f5e Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sun, 8 Sep 2024 21:36:46 +0200 Subject: [PATCH 099/135] Initial implementation of parallel filtering --- Cargo.toml | 1 + src/dither.rs | 4 +- src/filters.rs | 101 +++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bdd08e0..b1bb5a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ rawsample = "0.2.0" circular-queue = "0.2.6" parking_lot = { version = "0.12.1", features = ["hardware-lock-elision"] } crossbeam-channel = "0.5" +rayon = "1.10.0" [build-dependencies] version_check = "0.9" diff --git a/src/dither.rs b/src/dither.rs index 2b7ff22..4d51344 100644 --- a/src/dither.rs +++ b/src/dither.rs @@ -10,7 +10,7 @@ pub struct Dither<'a> { pub name: String, pub scalefact: PrcFmt, // have to `Box` because `dyn Ditherer` is not `Sized`. - ditherer: Box, + ditherer: Box, shaper: Option>, } @@ -453,7 +453,7 @@ impl<'a> NoiseShaper<'a> { } impl<'a> Dither<'a> { - pub fn new( + pub fn new( name: &str, bits: usize, ditherer: D, diff --git a/src/filters.rs b/src/filters.rs index b60bb8c..39a42db 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -20,6 +20,8 @@ use std::io::{BufRead, Seek, SeekFrom}; use std::sync::Arc; use std::time::Instant; +use rayon::prelude::*; + use crate::PrcFmt; use crate::ProcessingParameters; use crate::Res; @@ -170,7 +172,7 @@ pub fn read_wav(filename: &str, channel: usize) -> Res> { pub struct FilterGroup { channel: usize, - filters: Vec>, + filters: Vec>, } impl FilterGroup { @@ -184,11 +186,11 @@ impl FilterGroup { processing_params: Arc, ) -> Self { debug!("Build filter group from config"); - let mut filters = Vec::>::new(); + let mut filters = Vec::>::new(); for name in names { let filter_cfg = filter_configs[name].clone(); trace!("Create filter {} with config {:?}", name, filter_cfg); - let filter: Box = + let filter: Box = match filter_cfg { config::Filter::Conv { parameters, .. } => Box::new( fftconv::FftConv::from_config(name, waveform_length, parameters), @@ -286,11 +288,48 @@ impl FilterGroup { } } +pub struct ParallelFilters { + filters: Vec>>, +} + +impl ParallelFilters { + pub fn update_parameters( + &mut self, + filterconfigs: HashMap, + changed: &[String], + ) { + for channel_filters in &mut self.filters { + for filter in channel_filters { + if changed.iter().any(|n| n == filter.name()) { + filter.update_parameters(filterconfigs[filter.name()].clone()); + } + } + } + } + + /// Apply all the filters to an AudioChunk. + fn process_chunk(&mut self, input: &mut AudioChunk) -> Res<()> { + self.filters + .par_iter_mut() + .zip(input.waveforms.par_iter_mut()) + .for_each(|(f, w)| { + for filt in f { + if !w.is_empty() { + let _ = filt.process_waveform(w); + } + } + }); + + Ok(()) + } +} + /// A Pipeline is made up of a series of PipelineSteps, -/// each one can be a single Mixer of a group of Filters +/// each one can be a single Mixer or a group of Filters pub enum PipelineStep { MixerStep(mixer::Mixer), FilterStep(FilterGroup), + ParallelFiltersStep(ParallelFilters), ProcessorStep(Box), } @@ -398,6 +437,7 @@ impl Pipeline { 0, ); let secs_per_chunk = conf.devices.chunksize as f32 / conf.devices.samplerate as f32; + steps = parallelize_filters(&mut steps, conf.devices.capture.channels()); Pipeline { steps, volume, @@ -424,6 +464,9 @@ impl Pipeline { PipelineStep::FilterStep(flt) => { flt.update_parameters(conf.filters.as_ref().unwrap().clone(), filters); } + PipelineStep::ParallelFiltersStep(flt) => { + flt.update_parameters(conf.filters.as_ref().unwrap().clone(), filters); + } PipelineStep::ProcessorStep(proc) => { if processors.iter().any(|n| n == proc.name()) { proc.update_parameters( @@ -447,6 +490,9 @@ impl Pipeline { PipelineStep::FilterStep(flt) => { flt.process_chunk(&mut chunk).unwrap(); } + PipelineStep::ParallelFiltersStep(flt) => { + flt.process_chunk(&mut chunk).unwrap(); + } PipelineStep::ProcessorStep(comp) => { comp.process_chunk(&mut chunk).unwrap(); } @@ -460,6 +506,53 @@ impl Pipeline { } } +// Loop trough the pipeline to merge individual filter steps, +// in order use rayon to apply them in parallel. +fn parallelize_filters(steps: &mut Vec, nbr_channels: usize) -> Vec { + let mut new_steps: Vec = Vec::new(); + let mut parfilt = None; + let mut active_channels = nbr_channels; + for step in steps.drain(..) { + match step { + PipelineStep::MixerStep(ref mix) => { + if parfilt.is_some() { + new_steps.push(PipelineStep::ParallelFiltersStep(parfilt.take().unwrap())); + } + active_channels = mix.channels_out; + new_steps.push(step); + } + PipelineStep::ProcessorStep(_) => { + if parfilt.is_some() { + new_steps.push(PipelineStep::ParallelFiltersStep(parfilt.take().unwrap())); + } + new_steps.push(step); + } + PipelineStep::ParallelFiltersStep(_) => { + if parfilt.is_some() { + new_steps.push(PipelineStep::ParallelFiltersStep(parfilt.take().unwrap())); + } + new_steps.push(step); + } + PipelineStep::FilterStep(mut flt) => { + if parfilt.is_none() { + let mut filters = Vec::with_capacity(active_channels); + for _ in 0..active_channels { + filters.push(Vec::new()); + } + parfilt = Some(ParallelFilters { filters }); + } + if let Some(ref mut f) = parfilt { + f.filters[flt.channel].append(&mut flt.filters); + } + } + } + } + if parfilt.is_some() { + new_steps.push(PipelineStep::ParallelFiltersStep(parfilt.take().unwrap())); + } + new_steps +} + /// Validate the filter config, to give a helpful message intead of a panic. pub fn validate_filter(fs: usize, filter_config: &config::Filter) -> Res<()> { match filter_config { From 97c4a13c5ad65082b701aa62932d1e91468f35d7 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sun, 8 Sep 2024 21:51:07 +0200 Subject: [PATCH 100/135] Add logging in filter sorting function --- src/filters.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/filters.rs b/src/filters.rs index 39a42db..13ae99b 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -509,6 +509,7 @@ impl Pipeline { // Loop trough the pipeline to merge individual filter steps, // in order use rayon to apply them in parallel. fn parallelize_filters(steps: &mut Vec, nbr_channels: usize) -> Vec { + debug!("Merging filter steps to enable parallel processing"); let mut new_steps: Vec = Vec::new(); let mut parfilt = None; let mut active_channels = nbr_channels; @@ -516,25 +517,32 @@ fn parallelize_filters(steps: &mut Vec, nbr_channels: usize) -> Ve match step { PipelineStep::MixerStep(ref mix) => { if parfilt.is_some() { + debug!("Append parallel filter step to pipeline"); new_steps.push(PipelineStep::ParallelFiltersStep(parfilt.take().unwrap())); } active_channels = mix.channels_out; + debug!("Append mixer step to pipeline"); new_steps.push(step); } PipelineStep::ProcessorStep(_) => { if parfilt.is_some() { + debug!("Append parallel filter step to pipeline"); new_steps.push(PipelineStep::ParallelFiltersStep(parfilt.take().unwrap())); } + debug!("Append processor step to pipeline"); new_steps.push(step); } PipelineStep::ParallelFiltersStep(_) => { if parfilt.is_some() { + debug!("Append parallel filter step to pipeline"); new_steps.push(PipelineStep::ParallelFiltersStep(parfilt.take().unwrap())); } + debug!("Append existing parallel filter step to pipeline"); new_steps.push(step); } PipelineStep::FilterStep(mut flt) => { if parfilt.is_none() { + debug!("Start new parallel filter step"); let mut filters = Vec::with_capacity(active_channels); for _ in 0..active_channels { filters.push(Vec::new()); @@ -542,12 +550,18 @@ fn parallelize_filters(steps: &mut Vec, nbr_channels: usize) -> Ve parfilt = Some(ParallelFilters { filters }); } if let Some(ref mut f) = parfilt { + debug!( + "Adding {} filters to channel {} of parallel filter step", + flt.filters.len(), + flt.channel + ); f.filters[flt.channel].append(&mut flt.filters); } } } } if parfilt.is_some() { + debug!("Append parallel filter step to pipeline"); new_steps.push(PipelineStep::ParallelFiltersStep(parfilt.take().unwrap())); } new_steps From 94636e766a1bc1eb8c4396ee08d7900f64884da0 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Mon, 9 Sep 2024 22:08:50 +0200 Subject: [PATCH 101/135] Make parallel processing optional --- .gitignore | 5 ++++- src/config.rs | 6 ++++++ src/filters.rs | 10 +++++----- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 00c5468..05ebb9e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ default_config.yml active_config.yml camilladsp.log configs/ -coeffs/ \ No newline at end of file +coeffs/ +.venv +*.csv +*.exe \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index a3889e7..0656d14 100644 --- a/src/config.rs +++ b/src/config.rs @@ -594,6 +594,8 @@ pub struct Devices { pub volume_ramp_time: Option, #[serde(default)] pub volume_limit: Option, + #[serde(default)] + pub multithreaded: Option, } // Getters for all the defaults @@ -641,6 +643,10 @@ impl Devices { pub fn volume_limit(&self) -> f32 { self.volume_limit.unwrap_or(50.0) } + + pub fn multithreaded(&self) -> bool { + self.multithreaded.unwrap_or(false) + } } #[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)] diff --git a/src/filters.rs b/src/filters.rs index 13ae99b..598fa48 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -312,14 +312,12 @@ impl ParallelFilters { self.filters .par_iter_mut() .zip(input.waveforms.par_iter_mut()) + .filter(|(f, w)| !f.is_empty() && !w.is_empty()) .for_each(|(f, w)| { for filt in f { - if !w.is_empty() { - let _ = filt.process_waveform(w); - } + let _ = filt.process_waveform(w); } }); - Ok(()) } } @@ -437,7 +435,9 @@ impl Pipeline { 0, ); let secs_per_chunk = conf.devices.chunksize as f32 / conf.devices.samplerate as f32; - steps = parallelize_filters(&mut steps, conf.devices.capture.channels()); + if conf.devices.multithreaded() { + steps = parallelize_filters(&mut steps, conf.devices.capture.channels()); + } Pipeline { steps, volume, From 830bb85ed5cb0cfdccb0395451703cdebaa9ff4e Mon Sep 17 00:00:00 2001 From: HEnquist Date: Tue, 10 Sep 2024 21:35:04 +0200 Subject: [PATCH 102/135] Raise worker thread prio, add option to set number of threads --- src/config.rs | 6 ++++++ src/processing.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/config.rs b/src/config.rs index 0656d14..677ed5c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -596,6 +596,8 @@ pub struct Devices { pub volume_limit: Option, #[serde(default)] pub multithreaded: Option, + #[serde(default)] + pub worker_threads: Option, } // Getters for all the defaults @@ -647,6 +649,10 @@ impl Devices { pub fn multithreaded(&self) -> bool { self.multithreaded.unwrap_or(false) } + + pub fn worker_threads(&self) -> usize { + self.worker_threads.unwrap_or(0) + } } #[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)] diff --git a/src/processing.rs b/src/processing.rs index 8e5c66e..a9033f8 100644 --- a/src/processing.rs +++ b/src/processing.rs @@ -20,6 +20,8 @@ pub fn run_processing( thread::spawn(move || { let chunksize = conf_proc.devices.chunksize; let samplerate = conf_proc.devices.samplerate; + let multithreaded = conf_proc.devices.multithreaded(); + let nbr_threads = conf_proc.devices.worker_threads(); let mut pipeline = filters::Pipeline::from_config(conf_proc, processing_params.clone()); debug!("build filters, waiting to start processing loop"); @@ -38,6 +40,44 @@ pub fn run_processing( } }; + // Initialize rayon thread pool + if multithreaded { + match rayon::ThreadPoolBuilder::new() + .num_threads(nbr_threads) + .build_global() + { + Ok(_) => { + debug!( + "Initialized global thread pool with {} workers", + rayon::current_num_threads() + ); + rayon::broadcast(|_| { + match promote_current_thread_to_real_time( + chunksize as u32, + samplerate as u32, + ) { + Ok(_) => { + debug!( + "Worker thread {} has real-time priority.", + rayon::current_thread_index().unwrap_or_default() + ); + } + Err(err) => { + warn!( + "Worker thread {} could not get real time priority, error: {}", + rayon::current_thread_index().unwrap_or_default(), + err + ); + } + }; + }); + } + Err(err) => { + warn!("Failed to build thread pool, error: {}", err); + } + }; + } + barrier_proc.wait(); debug!("Processing loop starts now!"); loop { From 8fc02513a03b302b3857bdf4f5debf6d4e3aa9d1 Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 10 Sep 2024 22:44:55 +0200 Subject: [PATCH 103/135] Mention multithreading in readme --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index c6e823e..c70cf0e 100644 --- a/README.md +++ b/README.md @@ -886,6 +886,8 @@ devices: rate_measure_interval: 1.0 (*) volume_ramp_time: 400.0 (*) volume_limit: -12.0 (*) + multithreaded: false (*) + worker_threads: 4 (*) capture: type: Pulse channels: 2 @@ -1001,6 +1003,30 @@ A parameter marked (*) in any example is optional. If they are left out from the This setting controls the duration of this ramp when changing volume of the default volume control. The value must not be negative. If left out or set to `null`, it defaults to 400 ms. +* `multithreaded` and `worker_threads` (optional, defaults to `false` and automatic) + Setting `multithreaded` to `true` enables multithreaded processing. + When enabled, CamillaDSP creates a number of filtering tasks, by grouping the filters for each channel. + These tasks are then sent to a thread pool, where a number of threads are waiting to pick up work. + On a machine with multiple CPU cores, this allows filters to be processed in parallel, + which may increase performance. + After the workers have finished all the tasks, the results are returned to the main processing thread. + + Since Mixers and Processors work on all channels in the pipeline, + these cannnot be parallelized and are processed in the main thread. + Therefore, only the filters between mixers and/or processors can be + parallelized. + + Multithreaded processing can help for configurations that require a lot of processing power, + for example by using very long FIR filters, high sample rates, or very large number of channels. + It should only be used if needed, and should normally be disabled. + The synchronization with the worker threads adds some overhead that increases the overall CPU usage. + It also makes CamillaDSP more likely to be affected by other processes using the CPU, + which may cause buffer underruns. + + The number of worker threads can set manually using the `worker_threads` setting. + Leave it out or set it to zero to use the default of one worker thread per hardware thread + of the machine. + * `capture` and `playback` Input and output devices are defined in the same way. A device needs: From 9327529ae182de4c25c93ea2b6e1c4367d999e49 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Fri, 13 Sep 2024 20:59:27 +0200 Subject: [PATCH 104/135] Add log messages to help with multithreading settings --- src/processing.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/processing.rs b/src/processing.rs index a9033f8..9d5a1e9 100644 --- a/src/processing.rs +++ b/src/processing.rs @@ -22,6 +22,27 @@ pub fn run_processing( let samplerate = conf_proc.devices.samplerate; let multithreaded = conf_proc.devices.multithreaded(); let nbr_threads = conf_proc.devices.worker_threads(); + let hw_threads = std::thread::available_parallelism() + .map(|p| p.get()) + .unwrap_or_default(); + if nbr_threads > hw_threads && multithreaded { + warn!( + "Requested {} worker threads. For optimal performance, this number should not \ + exceed the available CPU cores, which is {}.", + nbr_threads, hw_threads + ); + } + if hw_threads == 1 && multithreaded { + warn!( + "This system only has one CPU core, multithreaded processing is not recommended." + ); + } + if nbr_threads == 1 && multithreaded { + warn!( + "Requested multithreaded processing with one worker thread. \ + Performance can improve by adding more threads or disabling multithreading." + ); + } let mut pipeline = filters::Pipeline::from_config(conf_proc, processing_params.clone()); debug!("build filters, waiting to start processing loop"); From 8fc1c1f2bfd915a9d3bbaa3a8469dcd85f47d2ca Mon Sep 17 00:00:00 2001 From: HEnquist Date: Fri, 13 Sep 2024 21:57:30 +0200 Subject: [PATCH 105/135] Improve readme on multithreading --- README.md | 51 +++++++++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index c70cf0e..8e7940f 100644 --- a/README.md +++ b/README.md @@ -993,10 +993,13 @@ A parameter marked (*) in any example is optional. If they are left out from the * `stop_on_rate_change` and `rate_measure_interval` (both optional) - Setting `stop_on_rate_change` to `true` makes CamillaDSP stop the processing if the measured capture sample rate changes. Default is `false`. + Setting `stop_on_rate_change` to `true` makes CamillaDSP stop the processing + if the measured capture sample rate changes. + Default is `false`. The `rate_measure_interval` setting is used for adjusting the measurement period. A longer period gives a more accurate measurement of the rate, at the cost of slower response when the rate changes. - The default is 1.0 seconds. Processing will stop after 3 measurements in a row are more than 4% off from the configured rate. + The default is 1.0 seconds. + Processing will stop after 3 measurements in a row are more than 4% off from the configured rate. The value of 4% is chosen to allow some variation, while still catching changes between for example 44.1 to 48 kHz. * `volume_ramp_time` (optional, defaults to 400 ms) @@ -1005,27 +1008,29 @@ A parameter marked (*) in any example is optional. If they are left out from the * `multithreaded` and `worker_threads` (optional, defaults to `false` and automatic) Setting `multithreaded` to `true` enables multithreaded processing. - When enabled, CamillaDSP creates a number of filtering tasks, by grouping the filters for each channel. - These tasks are then sent to a thread pool, where a number of threads are waiting to pick up work. + When this is enabled, CamillaDSP creates several filtering tasks by grouping the filters for each channel. + These tasks are then sent to a thread pool, where multiple threads are ready to pick up the work. On a machine with multiple CPU cores, this allows filters to be processed in parallel, - which may increase performance. - After the workers have finished all the tasks, the results are returned to the main processing thread. - - Since Mixers and Processors work on all channels in the pipeline, - these cannnot be parallelized and are processed in the main thread. - Therefore, only the filters between mixers and/or processors can be - parallelized. - - Multithreaded processing can help for configurations that require a lot of processing power, - for example by using very long FIR filters, high sample rates, or very large number of channels. - It should only be used if needed, and should normally be disabled. - The synchronization with the worker threads adds some overhead that increases the overall CPU usage. - It also makes CamillaDSP more likely to be affected by other processes using the CPU, + potentially boosting performance. + Once all tasks are completed, the results are returned to the main processing thread. + + However, Mixers and Processors, which work on all channels in the pipeline, + cannot be parallelized and are processed in the main thread. + Therefore, only the filters between mixers and/or processors can be parallelized. + + Multithreaded processing is beneficial for configurations that require significant processing power, + such as using very long FIR filters, high sample rates, or a large number of channels. + It should only be enabled if necessary, as it typically should remain disabled. + Synchronizing with worker threads adds some overhead, increasing overall CPU usage. + It also makes CamillaDSP more susceptible to other processes using the CPU, which may cause buffer underruns. - The number of worker threads can set manually using the `worker_threads` setting. - Leave it out or set it to zero to use the default of one worker thread per hardware thread - of the machine. + An exception to this recommendation is when both the input and output are files on disk, + allowing processing to run faster than real time. + In this scenario, multithreading is likely to improve throughput and should usually be enabled. + + The number of worker threads can be set manually using the `worker_threads` setting. + If left out or set to zero, it defaults to one worker thread per hardware thread of the machine. * `capture` and `playback` Input and output devices are defined in the same way. @@ -1161,7 +1166,8 @@ A parameter marked (*) in any example is optional. If they are left out from the ``` - The `RawFile` and `Stdin` capture devices support two additional optional parameters, for advanced handling of raw files and testing: + The `RawFile` and `Stdin` capture devices support two additional optional parameters, + for advanced handling of raw files and testing: * `skip_bytes`: Number of bytes to skip at the beginning of the file or stream. This can be used to skip over the header of some formats like .wav (which often has a 44-byte header). @@ -1169,7 +1175,8 @@ A parameter marked (*) in any example is optional. If they are left out from the * `read_bytes`: Read only up until the specified number of bytes. Leave it out or set it to zero to read until the end of the file or stream. - * Example, this will skip the first 50 bytes of the file (index 0-49) and then read the following 200 bytes (index 50-249). + * Example, this will skip the first 50 bytes of the file (index 0-49) + and then read the following 200 bytes (index 50-249). ``` skip_bytes: 50 read_bytes: 200 From 3d526b8792dac928ffe8b289ca6069ce527e97ba Mon Sep 17 00:00:00 2001 From: HEnquist Date: Fri, 13 Sep 2024 22:19:50 +0200 Subject: [PATCH 106/135] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c09d451..7d7e801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## v3.0.0 New features: +- Optional multithreaded filter processing. +- Request higher proprity of audio threads for improved stability. - Add a signal generator capture device. - Optionally write wav header when outputting to file or stdout. - Add `WavFile` capture device type for reading wav files. From 6e4e9940c431df352151f72295a072811b49cc47 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Thu, 19 Sep 2024 20:58:55 +0200 Subject: [PATCH 107/135] Make loudness filter log less on info level --- src/loudness.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/loudness.rs b/src/loudness.rs index 6be8441..e183518 100644 --- a/src/loudness.rs +++ b/src/loudness.rs @@ -100,7 +100,7 @@ impl Filter for Loudness { let high_boost = (relboost * self.high_boost) as PrcFmt; let low_boost = (relboost * self.low_boost) as PrcFmt; self.active = relboost > 0.001; - info!( + debug!( "Updating loudness biquads, relative boost {}%", 100.0 * relboost ); From 602d660aeaa715b04c8e5b352a23d30fd3ae7896 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Thu, 19 Sep 2024 22:19:50 +0200 Subject: [PATCH 108/135] Add optional log file rotation --- CHANGELOG.md | 1 + README.md | 19 ++++++++++++++++--- src/bin.rs | 46 ++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d7e801..76b474c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ New features: - Optional user-defined volume limits for volume adjust commands. - Add noise gate. - Add optional channel labels for capture devices and mixers. +- Optional log file rotation. Changes: - Remove the optional use of FFTW instead of RustFFT. - Rename `File` capture device to `RawFile`. diff --git a/README.md b/README.md index 8e7940f..36cc88c 100644 --- a/README.md +++ b/README.md @@ -457,7 +457,9 @@ Options: -s, --statefile Use the given file to persist the state -v... Increase message verbosity -l, --loglevel Set log level [possible values: trace, debug, info, warn, error, off] - -o, --logfile Write logs to file + -o, --logfile Write logs to the given file path + --log_rotate_size Rotate log file when the size in bytes exceeds this value + --log_keep_nbr Number of previous log files to keep -a, --address
IP address to bind websocket server to -p, --port Port for websocket server -w, --wait Wait for config from websocket @@ -493,8 +495,19 @@ Alternatively, the log level can be changed with the verbosity flag. By passing the verbosity flag once, `-v`, `debug` messages are enabled. If it's given twice, `-vv`, it also prints `trace` messages. -The log messages are normally written to the terminal via stderr, but they can instead be written to a file by giving the `--logfile` option. -The argument should be the path to the logfile. If this file is not writable, CamillaDSP will panic and exit. +The log messages are normally written to the terminal via stderr, +but they can instead be written to a file by giving the `--logfile` option. +The argument should be the path to the logfile. +If this file is not writable, CamillaDSP will panic and exit. + +Log rotation can be enabled by the `--log_rotate_size` option. +This creates a new log file whenever the log fize size exceeds the given size in bytes. +When rotation is enabled the current log file gets an added infix of `_rCURRENT`, +so for example `logfile.log` becomes `logfile_rCURRENT.log`. +When the file is rotated, the old logs are kept with a timestamp as infix, +for example `logfile_r2023-01-29_12-59-00.log`. +The default is to keep all previous log files, +but this can be limited by setting the `--log_keep_nbr` option to the desired number. ### Persistent storage of state diff --git a/src/bin.rs b/src/bin.rs index 966ae28..98dc9b6 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -509,11 +509,31 @@ fn main_process() -> i32 { ) .arg( Arg::new("logfile") - .help("Write logs to file") + .help("Write logs to the given file path") .short('o') .long("logfile") .value_name("LOGFILE") - .display_order(100) + .display_order(101) + .action(ArgAction::Set), + ) + .arg( + Arg::new("log_rotate_size") + .help("Rotate log file when the size in bytes exceeds this value") + .long("log_rotate_size") + .value_name("ROTATE_SIZE") + .display_order(102) + .requires("logfile") + .value_parser(clap::value_parser!(u32).range(1000..)) + .action(ArgAction::Set), + ) + .arg( + Arg::new("log_keep_nbr") + .help("Number of previous log files to keep") + .long("log_keep_nbr") + .value_name("KEEP_NBR") + .display_order(103) + .requires("log_rotate_size") + .value_parser(clap::value_parser!(u32)) .action(ArgAction::Set), ) .arg( @@ -722,13 +742,27 @@ fn main_process() -> i32 { fullpath.push(path); path = fullpath; } - flexi_logger::Logger::try_with_str(loglevel) + let mut logger = flexi_logger::Logger::try_with_str(loglevel) .unwrap() .format(custom_logger_format) .log_to_file(flexi_logger::FileSpec::try_from(path).unwrap()) - .write_mode(flexi_logger::WriteMode::Async) - .start() - .unwrap() + .write_mode(flexi_logger::WriteMode::Async); + + let cleanup = if let Some(keep_nbr) = matches.get_one::("log_keep_nbr") { + flexi_logger::Cleanup::KeepLogFiles(*keep_nbr as usize) + } else { + flexi_logger::Cleanup::Never + }; + + if let Some(rotate_size) = matches.get_one::("log_rotate_size") { + logger = logger.rotate( + flexi_logger::Criterion::Size(*rotate_size as u64), + flexi_logger::Naming::Timestamps, + cleanup, + ); + } + + logger.start().unwrap() } else { flexi_logger::Logger::try_with_str(loglevel) .unwrap() From b2fbed64961c747ef148cf6a725000c29d6b91af Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sun, 22 Sep 2024 21:49:41 +0200 Subject: [PATCH 109/135] Improve readme on choice of chunksize --- README.md | 45 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 36cc88c..b040441 100644 --- a/README.md +++ b/README.md @@ -922,18 +922,51 @@ A parameter marked (*) in any example is optional. If they are left out from the * `chunksize` - All processing is done in chunks of data. The `chunksize` is the number of samples each chunk will have per channel. - It's good if the number is an "easy" number like a power of two, since this speeds up the FFT in the Convolution filter. + All processing is done in chunks of data. + The `chunksize` is the number of samples each chunk will have per channel. + Suggested starting points for different sample rates: - 44.1 or 48 kHz: 1024 - 88.2 or 96 kHz: 2048 - 176.4 or 192 kHz: 4096 - The duration in seconds of a chunk is `chunksize/samplerate`, so the suggested values corresponds to about 22 ms per chunk. - This is a reasonable value, and making it shorter can increase the cpu usage and make buffer underruns more likely. + The duration in seconds of a chunk is `chunksize/samplerate`, + so the suggested values corresponds to about 22 ms per chunk. + This is a reasonable value. + + A larger chunk size generally reduces CPU usage, + but since the entire chunk must be captured before processing, + it can cause unacceptably long delays. + Conversely, using a smaller chunk size can reduce latency + but will increase CPU usage and the risk of buffer underruns. + + __Choosing chunk size for best performance__ + + FIR filters are automatically padded as needed, + so there is no need match chunk size and filter length. + + CamillaDSP uses FFT for convolution, with an FFT length of `2 * chunksize`. + Therefore, the chunk size should be chosen for optimal FFT performance. + + Using a power of two for the chunk size is ideal for best performance. + The FFT also works well with numbers that can be expressed as products + of small primes, like `2^4 * 3^3 = 1296`. + + Avoid using prime numbers, such as 1297, + or numbers with large prime factors, like `29 * 43 = 1247`. + + __Long FIR filters__ + + When a FIR filter is longer than the chunk size, the convolver uses segmented convolution. + The number of segments is calculated as `filter_length / chunk size`, + and rounded up to the nearest integer. + + Using a smaller chunk size (more segments) can reduce latency + but is less efficient and needs more processing power. + If you have long FIR filters, try different chunk sizes + to find the best balance between latency and processing power. - If you have long FIR filters you can reduce CPU usage by making the chunksize larger. - When increasing, try increasing in factors of two, like 1024 -> 2048 or 4096 -> 8192. + When increasing the chunk size, try doubling it, like going from 1024 to 2048 or 4096 to 8192. * `queuelimit` (optional, defaults to 4) From 24d1516453045a8406c85efa6f14588345511a98 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sun, 22 Sep 2024 22:32:24 +0200 Subject: [PATCH 110/135] Update outdated readme on clock adjust in Blackhole --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b040441..8f4d21f 100644 --- a/README.md +++ b/README.md @@ -988,7 +988,7 @@ A parameter marked (*) in any example is optional. If they are left out from the Setting the rate can be done in two ways. * Some capture devices provide a way to adjust the speed of their virtual sample clock (also called pitch adjust). This is available with the Alsa Loopback and USB Audio gadget devices on Linux, - as well as the latest (currently unreleased) version or BlackHole on macOS. + as well as BlackHole version 0.5.0 and later on macOS. When capturing from any of these devices, the adjustment can be done by tuning the virtual sample clock of the device. This avoids the need for asynchronous resampling. * If asynchronous resampling is enabled, the adjustment can be done by tuning the resampling ratio. From 170fb479a07297ec77ff1f8ad93083b4fc10e186 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Mon, 23 Sep 2024 22:42:06 +0200 Subject: [PATCH 111/135] WIP alsa follow mute --- src/alsadevice.rs | 21 ++++++++++++++++++++- src/alsadevice_utils.rs | 22 ++++++++++++++++++++++ src/audiodevice.rs | 2 ++ src/bin.rs | 4 ++++ src/config.rs | 2 ++ src/lib.rs | 1 + 6 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index f22d7a3..d9b82c1 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -68,6 +68,7 @@ pub struct AlsaCaptureDevice { pub rate_measure_interval: f32, pub stop_on_inactive: bool, pub follow_volume_control: Option, + pub follow_mute_control: Option, } struct CaptureChannels { @@ -747,7 +748,13 @@ fn capture_loop_bytes( "Capture Pitch 1000000", ); - capture_elements.find_elements(h, device, subdevice, ¶ms.follow_volume_control); + capture_elements.find_elements( + h, + device, + subdevice, + ¶ms.follow_volume_control, + ¶ms.follow_mute_control, + ); if let Some(c) = &ctl { if let Some(ref vol_elem) = capture_elements.volume { let vol_db = vol_elem.read_volume_in_db(c); @@ -759,6 +766,16 @@ fn capture_loop_bytes( .unwrap_or_default(); } } + if let Some(ref mute_elem) = capture_elements.mute { + let active = mute_elem.read_as_boolean(c); + info!("Using initial active switch from Alsa: {:?}", active); + if let Some(active_val) = active { + channels + .status + .send(StatusMessage::SetMute(!active_val)) + .unwrap_or_default(); + } + } } } if element_loopback.is_some() || element_uac2_gadget.is_some() { @@ -1160,6 +1177,7 @@ impl CaptureDevice for AlsaCaptureDevice { let rate_measure_interval = self.rate_measure_interval; let stop_on_inactive = self.stop_on_inactive; let follow_volume_control = self.follow_volume_control.clone(); + let follow_mute_control = self.follow_mute_control.clone(); let mut buf_manager = CaptureBufferManager::new( chunksize as Frames, samplerate as f32 / capture_samplerate as f32, @@ -1207,6 +1225,7 @@ impl CaptureDevice for AlsaCaptureDevice { rate_measure_interval, stop_on_inactive, follow_volume_control, + follow_mute_control, }; let cap_channels = CaptureChannels { audio: channel, diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index c21e203..eebe377 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -38,6 +38,7 @@ pub struct CaptureParams { pub rate_measure_interval: f32, pub stop_on_inactive: bool, pub follow_volume_control: Option, + pub follow_mute_control: Option, } pub struct PlaybackParams { @@ -326,6 +327,7 @@ pub struct CaptureElements<'a> { // pub loopback_channels: Option>, pub gadget_rate: Option>, pub volume: Option>, + pub mute: Option>, } pub struct FileDescriptors { @@ -405,6 +407,12 @@ pub fn process_events( .send(StatusMessage::SetVolume(vol)) .unwrap_or_default(); } + EventAction::SetMute(mute) => { + debug!("Alsa mute change event, set mute state to {}", mute); + status_channel + .send(StatusMessage::SetMute(mute)) + .unwrap_or_default(); + } EventAction::None => {} } } @@ -414,6 +422,7 @@ pub fn process_events( pub enum EventAction { None, SetVolume(f32), + SetMute(bool), FormatChange(usize), SourceInactive, } @@ -476,6 +485,15 @@ pub fn get_event_action( } } } + if let Some(eldata) = &elems.mute { + if eldata.numid == numid { + let active = eldata.read_as_boolean(ctl); + debug!("Mixer switch active: {:?}", active); + if let Some(active_val) = active { + return EventAction::SetMute(!active_val); + } + } + } if let Some(eldata) = &elems.gadget_rate { if eldata.numid == numid { let value = eldata.read_as_int(); @@ -503,6 +521,7 @@ impl<'a> CaptureElements<'a> { device: u32, subdevice: u32, volume_name: &Option, + mute_name: &Option, ) { self.loopback_active = find_elem( h, @@ -524,6 +543,9 @@ impl<'a> CaptureElements<'a> { self.volume = volume_name .as_ref() .and_then(|name| find_elem(h, ElemIface::Mixer, None, None, name)); + self.mute = mute_name + .as_ref() + .and_then(|name| find_elem(h, ElemIface::Mixer, None, None, name)); } } diff --git a/src/audiodevice.rs b/src/audiodevice.rs index 5ce01ca..bac0762 100644 --- a/src/audiodevice.rs +++ b/src/audiodevice.rs @@ -556,6 +556,7 @@ pub fn new_capture_device(conf: config::Devices) -> Box { format, stop_on_inactive, ref follow_volume_control, + ref follow_mute_control, .. } => Box::new(alsadevice::AlsaCaptureDevice { devname: device.clone(), @@ -571,6 +572,7 @@ pub fn new_capture_device(conf: config::Devices) -> Box { rate_measure_interval: conf.rate_measure_interval(), stop_on_inactive: stop_on_inactive.unwrap_or_default(), follow_volume_control: follow_volume_control.clone(), + follow_mute_control: follow_mute_control.clone(), }), #[cfg(feature = "pulse-backend")] config::CaptureDevice::Pulse { diff --git a/src/bin.rs b/src/bin.rs index 98dc9b6..2899fcb 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -401,6 +401,10 @@ fn run( debug!("SetVolume message to {} dB received", vol); status_structs.processing.set_target_volume(0, vol); } + StatusMessage::SetMute(mute) => { + debug!("SetMute message to {} received", mute); + status_structs.processing.set_mute(0, mute); + } }, Err(err) => { warn!("Capture, Playback and Processing threads have exited: {}", err); diff --git a/src/config.rs b/src/config.rs index 677ed5c..523a295 100644 --- a/src/config.rs +++ b/src/config.rs @@ -199,6 +199,8 @@ pub enum CaptureDevice { #[serde(default)] follow_volume_control: Option, #[serde(default)] + follow_mute_control: Option, + #[serde(default)] labels: Option>>, }, #[cfg(all(target_os = "linux", feature = "bluez-backend"))] diff --git a/src/lib.rs b/src/lib.rs index f5ee2d5..5ec12c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -156,6 +156,7 @@ pub enum StatusMessage { CaptureDone, SetSpeed(f64), SetVolume(f32), + SetMute(bool), } pub enum CommandMessage { From cb4d31af35842ea16bf69bb4895d7a2a1dbc886c Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Tue, 24 Sep 2024 16:54:32 +0200 Subject: [PATCH 112/135] Fix typos --- src/alsadevice.rs | 2 +- src/alsadevice_utils.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index d9b82c1..f785b5b 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -767,7 +767,7 @@ fn capture_loop_bytes( } } if let Some(ref mute_elem) = capture_elements.mute { - let active = mute_elem.read_as_boolean(c); + let active = mute_elem.read_as_bool(); info!("Using initial active switch from Alsa: {:?}", active); if let Some(active_val) = active { channels diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index eebe377..9dd7c63 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -487,7 +487,7 @@ pub fn get_event_action( } if let Some(eldata) = &elems.mute { if eldata.numid == numid { - let active = eldata.read_as_boolean(ctl); + let active = eldata.read_as_bool(); debug!("Mixer switch active: {:?}", active); if let Some(active_val) = active { return EventAction::SetMute(!active_val); From f3c6f738f9d8eeb9f163999c947946aad5e276c8 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Wed, 25 Sep 2024 20:47:49 +0200 Subject: [PATCH 113/135] WIP update the gadget controls --- src/alsadevice.rs | 25 +++++++++++++++++------- src/alsadevice_utils.rs | 42 +++++++++++++++++++++++++++++++++-------- src/audiodevice.rs | 3 ++- src/bin.rs | 1 + src/filedevice.rs | 3 ++- src/generatordevice.rs | 2 ++ 6 files changed, 59 insertions(+), 17 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index f785b5b..9ed8a1a 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -29,7 +29,7 @@ use crate::alsadevice_buffermanager::{ use crate::alsadevice_utils::{ find_elem, list_channels_as_text, list_device_names, list_formats_as_text, list_samplerates_as_text, pick_preferred_format, process_events, state_desc, CaptureElements, - CaptureParams, CaptureResult, ElemData, FileDescriptors, PlaybackParams, + CaptureParams, CaptureResult, ElemData, FileDescriptors, PlaybackParams, sync_linked_controls }; use crate::helpers::PIRateController; use crate::CommandMessage; @@ -37,7 +37,7 @@ use crate::PrcFmt; use crate::ProcessingState; use crate::Res; use crate::StatusMessage; -use crate::{CaptureStatus, PlaybackStatus}; +use crate::{CaptureStatus, PlaybackStatus, ProcessingParameters}; lazy_static! { static ref ALSA_MUTEX: Mutex<()> = Mutex::new(()); @@ -228,7 +228,8 @@ fn capture_buffer( hctl: &Option, elems: &CaptureElements, status_channel: &crossbeam_channel::Sender, - params: &CaptureParams, + params: &mut CaptureParams, + processing_params: &Arc, ) -> Res { let capture_state = pcmdevice.state_raw(); if capture_state == alsa_sys::SND_PCM_STATE_XRUN as i32 { @@ -271,9 +272,9 @@ fn capture_buffer( return Ok(CaptureResult::Stalled); } if pollresult.ctl { - trace!("Got a control events"); + trace!("Got a control event"); if let Some(c) = ctl { - let event_result = process_events(c, elems, status_channel, params); + let event_result = process_events(c, elems, status_channel, params, processing_params); match event_result { CaptureResult::Done => return Ok(event_result), CaptureResult::Stalled => debug!("Capture device is stalled"), @@ -284,6 +285,7 @@ fn capture_buffer( let ev = h.handle_events().unwrap(); trace!("hctl handle events {}", ev); } + sync_linked_controls(processing_params, params); } if pollresult.pcm { trace!("Capture waited for {:?}", start.map(|s| s.elapsed())); @@ -700,9 +702,10 @@ fn drain_check_eos(audio: &mpsc::Receiver) -> Option fn capture_loop_bytes( channels: CaptureChannels, pcmdevice: &alsa::PCM, - params: CaptureParams, + mut params: CaptureParams, mut resampler: Option>>, buf_manager: &mut CaptureBufferManager, + processing_params: &Arc, ) { let io = pcmdevice.io_bytes(); let pcminfo = pcmdevice.info().unwrap(); @@ -728,6 +731,7 @@ fn capture_loop_bytes( if let Some(c) = &ctl { c.subscribe_events(true).unwrap(); } + if let Some(h) = &hctl { let ctl_fds = h.get().unwrap(); file_descriptors.fds.extend(ctl_fds.iter()); @@ -760,6 +764,7 @@ fn capture_loop_bytes( let vol_db = vol_elem.read_volume_in_db(c); info!("Using initial volume from Alsa: {:?}", vol_db); if let Some(vol) = vol_db { + params.followed_volume_value = Some(vol); channels .status .send(StatusMessage::SetVolume(vol)) @@ -770,6 +775,7 @@ fn capture_loop_bytes( let active = mute_elem.read_as_bool(); info!("Using initial active switch from Alsa: {:?}", active); if let Some(active_val) = active { + params.followed_mute_value = Some(!active_val); channels .status .send(StatusMessage::SetMute(!active_val)) @@ -900,7 +906,8 @@ fn capture_loop_bytes( &hctl, &capture_elements, &channels.status, - ¶ms, + &mut params, + processing_params, ); match capture_res { Ok(CaptureResult::Normal) => { @@ -1161,6 +1168,7 @@ impl CaptureDevice for AlsaCaptureDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, + processing_params: Arc, ) -> Res>> { let devname = self.devname.clone(); let samplerate = self.samplerate; @@ -1226,6 +1234,8 @@ impl CaptureDevice for AlsaCaptureDevice { stop_on_inactive, follow_volume_control, follow_mute_control, + followed_mute_value: None, + followed_volume_value: None, }; let cap_channels = CaptureChannels { audio: channel, @@ -1238,6 +1248,7 @@ impl CaptureDevice for AlsaCaptureDevice { cap_params, resampler, &mut buf_manager, + &processing_params, ); } Err(err) => { diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index 9dd7c63..364e762 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -11,6 +11,8 @@ use parking_lot::RwLock; use std::ffi::CString; use std::sync::Arc; +use crate::ProcessingParameters; + const STANDARD_RATES: [u32; 17] = [ 5512, 8000, 11025, 16000, 22050, 32000, 44100, 48000, 64000, 88200, 96000, 176400, 192000, 352800, 384000, 705600, 768000, @@ -39,6 +41,8 @@ pub struct CaptureParams { pub stop_on_inactive: bool, pub follow_volume_control: Option, pub follow_mute_control: Option, + pub followed_volume_value: Option, + pub followed_mute_value: Option, } pub struct PlaybackParams { @@ -376,7 +380,8 @@ pub fn process_events( ctl: &Ctl, elems: &CaptureElements, status_channel: &crossbeam_channel::Sender, - params: &CaptureParams, + params: &mut CaptureParams, + processing_params: &Arc, ) -> CaptureResult { while let Ok(Some(ev)) = ctl.read() { let nid = ev.get_id().get_numid(); @@ -403,15 +408,19 @@ pub fn process_events( } EventAction::SetVolume(vol) => { debug!("Alsa volume change event, set main fader to {} dB", vol); - status_channel - .send(StatusMessage::SetVolume(vol)) - .unwrap_or_default(); + processing_params.set_target_volume(0, vol); + params.followed_volume_value = Some(vol); + //status_channel + // .send(StatusMessage::SetVolume(vol)) + // .unwrap_or_default(); } EventAction::SetMute(mute) => { debug!("Alsa mute change event, set mute state to {}", mute); - status_channel - .send(StatusMessage::SetMute(mute)) - .unwrap_or_default(); + processing_params.set_mute(0, mute); + params.followed_mute_value = Some(mute); + //status_channel + // .send(StatusMessage::SetMute(mute)) + // .unwrap_or_default(); } EventAction::None => {} } @@ -431,7 +440,7 @@ pub fn get_event_action( numid: u32, elems: &CaptureElements, ctl: &Ctl, - params: &CaptureParams, + params: &mut CaptureParams, ) -> EventAction { if let Some(eldata) = &elems.loopback_active { if eldata.numid == numid { @@ -481,6 +490,7 @@ pub fn get_event_action( let vol_db = eldata.read_volume_in_db(ctl); debug!("Mixer volume control: {:?} dB", vol_db); if let Some(vol) = vol_db { + params.followed_volume_value = Some(vol); return EventAction::SetVolume(vol); } } @@ -490,6 +500,7 @@ pub fn get_event_action( let active = eldata.read_as_bool(); debug!("Mixer switch active: {:?}", active); if let Some(active_val) = active { + params.followed_mute_value = Some(!active_val); return EventAction::SetMute(!active_val); } } @@ -572,3 +583,18 @@ pub fn find_elem<'a>( ElemData { element: e, numid } }) } + +pub fn sync_linked_controls(processing_params: &Arc, capture_params: &mut CaptureParams) { + if let Some(vol) = capture_params.followed_volume_value { + let target_vol = processing_params.target_volume(0); + if (vol - target_vol).abs() > 0.1 { + info!("Updating linked volume control to {} dB", target_vol); + } + } + if let Some(mute) = capture_params.followed_mute_value { + let target_mute = processing_params.is_mute(0); + if mute != target_mute { + info!("Updating linked mute control to {}", target_mute); + } + } +} \ No newline at end of file diff --git a/src/audiodevice.rs b/src/audiodevice.rs index bac0762..a79b5b5 100644 --- a/src/audiodevice.rs +++ b/src/audiodevice.rs @@ -37,7 +37,7 @@ use crate::CommandMessage; use crate::PrcFmt; use crate::Res; use crate::StatusMessage; -use crate::{CaptureStatus, PlaybackStatus}; +use crate::{CaptureStatus, PlaybackStatus, ProcessingParameters}; pub const RATE_CHANGE_THRESHOLD_COUNT: usize = 3; pub const RATE_CHANGE_THRESHOLD_VALUE: f32 = 0.04; @@ -238,6 +238,7 @@ pub trait CaptureDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, + capture_status: Arc, ) -> Res>>; } diff --git a/src/bin.rs b/src/bin.rs index 2899fcb..976d9dc 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -176,6 +176,7 @@ fn run( tx_status_cap, rx_command_cap, status_structs.capture.clone(), + status_structs.processing.clone(), ) .unwrap(); diff --git a/src/filedevice.rs b/src/filedevice.rs index 4c29e9d..fe50cd3 100644 --- a/src/filedevice.rs +++ b/src/filedevice.rs @@ -31,7 +31,7 @@ use crate::PrcFmt; use crate::ProcessingState; use crate::Res; use crate::StatusMessage; -use crate::{CaptureStatus, PlaybackStatus}; +use crate::{CaptureStatus, PlaybackStatus, ProcessingParameters}; pub struct FilePlaybackDevice { pub destination: PlaybackDest, @@ -538,6 +538,7 @@ impl CaptureDevice for FileCaptureDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, + _processing_status: Arc, ) -> Res>> { let source = self.source.clone(); let samplerate = self.samplerate; diff --git a/src/generatordevice.rs b/src/generatordevice.rs index 6a58d28..4ca011a 100644 --- a/src/generatordevice.rs +++ b/src/generatordevice.rs @@ -15,6 +15,7 @@ use crate::CaptureStatus; use crate::CommandMessage; use crate::PrcFmt; use crate::ProcessingState; +use crate::ProcessingParameters; use crate::Res; use crate::StatusMessage; @@ -203,6 +204,7 @@ impl CaptureDevice for GeneratorDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, + _processing_status: Arc, ) -> Res>> { let samplerate = self.samplerate; let chunksize = self.chunksize; From 4387664584e9402ba29d33696cbaa4eac300d4ee Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Wed, 25 Sep 2024 21:07:27 +0200 Subject: [PATCH 114/135] Move ctrl sync to end of loop --- src/alsadevice.rs | 10 ++++++---- src/alsadevice_utils.rs | 7 +++++-- src/generatordevice.rs | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 9ed8a1a..fc32c51 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -28,8 +28,9 @@ use crate::alsadevice_buffermanager::{ }; use crate::alsadevice_utils::{ find_elem, list_channels_as_text, list_device_names, list_formats_as_text, - list_samplerates_as_text, pick_preferred_format, process_events, state_desc, CaptureElements, - CaptureParams, CaptureResult, ElemData, FileDescriptors, PlaybackParams, sync_linked_controls + list_samplerates_as_text, pick_preferred_format, process_events, state_desc, + sync_linked_controls, CaptureElements, CaptureParams, CaptureResult, ElemData, FileDescriptors, + PlaybackParams, }; use crate::helpers::PIRateController; use crate::CommandMessage; @@ -274,7 +275,8 @@ fn capture_buffer( if pollresult.ctl { trace!("Got a control event"); if let Some(c) = ctl { - let event_result = process_events(c, elems, status_channel, params, processing_params); + let event_result = + process_events(c, elems, status_channel, params, processing_params); match event_result { CaptureResult::Done => return Ok(event_result), CaptureResult::Stalled => debug!("Capture device is stalled"), @@ -285,7 +287,6 @@ fn capture_buffer( let ev = h.handle_events().unwrap(); trace!("hctl handle events {}", ev); } - sync_linked_controls(processing_params, params); } if pollresult.pcm { trace!("Capture waited for {:?}", start.map(|s| s.elapsed())); @@ -1036,6 +1037,7 @@ fn capture_loop_bytes( break; } } + sync_linked_controls(processing_params, &mut params); } if let Some(h) = thread_handle { match demote_current_thread_from_real_time(h) { diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index 364e762..7f62e67 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -584,7 +584,10 @@ pub fn find_elem<'a>( }) } -pub fn sync_linked_controls(processing_params: &Arc, capture_params: &mut CaptureParams) { +pub fn sync_linked_controls( + processing_params: &Arc, + capture_params: &mut CaptureParams, +) { if let Some(vol) = capture_params.followed_volume_value { let target_vol = processing_params.target_volume(0); if (vol - target_vol).abs() > 0.1 { @@ -597,4 +600,4 @@ pub fn sync_linked_controls(processing_params: &Arc, captu info!("Updating linked mute control to {}", target_mute); } } -} \ No newline at end of file +} diff --git a/src/generatordevice.rs b/src/generatordevice.rs index 4ca011a..ee13cc0 100644 --- a/src/generatordevice.rs +++ b/src/generatordevice.rs @@ -14,8 +14,8 @@ use rand_distr::{Distribution, Uniform}; use crate::CaptureStatus; use crate::CommandMessage; use crate::PrcFmt; -use crate::ProcessingState; use crate::ProcessingParameters; +use crate::ProcessingState; use crate::Res; use crate::StatusMessage; From 1635212cedd670dff9ec4a3cfd72617cd9c2712f Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Wed, 25 Sep 2024 21:28:07 +0200 Subject: [PATCH 115/135] Write the elements --- src/alsadevice.rs | 2 +- src/alsadevice_utils.rs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index fc32c51..8a41462 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -1037,7 +1037,7 @@ fn capture_loop_bytes( break; } } - sync_linked_controls(processing_params, &mut params); + sync_linked_controls(processing_params, &mut params, &mut capture_elements); } if let Some(h) = thread_handle { match demote_current_thread_from_real_time(h) { diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index 7f62e67..168d95a 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -315,12 +315,30 @@ impl<'a> ElemData<'a> { }) } + pub fn write_volume_in_db(&self, ctl: &Ctl, value: f32) { + let intval = ctl.convert_from_db( + &self.element.get_id().unwrap(), + alsa::mixer::MilliBel::from_db(value), + alsa::Round::Floor, + ); + if let Ok(val) = intval { + self.write_as_int(val as i32); + } + } + pub fn write_as_int(&self, value: i32) { let mut elval = ElemValue::new(ElemType::Integer).unwrap(); if elval.set_integer(0, value).is_some() { self.element.write(&elval).unwrap_or_default(); } } + + pub fn write_as_bool(&self, value: bool) { + let mut elval = ElemValue::new(ElemType::Boolean).unwrap(); + if elval.set_boolean(0, value).is_some() { + self.element.write(&elval).unwrap_or_default(); + } + } } #[derive(Default)] @@ -587,6 +605,7 @@ pub fn find_elem<'a>( pub fn sync_linked_controls( processing_params: &Arc, capture_params: &mut CaptureParams, + elements: &mut CaptureElements, ) { if let Some(vol) = capture_params.followed_volume_value { let target_vol = processing_params.target_volume(0); @@ -598,6 +617,9 @@ pub fn sync_linked_controls( let target_mute = processing_params.is_mute(0); if mute != target_mute { info!("Updating linked mute control to {}", target_mute); + if let Some(vol_elem) = &elements.mute { + vol_elem.write_as_bool(mute); + } } } } From 81a1b1e41b0d5c3c6dc057445e39f9d3bcff40d1 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Wed, 25 Sep 2024 21:36:30 +0200 Subject: [PATCH 116/135] Write the elements --- src/alsadevice.rs | 2 +- src/alsadevice_utils.rs | 28 +++++++++++++++++----------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 8a41462..98a7ca9 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -1037,7 +1037,7 @@ fn capture_loop_bytes( break; } } - sync_linked_controls(processing_params, &mut params, &mut capture_elements); + sync_linked_controls(processing_params, &mut params, &mut capture_elements, &ctl); } if let Some(h) = thread_handle { match demote_current_thread_from_real_time(h) { diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index 168d95a..b77d48f 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -606,19 +606,25 @@ pub fn sync_linked_controls( processing_params: &Arc, capture_params: &mut CaptureParams, elements: &mut CaptureElements, + ctl: &Option, ) { - if let Some(vol) = capture_params.followed_volume_value { - let target_vol = processing_params.target_volume(0); - if (vol - target_vol).abs() > 0.1 { - info!("Updating linked volume control to {} dB", target_vol); + if let Some(c) = ctl { + if let Some(vol) = capture_params.followed_volume_value { + let target_vol = processing_params.target_volume(0); + if (vol - target_vol).abs() > 0.1 { + info!("Updating linked volume control to {} dB", target_vol); + } + if let Some(vol_elem) = &elements.volume { + vol_elem.write_volume_in_db(c, target_vol); + } } - } - if let Some(mute) = capture_params.followed_mute_value { - let target_mute = processing_params.is_mute(0); - if mute != target_mute { - info!("Updating linked mute control to {}", target_mute); - if let Some(vol_elem) = &elements.mute { - vol_elem.write_as_bool(mute); + if let Some(mute) = capture_params.followed_mute_value { + let target_mute = processing_params.is_mute(0); + if mute != target_mute { + info!("Updating linked mute control to {}", target_mute); + if let Some(mute_elem) = &elements.mute { + mute_elem.write_as_bool(target_mute); + } } } } From b16cb2863bf018edbed591be78de79814ce10dfe Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Wed, 25 Sep 2024 21:41:48 +0200 Subject: [PATCH 117/135] Flip alsa mute --- src/alsadevice_utils.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index b77d48f..b04b3d1 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -621,9 +621,9 @@ pub fn sync_linked_controls( if let Some(mute) = capture_params.followed_mute_value { let target_mute = processing_params.is_mute(0); if mute != target_mute { - info!("Updating linked mute control to {}", target_mute); + info!("Updating linked switch control to {}", !target_mute); if let Some(mute_elem) = &elements.mute { - mute_elem.write_as_bool(target_mute); + mute_elem.write_as_bool(!target_mute); } } } From 96898d2c9f024c14ead08fb5aeb36b544f022e93 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Thu, 26 Sep 2024 20:30:10 +0200 Subject: [PATCH 118/135] Add new start arg to all backends --- src/audiodevice.rs | 2 +- src/coreaudiodevice.rs | 2 ++ src/cpaldevice.rs | 2 ++ src/filedevice.rs | 2 +- src/generatordevice.rs | 2 +- src/pulsedevice.rs | 2 ++ src/wasapidevice.rs | 2 ++ 7 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/audiodevice.rs b/src/audiodevice.rs index a79b5b5..8198bb9 100644 --- a/src/audiodevice.rs +++ b/src/audiodevice.rs @@ -238,7 +238,7 @@ pub trait CaptureDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, - capture_status: Arc, + processing_params: Arc, ) -> Res>>; } diff --git a/src/coreaudiodevice.rs b/src/coreaudiodevice.rs index 6e9a6ef..12ee021 100644 --- a/src/coreaudiodevice.rs +++ b/src/coreaudiodevice.rs @@ -38,6 +38,7 @@ use audio_thread_priority::{ use crate::CommandMessage; use crate::PrcFmt; +use crate::ProcessingParameters; use crate::ProcessingState; use crate::Res; use crate::StatusMessage; @@ -652,6 +653,7 @@ impl CaptureDevice for CoreaudioCaptureDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, + _processing_params: Arc, ) -> Res>> { let devname = self.devname.clone(); let samplerate = self.samplerate; diff --git a/src/cpaldevice.rs b/src/cpaldevice.rs index ea4c05c..b86e933 100644 --- a/src/cpaldevice.rs +++ b/src/cpaldevice.rs @@ -22,6 +22,7 @@ use std::time; use crate::CommandMessage; use crate::NewValue; use crate::PrcFmt; +use crate::ProcessingParameters; use crate::ProcessingState; use crate::Res; use crate::StatusMessage; @@ -472,6 +473,7 @@ impl CaptureDevice for CpalCaptureDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, + _processing_params: Arc, ) -> Res>> { let host_cfg = self.host.clone(); let devname = self.devname.clone(); diff --git a/src/filedevice.rs b/src/filedevice.rs index fe50cd3..66c868a 100644 --- a/src/filedevice.rs +++ b/src/filedevice.rs @@ -538,7 +538,7 @@ impl CaptureDevice for FileCaptureDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, - _processing_status: Arc, + _processing_params: Arc, ) -> Res>> { let source = self.source.clone(); let samplerate = self.samplerate; diff --git a/src/generatordevice.rs b/src/generatordevice.rs index ee13cc0..a2b3cd9 100644 --- a/src/generatordevice.rs +++ b/src/generatordevice.rs @@ -204,7 +204,7 @@ impl CaptureDevice for GeneratorDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, - _processing_status: Arc, + _processing_params: Arc, ) -> Res>> { let samplerate = self.samplerate; let chunksize = self.chunksize; diff --git a/src/pulsedevice.rs b/src/pulsedevice.rs index 78c37b4..ab06f80 100644 --- a/src/pulsedevice.rs +++ b/src/pulsedevice.rs @@ -17,6 +17,7 @@ use std::time::{Duration, Instant}; use crate::CommandMessage; use crate::PrcFmt; +use crate::ProcessingParameters; use crate::ProcessingState; use crate::Res; use crate::StatusMessage; @@ -261,6 +262,7 @@ impl CaptureDevice for PulseCaptureDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, + _processing_params: Arc, ) -> Res>> { let devname = self.devname.clone(); let samplerate = self.samplerate; diff --git a/src/wasapidevice.rs b/src/wasapidevice.rs index f0c4c44..c1bc24e 100644 --- a/src/wasapidevice.rs +++ b/src/wasapidevice.rs @@ -23,6 +23,7 @@ use audio_thread_priority::{ use crate::CommandMessage; use crate::PrcFmt; +use crate::ProcessingParameters; use crate::ProcessingState; use crate::Res; use crate::StatusMessage; @@ -933,6 +934,7 @@ impl CaptureDevice for WasapiCaptureDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, + _processing_params: Arc, ) -> Res>> { let exclusive = self.exclusive; let loopback = self.loopback; From 4758efafbb62a876adfdb98d96345fb1423a7209 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Thu, 26 Sep 2024 20:54:05 +0200 Subject: [PATCH 119/135] Rename volume following to linking, update readme --- backend_alsa.md | 31 ++++++++++++++++++++++++------- src/alsadevice.rs | 24 ++++++++++++------------ src/alsadevice_utils.rs | 20 ++++++++++---------- src/audiodevice.rs | 8 ++++---- src/config.rs | 4 ++-- 5 files changed, 52 insertions(+), 35 deletions(-) diff --git a/backend_alsa.md b/backend_alsa.md index e11dc20..ded43ec 100644 --- a/backend_alsa.md +++ b/backend_alsa.md @@ -179,13 +179,18 @@ but are supported by very few devices. Therefore these are checked last. Please also see [Find valid playback and capture parameters](#find-valid-playback-and-capture-parameters). ### Linking volume control to device volume -It is possible to let CamillaDSP follow the a volume control of the capture device. +It is possible to let CamillaDSP link its volume and mute controls to controls on the capture device. This is mostly useful when capturing from the USB Audio Gadget, -which provides a control named `PCM Capture Volume` that is controlled by the USB host. +which provides a volume control named `PCM Capture Volume` +and a mute control called `PCM Capture Switch` that are controlled by the USB host. -This does not alter the signal, and can be used to forward the volume setting from a player to CamillaDSP. -To enable this, set the `follow_volume_control` setting to the name of the volume control. -Any change of the volume then gets applied to the CamillaDSP main volume control. +This volume control does not alter the signal, +and can be used to forward the volume setting from a player to CamillaDSP. +To enable this, set the `link_volume_control` setting to the name of the volume control. +The corresponding setting for the mute control is `link_mute_control`. +Any change of the volume or mute then gets applied to the CamillaDSP main volume control. +The link works in both directions, so that volume and mute changes requested +over the websocket interface also get sent to the USB host. The available controls for a device can be listed with `amixer`. List controls for card 1: @@ -198,9 +203,11 @@ List controls with values and more details: amixer -c 1 contents ``` -The chosen control should be one that does not affect the signal volume, +The chosen volume control should be one that does not affect the signal volume, otherwise the volume gets applied twice. -It must also have a scale in decibel like in this example: +It must also have a scale in decibel, and take a single value (`values=1`). + +Example: ``` numid=15,iface=MIXER,name='Master Playback Volume' ; type=INTEGER,access=rw---R--,values=1,min=0,max=87,step=0 @@ -208,6 +215,16 @@ numid=15,iface=MIXER,name='Master Playback Volume' | dBscale-min=-65.25dB,step=0.75dB,mute=0 ``` +The mute control shoule be a _switch_, meaning that is has states `on` and `off`, +where `on` is not muted and `off` is muted. +It must also take a single value (`values=1`). + +Example: +``` +numid=6,iface=MIXER,name='PCM Capture Switch' + ; type=BOOLEAN,access=rw------,values=1 + : values=on +``` ### Subscribe to Alsa control events The Alsa capture device subscribes to control events from the USB Gadget and Loopback devices. diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 98a7ca9..50c2c09 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -68,8 +68,8 @@ pub struct AlsaCaptureDevice { pub stop_on_rate_change: bool, pub rate_measure_interval: f32, pub stop_on_inactive: bool, - pub follow_volume_control: Option, - pub follow_mute_control: Option, + pub link_volume_control: Option, + pub link_mute_control: Option, } struct CaptureChannels { @@ -757,15 +757,15 @@ fn capture_loop_bytes( h, device, subdevice, - ¶ms.follow_volume_control, - ¶ms.follow_mute_control, + ¶ms.link_volume_control, + ¶ms.link_mute_control, ); if let Some(c) = &ctl { if let Some(ref vol_elem) = capture_elements.volume { let vol_db = vol_elem.read_volume_in_db(c); info!("Using initial volume from Alsa: {:?}", vol_db); if let Some(vol) = vol_db { - params.followed_volume_value = Some(vol); + params.linked_volume_value = Some(vol); channels .status .send(StatusMessage::SetVolume(vol)) @@ -776,7 +776,7 @@ fn capture_loop_bytes( let active = mute_elem.read_as_bool(); info!("Using initial active switch from Alsa: {:?}", active); if let Some(active_val) = active { - params.followed_mute_value = Some(!active_val); + params.linked_mute_value = Some(!active_val); channels .status .send(StatusMessage::SetMute(!active_val)) @@ -1186,8 +1186,8 @@ impl CaptureDevice for AlsaCaptureDevice { let stop_on_rate_change = self.stop_on_rate_change; let rate_measure_interval = self.rate_measure_interval; let stop_on_inactive = self.stop_on_inactive; - let follow_volume_control = self.follow_volume_control.clone(); - let follow_mute_control = self.follow_mute_control.clone(); + let link_volume_control = self.link_volume_control.clone(); + let link_mute_control = self.link_mute_control.clone(); let mut buf_manager = CaptureBufferManager::new( chunksize as Frames, samplerate as f32 / capture_samplerate as f32, @@ -1234,10 +1234,10 @@ impl CaptureDevice for AlsaCaptureDevice { stop_on_rate_change, rate_measure_interval, stop_on_inactive, - follow_volume_control, - follow_mute_control, - followed_mute_value: None, - followed_volume_value: None, + link_volume_control, + link_mute_control, + linked_mute_value: None, + linked_volume_value: None, }; let cap_channels = CaptureChannels { audio: channel, diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index b04b3d1..e5331dc 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -39,10 +39,10 @@ pub struct CaptureParams { pub stop_on_rate_change: bool, pub rate_measure_interval: f32, pub stop_on_inactive: bool, - pub follow_volume_control: Option, - pub follow_mute_control: Option, - pub followed_volume_value: Option, - pub followed_mute_value: Option, + pub link_volume_control: Option, + pub link_mute_control: Option, + pub linked_volume_value: Option, + pub linked_mute_value: Option, } pub struct PlaybackParams { @@ -427,7 +427,7 @@ pub fn process_events( EventAction::SetVolume(vol) => { debug!("Alsa volume change event, set main fader to {} dB", vol); processing_params.set_target_volume(0, vol); - params.followed_volume_value = Some(vol); + params.linked_volume_value = Some(vol); //status_channel // .send(StatusMessage::SetVolume(vol)) // .unwrap_or_default(); @@ -435,7 +435,7 @@ pub fn process_events( EventAction::SetMute(mute) => { debug!("Alsa mute change event, set mute state to {}", mute); processing_params.set_mute(0, mute); - params.followed_mute_value = Some(mute); + params.linked_mute_value = Some(mute); //status_channel // .send(StatusMessage::SetMute(mute)) // .unwrap_or_default(); @@ -508,7 +508,7 @@ pub fn get_event_action( let vol_db = eldata.read_volume_in_db(ctl); debug!("Mixer volume control: {:?} dB", vol_db); if let Some(vol) = vol_db { - params.followed_volume_value = Some(vol); + params.linked_volume_value = Some(vol); return EventAction::SetVolume(vol); } } @@ -518,7 +518,7 @@ pub fn get_event_action( let active = eldata.read_as_bool(); debug!("Mixer switch active: {:?}", active); if let Some(active_val) = active { - params.followed_mute_value = Some(!active_val); + params.linked_mute_value = Some(!active_val); return EventAction::SetMute(!active_val); } } @@ -609,7 +609,7 @@ pub fn sync_linked_controls( ctl: &Option, ) { if let Some(c) = ctl { - if let Some(vol) = capture_params.followed_volume_value { + if let Some(vol) = capture_params.linked_volume_value { let target_vol = processing_params.target_volume(0); if (vol - target_vol).abs() > 0.1 { info!("Updating linked volume control to {} dB", target_vol); @@ -618,7 +618,7 @@ pub fn sync_linked_controls( vol_elem.write_volume_in_db(c, target_vol); } } - if let Some(mute) = capture_params.followed_mute_value { + if let Some(mute) = capture_params.linked_mute_value { let target_mute = processing_params.is_mute(0); if mute != target_mute { info!("Updating linked switch control to {}", !target_mute); diff --git a/src/audiodevice.rs b/src/audiodevice.rs index 8198bb9..da104ca 100644 --- a/src/audiodevice.rs +++ b/src/audiodevice.rs @@ -556,8 +556,8 @@ pub fn new_capture_device(conf: config::Devices) -> Box { ref device, format, stop_on_inactive, - ref follow_volume_control, - ref follow_mute_control, + ref link_volume_control, + ref link_mute_control, .. } => Box::new(alsadevice::AlsaCaptureDevice { devname: device.clone(), @@ -572,8 +572,8 @@ pub fn new_capture_device(conf: config::Devices) -> Box { stop_on_rate_change: conf.stop_on_rate_change(), rate_measure_interval: conf.rate_measure_interval(), stop_on_inactive: stop_on_inactive.unwrap_or_default(), - follow_volume_control: follow_volume_control.clone(), - follow_mute_control: follow_mute_control.clone(), + link_volume_control: link_volume_control.clone(), + link_mute_control: link_mute_control.clone(), }), #[cfg(feature = "pulse-backend")] config::CaptureDevice::Pulse { diff --git a/src/config.rs b/src/config.rs index 523a295..961f363 100644 --- a/src/config.rs +++ b/src/config.rs @@ -197,9 +197,9 @@ pub enum CaptureDevice { #[serde(default)] stop_on_inactive: Option, #[serde(default)] - follow_volume_control: Option, + link_volume_control: Option, #[serde(default)] - follow_mute_control: Option, + link_mute_control: Option, #[serde(default)] labels: Option>>, }, From 385105c6c759ffda741990ccf96539704d2e89a1 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sat, 28 Sep 2024 10:05:15 +0200 Subject: [PATCH 120/135] Change vol sync logs to debug --- src/alsadevice_utils.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index e5331dc..40310cc 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -612,7 +612,7 @@ pub fn sync_linked_controls( if let Some(vol) = capture_params.linked_volume_value { let target_vol = processing_params.target_volume(0); if (vol - target_vol).abs() > 0.1 { - info!("Updating linked volume control to {} dB", target_vol); + debug!("Updating linked volume control to {} dB", target_vol); } if let Some(vol_elem) = &elements.volume { vol_elem.write_volume_in_db(c, target_vol); @@ -621,7 +621,7 @@ pub fn sync_linked_controls( if let Some(mute) = capture_params.linked_mute_value { let target_mute = processing_params.is_mute(0); if mute != target_mute { - info!("Updating linked switch control to {}", !target_mute); + debug!("Updating linked switch control to {}", !target_mute); if let Some(mute_elem) = &elements.mute { mute_elem.write_as_bool(!target_mute); } From d1e4a35fd6bb200cab3904e20da48187971b8723 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sun, 29 Sep 2024 20:47:53 +0200 Subject: [PATCH 121/135] Tweak reame on chunk size --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8f4d21f..bedda59 100644 --- a/README.md +++ b/README.md @@ -938,7 +938,10 @@ A parameter marked (*) in any example is optional. If they are left out from the but since the entire chunk must be captured before processing, it can cause unacceptably long delays. Conversely, using a smaller chunk size can reduce latency - but will increase CPU usage and the risk of buffer underruns. + but will increase CPU usage. + Additionally, the shorter duration of each chunk makes CamillaDSP + more vulnerable to disruptions from other system activities, + potentially causing buffer underruns. __Choosing chunk size for best performance__ @@ -961,8 +964,11 @@ A parameter marked (*) in any example is optional. If they are left out from the The number of segments is calculated as `filter_length / chunk size`, and rounded up to the nearest integer. - Using a smaller chunk size (more segments) can reduce latency - but is less efficient and needs more processing power. + Using a smaller chunk size (i.e. more segments) reduces latency + but makes the convoultion process less efficient and thus needs more processing power. + Although a smaller chunk size leads to increased CPU usage for all filters, + the difference is larger for FIR filters than the other types. + If you have long FIR filters, try different chunk sizes to find the best balance between latency and processing power. From 3c65116ae292d0f2072ef1c7edbff31f5c41ee50 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sun, 13 Oct 2024 22:40:17 +0200 Subject: [PATCH 122/135] Update some outdated example configs --- exampleconfigs/ditherplay.yml | 4 ++-- exampleconfigs/resample_file.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/exampleconfigs/ditherplay.yml b/exampleconfigs/ditherplay.yml index 722c42c..71d7739 100644 --- a/exampleconfigs/ditherplay.yml +++ b/exampleconfigs/ditherplay.yml @@ -27,13 +27,13 @@ filters: dithereven: type: Dither parameters: - type: Uniform + type: Flat bits: 8 amplitude: 1.0 dithersimple: type: Dither parameters: - type: Simple + type: Highpass bits: 8 ditherfancy: type: Dither diff --git a/exampleconfigs/resample_file.yml b/exampleconfigs/resample_file.yml index 55b870d..576dcfd 100644 --- a/exampleconfigs/resample_file.yml +++ b/exampleconfigs/resample_file.yml @@ -7,12 +7,12 @@ devices: profile: Fast capture_samplerate: 44100 playback: - type: RawFile + type: File channels: 2 filename: "result_f64.raw" format: FLOAT64LE capture: - type: File + type: RawFile channels: 2 filename: "sine_120_44100_f64_2ch.raw" format: FLOAT64LE From e42710aebfc22ccb97274f2851557c3f8fc19294 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sat, 19 Oct 2024 22:08:09 +0200 Subject: [PATCH 123/135] Add build with rust 1.75 to keep win7 compatibility --- .github/workflows/ci_test.yml | 16 ++++++++++++++++ .github/workflows/publish.yml | 23 +++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/.github/workflows/ci_test.yml b/.github/workflows/ci_test.yml index 929f0dd..3135dcb 100644 --- a/.github/workflows/ci_test.yml +++ b/.github/workflows/ci_test.yml @@ -142,6 +142,22 @@ jobs: - name: Run cargo test run: cargo test --no-default-features + check_test_windows7: + name: Check and test Windows7 (rustc 1.75) + runs-on: windows-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@1.75.0 + + - name: Run cargo check + run: cargo check --no-default-features + + - name: Run cargo test + run: cargo test --no-default-features + check_test_macos: name: Check and test macOS runs-on: macos-latest diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 27bf859..a94744f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -168,6 +168,29 @@ jobs: asset_name: camilladsp-windows-amd64.zip tag: ${{ github.ref }} + windows7: + name: Windows7 + runs-on: windows-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@1.75.0 + + - name: Build + run: cargo build --release + + - name: Compress + run: powershell Compress-Archive target/release/camilladsp.exe camilladsp.zip + + - name: Upload binaries to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: camilladsp.zip + asset_name: camilladsp-windows7-amd64.zip + tag: ${{ github.ref }} macos: name: macOS runs-on: macos-latest From 2ceaf0de43505405d296738e16fce19db1d815a6 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sat, 26 Oct 2024 17:48:32 +0200 Subject: [PATCH 124/135] Update version in readme --- README.md | 2 +- backend_coreaudio.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bedda59..948a599 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# CamillaDSP v2.0 +# CamillaDSP v3.0 ![CI test and lint](https://github.com/HEnquist/camilladsp/workflows/CI%20test%20and%20lint/badge.svg) A tool to create audio processing pipelines for applications such as active crossovers or room correction. diff --git a/backend_coreaudio.md b/backend_coreaudio.md index 56187f5..2c2c8ce 100644 --- a/backend_coreaudio.md +++ b/backend_coreaudio.md @@ -2,7 +2,8 @@ ## Introduction CoreAudio is the standard audio API of macOS. -The CoreAudio support of CamillaDSP is provided via the [coreaudio-rs library](https://github.com/RustAudio/coreaudio-rs). +The CoreAudio support of CamillaDSP is provided via the +[coreaudio-rs library](https://github.com/RustAudio/coreaudio-rs). CoreAudio is a large API that offers several ways to accomplish most common tasks. CamillaDSP uses the low-level AudioUnits for playback and capture. From 5b496e87180f921e8449367d5e4f91757a1d1a92 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Sat, 2 Nov 2024 21:21:51 +0100 Subject: [PATCH 125/135] Skip updating shared data instead of blocking --- src/alsadevice.rs | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 50c2c09..386ea76 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -589,8 +589,7 @@ fn playback_loop_bytes( if !device_stalled { // updates only for non-stalled device chunk.update_stats(&mut chunk_stats); - { - let mut playback_status = params.playback_status.write(); + if let Some(mut playback_status) = params.playback_status.try_write() { if conversion_result.1 > 0 { playback_status.clipped_samples += conversion_result.1; } @@ -600,6 +599,8 @@ fn playback_loop_bytes( playback_status .signal_peak .add_record(chunk_stats.peak_linear()); + } else { + warn!("playback status blocked, skip update"); } if let Some(delay) = delay_at_chunk_recvd { if delay != 0 { @@ -628,13 +629,16 @@ fn playback_loop_bytes( .unwrap_or(()); } } - let mut playback_status = params.playback_status.write(); - playback_status.buffer_level = avg_delay as usize; - debug!( - "PB: buffer level: {:.1}, signal rms: {:?}", - avg_delay, - playback_status.signal_rms.last_sqrt() - ); + if let Some(mut playback_status) = params.playback_status.try_write() { + playback_status.buffer_level = avg_delay as usize; + debug!( + "PB: buffer level: {:.1}, signal rms: {:?}", + avg_delay, + playback_status.signal_rms.last_sqrt() + ); + } else { + warn!("playback params blocked, skip update 2"); + } } } } @@ -914,8 +918,7 @@ fn capture_loop_bytes( Ok(CaptureResult::Normal) => { xtrace!("Captured {} bytes", capture_bytes); averager.add_value(capture_bytes); - { - let capture_status = params.capture_status.upgradable_read(); + if let Some(capture_status) = params.capture_status.try_upgradable_read() { if averager.larger_than_millis(capture_status.update_interval as u64) { device_stalled = false; let bytes_per_sec = averager.average(); @@ -923,12 +926,19 @@ fn capture_loop_bytes( let measured_rate_f = bytes_per_sec / (params.channels * params.store_bytes_per_sample) as f64; trace!("Measured sample rate is {:.1} Hz", measured_rate_f); - let mut capture_status = RwLockUpgradableReadGuard::upgrade(capture_status); // to write lock - capture_status.measured_samplerate = measured_rate_f as usize; - capture_status.signal_range = value_range as f32; - capture_status.rate_adjust = rate_adjust as f32; - capture_status.state = state; + if let Ok(mut capture_status) = + RwLockUpgradableReadGuard::try_upgrade(capture_status) + { + capture_status.measured_samplerate = measured_rate_f as usize; + capture_status.signal_range = value_range as f32; + capture_status.rate_adjust = rate_adjust as f32; + capture_status.state = state; + } else { + warn!("capture status upgrade blocked, skip update"); + } } + } else { + warn!("capture status blocked, skip update"); } watcher_averager.add_value(capture_bytes); if watcher_averager.larger_than_millis(rate_measure_interval_ms) { @@ -995,14 +1005,15 @@ fn capture_loop_bytes( ¶ms.capture_status.read().used_channels, ); chunk.update_stats(&mut chunk_stats); - { - let mut capture_status = params.capture_status.write(); + if let Some(mut capture_status) = params.capture_status.try_write() { capture_status .signal_rms .add_record_squared(chunk_stats.rms_linear()); capture_status .signal_peak .add_record(chunk_stats.peak_linear()); + } else { + warn!("capture status blocked, skip update 2"); } value_range = chunk.maxval - chunk.minval; trace!("Captured chunk with value range {}", value_range); From c9eb61aeec85c3f845c8fe600f00e5d9b4cddf28 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Mon, 4 Nov 2024 20:53:52 +0100 Subject: [PATCH 126/135] Increase alsa capture timeout --- src/alsadevice.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 386ea76..4241d24 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -255,9 +255,9 @@ fn capture_buffer( let millis_per_chunk = 1000 * frames_to_read / params.samplerate; loop { - let mut timeout_millis = 4 * millis_per_chunk as u32; - if timeout_millis < 10 { - timeout_millis = 10; + let mut timeout_millis = 8 * millis_per_chunk as u32; + if timeout_millis < 20 { + timeout_millis = 20; } let start = if log_enabled!(log::Level::Trace) { Some(Instant::now()) From bfccde2d441c150df81c49d5cdde1ab35fc8cc6b Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Mon, 4 Nov 2024 20:59:19 +0100 Subject: [PATCH 127/135] Change warn to optional trace when shared data is blocked --- src/alsadevice.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 4241d24..90a1e2b 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -600,7 +600,7 @@ fn playback_loop_bytes( .signal_peak .add_record(chunk_stats.peak_linear()); } else { - warn!("playback status blocked, skip update"); + xtrace!("playback status blocked, skip update"); } if let Some(delay) = delay_at_chunk_recvd { if delay != 0 { @@ -637,7 +637,7 @@ fn playback_loop_bytes( playback_status.signal_rms.last_sqrt() ); } else { - warn!("playback params blocked, skip update 2"); + xtrace!("playback params blocked, skip rms update"); } } } @@ -938,7 +938,7 @@ fn capture_loop_bytes( } } } else { - warn!("capture status blocked, skip update"); + xtrace!("capture status blocked, skip update"); } watcher_averager.add_value(capture_bytes); if watcher_averager.larger_than_millis(rate_measure_interval_ms) { @@ -1013,7 +1013,7 @@ fn capture_loop_bytes( .signal_peak .add_record(chunk_stats.peak_linear()); } else { - warn!("capture status blocked, skip update 2"); + xtrace!("capture status blocked, skip rms update"); } value_range = chunk.maxval - chunk.minval; trace!("Captured chunk with value range {}", value_range); From b7736f72e3abf6c28fe613587df9064868c9262b Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Mon, 4 Nov 2024 21:12:11 +0100 Subject: [PATCH 128/135] No blocking also in file device --- src/alsadevice.rs | 2 +- src/filedevice.rs | 30 +++++++++++++++++++----------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 90a1e2b..5660fa6 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -934,7 +934,7 @@ fn capture_loop_bytes( capture_status.rate_adjust = rate_adjust as f32; capture_status.state = state; } else { - warn!("capture status upgrade blocked, skip update"); + xtrace!("capture status upgrade blocked, skip update"); } } } else { diff --git a/src/filedevice.rs b/src/filedevice.rs index 66c868a..fffa0eb 100644 --- a/src/filedevice.rs +++ b/src/filedevice.rs @@ -173,8 +173,7 @@ impl PlaybackDevice for FilePlaybackDevice { } }; chunk.update_stats(&mut chunk_stats); - { - let mut playback_status = playback_status.write(); + if let Some(mut playback_status) = playback_status.try_write() { if nbr_clipped > 0 { playback_status.clipped_samples += nbr_clipped; } @@ -184,6 +183,8 @@ impl PlaybackDevice for FilePlaybackDevice { playback_status .signal_peak .add_record(chunk_stats.peak_linear()); + } else { + xtrace!("playback status blocked, skip rms update"); } } Ok(AudioMessage::Pause) => { @@ -428,20 +429,26 @@ fn capture_loop( nbr_bytes_read += bytes; averager.add_value(bytes); - { - let capture_status = params.capture_status.upgradable_read(); + if let Some(capture_status) = params.capture_status.try_upgradable_read() { if averager.larger_than_millis(capture_status.update_interval as u64) { let bytes_per_sec = averager.average(); averager.restart(); let measured_rate_f = bytes_per_sec / (params.channels * params.store_bytes_per_sample) as f64; trace!("Measured sample rate is {:.1} Hz", measured_rate_f); - let mut capture_status = RwLockUpgradableReadGuard::upgrade(capture_status); // to write lock - capture_status.measured_samplerate = measured_rate_f as usize; - capture_status.signal_range = value_range as f32; - capture_status.rate_adjust = rate_adjust as f32; - capture_status.state = state; + if let Ok(mut capture_status) = + RwLockUpgradableReadGuard::try_upgrade(capture_status) + { + capture_status.measured_samplerate = measured_rate_f as usize; + capture_status.signal_range = value_range as f32; + capture_status.rate_adjust = rate_adjust as f32; + capture_status.state = state; + } else { + xtrace!("capture status upgrade blocked, skip update"); + } } + } else { + xtrace!("capture status blocked, skip update"); } watcher_averager.add_value(bytes); if watcher_averager.larger_than_millis(rate_measure_interval_ms) { @@ -484,14 +491,15 @@ fn capture_loop( ¶ms.capture_status.read().used_channels, ); chunk.update_stats(&mut chunk_stats); - { - let mut capture_status = params.capture_status.write(); + if let Some(mut capture_status) = params.capture_status.try_write() { capture_status .signal_rms .add_record_squared(chunk_stats.rms_linear()); capture_status .signal_peak .add_record(chunk_stats.peak_linear()); + } else { + xtrace!("capture status blocked, skip rms update"); } value_range = chunk.maxval - chunk.minval; state = silence_counter.update(value_range); From ac046373bc2646734568eec3c27a7e995d1457df Mon Sep 17 00:00:00 2001 From: Henrik Date: Mon, 4 Nov 2024 21:24:49 +0100 Subject: [PATCH 129/135] No blocking for shared data updates in coreaudio --- src/coreaudiodevice.rs | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/coreaudiodevice.rs b/src/coreaudiodevice.rs index 12ee021..d04daf0 100644 --- a/src/coreaudiodevice.rs +++ b/src/coreaudiodevice.rs @@ -574,8 +574,7 @@ impl PlaybackDevice for CoreaudioPlaybackDevice { &mut buf, &SampleFormat::FLOAT32LE, ); - { - let mut playback_status = playback_status.write(); + if let Some(mut playback_status) = playback_status.try_write() { if conversion_result.1 > 0 { playback_status.clipped_samples += conversion_result.1; } @@ -586,6 +585,9 @@ impl PlaybackDevice for CoreaudioPlaybackDevice { .signal_peak .add_record(chunk_stats.peak_linear()); } + else { + xtrace!("playback status blocket, skip rms update"); + } match tx_dev.send(PlaybackDeviceMessage::Data(buf)) { Ok(_) => {} Err(err) => { @@ -918,13 +920,15 @@ impl CaptureDevice for CoreaudioCaptureDevice { tries += 1; } if data_queue.len() < (blockalign * capture_frames) { - { - let mut capture_status = capture_status.write(); + if let Some(mut capture_status) = capture_status.try_write() { capture_status.measured_samplerate = 0; capture_status.signal_range = 0.0; capture_status.rate_adjust = 0.0; capture_status.state = ProcessingState::Stalled; } + else { + xtrace!("capture status blocked, skip update"); + } let msg = AudioMessage::Pause; if channel.send(msg).is_err() { info!("Processing thread has already stopped."); @@ -943,8 +947,7 @@ impl CaptureDevice for CoreaudioCaptureDevice { &capture_status.read().used_channels, ); averager.add_value(capture_frames + data_queue.len()/blockalign - prev_len/blockalign); - { - let capture_status = capture_status.upgradable_read(); + if let Some(capture_status) = capture_status.try_upgradable_read() { if averager.larger_than_millis(capture_status.update_interval as u64) { let samples_per_sec = averager.average(); @@ -954,13 +957,20 @@ impl CaptureDevice for CoreaudioCaptureDevice { "Measured sample rate is {:.1} Hz", measured_rate_f ); - let mut capture_status = RwLockUpgradableReadGuard::upgrade(capture_status); // to write lock - capture_status.measured_samplerate = measured_rate_f as usize; - capture_status.signal_range = value_range as f32; - capture_status.rate_adjust = rate_adjust as f32; - capture_status.state = state; + if let Ok(mut capture_status) = RwLockUpgradableReadGuard::try_upgrade(capture_status) { + capture_status.measured_samplerate = measured_rate_f as usize; + capture_status.signal_range = value_range as f32; + capture_status.rate_adjust = rate_adjust as f32; + capture_status.state = state; + } + else { + xtrace!("capture status upgrade blocked, skip update"); + } } } + else { + xtrace!("capture status blocked, skip update"); + } watcher_averager.add_value(capture_frames + data_queue.len()/blockalign - prev_len/blockalign); if watcher_averager.larger_than_millis(rate_measure_interval) { @@ -984,11 +994,13 @@ impl CaptureDevice for CoreaudioCaptureDevice { } prev_len = data_queue.len(); chunk.update_stats(&mut chunk_stats); - { - let mut capture_status = capture_status.write(); + if let Some(mut capture_status) = capture_status.try_write() { capture_status.signal_rms.add_record_squared(chunk_stats.rms_linear()); capture_status.signal_peak.add_record(chunk_stats.peak_linear()); } + else { + xtrace!("capture status blocked, skip rms update"); + } value_range = chunk.maxval - chunk.minval; state = silence_counter.update(value_range); if state == ProcessingState::Running { From 1413597508f968bb4bbb4767f8f94e89a7a982a4 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Mon, 4 Nov 2024 21:33:18 +0100 Subject: [PATCH 130/135] Change log level for unused alsa events --- src/alsadevice_utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index 40310cc..4f8fa39 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -539,7 +539,7 @@ pub fn get_event_action( } } } - debug!("Ignoring event from unknown numid {}", numid); + trace!("Ignoring event from control with numid {}", numid); EventAction::None } From e601ec90cb915a9cf599ca8e687404b165eb92b7 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Tue, 5 Nov 2024 20:50:41 +0100 Subject: [PATCH 131/135] Avoid blocking wasapi threads --- src/wasapidevice.rs | 91 +++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/src/wasapidevice.rs b/src/wasapidevice.rs index c1bc24e..e27387a 100644 --- a/src/wasapidevice.rs +++ b/src/wasapidevice.rs @@ -774,40 +774,41 @@ impl PlaybackDevice for WasapiPlaybackDevice { channels * chunk.frames * sample_format.bytes_per_sample() ]; buffer_avg.add_value(buffer_fill.try_lock().map(|b| b.estimate() as f64).unwrap_or_default()); - { - if adjust && timer.larger_than_millis((1000.0 * adjust_period) as u64) { - if let Some(av_delay) = buffer_avg.average() { - let speed = rate_controller.next(av_delay); - timer.restart(); - buffer_avg.restart(); - debug!( - "Current buffer level {:.1}, set capture rate to {:.4}%", - av_delay, - 100.0 * speed - ); - status_channel - .send(StatusMessage::SetSpeed(speed)) - .unwrap_or(()); - playback_status.write().buffer_level = - av_delay as usize; - } + + if adjust && timer.larger_than_millis((1000.0 * adjust_period) as u64) { + if let Some(av_delay) = buffer_avg.average() { + let speed = rate_controller.next(av_delay); + timer.restart(); + buffer_avg.restart(); + debug!( + "Current buffer level {:.1}, set capture rate to {:.4}%", + av_delay, + 100.0 * speed + ); + status_channel + .send(StatusMessage::SetSpeed(speed)) + .unwrap_or(()); + playback_status.write().buffer_level = + av_delay as usize; } - conversion_result = - chunk_to_buffer_rawbytes(&chunk, &mut buf, &sample_format); - chunk.update_stats(&mut chunk_stats); - { - let mut playback_status = playback_status.write(); - if conversion_result.1 > 0 { - playback_status.clipped_samples += - conversion_result.1; - } - playback_status - .signal_rms - .add_record_squared(chunk_stats.rms_linear()); - playback_status - .signal_peak - .add_record(chunk_stats.peak_linear()); + } + conversion_result = + chunk_to_buffer_rawbytes(&chunk, &mut buf, &sample_format); + chunk.update_stats(&mut chunk_stats); + if let Some(mut playback_status) = playback_status.try_write() { + if conversion_result.1 > 0 { + playback_status.clipped_samples += + conversion_result.1; } + playback_status + .signal_rms + .add_record_squared(chunk_stats.rms_linear()); + playback_status + .signal_peak + .add_record(chunk_stats.peak_linear()); + } + else { + xtrace!("playback status blocked, skip rms update"); } match tx_dev.send(PlaybackDeviceMessage::Data(buf)) { Ok(_) => {} @@ -1148,20 +1149,26 @@ impl CaptureDevice for WasapiCaptureDevice { *element = data_queue.pop_front().unwrap(); } averager.add_value(capture_frames); - { - let capture_status = capture_status.upgradable_read(); + if let Some(capture_status) = capture_status.try_upgradable_read() { if averager.larger_than_millis(capture_status.update_interval as u64) { let samples_per_sec = averager.average(); averager.restart(); let measured_rate_f = samples_per_sec; - let mut capture_status = RwLockUpgradableReadGuard::upgrade(capture_status); // to write lock - capture_status.measured_samplerate = measured_rate_f as usize; - capture_status.signal_range = value_range as f32; - capture_status.rate_adjust = rate_adjust as f32; - capture_status.state = state; + if let Ok(mut capture_status) = RwLockUpgradableReadGuard::try_upgrade(capture_status) { + capture_status.measured_samplerate = measured_rate_f as usize; + capture_status.signal_range = value_range as f32; + capture_status.rate_adjust = rate_adjust as f32; + capture_status.state = state; + } + else { + xtrace!("capture status upgrade blocked, skip update"); + } } } + else { + xtrace!("capture status blocked, skip update"); + } watcher_averager.add_value(capture_frames); if watcher_averager.larger_than_millis(rate_measure_interval) { @@ -1191,11 +1198,13 @@ impl CaptureDevice for WasapiCaptureDevice { &capture_status.read().used_channels, ); chunk.update_stats(&mut chunk_stats); - { - let mut capture_status = capture_status.write(); + if let Some(mut capture_status) = capture_status.try_write() { capture_status.signal_rms.add_record_squared(chunk_stats.rms_linear()); capture_status.signal_peak.add_record(chunk_stats.peak_linear()); } + else { + xtrace!("capture status blocked, skip rms update"); + } value_range = chunk.maxval - chunk.minval; state = silence_counter.update(value_range); if state == ProcessingState::Running { From 1f510c577ccaaf430cdab31c6b8c5ac1e333a272 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sat, 9 Nov 2024 09:58:14 +0100 Subject: [PATCH 132/135] Fix target level limit check --- src/config.rs | 4 ++-- troubleshooting.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config.rs b/src/config.rs index 961f363..ad33403 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1832,8 +1832,8 @@ pub fn validate_config(conf: &mut Configuration, filename: Option<&str>) -> Res< #[cfg(not(target_os = "linux"))] let target_level_limit = 2 * conf.devices.chunksize; - if conf.devices.target_level() >= target_level_limit { - let msg = format!("target_level can't be larger than {}", target_level_limit); + if conf.devices.target_level() > target_level_limit { + let msg = format!("target_level cannot be larger than {}", target_level_limit); return Err(ConfigError::new(&msg).into()); } if let Some(period) = conf.devices.adjust_period { diff --git a/troubleshooting.md b/troubleshooting.md index 9c3a52f..3ea6cbc 100644 --- a/troubleshooting.md +++ b/troubleshooting.md @@ -13,7 +13,7 @@ The config file is invalid Yaml. The error from the Yaml parser is printed in the next line. ### Config options -- target_level can't be larger than *1234*, +- target_level cannot be larger than *1234*, Target level can't be larger than twice the chunksize. From f6d4b942f991f833931c6b8f93704de78a24f0bd Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sun, 10 Nov 2024 21:43:14 +0100 Subject: [PATCH 133/135] Update for new macos runners --- .github/workflows/ci_test.yml | 15 ++++++++------- .github/workflows/publish.yml | 12 +++++------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci_test.yml b/.github/workflows/ci_test.yml index 3135dcb..7672909 100644 --- a/.github/workflows/ci_test.yml +++ b/.github/workflows/ci_test.yml @@ -159,8 +159,8 @@ jobs: run: cargo test --no-default-features check_test_macos: - name: Check and test macOS - runs-on: macos-latest + name: Check and test macOS Intel + runs-on: macos-13 steps: - name: Checkout sources uses: actions/checkout@v4 @@ -175,7 +175,7 @@ jobs: run: cargo test --no-default-features check_macos_arm: - name: Check macOS aarch64 + name: Check and test macOS Arm runs-on: macos-latest steps: - name: Checkout sources @@ -183,11 +183,12 @@ jobs: - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable - with: - targets: aarch64-apple-darwin - - name: Run cargo check for arm - run: cargo check --target aarch64-apple-darwin + - name: Run cargo check + run: cargo check --no-default-features + + - name: Run cargo test + run: cargo test --no-default-features diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a94744f..4d95c89 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -192,8 +192,8 @@ jobs: asset_name: camilladsp-windows7-amd64.zip tag: ${{ github.ref }} macos: - name: macOS - runs-on: macos-latest + name: macOS Intel + runs-on: macos-13 steps: - name: Checkout sources uses: actions/checkout@v4 @@ -217,7 +217,7 @@ jobs: macos_arm: - name: macOS aarch64 + name: macOS Arm runs-on: macos-latest steps: - name: Checkout sources @@ -225,14 +225,12 @@ jobs: - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable - with: - targets: aarch64-apple-darwin - name: Build - run: cargo build --release --target aarch64-apple-darwin + run: cargo build --release - name: Compress - run: tar -zcvf camilladsp.tar.gz -C target/aarch64-apple-darwin/release camilladsp + run: tar -zcvf camilladsp.tar.gz -C target/release camilladsp - name: Upload binaries to release uses: svenstaro/upload-release-action@v2 From edbb4ce1b41701322ebe0f534cb2558111d180c5 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Sun, 5 Jan 2025 00:07:09 +0100 Subject: [PATCH 134/135] Use avail instead of delay to avoid glitches --- src/alsadevice.rs | 13 ++++++------- src/alsadevice_buffermanager.rs | 10 ++++++++++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/alsadevice.rs b/src/alsadevice.rs index 5660fa6..bf30dd1 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -520,14 +520,14 @@ fn playback_loop_bytes( match msg { Ok(AudioMessage::Audio(chunk)) => { // measure delay only on running non-stalled device - let delay_at_chunk_recvd = if !device_stalled + let avail_at_chunk_recvd = if !device_stalled && pcmdevice.state_raw() == alsa_sys::SND_PCM_STATE_RUNNING as i32 { - pcmdevice.status().ok().map(|status| status.get_delay()) + pcmdevice.avail().ok() } else { None }; - //trace!("PB: Delay at chunk rcvd: {:?}", delay_at_chunk_recvd); + //trace!("PB: Avail at chunk rcvd: {:?}", avail_at_chunk_recvd); conversion_result = chunk_to_buffer_rawbytes(&chunk, &mut buffer, ¶ms.sample_format); @@ -602,10 +602,9 @@ fn playback_loop_bytes( } else { xtrace!("playback status blocked, skip update"); } - if let Some(delay) = delay_at_chunk_recvd { - if delay != 0 { - buffer_avg.add_value(delay as f64); - } + if let Some(avail) = avail_at_chunk_recvd { + let delay = buf_manager.current_delay(avail); + buffer_avg.add_value(delay as f64); } if timer.larger_than_millis((1000.0 * params.adjust_period) as u64) { if let Some(avg_delay) = buffer_avg.average() { diff --git a/src/alsadevice_buffermanager.rs b/src/alsadevice_buffermanager.rs index 4534b18..fd61aef 100644 --- a/src/alsadevice_buffermanager.rs +++ b/src/alsadevice_buffermanager.rs @@ -125,6 +125,8 @@ pub trait DeviceBufferManager { // +1 to make sure the device really stalls data.bufsize - data.avail_min + 1 } + + fn current_delay(&self, avail: Frames) -> Frames; } #[derive(Debug)] @@ -182,6 +184,10 @@ impl DeviceBufferManager for CaptureBufferManager { self.data.threshold = threshold; Ok(()) } + + fn current_delay(&self, avail: Frames) -> Frames { + avail + } } #[derive(Debug)] @@ -233,4 +239,8 @@ impl DeviceBufferManager for PlaybackBufferManager { self.data.threshold = threshold; Ok(()) } + + fn current_delay(&self, avail: Frames) -> Frames { + self.data.bufsize - avail + } } From 9fc7517f335375214d72c0187585165b260b4db2 Mon Sep 17 00:00:00 2001 From: Henrik Enquist Date: Sun, 5 Jan 2025 20:19:18 +0100 Subject: [PATCH 135/135] Clippy --- src/alsadevice_utils.rs | 2 +- src/countertimer.rs | 5 ++--- src/dither.rs | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index 4f8fa39..fd3f092 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -292,7 +292,7 @@ pub struct ElemData<'a> { numid: u32, } -impl<'a> ElemData<'a> { +impl ElemData<'_> { pub fn read_as_int(&self) -> Option { self.element .read() diff --git a/src/countertimer.rs b/src/countertimer.rs index 8cb4c2a..07661cc 100644 --- a/src/countertimer.rs +++ b/src/countertimer.rs @@ -4,9 +4,6 @@ use crate::ProcessingState; use std::collections::VecDeque; use std::time::{Duration, Instant}; -/// A counter for watching if the signal has been silent -/// for longer than a given limit. - pub struct DeviceBufferEstimator { update_time: Instant, frames: usize, @@ -38,6 +35,8 @@ impl DeviceBufferEstimator { } } +/// A counter for watching if the signal has been silent +/// for longer than a given limit. pub struct SilenceCounter { silence_threshold: PrcFmt, silence_limit_nbr: usize, diff --git a/src/dither.rs b/src/dither.rs index 4d51344..4a8a28f 100644 --- a/src/dither.rs +++ b/src/dither.rs @@ -551,7 +551,7 @@ impl<'a> Dither<'a> { } } -impl<'a> Filter for Dither<'a> { +impl Filter for Dither<'_> { fn name(&self) -> &str { &self.name }