├── 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.
--------------------------------------------------------------------------------