├── testdata ├── text.txt ├── text_header.txt ├── f32_ex.wav ├── int16.raw ├── int24.raw ├── int243.raw ├── int32.raw ├── int32.wav ├── float32.raw └── float64.raw ├── loudness.png ├── overview.png ├── .gitignore ├── testscripts ├── makerawspike.py ├── test_file_sine.yml ├── config_load_test │ ├── conf1.yml │ ├── conf2.yml │ ├── conf3.yml │ ├── conf4.yml │ └── test_set_config.py ├── makesineraw.py ├── sineraw_stdout.py ├── test_file.yml ├── makefircoeffs.py ├── log_load_and_level.py └── fft_file.py ├── exampleconfigs ├── stdio_inout.yml ├── stdio_capt.yml ├── stdio_pb.yml ├── file_capt.yml ├── file_pb.yml ├── simpleconfig_resample.yml ├── resample_file.yml ├── nomixers.yml ├── tokens.yml ├── nofilters.yml ├── gainconfig.yml ├── brokenconfig.yml ├── simpleconfig.yml ├── ditherplay.yml ├── pulseconfig.yml ├── simpleconfig_plot.yml ├── lf_compressor.yml └── all_biquads.yml ├── Dockerfile_armv8 ├── Dockerfile_armv7 ├── Cross.toml ├── Dockerfile_armv6 ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── ci_test.yml │ └── publish.yml ├── tested_devices.md ├── src ├── filereader.rs ├── filedevice_bluez.rs ├── filereader_nonblock.rs ├── limiter.rs ├── statefile.rs ├── diffeq.rs ├── helpers.rs ├── noisegate.rs ├── processing.rs ├── generatordevice.rs ├── alsadevice_buffermanager.rs ├── compressor.rs └── loudness.rs ├── testing.md ├── coefficients_from_wav.md ├── translate_rew_xml.py ├── benches └── filters.rs ├── Cargo.toml ├── filterfunctions.md ├── troubleshooting.md ├── FAQ.md ├── web └── camilla.html ├── backend_coreaudio.md ├── backend_wasapi.md ├── stepbystep.md ├── filter2.txt ├── filter_44100_2.txt └── backend_alsa.md /testdata/text.txt: -------------------------------------------------------------------------------- 1 | -1.0 2 | -0.5 3 | 0.0 4 | 0.5 5 | 1.0 6 | -------------------------------------------------------------------------------- /loudness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HEnquist/camilladsp/HEAD/loudness.png -------------------------------------------------------------------------------- /overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HEnquist/camilladsp/HEAD/overview.png -------------------------------------------------------------------------------- /testdata/text_header.txt: -------------------------------------------------------------------------------- 1 | some values 2 | -1.0 3 | -0.5 4 | 0.0 5 | 0.5 6 | 1.0 7 | -------------------------------------------------------------------------------- /testdata/f32_ex.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HEnquist/camilladsp/HEAD/testdata/f32_ex.wav -------------------------------------------------------------------------------- /testdata/int16.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HEnquist/camilladsp/HEAD/testdata/int16.raw -------------------------------------------------------------------------------- /testdata/int24.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HEnquist/camilladsp/HEAD/testdata/int24.raw -------------------------------------------------------------------------------- /testdata/int243.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HEnquist/camilladsp/HEAD/testdata/int243.raw -------------------------------------------------------------------------------- /testdata/int32.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HEnquist/camilladsp/HEAD/testdata/int32.raw -------------------------------------------------------------------------------- /testdata/int32.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HEnquist/camilladsp/HEAD/testdata/int32.wav -------------------------------------------------------------------------------- /testdata/float32.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HEnquist/camilladsp/HEAD/testdata/float32.raw -------------------------------------------------------------------------------- /testdata/float64.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HEnquist/camilladsp/HEAD/testdata/float64.raw -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | *.raw 5 | *.wav 6 | default_config.yml 7 | active_config.yml 8 | camilladsp.log 9 | configs/ 10 | coeffs/ 11 | .venv 12 | *.csv 13 | *.exe -------------------------------------------------------------------------------- /testscripts/makerawspike.py: -------------------------------------------------------------------------------- 1 | # Make a simple spike for testing purposes 2 | import numpy as np 3 | 4 | 5 | spike = np.zeros(2**12, dtype="float64") 6 | spike[1024] = 1.0 7 | 8 | spike.tofile("spike_f64.raw") 9 | 10 | 11 | -------------------------------------------------------------------------------- /exampleconfigs/stdio_inout.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 1024 5 | capture: 6 | type: Stdin 7 | channels: 2 8 | format: S16LE 9 | playback: 10 | type: Stdout 11 | channels: 2 12 | format: S16LE 13 | -------------------------------------------------------------------------------- /Dockerfile_armv8: -------------------------------------------------------------------------------- 1 | FROM rustembedded/cross:aarch64-unknown-linux-gnu 2 | 3 | ENV PKG_CONFIG_ALLOW_CROSS 1 4 | ENV PKG_CONFIG_PATH /usr/lib/aarch64-linux-gnu/pkgconfig/ 5 | 6 | RUN dpkg --add-architecture arm64 && \ 7 | apt-get update && \ 8 | apt-get install libasound2-dev:arm64 openssl:arm64 libssl-dev:arm64 -y \ -------------------------------------------------------------------------------- /Dockerfile_armv7: -------------------------------------------------------------------------------- 1 | FROM rustembedded/cross:armv7-unknown-linux-gnueabihf 2 | 3 | ENV PKG_CONFIG_ALLOW_CROSS 1 4 | ENV PKG_CONFIG_PATH /usr/lib/arm-linux-gnueabihf/pkgconfig/ 5 | 6 | RUN dpkg --add-architecture armhf && \ 7 | apt-get update && \ 8 | apt-get install libasound2-dev:armhf openssl:armhf libssl-dev:armhf -y \ -------------------------------------------------------------------------------- /exampleconfigs/stdio_capt.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 1024 5 | silence_threshold: -60 6 | silence_timeout: 3.0 7 | capture: 8 | type: Pulse 9 | channels: 2 10 | device: "MySink.monitor" 11 | format: S16LE 12 | playback: 13 | type: Stdout 14 | channels: 2 15 | format: S16LE 16 | -------------------------------------------------------------------------------- /exampleconfigs/stdio_pb.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 1024 5 | silence_threshold: -60 6 | silence_timeout: 3.0 7 | capture: 8 | type: Stdin 9 | channels: 2 10 | format: S16LE 11 | playback: 12 | type: Pulse 13 | channels: 2 14 | device: "alsa_output.pci-0000_03_00.6.analog-stereo" 15 | format: S32LE 16 | -------------------------------------------------------------------------------- /exampleconfigs/file_capt.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 1024 5 | silence_threshold: -60 6 | silence_timeout: 3.0 7 | capture: 8 | type: Pulse 9 | channels: 2 10 | device: "MySink.monitor" 11 | format: S16LE 12 | playback: 13 | type: File 14 | channels: 2 15 | filename: "/home/henrik/test.raw" 16 | format: S16LE 17 | -------------------------------------------------------------------------------- /exampleconfigs/file_pb.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 1024 5 | silence_threshold: -60 6 | silence_timeout: 3.0 7 | capture: 8 | type: RawFile 9 | channels: 2 10 | filename: "/home/henrik/test.raw" 11 | format: S16LE 12 | playback: 13 | type: Pulse 14 | channels: 2 15 | device: "alsa_output.pci-0000_03_00.6.analog-stereo" 16 | format: S32LE 17 | -------------------------------------------------------------------------------- /exampleconfigs/simpleconfig_resample.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 48000 4 | chunksize: 1024 5 | resampler: 6 | type: AsyncSinc 7 | profile: Balanced 8 | capture_samplerate: 44100 9 | capture: 10 | type: RawFile 11 | filename: "dummy" 12 | channels: 2 13 | format: S16LE 14 | playback: 15 | type: File 16 | filename: "dummy" 17 | channels: 2 18 | format: S16LE 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /exampleconfigs/resample_file.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 96000 4 | chunksize: 1024 5 | resampler: 6 | type: AsyncSinc 7 | profile: Fast 8 | capture_samplerate: 44100 9 | playback: 10 | type: File 11 | channels: 2 12 | filename: "result_f64.raw" 13 | format: FLOAT64LE 14 | capture: 15 | type: RawFile 16 | channels: 2 17 | filename: "sine_120_44100_f64_2ch.raw" 18 | format: FLOAT64LE 19 | extra_samples: 0 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.arm-unknown-linux-gnueabihf] 2 | image = "cross/armv6:v1" 3 | 4 | [target.arm-unknown-linux-gnueabihf.env] 5 | passthrough = [ 6 | "RUSTFLAGS", 7 | "LD_LIBRARY_PATH", 8 | ] 9 | 10 | [target.armv7-unknown-linux-gnueabihf] 11 | image = "cross/armv7:v1" 12 | 13 | [target.armv7-unknown-linux-gnueabihf.env] 14 | passthrough = [ 15 | "RUSTFLAGS", 16 | ] 17 | 18 | [target.aarch64-unknown-linux-gnu] 19 | image = "cross/armv8:v1" 20 | 21 | [target.aarch64-unknown-linux-gnu.env] 22 | passthrough = [ 23 | "RUSTFLAGS", 24 | ] 25 | -------------------------------------------------------------------------------- /exampleconfigs/nomixers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 1024 5 | capture: 6 | type: RawFile 7 | filename: "dummy" 8 | channels: 2 9 | format: S16LE 10 | playback: 11 | type: File 12 | filename: "dummy" 13 | channels: 2 14 | format: S16LE 15 | 16 | filters: 17 | lowpass_fir: 18 | type: Conv 19 | parameters: 20 | type: Raw 21 | filename: filter.txt 22 | 23 | pipeline: 24 | - type: Filter 25 | channels: [0, 1] 26 | names: 27 | - lowpass_fir 28 | 29 | 30 | -------------------------------------------------------------------------------- /testscripts/test_file_sine.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 1024 5 | playback: 6 | type: RawFile 7 | channels: 2 8 | filename: "result_f64.raw" 9 | format: FLOAT64LE 10 | capture: 11 | type: File 12 | channels: 2 13 | filename: "sine_1000_44100_20s_f64_2ch.raw" 14 | format: FLOAT64LE 15 | extra_samples: 0 16 | 17 | filters: 18 | dummy: 19 | type: Conv 20 | parameters: 21 | type: Raw 22 | filename: testscripts/spike_f64_65k.raw 23 | format: FLOAT64LE 24 | 25 | pipeline: 26 | - type: Filter 27 | channels: [0, 1] 28 | names: 29 | - dummy 30 | -------------------------------------------------------------------------------- /testscripts/config_load_test/conf1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 512 5 | enable_rate_adjust: true 6 | capture: 7 | type: CoreAudio 8 | channels: 2 9 | device: "BlackHole 2ch" 10 | playback: 11 | type: CoreAudio 12 | channels: 2 13 | device: "MacBook Air Speakers" 14 | 15 | filters: 16 | testfilter: 17 | type: Biquad 18 | description: "nbr 1" 19 | parameters: 20 | type: Lowpass 21 | freq: 5000 22 | q: 0.7 23 | 24 | pipeline: 25 | - type: Filter 26 | names: 27 | - testfilter 28 | channels: [0] 29 | - type: Filter 30 | names: 31 | - testfilter 32 | channels: [1] -------------------------------------------------------------------------------- /testscripts/config_load_test/conf2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 1024 5 | enable_rate_adjust: true 6 | capture: 7 | type: CoreAudio 8 | channels: 2 9 | device: "BlackHole 2ch" 10 | playback: 11 | type: CoreAudio 12 | channels: 2 13 | device: "MacBook Air Speakers" 14 | 15 | filters: 16 | testfilter: 17 | type: Biquad 18 | description: "nbr 2" 19 | parameters: 20 | type: Lowpass 21 | freq: 500 22 | q: 0.7 23 | 24 | pipeline: 25 | - type: Filter 26 | names: 27 | - testfilter 28 | channels: [0] 29 | - type: Filter 30 | names: 31 | - testfilter 32 | channels: [1] -------------------------------------------------------------------------------- /testscripts/config_load_test/conf3.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 2048 5 | enable_rate_adjust: true 6 | capture: 7 | type: CoreAudio 8 | channels: 2 9 | device: "BlackHole 2ch" 10 | playback: 11 | type: CoreAudio 12 | channels: 2 13 | device: "MacBook Air Speakers" 14 | 15 | filters: 16 | testfilter: 17 | type: Biquad 18 | description: "nbr 3" 19 | parameters: 20 | type: Highpass 21 | freq: 5000 22 | q: 0.7 23 | 24 | pipeline: 25 | - type: Filter 26 | names: 27 | - testfilter 28 | channels: [0] 29 | - type: Filter 30 | names: 31 | - testfilter 32 | channels: [1] -------------------------------------------------------------------------------- /testscripts/config_load_test/conf4.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 4096 5 | enable_rate_adjust: true 6 | capture: 7 | type: CoreAudio 8 | channels: 2 9 | device: "BlackHole 2ch" 10 | playback: 11 | type: CoreAudio 12 | channels: 2 13 | device: "MacBook Air Speakers" 14 | 15 | filters: 16 | testfilter: 17 | type: Biquad 18 | description: "nbr 4" 19 | parameters: 20 | type: Highpass 21 | freq: 100 22 | q: 0.7 23 | 24 | pipeline: 25 | - type: Filter 26 | names: 27 | - testfilter 28 | channels: [0] 29 | - type: Filter 30 | names: 31 | - testfilter 32 | channels: [1] -------------------------------------------------------------------------------- /Dockerfile_armv6: -------------------------------------------------------------------------------- 1 | FROM rustembedded/cross:arm-unknown-linux-gnueabihf 2 | 3 | ENV PKG_CONFIG_ALLOW_CROSS 1 4 | ENV PKG_CONFIG_PATH /usr/lib/arm-linux-gnueabihf/pkgconfig/ 5 | 6 | RUN apt-get update && apt-get install -y --no-install-recommends apt-utils 7 | RUN dpkg --add-architecture armhf 8 | RUN apt-get update && apt-get -y install libasound2-dev:armhf libasound2:armhf 9 | 10 | ENV CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_RUSTFLAGS="-L /usr/lib/arm-linux-gnueabihf $CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_RUSTFLAGS" 11 | ENV CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_RUSTFLAGS="-C link-args=-Wl,-rpath-link,/usr/lib/arm-linux-gnueabihf $CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_RUSTFLAGS" 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **System info** 21 | - operating system 22 | - cpu architecture 23 | - camilladsp version 24 | 25 | **Configuration** 26 | Attach the configuration file that was used when the problem occured. 27 | 28 | **Logs** 29 | run camilladsp with debug logging (-v). If it's short, paste it directly here, otherwise attach it as a separate file. 30 | -------------------------------------------------------------------------------- /testscripts/makesineraw.py: -------------------------------------------------------------------------------- 1 | # Make simple sines for testing purposes 2 | # Example: 20 seconds of 1kHz + 2 kHz at 44.1 kHz 3 | # > python testscripts/makesineraw.py 44100 20 1000 2000 4 | import numpy as np 5 | import sys 6 | f = float(sys.argv[3]) 7 | fs = float(sys.argv[1]) 8 | length = int(sys.argv[2]) 9 | t = np.linspace(0, 20, num=int(20*fs), endpoint=False) 10 | wave = 0.5*np.sin(f*2*np.pi*t) 11 | f_label = "{:.0f}".format(f) 12 | for f2 in sys.argv[4:]: 13 | f2f = float(f2) 14 | wave += 0.5*np.sin(f2f*2*np.pi*t) 15 | f_label = "{}-{:.0f}".format(f_label, f2f) 16 | 17 | wave= np.reshape(wave,(-1,1)) 18 | wave = np.concatenate((wave, wave), axis=1) 19 | 20 | wave64 = wave.astype('float64') 21 | 22 | name = "sine_{}_{:.0f}_{}s_f64_2ch.raw".format(f_label, fs, length) 23 | #print(wave64) 24 | wave64.tofile(name) 25 | 26 | 27 | -------------------------------------------------------------------------------- /exampleconfigs/tokens.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 1024 5 | capture: 6 | type: RawFile 7 | filename: "dummy" 8 | channels: 2 9 | format: S16LE 10 | playback: 11 | type: File 12 | filename: "dummy" 13 | channels: 2 14 | format: S16LE 15 | 16 | filters: 17 | filter44100: 18 | type: Biquad 19 | parameters: 20 | type: Highpass 21 | freq: 80 22 | q: 0.5 23 | filter48000: 24 | type: Biquad 25 | parameters: 26 | type: Highpass 27 | freq: 100 28 | q: 0.5 29 | demofilter: 30 | type: Conv 31 | parameters: 32 | type: Raw 33 | filename: filter_$samplerate$_$channels$.txt 34 | 35 | pipeline: 36 | - type: Filter 37 | channels: [0, 1] 38 | names: 39 | - demofilter 40 | - filter$samplerate$ 41 | 42 | 43 | -------------------------------------------------------------------------------- /testscripts/sineraw_stdout.py: -------------------------------------------------------------------------------- 1 | # Make a simple sine for testing purposes 2 | import numpy as np 3 | import sys 4 | import time 5 | import math 6 | 7 | f1 = 1000 8 | f2 = 1200 9 | fs = 44100 10 | length = 1024 11 | active = True 12 | period = 5 13 | t_end = 0 14 | 15 | while True: 16 | t_start = t_end 17 | t_end = t_start + length/fs 18 | if math.floor(t_start/period)%2 == 0: 19 | t = np.linspace(t_start, t_end, num=length, endpoint=False) 20 | wave1 = 0.5*np.sin(f1*2*np.pi*t) 21 | wave2 = 0.5*np.sin(f2*2*np.pi*t) 22 | wave1 = np.reshape(wave1,(-1,1)) 23 | wave2 = np.reshape(wave2,(-1,1)) 24 | wave = np.concatenate((wave1, wave2), axis=1) 25 | 26 | wave64 = wave.astype('float64') 27 | sys.stdout.buffer.write(wave64.tobytes()) 28 | else: 29 | time.sleep(t_end-t_start) 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /exampleconfigs/nofilters.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 1024 5 | capture: 6 | type: RawFile 7 | filename: "dummy" 8 | channels: 2 9 | format: S16LE 10 | playback: 11 | type: File 12 | filename: "dummy" 13 | channels: 2 14 | format: S16LE 15 | 16 | mixers: 17 | mono: 18 | channels: 19 | in: 2 20 | out: 2 21 | mapping: 22 | - dest: 0 23 | sources: 24 | - channel: 0 25 | gain: -6 26 | inverted: false 27 | - channel: 1 28 | gain: -6 29 | inverted: false 30 | - dest: 1 31 | sources: 32 | - channel: 0 33 | gain: -6 34 | inverted: false 35 | - channel: 1 36 | gain: -6 37 | inverted: false 38 | 39 | pipeline: 40 | - type: Mixer 41 | name: mono 42 | 43 | 44 | -------------------------------------------------------------------------------- /exampleconfigs/gainconfig.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 1024 5 | capture: 6 | type: RawFile 7 | filename: "dummy" 8 | channels: 2 9 | format: S16LE 10 | playback: 11 | type: File 12 | filename: "dummy" 13 | channels: 2 14 | format: S16LE 15 | 16 | filters: 17 | delay1: 18 | type: Delay 19 | parameters: 20 | delay: 500 21 | 22 | mixers: 23 | mono: 24 | channels: 25 | in: 2 26 | out: 2 27 | mapping: 28 | - dest: 0 29 | sources: 30 | - channel: 0 31 | gain: -6 32 | inverted: false 33 | - channel: 1 34 | gain: -6 35 | inverted: false 36 | - dest: 1 37 | sources: 38 | - channel: 0 39 | gain: -6 40 | inverted: false 41 | - channel: 1 42 | gain: -6 43 | inverted: false 44 | 45 | pipeline: 46 | - type: Mixer 47 | name: mono 48 | - type: Filter 49 | channels: [0, 1] 50 | names: 51 | - delay1 52 | 53 | 54 | -------------------------------------------------------------------------------- /exampleconfigs/brokenconfig.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 1024 5 | capture: 6 | type: RawFile 7 | filename: "dummy" 8 | channels: 2 9 | format: S16LE 10 | playback: 11 | type: File 12 | filename: "dummy" 13 | channels: 2 14 | format: S16LE 15 | 16 | filters: 17 | lp1: 18 | type: Biquad 19 | parameters: 20 | type: Highpass 21 | freq: 1000 22 | q: 0.5 23 | 24 | mixers: 25 | mono: 26 | channels: 27 | in: 3 28 | out: 2 29 | mapping: 30 | - dest: 0 31 | sources: 32 | - channel: 0 33 | gain: -6 34 | inverted: false 35 | - channel: 1 36 | gain: -6 37 | inverted: false 38 | - dest: 1 39 | sources: 40 | - channel: 0 41 | gain: -6 42 | inverted: false 43 | - channel: 1 44 | gain: -6 45 | inverted: false 46 | 47 | pipeline: 48 | - type: Mixer 49 | name: mono 50 | - type: Filter 51 | channels: [0, 1] 52 | names: 53 | - lp1 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /exampleconfigs/simpleconfig.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 1024 5 | target_level: 512 6 | enable_rate_adjust: true 7 | capture: 8 | type: Alsa 9 | channels: 2 10 | device: "hw:Loopback,0,1" 11 | format: S16LE 12 | playback: 13 | type: Alsa 14 | channels: 2 15 | device: "hw:Loopback,0,5" 16 | format: S32LE 17 | 18 | filters: 19 | lowpass_fir: 20 | type: Conv 21 | parameters: 22 | type: Raw 23 | filename: filter.txt 24 | 25 | mixers: 26 | monomix: 27 | channels: 28 | in: 2 29 | out: 2 30 | mapping: 31 | - dest: 0 32 | sources: 33 | - channel: 0 34 | gain: -6 35 | inverted: false 36 | - channel: 1 37 | gain: -6 38 | inverted: false 39 | - dest: 1 40 | sources: 41 | - channel: 0 42 | gain: -6 43 | inverted: false 44 | - channel: 1 45 | gain: -6 46 | inverted: false 47 | 48 | pipeline: 49 | - type: Mixer 50 | name: monomix 51 | - type: Filter 52 | channels: [0, 1] 53 | names: 54 | - lowpass_fir 55 | 56 | 57 | -------------------------------------------------------------------------------- /exampleconfigs/ditherplay.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 4096 5 | capture: 6 | type: RawFile 7 | filename: "dummy" 8 | channels: 2 9 | format: S16LE 10 | playback: 11 | type: File 12 | filename: "dummy" 13 | channels: 2 14 | format: S16LE 15 | 16 | filters: 17 | atten: 18 | type: Gain 19 | parameters: 20 | gain: -12 21 | inverted: false 22 | quantize: 23 | type: Dither 24 | parameters: 25 | type: None 26 | bits: 8 27 | dithereven: 28 | type: Dither 29 | parameters: 30 | type: Flat 31 | bits: 8 32 | amplitude: 1.0 33 | dithersimple: 34 | type: Dither 35 | parameters: 36 | type: Highpass 37 | bits: 8 38 | ditherfancy: 39 | type: Dither 40 | parameters: 41 | type: Lipshitz441 42 | bits: 8 43 | ditherfancy2: 44 | type: Dither 45 | parameters: 46 | type: Fweighted441 47 | bits: 8 48 | ditherfancy3: 49 | type: Dither 50 | parameters: 51 | type: Shibata441 52 | bits: 8 53 | 54 | pipeline: 55 | - type: Filter 56 | channels: [0, 1] 57 | names: 58 | - atten 59 | - ditherfancy2 60 | 61 | 62 | -------------------------------------------------------------------------------- /testscripts/test_file.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 1027 5 | target_level: 512 6 | adjust_period: 10 7 | playback: 8 | type: RawFile 9 | channels: 2 10 | filename: "result_i32.raw" 11 | format: S32LE 12 | capture: 13 | type: File 14 | channels: 1 15 | filename: "spike_i32.raw" 16 | format: S32LE 17 | extra_samples: 1 18 | 19 | mixers: 20 | splitter: 21 | channels: 22 | in: 1 23 | out: 2 24 | mapping: 25 | - dest: 0 26 | sources: 27 | - channel: 0 28 | gain: 0 29 | inverted: false 30 | - dest: 1 31 | sources: 32 | - channel: 0 33 | gain: 0 34 | inverted: false 35 | 36 | filters: 37 | testlp: 38 | type: BiquadCombo 39 | parameters: 40 | type: LinkwitzRileyLowpass 41 | freq: 1000 42 | order: 4 43 | testhp: 44 | type: BiquadCombo 45 | parameters: 46 | type: LinkwitzRileyHighpass 47 | freq: 1000 48 | order: 4 49 | 50 | pipeline: 51 | - type: Mixer 52 | name: splitter 53 | - type: Filter 54 | channels: [0] 55 | names: 56 | - testlp 57 | - type: Filter 58 | channels: [1] 59 | names: 60 | - testhp 61 | -------------------------------------------------------------------------------- /testscripts/makefircoeffs.py: -------------------------------------------------------------------------------- 1 | # Make short FIR coeffs in different formats, for testing importing 2 | import numpy as np 3 | 4 | start = np.linspace(0, 1, 16) 5 | mid = np.linspace(1,-1, 32) 6 | end = np.linspace(-1, 0, 16) 7 | impulse = np.concatenate((start,mid,end)) 8 | float64 = np.array(impulse, dtype="float64") 9 | float32 = np.array(impulse, dtype="float32") 10 | int16 = np.array((2**15-1)*impulse, dtype="int16") 11 | int24 = np.array((2**23-1)*impulse, dtype="int32") 12 | int32 = np.array((2**31-1)*impulse, dtype="int32") 13 | 14 | float64.tofile("float64.raw") 15 | float32.tofile("float32.raw") 16 | int16.tofile("int16.raw") 17 | int24.tofile("int24.raw") 18 | int32.tofile("int32.raw") 19 | 20 | 21 | float64 = np.array([-1.0, -0.5, 0.0, 0.5, 1.0], dtype="float64") 22 | float32 = np.array([-1.0, -0.5, 0.0, 0.5, 1.0], dtype="float32") 23 | int16 = np.array([-2**15, -2**14, 0.0, 2**14, 2**15-1], dtype="int16") 24 | int24 = np.array([-2**23, -2**22, 0.0, 2**22, 2**23-1], dtype="int32") 25 | int32 = np.array([-2**31, -2**30, 0.0, 2**30, 2**31-1], dtype="int32") 26 | 27 | float64.tofile("testdata/float64.raw") 28 | float32.tofile("testdata/float32.raw") 29 | int16.tofile("testdata/int16.raw") 30 | int24.tofile("testdata/int24.raw") 31 | int32.tofile("testdata/int32.raw") 32 | -------------------------------------------------------------------------------- /tested_devices.md: -------------------------------------------------------------------------------- 1 | # Tested devices 2 | 3 | The following devices have been reported to work well for multichannel output with CamillaDSP. 4 | 5 | Only devices providing more than 2 channels are listed. 6 | 7 | If a device isn't listed it doesn't mean that it isn't working, just that there is no information yet. The same applied to empty fields in the table. 8 | 9 | | Device | Outputs | Linux | Windows | Macos | Comments | 10 | | ------ | ------- | ----- | ------- | ----- | -------- | 11 | | Okto Research dac8 Pro | 8 | OKa | | | a) Some firmware versions give trouble under Linux | 12 | | Sound Blaster X-Fi Surround 5.1 | 6 | OKa | OK | | a) The Linux driver only supports 48 kHz when using 6 channels | 13 | | Asus Xonar U7 MK2 | 8 | OK | OK| | | 14 | | Asus Xonar U5 | 6 | OK | | | | 15 | | RME Digiface USB | 10a | no | OK | OK | a) Two analog channels intended for headphones, the rest are digital only. 8 digital channels in AES/spdif mode, up to 32 in ADAT mode | 16 | | DIYINHK DXIO32ch USB to I2S interface | 8a | OK | OK | | Digital (I2S) output only. a) 8 channels with standard firmware, up to 32 with alternate firmware using TDM mode | 17 | | Focusrite Scarlett 18i20 gen1 | 20 | OK | | | | 18 | | Focusrite Scarlett 4i4 gen? | 4 | OK | | | | -------------------------------------------------------------------------------- /src/filereader.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::io::ErrorKind; 3 | use std::io::Read; 4 | use std::time::Duration; 5 | 6 | use crate::filedevice::{ReadResult, Reader}; 7 | 8 | pub struct BlockingReader { 9 | inner: R, 10 | } 11 | 12 | impl BlockingReader { 13 | pub fn new(inner: R) -> Self { 14 | BlockingReader { inner } 15 | } 16 | } 17 | 18 | impl Reader for BlockingReader { 19 | fn read(&mut self, data: &mut [u8]) -> Result> { 20 | let requested = data.len(); 21 | let mut buf = &mut *data; 22 | while !buf.is_empty() { 23 | match self.inner.read(buf) { 24 | Ok(0) => break, 25 | Ok(n) => { 26 | let tmp = buf; 27 | buf = &mut tmp[n..]; 28 | } 29 | Err(ref e) if e.kind() == ErrorKind::Interrupted => { 30 | debug!("got Interrupted"); 31 | std::thread::sleep(Duration::from_millis(10)) 32 | } 33 | Err(e) => return Err(Box::new(e)), 34 | } 35 | } 36 | if !buf.is_empty() { 37 | Ok(ReadResult::EndOfFile(requested - buf.len())) 38 | } else { 39 | Ok(ReadResult::Complete(requested)) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | ## Built-in tests 4 | Some of the functionality is covered by tests implemented in Rust. 5 | These tests are run via cargo: 6 | ```sh 7 | cargo test 8 | ``` 9 | 10 | ## Config update tests 11 | A set of tests for testing that changing the running configuration works correctly 12 | is implemeted as a Python test script. 13 | This requires the Python packages `pytest` and `pycamilladsp` to run. 14 | 15 | Some tests trigger a config reload by sending SIGHUP to the camilladsp process. 16 | This is not available on Windows. 17 | It uses the `pgrep` command for getting the PID of the running camilladsp process, 18 | and it assumes that only one camilladsp instance is running. 19 | 20 | To run the tests, prepare four valid config files, named "conf1.yml" to "conf4.yml". 21 | Example files are available in `testscripts/config_load_test`. 22 | Place the new config files in that folder. 23 | 24 | Start camilladsp in wait mode, with the websocket server listening on port 1234: 25 | ```sh 26 | camilladsp -w -v -p1234 27 | ``` 28 | 29 | Now start the tests: 30 | ```sh 31 | cd testscripts/config_load_test 32 | pytest -v 33 | ``` 34 | 35 | A complete run takes a couple of minutes. 36 | 37 | # Benchmarks 38 | 39 | There are benchmarks to monitor the performance of some filters. 40 | These use the `criterion` framework. 41 | Run them with cargo: 42 | ```sh 43 | cargo bench 44 | ``` 45 | -------------------------------------------------------------------------------- /exampleconfigs/pulseconfig.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 1024 5 | silence_threshold: -61 6 | silence_timeout: 3.0 7 | capture: 8 | type: Pulse 9 | channels: 2 10 | device: "MySink.monitor" 11 | format: S16LE 12 | playback: 13 | type: Pulse 14 | channels: 2 15 | device: "alsa_output.pci-0000_03_00.6.analog-stereo" 16 | format: S32LE 17 | 18 | filters: 19 | lp1: 20 | type: Biquad 21 | parameters: 22 | type: Lowpass 23 | freq: 1500 24 | q: 0.5 25 | atten: 26 | type: Gain 27 | parameters: 28 | gain: -3 29 | inverted: false 30 | lowpass_fir: 31 | type: Conv 32 | parameters: 33 | type: Raw 34 | filename: filter2.txt 35 | 36 | mixers: 37 | mono: 38 | channels: 39 | in: 2 40 | out: 2 41 | mapping: 42 | - dest: 0 43 | sources: 44 | - channel: 0 45 | gain: -7 46 | inverted: false 47 | - channel: 1 48 | gain: -6 49 | inverted: false 50 | - dest: 1 51 | sources: 52 | - channel: 0 53 | gain: -6 54 | inverted: false 55 | - channel: 1 56 | gain: -6 57 | inverted: false 58 | 59 | pipeline: 60 | - type: Filter 61 | channels: [0, 1] 62 | names: 63 | - atten 64 | - lowpass_fir 65 | 66 | 67 | -------------------------------------------------------------------------------- /coefficients_from_wav.md: -------------------------------------------------------------------------------- 1 | # Converting a wav file to raw data 2 | 3 | The `File` capture device of CamillaDSP can only read raw data. If you want feed it a wav file, this must first be converted to raw data before it can be used. 4 | 5 | ## Using sox 6 | The conversion can be done on the command line using `sox`. 7 | 8 | Convert a wav file to a 32-bit raw file: 9 | ```sh 10 | sox example.wav --bits 32 example.raw 11 | ``` 12 | 13 | ## Using Audacity 14 | [Audacity](https://www.audacityteam.org/) can export audio data as raw files. 15 | 16 | To convert a wav file, follow these steps: 17 | 1) Open the wav file 18 | 2) [and export it as raw samples](https://manual.audacityteam.org/man/other_uncompressed_files_export_options.html) 19 | - In the Header dropdown, select "RAW (header-less)" 20 | - In the Encoding dropdown, select the format you want to use, for example "Signed 32-bit PCM" for 32 bit singed integers, or "32-bit float" for 32-bit float format. 21 | - Give a suitable filename and click "save". 22 | - If the "Edit Metadata Tags" dialog pops up, just click "Ok". 23 | 24 | ## Checking the result 25 | Audacity can also read raw files, and this can be used to verify that the exported file looks reasonable. 26 | Just use the [File / Import / Raw Data](https://manual.audacityteam.org/man/file_menu_import.html) function. Select the same encoding as when saving, and little-endian byte order. 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /exampleconfigs/simpleconfig_plot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 1024 5 | capture: 6 | type: RawFile 7 | filename: "dummy" 8 | channels: 2 9 | format: S16LE 10 | playback: 11 | type: File 12 | filename: "dummy" 13 | channels: 4 14 | format: S16LE 15 | 16 | filters: 17 | lowpass_fir: 18 | type: Conv 19 | parameters: 20 | type: Raw 21 | filename: filter.txt 22 | highpass_fir: 23 | type: Conv 24 | parameters: 25 | type: Raw 26 | filename: filter.txt 27 | peak1: 28 | type: Biquad 29 | parameters: 30 | type: Peaking 31 | freq: 100 32 | q: 2.0 33 | gain: -20 34 | 35 | mixers: 36 | mono: 37 | channels: 38 | in: 2 39 | out: 4 40 | mapping: 41 | - dest: 0 42 | sources: 43 | - channel: 0 44 | gain: -6 45 | inverted: false 46 | - dest: 1 47 | sources: 48 | - channel: 1 49 | gain: -6 50 | inverted: false 51 | - dest: 2 52 | sources: 53 | - channel: 0 54 | gain: -6 55 | inverted: false 56 | - dest: 3 57 | sources: 58 | - channel: 1 59 | gain: -6 60 | inverted: false 61 | pipeline: 62 | - type: Mixer 63 | name: mono 64 | - type: Filter 65 | channels: [0, 1] 66 | names: 67 | - lowpass_fir 68 | - peak1 69 | - type: Filter 70 | channels: [2, 3] 71 | names: 72 | - highpass_fir 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/filedevice_bluez.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::io::Read; 3 | use std::os::unix::io::{AsRawFd, RawFd}; 4 | use std::sync::Arc; 5 | use zbus::blocking::Connection; 6 | use zbus::zvariant::OwnedFd; 7 | use zbus::Message; 8 | 9 | use crate::filereader_nonblock::NonBlockingReader; 10 | 11 | pub struct WrappedBluezFd { 12 | pipe_fd: zbus::zvariant::OwnedFd, 13 | _ctrl_fd: zbus::zvariant::OwnedFd, 14 | _msg: Arc, 15 | } 16 | 17 | impl WrappedBluezFd { 18 | fn new_from_open_message(r: Arc) -> WrappedBluezFd { 19 | let (pipe_fd, ctrl_fd): (OwnedFd, OwnedFd) = r.body().unwrap(); 20 | WrappedBluezFd { 21 | pipe_fd, 22 | _ctrl_fd: ctrl_fd, 23 | _msg: r, 24 | } 25 | } 26 | } 27 | 28 | impl Read for WrappedBluezFd { 29 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 30 | nix::unistd::read(self.pipe_fd.as_raw_fd(), buf).map_err(io::Error::from) 31 | } 32 | } 33 | 34 | impl AsRawFd for WrappedBluezFd { 35 | fn as_raw_fd(&self) -> RawFd { 36 | self.pipe_fd.as_raw_fd() 37 | } 38 | } 39 | 40 | pub fn open_bluez_dbus_fd<'a>( 41 | service: String, 42 | path: String, 43 | chunksize: usize, 44 | samplerate: usize, 45 | ) -> Result>, zbus::Error> { 46 | let conn1 = Connection::system()?; 47 | let res = conn1.call_method(Some(service), path, Some("org.bluealsa.PCM1"), "Open", &())?; 48 | 49 | let reader = Box::new(NonBlockingReader::new( 50 | WrappedBluezFd::new_from_open_message(res), 51 | 2 * 1000 * chunksize as u64 / samplerate as u64, 52 | )); 53 | Ok(reader) 54 | } 55 | -------------------------------------------------------------------------------- /translate_rew_xml.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | import sys 3 | import yaml 4 | 5 | try: 6 | fname = sys.argv[1] 7 | except Exception: 8 | print("Translate a REW XML equalization file to CamillaDSP filters.", file=sys.stderr) 9 | print("This script creates the 'filters' and 'pipeline' sections of a CamillaDSP config.", file=sys.stderr) 10 | print("Usage:", file=sys.stderr) 11 | print("> python translate_rew_xml.py file_from_rew.xml", file=sys.stderr) 12 | print("Output can also be redirected to a file:", file=sys.stderr) 13 | print("> python translate_rew_xml.py file_from_rew.xml > my_rew_filter.yml", file=sys.stderr) 14 | sys.exit() 15 | 16 | tree = ET.parse(fname) 17 | root = tree.getroot() 18 | 19 | filters = {} 20 | pipeline = [] 21 | 22 | for channel, speaker in enumerate(root): 23 | speakername = speaker.get('location') 24 | print(f"Found speaker: {speakername}", file=sys.stderr) 25 | pipelinestep = {"type": "Filter", "channel": channel, "names": [] } 26 | for filt in speaker: 27 | filt_num = filt.get('number') 28 | filt_enabled = filt.get('enabled') 29 | freq = float(filt.find('frequency').text) 30 | gain = float(filt.find('level').text) 31 | q = float(filt.find('Q').text) 32 | print(f"Found filter: {filt_num}, enabled: {filt_enabled}, f: {freq}, Q: {q}, gain: {gain}", file=sys.stderr) 33 | filter_name = f"{speakername}_{filt_num}" 34 | filtparams = {"type": "Peaking", "freq": freq, "gain": gain, "q": q} 35 | filtdata = {"type": "Biquad", "parameters": filtparams } 36 | filters[filter_name] = filtdata 37 | pipelinestep["names"].append(filter_name) 38 | pipeline.append(pipelinestep) 39 | 40 | print("\nTranslated config, copy-paste into CamillaDSP config file:\n", file=sys.stderr) 41 | config = {"filters": filters, "pipeline": pipeline} 42 | print(yaml.dump(config)) 43 | 44 | -------------------------------------------------------------------------------- /testscripts/log_load_and_level.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import time 3 | from datetime import datetime 4 | from camilladsp import CamillaClient 5 | from matplotlib import pyplot 6 | 7 | cdsp = CamillaClient("localhost", 1234) 8 | cdsp.connect() 9 | 10 | loop_delay = 0.5 11 | plot_interval = 10 12 | 13 | times = [] 14 | loads = [] 15 | levels = [] 16 | 17 | start = time.time() 18 | start_time = datetime.now().strftime("%y.%m.%d_%H.%M.%S") 19 | 20 | pyplot.ion() 21 | fig = pyplot.figure() 22 | ax1 = fig.add_subplot(311) 23 | plot1, = ax1.plot([], []) 24 | ax2 = fig.add_subplot(312) 25 | ax3 = fig.add_subplot(313) 26 | plot3, = ax3.plot([], []) 27 | 28 | 29 | running = True 30 | plot_counter = 0 31 | try: 32 | while running: 33 | now = time.time() 34 | prc_load = cdsp.status.processing_load() 35 | buffer_level = cdsp.status.buffer_level() 36 | times.append(now - start) 37 | loads.append(prc_load) 38 | levels.append(buffer_level) 39 | plot_counter += 1 40 | if plot_counter > plot_interval: 41 | plot_counter = 0 42 | #ax.plot(times, loads) 43 | plot1.set_data(times, loads) 44 | plot3.set_data(times, levels) 45 | ax1.relim() 46 | ax1.autoscale_view(True, True, True) 47 | ax3.relim() 48 | ax3.autoscale_view(True, True, True) 49 | ax2.cla() 50 | ax2.hist(loads) 51 | 52 | # drawing updated values 53 | pyplot.draw() 54 | fig.canvas.draw() 55 | fig.canvas.flush_events() 56 | print(now) 57 | #pyplot.show() 58 | time.sleep(loop_delay) 59 | except KeyboardInterrupt: 60 | print("stopping") 61 | pass 62 | 63 | csv_name = f"loadlog_{start_time}.csv" 64 | with open(csv_name, 'w', newline='') as f: 65 | writer = csv.writer(f) 66 | writer.writerow(["time", "load", "bufferlevel"]) 67 | writer.writerows(zip(times, loads, levels)) 68 | 69 | print(f"saved {len(times)} records to '{csv_name}'") -------------------------------------------------------------------------------- /testscripts/fft_file.py: -------------------------------------------------------------------------------- 1 | # run example: python fft_file.py result_i32.raw S32LE 44100 2 2 | 3 | import numpy as np 4 | import numpy.fft as fft 5 | import csv 6 | import yaml 7 | import sys 8 | from matplotlib import pyplot as plt 9 | from matplotlib.patches import Rectangle 10 | import math 11 | 12 | fname = sys.argv[1] 13 | datafmt = sys.argv[2] 14 | srate = int(sys.argv[3]) 15 | nchannels = int(sys.argv[4]) 16 | try: 17 | window = int(sys.argv[5]) 18 | except: 19 | window = 0 20 | 21 | if datafmt == "text": 22 | with open(fname) as f: 23 | values = [float(row[0]) for row in csv.reader(f)] 24 | elif datafmt == "FLOAT64LE": 25 | values = np.fromfile(fname, dtype=float) 26 | elif datafmt == "FLOAT32LE": 27 | values = np.fromfile(fname, dtype=np.float32) 28 | elif datafmt == "S16LE": 29 | values = np.fromfile(fname, dtype=np.int16)/(2**15-1) 30 | elif datafmt == "S24LE": 31 | values = np.fromfile(fname, dtype=np.int32)/(2**23-1) 32 | elif datafmt == "S32LE": 33 | values = np.fromfile(fname, dtype=np.int32)/(2**31-1) 34 | elif datafmt == "S64LE": 35 | values = np.fromfile(fname, dtype=np.int64)/(2**31-1) 36 | 37 | all_values = np.reshape(values, (nchannels, -1), order='F') 38 | 39 | plt.figure(num="FFT of {}".format(fname)) 40 | for chan in range(nchannels): 41 | chanvals = all_values[chan,:] 42 | npoints = len(chanvals) 43 | if window>0: 44 | #chanvals = chanvals[1024:700000] 45 | npoints = len(chanvals) 46 | for n in range(window): 47 | chanvals = chanvals*np.blackman(npoints) 48 | print(npoints) 49 | t = np.linspace(0, npoints/srate, npoints, endpoint=False) 50 | f = np.linspace(0, srate/2.0, math.floor(npoints/2)) 51 | valfft = fft.fft(chanvals) 52 | cut = valfft[0:math.floor(npoints/2)] 53 | gain = 20*np.log10(np.abs(cut)) 54 | if window: 55 | gain = gain-np.max(gain) 56 | phase = 180/np.pi*np.angle(cut) 57 | plt.subplot(2,1,1) 58 | plt.semilogx(f, gain) 59 | #plt.subplot(3,1,2) 60 | #plt.semilogx(f, phase) 61 | 62 | #plt.gca().set(xlim=(10, srate/2.0)) 63 | plt.subplot(2,1,2) 64 | plt.plot(t, chanvals) 65 | 66 | 67 | plt.show() 68 | 69 | -------------------------------------------------------------------------------- /benches/filters.rs: -------------------------------------------------------------------------------- 1 | extern crate criterion; 2 | use criterion::{criterion_group, criterion_main, Bencher, BenchmarkId, Criterion}; 3 | extern crate camillalib; 4 | 5 | use camillalib::biquad::{Biquad, BiquadCoefficients}; 6 | use camillalib::diffeq::DiffEq; 7 | use camillalib::fftconv::FftConv; 8 | use camillalib::filters::Filter; 9 | use camillalib::PrcFmt; 10 | 11 | /// Bench a single convolution 12 | fn run_conv(b: &mut Bencher, len: usize, chunksize: usize) { 13 | let filter = vec![0.0 as PrcFmt; len]; 14 | let mut conv = FftConv::new("test", chunksize, &filter); 15 | let mut waveform = vec![0.0 as PrcFmt; chunksize]; 16 | 17 | //let mut spectrum = signal.clone(); 18 | b.iter(|| conv.process_waveform(&mut waveform)); 19 | } 20 | 21 | /// Run all convolution benches 22 | fn bench_conv(c: &mut Criterion) { 23 | let mut group = c.benchmark_group("Conv"); 24 | let chunksize = 1024; 25 | for filterlen in [chunksize, 4 * chunksize, 16 * chunksize].iter() { 26 | group.bench_with_input( 27 | BenchmarkId::new("FftConv", filterlen), 28 | filterlen, 29 | |b, filterlen| run_conv(b, *filterlen, chunksize), 30 | ); 31 | } 32 | group.finish(); 33 | } 34 | 35 | /// Bench biquad 36 | fn bench_biquad(c: &mut Criterion) { 37 | let chunksize = 1024; 38 | let coeffs = BiquadCoefficients::new( 39 | -0.1462978543780541, 40 | 0.005350765548905586, 41 | 0.21476322779271284, 42 | 0.4295264555854257, 43 | 0.21476322779271284, 44 | ); 45 | let mut bq = Biquad::new("test", chunksize, coeffs); 46 | let mut waveform = vec![0.0 as PrcFmt; chunksize]; 47 | 48 | c.bench_function("Biquad", |b| b.iter(|| bq.process_waveform(&mut waveform))); 49 | } 50 | 51 | /// Bench diffew 52 | fn bench_diffeq(c: &mut Criterion) { 53 | let chunksize = 1024; 54 | let mut de = DiffEq::new( 55 | "test", 56 | vec![1.0, -0.1462978543780541, 0.005350765548905586], 57 | vec![0.21476322779271284, 0.4295264555854257, 0.21476322779271284], 58 | ); 59 | let mut waveform = vec![0.0 as PrcFmt; chunksize]; 60 | 61 | c.bench_function("DiffEq", |b| b.iter(|| de.process_waveform(&mut waveform))); 62 | } 63 | 64 | criterion_group!(benches, bench_conv, bench_biquad, bench_diffeq); 65 | 66 | criterion_main!(benches); 67 | -------------------------------------------------------------------------------- /exampleconfigs/lf_compressor.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 4096 5 | resampler: null 6 | capture: 7 | type: RawFile 8 | filename: "dummy" 9 | channels: 2 10 | format: S16LE 11 | playback: 12 | type: File 13 | filename: "dummy" 14 | channels: 2 15 | format: S16LE 16 | 17 | mixers: 18 | to_four: 19 | channels: 20 | in: 2 21 | out: 4 22 | mapping: 23 | - dest: 0 24 | sources: 25 | - channel: 0 26 | gain: 0 27 | inverted: false 28 | - dest: 1 29 | sources: 30 | - channel: 1 31 | gain: 0 32 | inverted: false 33 | - dest: 2 34 | sources: 35 | - channel: 0 36 | gain: 0 37 | inverted: false 38 | - dest: 3 39 | sources: 40 | - channel: 1 41 | gain: 0 42 | inverted: false 43 | to_two: 44 | channels: 45 | in: 4 46 | out: 2 47 | mapping: 48 | - dest: 0 49 | sources: 50 | - channel: 0 51 | gain: 0 52 | inverted: false 53 | - channel: 2 54 | gain: 0 55 | inverted: false 56 | - dest: 1 57 | sources: 58 | - channel: 1 59 | gain: 0 60 | inverted: false 61 | - channel: 3 62 | gain: 0 63 | inverted: false 64 | filters: 65 | highpass: 66 | type: BiquadCombo 67 | parameters: 68 | type: LinkwitzRileyHighpass 69 | freq: 80 70 | order: 4 71 | lowpass: 72 | type: BiquadCombo 73 | parameters: 74 | type: LinkwitzRileyLowpass 75 | freq: 80 76 | order: 4 77 | 78 | processors: 79 | democompr: 80 | type: Compressor 81 | parameters: 82 | channels: 4 83 | attack: 0.1 84 | release: 1.0 85 | threshold: -20 86 | factor: 4.0 87 | makeup_gain: 0 88 | soft_clip: true 89 | clip_limit: -6 90 | monitor_channels: [2, 3] 91 | process_channels: [2, 3] 92 | 93 | pipeline: 94 | - type: Mixer 95 | name: to_four 96 | - type: Filter 97 | channels: [0, 1] 98 | names: 99 | - highpass 100 | - type: Filter 101 | channels: [2, 3] 102 | names: 103 | - lowpass 104 | - type: Processor 105 | name: democompr 106 | - type: Mixer 107 | name: to_two 108 | 109 | -------------------------------------------------------------------------------- /src/filereader_nonblock.rs: -------------------------------------------------------------------------------- 1 | use nix; 2 | 3 | use std::error::Error; 4 | use std::io::ErrorKind; 5 | use std::io::Read; 6 | use std::os::unix::io::{AsRawFd, BorrowedFd}; 7 | use std::time; 8 | use std::time::Duration; 9 | 10 | use crate::filedevice::{ReadResult, Reader}; 11 | 12 | pub struct NonBlockingReader<'a, R: 'a> { 13 | poll: nix::poll::PollFd<'a>, 14 | signals: nix::sys::signal::SigSet, 15 | timeout: Option, 16 | timelimit: time::Duration, 17 | inner: R, 18 | } 19 | 20 | impl<'a, R: Read + AsRawFd + 'a> NonBlockingReader<'a, R> { 21 | pub fn new(inner: R, timeout_millis: u64) -> Self { 22 | let flags = nix::poll::PollFlags::POLLIN; 23 | let poll: nix::poll::PollFd<'_> = 24 | nix::poll::PollFd::new(unsafe { BorrowedFd::borrow_raw(inner.as_raw_fd()) }, flags); 25 | let mut signals = nix::sys::signal::SigSet::empty(); 26 | signals.add(nix::sys::signal::Signal::SIGIO); 27 | let timelimit = time::Duration::from_millis(timeout_millis); 28 | let timeout = nix::sys::time::TimeSpec::from_duration(timelimit); 29 | NonBlockingReader { 30 | poll, 31 | signals, 32 | timeout: Some(timeout), 33 | timelimit, 34 | inner, 35 | } 36 | } 37 | } 38 | 39 | impl<'a, R: Read + AsRawFd + 'a> Reader for NonBlockingReader<'a, R> { 40 | fn read(&mut self, data: &mut [u8]) -> Result> { 41 | let mut buf = &mut *data; 42 | let mut bytes_read = 0; 43 | let start = time::Instant::now(); 44 | loop { 45 | let res = nix::poll::ppoll(&mut [self.poll], self.timeout, Some(self.signals))?; 46 | //println!("loop..."); 47 | if res == 0 { 48 | return Ok(ReadResult::Timeout(bytes_read)); 49 | } else { 50 | let n = self.inner.read(buf); 51 | match n { 52 | Ok(0) => return Ok(ReadResult::EndOfFile(bytes_read)), 53 | Ok(n) => { 54 | let tmp = buf; 55 | buf = &mut tmp[n..]; 56 | bytes_read += n; 57 | } 58 | Err(ref e) if e.kind() == ErrorKind::Interrupted => { 59 | debug!("got Interrupted"); 60 | std::thread::sleep(Duration::from_millis(10)) 61 | } 62 | Err(e) => return Err(Box::new(e)), 63 | } 64 | } 65 | if buf.is_empty() { 66 | return Ok(ReadResult::Complete(bytes_read)); 67 | } else if start.elapsed() > self.timelimit { 68 | return Ok(ReadResult::Timeout(bytes_read)); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/limiter.rs: -------------------------------------------------------------------------------- 1 | use crate::config; 2 | use crate::filters::Filter; 3 | use crate::PrcFmt; 4 | use crate::Res; 5 | 6 | const CUBEFACTOR: PrcFmt = 1.0 / 6.75; // = 1 / (2 * 1.5^3) 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct Limiter { 10 | pub name: String, 11 | pub soft_clip: bool, 12 | pub clip_limit: PrcFmt, 13 | } 14 | 15 | impl Limiter { 16 | /// Creates a Compressor from a config struct 17 | pub fn from_config(name: &str, config: config::LimiterParameters) -> Self { 18 | let clip_limit = (10.0 as PrcFmt).powf(config.clip_limit / 20.0); 19 | 20 | debug!( 21 | "Creating limiter '{}', soft_clip: {}, clip_limit dB: {}, linear: {}", 22 | name, 23 | config.soft_clip(), 24 | config.clip_limit, 25 | clip_limit 26 | ); 27 | 28 | Limiter { 29 | name: name.to_string(), 30 | soft_clip: config.soft_clip(), 31 | clip_limit, 32 | } 33 | } 34 | 35 | fn apply_soft_clip(&self, input: &mut [PrcFmt]) { 36 | for val in input.iter_mut() { 37 | let mut scaled = *val / self.clip_limit; 38 | scaled = scaled.clamp(-1.5, 1.5); 39 | scaled -= CUBEFACTOR * scaled.powi(3); 40 | *val = scaled * self.clip_limit; 41 | } 42 | } 43 | 44 | fn apply_hard_clip(&self, input: &mut [PrcFmt]) { 45 | for val in input.iter_mut() { 46 | *val = val.clamp(-self.clip_limit, self.clip_limit); 47 | } 48 | } 49 | 50 | pub fn apply_clip(&self, input: &mut [PrcFmt]) { 51 | if self.soft_clip { 52 | self.apply_soft_clip(input); 53 | } else { 54 | self.apply_hard_clip(input); 55 | } 56 | } 57 | } 58 | 59 | impl Filter for Limiter { 60 | fn name(&self) -> &str { 61 | &self.name 62 | } 63 | 64 | /// Apply a Compressor to an AudioChunk, modifying it in-place. 65 | fn process_waveform(&mut self, waveform: &mut [PrcFmt]) -> Res<()> { 66 | self.apply_clip(waveform); 67 | Ok(()) 68 | } 69 | 70 | fn update_parameters(&mut self, config: config::Filter) { 71 | if let config::Filter::Limiter { 72 | parameters: config, .. 73 | } = config 74 | { 75 | let clip_limit = (10.0 as PrcFmt).powf(config.clip_limit / 20.0); 76 | 77 | self.soft_clip = config.soft_clip(); 78 | self.clip_limit = clip_limit; 79 | debug!( 80 | "Updated limiter '{}', soft_clip: {}, clip_limit dB: {}, linear: {}", 81 | self.name, 82 | config.soft_clip(), 83 | config.clip_limit, 84 | clip_limit 85 | ); 86 | } else { 87 | // This should never happen unless there is a bug somewhere else 88 | panic!("Invalid config change!"); 89 | } 90 | } 91 | } 92 | 93 | /// Validate the limiter config, always return ok to allow any config. 94 | pub fn validate_config(_config: &config::LimiterParameters) -> Res<()> { 95 | Ok(()) 96 | } 97 | -------------------------------------------------------------------------------- /src/statefile.rs: -------------------------------------------------------------------------------- 1 | //use crate::config::Configuration; 2 | use parking_lot::Mutex; 3 | use serde::{Deserialize, Serialize}; 4 | use std::fs::File; 5 | use std::io::BufReader; 6 | use std::io::Read; 7 | use std::sync::atomic::{AtomicBool, Ordering}; 8 | use std::sync::Arc; 9 | 10 | use crate::ProcessingParameters; 11 | 12 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] 13 | #[serde(deny_unknown_fields)] 14 | pub struct State { 15 | pub config_path: Option, 16 | pub mute: [bool; 5], 17 | pub volume: [f32; 5], 18 | } 19 | 20 | pub fn load_state(filename: &str) -> Option { 21 | let file = match File::open(filename) { 22 | Ok(f) => f, 23 | Err(err) => { 24 | warn!("Could not read statefile '{filename}'. Error: {err}"); 25 | return None; 26 | } 27 | }; 28 | let mut buffered_reader = BufReader::new(file); 29 | let mut contents = String::new(); 30 | let _number_of_bytes: usize = match buffered_reader.read_to_string(&mut contents) { 31 | Ok(number_of_bytes) => number_of_bytes, 32 | Err(err) => { 33 | warn!("Could not read statefile '{filename}'. Error: {err}"); 34 | return None; 35 | } 36 | }; 37 | let state: State = match serde_yaml::from_str(&contents) { 38 | Ok(st) => st, 39 | Err(err) => { 40 | warn!("Invalid statefile, ignoring! Error:\n{err}"); 41 | return None; 42 | } 43 | }; 44 | Some(state) 45 | } 46 | 47 | pub fn save_state( 48 | filename: &str, 49 | config_path: &Arc>>, 50 | params: &ProcessingParameters, 51 | unsaved_changes: &Arc, 52 | ) { 53 | let state = State { 54 | config_path: config_path.lock().as_ref().map(|s| s.to_string()), 55 | volume: params.volumes(), 56 | mute: params.mutes(), 57 | }; 58 | if save_state_to_file(filename, &state) { 59 | unsaved_changes.store(false, Ordering::Relaxed); 60 | } 61 | } 62 | 63 | pub fn save_state_to_file(filename: &str, state: &State) -> bool { 64 | debug!("Saving state to {}", filename); 65 | match std::fs::OpenOptions::new() 66 | .write(true) 67 | .create(true) 68 | .truncate(true) 69 | .open(filename) 70 | { 71 | Ok(f) => { 72 | if let Err(writeerr) = serde_yaml::to_writer(&f, &state) { 73 | error!( 74 | "Unable to write to statefile '{}', error: {}", 75 | filename, writeerr 76 | ); 77 | return false; 78 | } 79 | if let Err(syncerr) = &f.sync_all() { 80 | error!( 81 | "Unable to commit statefile '{}' data to disk, error: {}", 82 | filename, syncerr 83 | ); 84 | return false; 85 | } 86 | true 87 | } 88 | Err(openerr) => { 89 | error!("Unable to open statefile {}, error: {}", filename, openerr); 90 | false 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "CamillaDSP" 3 | version = "3.0.1" 4 | authors = ["Henrik Enquist "] 5 | edition = "2021" 6 | description = "A flexible tool for processing audio" 7 | rust-version = "1.74" 8 | 9 | [features] 10 | default = ["websocket"] 11 | pulse-backend = ["libpulse-simple-binding", "libpulse-binding"] 12 | cpal-backend = ["cpal"] 13 | jack-backend = ["cpal-backend", "cpal/jack"] 14 | bluez-backend = ["zbus"] 15 | 32bit = [] 16 | websocket = ["tungstenite"] 17 | secure-websocket = ["websocket", "native-tls", "tungstenite/native-tls"] 18 | debug = [] 19 | avoid-rustc-issue-116359 = [] 20 | 21 | [lib] 22 | name = "camillalib" 23 | path = "src/lib.rs" 24 | 25 | [[bin]] 26 | name = "camilladsp" 27 | path = "src/bin.rs" 28 | 29 | [target.'cfg(target_os="linux")'.dependencies] 30 | alsa = "0.9.0" 31 | alsa-sys = "0.3.1" 32 | nix = { version = "0.28", features = ["poll", "signal"] } 33 | zbus = { version = "3.0.0", optional = true } 34 | 35 | [target.'cfg(target_os="macos")'.dependencies] 36 | #coreaudio-rs = { path = "../coreaudio-rs" } 37 | #coreaudio-rs = { git = "https://github.com/HEnquist/coreaudio-rs", tag="v0.11.1-rc1" } 38 | coreaudio-rs = "0.11.1" 39 | dispatch = "0.2.0" 40 | 41 | [target.'cfg(target_os="windows")'.dependencies] 42 | #wasapi = { path = "../../rust/wasapi" } 43 | #wasapi = { git = "https://github.com/HEnquist/wasapi-rs", branch = "win041" } 44 | wasapi = "0.15.0" 45 | windows = {version = "0.57.0", features = ["Win32_System_Threading", "Win32_Foundation"] } 46 | 47 | [dependencies] 48 | serde = { version = "1.0", features = ["derive"] } 49 | serde_yaml = "0.8" 50 | serde_json = "1.0" 51 | serde_with = "1.11" 52 | realfft = "3.0.0" 53 | #realfft = { git = "https://github.com/HEnquist/realfft", branch = "better_errors" } 54 | num-complex = "0.4" 55 | num-traits = "0.2" 56 | signal-hook = "0.3.8" 57 | rand = { version = "0.8.3", default-features = false, features = ["small_rng", "std"] } 58 | rand_distr = "0.4.3" 59 | clap = { version = "4.5.4", features = ["cargo"] } 60 | lazy_static = "1.4.0" 61 | log = "0.4.14" 62 | flexi_logger = { version = "0.27.2", features = ["async", "colors"] } 63 | chrono = "0.4" 64 | tungstenite = { version = "0.21.0", optional = true } 65 | native-tls = { version = "0.2.7", optional = true } 66 | libpulse-binding = { version = "2.0", optional = true } 67 | libpulse-simple-binding = { version = "2.0", optional = true } 68 | rubato = "0.15.0" 69 | #rubato = { git = "https://github.com/HEnquist/rubato", branch = "optional_fft" } 70 | cpal = { version = "0.13.3", optional = true } 71 | #rawsample = { path = "../../rust/rawsample" } 72 | #rawsample = { git = "https://github.com/HEnquist/rawsample", branch = "main" } 73 | rawsample = "0.2.0" 74 | circular-queue = "0.2.6" 75 | parking_lot = { version = "0.12.1", features = ["hardware-lock-elision"] } 76 | crossbeam-channel = "0.5" 77 | rayon = "1.10.0" 78 | audio_thread_priority = { version = "0.32.0", default-features = false } 79 | 80 | [build-dependencies] 81 | version_check = "0.9" 82 | 83 | [dev-dependencies] 84 | criterion = "0.3" 85 | 86 | [[bench]] 87 | name = "filters" 88 | harness = false 89 | 90 | [profile.release] 91 | codegen-units = 1 92 | -------------------------------------------------------------------------------- /filterfunctions.md: -------------------------------------------------------------------------------- 1 | # Building higher order filters with Biquads 2 | The standard filters in CamillaDSP are not of a specific type, like Butterworth. Instead they are generic, with an adjustable q-value. To make the normal filters, one or several generic filters have to be used together. 3 | 4 | 5 | # Bessel 6 | Making a Bessel filter with a set of Biquads requires creating several Biquads, each with a unique Q and cut-off frequency. 7 | 8 | ## Multiplication factor for frequency: 9 | 10 | | Order | Biquad 1 | Biquad 2 | Biquad 3 | Biquad 4 | 11 | |-----------|-----|----|----|----| 12 | | 1| 1.0* | | | | 13 | | 2| 1.27201964951 | | | | 14 | | 3| 1.32267579991* | 1.44761713315 | | | 15 | | 4| 1.60335751622 | 1.43017155999 | | | 16 | | 5| 1.50231627145* | 1.75537777664 | 1.5563471223 | | 17 | | 6| 1.9047076123 | 1.68916826762 | 1.60391912877 | | 18 | | 7| 1.68436817927* | 2.04949090027 | 1.82241747886 | 1.71635604487 | 19 | | 8| 2.18872623053 | 1.95319575902 | 1.8320926012 | 1.77846591177 | 20 | 21 | The asterisk (*) indicates that this is a 1st order filter. 22 | 23 | 24 | ## Q values: 25 | 26 | | Order | Biquad 1 | Biquad 2 | Biquad 3 | Biquad 4 | 27 | |-------|---------------|----------------|---------------|--------------| 28 | | 1 | (1st order) | | | | 29 | | 2 | 0.57735026919 | | | | 30 | | 3 | (1st order) | 0.691046625825 | | | 31 | | 4 | 0.805538281842| 0.521934581669 | | | 32 | | 5 | (1st order) | 0.916477373948 |0.563535620851 | | 33 | | 6 | 1.02331395383 | 0.611194546878 |0.510317824749 | | 34 | | 7 | (1st order) | 1.12625754198 |0.660821389297 |0.5323556979 | 35 | | 8 | 1.22566942541 | 0.710852074442 |0.559609164796 |0.505991069397| 36 | 37 | ## Example Bessel filter 38 | Let's make a 5th order Lowpass at 1 kHz. Loking at the tables we see that we need three filters. The first should be a 1st order while the second and third are 2nd order. 39 | - First filter, type LowpassFO: 40 | * freq = 1kHz * 1.50231627145 = 1502Hz 41 | * (no q-value) 42 | - Second filter, type Lowpass: 43 | * freq = 1kHz * 1.75537777664 = 1755Hz 44 | * q = 0.916477373948 45 | - Third filter, type Lowpass: 46 | * freq = 1kHz * 1.5563471223 = 1556Hz 47 | * q = 0.563535620851 48 | 49 | # Butterworth and Linkwitz-Riley 50 | For an Nth order Butterworth you will have N/2 biquad 51 | sections if N is even, and ((N+1)/2 if N is odd. 52 | For odd filters one of the Biquads will be a first order filter. 53 | Each filter will have the same resonant frequency f0 and the second order filters will have Q according to this formula: 54 | ``` 55 | Q = 1/( 2*sin((pi/N)*(n + 1/2)) ) 56 | ``` 57 | where `0 <= n < (N-1)/2` 58 | 59 | 60 | ## Table for q-values 61 | Butterworth and Linkwitz-Riley filtes can easily be built with Biquads. The following table lists the most common ones. High- and lowpass use the same parameters. 62 | 63 | | Type | Order | Biquad 1 | Biquad 2 | Biquad 3 | Biquad 4 | 64 | |----------------|-------|----------|----------|----------|----------| 65 | | Butterworth | 2 | 0.71 | | | | 66 | | | 4 | 0.54 | 1.31 | | | 67 | | | 8 | 0.51 | 0.6 | 0.9 | 2.56 | 68 | | Linkwitz-Riley | 2 | 0.5 | | | | 69 | | | 4 | 0.71 | 0.71 | | | 70 | | | 8 | 0.54 | 1.31 | 0.54 | 1.31 | 71 | 72 | Note that a 4th order LR iconsists of two 2nd order Butterworth filters, and that an 8th order LR consists of two 4:th order Butterworth filters. -------------------------------------------------------------------------------- /troubleshooting.md: -------------------------------------------------------------------------------- 1 | ## Description of error messages 2 | ### Config files 3 | - Could not open config file 'examplefile.yml'. Error: *description from OS* 4 | 5 | The specified file could not be opened. The description from the OS may give more info. 6 | 7 | - Could not read config file 'examplefile.yml'. Error: *description from OS* 8 | 9 | The specified file could be opened but not read. The description from the OS may give more info. 10 | 11 | - Invalid config file! *error description from Yaml parser* 12 | 13 | The config file is invalid Yaml. The error from the Yaml parser is printed in the next line. 14 | 15 | ### Config options 16 | - target_level cannot be larger than *1234*, 17 | 18 | Target level can't be larger than twice the chunksize. 19 | 20 | 21 | ### Pipeline 22 | - Use of missing mixer '*mixername*' 23 | 24 | The pipeline lists a mixer named "mixername", but the corresponding definition doesn't exist in the "Mixers" section. 25 | 26 | - Mixer '*mixername*' has wrong number of input channels. Expected *X*, found *Y*. 27 | 28 | This means that there is a mismatch in the number of channels. The number of input channels of a mixer 29 | must match the number of output channels of the previous step in the pipeline. If there is only one mixer, 30 | or this mixer is the first one, then the input channels must match the number of channels of the capture device. 31 | 32 | 33 | 34 | - Pipeline outputs *X* channels, playback device has *Y*. 35 | 36 | This means that there is a mismatch in the number of channels. The number of channels of the playback device 37 | must match the number of output channels of the previous step in the pipeline. If the pipeline doesn't contain any mixer, then the playback device must have the same number of channels as the capture device. If there is one or more mixers, then the output channels of the last mixer must match the number of channels of the playback device. 38 | 39 | - Use of missing filter '*filtername*' 40 | 41 | The pipeline lists a filter named "filtername", but the corresponding definition doesn't exist in the "Filters" section. 42 | 43 | - Use of non existing channel *X* 44 | 45 | A filter step was defined that tries to filter a non-existing channel. 46 | The available channel numbers are 0 to X-1, where X is the active number of channels. If there is no Mixer in front 47 | of this filter, then X is the number of channels of the capture device. If there is a mixer, then X is 48 | the number of output channels of that mixer. 49 | 50 | ### Filters 51 | 52 | - Invalid filter '*filtername*'. Reason: *description from parser* 53 | 54 | The definition of the mixer is somehow wrong. The "Reason" should give more info. 55 | 56 | conv filter: 57 | - Conv coefficients are empty 58 | 59 | The coefficient file for a filter was found to be empty. 60 | 61 | - Could not open coefficient file '*examplefile.raw*'. Reason: *description from OS* 62 | 63 | The specified file could not be opened. The description from the OS may give more info. 64 | 65 | - Can't parse value on line *X* of file '*examplefile.txt*'. Reason: *description from parser* 66 | 67 | The value on the specified line could not be parsed as a number. Check that the file only contains numbers. 68 | 69 | - Unstable filter specified 70 | 71 | This means that a Biquad filter definition was found to give an unstable filter, 72 | meaning that the output signal can grow uncontrolled. Check that the coefficients were entered correctly. 73 | 74 | - Negative delay specified 75 | 76 | The Delay filter can only provide positive delays. 77 | 78 | ### Mixers 79 | 80 | - Invalid mixer '*mixername*'. Reason: *description from parser* 81 | 82 | The definition of the mixer is somehow wrong. The "Reason" should give more info. 83 | 84 | - Invalid destination channel *X*, max is *Y*. 85 | 86 | A mapping was defined that tries to use a non-existing output channel. 87 | The available destination channel numbers are 0 to output channels - 1. 88 | 89 | - Invalid source channel *X*, max is *Y*. 90 | 91 | A mapping was defined that tries to use a non-existing input channel. 92 | The available source channel numbers are 0 to input channels - 1. 93 | 94 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently asked questions 2 | 3 | ## General 4 | 5 | - Who is Camilla? 6 | 7 | Camilla is my daughters middle name. 8 | 9 | ## Config files 10 | 11 | - Why do I get a cryptic error message when my config file looks ok? 12 | 13 | In YAML it is very important that the indentation is correct, otherwise the parser is not able to deduce which properties belong to what level in the tree. 14 | This can result in an error message like this: 15 | ``` 16 | ERRO Invalid config file! 17 | mapping values are not allowed in this context at line 12 column 13, module: camilladsp 18 | ``` 19 | Check the file carefully, to make sure everything is properly indented. Use only spaces, never tabs. 20 | 21 | ## Capture and playback 22 | 23 | - Why do I get buffer underruns sometimes even if everything looks correct? 24 | 25 | If the playback sound card and the capture sound card have independent sample clocks, then they will never be perfectly synchronized. One of them will always run slightly faster than the other. If the playback device is the faster one, then it will sometimes run out of data to play. This will lead to almost periodic dropouts. If instead the capture device is the faster one, then there will instead be a slowly increasing delay between input and output. 26 | 27 | Use the `enable_rate_adjust` option (with the asynchronous resampler if needed) to match the rates of the capture and playback devices. 28 | 29 | - Why do I get only distorted noise when using 24-bit samples? 30 | 31 | There are two 24-bit formats, and it's very important to pick the right one. Both use three bytes to store each sample, but they are packed in different ways. 32 | - S24LE: This format stores each 24-bit sample using 32 bits (4 bytes). The 24-bit data is stored in the lower three bytes, and the highest byte is padding. 33 | 34 | - S24LE3: Here only the three data bytes are stored, without any padding. 35 | 36 | Let's make up three samples and write them as bytes in hex. We use little-endian byte order, hence the first byte is the least significant. 37 | 38 | Sample 1: `0xA1, 0xA2, 0xA3`, 39 | 40 | Sample 2: `0xB1, 0xB2, 0xB3`, 41 | 42 | Sample 3: `0xC1, 0xC2, 0xC3` 43 | 44 | Stored as S24LE: `0xA1, 0xA2, 0xA3, 0x00, 0xB1, 0xB2, 0xB3, 0x00, 0xC1, 0xC2, 0xC3, 0x00` 45 | 46 | Stored as S24LE3: `0xA1, 0xA2, 0xA3, 0xB1, 0xB2, 0xB3, 0xC1, 0xC2, 0xC3` 47 | 48 | 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. 49 | 50 | - Why don't I get any sound on MacOS? 51 | 52 | Apps need to be granted access to the microphone in order to record sound from any source. 53 | Without microphone access, things appear to be running well but only silence is recorded. 54 | See [Microphone access](./backend_coreaudio.md#microphone-access) 55 | 56 | ## Filtering 57 | 58 | - I only have filters with negative gain, why do I get clipping anyway? 59 | 60 | It's not very intuitive, but the peak amplitude can actually increase even though you apply filters that only attenuate. 61 | 62 | The signal is a sum of a large number of frequency components, and in each particular sample some components 63 | will add to increase the amplitude while other decrease it. 64 | If a filter happens to remove a component that lowers the amplitude in a sample, then the value here will go up. 65 | Also all filters affect the phase in a wide range, and this also makes the components sum up to a new waveform that can have higher peaks. 66 | This is mostly a problem with modern productions that are already a bit clipped to begin with, meaning they have many samples at max amplitude. 67 | Try adding a -3 dB Gain filter, that should be enough in most cases. 68 | 69 | - When do I need to use an asynchronous resampler? 70 | 71 | The asynchronous resampler must be used when the ratio between the input and output sample rates cannot be expressed as a fixed ratio. 72 | This is only the case when resampling to adaptively match the rate of two devices with independent clocks, where the ratio drifts a little all the time. 73 | Note that resampling between the fixed rates 44.1 kHz -> 48 kHz corresponds to a ratio of 160/147, and can be handled by the synchronous resampler. 74 | This works for any fixed resampling between the standard rates, 44.1 <-> 96 kHz, 88.2 <-> 192 kHz, 88.1 <-> 48 kHz etc. 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/diffeq.rs: -------------------------------------------------------------------------------- 1 | use crate::config; 2 | use crate::filters::Filter; 3 | 4 | // Sample format 5 | //type SmpFmt = i16; 6 | use crate::PrcFmt; 7 | use crate::Res; 8 | 9 | #[derive(Clone, Debug)] 10 | pub struct DiffEq { 11 | pub x: Vec, 12 | pub y: Vec, 13 | pub a: Vec, 14 | pub a_len: usize, 15 | pub b: Vec, 16 | pub b_len: usize, 17 | pub idx_x: usize, 18 | pub idx_y: usize, 19 | pub name: String, 20 | } 21 | 22 | impl DiffEq { 23 | pub fn new(name: &str, a_in: Vec, b_in: Vec) -> Self { 24 | let name = name.to_string(); 25 | 26 | let a = if a_in.is_empty() { vec![1.0] } else { a_in }; 27 | 28 | let b = if b_in.is_empty() { vec![1.0] } else { b_in }; 29 | 30 | let x = vec![0.0; b.len()]; 31 | let y = vec![0.0; a.len()]; 32 | 33 | let a_len = a.len(); 34 | let b_len = b.len(); 35 | DiffEq { 36 | x, 37 | y, 38 | a, 39 | a_len, 40 | b, 41 | b_len, 42 | idx_x: 0, 43 | idx_y: 0, 44 | name, 45 | } 46 | } 47 | 48 | pub fn from_config(name: &str, conf: config::DiffEqParameters) -> Self { 49 | let a = conf.a(); 50 | let b = conf.b(); 51 | DiffEq::new(name, a, b) 52 | } 53 | 54 | /// Process a single sample 55 | fn process_single(&mut self, input: PrcFmt) -> PrcFmt { 56 | let mut out = 0.0; 57 | self.idx_x = (self.idx_x + 1) % self.b_len; 58 | self.idx_y = (self.idx_y + 1) % self.a_len; 59 | self.x[self.idx_x] = input; 60 | for n in 0..self.b_len { 61 | let n_idx = (self.idx_x + self.b_len - n) % self.b_len; 62 | out += self.b[n] * self.x[n_idx]; 63 | } 64 | for p in 1..self.a_len { 65 | let p_idx = (self.idx_y + self.a_len - p) % self.a_len; 66 | out -= self.a[p] * self.y[p_idx]; 67 | } 68 | self.y[self.idx_y] = out; 69 | out 70 | } 71 | 72 | /// Flush stored subnormal numbers to zero. 73 | fn flush_subnormals(&mut self) { 74 | for (n, x) in self.x.iter_mut().enumerate() { 75 | if x.is_subnormal() { 76 | trace!( 77 | "DiffEq filter '{}', flushing subnormal x at index {}", 78 | self.name, 79 | n 80 | ); 81 | *x = 0.0; 82 | } 83 | } 84 | for (n, y) in self.y.iter_mut().enumerate() { 85 | if y.is_subnormal() { 86 | trace!( 87 | "DiffEq filter '{}', flushing subnormal y at index {}", 88 | self.name, 89 | n 90 | ); 91 | *y = 0.0; 92 | } 93 | } 94 | } 95 | } 96 | 97 | impl Filter for DiffEq { 98 | fn name(&self) -> &str { 99 | &self.name 100 | } 101 | 102 | fn process_waveform(&mut self, waveform: &mut [PrcFmt]) -> Res<()> { 103 | for item in waveform.iter_mut() { 104 | *item = self.process_single(*item); 105 | } 106 | self.flush_subnormals(); 107 | Ok(()) 108 | } 109 | 110 | fn update_parameters(&mut self, conf: config::Filter) { 111 | if let config::Filter::DiffEq { 112 | parameters: conf, .. 113 | } = conf 114 | { 115 | *self = DiffEq::from_config(&self.name, conf); 116 | } else { 117 | // This should never happen unless there is a bug somewhere else 118 | unreachable!("Invalid config change!"); 119 | } 120 | } 121 | } 122 | 123 | pub fn validate_config(_parameters: &config::DiffEqParameters) -> Res<()> { 124 | // TODO add check for stability 125 | Ok(()) 126 | } 127 | 128 | #[cfg(test)] 129 | mod tests { 130 | use crate::diffeq::DiffEq; 131 | use crate::filters::Filter; 132 | use crate::PrcFmt; 133 | 134 | fn is_close(left: PrcFmt, right: PrcFmt, maxdiff: PrcFmt) -> bool { 135 | println!("{left} - {right}"); 136 | (left - right).abs() < maxdiff 137 | } 138 | 139 | fn compare_waveforms(left: Vec, right: Vec, maxdiff: PrcFmt) -> bool { 140 | for (val_l, val_r) in left.iter().zip(right.iter()) { 141 | if !is_close(*val_l, *val_r, maxdiff) { 142 | return false; 143 | } 144 | } 145 | true 146 | } 147 | 148 | #[test] 149 | fn check_result() { 150 | let mut filter = DiffEq::new( 151 | "test", 152 | vec![1.0, -0.1462978543780541, 0.005350765548905586], 153 | vec![0.21476322779271284, 0.4295264555854257, 0.21476322779271284], 154 | ); 155 | let mut wave = vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; 156 | let expected = vec![0.215, 0.461, 0.281, 0.039, 0.004, 0.0, 0.0, 0.0]; 157 | filter.process_waveform(&mut wave).unwrap(); 158 | assert!(compare_waveforms(wave, expected, 1e-3)); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /exampleconfigs/all_biquads.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | samplerate: 44100 4 | chunksize: 1024 5 | capture: 6 | type: RawFile 7 | filename: "dummy" 8 | channels: 2 9 | format: S16LE 10 | playback: 11 | type: File 12 | filename: "dummy" 13 | channels: 2 14 | format: S16LE 15 | 16 | filters: 17 | 18 | # Free 19 | 20 | free: 21 | type: Biquad 22 | parameters: 23 | type: Free 24 | a1: -1.9114181152906082 25 | a2: 0.9155193893411311 26 | b0: 1.0051540967655406 27 | b1: -1.9114181152906082 28 | b2: 0.9103652925755905 29 | 30 | # Highpass / Lowpass 2nd order 31 | 32 | highpass: 33 | type: Biquad 34 | parameters: 35 | type: Highpass 36 | freq: 1000 37 | q: 1.0 38 | 39 | lowpass: 40 | type: Biquad 41 | parameters: 42 | type: Lowpass 43 | freq: 1000 44 | q: 1.0 45 | 46 | # Highpass / Lowpass 1st order 47 | 48 | highpass_first_order: 49 | type: Biquad 50 | parameters: 51 | type: HighpassFO 52 | freq: 1000 53 | 54 | lowpass_first_order: 55 | type: Biquad 56 | parameters: 57 | type: LowpassFO 58 | freq: 1000 59 | 60 | # Highshelf and Lowshelf 61 | 62 | highshelf_using_slope: 63 | type: Biquad 64 | parameters: 65 | type: Highshelf 66 | freq: 1000 67 | gain: -12 68 | slope: 6 69 | 70 | highshelf_using_q: 71 | type: Biquad 72 | parameters: 73 | type: Highshelf 74 | freq: 1000 75 | gain: -12 76 | q: 0.6 77 | 78 | lowshelf_using_slope: 79 | type: Biquad 80 | parameters: 81 | type: Lowshelf 82 | freq: 1000 83 | gain: -12 84 | slope: 6 85 | 86 | lowshelf_using_q: 87 | type: Biquad 88 | parameters: 89 | type: Lowshelf 90 | freq: 1000 91 | gain: -12 92 | q: 0.6 93 | 94 | highshelf_first_order: 95 | type: Biquad 96 | parameters: 97 | type: HighshelfFO 98 | freq: 100 99 | gain: -6 100 | 101 | lowshelf_first_order: 102 | type: Biquad 103 | parameters: 104 | type: LowshelfFO 105 | freq: 100 106 | gain: -6 107 | 108 | # Peaking / Notch / Bandpass 109 | 110 | peaking_using_q: 111 | type: Biquad 112 | parameters: 113 | type: Peaking 114 | freq: 1000 115 | gain: 12.0 # use negative gain to use as dip filter 116 | q: 2.0 117 | 118 | peaking_using_bandwidth: 119 | type: Biquad 120 | parameters: 121 | type: Peaking 122 | freq: 1000 123 | gain: 12.0 124 | bandwidth: 2.0 125 | 126 | notch_using_q: 127 | type: Biquad 128 | parameters: 129 | type: Notch 130 | freq: 1000 131 | q: 2.0 132 | 133 | notch_using_bandwidth: 134 | type: Biquad 135 | parameters: 136 | type: Notch 137 | freq: 1000 138 | bandwidth: 2.0 139 | 140 | general_notch: 141 | type: Biquad 142 | parameters: 143 | type: GeneralNotch 144 | freq_p: 1000 145 | freq_z: 2000 146 | q_p: 2.0 147 | normalize_at_dc: true 148 | 149 | bandpass_using_q: 150 | type: Biquad 151 | parameters: 152 | type: Bandpass 153 | freq: 1000 154 | q: 0.5 155 | 156 | bandpass_using_bandwidth: 157 | type: Biquad 158 | parameters: 159 | type: Bandpass 160 | freq: 1000 161 | bandwidth: 1.2 162 | 163 | # Allpass 164 | 165 | allpass_using_q: 166 | type: Biquad 167 | parameters: 168 | type: Allpass 169 | freq: 1000 170 | q: 0.5 171 | 172 | allpass_using_bandwidth: 173 | type: Biquad 174 | parameters: 175 | type: Allpass 176 | freq: 1000 177 | bandwidth: 1.2 178 | 179 | allpass_first_order: 180 | type: Biquad 181 | parameters: 182 | type: AllpassFO 183 | freq: 1000 184 | 185 | # Linkwitz transform 186 | 187 | linkwitztransform: 188 | type: Biquad 189 | parameters: 190 | type: LinkwitzTransform 191 | freq_act: 100 192 | q_act: 1.2 193 | freq_target: 25 194 | q_target: 0.7 195 | 196 | # Biquad combo filters: Butterworth HP and LP / Linkwitz-Riley HP and LP / 5-Point parametric Equalizer 197 | 198 | Butterworth_highpass: 199 | type: BiquadCombo 200 | parameters: 201 | type: ButterworthHighpass 202 | freq: 1000 203 | order: 4 204 | 205 | Butterworth_lowpass: 206 | type: BiquadCombo 207 | parameters: 208 | type: ButterworthLowpass 209 | freq: 1000 210 | order: 4 211 | 212 | LR_highpass: 213 | type: BiquadCombo 214 | parameters: 215 | type: LinkwitzRileyHighpass 216 | freq: 1000 217 | order: 4 218 | 219 | LR_lowpass: 220 | type: BiquadCombo 221 | parameters: 222 | type: LinkwitzRileyLowpass 223 | freq: 1000 224 | order: 4 225 | 226 | Tilt: 227 | type: BiquadCombo 228 | parameters: 229 | type: Tilt 230 | gain: -5.0 231 | 232 | FivePointParametricEqualizer: 233 | type: BiquadCombo 234 | parameters: 235 | type: FivePointPeq 236 | # LowShelf 237 | fls: 125 238 | gls: 1.0 239 | qls: 0.7 240 | # Peaking_1 241 | fp1: 400 242 | gp1: -0.5 243 | qp1: 0.7 244 | # Peaking_2 245 | fp2: 1000 246 | gp2: 1.5 247 | qp2: 0.7 248 | # Peaking_3 249 | fp3: 2500 250 | gp3: -0.25 251 | qp3: 0.7 252 | # HighShelf 253 | fhs: 8000 254 | ghs: 0.5 255 | qhs: 0.7 256 | 257 | Graphic: 258 | type: BiquadCombo 259 | parameters: 260 | type: GraphicEqualizer 261 | freq_min: 20 262 | freq_max: 20000 263 | gains: [0.0, 1.0, 2.0, 1.0, 0.0] 264 | 265 | 266 | 267 | 268 | -------------------------------------------------------------------------------- /web/camilla.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CamillaDSP config 7 | 12 | 13 | 14 | 33 | 34 |
Configuration 35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 |

43 | Configuration file 44 |
45 |
46 | 47 | 48 | 49 |

50 | Command response 51 |
52 |
53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /.github/workflows/ci_test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI test and lint 4 | 5 | jobs: 6 | check_lint_test_linux: 7 | name: Check test and lint Linux 8 | runs-on: ubuntu-latest 9 | #container: ubuntu:20.04 10 | steps: 11 | - name: Checkout sources 12 | uses: actions/checkout@v4 13 | 14 | - name: Update package list 15 | run: sudo apt-get update 16 | 17 | - name: Install utils 18 | run: sudo apt-get install curl wget -y 19 | 20 | - name: Install stable toolchain 21 | uses: dtolnay/rust-toolchain@stable 22 | with: 23 | components: rustfmt, clippy 24 | 25 | - name: Install Alsa devel 26 | run: sudo apt-get install libasound2-dev -y 27 | 28 | - name: Install PulseAudio 29 | run: DEBIAN_FRONTEND="noninteractive" sudo apt-get install libpulse0 libpulse-dev -y 30 | 31 | - name: Install Jack 32 | run: DEBIAN_FRONTEND="noninteractive" sudo apt-get install libjack-dev -y 33 | 34 | - name: Install OpenSSL 35 | run: sudo apt-get install openssl libssl-dev -y 36 | 37 | - name: Run cargo check 38 | run: cargo check 39 | 40 | - name: Run cargo check nodefault 41 | run: cargo check --no-default-features 42 | 43 | - name: Run cargo test with all supported backends 44 | run: cargo test --features bluez-backend,cpal-backend,jack-backend,pulse-backend, 45 | 46 | - name: Run cargo test with all optional features 47 | run: cargo test --features 32bit,debug,secure-websocket 48 | 49 | - name: Run cargo fmt 50 | run: cargo fmt --all -- --check 51 | 52 | - name: Run cargo clippy 53 | run: cargo clippy -- -D warnings 54 | 55 | check_test_arm: 56 | name: Check and test Linux arm 57 | runs-on: ubuntu-latest 58 | steps: 59 | - name: Checkout sources 60 | uses: actions/checkout@v4 61 | 62 | - name: Install stable toolchain 63 | uses: dtolnay/rust-toolchain@stable 64 | with: 65 | targets: armv7-unknown-linux-gnueabihf 66 | 67 | - name: Install cross 68 | run: cargo install cross --git https://github.com/cross-rs/cross 69 | 70 | - name: Build image 71 | run: docker build -t cross/armv7:v1 --file Dockerfile_armv7 ./ 72 | 73 | - name: Run cargo check 74 | run: cross check --target armv7-unknown-linux-gnueabihf 75 | 76 | - name: Run cargo test for arm 77 | run: cross test --target armv7-unknown-linux-gnueabihf 78 | 79 | check_test_arm64: 80 | name: Check and test Linux arm 64bit 81 | runs-on: ubuntu-latest 82 | steps: 83 | - name: Checkout sources 84 | uses: actions/checkout@v4 85 | 86 | - name: Install stable toolchain 87 | uses: dtolnay/rust-toolchain@stable 88 | with: 89 | targets: aarch64-unknown-linux-gnu 90 | 91 | - name: Install cross 92 | run: cargo install cross --git https://github.com/cross-rs/cross 93 | 94 | - name: Build image 95 | run: docker build -t cross/armv8:v1 --file Dockerfile_armv8 ./ 96 | 97 | - name: Run cargo check 98 | run: cross check --target aarch64-unknown-linux-gnu 99 | 100 | - name: Run cargo test for arm 101 | run: cross test --target aarch64-unknown-linux-gnu 102 | 103 | check_test_armv6: 104 | name: Check and test Linux arm v6 105 | runs-on: ubuntu-latest 106 | steps: 107 | - name: Checkout sources 108 | uses: actions/checkout@v4 109 | 110 | - name: Install stable toolchain 111 | uses: dtolnay/rust-toolchain@stable 112 | with: 113 | targets: arm-unknown-linux-gnueabihf 114 | 115 | - name: Install cross 116 | run: cargo install cross --git https://github.com/cross-rs/cross 117 | 118 | - name: Build image 119 | run: docker build -t cross/armv6:v1 --file Dockerfile_armv6 ./ 120 | 121 | - name: Run cargo check 122 | run: cross check --target arm-unknown-linux-gnueabihf 123 | 124 | - name: Run cargo test for arm 125 | run: cross test --target arm-unknown-linux-gnueabihf 126 | env: 127 | LD_LIBRARY_PATH: /usr/lib/arm-linux-gnueabihf 128 | 129 | check_test_windows: 130 | name: Check and test Windows 131 | runs-on: windows-latest 132 | steps: 133 | - name: Checkout sources 134 | uses: actions/checkout@v4 135 | 136 | - name: Install stable toolchain 137 | uses: dtolnay/rust-toolchain@stable 138 | 139 | - name: Run cargo check 140 | run: cargo check --no-default-features 141 | 142 | - name: Run cargo test 143 | run: cargo test --no-default-features 144 | 145 | check_test_windows7: 146 | name: Check and test Windows7 (rustc 1.75) 147 | runs-on: windows-latest 148 | steps: 149 | - name: Checkout sources 150 | uses: actions/checkout@v4 151 | 152 | - name: Install stable toolchain 153 | uses: dtolnay/rust-toolchain@1.75.0 154 | 155 | - name: Run cargo check 156 | run: cargo check --no-default-features 157 | 158 | - name: Run cargo test 159 | run: cargo test --no-default-features 160 | 161 | check_test_macos: 162 | name: Check and test macOS Intel 163 | runs-on: macos-13 164 | steps: 165 | - name: Checkout sources 166 | uses: actions/checkout@v4 167 | 168 | - name: Install stable toolchain 169 | uses: dtolnay/rust-toolchain@stable 170 | 171 | - name: Run cargo check 172 | run: cargo check --no-default-features 173 | 174 | - name: Run cargo test 175 | run: cargo test --no-default-features 176 | 177 | check_macos_arm: 178 | name: Check and test macOS Arm 179 | runs-on: macos-latest 180 | steps: 181 | - name: Checkout sources 182 | uses: actions/checkout@v4 183 | 184 | - name: Install stable toolchain 185 | uses: dtolnay/rust-toolchain@stable 186 | 187 | - name: Run cargo check 188 | run: cargo check --no-default-features 189 | 190 | - name: Run cargo test 191 | run: cargo test --no-default-features 192 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /backend_coreaudio.md: -------------------------------------------------------------------------------- 1 | # CoreAudio (macOS) 2 | 3 | ## Introduction 4 | CoreAudio is the standard audio API of macOS. 5 | The CoreAudio support of CamillaDSP is provided via the 6 | [coreaudio-rs library](https://github.com/RustAudio/coreaudio-rs). 7 | 8 | CoreAudio is a large API that offers several ways to accomplish most common tasks. 9 | CamillaDSP uses the low-level AudioUnits for playback and capture. 10 | An AudioUnit that represents a hardware device has two stream formats. 11 | One format is used for communicating with the application. 12 | This is typically 32-bit float, the same format that CoreAudio uses internally. 13 | The other format (called the physical format) is the one used to send or receive data to/from the sound card driver. 14 | 15 | ## Microphone access 16 | In order to capture audio on macOS, an application needs the be given access. 17 | First time CamillaDSP is launched, the system should show a popup asking if the Terminal app 18 | should be allowed to use the microphone. 19 | This is somewhat misleading, as the microphone access covers all recording of sound, 20 | not only from the microphone. 21 | 22 | Without this access, there is no error message and CamillaDSP appears to be running ok, 23 | but only records silence. 24 | If this happens, open System Settings, select "Privacy & Security", and click "Microphone". 25 | Verify that "Terminal" is listed and enabled. 26 | 27 | There is no way to manually add approved apps to the list. 28 | If "Terminal" is not listed, try executing `tccutil reset Microphone` in the terminal. 29 | This resets the microphone access for all apps, 30 | and should make the popup appear next time CamillaDSP is started. 31 | 32 | 33 | ## Capturing audio from other applications 34 | 35 | To capture audio from applications a virtual sound card is needed. 36 | It is recommended to use [BlackHole](https://github.com/ExistentialAudio/BlackHole). 37 | This works on both Intel and Apple Silicon macs. 38 | Since version 0.5.0 Blackhole supports adjusting the rate of the virtual clock. 39 | This makes it possible to sync the virtual device with a real device, and avoid the need for asynchronous resampling. 40 | CamillaDSP supports and will use this functionality when it is available. 41 | 42 | An alternative is [Soundflower](https://github.com/mattingalls/Soundflower), which is older and only supports Intel macs. 43 | 44 | Some player applications can use hog mode to get exclusive access to the playback device. 45 | Using this with a virtual soundcard like BlackHole causes problems, and is therefore not recommended. 46 | 47 | ### Sending all audio to the virtual card 48 | Set the virtual sound card as the default playback device in the Sound preferences. 49 | This will work for all applications that respect this setting, which in practice is nearly all. 50 | The exceptions are the ones that provide their own way of selecting playback device. 51 | 52 | ### Capturing the audio 53 | When applications output their audio to the playback side of the virtual soundcard, then this audio can be captured from the capture side. 54 | This is done by giving the virtual soundcard as the capture device in the CamillaDSP configuration. 55 | 56 | ### Sample rate change notifications 57 | CamillaDSP will listen for notifications from CoreAudio. 58 | If the sample rate of the capture device changes, then CoreAudio will stop providing new samples to any client currently capturing from it. 59 | To continue from this state, the capture device needs to be closed and reopened. 60 | For CamillaDSP this means that the configuration must be reloaded. 61 | If the capture device sample rate changes, then CamillaDSP will stop. 62 | 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. 63 | 64 | ## Configuration of devices 65 | 66 | This example configuration will be used to explain the various options specific to CoreAudio: 67 | ``` 68 | capture: 69 | type: CoreAudio 70 | channels: 2 71 | device: "Soundflower (2ch)" (*) 72 | format: S32LE (*) 73 | playback: 74 | type: CoreAudio 75 | channels: 2 76 | device: "Built-in Output" (*) 77 | format: S24LE (*) 78 | exclusive: false (*) 79 | ``` 80 | The parameters marked (*) are optional. 81 | 82 | ### Device names 83 | 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. 84 | 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". 85 | 86 | Specifying `null` or leaving out `device` will give the default capture or playback device. 87 | 88 | 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 89 | Just download the binary and run it in a terminal. It will list all devices with the names. 90 | 91 | ### Sample format 92 | CamillaDSP always uses 32-bit float uses when transferring data to and from CoreAudio. 93 | The conversion from 32-bit float to the sample format used by the actual DAC (the physical format) is performed by CoreAudio. 94 | 95 | The physical format can be set using the "Audio MIDI Setup" app. 96 | 97 | The optional `format` parameter determines whether CamillaDSP should change the physical format or not. 98 | If a value is given, then CamillaDSP will change the setting to match the selected `format`. 99 | To do this, it fetches a list of the supported stream formats for the device. 100 | It then searches the list until it finds a suitable one. 101 | The criteria is that it must have the right sample rate, the right number of bits, 102 | and the right number type (float or integer). 103 | There exact representation of the given format isn't used. 104 | This means that S24LE and S24LE3 are equivalent, and the "LE" ending that means 105 | little-endian for other backends is ignored. 106 | 107 | This table shows the mapping between the format setting in "Audio MIDI Setup" and the CamillaDSP `format`: 108 | - 16-bit Integer: S16LE 109 | - 24-bit Integer: S24LE or S24LE3 110 | - 32-bit Integer: S32LE 111 | - 32-bit Float: FLOAT32LE 112 | 113 | If `format` is set to `null` or left out, then CamillaDSP will leave the sample format unchanged, and only switch the sample rate. 114 | 115 | The playback device has an `exclusive` setting for whether CamillaDSP should request exclusive 116 | access to the device or not. This is also known as hog mode. When enabled, no other application 117 | can output sound to the device while CamillaDSP runs. The setting is optional and defaults to false if left out. -------------------------------------------------------------------------------- /src/helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::PrcFmt; 2 | use num_complex::Complex; 3 | 4 | // element-wise product, result = slice_a * slice_b 5 | pub fn multiply_elements( 6 | result: &mut [Complex], 7 | slice_a: &[Complex], 8 | slice_b: &[Complex], 9 | ) { 10 | let len = result.len(); 11 | let mut res = &mut result[..len]; 12 | let mut val_a = &slice_a[..len]; 13 | let mut val_b = &slice_b[..len]; 14 | 15 | unsafe { 16 | while res.len() >= 8 { 17 | *res.get_unchecked_mut(0) = *val_a.get_unchecked(0) * *val_b.get_unchecked(0); 18 | *res.get_unchecked_mut(1) = *val_a.get_unchecked(1) * *val_b.get_unchecked(1); 19 | *res.get_unchecked_mut(2) = *val_a.get_unchecked(2) * *val_b.get_unchecked(2); 20 | *res.get_unchecked_mut(3) = *val_a.get_unchecked(3) * *val_b.get_unchecked(3); 21 | *res.get_unchecked_mut(4) = *val_a.get_unchecked(4) * *val_b.get_unchecked(4); 22 | *res.get_unchecked_mut(5) = *val_a.get_unchecked(5) * *val_b.get_unchecked(5); 23 | *res.get_unchecked_mut(6) = *val_a.get_unchecked(6) * *val_b.get_unchecked(6); 24 | *res.get_unchecked_mut(7) = *val_a.get_unchecked(7) * *val_b.get_unchecked(7); 25 | res = &mut res[8..]; 26 | val_a = val_a.get_unchecked(8..); 27 | val_b = val_b.get_unchecked(8..); 28 | } 29 | } 30 | for (r, val) in res 31 | .iter_mut() 32 | .zip(val_a.iter().zip(val_b.iter()).map(|(a, b)| *a * *b)) 33 | { 34 | *r = val; 35 | } 36 | } 37 | 38 | // element-wise add product, result = result + slice_a * slice_b 39 | pub fn multiply_add_elements( 40 | result: &mut [Complex], 41 | slice_a: &[Complex], 42 | slice_b: &[Complex], 43 | ) { 44 | let len = result.len(); 45 | let mut res = &mut result[..len]; 46 | let mut val_a = &slice_a[..len]; 47 | let mut val_b = &slice_b[..len]; 48 | 49 | unsafe { 50 | while res.len() >= 8 { 51 | *res.get_unchecked_mut(0) += *val_a.get_unchecked(0) * *val_b.get_unchecked(0); 52 | *res.get_unchecked_mut(1) += *val_a.get_unchecked(1) * *val_b.get_unchecked(1); 53 | *res.get_unchecked_mut(2) += *val_a.get_unchecked(2) * *val_b.get_unchecked(2); 54 | *res.get_unchecked_mut(3) += *val_a.get_unchecked(3) * *val_b.get_unchecked(3); 55 | *res.get_unchecked_mut(4) += *val_a.get_unchecked(4) * *val_b.get_unchecked(4); 56 | *res.get_unchecked_mut(5) += *val_a.get_unchecked(5) * *val_b.get_unchecked(5); 57 | *res.get_unchecked_mut(6) += *val_a.get_unchecked(6) * *val_b.get_unchecked(6); 58 | *res.get_unchecked_mut(7) += *val_a.get_unchecked(7) * *val_b.get_unchecked(7); 59 | res = &mut res[8..]; 60 | val_a = val_a.get_unchecked(8..); 61 | val_b = val_b.get_unchecked(8..); 62 | } 63 | } 64 | for (r, val) in res 65 | .iter_mut() 66 | .zip(val_a.iter().zip(val_b.iter()).map(|(a, b)| *a * *b)) 67 | { 68 | *r += val; 69 | } 70 | } 71 | 72 | // Inplace recalculation of values positive values 0..1 to dB. 73 | pub fn linear_to_db(values: &mut [f32]) { 74 | values.iter_mut().for_each(|val| { 75 | if *val == 0.0 { 76 | *val = -1000.0; 77 | } else { 78 | *val = 20.0 * val.log10(); 79 | } 80 | }); 81 | } 82 | 83 | // A simple PI controller for rate adjustments 84 | pub struct PIRateController { 85 | target_level: f64, 86 | interval: f64, 87 | k_p: f64, 88 | k_i: f64, 89 | frames_per_interval: f64, 90 | accumulated: f64, 91 | ramp_steps: usize, 92 | ramp_trigger_limit: f64, 93 | ramp_start: f64, 94 | ramp_step: usize, 95 | } 96 | 97 | impl PIRateController { 98 | /// Create a new controller with default gains 99 | pub fn new_with_default_gains(fs: usize, interval: f64, target_level: usize) -> Self { 100 | let k_p = 0.2; 101 | let k_i = 0.004; 102 | let ramp_steps = 20; 103 | let ramp_trigger_limit = 0.33; 104 | Self::new( 105 | fs, 106 | interval, 107 | target_level, 108 | k_p, 109 | k_i, 110 | ramp_steps, 111 | ramp_trigger_limit, 112 | ) 113 | } 114 | 115 | pub fn new( 116 | fs: usize, 117 | interval: f64, 118 | target_level: usize, 119 | k_p: f64, 120 | k_i: f64, 121 | ramp_steps: usize, 122 | ramp_trigger_limit: f64, 123 | ) -> Self { 124 | let frames_per_interval = interval * fs as f64; 125 | Self { 126 | target_level: target_level as f64, 127 | interval, 128 | k_p, 129 | k_i, 130 | frames_per_interval, 131 | accumulated: 0.0, 132 | ramp_steps, 133 | ramp_trigger_limit, 134 | ramp_start: target_level as f64, 135 | ramp_step: 0, 136 | } 137 | } 138 | 139 | /// Calculate the control output for the next measured value 140 | pub fn next(&mut self, level: f64) -> f64 { 141 | if self.ramp_step >= self.ramp_steps 142 | && ((self.target_level - level) / self.target_level).abs() > self.ramp_trigger_limit 143 | { 144 | self.ramp_start = level; 145 | self.ramp_step = 0; 146 | debug!( 147 | "Rate controller, buffer level is {}, starting to adjust back towards target of {}", 148 | level, self.target_level 149 | ); 150 | } 151 | if self.ramp_step == 0 { 152 | self.ramp_start = level; 153 | } 154 | let current_target = if self.ramp_step < self.ramp_steps { 155 | self.ramp_step += 1; 156 | let tgt = self.ramp_start 157 | + (self.target_level - self.ramp_start) 158 | * (1.0 159 | - ((self.ramp_steps as f64 - self.ramp_step as f64) 160 | / self.ramp_steps as f64) 161 | .powi(4)); 162 | debug!( 163 | "Rate controller, ramp step {}/{}, current target {}", 164 | self.ramp_step, self.ramp_steps, tgt 165 | ); 166 | tgt 167 | } else { 168 | self.target_level 169 | }; 170 | let err = level - current_target; 171 | let rel_err = err / self.frames_per_interval; 172 | self.accumulated += rel_err * self.interval; 173 | let proportional = self.k_p * rel_err; 174 | let integral = self.k_i * self.accumulated; 175 | let mut output = proportional + integral; 176 | trace!( 177 | "Rate controller, error: {}, output: {}, P: {}, I: {}", 178 | err, 179 | output, 180 | proportional, 181 | integral 182 | ); 183 | output = output.clamp(-0.005, 0.005); 184 | 1.0 - output 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /backend_wasapi.md: -------------------------------------------------------------------------------- 1 | # WASAPI (Windows) 2 | 3 | ## Introduction 4 | 5 | The WASAPI audio API was introduced with Windows Vista. 6 | It offers two modes, "shared" and "exclusive", that offer different features and are intended for different use cases. CamillaDSP supports both modes. 7 | 8 | ### Shared mode 9 | This is the mode that most applications use. As the name suggests, this mode allows an audio device to be shared by several applications. 10 | 11 | In shared mode the audio device then operates at a fixed sample rate and sample format. 12 | Every stream sent to it (or recorded from it) is resampled to/from the shared rate and format. 13 | 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. 14 | Internally, the Windows audio stack uses 32-bit float as the sample format. 15 | The audio passes through the Windows mixer and volume control. 16 | 17 | In shared mode, these points apply for the CamillaDSP configuration: 18 | - The `samplerate` parameter must match the "Default format" setting of the device. 19 | To change this, open "Sound" in the Control panel, select the sound card, and click "Properties". 20 | Then open the "Advanced" tab and select the desired format under "Default Format". 21 | Pick the desired sample rate, and the largest number of bits available. 22 | - [Loopback](#loopback-capture) capture mode is available. 23 | - The sample format is always 32-bit float (`FLOAT32LE`). 24 | 25 | 26 | ### Exclusive mode 27 | This mode is often used for high quality music playback. 28 | 29 | In this mode one application takes full control over an audio device. Only one application at a time can use the device. 30 | The sample rate and sample format can be changed, and the audio does not pass through the Windows mixer and volume control. 31 | This allows bit-perfect playback at any sample rate and sample format the hardware supports. 32 | While an application holds the device in exclusive mode, other apps will not be able to play for example notification sounds. 33 | 34 | In exclusive mode, these points apply for the CamillaDSP configuration: 35 | - CamillaDSP is able to control the sample rate of the devices. 36 | - The sample format must be one that the device driver can accept. 37 | This usually matches the hardware capabilities of the device. 38 | For example a 24-bit USB dac is likely to accept the `S16LE` and `S24LE3` formats. 39 | Other formats may be supported depending on driver support. 40 | Note that all sample formats may not be available at all sample rates. 41 | A USB device might support both 16 and 24 bits at up to 96 kHz, but only 16 bits above that. 42 | - [Loopback](#loopback-capture) capture mode is __not__ available. 43 | 44 | ## Capturing audio from other applications 45 | 46 | CamillaDSP must capture audio from a capture device. This can either be a virtual sound card, or an additional card in loopback mode. 47 | 48 | ### Virtual sound card 49 | 50 | When using a virtual sound card (sometimes called loopback device), all applications output their audio to the playback side of this virtual sound card. 51 | 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. 52 | 53 | #### Sending all audio to the virtual card 54 | Set VB-CABLE as the default playback device in the Windows sound control panel. 55 | Open "Sound" in the Control Panel, then in the "Playback" tab select "CABLE Input" and click the "Set Default" button. 56 | This will work for all applications that respect this setting, which in practice is nearly all. 57 | The exceptions are the ones that provide their own way of selecting playback device. 58 | 59 | #### Capturing the audio 60 | The next step is to figure out the device name to enter in the CamillaDSP configuration. 61 | Again open "Sound" in the Control Panel, and switch to the Recording tab. 62 | There should be a device listed as "CABLE Output". 63 | Unless the default names have been changed, the device name to enter in the CamillaDSP config is "CABLE Output (VB-Audio Virtual Cable)". 64 | See also [Device names](#device-names) for more details on how to build the device names. 65 | 66 | ### Loopback capture 67 | In loopback mode the audio is captured from a Playback device. 68 | This allows capturing the sound that a card is playing. 69 | In this mode, a spare unused sound card is used (note that this card can be either real or virtual). 70 | The built in audio of the computer should work. The quality of the card doesn't matter, 71 | since the audio data will not be routed through it. This requires using [Shared mode](#shared-mode). 72 | 73 | Open the Sound Control Panel app, and locate the unused card in the "Playback" tab. 74 | Set it as default device. See [Device names](#device-names) for how to write the device name to enter in the CamillaDSP configuration. 75 | 76 | ## Configuration of devices 77 | 78 | This example configuration will be used to explain the various options specific to WASAPI: 79 | ``` 80 | capture: 81 | type: Wasapi 82 | channels: 2 83 | device: "CABLE Output (VB-Audio Virtual Cable)" (*) 84 | format: FLOAT32LE 85 | exclusive: false (*) 86 | loopback: false (*) 87 | playback: 88 | type: Wasapi 89 | channels: 2 90 | device: "SPDIF Interface (FX-AUDIO-DAC-X6)" (*) 91 | format: S24LE3 92 | exclusive: true (*) 93 | ``` 94 | The parameters marked (*) are optional. 95 | 96 | ### Device names 97 | The device names that are used for `device` for both playback and capture are entered as shown in the Windows volume control. 98 | 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. 99 | This displays a list of all playback devices, with their names in the right format. 100 | The names can also be seen in the "Sound" control panel app. Look at either the "Playback" or "Recording" tab. 101 | The device name is built from the input/output name and card name, and the format is "{input/output name} ({card name})". 102 | For example, the VB-CABLE device name is "CABLE Output (VB-Audio Virtual Cable)", 103 | and the built in audio of a desktop computer can be "Speakers (Realtek(R) Audio)". 104 | 105 | Specifying `null` or leaving out `device` will give the default capture or playback device. 106 | 107 | 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 108 | 109 | Just download the binary and run it in a terminal. It will list all devices with the names. 110 | The parameters shown are for shared mode, more sample rates and sample formats will likely be available in exclusive mode. 111 | 112 | ### Shared or exclusive mode 113 | Set `exclusive` to `true` to enable exclusive mode. 114 | Setting it to `false` or leaving it out means that shared mode will be used. 115 | Playback and capture are independent, they do not need to use the same mode. 116 | 117 | ### Loopback capture 118 | Setting `loopback` to `true` enables loopback capture. 119 | This requires using shared mode for the capture device. 120 | See [Loopback capture](#loopback-capture) for more details. -------------------------------------------------------------------------------- /src/noisegate.rs: -------------------------------------------------------------------------------- 1 | use crate::audiodevice::AudioChunk; 2 | use crate::config; 3 | use crate::filters::Processor; 4 | use crate::PrcFmt; 5 | use crate::Res; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct NoiseGate { 9 | pub name: String, 10 | pub channels: usize, 11 | pub monitor_channels: Vec, 12 | pub process_channels: Vec, 13 | pub attack: PrcFmt, 14 | pub release: PrcFmt, 15 | pub threshold: PrcFmt, 16 | pub factor: PrcFmt, 17 | pub samplerate: usize, 18 | pub scratch: Vec, 19 | pub prev_loudness: PrcFmt, 20 | } 21 | 22 | impl NoiseGate { 23 | /// Creates a NoiseGate from a config struct 24 | pub fn from_config( 25 | name: &str, 26 | config: config::NoiseGateParameters, 27 | samplerate: usize, 28 | chunksize: usize, 29 | ) -> Self { 30 | let name = name.to_string(); 31 | let channels = config.channels; 32 | let srate = samplerate as PrcFmt; 33 | let mut monitor_channels = config.monitor_channels(); 34 | if monitor_channels.is_empty() { 35 | for n in 0..channels { 36 | monitor_channels.push(n); 37 | } 38 | } 39 | let mut process_channels = config.process_channels(); 40 | if process_channels.is_empty() { 41 | for n in 0..channels { 42 | process_channels.push(n); 43 | } 44 | } 45 | let attack = (-1.0 / srate / config.attack).exp(); 46 | let release = (-1.0 / srate / config.release).exp(); 47 | let scratch = vec![0.0; chunksize]; 48 | 49 | debug!("Creating noisegate '{}', channels: {}, monitor_channels: {:?}, process_channels: {:?}, attack: {}, release: {}, threshold: {}, attenuation: {}", 50 | name, channels, process_channels, monitor_channels, attack, release, config.threshold, config.attenuation); 51 | 52 | let factor = (10.0 as PrcFmt).powf(-config.attenuation / 20.0); 53 | 54 | NoiseGate { 55 | name, 56 | channels, 57 | monitor_channels, 58 | process_channels, 59 | attack, 60 | release, 61 | threshold: config.threshold, 62 | factor, 63 | samplerate, 64 | scratch, 65 | prev_loudness: 0.0, 66 | } 67 | } 68 | 69 | /// Sum all channels that are included in loudness monitoring, store result in self.scratch 70 | fn sum_monitor_channels(&mut self, input: &AudioChunk) { 71 | let ch = self.monitor_channels[0]; 72 | self.scratch.copy_from_slice(&input.waveforms[ch]); 73 | for ch in self.monitor_channels.iter().skip(1) { 74 | for (acc, val) in self.scratch.iter_mut().zip(input.waveforms[*ch].iter()) { 75 | *acc += *val; 76 | } 77 | } 78 | } 79 | 80 | /// Estimate loudness, store result in self.scratch 81 | fn estimate_loudness(&mut self) { 82 | for val in self.scratch.iter_mut() { 83 | // convert to dB 84 | *val = 20.0 * (val.abs() + 1.0e-9).log10(); 85 | if *val >= self.prev_loudness { 86 | *val = self.attack * self.prev_loudness + (1.0 - self.attack) * *val; 87 | } else { 88 | *val = self.release * self.prev_loudness + (1.0 - self.release) * *val; 89 | } 90 | self.prev_loudness = *val; 91 | } 92 | } 93 | 94 | /// Calculate linear gain, store result in self.scratch 95 | fn calculate_linear_gain(&mut self) { 96 | for val in self.scratch.iter_mut() { 97 | if *val < self.threshold { 98 | *val = self.factor; 99 | } else { 100 | *val = 1.0; 101 | } 102 | } 103 | } 104 | 105 | fn apply_gain(&self, input: &mut [PrcFmt]) { 106 | for (val, gain) in input.iter_mut().zip(self.scratch.iter()) { 107 | *val *= gain; 108 | } 109 | } 110 | } 111 | 112 | impl Processor for NoiseGate { 113 | fn name(&self) -> &str { 114 | &self.name 115 | } 116 | 117 | /// Apply a NoiseGate to an AudioChunk, modifying it in-place. 118 | fn process_chunk(&mut self, input: &mut AudioChunk) -> Res<()> { 119 | self.sum_monitor_channels(input); 120 | self.estimate_loudness(); 121 | self.calculate_linear_gain(); 122 | for ch in self.process_channels.iter() { 123 | self.apply_gain(&mut input.waveforms[*ch]); 124 | } 125 | Ok(()) 126 | } 127 | 128 | fn update_parameters(&mut self, config: config::Processor) { 129 | if let config::Processor::NoiseGate { 130 | parameters: config, .. 131 | } = config 132 | { 133 | let channels = config.channels; 134 | let srate = self.samplerate as PrcFmt; 135 | let mut monitor_channels = config.monitor_channels(); 136 | if monitor_channels.is_empty() { 137 | for n in 0..channels { 138 | monitor_channels.push(n); 139 | } 140 | } 141 | let mut process_channels = config.process_channels(); 142 | if process_channels.is_empty() { 143 | for n in 0..channels { 144 | process_channels.push(n); 145 | } 146 | } 147 | let attack = (-1.0 / srate / config.attack).exp(); 148 | let release = (-1.0 / srate / config.release).exp(); 149 | 150 | self.monitor_channels = monitor_channels; 151 | self.process_channels = process_channels; 152 | self.attack = attack; 153 | self.release = release; 154 | self.threshold = config.threshold; 155 | self.factor = (10.0 as PrcFmt).powf(-config.attenuation / 20.0); 156 | 157 | debug!("Updated noise gate '{}', monitor_channels: {:?}, process_channels: {:?}, attack: {}, release: {}, threshold: {}, attenuation: {}", 158 | self.name, self.process_channels, self.monitor_channels, attack, release, config.threshold, config.attenuation); 159 | } else { 160 | // This should never happen unless there is a bug somewhere else 161 | panic!("Invalid config change!"); 162 | } 163 | } 164 | } 165 | 166 | /// Validate the noise gate config, to give a helpful message intead of a panic. 167 | pub fn validate_noise_gate(config: &config::NoiseGateParameters) -> Res<()> { 168 | let channels = config.channels; 169 | if config.attack <= 0.0 { 170 | let msg = "Attack value must be larger than zero."; 171 | return Err(config::ConfigError::new(msg).into()); 172 | } 173 | if config.release <= 0.0 { 174 | let msg = "Release value must be larger than zero."; 175 | return Err(config::ConfigError::new(msg).into()); 176 | } 177 | for ch in config.monitor_channels().iter() { 178 | if *ch >= channels { 179 | let msg = format!( 180 | "Invalid monitor channel: {}, max is: {}.", 181 | *ch, 182 | channels - 1 183 | ); 184 | return Err(config::ConfigError::new(&msg).into()); 185 | } 186 | } 187 | for ch in config.process_channels().iter() { 188 | if *ch >= channels { 189 | let msg = format!( 190 | "Invalid channel to process: {}, max is: {}.", 191 | *ch, 192 | channels - 1 193 | ); 194 | return Err(config::ConfigError::new(&msg).into()); 195 | } 196 | } 197 | Ok(()) 198 | } 199 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | linux: 10 | name: Linux 11 | runs-on: ubuntu-latest 12 | #container: ubuntu:20.04 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v4 16 | 17 | - name: Update package list 18 | run: sudo apt-get update 19 | 20 | - name: Install utils 21 | run: sudo apt-get install curl wget -y 22 | 23 | - name: Install stable toolchain 24 | uses: dtolnay/rust-toolchain@stable 25 | 26 | - name: Install Alsa devel 27 | run: sudo apt-get install libasound2-dev -y 28 | 29 | - name: Install PulseAudio 30 | run: DEBIAN_FRONTEND="noninteractive" sudo apt-get install libpulse0 libpulse-dev -y 31 | 32 | - name: Install OpenSSL 33 | run: sudo apt-get install openssl libssl-dev -y 34 | 35 | - name: Build 36 | run: cargo build --release --features pulse-backend 37 | 38 | - name: Compress 39 | run: tar -zcvf camilladsp.tar.gz -C target/release camilladsp 40 | 41 | - name: Upload binaries to release 42 | uses: svenstaro/upload-release-action@v2 43 | with: 44 | repo_token: ${{ secrets.GITHUB_TOKEN }} 45 | file: camilladsp.tar.gz 46 | asset_name: camilladsp-linux-amd64.tar.gz 47 | tag: ${{ github.ref }} 48 | 49 | arm: 50 | name: Pi 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: Checkout sources 54 | uses: actions/checkout@v4 55 | 56 | - name: Install stable toolchain 57 | uses: dtolnay/rust-toolchain@stable 58 | with: 59 | targets: armv7-unknown-linux-gnueabihf 60 | 61 | - name: Install cross 62 | run: cargo install cross --git https://github.com/cross-rs/cross 63 | 64 | - name: Build image 65 | run: docker build -t cross/armv7:v1 --file Dockerfile_armv7 ./ 66 | 67 | - name: Build 68 | run: cross build --release --target armv7-unknown-linux-gnueabihf 69 | env: 70 | RUSTFLAGS: "-C target-feature=+neon" 71 | 72 | - name: Compress 73 | run: tar -zcvf camilladsp.tar.gz -C target/armv7-unknown-linux-gnueabihf/release camilladsp 74 | 75 | - name: Upload binaries to release 76 | uses: svenstaro/upload-release-action@v2 77 | with: 78 | repo_token: ${{ secrets.GITHUB_TOKEN }} 79 | file: camilladsp.tar.gz 80 | asset_name: camilladsp-linux-armv7.tar.gz 81 | tag: ${{ github.ref }} 82 | 83 | arm64: 84 | name: Pi64 85 | runs-on: ubuntu-latest 86 | steps: 87 | - name: Checkout sources 88 | uses: actions/checkout@v4 89 | 90 | - name: Install stable toolchain 91 | uses: dtolnay/rust-toolchain@stable 92 | with: 93 | targets: aarch64-unknown-linux-gnu 94 | 95 | - name: Install cross 96 | run: cargo install cross --git https://github.com/cross-rs/cross 97 | 98 | - name: Build image 99 | run: docker build -t cross/armv8:v1 --file Dockerfile_armv8 ./ 100 | 101 | - name: Build 102 | run: cross build --release --target aarch64-unknown-linux-gnu 103 | 104 | - name: Compress 105 | run: tar -zcvf camilladsp.tar.gz -C target/aarch64-unknown-linux-gnu/release camilladsp 106 | 107 | - name: Upload binaries to release 108 | uses: svenstaro/upload-release-action@v2 109 | with: 110 | repo_token: ${{ secrets.GITHUB_TOKEN }} 111 | file: camilladsp.tar.gz 112 | asset_name: camilladsp-linux-aarch64.tar.gz 113 | tag: ${{ github.ref }} 114 | 115 | armv6: 116 | name: Pi-Zero 117 | runs-on: ubuntu-latest 118 | steps: 119 | - name: Checkout sources 120 | uses: actions/checkout@v4 121 | 122 | - name: Install stable toolchain 123 | uses: dtolnay/rust-toolchain@stable 124 | with: 125 | targets: arm-unknown-linux-gnueabihf 126 | 127 | - name: Install cross 128 | run: cargo install cross --git https://github.com/cross-rs/cross 129 | 130 | - name: Build image 131 | run: docker build -t cross/armv6:v1 --file Dockerfile_armv6 ./ 132 | 133 | - name: Build 134 | run: cross build --release --target arm-unknown-linux-gnueabihf 135 | 136 | - name: Compress 137 | run: tar -zcvf camilladsp.tar.gz -C target/arm-unknown-linux-gnueabihf/release camilladsp 138 | 139 | - name: Upload binaries to release 140 | uses: svenstaro/upload-release-action@v2 141 | with: 142 | repo_token: ${{ secrets.GITHUB_TOKEN }} 143 | file: camilladsp.tar.gz 144 | asset_name: camilladsp-linux-armv6.tar.gz 145 | tag: ${{ github.ref }} 146 | 147 | windows: 148 | name: Windows 149 | runs-on: windows-latest 150 | steps: 151 | - name: Checkout sources 152 | uses: actions/checkout@v4 153 | 154 | - name: Install stable toolchain 155 | uses: dtolnay/rust-toolchain@stable 156 | 157 | - name: Build 158 | run: cargo build --release 159 | 160 | - name: Compress 161 | run: powershell Compress-Archive target/release/camilladsp.exe camilladsp.zip 162 | 163 | - name: Upload binaries to release 164 | uses: svenstaro/upload-release-action@v2 165 | with: 166 | repo_token: ${{ secrets.GITHUB_TOKEN }} 167 | file: camilladsp.zip 168 | asset_name: camilladsp-windows-amd64.zip 169 | tag: ${{ github.ref }} 170 | 171 | windows7: 172 | name: Windows7 173 | runs-on: windows-latest 174 | steps: 175 | - name: Checkout sources 176 | uses: actions/checkout@v4 177 | 178 | - name: Install stable toolchain 179 | uses: dtolnay/rust-toolchain@1.75.0 180 | 181 | - name: Build 182 | run: cargo build --release 183 | 184 | - name: Compress 185 | run: powershell Compress-Archive target/release/camilladsp.exe camilladsp.zip 186 | 187 | - name: Upload binaries to release 188 | uses: svenstaro/upload-release-action@v2 189 | with: 190 | repo_token: ${{ secrets.GITHUB_TOKEN }} 191 | file: camilladsp.zip 192 | asset_name: camilladsp-windows7-amd64.zip 193 | tag: ${{ github.ref }} 194 | macos: 195 | name: macOS Intel 196 | runs-on: macos-13 197 | steps: 198 | - name: Checkout sources 199 | uses: actions/checkout@v4 200 | 201 | - name: Install stable toolchain 202 | uses: dtolnay/rust-toolchain@stable 203 | 204 | - name: Build 205 | run: cargo build --release 206 | 207 | - name: Compress 208 | run: tar -zcvf camilladsp.tar.gz -C target/release camilladsp 209 | 210 | - name: Upload binaries to release 211 | uses: svenstaro/upload-release-action@v2 212 | with: 213 | repo_token: ${{ secrets.GITHUB_TOKEN }} 214 | file: camilladsp.tar.gz 215 | asset_name: camilladsp-macos-amd64.tar.gz 216 | tag: ${{ github.ref }} 217 | 218 | 219 | macos_arm: 220 | name: macOS Arm 221 | runs-on: macos-latest 222 | steps: 223 | - name: Checkout sources 224 | uses: actions/checkout@v4 225 | 226 | - name: Install stable toolchain 227 | uses: dtolnay/rust-toolchain@stable 228 | 229 | - name: Build 230 | run: cargo build --release 231 | 232 | - name: Compress 233 | run: tar -zcvf camilladsp.tar.gz -C target/release camilladsp 234 | 235 | - name: Upload binaries to release 236 | uses: svenstaro/upload-release-action@v2 237 | with: 238 | repo_token: ${{ secrets.GITHUB_TOKEN }} 239 | file: camilladsp.tar.gz 240 | asset_name: camilladsp-macos-aarch64.tar.gz 241 | tag: ${{ github.ref }} -------------------------------------------------------------------------------- /src/processing.rs: -------------------------------------------------------------------------------- 1 | use crate::audiodevice::*; 2 | use crate::config; 3 | use crate::filters; 4 | use crate::ProcessingParameters; 5 | use audio_thread_priority::{ 6 | demote_current_thread_from_real_time, promote_current_thread_to_real_time, 7 | }; 8 | use std::sync::mpsc; 9 | use std::sync::{Arc, Barrier}; 10 | use std::thread; 11 | 12 | pub fn run_processing( 13 | conf_proc: config::Configuration, 14 | barrier_proc: Arc, 15 | tx_pb: mpsc::SyncSender, 16 | rx_cap: mpsc::Receiver, 17 | rx_pipeconf: mpsc::Receiver<(config::ConfigChange, config::Configuration)>, 18 | processing_params: Arc, 19 | ) -> thread::JoinHandle<()> { 20 | thread::spawn(move || { 21 | let chunksize = conf_proc.devices.chunksize; 22 | let samplerate = conf_proc.devices.samplerate; 23 | let multithreaded = conf_proc.devices.multithreaded(); 24 | let nbr_threads = conf_proc.devices.worker_threads(); 25 | let hw_threads = std::thread::available_parallelism() 26 | .map(|p| p.get()) 27 | .unwrap_or_default(); 28 | if nbr_threads > hw_threads && multithreaded { 29 | warn!( 30 | "Requested {} worker threads. For optimal performance, this number should not \ 31 | exceed the available CPU cores, which is {}.", 32 | nbr_threads, hw_threads 33 | ); 34 | } 35 | if hw_threads == 1 && multithreaded { 36 | warn!( 37 | "This system only has one CPU core, multithreaded processing is not recommended." 38 | ); 39 | } 40 | if nbr_threads == 1 && multithreaded { 41 | warn!( 42 | "Requested multithreaded processing with one worker thread. \ 43 | Performance can improve by adding more threads or disabling multithreading." 44 | ); 45 | } 46 | let mut pipeline = filters::Pipeline::from_config(conf_proc, processing_params.clone()); 47 | debug!("build filters, waiting to start processing loop"); 48 | 49 | let thread_handle = 50 | match promote_current_thread_to_real_time(chunksize as u32, samplerate as u32) { 51 | Ok(h) => { 52 | debug!("Processing thread has real-time priority."); 53 | Some(h) 54 | } 55 | Err(err) => { 56 | warn!( 57 | "Processing thread could not get real time priority, error: {}", 58 | err 59 | ); 60 | None 61 | } 62 | }; 63 | 64 | // Initialize rayon thread pool 65 | if multithreaded { 66 | match rayon::ThreadPoolBuilder::new() 67 | .num_threads(nbr_threads) 68 | .build_global() 69 | { 70 | Ok(_) => { 71 | debug!( 72 | "Initialized global thread pool with {} workers", 73 | rayon::current_num_threads() 74 | ); 75 | rayon::broadcast(|_| { 76 | match promote_current_thread_to_real_time( 77 | chunksize as u32, 78 | samplerate as u32, 79 | ) { 80 | Ok(_) => { 81 | debug!( 82 | "Worker thread {} has real-time priority.", 83 | rayon::current_thread_index().unwrap_or_default() 84 | ); 85 | } 86 | Err(err) => { 87 | warn!( 88 | "Worker thread {} could not get real time priority, error: {}", 89 | rayon::current_thread_index().unwrap_or_default(), 90 | err 91 | ); 92 | } 93 | }; 94 | }); 95 | } 96 | Err(err) => { 97 | warn!("Failed to build thread pool, error: {}", err); 98 | } 99 | }; 100 | } 101 | 102 | barrier_proc.wait(); 103 | debug!("Processing loop starts now!"); 104 | loop { 105 | match rx_cap.recv() { 106 | Ok(AudioMessage::Audio(mut chunk)) => { 107 | //trace!("AudioMessage::Audio received"); 108 | chunk = pipeline.process_chunk(chunk); 109 | let msg = AudioMessage::Audio(chunk); 110 | if tx_pb.send(msg).is_err() { 111 | info!("Playback thread has already stopped."); 112 | break; 113 | } 114 | } 115 | Ok(AudioMessage::EndOfStream) => { 116 | trace!("AudioMessage::EndOfStream received"); 117 | let msg = AudioMessage::EndOfStream; 118 | if tx_pb.send(msg).is_err() { 119 | info!("Playback thread has already stopped."); 120 | } 121 | break; 122 | } 123 | Ok(AudioMessage::Pause) => { 124 | trace!("AudioMessage::Pause received"); 125 | let msg = AudioMessage::Pause; 126 | if tx_pb.send(msg).is_err() { 127 | info!("Playback thread has already stopped."); 128 | break; 129 | } 130 | } 131 | Err(err) => { 132 | error!("Message channel error: {}", err); 133 | let msg = AudioMessage::EndOfStream; 134 | if tx_pb.send(msg).is_err() { 135 | info!("Playback thread has already stopped."); 136 | } 137 | break; 138 | } 139 | } 140 | if let Ok((diff, new_config)) = rx_pipeconf.try_recv() { 141 | trace!("Message received on config channel"); 142 | match diff { 143 | config::ConfigChange::Pipeline | config::ConfigChange::MixerParameters => { 144 | debug!("Rebuilding pipeline."); 145 | let new_pipeline = 146 | filters::Pipeline::from_config(new_config, processing_params.clone()); 147 | pipeline = new_pipeline; 148 | } 149 | config::ConfigChange::FilterParameters { 150 | filters, 151 | mixers, 152 | processors, 153 | } => { 154 | debug!( 155 | "Updating parameters of filters: {:?}, mixers: {:?}.", 156 | filters, mixers 157 | ); 158 | pipeline.update_parameters(new_config, &filters, &mixers, &processors); 159 | } 160 | config::ConfigChange::Devices => { 161 | let msg = AudioMessage::EndOfStream; 162 | tx_pb.send(msg).unwrap(); 163 | break; 164 | } 165 | _ => {} 166 | }; 167 | }; 168 | } 169 | processing_params.set_processing_load(0.0); 170 | if let Some(h) = thread_handle { 171 | match demote_current_thread_from_real_time(h) { 172 | Ok(_) => { 173 | debug!("Processing thread returned to normal priority.") 174 | } 175 | Err(_) => { 176 | warn!("Could not bring the processing thread back to normal priority.") 177 | } 178 | }; 179 | } 180 | }) 181 | } 182 | -------------------------------------------------------------------------------- /src/generatordevice.rs: -------------------------------------------------------------------------------- 1 | use crate::audiodevice::*; 2 | use crate::config; 3 | 4 | use std::f64::consts::PI; 5 | use std::sync::mpsc; 6 | use std::sync::{Arc, Barrier}; 7 | use std::thread; 8 | 9 | use parking_lot::RwLock; 10 | 11 | use rand::{rngs::SmallRng, SeedableRng}; 12 | use rand_distr::{Distribution, Uniform}; 13 | 14 | use crate::CaptureStatus; 15 | use crate::CommandMessage; 16 | use crate::PrcFmt; 17 | use crate::ProcessingParameters; 18 | use crate::ProcessingState; 19 | use crate::Res; 20 | use crate::StatusMessage; 21 | 22 | struct SineGenerator { 23 | time: f64, 24 | freq: f64, 25 | delta_t: f64, 26 | amplitude: PrcFmt, 27 | } 28 | 29 | impl SineGenerator { 30 | fn new(freq: f64, fs: usize, amplitude: PrcFmt) -> Self { 31 | SineGenerator { 32 | time: 0.0, 33 | freq, 34 | delta_t: 1.0 / fs as f64, 35 | amplitude, 36 | } 37 | } 38 | } 39 | 40 | impl Iterator for SineGenerator { 41 | type Item = PrcFmt; 42 | fn next(&mut self) -> Option { 43 | let output = (self.freq * self.time * PI * 2.).sin() as PrcFmt * self.amplitude; 44 | self.time += self.delta_t; 45 | Some(output) 46 | } 47 | } 48 | 49 | struct SquareGenerator { 50 | time: f64, 51 | freq: f64, 52 | delta_t: f64, 53 | amplitude: PrcFmt, 54 | } 55 | 56 | impl SquareGenerator { 57 | fn new(freq: f64, fs: usize, amplitude: PrcFmt) -> Self { 58 | SquareGenerator { 59 | time: 0.0, 60 | freq, 61 | delta_t: 1.0 / fs as f64, 62 | amplitude, 63 | } 64 | } 65 | } 66 | 67 | impl Iterator for SquareGenerator { 68 | type Item = PrcFmt; 69 | fn next(&mut self) -> Option { 70 | let output = (self.freq * self.time * PI * 2.).sin().signum() as PrcFmt * self.amplitude; 71 | self.time += self.delta_t; 72 | Some(output) 73 | } 74 | } 75 | 76 | struct NoiseGenerator { 77 | rng: SmallRng, 78 | distribution: Uniform, 79 | } 80 | 81 | impl NoiseGenerator { 82 | fn new(amplitude: PrcFmt) -> Self { 83 | let rng = SmallRng::from_entropy(); 84 | let distribution = Uniform::new_inclusive(-amplitude, amplitude); 85 | NoiseGenerator { rng, distribution } 86 | } 87 | } 88 | 89 | impl Iterator for NoiseGenerator { 90 | type Item = PrcFmt; 91 | fn next(&mut self) -> Option { 92 | Some(self.distribution.sample(&mut self.rng)) 93 | } 94 | } 95 | 96 | pub struct GeneratorDevice { 97 | pub chunksize: usize, 98 | pub samplerate: usize, 99 | pub channels: usize, 100 | pub signal: config::Signal, 101 | } 102 | 103 | struct CaptureChannels { 104 | audio: mpsc::SyncSender, 105 | status: crossbeam_channel::Sender, 106 | command: mpsc::Receiver, 107 | } 108 | 109 | struct GeneratorParams { 110 | channels: usize, 111 | chunksize: usize, 112 | capture_status: Arc>, 113 | signal: config::Signal, 114 | samplerate: usize, 115 | } 116 | 117 | fn decibel_to_amplitude(level: PrcFmt) -> PrcFmt { 118 | (10.0 as PrcFmt).powf(level / 20.0) 119 | } 120 | 121 | fn capture_loop(params: GeneratorParams, msg_channels: CaptureChannels) { 122 | debug!("starting generator loop"); 123 | let mut chunk_stats = ChunkStats { 124 | rms: vec![0.0; params.channels], 125 | peak: vec![0.0; params.channels], 126 | }; 127 | let mut sine_gen; 128 | let mut square_gen; 129 | let mut noise_gen; 130 | 131 | let mut generator: &mut dyn Iterator = match params.signal { 132 | config::Signal::Sine { freq, level } => { 133 | sine_gen = SineGenerator::new(freq, params.samplerate, decibel_to_amplitude(level)); 134 | &mut sine_gen as &mut dyn Iterator 135 | } 136 | config::Signal::Square { freq, level } => { 137 | square_gen = SquareGenerator::new(freq, params.samplerate, decibel_to_amplitude(level)); 138 | &mut square_gen as &mut dyn Iterator 139 | } 140 | config::Signal::WhiteNoise { level } => { 141 | noise_gen = NoiseGenerator::new(decibel_to_amplitude(level)); 142 | &mut noise_gen as &mut dyn Iterator 143 | } 144 | }; 145 | 146 | loop { 147 | match msg_channels.command.try_recv() { 148 | Ok(CommandMessage::Exit) => { 149 | debug!("Exit message received, sending EndOfStream"); 150 | let msg = AudioMessage::EndOfStream; 151 | msg_channels.audio.send(msg).unwrap_or(()); 152 | msg_channels 153 | .status 154 | .send(StatusMessage::CaptureDone) 155 | .unwrap_or(()); 156 | break; 157 | } 158 | Ok(CommandMessage::SetSpeed { .. }) => { 159 | warn!("Signal generator does not support rate adjust. Ignoring request."); 160 | } 161 | Err(mpsc::TryRecvError::Empty) => {} 162 | Err(mpsc::TryRecvError::Disconnected) => { 163 | error!("Command channel was closed"); 164 | break; 165 | } 166 | }; 167 | let mut waveform = vec![0.0; params.chunksize]; 168 | for (sample, value) in waveform.iter_mut().zip(&mut generator) { 169 | *sample = value; 170 | } 171 | let mut waveforms = Vec::with_capacity(params.channels); 172 | waveforms.push(waveform); 173 | for _ in 1..params.channels { 174 | waveforms.push(waveforms[0].clone()); 175 | } 176 | 177 | let chunk = AudioChunk::new(waveforms, 1.0, -1.0, params.chunksize, params.chunksize); 178 | 179 | chunk.update_stats(&mut chunk_stats); 180 | { 181 | let mut capture_status = params.capture_status.write(); 182 | capture_status 183 | .signal_rms 184 | .add_record_squared(chunk_stats.rms_linear()); 185 | capture_status 186 | .signal_peak 187 | .add_record(chunk_stats.peak_linear()); 188 | } 189 | let msg = AudioMessage::Audio(chunk); 190 | if msg_channels.audio.send(msg).is_err() { 191 | info!("Processing thread has already stopped."); 192 | break; 193 | } 194 | } 195 | params.capture_status.write().state = ProcessingState::Inactive; 196 | } 197 | 198 | /// Start a capture thread providing AudioMessages via a channel 199 | impl CaptureDevice for GeneratorDevice { 200 | fn start( 201 | &mut self, 202 | channel: mpsc::SyncSender, 203 | barrier: Arc, 204 | status_channel: crossbeam_channel::Sender, 205 | command_channel: mpsc::Receiver, 206 | capture_status: Arc>, 207 | _processing_params: Arc, 208 | ) -> Res>> { 209 | let samplerate = self.samplerate; 210 | let chunksize = self.chunksize; 211 | let channels = self.channels; 212 | let signal = self.signal; 213 | 214 | let handle = thread::Builder::new() 215 | .name("SignalGenerator".to_string()) 216 | .spawn(move || { 217 | let params = GeneratorParams { 218 | signal, 219 | samplerate, 220 | channels, 221 | chunksize, 222 | capture_status, 223 | }; 224 | match status_channel.send(StatusMessage::CaptureReady) { 225 | Ok(()) => {} 226 | Err(_err) => {} 227 | } 228 | barrier.wait(); 229 | let msg_channels = CaptureChannels { 230 | audio: channel, 231 | status: status_channel, 232 | command: command_channel, 233 | }; 234 | debug!("starting captureloop"); 235 | capture_loop(params, msg_channels); 236 | }) 237 | .unwrap(); 238 | Ok(Box::new(handle)) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /testscripts/config_load_test/test_set_config.py: -------------------------------------------------------------------------------- 1 | import time 2 | import camilladsp 3 | import pytest 4 | import os 5 | import signal 6 | import shutil 7 | from subprocess import check_output 8 | from copy import deepcopy 9 | import yaml 10 | import json 11 | 12 | # ---------- Constants ----------- 13 | 14 | CONFIGS = [] 15 | PATHS = [] 16 | for n in range(4): 17 | path = os.path.join(os.path.dirname(__file__), f"conf{n+1}.yml") 18 | PATHS.append(path) 19 | with open(path) as f: 20 | CONFIGS.append(f.read()) 21 | TEMP_PATH = os.path.join(os.path.dirname(__file__), f"temp.yml") 22 | 23 | 24 | # ---------- Test fixtures ----------- 25 | 26 | @pytest.fixture 27 | def camillaclient(): 28 | cdsp = camilladsp.CamillaClient("localhost", 1234) 29 | cdsp.connect() 30 | yield cdsp 31 | 32 | @pytest.fixture 33 | def cdsp_pid(): 34 | res = check_output(["pgrep","camilladsp"]) 35 | pid = int(res.decode()) 36 | return pid 37 | 38 | 39 | # ---------- Helper functions ----------- 40 | 41 | def assert_active(cdsp, expected_desc): 42 | conf = cdsp.config.active() 43 | desc = conf["filters"]["testfilter"]["description"] 44 | assert desc == expected_desc 45 | 46 | def set_via_sighup(pid, index): 47 | # copy config 48 | shutil.copy(PATHS[index], TEMP_PATH) 49 | # send sighup 50 | os.kill(pid, signal.SIGHUP) 51 | 52 | def set_via_path(client, index): 53 | client.config.set_file_path(PATHS[index]) 54 | client.general.reload() 55 | 56 | 57 | # ---------- Test sending a config via ws ----------- 58 | 59 | def test_slow_via_ws(camillaclient): 60 | # Apply them all slowly 61 | print("Changing slowly") 62 | for n in range(4): 63 | print(f"Set conf{n+1}") 64 | camillaclient.config.set_active_raw(CONFIGS[n]) 65 | time.sleep(1) 66 | assert_active(camillaclient, f"nbr {n+1}") 67 | 68 | # Apply them with short delay 69 | @pytest.mark.parametrize("delay,reps", [(0.1, 50), (0.01, 50), (0.001, 50)]) 70 | def test_set_via_ws(camillaclient, delay, reps): 71 | print(f"Changing with {1000*delay} ms delay") 72 | print("Set conf1") 73 | camillaclient.config.set_active_raw(CONFIGS[0]) 74 | time.sleep(1) 75 | assert_active(camillaclient, "nbr 1") 76 | print("Set conf2, 3, 4, 2, 3, 4, ...") 77 | for r in range(reps): 78 | print("repetition", r) 79 | camillaclient.config.set_active_raw(CONFIGS[1]) 80 | time.sleep(delay) 81 | camillaclient.config.set_active_raw(CONFIGS[2]) 82 | time.sleep(delay) 83 | camillaclient.config.set_active_raw(CONFIGS[3]) 84 | time.sleep(0.5) 85 | assert_active(camillaclient, "nbr 4") 86 | 87 | def test_only_pipeline_via_ws(camillaclient): 88 | # Change between configs that only differ in the pipeline 89 | print("Changing slowly") 90 | for n in range(4): 91 | print(f"Set config 1") 92 | conf = yaml.safe_load(CONFIGS[0]) 93 | # conf1 unmodified 94 | camillaclient.config.set_active(conf) 95 | time.sleep(1) 96 | active = camillaclient.config.active() 97 | assert active["pipeline"][0]["names"] == ["testfilter"] 98 | 99 | # conf1 with added filter in pipeline 100 | active["pipeline"][0]["names"] = ["testfilter", "testfilter"] 101 | camillaclient.config.set_active(active) 102 | time.sleep(1) 103 | active = camillaclient.config.active() 104 | assert active["pipeline"][0]["names"] == ["testfilter", "testfilter"] 105 | 106 | # conf1 with empty pipeline 107 | active["pipeline"] = [] 108 | camillaclient.config.set_active(active) 109 | time.sleep(1) 110 | active = camillaclient.config.active() 111 | assert active["pipeline"] == [] 112 | 113 | def test_only_filter_via_ws(camillaclient): 114 | # Change between configs that only differ in the filter defs 115 | print("Changing slowly") 116 | for n in range(4): 117 | print(f"Set config 1") 118 | conf = yaml.safe_load(CONFIGS[0]) 119 | # conf1 unmodified 120 | camillaclient.config.set_active(conf) 121 | time.sleep(1) 122 | active = camillaclient.config.active() 123 | assert active["filters"]["testfilter"]["parameters"]["freq"] == 5000.0 124 | 125 | # conf1 with added filter in pipeline 126 | active["filters"]["testfilter"]["parameters"]["freq"] = 6000.0 127 | camillaclient.config.set_active(active) 128 | time.sleep(1) 129 | active = camillaclient.config.active() 130 | assert active["filters"]["testfilter"]["parameters"]["freq"] == 6000.0 131 | 132 | # conf1 with empty pipeline 133 | active["filters"]["testfilter"]["parameters"]["freq"] = 7000.0 134 | camillaclient.config.set_active(active) 135 | time.sleep(1) 136 | active = camillaclient.config.active() 137 | assert active["filters"]["testfilter"]["parameters"]["freq"] == 7000.0 138 | 139 | def test_only_pipeline_json_via_ws(camillaclient): 140 | # Change between configs that only differ in the pipeline, sent as json 141 | print("Changing slowly") 142 | for n in range(4): 143 | print(f"Set config 1") 144 | conf = yaml.safe_load(CONFIGS[0]) 145 | # conf1 unmodified 146 | camillaclient.config.set_active_json(json.dumps(conf)) 147 | time.sleep(1) 148 | active = camillaclient.config.active() 149 | assert active["pipeline"][0]["names"] == ["testfilter"] 150 | 151 | # conf1 with added filter in pipeline 152 | active["pipeline"][0]["names"] = ["testfilter", "testfilter"] 153 | camillaclient.config.set_active_json(json.dumps(active)) 154 | time.sleep(1) 155 | active = camillaclient.config.active() 156 | assert active["pipeline"][0]["names"] == ["testfilter", "testfilter"] 157 | 158 | # conf1 with empty pipeline 159 | active["pipeline"] = [] 160 | camillaclient.config.set_active_json(json.dumps(active)) 161 | time.sleep(1) 162 | active = camillaclient.config.active() 163 | assert active["pipeline"] == [] 164 | 165 | # ---------- Test changing config by changing config path and reloading ----------- 166 | 167 | def test_slow_via_path(camillaclient): 168 | # Apply them all slowly 169 | print("Changing slowly") 170 | for n in range(4): 171 | print(f"Set conf{n+1}") 172 | camillaclient.config.set_file_path(PATHS[n]) 173 | camillaclient.general.reload() 174 | time.sleep(1) 175 | assert_active(camillaclient, f"nbr {n+1}") 176 | 177 | # Apply them with short delay 178 | @pytest.mark.parametrize("delay,reps", [(0.1, 50), (0.01, 50), (0.001, 50)]) 179 | def test_set_via_path(camillaclient, delay, reps): 180 | print(f"Changing with {1000*delay} ms delay") 181 | print("Set conf1") 182 | set_via_path(camillaclient, 0) 183 | time.sleep(1) 184 | assert_active(camillaclient, "nbr 1") 185 | print("Set conf2, 3, 4, 2, 3, 4, ...") 186 | for r in range(reps): 187 | print("repetition", r) 188 | set_via_path(camillaclient, 1) 189 | time.sleep(delay) 190 | set_via_path(camillaclient, 2) 191 | time.sleep(delay) 192 | set_via_path(camillaclient, 3) 193 | time.sleep(0.5) 194 | assert_active(camillaclient, "nbr 4") 195 | 196 | # ---------- Test changing config by updating the file and sending SIGHUP ----------- 197 | 198 | def test_slow_via_sighup(camillaclient, cdsp_pid): 199 | shutil.copy(PATHS[0], TEMP_PATH) 200 | camillaclient.config.set_file_path(TEMP_PATH) 201 | for n in range(4): 202 | print(f"Set conf{n+1}") 203 | set_via_sighup(cdsp_pid, n) 204 | time.sleep(1) 205 | assert_active(camillaclient, f"nbr {n+1}") 206 | 207 | # Apply them with short delay 208 | @pytest.mark.parametrize("delay,reps", [(0.1, 50), (0.01, 50), (0.001, 50)]) 209 | def test_set_via_sighup(camillaclient, cdsp_pid, delay, reps): 210 | print(f"Changing with {1000*delay} ms delay") 211 | print("Set conf1") 212 | camillaclient.config.set_file_path(TEMP_PATH) 213 | set_via_sighup(cdsp_pid, 0) 214 | time.sleep(1) 215 | assert_active(camillaclient, "nbr 1") 216 | print("Set conf2, 3, 4, 2, 3, 4, ...") 217 | for r in range(reps): 218 | print("repetition", r) 219 | set_via_sighup(cdsp_pid, 1) 220 | time.sleep(delay) 221 | set_via_sighup(cdsp_pid, 2) 222 | time.sleep(delay) 223 | set_via_sighup(cdsp_pid, 3) 224 | time.sleep(0.5) 225 | assert_active(camillaclient, "nbr 4") 226 | -------------------------------------------------------------------------------- /src/alsadevice_buffermanager.rs: -------------------------------------------------------------------------------- 1 | extern crate alsa; 2 | extern crate nix; 3 | use alsa::pcm::{Frames, HwParams, SwParams}; 4 | use std::fmt::Debug; 5 | use std::thread; 6 | use std::time::Duration; 7 | 8 | use crate::config; 9 | use crate::Res; 10 | 11 | pub trait DeviceBufferManager { 12 | // intended for internal use 13 | fn data(&self) -> &DeviceBufferData; 14 | fn data_mut(&mut self) -> &mut DeviceBufferData; 15 | 16 | fn apply_start_threshold(&mut self, swp: &SwParams) -> Res<()>; 17 | 18 | // Calculate a power-of-two buffer size that is large enough to accommodate any changes due to resampling, 19 | // and at least 4 times the minimum period size to avoid random broken pipes. 20 | fn calculate_buffer_size(&self, min_period: Frames) -> Frames { 21 | let data = self.data(); 22 | let mut frames_needed = 3.0 * data.chunksize as f32 / data.resampling_ratio; 23 | if frames_needed < 4.0 * min_period as f32 { 24 | frames_needed = 4.0 * min_period as f32; 25 | debug!( 26 | "Minimum period is {} frames, buffer size is minimum {} frames", 27 | min_period, frames_needed 28 | ); 29 | } 30 | 2.0f32.powi(frames_needed.log2().ceil() as i32) as Frames 31 | } 32 | 33 | // Calculate an alternative buffer size that is 3 multiplied by a power-of-two, 34 | // and at least 4 times the minimum period size to avoid random broken pipes. 35 | // This is for some devices that cannot work with the default setting, 36 | // and when set_buffer_size_near() does not return a working alternative near the requested one. 37 | // Caused by driver bugs? 38 | fn calculate_buffer_size_alt(&self, min_period: Frames) -> Frames { 39 | let data = self.data(); 40 | let mut frames_needed = 3.0 * data.chunksize as f32 / data.resampling_ratio; 41 | if frames_needed < 4.0 * min_period as f32 { 42 | frames_needed = 4.0 * min_period as f32; 43 | debug!( 44 | "Minimum period is {} frames, alternate buffer size is minimum {} frames", 45 | min_period, frames_needed 46 | ); 47 | } 48 | 3 * 2.0f32.powi((frames_needed / 3.0).log2().ceil() as i32) as Frames 49 | } 50 | 51 | // Calculate a buffer size and apply it to a hwp container. Only for use when opening a device. 52 | fn apply_buffer_size(&mut self, hwp: &HwParams) -> Res<()> { 53 | let min_period = hwp.get_period_size_min().unwrap_or(0); 54 | let buffer_frames = self.calculate_buffer_size(min_period); 55 | let alt_buffer_frames = self.calculate_buffer_size_alt(min_period); 56 | let data = self.data_mut(); 57 | debug!("Setting buffer size to {} frames", buffer_frames); 58 | match hwp.set_buffer_size_near(buffer_frames) { 59 | Ok(frames) => { 60 | data.bufsize = frames; 61 | } 62 | Err(_) => { 63 | debug!( 64 | "Device did not accept a buffer size of {} frames, trying again with {}", 65 | buffer_frames, alt_buffer_frames 66 | ); 67 | data.bufsize = hwp.set_buffer_size_near(alt_buffer_frames)?; 68 | } 69 | } 70 | debug!("Device is using a buffer size of {} frames", data.bufsize); 71 | Ok(()) 72 | } 73 | 74 | // Calculate a period size and apply it to a hwp container. Only for use when opening a device, after setting buffer size. 75 | fn apply_period_size(&mut self, hwp: &HwParams) -> Res<()> { 76 | let data = self.data_mut(); 77 | let period_frames = data.bufsize / 8; 78 | debug!("Setting period size to {} frames", period_frames); 79 | match hwp.set_period_size_near(period_frames, alsa::ValueOr::Nearest) { 80 | Ok(frames) => { 81 | data.period = frames; 82 | } 83 | Err(_) => { 84 | let alt_period_frames = 85 | 3 * 2.0f32.powi((period_frames as f32 / 2.0).log2().ceil() as i32) as Frames; 86 | debug!( 87 | "Device did not accept a period size of {} frames, trying again with {}", 88 | period_frames, alt_period_frames 89 | ); 90 | data.period = 91 | hwp.set_period_size_near(alt_period_frames, alsa::ValueOr::Nearest)?; 92 | } 93 | } 94 | debug!("Device is using a period size of {} frames", data.period); 95 | Ok(()) 96 | } 97 | 98 | // Update avail_min so set target for snd_pcm_wait. 99 | fn apply_avail_min(&mut self, swp: &SwParams) -> Res<()> { 100 | let data = self.data_mut(); 101 | // maximum timing safety - headroom for one io_size only 102 | if data.io_size > data.bufsize { 103 | let msg = format!("Trying to set avail_min to {}, must be smaller than or equal to device buffer size of {}", 104 | data.io_size, data.bufsize); 105 | error!("{}", msg); 106 | return Err(config::ConfigError::new(&msg).into()); 107 | } 108 | data.avail_min = data.io_size; 109 | swp.set_avail_min(data.avail_min)?; 110 | Ok(()) 111 | } 112 | 113 | fn update_io_size(&mut self, swp: &SwParams, io_size: Frames) -> Res<()> { 114 | let data = self.data_mut(); 115 | data.io_size = io_size; 116 | // must update avail_min 117 | self.apply_avail_min(swp)?; 118 | // must update threshold 119 | self.apply_start_threshold(swp)?; 120 | Ok(()) 121 | } 122 | 123 | fn frames_to_stall(&self) -> Frames { 124 | let data = self.data(); 125 | // +1 to make sure the device really stalls 126 | data.bufsize - data.avail_min + 1 127 | } 128 | 129 | fn current_delay(&self, avail: Frames) -> Frames; 130 | } 131 | 132 | #[derive(Debug)] 133 | pub struct DeviceBufferData { 134 | bufsize: Frames, 135 | period: Frames, 136 | threshold: Frames, 137 | avail_min: Frames, 138 | io_size: Frames, /* size of read/write block */ 139 | chunksize: Frames, 140 | resampling_ratio: f32, 141 | } 142 | 143 | impl DeviceBufferData { 144 | pub fn buffersize(&self) -> Frames { 145 | self.bufsize 146 | } 147 | } 148 | 149 | #[derive(Debug)] 150 | pub struct CaptureBufferManager { 151 | pub data: DeviceBufferData, 152 | } 153 | 154 | impl CaptureBufferManager { 155 | pub fn new(chunksize: Frames, resampling_ratio: f32) -> Self { 156 | let init_io_size = (chunksize as f32 / resampling_ratio) as Frames; 157 | CaptureBufferManager { 158 | data: DeviceBufferData { 159 | bufsize: 0, 160 | period: 0, 161 | threshold: 0, 162 | avail_min: 0, 163 | io_size: init_io_size, 164 | resampling_ratio, 165 | chunksize, 166 | }, 167 | } 168 | } 169 | } 170 | 171 | impl DeviceBufferManager for CaptureBufferManager { 172 | fn data(&self) -> &DeviceBufferData { 173 | &self.data 174 | } 175 | 176 | fn data_mut(&mut self) -> &mut DeviceBufferData { 177 | &mut self.data 178 | } 179 | 180 | fn apply_start_threshold(&mut self, swp: &SwParams) -> Res<()> { 181 | // immediate start after pcmdev.prepare 182 | let threshold = 0; 183 | swp.set_start_threshold(threshold)?; 184 | self.data.threshold = threshold; 185 | Ok(()) 186 | } 187 | 188 | fn current_delay(&self, avail: Frames) -> Frames { 189 | avail 190 | } 191 | } 192 | 193 | #[derive(Debug)] 194 | pub struct PlaybackBufferManager { 195 | pub data: DeviceBufferData, 196 | target_level: Frames, 197 | } 198 | 199 | impl PlaybackBufferManager { 200 | pub fn new(chunksize: Frames, target_level: Frames) -> Self { 201 | PlaybackBufferManager { 202 | data: DeviceBufferData { 203 | bufsize: 0, 204 | period: 0, 205 | threshold: 0, 206 | avail_min: 0, 207 | io_size: chunksize, 208 | resampling_ratio: 1.0, 209 | chunksize, 210 | }, 211 | target_level, 212 | } 213 | } 214 | 215 | pub fn sleep_for_target_delay(&self, millis_per_frame: f32) { 216 | let sleep_millis = (self.target_level as f32 * millis_per_frame) as u64; 217 | trace!( 218 | "Sleeping for {} frames = {} ms", 219 | self.target_level, 220 | sleep_millis 221 | ); 222 | thread::sleep(Duration::from_millis(sleep_millis)); 223 | } 224 | } 225 | 226 | impl DeviceBufferManager for PlaybackBufferManager { 227 | fn data(&self) -> &DeviceBufferData { 228 | &self.data 229 | } 230 | 231 | fn data_mut(&mut self) -> &mut DeviceBufferData { 232 | &mut self.data 233 | } 234 | 235 | fn apply_start_threshold(&mut self, swp: &SwParams) -> Res<()> { 236 | // start on first write of any size 237 | let threshold = 1; 238 | swp.set_start_threshold(threshold)?; 239 | self.data.threshold = threshold; 240 | Ok(()) 241 | } 242 | 243 | fn current_delay(&self, avail: Frames) -> Frames { 244 | self.data.bufsize - avail 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/compressor.rs: -------------------------------------------------------------------------------- 1 | use crate::audiodevice::AudioChunk; 2 | use crate::config; 3 | use crate::filters::Processor; 4 | use crate::limiter::Limiter; 5 | use crate::PrcFmt; 6 | use crate::Res; 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct Compressor { 10 | pub name: String, 11 | pub channels: usize, 12 | pub monitor_channels: Vec, 13 | pub process_channels: Vec, 14 | pub attack: PrcFmt, 15 | pub release: PrcFmt, 16 | pub threshold: PrcFmt, 17 | pub factor: PrcFmt, 18 | pub makeup_gain: PrcFmt, 19 | pub limiter: Option, 20 | pub samplerate: usize, 21 | pub scratch: Vec, 22 | pub prev_loudness: PrcFmt, 23 | } 24 | 25 | impl Compressor { 26 | /// Creates a Compressor from a config struct 27 | pub fn from_config( 28 | name: &str, 29 | config: config::CompressorParameters, 30 | samplerate: usize, 31 | chunksize: usize, 32 | ) -> Self { 33 | let name = name.to_string(); 34 | let channels = config.channels; 35 | let srate = samplerate as PrcFmt; 36 | let mut monitor_channels = config.monitor_channels(); 37 | if monitor_channels.is_empty() { 38 | for n in 0..channels { 39 | monitor_channels.push(n); 40 | } 41 | } 42 | let mut process_channels = config.process_channels(); 43 | if process_channels.is_empty() { 44 | for n in 0..channels { 45 | process_channels.push(n); 46 | } 47 | } 48 | let attack = (-1.0 / srate / config.attack).exp(); 49 | let release = (-1.0 / srate / config.release).exp(); 50 | let clip_limit = config 51 | .clip_limit 52 | .map(|lim| (10.0 as PrcFmt).powf(lim / 20.0)); 53 | 54 | let scratch = vec![0.0; chunksize]; 55 | 56 | debug!("Creating compressor '{}', channels: {}, monitor_channels: {:?}, process_channels: {:?}, attack: {}, release: {}, threshold: {}, factor: {}, makeup_gain: {}, soft_clip: {}, clip_limit: {:?}", 57 | name, channels, process_channels, monitor_channels, attack, release, config.threshold, config.factor, config.makeup_gain(), config.soft_clip(), clip_limit); 58 | let limiter = if let Some(limit) = config.clip_limit { 59 | let limitconf = config::LimiterParameters { 60 | clip_limit: limit, 61 | soft_clip: config.soft_clip, 62 | }; 63 | Some(Limiter::from_config("Limiter", limitconf)) 64 | } else { 65 | None 66 | }; 67 | 68 | Compressor { 69 | name, 70 | channels, 71 | monitor_channels, 72 | process_channels, 73 | attack, 74 | release, 75 | threshold: config.threshold, 76 | factor: config.factor, 77 | makeup_gain: config.makeup_gain(), 78 | limiter, 79 | samplerate, 80 | scratch, 81 | prev_loudness: -100.0, 82 | } 83 | } 84 | 85 | /// Sum all channels that are included in loudness monitoring, store result in self.scratch 86 | fn sum_monitor_channels(&mut self, input: &AudioChunk) { 87 | let ch = self.monitor_channels[0]; 88 | self.scratch.copy_from_slice(&input.waveforms[ch]); 89 | for ch in self.monitor_channels.iter().skip(1) { 90 | for (acc, val) in self.scratch.iter_mut().zip(input.waveforms[*ch].iter()) { 91 | *acc += *val; 92 | } 93 | } 94 | } 95 | 96 | /// Estimate loudness, store result in self.scratch 97 | fn estimate_loudness(&mut self) { 98 | for val in self.scratch.iter_mut() { 99 | // convert to dB 100 | *val = 20.0 * (val.abs() + 1.0e-9).log10(); 101 | if *val >= self.prev_loudness { 102 | *val = self.attack * self.prev_loudness + (1.0 - self.attack) * *val; 103 | } else { 104 | *val = self.release * self.prev_loudness + (1.0 - self.release) * *val; 105 | } 106 | self.prev_loudness = *val; 107 | } 108 | } 109 | 110 | /// Calculate linear gain, store result in self.scratch 111 | fn calculate_linear_gain(&mut self) { 112 | for val in self.scratch.iter_mut() { 113 | if *val > self.threshold { 114 | *val = -(*val - self.threshold) * (self.factor - 1.0) / self.factor; 115 | } else { 116 | *val = 0.0; 117 | } 118 | *val += self.makeup_gain; 119 | *val = (10.0 as PrcFmt).powf(*val / 20.0); 120 | } 121 | } 122 | 123 | fn apply_gain(&self, input: &mut [PrcFmt]) { 124 | for (val, gain) in input.iter_mut().zip(self.scratch.iter()) { 125 | *val *= gain; 126 | } 127 | } 128 | 129 | fn apply_limiter(&self, input: &mut [PrcFmt]) { 130 | if let Some(limiter) = &self.limiter { 131 | limiter.apply_clip(input); 132 | } 133 | } 134 | } 135 | 136 | impl Processor for Compressor { 137 | fn name(&self) -> &str { 138 | &self.name 139 | } 140 | 141 | /// Apply a Compressor to an AudioChunk, modifying it in-place. 142 | fn process_chunk(&mut self, input: &mut AudioChunk) -> Res<()> { 143 | self.sum_monitor_channels(input); 144 | self.estimate_loudness(); 145 | self.calculate_linear_gain(); 146 | for ch in self.process_channels.iter() { 147 | self.apply_gain(&mut input.waveforms[*ch]); 148 | self.apply_limiter(&mut input.waveforms[*ch]); 149 | } 150 | Ok(()) 151 | } 152 | 153 | fn update_parameters(&mut self, config: config::Processor) { 154 | if let config::Processor::Compressor { 155 | parameters: config, .. 156 | } = config 157 | { 158 | let channels = config.channels; 159 | let srate = self.samplerate as PrcFmt; 160 | let mut monitor_channels = config.monitor_channels(); 161 | if monitor_channels.is_empty() { 162 | for n in 0..channels { 163 | monitor_channels.push(n); 164 | } 165 | } 166 | let mut process_channels = config.process_channels(); 167 | if process_channels.is_empty() { 168 | for n in 0..channels { 169 | process_channels.push(n); 170 | } 171 | } 172 | let attack = (-1.0 / srate / config.attack).exp(); 173 | let release = (-1.0 / srate / config.release).exp(); 174 | let clip_limit = config 175 | .clip_limit 176 | .map(|lim| (10.0 as PrcFmt).powf(lim / 20.0)); 177 | 178 | let limiter = if let Some(limit) = config.clip_limit { 179 | let limitconf = config::LimiterParameters { 180 | clip_limit: limit, 181 | soft_clip: config.soft_clip, 182 | }; 183 | Some(Limiter::from_config("Limiter", limitconf)) 184 | } else { 185 | None 186 | }; 187 | 188 | self.monitor_channels = monitor_channels; 189 | self.process_channels = process_channels; 190 | self.attack = attack; 191 | self.release = release; 192 | self.threshold = config.threshold; 193 | self.factor = config.factor; 194 | self.makeup_gain = config.makeup_gain(); 195 | self.limiter = limiter; 196 | 197 | debug!("Updated compressor '{}', monitor_channels: {:?}, process_channels: {:?}, attack: {}, release: {}, threshold: {}, factor: {}, makeup_gain: {}, soft_clip: {}, clip_limit: {:?}", 198 | self.name, self.process_channels, self.monitor_channels, attack, release, config.threshold, config.factor, config.makeup_gain(), config.soft_clip(), clip_limit); 199 | } else { 200 | // This should never happen unless there is a bug somewhere else 201 | panic!("Invalid config change!"); 202 | } 203 | } 204 | } 205 | 206 | /// Validate the compressor config, to give a helpful message intead of a panic. 207 | pub fn validate_compressor(config: &config::CompressorParameters) -> Res<()> { 208 | let channels = config.channels; 209 | if config.attack <= 0.0 { 210 | let msg = "Attack value must be larger than zero."; 211 | return Err(config::ConfigError::new(msg).into()); 212 | } 213 | if config.release <= 0.0 { 214 | let msg = "Release value must be larger than zero."; 215 | return Err(config::ConfigError::new(msg).into()); 216 | } 217 | for ch in config.monitor_channels().iter() { 218 | if *ch >= channels { 219 | let msg = format!( 220 | "Invalid monitor channel: {}, max is: {}.", 221 | *ch, 222 | channels - 1 223 | ); 224 | return Err(config::ConfigError::new(&msg).into()); 225 | } 226 | } 227 | for ch in config.process_channels().iter() { 228 | if *ch >= channels { 229 | let msg = format!( 230 | "Invalid channel to process: {}, max is: {}.", 231 | *ch, 232 | channels - 1 233 | ); 234 | return Err(config::ConfigError::new(&msg).into()); 235 | } 236 | } 237 | Ok(()) 238 | } 239 | -------------------------------------------------------------------------------- /src/loudness.rs: -------------------------------------------------------------------------------- 1 | use crate::basicfilters::Gain; 2 | use crate::biquad; 3 | use crate::config; 4 | use crate::filters::Filter; 5 | use std::sync::Arc; 6 | 7 | use crate::PrcFmt; 8 | use crate::ProcessingParameters; 9 | use crate::Res; 10 | 11 | pub struct Loudness { 12 | pub name: String, 13 | current_volume: PrcFmt, 14 | processing_params: Arc, 15 | reference_level: f32, 16 | high_boost: f32, 17 | low_boost: f32, 18 | high_biquad: biquad::Biquad, 19 | low_biquad: biquad::Biquad, 20 | fader: usize, 21 | active: bool, 22 | gain: Option, 23 | } 24 | 25 | fn rel_boost(level: f32, reference: f32) -> f32 { 26 | let rel_boost = (reference - level) / 20.0; 27 | rel_boost.clamp(0.0, 1.0) 28 | } 29 | 30 | impl Loudness { 31 | pub fn from_config( 32 | name: &str, 33 | conf: config::LoudnessParameters, 34 | samplerate: usize, 35 | processing_params: Arc, 36 | ) -> Self { 37 | info!("Create loudness filter"); 38 | let fader = conf.fader(); 39 | let current_volume = processing_params.target_volume(fader); 40 | let relboost = rel_boost(current_volume, conf.reference_level); 41 | let active = relboost > 0.01; 42 | let high_boost = (relboost * conf.high_boost()) as PrcFmt; 43 | let low_boost = (relboost * conf.low_boost()) as PrcFmt; 44 | let highshelf_conf = config::BiquadParameters::Highshelf(config::ShelfSteepness::Slope { 45 | freq: 3500.0, 46 | slope: 12.0, 47 | gain: high_boost, 48 | }); 49 | let lowshelf_conf = config::BiquadParameters::Lowshelf(config::ShelfSteepness::Slope { 50 | freq: 70.0, 51 | slope: 12.0, 52 | gain: low_boost, 53 | }); 54 | let gain = if conf.attenuate_mid() { 55 | let max_gain = low_boost.max(high_boost); 56 | let gain_params = config::GainParameters { 57 | gain: -max_gain, 58 | inverted: None, 59 | mute: None, 60 | scale: None, 61 | }; 62 | Some(Gain::from_config("midgain", gain_params)) 63 | } else { 64 | None 65 | }; 66 | 67 | let high_biquad_coeffs = 68 | biquad::BiquadCoefficients::from_config(samplerate, highshelf_conf); 69 | let low_biquad_coeffs = biquad::BiquadCoefficients::from_config(samplerate, lowshelf_conf); 70 | let high_biquad = biquad::Biquad::new("highshelf", samplerate, high_biquad_coeffs); 71 | let low_biquad = biquad::Biquad::new("lowshelf", samplerate, low_biquad_coeffs); 72 | Loudness { 73 | name: name.to_string(), 74 | current_volume: current_volume as PrcFmt, 75 | reference_level: conf.reference_level, 76 | high_boost: conf.high_boost(), 77 | low_boost: conf.low_boost(), 78 | high_biquad, 79 | low_biquad, 80 | processing_params, 81 | fader, 82 | active, 83 | gain, 84 | } 85 | } 86 | } 87 | 88 | impl Filter for Loudness { 89 | fn name(&self) -> &str { 90 | &self.name 91 | } 92 | 93 | fn process_waveform(&mut self, waveform: &mut [PrcFmt]) -> Res<()> { 94 | let shared_vol = self.processing_params.current_volume(self.fader); 95 | 96 | // Volume setting changed 97 | if (shared_vol - self.current_volume as f32).abs() > 0.01 { 98 | self.current_volume = shared_vol as PrcFmt; 99 | let relboost = rel_boost(self.current_volume as f32, self.reference_level); 100 | let high_boost = (relboost * self.high_boost) as PrcFmt; 101 | let low_boost = (relboost * self.low_boost) as PrcFmt; 102 | self.active = relboost > 0.001; 103 | debug!( 104 | "Updating loudness biquads, relative boost {}%", 105 | 100.0 * relboost 106 | ); 107 | let highshelf_conf = 108 | config::BiquadParameters::Highshelf(config::ShelfSteepness::Slope { 109 | freq: 3500.0, 110 | slope: 12.0, 111 | gain: high_boost, 112 | }); 113 | let lowshelf_conf = config::BiquadParameters::Lowshelf(config::ShelfSteepness::Slope { 114 | freq: 70.0, 115 | slope: 12.0, 116 | gain: low_boost, 117 | }); 118 | self.high_biquad.update_parameters(config::Filter::Biquad { 119 | parameters: highshelf_conf, 120 | description: None, 121 | }); 122 | self.low_biquad.update_parameters(config::Filter::Biquad { 123 | parameters: lowshelf_conf, 124 | description: None, 125 | }); 126 | if let Some(gain) = &mut self.gain { 127 | let max_gain = low_boost.max(high_boost); 128 | let gain_params = config::GainParameters { 129 | gain: -max_gain, 130 | inverted: None, 131 | mute: None, 132 | scale: None, 133 | }; 134 | gain.update_parameters(config::Filter::Gain { 135 | description: None, 136 | parameters: gain_params, 137 | }); 138 | } 139 | } 140 | if self.active { 141 | trace!("Applying loudness biquads"); 142 | self.high_biquad.process_waveform(waveform).unwrap(); 143 | self.low_biquad.process_waveform(waveform).unwrap(); 144 | if let Some(gain) = &mut self.gain { 145 | gain.process_waveform(waveform).unwrap(); 146 | } 147 | } 148 | Ok(()) 149 | } 150 | 151 | fn update_parameters(&mut self, conf: config::Filter) { 152 | if let config::Filter::Loudness { 153 | parameters: conf, .. 154 | } = conf 155 | { 156 | self.fader = conf.fader(); 157 | let current_volume = self.processing_params.current_volume(self.fader); 158 | let relboost = rel_boost(current_volume, conf.reference_level); 159 | let high_boost = (relboost * conf.high_boost()) as PrcFmt; 160 | let low_boost = (relboost * conf.low_boost()) as PrcFmt; 161 | self.active = relboost > 0.001; 162 | let highshelf_conf = 163 | config::BiquadParameters::Highshelf(config::ShelfSteepness::Slope { 164 | freq: 3500.0, 165 | slope: 12.0, 166 | gain: high_boost, 167 | }); 168 | let lowshelf_conf = config::BiquadParameters::Lowshelf(config::ShelfSteepness::Slope { 169 | freq: 70.0, 170 | slope: 12.0, 171 | gain: low_boost, 172 | }); 173 | self.high_biquad.update_parameters(config::Filter::Biquad { 174 | parameters: highshelf_conf, 175 | description: None, 176 | }); 177 | self.low_biquad.update_parameters(config::Filter::Biquad { 178 | parameters: lowshelf_conf, 179 | description: None, 180 | }); 181 | if conf.attenuate_mid() { 182 | let max_gain = low_boost.max(high_boost); 183 | let gain_params = config::GainParameters { 184 | gain: -max_gain, 185 | inverted: None, 186 | mute: None, 187 | scale: None, 188 | }; 189 | if let Some(gain) = &mut self.gain { 190 | gain.update_parameters(config::Filter::Gain { 191 | description: None, 192 | parameters: gain_params, 193 | }); 194 | } else { 195 | self.gain = Some(Gain::from_config("midgain", gain_params)) 196 | } 197 | } else { 198 | self.gain = None 199 | } 200 | 201 | self.reference_level = conf.reference_level; 202 | self.high_boost = conf.high_boost(); 203 | self.low_boost = conf.low_boost(); 204 | } else { 205 | // This should never happen unless there is a bug somewhere else 206 | panic!("Invalid config change!"); 207 | } 208 | } 209 | } 210 | 211 | /// Validate a Loudness config. 212 | pub fn validate_config(conf: &config::LoudnessParameters) -> Res<()> { 213 | if conf.reference_level > 20.0 { 214 | return Err(config::ConfigError::new("Reference level must be less than 20").into()); 215 | } else if conf.reference_level < -100.0 { 216 | return Err(config::ConfigError::new("Reference level must be higher than -100").into()); 217 | } else if conf.high_boost() < 0.0 { 218 | return Err(config::ConfigError::new("High boost cannot be less than 0").into()); 219 | } else if conf.low_boost() < 0.0 { 220 | return Err(config::ConfigError::new("Low boost cannot be less than 0").into()); 221 | } else if conf.high_boost() > 20.0 { 222 | return Err(config::ConfigError::new("High boost cannot be larger than 20").into()); 223 | } else if conf.low_boost() > 20.0 { 224 | return Err(config::ConfigError::new("Low boost cannot be larger than 20").into()); 225 | } 226 | Ok(()) 227 | } 228 | -------------------------------------------------------------------------------- /stepbystep.md: -------------------------------------------------------------------------------- 1 | # Building a config file step by step 2 | Here we'll build up a full CamillaDSP config file, step by step, to help 3 | making it easier to understand how things are connected. 4 | This will be a simple 2-way crossover with 2 channels in and 4 out. 5 | 6 | 7 | ## Devices 8 | First we need to define the input and output devices. Here let's assume 9 | we already figured out all the Loopbacks etc and already know the devices to use. 10 | We need to decide a sample rate, let's go with 44100. 11 | For chunksize, 1024 is a good starting point. 12 | This gives a fairly short delay, and low risk of buffer underruns. 13 | The best sample format this playback device supports is 32 bit integer so let's put that. 14 | The Loopback capture device supports all sample formats so let's just pick a good one. 15 | ```yaml 16 | --- 17 | title: "Example crossover" 18 | description: "An example of a simple 2-way crossover" 19 | 20 | devices: 21 | samplerate: 44100 22 | chunksize: 1024 23 | capture: 24 | type: Alsa 25 | channels: 2 26 | device: "hw:Loopback,0,0" 27 | format: S32LE 28 | playback: 29 | type: Alsa 30 | channels: 4 31 | device: "hw:Generic_1" 32 | format: S32LE 33 | ``` 34 | 35 | ## Mixer 36 | We have 2 channels coming in but we need to have 4 going out. 37 | For this to work we have to add two more channels. Thus a mixer is needed. 38 | Lets name it "to4chan" and use output channels 0 & 1 for the woofers, and 2 & 3 for tweeters. 39 | Then we want to leave channels 0 & 1 as they are, and copy 0 -> 2 and 1 -> 3. 40 | Lets start with channels 0 and 1, that should just pass through. 41 | For each output channel we define a list of sources. Here it's a list of one. 42 | So for each output channel X we add a section under "mapping": 43 | ```yaml 44 | mapping: 45 | - dest: X 46 | sources: 47 | - channel: Y 48 | gain: 0 49 | inverted: false 50 | ``` 51 | 52 | To copy we just need to say that output channel 0 should have channel 0 as source, with gain 0. 53 | This part becomes: 54 | ```yaml 55 | mixers: 56 | to4chan: 57 | description: "Expand 2 channels to 4" 58 | channels: 59 | in: 2 60 | out: 4 61 | mapping: 62 | - dest: 0 63 | sources: 64 | - channel: 0 65 | gain: 0 66 | inverted: false 67 | - dest: 1 68 | sources: 69 | - channel: 1 70 | gain: 0 71 | inverted: false 72 | ``` 73 | 74 | Then we add the two new channels, by copying from channels 0 and 1: 75 | ```yaml 76 | mixers: 77 | to4chan: 78 | description: "Expand 2 channels to 4" 79 | channels: 80 | in: 2 81 | out: 4 82 | mapping: 83 | - dest: 0 84 | sources: 85 | - channel: 0 86 | gain: 0 87 | inverted: false 88 | - dest: 1 89 | sources: 90 | - channel: 1 91 | gain: 0 92 | inverted: false 93 | - dest: 2 <---- new! 94 | sources: 95 | - channel: 0 96 | gain: 0 97 | inverted: false 98 | - dest: 3 <---- new! 99 | sources: 100 | - channel: 1 101 | gain: 0 102 | inverted: false 103 | ``` 104 | 105 | ## Pipeline 106 | We now have all we need to build a working pipeline. 107 | It won't do any filtering yet so this is only for a quick test. 108 | We only need a single step in the pipeline, for the "to4chan" mixer. 109 | ```yaml 110 | pipeline: 111 | - type: Mixer 112 | name: to4chan 113 | ``` 114 | Put everything together, and run it. It should work and give unfiltered output on 4 channels. 115 | 116 | 117 | ## Filters 118 | The poor tweeters don't like the full range signal so we need lowpass filters for them. 119 | Left and right should be filtered with the same settings, so a single definition is enough. 120 | Let's use a simple 2nd order Butterworth at 2 kHz and name it "highpass2k". 121 | 122 | Create a "filters" section like this: 123 | ```yaml 124 | filters: 125 | highpass2k: 126 | type: Biquad 127 | parameters: 128 | type: Highpass 129 | freq: 2000 130 | q: 0.707 131 | ``` 132 | Next we need to plug this into the pipeline after the mixer. 133 | Thus we need to extend the pipeline with a "Filter" step, 134 | that acts on the two tweeter channels. 135 | 136 | ```yaml 137 | pipeline: 138 | - type: Mixer 139 | name: to4chan 140 | - type: Filter <---- here! 141 | channels: [2, 3] 142 | names: 143 | - highpass2k 144 | ``` 145 | 146 | When we try this we get properly filtered output for the tweeters on channels 2 and 3. 147 | Let's fix the woofers as well. 148 | Then we need a lowpass filter, so we add a definition to the filters section. 149 | ```yaml 150 | filters: 151 | highpass2k: 152 | type: Biquad 153 | parameters: 154 | type: Highpass 155 | freq: 2000 156 | q: 0.707 157 | lowpass2k: 158 | type: Biquad 159 | parameters: 160 | type: Lowpass 161 | freq: 2000 162 | q: 0.707 163 | ``` 164 | 165 | Then we plug the woofer filter into the pipeline with a new Filter block: 166 | ```yaml 167 | pipeline: 168 | - type: Mixer 169 | name: to4chan 170 | - type: Filter 171 | channels: [2, 3] 172 | names: 173 | - highpass2k 174 | - type: Filter <---- new! 175 | channels: [0, 1] 176 | names: 177 | - lowpass2k 178 | ``` 179 | 180 | We try this and it works, but the sound isn't very nice. 181 | First off, the tweeters have higher sensitivity than the woofers, so they need to be attenuated. 182 | This can be done in the mixer, or via a separate "Gain" filter. 183 | Let's do it in the mixer, and attenuate by 5 dB. 184 | 185 | Just modify the "gain" parameters in the mixer config: 186 | ```yaml 187 | mixers: 188 | to4chan: 189 | description: "Expand 2 channels to 4" 190 | channels: 191 | in: 2 192 | out: 4 193 | mapping: 194 | - dest: 0 195 | sources: 196 | - channel: 0 197 | gain: 0 198 | inverted: false 199 | - dest: 1 200 | sources: 201 | - channel: 1 202 | gain: 0 203 | inverted: false 204 | - dest: 2 205 | sources: 206 | - channel: 0 207 | gain: -5.0 <---- here! 208 | inverted: false 209 | - dest: 3 210 | sources: 211 | - channel: 1 212 | gain: -5.0 <---- here! 213 | inverted: false 214 | ``` 215 | This is far better but we need baffle step compensation as well. 216 | We can do this with a "Highshelf" filter. 217 | The measurements say we need to attenuate by 4 dB from 500 Hz and up. 218 | 219 | Add this filter definition: 220 | ```yaml 221 | bafflestep: 222 | type: Biquad 223 | parameters: 224 | type: Highshelf 225 | freq: 500 226 | slope: 6.0 227 | gain: -4.0 228 | ``` 229 | The baffle step correction should be applied to both woofers and tweeters, 230 | so let's add this in a new Filter step before the Mixer: 231 | ```yaml 232 | pipeline: 233 | - type: Filter \ 234 | channels: [0, 1] | <---- new 235 | names: | 236 | - bafflestep / 237 | - type: Mixer 238 | name: to4chan 239 | - type: Filter 240 | channels: [2, 3] 241 | names: 242 | - highpass2k 243 | - type: Filter 244 | channels: [0, 1] 245 | names: 246 | - lowpass2k 247 | ``` 248 | The last thing we need to do is to adjust the delay between tweeter and woofer. 249 | Measurements tell us we need to delay the tweeter by 0.5 ms. 250 | 251 | Add this filter definition: 252 | ```yaml 253 | tweeterdelay: 254 | type: Delay 255 | parameters: 256 | delay: 0.5 257 | unit: ms 258 | ``` 259 | 260 | Now we add this to the tweeter channels: 261 | ```yaml 262 | pipeline: 263 | - type: Filter 264 | channels: [0, 1] 265 | names: 266 | - bafflestep 267 | - type: Mixer 268 | name: to4chan 269 | - type: Filter 270 | channels: [2, 3] 271 | names: 272 | - highpass2k 273 | - tweeterdelay <---- here! 274 | - type: Filter 275 | channels: [0, 1] 276 | names: 277 | - lowpass2k 278 | ``` 279 | And we are done! 280 | 281 | ## Result 282 | 283 | Now we have all the parts of the configuration. 284 | As a final touch, let's add descriptions to all pipeline steps 285 | while we have things fresh in memory. 286 | 287 | ```yaml 288 | --- 289 | title: "Example crossover" 290 | description: "An example of a simple 2-way crossover" 291 | 292 | devices: 293 | samplerate: 44100 294 | chunksize: 1024 295 | capture: 296 | type: Alsa 297 | channels: 2 298 | device: "hw:Loopback,0,0" 299 | format: S32LE 300 | playback: 301 | type: Alsa 302 | channels: 4 303 | device: "hw:Generic_1" 304 | format: S32LE 305 | 306 | mixers: 307 | to4chan: 308 | description: "Expand 2 channels to 4" 309 | channels: 310 | in: 2 311 | out: 4 312 | mapping: 313 | - dest: 0 314 | sources: 315 | - channel: 0 316 | gain: 0 317 | inverted: false 318 | - dest: 1 319 | sources: 320 | - channel: 1 321 | gain: 0 322 | inverted: false 323 | - dest: 2 324 | sources: 325 | - channel: 0 326 | gain: -5.0 327 | inverted: false 328 | - dest: 3 329 | sources: 330 | - channel: 1 331 | gain: -5.0 332 | inverted: false 333 | 334 | filters: 335 | highpass2k: 336 | type: Biquad 337 | description: "2nd order highpass crossover" 338 | parameters: 339 | type: Highpass 340 | freq: 2000 341 | q: 0.707 342 | lowpass2k: 343 | type: Biquad 344 | description: "2nd order lowpass crossover" 345 | parameters: 346 | type: Lowpass 347 | freq: 2000 348 | q: 0.707 349 | bafflestep: 350 | type: Biquad 351 | description: "Baffle step compensation" 352 | parameters: 353 | type: Highshelf 354 | freq: 500 355 | slope: 6.0 356 | gain: -4.0 357 | tweeterdelay: 358 | type: Delay 359 | description: "Time alignment for tweeters" 360 | parameters: 361 | delay: 0.5 362 | unit: ms 363 | 364 | pipeline: 365 | - type: Filter 366 | description: "Pre-mixer filters" 367 | channels: [0, 1] 368 | names: 369 | - bafflestep 370 | - type: Mixer 371 | name: to4chan 372 | - type: Filter 373 | description: "Highpass for tweeters" 374 | channels: [2, 3] 375 | names: 376 | - highpass2k 377 | - tweeterdelay 378 | - type: Filter 379 | description: "Lowpass for woofers" 380 | channels: [0, 1] 381 | names: 382 | - lowpass2k 383 | ``` 384 | -------------------------------------------------------------------------------- /filter2.txt: -------------------------------------------------------------------------------- 1 | 0.0 2 | 0.0 3 | 0.0 4 | 0.0 5 | 0.0 6 | 0.0 7 | 0.0 8 | 0.0 9 | 0.0 10 | 0.0 11 | 0.0 12 | 0.0 13 | 0.0 14 | 0.0 15 | 0.0 16 | 0.0 17 | 0.0 18 | 0.0 19 | 0.0 20 | 0.0 21 | 0.0 22 | 0.0 23 | 0.0 24 | 0.0 25 | 0.0 26 | 0.0 27 | 0.0 28 | 0.0 29 | 0.0 30 | 0.0 31 | 0.0 32 | 0.0 33 | 0.0 34 | 0.0 35 | 0.0 36 | 0.0 37 | 0.0 38 | 0.0 39 | 0.0 40 | 0.0 41 | 0.0 42 | 0.0 43 | 0.0 44 | 0.0 45 | 0.0 46 | 0.0 47 | 0.0 48 | 0.0 49 | 0.0 50 | 0.0 51 | 0.0 52 | 0.0 53 | 0.0 54 | 0.0 55 | 0.0 56 | 0.0 57 | 0.0 58 | 0.0 59 | 0.0 60 | 0.0 61 | 0.0 62 | 0.0 63 | 0.0 64 | 0.0 65 | 0.0 66 | 0.0 67 | 0.0 68 | 0.0 69 | 0.0 70 | 0.0 71 | 0.0 72 | 0.0 73 | 0.0 74 | 0.0 75 | 0.0 76 | 0.0 77 | 0.0 78 | 0.0 79 | 0.0 80 | 0.0 81 | 0.0 82 | 0.0 83 | 0.0 84 | 0.0 85 | 0.0 86 | 0.0 87 | 0.0 88 | 0.0 89 | 0.0 90 | 0.0 91 | 0.0 92 | 0.0 93 | 0.0 94 | 0.0 95 | 0.0 96 | 0.0 97 | 0.0 98 | 0.0 99 | 0.0 100 | 0.0 101 | 0.0 102 | 0.0 103 | 0.0 104 | 0.0 105 | 0.0 106 | 0.0 107 | 0.0 108 | 0.0 109 | 0.0 110 | 0.0 111 | 0.0 112 | 0.0 113 | 0.0 114 | 0.0 115 | 0.0 116 | 0.0 117 | 0.0 118 | 0.0 119 | 0.0 120 | 0.0 121 | 0.0 122 | 0.0 123 | 0.0 124 | 0.0 125 | 0.0 126 | 0.0 127 | 0.0 128 | 0.0 129 | 0.0 130 | 0.0 131 | 0.0 132 | 0.0 133 | 0.0 134 | 0.0 135 | 0.0 136 | 0.0 137 | 0.0 138 | 0.0 139 | 0.0 140 | 0.0 141 | 0.0 142 | 0.0 143 | 0.0 144 | 0.0 145 | 0.0 146 | 0.0 147 | 0.0 148 | 0.0 149 | 0.0 150 | 0.0 151 | 0.0 152 | 0.0 153 | 0.0 154 | 0.0 155 | 0.0 156 | 0.0 157 | 0.0 158 | 0.0 159 | 0.0 160 | 0.0 161 | 0.0 162 | 0.0 163 | 0.0 164 | 0.0 165 | 0.0 166 | 0.0 167 | 0.0 168 | 0.0 169 | 0.0 170 | 0.0 171 | 0.0 172 | 0.0 173 | 0.0 174 | 0.0 175 | 0.0 176 | 0.0 177 | 0.0 178 | 0.0 179 | 0.0 180 | 0.0 181 | 0.0 182 | 0.0 183 | 0.0 184 | 0.0 185 | 0.0 186 | 0.0 187 | 0.0 188 | 0.0 189 | 0.0 190 | 0.0 191 | 0.0 192 | 0.0 193 | 0.0 194 | 0.0 195 | 0.0 196 | 0.0 197 | 0.0 198 | 0.0 199 | 0.0 200 | 0.0 201 | 0.0 202 | 0.0 203 | 0.0 204 | 0.0 205 | 0.0 206 | 0.0 207 | 0.0 208 | 0.0 209 | 0.0 210 | 0.0 211 | 0.0 212 | 0.0 213 | 0.0 214 | 0.0 215 | 0.0 216 | 0.0 217 | 0.0 218 | 0.0 219 | 0.0 220 | 0.0 221 | 0.0 222 | 0.0 223 | 0.0 224 | 0.0 225 | 0.0 226 | 0.0 227 | 0.0 228 | 0.0 229 | 0.0 230 | 0.0 231 | 0.0 232 | 0.0 233 | 0.0 234 | 0.0 235 | 0.0 236 | 0.0 237 | 0.0 238 | 0.0 239 | 0.0 240 | 0.0 241 | 0.0 242 | 0.0 243 | 0.0 244 | 0.0 245 | 0.0 246 | 0.0 247 | 0.0 248 | 0.0 249 | 0.0 250 | 0.0 251 | 0.0 252 | 0.0 253 | 0.0 254 | 0.0 255 | 0.0 256 | 0.0 257 | 0.0 258 | 0.0 259 | 0.0 260 | 0.0 261 | 0.0 262 | 0.0 263 | 0.0 264 | 0.0 265 | 0.0 266 | 0.0 267 | 0.0 268 | 0.0 269 | 0.0 270 | 0.0 271 | 0.0 272 | 0.0 273 | 0.0 274 | 0.0 275 | 0.0 276 | 0.0 277 | 0.0 278 | 0.0 279 | 0.0 280 | 0.0 281 | 0.0 282 | 0.0 283 | 0.0 284 | 0.0 285 | 0.0 286 | 0.0 287 | 0.0 288 | 0.0 289 | 0.0 290 | 0.0 291 | 0.0 292 | 0.0 293 | 0.0 294 | 0.0 295 | 0.0 296 | 0.0 297 | 0.0 298 | 0.0 299 | 0.0 300 | 0.0 301 | 0.0 302 | 0.0 303 | 0.0 304 | 0.0 305 | 0.0 306 | 0.0 307 | 0.0 308 | 0.0 309 | 0.0 310 | 0.0 311 | 0.0 312 | 0.0 313 | 0.0 314 | 0.0 315 | 0.0 316 | 0.0 317 | 0.0 318 | 0.0 319 | 0.0 320 | 0.0 321 | 0.0 322 | 0.0 323 | 0.0 324 | 0.0 325 | 0.0 326 | 0.0 327 | 0.0 328 | 0.0 329 | 0.0 330 | 0.0 331 | 0.0 332 | 0.0 333 | 0.0 334 | 0.0 335 | 0.0 336 | 0.0 337 | 0.0 338 | 0.0 339 | 0.0 340 | 0.0 341 | 0.0 342 | 0.0 343 | 0.0 344 | 0.0 345 | 0.0 346 | 0.0 347 | 0.0 348 | 0.0 349 | 0.0 350 | 0.0 351 | 0.0 352 | 0.0 353 | 0.0 354 | 0.0 355 | 0.0 356 | 0.0 357 | 0.0 358 | 0.0 359 | 0.0 360 | 0.0 361 | 0.0 362 | 0.0 363 | 0.0 364 | 0.0 365 | 0.0 366 | 0.0 367 | 0.0 368 | 0.0 369 | 0.0 370 | 0.0 371 | 0.0 372 | 0.0 373 | 0.0 374 | 0.0 375 | 0.0 376 | 0.0 377 | 0.0 378 | 0.0 379 | 0.0 380 | 0.0 381 | 0.0 382 | 0.0 383 | 0.0 384 | 0.0 385 | 0.0 386 | 0.0 387 | 0.0 388 | 0.0 389 | 0.0 390 | 0.0 391 | 0.0 392 | 0.0 393 | 0.0 394 | 0.0 395 | 0.0 396 | 0.0 397 | 0.0 398 | 0.0 399 | 0.0 400 | 0.0 401 | 0.0 402 | 0.0 403 | 0.0 404 | 0.0 405 | 0.0 406 | 0.0 407 | 0.0 408 | 0.0 409 | 0.0 410 | 0.0 411 | 0.0 412 | 0.0 413 | 0.0 414 | 0.0 415 | 0.0 416 | 0.0 417 | 0.0 418 | 0.0 419 | 0.0 420 | 0.0 421 | 0.0 422 | 0.0 423 | 0.0 424 | 0.0 425 | 0.0 426 | 0.0 427 | 0.0 428 | 0.0 429 | 0.0 430 | 0.0 431 | 0.0 432 | 0.0 433 | 0.0 434 | 0.0 435 | 0.0 436 | 0.0 437 | 0.0 438 | 0.0 439 | 0.0 440 | 0.0 441 | 0.0 442 | 0.0 443 | 0.0 444 | 0.0 445 | 0.0 446 | 0.0 447 | 0.0 448 | 0.0 449 | 0.0 450 | 0.0 451 | 0.0 452 | 0.0 453 | 0.0 454 | 0.0 455 | 0.0 456 | 0.0 457 | 0.0 458 | 0.0 459 | 0.0 460 | 0.0 461 | 0.0 462 | 0.0 463 | 0.0 464 | 0.0 465 | 0.0 466 | 0.0 467 | 0.0 468 | 0.0 469 | 0.0 470 | 0.0 471 | 0.0 472 | 0.0 473 | 0.0 474 | 0.0 475 | 0.0 476 | 0.0 477 | 0.0 478 | 0.0 479 | 0.0 480 | 0.0 481 | 0.0 482 | 0.0 483 | 0.0 484 | 0.0 485 | 0.0 486 | 0.0 487 | 0.0 488 | 0.0 489 | 0.0 490 | 0.0 491 | 0.0 492 | 0.0 493 | 0.0 494 | 0.0 495 | 0.0 496 | 0.0 497 | 0.0 498 | 0.0 499 | 0.0 500 | 0.0 501 | 0.0 502 | 0.0 503 | 0.0 504 | 0.0 505 | 0.0 506 | 0.0 507 | 0.0 508 | 0.0 509 | 0.0 510 | 0.0 511 | 0.0 512 | 1.0 513 | 0.0 514 | 0.0 515 | 0.0 516 | 0.0 517 | 0.0 518 | 0.0 519 | 0.0 520 | 0.0 521 | 0.0 522 | 0.0 523 | 0.0 524 | 0.0 525 | 0.0 526 | 0.0 527 | 0.0 528 | 0.0 529 | 0.0 530 | 0.0 531 | 0.0 532 | 0.0 533 | 0.0 534 | 0.0 535 | 0.0 536 | 0.0 537 | 0.0 538 | 0.0 539 | 0.0 540 | 0.0 541 | 0.0 542 | 0.0 543 | 0.0 544 | 0.0 545 | 0.0 546 | 0.0 547 | 0.0 548 | 0.0 549 | 0.0 550 | 0.0 551 | 0.0 552 | 0.0 553 | 0.0 554 | 0.0 555 | 0.0 556 | 0.0 557 | 0.0 558 | 0.0 559 | 0.0 560 | 0.0 561 | 0.0 562 | 0.0 563 | 0.0 564 | 0.0 565 | 0.0 566 | 0.0 567 | 0.0 568 | 0.0 569 | 0.0 570 | 0.0 571 | 0.0 572 | 0.0 573 | 0.0 574 | 0.0 575 | 0.0 576 | 0.0 577 | 0.0 578 | 0.0 579 | 0.0 580 | 0.0 581 | 0.0 582 | 0.0 583 | 0.0 584 | 0.0 585 | 0.0 586 | 0.0 587 | 0.0 588 | 0.0 589 | 0.0 590 | 0.0 591 | 0.0 592 | 0.0 593 | 0.0 594 | 0.0 595 | 0.0 596 | 0.0 597 | 0.0 598 | 0.0 599 | 0.0 600 | 0.0 601 | 0.0 602 | 0.0 603 | 0.0 604 | 0.0 605 | 0.0 606 | 0.0 607 | 0.0 608 | 0.0 609 | 0.0 610 | 0.0 611 | 0.0 612 | 0.0 613 | 0.0 614 | 0.0 615 | 0.0 616 | 0.0 617 | 0.0 618 | 0.0 619 | 0.0 620 | 0.0 621 | 0.0 622 | 0.0 623 | 0.0 624 | 0.0 625 | 0.0 626 | 0.0 627 | 0.0 628 | 0.0 629 | 0.0 630 | 0.0 631 | 0.0 632 | 0.0 633 | 0.0 634 | 0.0 635 | 0.0 636 | 0.0 637 | 0.0 638 | 0.0 639 | 0.0 640 | 0.0 641 | 0.0 642 | 0.0 643 | 0.0 644 | 0.0 645 | 0.0 646 | 0.0 647 | 0.0 648 | 0.0 649 | 0.0 650 | 0.0 651 | 0.0 652 | 0.0 653 | 0.0 654 | 0.0 655 | 0.0 656 | 0.0 657 | 0.0 658 | 0.0 659 | 0.0 660 | 0.0 661 | 0.0 662 | 0.0 663 | 0.0 664 | 0.0 665 | 0.0 666 | 0.0 667 | 0.0 668 | 0.0 669 | 0.0 670 | 0.0 671 | 0.0 672 | 0.0 673 | 0.0 674 | 0.0 675 | 0.0 676 | 0.0 677 | 0.0 678 | 0.0 679 | 0.0 680 | 0.0 681 | 0.0 682 | 0.0 683 | 0.0 684 | 0.0 685 | 0.0 686 | 0.0 687 | 0.0 688 | 0.0 689 | 0.0 690 | 0.0 691 | 0.0 692 | 0.0 693 | 0.0 694 | 0.0 695 | 0.0 696 | 0.0 697 | 0.0 698 | 0.0 699 | 0.0 700 | 0.0 701 | 0.0 702 | 0.0 703 | 0.0 704 | 0.0 705 | 0.0 706 | 0.0 707 | 0.0 708 | 0.0 709 | 0.0 710 | 0.0 711 | 0.0 712 | 0.0 713 | 0.0 714 | 0.0 715 | 0.0 716 | 0.0 717 | 0.0 718 | 0.0 719 | 0.0 720 | 0.0 721 | 0.0 722 | 0.0 723 | 0.0 724 | 0.0 725 | 0.0 726 | 0.0 727 | 0.0 728 | 0.0 729 | 0.0 730 | 0.0 731 | 0.0 732 | 0.0 733 | 0.0 734 | 0.0 735 | 0.0 736 | 0.0 737 | 0.0 738 | 0.0 739 | 0.0 740 | 0.0 741 | 0.0 742 | 0.0 743 | 0.0 744 | 0.0 745 | 0.0 746 | 0.0 747 | 0.0 748 | 0.0 749 | 0.0 750 | 0.0 751 | 0.0 752 | 0.0 753 | 0.0 754 | 0.0 755 | 0.0 756 | 0.0 757 | 0.0 758 | 0.0 759 | 0.0 760 | 0.0 761 | 0.0 762 | 0.0 763 | 0.0 764 | 0.0 765 | 0.0 766 | 0.0 767 | 0.0 768 | 0.0 769 | 0.0 770 | 0.0 771 | 0.0 772 | 0.0 773 | 0.0 774 | 0.0 775 | 0.0 776 | 0.0 777 | 0.0 778 | 0.0 779 | 0.0 780 | 0.0 781 | 0.0 782 | 0.0 783 | 0.0 784 | 0.0 785 | 0.0 786 | 0.0 787 | 0.0 788 | 0.0 789 | 0.0 790 | 0.0 791 | 0.0 792 | 0.0 793 | 0.0 794 | 0.0 795 | 0.0 796 | 0.0 797 | 0.0 798 | 0.0 799 | 0.0 800 | 0.0 801 | 0.0 802 | 0.0 803 | 0.0 804 | 0.0 805 | 0.0 806 | 0.0 807 | 0.0 808 | 0.0 809 | 0.0 810 | 0.0 811 | 0.0 812 | 0.0 813 | 0.0 814 | 0.0 815 | 0.0 816 | 0.0 817 | 0.0 818 | 0.0 819 | 0.0 820 | 0.0 821 | 0.0 822 | 0.0 823 | 0.0 824 | 0.0 825 | 0.0 826 | 0.0 827 | 0.0 828 | 0.0 829 | 0.0 830 | 0.0 831 | 0.0 832 | 0.0 833 | 0.0 834 | 0.0 835 | 0.0 836 | 0.0 837 | 0.0 838 | 0.0 839 | 0.0 840 | 0.0 841 | 0.0 842 | 0.0 843 | 0.0 844 | 0.0 845 | 0.0 846 | 0.0 847 | 0.0 848 | 0.0 849 | 0.0 850 | 0.0 851 | 0.0 852 | 0.0 853 | 0.0 854 | 0.0 855 | 0.0 856 | 0.0 857 | 0.0 858 | 0.0 859 | 0.0 860 | 0.0 861 | 0.0 862 | 0.0 863 | 0.0 864 | 0.0 865 | 0.0 866 | 0.0 867 | 0.0 868 | 0.0 869 | 0.0 870 | 0.0 871 | 0.0 872 | 0.0 873 | 0.0 874 | 0.0 875 | 0.0 876 | 0.0 877 | 0.0 878 | 0.0 879 | 0.0 880 | 0.0 881 | 0.0 882 | 0.0 883 | 0.0 884 | 0.0 885 | 0.0 886 | 0.0 887 | 0.0 888 | 0.0 889 | 0.0 890 | 0.0 891 | 0.0 892 | 0.0 893 | 0.0 894 | 0.0 895 | 0.0 896 | 0.0 897 | 0.0 898 | 0.0 899 | 0.0 900 | 0.0 901 | 0.0 902 | 0.0 903 | 0.0 904 | 0.0 905 | 0.0 906 | 0.0 907 | 0.0 908 | 0.0 909 | 0.0 910 | 0.0 911 | 0.0 912 | 0.0 913 | 0.0 914 | 0.0 915 | 0.0 916 | 0.0 917 | 0.0 918 | 0.0 919 | 0.0 920 | 0.0 921 | 0.0 922 | 0.0 923 | 0.0 924 | 0.0 925 | 0.0 926 | 0.0 927 | 0.0 928 | 0.0 929 | 0.0 930 | 0.0 931 | 0.0 932 | 0.0 933 | 0.0 934 | 0.0 935 | 0.0 936 | 0.0 937 | 0.0 938 | 0.0 939 | 0.0 940 | 0.0 941 | 0.0 942 | 0.0 943 | 0.0 944 | 0.0 945 | 0.0 946 | 0.0 947 | 0.0 948 | 0.0 949 | 0.0 950 | 0.0 951 | 0.0 952 | 0.0 953 | 0.0 954 | 0.0 955 | 0.0 956 | 0.0 957 | 0.0 958 | 0.0 959 | 0.0 960 | 0.0 961 | 0.0 962 | 0.0 963 | 0.0 964 | 0.0 965 | 0.0 966 | 0.0 967 | 0.0 968 | 0.0 969 | 0.0 970 | 0.0 971 | 0.0 972 | 0.0 973 | 0.0 974 | 0.0 975 | 0.0 976 | 0.0 977 | 0.0 978 | 0.0 979 | 0.0 980 | 0.0 981 | 0.0 982 | 0.0 983 | 0.0 984 | 0.0 985 | 0.0 986 | 0.0 987 | 0.0 988 | 0.0 989 | 0.0 990 | 0.0 991 | 0.0 992 | 0.0 993 | 0.0 994 | 0.0 995 | 0.0 996 | 0.0 997 | 0.0 998 | 0.0 999 | 0.0 1000 | 0.0 1001 | 0.0 1002 | 0.0 1003 | 0.0 1004 | 0.0 1005 | 0.0 1006 | 0.0 1007 | 0.0 1008 | 0.0 1009 | 0.0 1010 | 0.0 1011 | 0.0 1012 | 0.0 1013 | 0.0 1014 | 0.0 1015 | 0.0 1016 | 0.0 1017 | 0.0 1018 | 0.0 1019 | 0.0 1020 | 0.0 1021 | 0.0 1022 | 0.0 1023 | 0.0 1024 | 0.0 -------------------------------------------------------------------------------- /filter_44100_2.txt: -------------------------------------------------------------------------------- 1 | 0.0 2 | 0.0 3 | 0.0 4 | 0.0 5 | 0.0 6 | 0.0 7 | 0.0 8 | 0.0 9 | 0.0 10 | 0.0 11 | 0.0 12 | 0.0 13 | 0.0 14 | 0.0 15 | 0.0 16 | 0.0 17 | 0.0 18 | 0.0 19 | 0.0 20 | 0.0 21 | 0.0 22 | 0.0 23 | 0.0 24 | 0.0 25 | 0.0 26 | 0.0 27 | 0.0 28 | 0.0 29 | 0.0 30 | 0.0 31 | 0.0 32 | 0.0 33 | 0.0 34 | 0.0 35 | 0.0 36 | 0.0 37 | 0.0 38 | 0.0 39 | 0.0 40 | 0.0 41 | 0.0 42 | 0.0 43 | 0.0 44 | 0.0 45 | 0.0 46 | 0.0 47 | 0.0 48 | 0.0 49 | 0.0 50 | 0.0 51 | 0.0 52 | 0.0 53 | 0.0 54 | 0.0 55 | 0.0 56 | 0.0 57 | 0.0 58 | 0.0 59 | 0.0 60 | 0.0 61 | 0.0 62 | 0.0 63 | 0.0 64 | 0.0 65 | 0.0 66 | 0.0 67 | 0.0 68 | 0.0 69 | 0.0 70 | 0.0 71 | 0.0 72 | 0.0 73 | 0.0 74 | 0.0 75 | 0.0 76 | 0.0 77 | 0.0 78 | 0.0 79 | 0.0 80 | 0.0 81 | 0.0 82 | 0.0 83 | 0.0 84 | 0.0 85 | 0.0 86 | 0.0 87 | 0.0 88 | 0.0 89 | 0.0 90 | 0.0 91 | 0.0 92 | 0.0 93 | 0.0 94 | 0.0 95 | 0.0 96 | 0.0 97 | 0.0 98 | 0.0 99 | 0.0 100 | 0.0 101 | 0.0 102 | 0.0 103 | 0.0 104 | 0.0 105 | 0.0 106 | 0.0 107 | 0.0 108 | 0.0 109 | 0.0 110 | 0.0 111 | 0.0 112 | 0.0 113 | 0.0 114 | 0.0 115 | 0.0 116 | 0.0 117 | 0.0 118 | 0.0 119 | 0.0 120 | 0.0 121 | 0.0 122 | 0.0 123 | 0.0 124 | 0.0 125 | 0.0 126 | 0.0 127 | 0.0 128 | 0.0 129 | 0.0 130 | 0.0 131 | 0.0 132 | 0.0 133 | 0.0 134 | 0.0 135 | 0.0 136 | 0.0 137 | 0.0 138 | 0.0 139 | 0.0 140 | 0.0 141 | 0.0 142 | 0.0 143 | 0.0 144 | 0.0 145 | 0.0 146 | 0.0 147 | 0.0 148 | 0.0 149 | 0.0 150 | 0.0 151 | 0.0 152 | 0.0 153 | 0.0 154 | 0.0 155 | 0.0 156 | 0.0 157 | 0.0 158 | 0.0 159 | 0.0 160 | 0.0 161 | 0.0 162 | 0.0 163 | 0.0 164 | 0.0 165 | 0.0 166 | 0.0 167 | 0.0 168 | 0.0 169 | 0.0 170 | 0.0 171 | 0.0 172 | 0.0 173 | 0.0 174 | 0.0 175 | 0.0 176 | 0.0 177 | 0.0 178 | 0.0 179 | 0.0 180 | 0.0 181 | 0.0 182 | 0.0 183 | 0.0 184 | 0.0 185 | 0.0 186 | 0.0 187 | 0.0 188 | 0.0 189 | 0.0 190 | 0.0 191 | 0.0 192 | 0.0 193 | 0.0 194 | 0.0 195 | 0.0 196 | 0.0 197 | 0.0 198 | 0.0 199 | 0.0 200 | 0.0 201 | 0.0 202 | 0.0 203 | 0.0 204 | 0.0 205 | 0.0 206 | 0.0 207 | 0.0 208 | 0.0 209 | 0.0 210 | 0.0 211 | 0.0 212 | 0.0 213 | 0.0 214 | 0.0 215 | 0.0 216 | 0.0 217 | 0.0 218 | 0.0 219 | 0.0 220 | 0.0 221 | 0.0 222 | 0.0 223 | 0.0 224 | 0.0 225 | 0.0 226 | 0.0 227 | 0.0 228 | 0.0 229 | 0.0 230 | 0.0 231 | 0.0 232 | 0.0 233 | 0.0 234 | 0.0 235 | 0.0 236 | 0.0 237 | 0.0 238 | 0.0 239 | 0.0 240 | 0.0 241 | 0.0 242 | 0.0 243 | 0.0 244 | 0.0 245 | 0.0 246 | 0.0 247 | 0.0 248 | 0.0 249 | 0.0 250 | 0.0 251 | 0.0 252 | 0.0 253 | 0.0 254 | 0.0 255 | 0.0 256 | 0.0 257 | 0.0 258 | 0.0 259 | 0.0 260 | 0.0 261 | 0.0 262 | 0.0 263 | 0.0 264 | 0.0 265 | 0.0 266 | 0.0 267 | 0.0 268 | 0.0 269 | 0.0 270 | 0.0 271 | 0.0 272 | 0.0 273 | 0.0 274 | 0.0 275 | 0.0 276 | 0.0 277 | 0.0 278 | 0.0 279 | 0.0 280 | 0.0 281 | 0.0 282 | 0.0 283 | 0.0 284 | 0.0 285 | 0.0 286 | 0.0 287 | 0.0 288 | 0.0 289 | 0.0 290 | 0.0 291 | 0.0 292 | 0.0 293 | 0.0 294 | 0.0 295 | 0.0 296 | 0.0 297 | 0.0 298 | 0.0 299 | 0.0 300 | 0.0 301 | 0.0 302 | 0.0 303 | 0.0 304 | 0.0 305 | 0.0 306 | 0.0 307 | 0.0 308 | 0.0 309 | 0.0 310 | 0.0 311 | 0.0 312 | 0.0 313 | 0.0 314 | 0.0 315 | 0.0 316 | 0.0 317 | 0.0 318 | 0.0 319 | 0.0 320 | 0.0 321 | 0.0 322 | 0.0 323 | 0.0 324 | 0.0 325 | 0.0 326 | 0.0 327 | 0.0 328 | 0.0 329 | 0.0 330 | 0.0 331 | 0.0 332 | 0.0 333 | 0.0 334 | 0.0 335 | 0.0 336 | 0.0 337 | 0.0 338 | 0.0 339 | 0.0 340 | 0.0 341 | 0.0 342 | 0.0 343 | 0.0 344 | 0.0 345 | 0.0 346 | 0.0 347 | 0.0 348 | 0.0 349 | 0.0 350 | 0.0 351 | 0.0 352 | 0.0 353 | 0.0 354 | 0.0 355 | 0.0 356 | 0.0 357 | 0.0 358 | 0.0 359 | 0.0 360 | 0.0 361 | 0.0 362 | 0.0 363 | 0.0 364 | 0.0 365 | 0.0 366 | 0.0 367 | 0.0 368 | 0.0 369 | 0.0 370 | 0.0 371 | 0.0 372 | 0.0 373 | 0.0 374 | 0.0 375 | 0.0 376 | 0.0 377 | 0.0 378 | 0.0 379 | 0.0 380 | 0.0 381 | 0.0 382 | 0.0 383 | 0.0 384 | 0.0 385 | 0.0 386 | 0.0 387 | 0.0 388 | 0.0 389 | 0.0 390 | 0.0 391 | 0.0 392 | 0.0 393 | 0.0 394 | 0.0 395 | 0.0 396 | 0.0 397 | 0.0 398 | 0.0 399 | 0.0 400 | 0.0 401 | 0.0 402 | 0.0 403 | 0.0 404 | 0.0 405 | 0.0 406 | 0.0 407 | 0.0 408 | 0.0 409 | 0.0 410 | 0.0 411 | 0.0 412 | 0.0 413 | 0.0 414 | 0.0 415 | 0.0 416 | 0.0 417 | 0.0 418 | 0.0 419 | 0.0 420 | 0.0 421 | 0.0 422 | 0.0 423 | 0.0 424 | 0.0 425 | 0.0 426 | 0.0 427 | 0.0 428 | 0.0 429 | 0.0 430 | 0.0 431 | 0.0 432 | 0.0 433 | 0.0 434 | 0.0 435 | 0.0 436 | 0.0 437 | 0.0 438 | 0.0 439 | 0.0 440 | 0.0 441 | 0.0 442 | 0.0 443 | 0.0 444 | 0.0 445 | 0.0 446 | 0.0 447 | 0.0 448 | 0.0 449 | 0.0 450 | 0.0 451 | 0.0 452 | 0.0 453 | 0.0 454 | 0.0 455 | 0.0 456 | 0.0 457 | 0.0 458 | 0.0 459 | 0.0 460 | 0.0 461 | 0.0 462 | 0.0 463 | 0.0 464 | 0.0 465 | 0.0 466 | 0.0 467 | 0.0 468 | 0.0 469 | 0.0 470 | 0.0 471 | 0.0 472 | 0.0 473 | 0.0 474 | 0.0 475 | 0.0 476 | 0.0 477 | 0.0 478 | 0.0 479 | 0.0 480 | 0.0 481 | 0.0 482 | 0.0 483 | 0.0 484 | 0.0 485 | 0.0 486 | 0.0 487 | 0.0 488 | 0.0 489 | 0.0 490 | 0.0 491 | 0.0 492 | 0.0 493 | 0.0 494 | 0.0 495 | 0.0 496 | 0.0 497 | 0.0 498 | 0.0 499 | 0.0 500 | 0.0 501 | 0.0 502 | 0.0 503 | 0.0 504 | 0.0 505 | 0.0 506 | 0.0 507 | 0.0 508 | 0.0 509 | 0.0 510 | 0.0 511 | 0.0 512 | 1.0 513 | 0.0 514 | 0.0 515 | 0.0 516 | 0.0 517 | 0.0 518 | 0.0 519 | 0.0 520 | 0.0 521 | 0.0 522 | 0.0 523 | 0.0 524 | 0.0 525 | 0.0 526 | 0.0 527 | 0.0 528 | 0.0 529 | 0.0 530 | 0.0 531 | 0.0 532 | 0.0 533 | 0.0 534 | 0.0 535 | 0.0 536 | 0.0 537 | 0.0 538 | 0.0 539 | 0.0 540 | 0.0 541 | 0.0 542 | 0.0 543 | 0.0 544 | 0.0 545 | 0.0 546 | 0.0 547 | 0.0 548 | 0.0 549 | 0.0 550 | 0.0 551 | 0.0 552 | 0.0 553 | 0.0 554 | 0.0 555 | 0.0 556 | 0.0 557 | 0.0 558 | 0.0 559 | 0.0 560 | 0.0 561 | 0.0 562 | 0.0 563 | 0.0 564 | 0.0 565 | 0.0 566 | 0.0 567 | 0.0 568 | 0.0 569 | 0.0 570 | 0.0 571 | 0.0 572 | 0.0 573 | 0.0 574 | 0.0 575 | 0.0 576 | 0.0 577 | 0.0 578 | 0.0 579 | 0.0 580 | 0.0 581 | 0.0 582 | 0.0 583 | 0.0 584 | 0.0 585 | 0.0 586 | 0.0 587 | 0.0 588 | 0.0 589 | 0.0 590 | 0.0 591 | 0.0 592 | 0.0 593 | 0.0 594 | 0.0 595 | 0.0 596 | 0.0 597 | 0.0 598 | 0.0 599 | 0.0 600 | 0.0 601 | 0.0 602 | 0.0 603 | 0.0 604 | 0.0 605 | 0.0 606 | 0.0 607 | 0.0 608 | 0.0 609 | 0.0 610 | 0.0 611 | 0.0 612 | 0.0 613 | 0.0 614 | 0.0 615 | 0.0 616 | 0.0 617 | 0.0 618 | 0.0 619 | 0.0 620 | 0.0 621 | 0.0 622 | 0.0 623 | 0.0 624 | 0.0 625 | 0.0 626 | 0.0 627 | 0.0 628 | 0.0 629 | 0.0 630 | 0.0 631 | 0.0 632 | 0.0 633 | 0.0 634 | 0.0 635 | 0.0 636 | 0.0 637 | 0.0 638 | 0.0 639 | 0.0 640 | 0.0 641 | 0.0 642 | 0.0 643 | 0.0 644 | 0.0 645 | 0.0 646 | 0.0 647 | 0.0 648 | 0.0 649 | 0.0 650 | 0.0 651 | 0.0 652 | 0.0 653 | 0.0 654 | 0.0 655 | 0.0 656 | 0.0 657 | 0.0 658 | 0.0 659 | 0.0 660 | 0.0 661 | 0.0 662 | 0.0 663 | 0.0 664 | 0.0 665 | 0.0 666 | 0.0 667 | 0.0 668 | 0.0 669 | 0.0 670 | 0.0 671 | 0.0 672 | 0.0 673 | 0.0 674 | 0.0 675 | 0.0 676 | 0.0 677 | 0.0 678 | 0.0 679 | 0.0 680 | 0.0 681 | 0.0 682 | 0.0 683 | 0.0 684 | 0.0 685 | 0.0 686 | 0.0 687 | 0.0 688 | 0.0 689 | 0.0 690 | 0.0 691 | 0.0 692 | 0.0 693 | 0.0 694 | 0.0 695 | 0.0 696 | 0.0 697 | 0.0 698 | 0.0 699 | 0.0 700 | 0.0 701 | 0.0 702 | 0.0 703 | 0.0 704 | 0.0 705 | 0.0 706 | 0.0 707 | 0.0 708 | 0.0 709 | 0.0 710 | 0.0 711 | 0.0 712 | 0.0 713 | 0.0 714 | 0.0 715 | 0.0 716 | 0.0 717 | 0.0 718 | 0.0 719 | 0.0 720 | 0.0 721 | 0.0 722 | 0.0 723 | 0.0 724 | 0.0 725 | 0.0 726 | 0.0 727 | 0.0 728 | 0.0 729 | 0.0 730 | 0.0 731 | 0.0 732 | 0.0 733 | 0.0 734 | 0.0 735 | 0.0 736 | 0.0 737 | 0.0 738 | 0.0 739 | 0.0 740 | 0.0 741 | 0.0 742 | 0.0 743 | 0.0 744 | 0.0 745 | 0.0 746 | 0.0 747 | 0.0 748 | 0.0 749 | 0.0 750 | 0.0 751 | 0.0 752 | 0.0 753 | 0.0 754 | 0.0 755 | 0.0 756 | 0.0 757 | 0.0 758 | 0.0 759 | 0.0 760 | 0.0 761 | 0.0 762 | 0.0 763 | 0.0 764 | 0.0 765 | 0.0 766 | 0.0 767 | 0.0 768 | 0.0 769 | 0.0 770 | 0.0 771 | 0.0 772 | 0.0 773 | 0.0 774 | 0.0 775 | 0.0 776 | 0.0 777 | 0.0 778 | 0.0 779 | 0.0 780 | 0.0 781 | 0.0 782 | 0.0 783 | 0.0 784 | 0.0 785 | 0.0 786 | 0.0 787 | 0.0 788 | 0.0 789 | 0.0 790 | 0.0 791 | 0.0 792 | 0.0 793 | 0.0 794 | 0.0 795 | 0.0 796 | 0.0 797 | 0.0 798 | 0.0 799 | 0.0 800 | 0.0 801 | 0.0 802 | 0.0 803 | 0.0 804 | 0.0 805 | 0.0 806 | 0.0 807 | 0.0 808 | 0.0 809 | 0.0 810 | 0.0 811 | 0.0 812 | 0.0 813 | 0.0 814 | 0.0 815 | 0.0 816 | 0.0 817 | 0.0 818 | 0.0 819 | 0.0 820 | 0.0 821 | 0.0 822 | 0.0 823 | 0.0 824 | 0.0 825 | 0.0 826 | 0.0 827 | 0.0 828 | 0.0 829 | 0.0 830 | 0.0 831 | 0.0 832 | 0.0 833 | 0.0 834 | 0.0 835 | 0.0 836 | 0.0 837 | 0.0 838 | 0.0 839 | 0.0 840 | 0.0 841 | 0.0 842 | 0.0 843 | 0.0 844 | 0.0 845 | 0.0 846 | 0.0 847 | 0.0 848 | 0.0 849 | 0.0 850 | 0.0 851 | 0.0 852 | 0.0 853 | 0.0 854 | 0.0 855 | 0.0 856 | 0.0 857 | 0.0 858 | 0.0 859 | 0.0 860 | 0.0 861 | 0.0 862 | 0.0 863 | 0.0 864 | 0.0 865 | 0.0 866 | 0.0 867 | 0.0 868 | 0.0 869 | 0.0 870 | 0.0 871 | 0.0 872 | 0.0 873 | 0.0 874 | 0.0 875 | 0.0 876 | 0.0 877 | 0.0 878 | 0.0 879 | 0.0 880 | 0.0 881 | 0.0 882 | 0.0 883 | 0.0 884 | 0.0 885 | 0.0 886 | 0.0 887 | 0.0 888 | 0.0 889 | 0.0 890 | 0.0 891 | 0.0 892 | 0.0 893 | 0.0 894 | 0.0 895 | 0.0 896 | 0.0 897 | 0.0 898 | 0.0 899 | 0.0 900 | 0.0 901 | 0.0 902 | 0.0 903 | 0.0 904 | 0.0 905 | 0.0 906 | 0.0 907 | 0.0 908 | 0.0 909 | 0.0 910 | 0.0 911 | 0.0 912 | 0.0 913 | 0.0 914 | 0.0 915 | 0.0 916 | 0.0 917 | 0.0 918 | 0.0 919 | 0.0 920 | 0.0 921 | 0.0 922 | 0.0 923 | 0.0 924 | 0.0 925 | 0.0 926 | 0.0 927 | 0.0 928 | 0.0 929 | 0.0 930 | 0.0 931 | 0.0 932 | 0.0 933 | 0.0 934 | 0.0 935 | 0.0 936 | 0.0 937 | 0.0 938 | 0.0 939 | 0.0 940 | 0.0 941 | 0.0 942 | 0.0 943 | 0.0 944 | 0.0 945 | 0.0 946 | 0.0 947 | 0.0 948 | 0.0 949 | 0.0 950 | 0.0 951 | 0.0 952 | 0.0 953 | 0.0 954 | 0.0 955 | 0.0 956 | 0.0 957 | 0.0 958 | 0.0 959 | 0.0 960 | 0.0 961 | 0.0 962 | 0.0 963 | 0.0 964 | 0.0 965 | 0.0 966 | 0.0 967 | 0.0 968 | 0.0 969 | 0.0 970 | 0.0 971 | 0.0 972 | 0.0 973 | 0.0 974 | 0.0 975 | 0.0 976 | 0.0 977 | 0.0 978 | 0.0 979 | 0.0 980 | 0.0 981 | 0.0 982 | 0.0 983 | 0.0 984 | 0.0 985 | 0.0 986 | 0.0 987 | 0.0 988 | 0.0 989 | 0.0 990 | 0.0 991 | 0.0 992 | 0.0 993 | 0.0 994 | 0.0 995 | 0.0 996 | 0.0 997 | 0.0 998 | 0.0 999 | 0.0 1000 | 0.0 1001 | 0.0 1002 | 0.0 1003 | 0.0 1004 | 0.0 1005 | 0.0 1006 | 0.0 1007 | 0.0 1008 | 0.0 1009 | 0.0 1010 | 0.0 1011 | 0.0 1012 | 0.0 1013 | 0.0 1014 | 0.0 1015 | 0.0 1016 | 0.0 1017 | 0.0 1018 | 0.0 1019 | 0.0 1020 | 0.0 1021 | 0.0 1022 | 0.0 1023 | 0.0 1024 | 0.0 -------------------------------------------------------------------------------- /backend_alsa.md: -------------------------------------------------------------------------------- 1 | # ALSA (Linux) 2 | 3 | ## Introduction 4 | 5 | ALSA is the low level audio API that is used in the Linux kernel. 6 | The ALSA project also maintains various user-space tools and utilities 7 | that are installed by default in most Linux distributions. 8 | 9 | This readme only covers some basics of ALSA. For more details, 10 | see for example the [ALSA Documentation](#alsa-documentation) and [A close look at ALSA](#a-close-look-at-alsa) 11 | 12 | ### Hardware devices 13 | 14 | In the ALSA scheme, a soundcard or dac corresponds to a "card". 15 | A card can have one or several inputs and/or outputs, denoted "devices". 16 | Finally each device can support one or several streams, called "subdevices". 17 | It depends on the driver implementation how the different physical ports of a card is exposed in terms of devices. 18 | For example a 4-channel unit may present a single 4-channel device, or two separate 2-channel devices. 19 | 20 | ### PCM devices 21 | 22 | An alsa PCM device can be many different things, like a simple alias for a hardware device, 23 | or any of the many plugins supported by ALSA. 24 | PCM devices are normally defined in the ALSA configuration file. 25 | See the [ALSA Plugin Documentation](#alsa-plugin-documentation) for a list of the available plugins. 26 | 27 | ### Find name of device 28 | To list all hardware playback devices use the `aplay` command with the `-l` option: 29 | ``` 30 | > aplay -l 31 | **** List of PLAYBACK Hardware Devices **** 32 | card 0: Generic [HD-Audio Generic], device 0: ALC236 Analog [ALC236 Analog] 33 | Subdevices: 1/1 34 | Subdevice #0: subdevice #0 35 | ``` 36 | 37 | To list all PCM devices use the `aplay` command with the `-L` option: 38 | ``` 39 | > aplay -L 40 | hdmi:CARD=Generic,DEV=0 41 | HD-Audio Generic, HDMI 0 42 | HDMI Audio Output 43 | ``` 44 | Capture devices can be found in the same way with `arecord -l` and `arecord -L`. 45 | 46 | A hardware device is accessed via the "hw" plugin. The device name is then prefixed by `hw:`. 47 | To use the ALC236 hardware device from above, 48 | put either `hw:Generic` (to use the name, recommended) or `hw:0` (to use the index) in the CamillaDSP config. 49 | 50 | To instead use the "hdmi" PCM device, it's enough to give the name `hdmi`. 51 | 52 | 53 | ### Find valid playback and capture parameters 54 | To find the parameters for the playback device "Generic" from the example above, again use `aplay`: 55 | ``` 56 | > aplay -v -D hw:Generic /dev/zero --dump-hw-params 57 | Playing raw data '/dev/zero' : Unsigned 8 bit, Rate 8000 Hz, Mono 58 | HW Params of device "hw:Generic": 59 | -------------------- 60 | ACCESS: MMAP_INTERLEAVED RW_INTERLEAVED 61 | FORMAT: S16_LE S32_LE 62 | SUBFORMAT: STD 63 | SAMPLE_BITS: [16 32] 64 | FRAME_BITS: [32 64] 65 | CHANNELS: 2 66 | RATE: [44100 48000] 67 | PERIOD_TIME: (333 96870749) 68 | PERIOD_SIZE: [16 4272000] 69 | PERIOD_BYTES: [128 34176000] 70 | PERIODS: [2 32] 71 | BUFFER_TIME: (666 178000000] 72 | BUFFER_SIZE: [32 8544000] 73 | BUFFER_BYTES: [128 68352000] 74 | TICK_TIME: ALL 75 | -------------------- 76 | aplay: set_params:1343: Sample format non available 77 | Available formats: 78 | - S16_LE 79 | - S32_LE 80 | ``` 81 | Ignore the error message at the end. The interesting fields are FORMAT, RATE and CHANNELS. 82 | In this example the sample formats this device can use are S16_LE and S32_LE (corresponding to S16LE and S32LE in CamillaDSP, 83 | see the [table of equivalent formats in the main README](./README.md#equivalent-formats) for the complete list). 84 | The sample rate can be either 44.1 or 48 kHz. And it supports only stereo playback (2 channels). 85 | 86 | ### Combinations of parameter values 87 | Note that all possible combinations of the shown parameters may not be supported by the device. 88 | For example many USB DACS only support 24-bit samples up to 96 kHz, 89 | so that only 16-bit samples are supported at 192 kHz. 90 | For other devices, the number of channels depends on the sample rate. 91 | This is common on studio interfaces that support [ADAT](#adat). 92 | 93 | CamillaDSP sets first the number of channels. 94 | Then it sets sample rate, and finally sample format. 95 | Setting a value for a parameter may restrict the allowed values for the ones that have not yet been set. 96 | For the USB DAC just mentioned, setting the sample rate to 192 kHz means that only the S16LE sample format is allowed. 97 | If the CamillaDSP configuration is set to 192 kHz and S24LE3, then there will be an error when setting the format. 98 | 99 | 100 | Capture parameters are determined in the same way with `arecord`: 101 | ``` 102 | > arecord -D hw:Generic /dev/null --dump-hw-params 103 | ``` 104 | This outputs the same table as for the aplay example above, but for a capture device. 105 | 106 | ## Routing all audio through CamillaDSP 107 | 108 | To route all audio through CamillaDSP using ALSA, the audio output from any application must be redirected. 109 | This can be acheived either by using an [ALSA Loopback device](#alsa-loopback), 110 | or the [ALSA CamillaDSP "I/O" plugin](#alsa-camilladsp-"io"-plugin). 111 | 112 | ### ALSA Loopback 113 | An ALSA Loopback card can be used. This behaves like a sound card that presents two devices. 114 | The sound being send to the playback side on one device can then be captured from the capture side on the other device. 115 | To load the kernel module type: 116 | ``` 117 | sudo modprobe snd-aloop 118 | ``` 119 | Find the name of the device: 120 | ``` 121 | aplay -l 122 | ``` 123 | 124 | Play a track on card "Loopback", device 1, subdevice 0: 125 | ``` 126 | aplay -D hw:Loopback,1,0 sometrack.wav 127 | ``` 128 | The audio can then be captured from card "Loopback", device 0, subdevice 0, by running `arecord` in a separate terminal: 129 | ``` 130 | arecord -D hw:Loopback,0,0 sometrack_copy.wav 131 | ``` 132 | The first application that opens either side of a Loopback decides the sample rate and format. 133 | If `aplay` is started first in this example, this means that `arecord` must use the same sample rate and format. 134 | To change format or rate, both sides of the loopback must first be closed. 135 | 136 | When using the ALSA Loopback approach, see the separate repository [camilladsp-config](#camilladsp-config). 137 | This contains example configuration files for setting up the entire system, and to have it start automatically after boot. 138 | 139 | ### ALSA CamillaDSP "I/O" plugin 140 | 141 | ALSA can be extended by plugins in user-space. 142 | One such plugin that is intended specifically for CamillaDSP 143 | is the [ALSA CamillaDSP "I/O" plugin](#alsa-camilladsp-plugin) by scripple. 144 | 145 | The plugin starts CamillaDSP whenever an application opens the CamillaDSP plugin PCM device. 146 | This makes it possible to support automatic switching of the sample rate. 147 | See the plugin readme for how to install and configure it. 148 | 149 | ## Configuration of devices 150 | 151 | This example configuration will be used to explain the various options specific to ALSA: 152 | ``` 153 | capture: 154 | type: Alsa 155 | channels: 2 156 | device: "hw:0,1" 157 | format: S16LE (*) 158 | stop_on_inactive: false (*) 159 | follow_volume_control: "PCM Playback Volume" (*) 160 | playback: 161 | type: Alsa 162 | channels: 2 163 | device: "hw:Generic_1" 164 | format: S32LE (*) 165 | ``` 166 | 167 | ### Device names 168 | See [Find name of device](#find-name-of-device) for what to write in the `device` field. 169 | 170 | ### Sample rate and format 171 | The sample format is optional. If set to `null` or left out, 172 | the highest quality available format is chosen automatically. 173 | 174 | When the format is set automatically, 32-bit integer (`S32LE`) is considered the best, 175 | followed by 24-bit (`S24LE3` and `S24LE`) and 16-bit integer (`S16LE`). 176 | The 32-bit (`FLOAT32LE`) and 64-bit (`FLOAT64LE`) float formats are high quality, 177 | but are supported by very few devices. Therefore these are checked last. 178 | 179 | Please also see [Find valid playback and capture parameters](#find-valid-playback-and-capture-parameters). 180 | 181 | ### Linking volume control to device volume 182 | It is possible to let CamillaDSP link its volume and mute controls to controls on the capture device. 183 | This is mostly useful when capturing from the USB Audio Gadget, 184 | which provides a volume control named `PCM Capture Volume` 185 | and a mute control called `PCM Capture Switch` that are controlled by the USB host. 186 | 187 | This volume control does not alter the signal, 188 | and can be used to forward the volume setting from a player to CamillaDSP. 189 | To enable this, set the `link_volume_control` setting to the name of the volume control. 190 | The corresponding setting for the mute control is `link_mute_control`. 191 | Any change of the volume or mute then gets applied to the CamillaDSP main volume control. 192 | The link works in both directions, so that volume and mute changes requested 193 | over the websocket interface also get sent to the USB host. 194 | 195 | The available controls for a device can be listed with `amixer`. 196 | List controls for card 1: 197 | ```sh 198 | amixer -c 1 controls 199 | ``` 200 | 201 | List controls with values and more details: 202 | ```sh 203 | amixer -c 1 contents 204 | ``` 205 | 206 | The chosen volume control should be one that does not affect the signal volume, 207 | otherwise the volume gets applied twice. 208 | It must also have a scale in decibel, and take a single value (`values=1`). 209 | 210 | Example: 211 | ``` 212 | numid=15,iface=MIXER,name='Master Playback Volume' 213 | ; type=INTEGER,access=rw---R--,values=1,min=0,max=87,step=0 214 | : values=52 215 | | dBscale-min=-65.25dB,step=0.75dB,mute=0 216 | ``` 217 | 218 | The mute control shoule be a _switch_, meaning that is has states `on` and `off`, 219 | where `on` is not muted and `off` is muted. 220 | It must also take a single value (`values=1`). 221 | 222 | Example: 223 | ``` 224 | numid=6,iface=MIXER,name='PCM Capture Switch' 225 | ; type=BOOLEAN,access=rw------,values=1 226 | : values=on 227 | ``` 228 | 229 | ### Subscribe to Alsa control events 230 | The Alsa capture device subscribes to control events from the USB Gadget and Loopback devices. 231 | For the loopback, it subscribes to events from the `PCM Slave Active` control, 232 | and for the gadget it subscribes to events from `Capture Rate`. 233 | Both of these can indicate when playback has stopped. 234 | If CamillaDSP should stop when that happens, set `stop_on_inactive` to `true`. 235 | For the loopback, this means that CamillaDSP releases the capture side, 236 | making it possible for a player application to re-open at another sample rate. 237 | 238 | For the gadget, the control can also indicate that the sample rate changed. 239 | When this happens, the capture can no longer continue and CamillaDSP will stop. 240 | The new sample rate can then be read by the `GetStopReason` websocket command. 241 | 242 | ## Links 243 | ### ALSA Documentation 244 | https://www.alsa-project.org/wiki/Documentation 245 | ### A close look at ALSA 246 | https://www.volkerschatz.com/noise/alsa.html 247 | ### ALSA Plugin Documentation 248 | https://www.alsa-project.org/alsa-doc/alsa-lib/pcm_plugins.html 249 | ### camilladsp-config 250 | https://github.com/HEnquist/camilladsp-config 251 | ### ALSA CamillaDSP plugin 252 | https://github.com/scripple/alsa_cdsp/ 253 | 254 | ## Notes 255 | ### ADAT 256 | ADAT achieves higher sampling rates by multiplexing two or four 44.1/48kHz audio streams into a single one. 257 | 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. --------------------------------------------------------------------------------