commit ff8706e3ddcc57323fa3263e795e8601d3806da8
parent f1892d1784807268d2d620bcb41d0c9dd5e64985
Author: Matt Demanett <matt@demanett.net>
Date: Wed, 19 Aug 2020 07:53:46 -0400
RANALYZER: swept-sine frequency response analyzer. #116
Diffstat:
15 files changed, 1058 insertions(+), 30 deletions(-)
diff --git a/README-prerelease.md b/README-prerelease.md
@@ -858,12 +858,34 @@ _Polyphony:_ Monophonic, with two exceptions:
- If an input is polyphonic, its channels are summed, and the spectra of the summed signal is displayed.
- A polyphonic input is copied unchanged (channels intact) to THRU.
-#### <a name="analyxerxl"></a> ANALYZER-XL
+#### <a name="analyzerxl"></a> ANALYZER-XL
An eight-channel, 42HP version of ANALYZER, with edge-to-edge-screen design. Options corresponding to ANALYZER's panel controls are available on the context (right-click) menu.
_Polyphony:_ Monophonic, but if an input is polyphonic, its channels are summed, and the spectra of the summed signal is displayed.
+#### <a name="ranalyzer"></a> RANALYZER
+
+RANALYZER is a frequency response analyzer: it passes a test signal to another module, expecting the output of that module to be patched back, and then displays the frequency spectrum of the response relative to the test signal.
+
+**This module is primarily useful to plugin developers**, especially when developing filters. Of course anyone may use it, to investigate the response of some module, or to tune a filter bank, or what have you.
+
+By default, will produce a one-shot test signal, emitted at SEND, upon receipt of a trigger (manual or CV) at the TRIG inputs. The duration of the test signal is always 16384 samples; thus the duration in time will depend on Rack's current sample rate. The same number of samples is collected from RETURN, if it is patched. Both signals are converted to the frequency domain, and a third "analysis" signal is generated by dividing the spectrum of the response signal by the spectrum of the test signal.
+
+By default, the module displays all three spectral signals; a context menu option sets it to display only the analysis signal.
+
+The test signal is a swept sine wave (or "chirp", see <a href="#chirp">CHIRP</a>), with an exponential (if the EXP toggle is on) or linear sweep. The start and end frequencies for sine sweep are set by the FREQ1 and FREQ2 knobs, each with a range in hertz from 1 to the Nyquist rate (half the sampling rate); if FREQ1 is less than FREQ2 the sweep is upwards in frequency, downwards otherwise.
+
+Patching an input to TEST overrides the swept sine generator; the TEST input is used as the test signal.
+
+If the LOOP toggle is enabled, the module continuously outputs, collects and displays signals, in 16384-sample blocks. Regardless of whether the collection cycle is triggered or looped, a pulse is emitted at the TRIG output when the cycle begins, and at the EOC output when it ends.
+
+The R. DELAY (response delay) control allows sample-accurate alignment of the test and response signals for analysis. When SEND is patched directly to the module to be analyzed, and that module's output is patched directly back to RETURN, and the analyzed module does not impose an internal sample delay, then the returned signal will be received by RANALYZER two samples later, relative to the test sample RANALYZER emits. More complicated patches between SEND and RETURN, or modules under test which have internal sample delays, can increase this, in which case R. DELAY may be set to whatever this actual sample delay is. In practice, getting this right will likely not be very important.
+
+The display's frequency and amplitude ranges can be set to a few different values on the context menu.
+
+_Polyphony:_ Monophonic.
+
#### <a name="vu"></a> VU
A stereo signal level visualizer/meter. The L channel is sent to both displays if if nothing is patched to R. Inputs to L and R are copied to the L and R outputs.
diff --git a/benchmarks/oscillator_benchmark.cpp b/benchmarks/oscillator_benchmark.cpp
@@ -175,3 +175,21 @@ static void BM_Oscillator_ChirpOscillatorExp(benchmark::State& state) {
}
}
BENCHMARK(BM_Oscillator_ChirpOscillatorExp);
+
+static void BM_Oscillator_PureChirpOscillatorLinear(benchmark::State& state) {
+ PureChirpOscillator o(44100.0, 100.0f, 20000.0f, 1.0f, true);
+
+ for (auto _ : state) {
+ o.next();
+ }
+}
+BENCHMARK(BM_Oscillator_PureChirpOscillatorLinear);
+
+static void BM_Oscillator_PureChirpOscillatorExp(benchmark::State& state) {
+ PureChirpOscillator o(44100.0, 100.0f, 20000.0f, 1.0f, false);
+
+ for (auto _ : state) {
+ o.next();
+ }
+}
+BENCHMARK(BM_Oscillator_PureChirpOscillatorExp);
diff --git a/plugin.json b/plugin.json
@@ -708,6 +708,15 @@
]
},
{
+ "slug": "Bogaudio-VU",
+ "name": "VU",
+ "description": "Stereo signal meter",
+ "manualUrl": "https://github.com/bogaudio/BogaudioModules/blob/master/README.md#vu",
+ "tags": [
+ "Visual"
+ ]
+ },
+ {
"slug": "Bogaudio-Analyzer",
"name": "ANALYZER",
"description": "4-channel spectrum analyzer",
@@ -726,10 +735,10 @@
]
},
{
- "slug": "Bogaudio-VU",
- "name": "VU",
- "description": "Stereo signal meter",
- "manualUrl": "https://github.com/bogaudio/BogaudioModules/blob/master/README.md#vu",
+ "slug": "Bogaudio-Ranalyzer",
+ "name": "RANALYZER",
+ "description": "Swept-sine frequency response analyzer",
+ "manualUrl": "https://github.com/bogaudio/BogaudioModules/blob/master/README.md#ranalyzer",
"tags": [
"Visual"
]
diff --git a/res-pp/Ranalyzer-pp.svg b/res-pp/Ranalyzer-pp.svg
@@ -0,0 +1,275 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="675.0" height="380.0" viewBox="0 0 675.0 380.0">
+ <style>text {
+ fill: #333;
+ font-family: 'Roboto', sans-serif;
+ font-weight: bold;
+}
+text.title {
+ font-family: 'Comfortaa', sans-serif;
+ font-weight: normal;
+}
+text.brand {
+ font-family: 'Audiowide', sans-serif;
+ font-weight: bold;
+}
+
+polyline {
+ stroke: #333;
+}
+polyline.guide {
+ stroke: #0f0;
+}
+path {
+ stroke: #333;
+}
+
+rect.module-background, .background-fill {
+ fill: #ddd;
+}
+polyline.module-border-inner {
+ stroke: #e4e4e4;
+}
+polyline.module-border-middle {
+ stroke: #ebebeb;
+}
+polyline.module-border-outer {
+ stroke: #f2f2f2;
+}
+
+g.io-group {
+}
+rect.input-background, rect.input-background-filler {
+ fill: #fafafa;
+}
+rect.output-background, rect.output-background-filler {
+ fill: #bbb;
+}
+text.input-label, text.output-label {
+ /* font-size: 6pt; */
+}
+polyline.input-label, polyline.output-label {
+}
+path.input-label, path.output-label {
+}
+
+circle.port-rim {
+ stroke: #f0f0f0;
+}
+circle.port-barrel {
+ stroke: #222;
+ fill: #222;
+}
+circle.knob-center {
+ fill: #eee;
+}
+circle.knob-rim {
+ fill: #333;
+}
+circle.knob-tick {
+ fill: #fff;
+}
+polyline.knob-tick {
+ stroke: #fff;
+}
+</style>
+ <style>
+ text {
+ fill: #fff;
+ }
+ text.name, text.brand {
+ font-family: 'Comfortaa', sans-serif;
+ font-size: 7pt;
+ font-weight: bold;
+ }
+
+ polyline {
+ stroke: #fff;
+ }
+ path {
+ stroke: #fff;
+ }
+
+ rect.input-background, rect.input-background-filler {
+ fill: #aaa;
+ }
+ rect.output-background, rect.output-background-filler {
+ fill: #666;
+ }
+ text.input-label {
+ fill: #222;
+ }
+ polyline.input-label, path.input-label {
+ stroke: #222;
+ }
+ text.output-label {
+ fill: #ddd;
+ }
+ polyline.output-label, path.output-label {
+ stroke: #ddd;
+ }
+ </style>
+
+ <defs>
+ <symbol id="dial-frequency-ranalyzer" viewbox="75 40">
+ <g transform="translate(37.5 20)">
+ <text font-size="6pt" text-anchor="middle" transform="rotate(-240) translate(21 0) rotate(240.0) translate(0 3)">1</text>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(-210.0) translate(16 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(-180.0) translate(16 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(-150.0) translate(16 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(-120.0) translate(16 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(-90.0) translate(16 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(-60.0) translate(16 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(-30.0) translate(16 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(0.0) translate(16 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(30.0) translate(16 0)"/>
+ <text font-size="6pt" text-anchor="middle" transform="rotate(60) translate(21 0) rotate(-60.0) translate(0 3)">NQ</text>
+ </g>
+ </symbol>
+
+ <symbol id="dial-delay-ranalyzer" viewbox="75 40">
+ <g transform="translate(37.5 20)">
+ <text font-size="6pt" text-anchor="middle" transform="rotate(-240) translate(16 0) rotate(240.0) translate(0 3)">2</text>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(-206.66666666666669) translate(11 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(-173.33333333333334) translate(11 0)"/>
+ <text font-size="6pt" text-anchor="middle" transform="rotate(-140.0) translate(16 0) rotate(140.0) translate(0 3)">8</text>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(-106.66666666666669) translate(11 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(-73.33333333333331) translate(11 0)"/>
+ <text font-size="6pt" text-anchor="middle" transform="rotate(-40.0) translate(16 0) rotate(40.0) translate(0 3)">14</text>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(-6.666666666666657) translate(11 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(26.66666666666663) translate(11 0)"/>
+ <text font-size="6pt" text-anchor="middle" transform="rotate(60) translate(16 0) rotate(-60.0) translate(0 3)">20</text>
+ </g>
+ </symbol>
+ </defs>
+
+ <rect width="100%" height="100%" fill="#000"/>
+ <text class="name" transform="translate(2.5 11)" letter-spacing="1.3">RANALYZER</text>
+ <text class="brand" transform="translate(2.5 375)" letter-spacing="2">BOGAUDIO</text>
+
+ <g transform="translate(0 30)">
+ <text font-size="7pt" letter-spacing="2px" text-anchor="middle" transform="translate(37.5 0)">FREQ1</text>
+ <g transform="translate(24.5 12)"><svg id="FREQUENCY1_PARAM">
+ <g transform="translate(13 13)">
+ <polyline points="-3,0 3,0" stroke-width="1" stroke="#00f"/>
+ <polyline points="0,-3 0,3" stroke-width="1" stroke="#00f"/>
+ <circle cx="0" cy="0" r="12.5" stroke-width="1" stroke="#00f" fill="none"/>
+ </g>
+ </svg></g>
+ <use xlink:href="#dial-frequency-ranalyzer" transform="translate(0 5)" href="#dial-frequency-ranalyzer"/>
+ </g>
+
+ <g transform="translate(0 91.5)">
+ <text font-size="7pt" letter-spacing="2px" text-anchor="middle" transform="translate(37.5 0)">FREQ2</text>
+ <g transform="translate(24.5 12)"><svg id="FREQUENCY2_PARAM">
+ <g transform="translate(13 13)">
+ <polyline points="-3,0 3,0" stroke-width="1" stroke="#00f"/>
+ <polyline points="0,-3 0,3" stroke-width="1" stroke="#00f"/>
+ <circle cx="0" cy="0" r="12.5" stroke-width="1" stroke="#00f" fill="none"/>
+ </g>
+ </svg></g>
+ <use xlink:href="#dial-frequency-ranalyzer" transform="translate(0 5)" href="#dial-frequency-ranalyzer"/>
+ </g>
+
+ <g class="io-group" transform="translate(0 147)">
+ <rect class="input-background" width="62" height="32" rx="5" transform="translate(6.5 0)"/>
+ <g transform="translate(18 7)"><svg id="TRIGGER_PARAM">
+ <g transform="translate(9 9)">
+ <circle cx="0" cy="0" r="8.5" stroke-width="1" stroke="#00f" fill="#f00"/>
+ </g>
+ </svg></g>
+ <g transform="translate(40.5 4)"><svg id="TRIGGER_INPUT">
+ <g transform="translate(12 12)">
+ <circle cx="0" cy="0" r="5" stroke-width="1" stroke="#0f0" fill="#0f0"/>
+ <circle cx="0" cy="0" r="10.5" stroke-width="3" stroke="#0f0" fill="none"/>
+ </g>
+ </svg></g>
+ <text class="input-label" font-size="5pt" letter-spacing="2px" text-anchor="middle" transform="translate(15 16) rotate(-90)">TRIG</text>
+
+ <g transform="translate(16 30)">
+ <rect class="input-background" width="43" height="32" rx="5" transform="translate(0 0)"/>
+ <rect class="input-background" width="43" height="10" transform="translate(0 0)"/>
+ <g transform="translate(14.5 4)"><svg id="TEST_INPUT">
+ <g transform="translate(12 12)">
+ <circle cx="0" cy="0" r="5" stroke-width="1" stroke="#0f0" fill="#0f0"/>
+ <circle cx="0" cy="0" r="10.5" stroke-width="3" stroke="#0f0" fill="none"/>
+ </g>
+ </svg></g>
+ <text class="input-label" font-size="5pt" letter-spacing="2px" text-anchor="middle" transform="translate(10 16.5) rotate(-90)">TEST</text>
+ </g>
+
+ <g transform="translate(0 67)">
+ <g transform="translate(5 0)">
+ <text font-size="5pt" letter-spacing="1.5px" transform="translate(0 6)">EXP</text>
+ <g transform="translate(18 -1)"><svg id="EXPONENTIAL_PARAM">
+ <g transform="translate(4.5 4.5)">
+ <circle r="4" stroke-width="1" stroke="#00f" fill="#f00"/>
+ </g>
+ </svg></g>
+ </g>
+ <g transform="translate(38 0)">
+ <text font-size="5pt" letter-spacing="1.5px" transform="translate(0 6)">LOOP</text>
+ <g transform="translate(24 -1)"><svg id="LOOP_PARAM">
+ <g transform="translate(4.5 4.5)">
+ <circle r="4" stroke-width="1" stroke="#00f" fill="#f00"/>
+ </g>
+ </svg></g>
+ </g>
+ </g>
+ </g>
+
+ <g transform="translate(0 237.5)">
+ <text font-size="7pt" letter-spacing="1.5px" text-anchor="middle" transform="translate(37.5 0)">R. DELAY</text>
+ <g transform="translate(29.5 12)"><svg id="DELAY_PARAM">
+ <g transform="translate(8 8)">
+ <polyline points="-3,0 3,0" stroke-width="1" stroke="#00f"/>
+ <polyline points="0,-3 0,3" stroke-width="1" stroke="#00f"/>
+ <circle r="7.5" stroke-width="1" stroke="#00f" fill="none"/>
+ </g>
+ </svg></g>
+ <use xlink:href="#dial-delay-ranalyzer" transform="translate(0 0)" href="#dial-delay-ranalyzer"/>
+ </g>
+
+ <g class="io-group" transform="translate(0 283)">
+ <rect class="output-background" width="64" height="77" rx="5" transform="translate(5.5 0)"/>
+ <g transform="translate(10.5 3)"><svg id="TRIGGER_OUTPUT">
+ <g transform="translate(12 12)">
+ <circle cx="0" cy="0" r="5" stroke-width="1" stroke="#f00" fill="#f00"/>
+ <circle cx="0" cy="0" r="10.5" stroke-width="3" stroke="#f00" fill="none"/>
+ </g>
+ </svg></g>
+ <text class="output-label" font-size="5pt" letter-spacing="2px" text-anchor="middle" transform="translate(22.5 35)">TRIG</text>
+ <g transform="translate(40.5 3)"><svg id="EOC_OUTPUT">
+ <g transform="translate(12 12)">
+ <circle cx="0" cy="0" r="5" stroke-width="1" stroke="#f00" fill="#f00"/>
+ <circle cx="0" cy="0" r="10.5" stroke-width="3" stroke="#f00" fill="none"/>
+ </g>
+ </svg></g>
+ <text class="output-label" font-size="5pt" letter-spacing="2px" text-anchor="middle" transform="translate(52.5 35)">EOC</text>
+ <g transform="translate(10.5 40)"><svg id="SEND_OUTPUT">
+ <g transform="translate(12 12)">
+ <circle cx="0" cy="0" r="5" stroke-width="1" stroke="#f00" fill="#f00"/>
+ <circle cx="0" cy="0" r="10.5" stroke-width="3" stroke="#f00" fill="none"/>
+ </g>
+ </svg></g>
+ <text class="output-label" font-size="5pt" letter-spacing="2px" text-anchor="middle" transform="translate(22.5 72)">SEND</text>
+ <g transform="translate(30 36)">
+ <rect class="input-background" width="32" height="39" rx="5" transform="translate(7.5 2)"/>
+ <rect class="input-background-filler" width="32" height="10" transform="translate(7.5 2)"/>
+ <rect class="input-background-filler" width="10" height="10" transform="translate(7.5 31)"/>
+ <g transform="translate(10.5 4)"><svg id="RETURN_INPUT">
+ <g transform="translate(12 12)">
+ <circle cx="0" cy="0" r="5" stroke-width="1" stroke="#0f0" fill="#0f0"/>
+ <circle cx="0" cy="0" r="10.5" stroke-width="3" stroke="#0f0" fill="none"/>
+ </g>
+ </svg></g>
+ <text class="input-label" font-size="5pt" letter-spacing="0.5px" text-anchor="middle" transform="translate(22.5 36)">RETURN</text>
+ </g>
+ </g>
+
+ <g transform="translate(75 0)"><svg id="DISPLAY_WIDGET">
+ <rect cx="0" cy="0" width="600" height="380" fill="#444"/>
+ </svg></g>
+
+
+</svg>
diff --git a/res-src/Ranalyzer-src.svg b/res-src/Ranalyzer-src.svg
@@ -0,0 +1,140 @@
+<module hp="45" noskin="true">
+ <style/>
+ <localstyle>
+ text {
+ fill: #fff;
+ }
+ text.name, text.brand {
+ font-family: 'Comfortaa', sans-serif;
+ font-size: 7pt;
+ font-weight: bold;
+ }
+
+ polyline {
+ stroke: #fff;
+ }
+ path {
+ stroke: #fff;
+ }
+
+ rect.input-background, rect.input-background-filler {
+ fill: #aaa;
+ }
+ rect.output-background, rect.output-background-filler {
+ fill: #666;
+ }
+ text.input-label {
+ fill: #222;
+ }
+ polyline.input-label, path.input-label {
+ stroke: #222;
+ }
+ text.output-label {
+ fill: #ddd;
+ }
+ polyline.output-label, path.output-label {
+ stroke: #ddd;
+ }
+ </localstyle>
+
+ <defs>
+ <symbol id="dial-frequency-ranalyzer" viewbox="75 40">
+ <g transform="translate(37.5 20)" var-scale="10.0">
+ <text font-size="6pt" text-anchor="middle" var-r="-240" transform="rotate($r) translate(21 0) rotate(-1.0*$r) translate(0 3)">1</text>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(1.0/$scale*300.0-240.0) translate(16 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(2.0/$scale*300.0-240.0) translate(16 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(3.0/$scale*300.0-240.0) translate(16 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(4.0/$scale*300.0-240.0) translate(16 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(5.0/$scale*300.0-240.0) translate(16 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(6.0/$scale*300.0-240.0) translate(16 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(7.0/$scale*300.0-240.0) translate(16 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(8.0/$scale*300.0-240.0) translate(16 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(9.0/$scale*300.0-240.0) translate(16 0)"/>
+ <text font-size="6pt" text-anchor="middle" var-r="60" transform="rotate($r) translate(21 0) rotate(-1.0*$r) translate(0 3)">NQ</text>
+ </g>
+ </symbol>
+
+ <symbol id="dial-delay-ranalyzer" viewbox="75 40">
+ <g transform="translate(37.5 20)" var-scale="18.0">
+ <text font-size="6pt" text-anchor="middle" var-r="-240" transform="rotate($r) translate(16 0) rotate(-1.0*$r) translate(0 3)">2</text>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(2.0/$scale*300.0-240.0) translate(11 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(4.0/$scale*300.0-240.0) translate(11 0)"/>
+ <text font-size="6pt" text-anchor="middle" var-r="6.0/$scale*300.0-240.0" transform="rotate($r) translate(16 0) rotate(-1.0*$r) translate(0 3)">8</text>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(8.0/$scale*300.0-240.0) translate(11 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(10.0/$scale*300.0-240.0) translate(11 0)"/>
+ <text font-size="6pt" text-anchor="middle" var-r="12.0/$scale*300.0-240.0" transform="rotate($r) translate(16 0) rotate(-1.0*$r) translate(0 3)">14</text>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(14.0/$scale*300.0-240.0) translate(11 0)"/>
+ <polyline points="0,0 2.5,0" stroke-width="0.7" fill="none" transform="rotate(16.0/$scale*300.0-240.0) translate(11 0)"/>
+ <text font-size="6pt" text-anchor="middle" var-r="60" transform="rotate($r) translate(16 0) rotate(-1.0*$r) translate(0 3)">20</text>
+ </g>
+ </symbol>
+ </defs>
+
+ <rect width="100%" height="100%" fill="#000"/>
+ <text class="name" transform="translate(2.5 11)" letter-spacing="1.3">RANALYZER</text>
+ <text class="brand" transform="translate(2.5 375)" letter-spacing="2">BOGAUDIO</text>
+
+ <g transform="translate(0 30)">
+ <text font-size="7pt" letter-spacing="2px" text-anchor="middle" transform="translate(37.5 0)">FREQ1</text>
+ <def id="FREQUENCY1_PARAM" xlink:href="#knob26" transform="translate(37.5-13.0 12)"/>
+ <use xlink:href="#dial-frequency-ranalyzer" transform="translate(0 5)"/>
+ </g>
+
+ <g transform="translate(0 91.5)">
+ <text font-size="7pt" letter-spacing="2px" text-anchor="middle" transform="translate(37.5 0)">FREQ2</text>
+ <def id="FREQUENCY2_PARAM" xlink:href="#knob26" transform="translate(37.5-13.0 12)"/>
+ <use xlink:href="#dial-frequency-ranalyzer" transform="translate(0 5)"/>
+ </g>
+
+ <g class="io-group" transform="translate(0 147)">
+ <rect class="input-background" width="62" height="32" rx="5" transform="translate(6.5 0)" />
+ <def id="TRIGGER_PARAM" xlink:href="#button" transform="translate(18 7)"/>
+ <def id="TRIGGER_INPUT" xlink:href="#input" transform="translate(40.5 4)"/>
+ <text class="input-label" font-size="5pt" letter-spacing="2px" text-anchor="middle" transform="translate(15 16) rotate(-90)">TRIG</text>
+
+ <g transform="translate(16 30)">
+ <rect class="input-background" width="43" height="32" rx="5" transform="translate(0 0)" />
+ <rect class="input-background" width="43" height="10" transform="translate(0 0)" />
+ <def id="TEST_INPUT" xlink:href="#input" transform="translate(14.5 4)"/>
+ <text class="input-label" font-size="5pt" letter-spacing="2px" text-anchor="middle" transform="translate(10 16.5) rotate(-90)">TEST</text>
+ </g>
+
+ <g transform="translate(0 67)">
+ <g transform="translate(5 0)">
+ <text font-size="5pt" letter-spacing="1.5px" transform="translate(0 6)">EXP</text>
+ <def id="EXPONENTIAL_PARAM" xlink:href="#button-small" transform="translate(18 -1)"/>
+ </g>
+ <g transform="translate(38 0)">
+ <text font-size="5pt" letter-spacing="1.5px" transform="translate(0 6)">LOOP</text>
+ <def id="LOOP_PARAM" xlink:href="#button-small" transform="translate(24 -1)"/>
+ </g>
+ </g>
+ </g>
+
+ <g transform="translate(0 237.5)">
+ <text font-size="7pt" letter-spacing="1.5px" text-anchor="middle" transform="translate(37.5 0)">R. DELAY</text>
+ <def id="DELAY_PARAM" xlink:href="#knob16" transform="translate(37.5-8.0 12)"/>
+ <use xlink:href="#dial-delay-ranalyzer" transform="translate(0 0)"/>
+ </g>
+
+ <g class="io-group" transform="translate(0 283)">
+ <rect class="output-background" width="64" height="77" rx="5" transform="translate(5.5 0)" />
+ <def id="TRIGGER_OUTPUT" xlink:href="#output" transform="translate(10.5 3)"/>
+ <text class="output-label" font-size="5pt" letter-spacing="2px" text-anchor="middle" transform="translate(22.5 35)">TRIG</text>
+ <def id="EOC_OUTPUT" xlink:href="#output" transform="translate(40.5 3)"/>
+ <text class="output-label" font-size="5pt" letter-spacing="2px" text-anchor="middle" transform="translate(52.5 35)">EOC</text>
+ <def id="SEND_OUTPUT" xlink:href="#output" transform="translate(10.5 40)"/>
+ <text class="output-label" font-size="5pt" letter-spacing="2px" text-anchor="middle" transform="translate(22.5 72)">SEND</text>
+ <g transform="translate(30 36)">
+ <rect class="input-background" width="32" height="39" rx="5" transform="translate(7.5 2)"/>
+ <rect class="input-background-filler" width="32" height="10" transform="translate(7.5 2)"/>
+ <rect class="input-background-filler" width="10" height="10" transform="translate(7.5 31)"/>
+ <def id="RETURN_INPUT" xlink:href="#input" transform="translate(10.5 4)"/>
+ <text class="input-label" font-size="5pt" letter-spacing="0.5px" text-anchor="middle" transform="translate(22.5 36)">RETURN</text>
+ </g>
+ </g>
+
+ <def id="DISPLAY_WIDGET" xlink:href="#display" var-width="600" var-height="380" transform="translate(75 0)"/>
+
+ <!-- <polyline class="guide" points="0,0 0,380" stroke-width="1" fill="none" transform="translate(37.5 0)"/> -->
+</module>
diff --git a/res-src/styles.css b/res-src/styles.css
@@ -15,6 +15,9 @@ text.brand {
polyline {
stroke: #333;
}
+polyline.guide {
+ stroke: #0f0;
+}
path {
stroke: #333;
}
diff --git a/res/Ranalyzer.svg b/res/Ranalyzer.svg
Binary files differ.
diff --git a/src/AnalyzerXL.cpp b/src/AnalyzerXL.cpp
@@ -40,6 +40,7 @@ json_t* AnalyzerXL::toJson(json_t* root) {
json_object_set_new(root, QUALITY_KEY, json_string(QUALITY_ULTRA_KEY));
break;
}
+ default:;
}
switch (_window) {
case AnalyzerCore::WINDOW_NONE: {
diff --git a/src/Ranalyzer.cpp b/src/Ranalyzer.cpp
@@ -0,0 +1,266 @@
+
+#include "Ranalyzer.hpp"
+
+#define RANGE_KEY "range"
+#define RANGE_DB_KEY "range_db"
+#define DISPLAY_ALL "display_all"
+
+void Ranalyzer::reset() {
+ _trigger.reset();
+ _triggerPulseGen.process(10.0f);
+ _eocPulseGen.process(10.0f);
+ _core.resetChannels();
+ _chirp.reset();
+ _run = false;
+}
+
+void Ranalyzer::sampleRateChange() {
+ reset();
+ _sampleRate = APP->engine->getSampleRate();
+ _sampleTime = 1.0f / _sampleRate;
+ _maxFrequency = roundf(maxFrequencyNyquistRatio * _sampleRate);
+ _chirp.setSampleRate(_sampleRate);
+}
+
+json_t* Ranalyzer::toJson(json_t* root) {
+ json_object_set_new(root, RANGE_KEY, json_real(_range));
+ json_object_set_new(root, RANGE_DB_KEY, json_real(_rangeDb));
+ json_object_set_new(root, DISPLAY_ALL, json_boolean(_displayAll));
+ return root;
+}
+
+void Ranalyzer::fromJson(json_t* root) {
+ json_t* jr = json_object_get(root, RANGE_KEY);
+ if (jr) {
+ _range = clamp(json_real_value(jr), -0.9f, 0.8f);
+ }
+
+ json_t* jrd = json_object_get(root, RANGE_DB_KEY);
+ if (jrd) {
+ _rangeDb = clamp(json_real_value(jrd), 80.0f, 140.0f);
+ }
+
+ json_t* da = json_object_get(root, DISPLAY_ALL);
+ if (da) {
+ setDisplayAll(json_boolean_value(da));
+ }
+}
+
+void Ranalyzer::modulate() {
+ _rangeMinHz = 0.0f;
+ _rangeMaxHz = 0.5f * _sampleRate;
+ if (_range < 0.0f) {
+ _rangeMaxHz *= 1.0f + _range;
+ }
+ else if (_range > 0.0f) {
+ _rangeMinHz = _range * _rangeMaxHz;
+ }
+
+ _exponential = params[EXPONENTIAL_PARAM].getValue() > 0.5f;
+ _loop = params[LOOP_PARAM].getValue() > 0.5f;
+ _returnSampleDelay = clamp((int)roundf(params[DELAY_PARAM].getValue()), 2, maxResponseDelay);
+
+ _frequency1 = clamp(params[FREQUENCY1_PARAM].getValue(), 0.0f, 1.0f);
+ _frequency1 *= _maxFrequency - minFrequency;
+ _frequency1 += minFrequency;
+
+ _frequency2 = clamp(params[FREQUENCY2_PARAM].getValue(), 0.0f, 1.0f);
+ _frequency2 *= _maxFrequency - minFrequency;
+ _frequency2 += minFrequency;
+}
+
+void Ranalyzer::processAll(const ProcessArgs& args) {
+ bool triggered = _trigger.process(params[TRIGGER_PARAM].getValue()*5.0f + inputs[TRIGGER_INPUT].getVoltage());
+ if (!_run) {
+ if (triggered || _loop) {
+ _run = true;
+ _bufferCount = _currentReturnSampleDelay = _returnSampleDelay;
+ _chirp.reset();
+ _chirp.setParams(_frequency1, _frequency2, (float)_core.size() / _sampleRate, !_exponential);
+ _triggerPulseGen.trigger(0.001f);
+ }
+ }
+
+ float out = 0.0f;
+ if (_run) {
+ if (inputs[TEST_INPUT].isConnected()) {
+ out = inputs[TEST_INPUT].getVoltage();
+ }
+ else {
+ out = _chirp.next() * 10.0f;
+ }
+ _inputBuffer.push(out);
+ if (_bufferCount > 0) {
+ --_bufferCount;
+ }
+ else {
+ _core.stepChannelSample(0, _inputBuffer.value(_currentReturnSampleDelay - 1));
+ _core.stepChannelSample(1, inputs[RETURN_INPUT].getVoltage());
+ }
+
+ if (_chirp.isCycleComplete()) {
+ _run = false;
+ _flush = true;
+ _bufferCount = _currentReturnSampleDelay;
+ }
+ }
+ else if (_flush) {
+ _core.stepChannelSample(0, _inputBuffer.value(_currentReturnSampleDelay - 1));
+ _core.stepChannelSample(1, inputs[RETURN_INPUT].getVoltage());
+ --_bufferCount;
+ if (_bufferCount < 1) {
+ _flush = false;
+ _eocPulseGen.trigger(0.001f);
+ }
+ }
+
+ outputs[SEND_OUTPUT].setVoltage(out);
+ outputs[TRIGGER_OUTPUT].setVoltage(_triggerPulseGen.process(_sampleTime) * 5.0f);
+ outputs[EOC_OUTPUT].setVoltage(_eocPulseGen.process(_sampleTime) * 5.0f);
+}
+
+void Ranalyzer::setDisplayAll(bool displayAll) {
+ _displayAll = displayAll;
+ if (_channelDisplayListener) {
+ if (_displayAll) {
+ _channelDisplayListener->displayChannels(true, true, true);
+ }
+ else {
+ _channelDisplayListener->displayChannels(false, false, true);
+ }
+ }
+}
+
+void Ranalyzer::setChannelDisplayListener(ChannelDisplayListener* listener) {
+ _channelDisplayListener = listener;
+}
+
+
+struct AnalysisBinsReader : AnalyzerDisplay::BinsReader {
+ AnalysisBinsReader(Ranalyzer* module) : AnalyzerDisplay::BinsReader(module) {}
+
+ float at(int i) override {
+ assert(_base->_core._nChannels == 3);
+
+ float test = _base->_core.getBins(0)[i];
+ float response = _base->_core.getBins(1)[i];
+ if (test > 0.0001f) {
+ return response / test;
+ }
+ return response;
+ }
+};
+
+
+struct RanalyzerDisplay : AnalyzerDisplay, ChannelDisplayListener {
+ RanalyzerDisplay(Ranalyzer* module, Vec size, bool drawInset)
+ : AnalyzerDisplay(module, size, drawInset)
+ {}
+
+ void displayChannels(bool c0, bool c1, bool c2) override {
+ displayChannel(0, c0);
+ displayChannel(1, c1);
+ displayChannel(2, c2);
+ }
+};
+
+
+struct RanalyzerWidget : BGModuleWidget {
+ static constexpr int hp = 45;
+
+ RanalyzerWidget(Ranalyzer* module) {
+ setModule(module);
+ box.size = Vec(RACK_GRID_WIDTH * hp, RACK_GRID_HEIGHT);
+ setPanel(box.size, "Ranalyzer", false);
+
+ {
+ auto inset = Vec(75, 1);
+ auto size = Vec(box.size.x - inset.x - 1, 378);
+ auto display = new RanalyzerDisplay(module, size, false);
+ display->box.pos = inset;
+ display->box.size = size;
+ if (module) {
+ display->setChannelBinsReader(2, new AnalysisBinsReader(module));
+ module->setChannelDisplayListener(display);
+ }
+ addChild(display);
+ }
+
+ // generated by svg_widgets.rb
+ auto frequency1ParamPosition = Vec(24.5, 42.0);
+ auto frequency2ParamPosition = Vec(24.5, 103.5);
+ auto triggerParamPosition = Vec(18.0, 154.0);
+ auto exponentialParamPosition = Vec(23.0, 213.0);
+ auto loopParamPosition = Vec(62.0, 213.0);
+ auto delayParamPosition = Vec(29.5, 249.5);
+
+ auto triggerInputPosition = Vec(40.5, 151.0);
+ auto testInputPosition = Vec(30.5, 181.0);
+ auto returnInputPosition = Vec(40.5, 323.0);
+
+ auto triggerOutputPosition = Vec(10.5, 286.0);
+ auto eocOutputPosition = Vec(40.5, 286.0);
+ auto sendOutputPosition = Vec(10.5, 323.0);
+ // end generated by svg_widgets.rb
+
+ {
+ auto w = createParam<Knob26>(frequency1ParamPosition, module, Ranalyzer::FREQUENCY1_PARAM);
+ auto k = dynamic_cast<BGKnob*>(w);
+ k->skinChanged("dark");
+ addParam(w);
+ }
+ {
+ auto w = createParam<Knob26>(frequency2ParamPosition, module, Ranalyzer::FREQUENCY2_PARAM);
+ auto k = dynamic_cast<BGKnob*>(w);
+ k->skinChanged("dark");
+ addParam(w);
+ }
+ addParam(createParam<Button18>(triggerParamPosition, module, Ranalyzer::TRIGGER_PARAM));
+ addParam(createParam<IndicatorButtonGreen9>(exponentialParamPosition, module, Ranalyzer::EXPONENTIAL_PARAM));
+ addParam(createParam<IndicatorButtonGreen9>(loopParamPosition, module, Ranalyzer::LOOP_PARAM));
+ {
+ auto w = createParam<Knob16>(delayParamPosition, module, Ranalyzer::DELAY_PARAM);
+ auto k = dynamic_cast<SvgKnob*>(w);
+ k->snap = true;
+ addParam(w);
+ }
+
+ addInput(createInput<Port24>(triggerInputPosition, module, Ranalyzer::TRIGGER_INPUT));
+ addInput(createInput<Port24>(testInputPosition, module, Ranalyzer::TEST_INPUT));
+ addInput(createInput<Port24>(returnInputPosition, module, Ranalyzer::RETURN_INPUT));
+
+ addOutput(createOutput<Port24>(triggerOutputPosition, module, Ranalyzer::TRIGGER_OUTPUT));
+ addOutput(createOutput<Port24>(eocOutputPosition, module, Ranalyzer::EOC_OUTPUT));
+ addOutput(createOutput<Port24>(sendOutputPosition, module, Ranalyzer::SEND_OUTPUT));
+ }
+
+ void contextMenu(Menu* menu) override {
+ auto a = dynamic_cast<Ranalyzer*>(module);
+ assert(a);
+
+ menu->addChild(new MenuLabel());
+ {
+ OptionsMenuItem* mi = new OptionsMenuItem("Frequency range");
+ mi->addItem(OptionMenuItem("Lower 25%", [a]() { return a->_range == -0.75f; }, [a]() { a->_range = -0.75f; }));
+ mi->addItem(OptionMenuItem("Lower 50%", [a]() { return a->_range == -0.5f; }, [a]() { a->_range = -0.5f; }));
+ mi->addItem(OptionMenuItem("Full", [a]() { return a->_range == 0.0f; }, [a]() { a->_range = 0.0f; }));
+ mi->addItem(OptionMenuItem("Upper 50%", [a]() { return a->_range == 0.5f; }, [a]() { a->_range = 0.5f; }));
+ mi->addItem(OptionMenuItem("Upper 25%", [a]() { return a->_range == 0.75f; }, [a]() { a->_range = 0.75f; }));
+ OptionsMenuItem::addToMenu(mi, menu);
+ }
+ {
+ OptionsMenuItem* mi = new OptionsMenuItem("Amplitude range");
+ mi->addItem(OptionMenuItem("To -60dB", [a]() { return a->_rangeDb == 80.0f; }, [a]() { a->_rangeDb = 80.0f; }));
+ mi->addItem(OptionMenuItem("To -120dB", [a]() { return a->_rangeDb == 140.0f; }, [a]() { a->_rangeDb = 140.0f; }));
+ OptionsMenuItem::addToMenu(mi, menu);
+ }
+ {
+ OptionsMenuItem* mi = new OptionsMenuItem("Display traces");
+ mi->addItem(OptionMenuItem("All", [a]() { return a->_displayAll; }, [a]() { a->setDisplayAll(true); }));
+ mi->addItem(OptionMenuItem("Analysis only", [a]() { return !a->_displayAll; }, [a]() { a->setDisplayAll(false); }));
+ OptionsMenuItem::addToMenu(mi, menu);
+ }
+ }
+};
+
+Model* modelRanalyzer = createModel<Ranalyzer, RanalyzerWidget>("Bogaudio-Ranalyzer", "RANALYZER", "Swept-sine frequency response analyzer", "Visual");
diff --git a/src/Ranalyzer.hpp b/src/Ranalyzer.hpp
@@ -0,0 +1,117 @@
+#pragma once
+
+#include "bogaudio.hpp"
+#include "analyzer_base.hpp"
+#include "dsp/oscillator.hpp"
+
+extern Model* modelRanalyzer;
+
+using namespace bogaudio::dsp;
+
+namespace bogaudio {
+
+struct ChannelDisplayListener {
+ virtual void displayChannels(bool c0, bool c1, bool c2) = 0;
+};
+
+struct Ranalyzer : AnalyzerBase {
+ enum ParamsIds {
+ FREQUENCY1_PARAM,
+ FREQUENCY2_PARAM,
+ TRIGGER_PARAM,
+ EXPONENTIAL_PARAM,
+ LOOP_PARAM,
+ DELAY_PARAM,
+ NUM_PARAMS
+ };
+
+ enum InputsIds {
+ TRIGGER_INPUT,
+ RETURN_INPUT,
+ TEST_INPUT,
+ NUM_INPUTS
+ };
+
+ enum OutputsIds {
+ TRIGGER_OUTPUT,
+ EOC_OUTPUT,
+ SEND_OUTPUT,
+ NUM_OUTPUTS
+ };
+
+ static constexpr float minFrequency = 1.0f;
+ static constexpr float maxFrequencyNyquistRatio = 0.49f;
+ static constexpr int maxResponseDelay = 20;
+
+ struct FrequencyParamQuantity : ParamQuantity {
+ float getDisplayValue() override {
+ float v = getValue();
+ if (!module) {
+ return v;
+ }
+
+ float vv = v * v;
+ vv *= roundf(APP->engine->getSampleRate() * Ranalyzer::maxFrequencyNyquistRatio) - Ranalyzer::minFrequency;
+ vv += Ranalyzer::minFrequency;
+ return vv;
+ }
+
+ void setDisplayValue(float displayValue) override {
+ if (!module) {
+ return;
+ }
+ displayValue -= Ranalyzer::minFrequency;
+ displayValue = std::max(0.0f, displayValue);
+ float v = displayValue / (roundf(APP->engine->getSampleRate() * Ranalyzer::maxFrequencyNyquistRatio) - Ranalyzer::minFrequency);
+ v = powf(v, 0.5f);
+ setValue(v);
+ }
+ };
+
+ PureChirpOscillator _chirp;
+ Trigger _trigger;
+ rack::dsp::PulseGenerator _triggerPulseGen;
+ rack::dsp::PulseGenerator _eocPulseGen;
+ float _sampleRate = 0.0f;
+ float _sampleTime = 0.0f;
+ float _maxFrequency = 0.0f;
+ bool _exponential = true;
+ bool _loop = false;
+ float _frequency1 = 0.0f;
+ float _frequency2 = 0.0f;
+ bool _run = false;
+ bool _flush = false;
+ int _returnSampleDelay = 2;
+ int _currentReturnSampleDelay = 0;
+ int _bufferCount = 0;
+ HistoryBuffer<float> _inputBuffer;
+ float _range = 0.0f;
+ bool _displayAll = true;
+ ChannelDisplayListener* _channelDisplayListener = NULL;
+
+ Ranalyzer()
+ : AnalyzerBase(3, NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS)
+ , _inputBuffer(maxResponseDelay, 0.0f)
+ {
+ configParam<FrequencyParamQuantity>(FREQUENCY1_PARAM, 0.0f, 1.0f, 0.0f, "Frequency 1", " Hz");
+ configParam<FrequencyParamQuantity>(FREQUENCY2_PARAM, 0.0f, 1.0f, 1.0f, "Frequency 2", " Hz");
+ configParam(TRIGGER_PARAM, 0.0f, 1.0f, 0.0f, "Trigger");
+ configParam(EXPONENTIAL_PARAM, 0.0f, 1.0f, 1.0f, "Exponential");
+ configParam(LOOP_PARAM, 0.0f, 1.0f, 0.0f, "Loop");
+ configParam(DELAY_PARAM, 2.0f, (float)maxResponseDelay, 2.0f, "Return sample delay");
+
+ _skinnable = false;
+ _core.setParams(1, AnalyzerCore::QUALITY_FIXED_16K, AnalyzerCore::WINDOW_NONE);
+ }
+
+ void reset() override;
+ void sampleRateChange() override;
+ json_t* toJson(json_t* root) override;
+ void fromJson(json_t* root) override;
+ void modulate() override;
+ void processAll(const ProcessArgs& args) override;
+ void setDisplayAll(bool displayAll);
+ void setChannelDisplayListener(ChannelDisplayListener* listener);
+};
+
+} // namespace bogaudio
diff --git a/src/analyzer_base.cpp b/src/analyzer_base.cpp
@@ -116,6 +116,13 @@ void AnalyzerCore::resetChannels() {
}
SpectrumAnalyzer::Size AnalyzerCore::size() {
+ switch (_quality) {
+ case QUALITY_FIXED_16K: {
+ return SpectrumAnalyzer::SIZE_16384;
+ }
+ default:;
+ }
+
if (APP->engine->getSampleRate() < 96000.0f) {
switch (_quality) {
case QUALITY_ULTRA: {
@@ -188,21 +195,7 @@ void AnalyzerCore::stepChannel(int channelIndex, Input& input) {
assert(channelIndex < _nChannels);
if (input.isConnected()) {
- if (!_channels[channelIndex]) {
- std::lock_guard<std::mutex> lock(_channelsMutex);
- _channels[channelIndex] = new ChannelAnalyzer(
- _size,
- _overlap,
- window(),
- APP->engine->getSampleRate(),
- _averageN,
- _binAverageN,
- _outBufs + 2 * channelIndex * _outBufferN,
- _outBufs + (2 * channelIndex + 1) * _outBufferN,
- _currentOutBufs[channelIndex]
- );
- }
- _channels[channelIndex]->step(input.getVoltageSum());
+ stepChannelSample(channelIndex, input.getVoltageSum());
}
else if (_channels[channelIndex]) {
std::lock_guard<std::mutex> lock(_channelsMutex);
@@ -211,6 +204,44 @@ void AnalyzerCore::stepChannel(int channelIndex, Input& input) {
}
}
+void AnalyzerCore::stepChannelSample(int channelIndex, float sample) {
+ assert(channelIndex >= 0);
+ assert(channelIndex < _nChannels);
+
+ if (!_channels[channelIndex]) {
+ std::lock_guard<std::mutex> lock(_channelsMutex);
+ _channels[channelIndex] = new ChannelAnalyzer(
+ _size,
+ _overlap,
+ window(),
+ APP->engine->getSampleRate(),
+ _averageN,
+ _binAverageN,
+ _outBufs + 2 * channelIndex * _outBufferN,
+ _outBufs + (2 * channelIndex + 1) * _outBufferN,
+ _currentOutBufs[channelIndex]
+ );
+ }
+ _channels[channelIndex]->step(sample);
+}
+
+
+void AnalyzerDisplay::setChannelBinsReader(int channel, BinsReader* br) {
+ assert(_channelBinsReaders);
+ assert(_module);
+ assert(channel < _module->_core._nChannels);
+ if (_channelBinsReaders[channel]) {
+ delete _channelBinsReaders[channel];
+ }
+ _channelBinsReaders[channel] = br; // br now owned here.
+}
+
+void AnalyzerDisplay::displayChannel(int channel, bool display) {
+ assert(_displayChannel);
+ assert(_module);
+ assert(channel < _module->_core._nChannels);
+ _displayChannel[channel] = display;
+}
void AnalyzerDisplay::draw(const DrawArgs& args) {
if (_module) {
@@ -245,8 +276,14 @@ void AnalyzerDisplay::draw(const DrawArgs& args) {
drawXAxis(args, strokeWidth, rangeMinHz, rangeMaxHz);
if (_module) {
for (int i = 0; i < _module->_core._nChannels; ++i) {
- if (_module->_core._channels[i]) {
- drawGraph(args, _module->_core.getBins(i), _module->_core._binsN, _channelColors[i % channelColorsN], strokeWidth, rangeMinHz, rangeMaxHz, rangeDb);
+ if (_displayChannel[i]) {
+ if (_module->_core._channels[i]) {
+ GenericBinsReader br(_module, i);
+ drawGraph(args, br, _channelColors[i % channelColorsN], strokeWidth, rangeMinHz, rangeMaxHz, rangeDb);
+ }
+ else if (_channelBinsReaders[i]) {
+ drawGraph(args, *_channelBinsReaders[i], _channelColors[i % channelColorsN], strokeWidth, rangeMinHz, rangeMaxHz, rangeDb);
+ }
}
}
}
@@ -499,8 +536,7 @@ void AnalyzerDisplay::drawXAxisLine(const DrawArgs& args, float hz, float rangeM
void AnalyzerDisplay::drawGraph(
const DrawArgs& args,
- const float* bins,
- int binsN,
+ BinsReader& bins,
NVGcolor color,
float strokeWidth,
float rangeMinHz,
@@ -517,7 +553,7 @@ void AnalyzerDisplay::drawGraph(
nvgStrokeWidth(args.vg, strokeWidth);
nvgBeginPath(args.vg);
for (int i = 0; i < pointsN; ++i) {
- int height = binValueToHeight(bins[pointsOffset + i], rangeDb);
+ int height = binValueToHeight(bins.at(pointsOffset + i), rangeDb);
if (i == 0) {
nvgMoveTo(args.vg, _insetLeft, _insetTop + (_graphSize.y - height));
}
diff --git a/src/analyzer_base.hpp b/src/analyzer_base.hpp
@@ -70,7 +70,8 @@ struct AnalyzerCore {
enum Quality {
QUALITY_ULTRA,
QUALITY_HIGH,
- QUALITY_GOOD
+ QUALITY_GOOD,
+ QUALITY_FIXED_16K
};
enum Window {
@@ -120,6 +121,7 @@ struct AnalyzerCore {
}
float getPeak(int channel);
void stepChannel(int channelIndex, Input& input);
+ void stepChannelSample(int channelIndex, float sample);
};
struct AnalyzerBase : BGModule {
@@ -134,6 +136,21 @@ struct AnalyzerBase : BGModule {
};
struct AnalyzerDisplay : TransparentWidget {
+ struct BinsReader {
+ AnalyzerBase* _base;
+
+ BinsReader(AnalyzerBase* base) : _base(base) {}
+ virtual ~BinsReader() {}
+ virtual float at(int i) = 0;
+ };
+
+ struct GenericBinsReader : BinsReader {
+ int _channel;
+
+ GenericBinsReader(AnalyzerBase* base, int channel) : BinsReader(base), _channel(channel) {}
+ float at(int i) override { return _base->_core.getBins(_channel)[i]; }
+ };
+
const int _insetAround = 2;
const int _insetLeft = _insetAround + 12;
const int _insetRight = _insetAround + 2;
@@ -166,6 +183,8 @@ struct AnalyzerDisplay : TransparentWidget {
bool _drawInset;
std::shared_ptr<Font> _font;
float _xAxisLogFactor = baseXAxisLogFactor;
+ BinsReader** _channelBinsReaders = NULL;
+ bool* _displayChannel = NULL;
AnalyzerDisplay(
AnalyzerBase* module,
@@ -178,15 +197,38 @@ struct AnalyzerDisplay : TransparentWidget {
, _drawInset(drawInset)
, _font(APP->window->loadFont(asset::plugin(pluginInstance, "res/fonts/inconsolata.ttf")))
{
+ if (_module) {
+ _channelBinsReaders = new BinsReader*[_module->_core._nChannels] {};
+ _displayChannel = new bool[_module->_core._nChannels] {};
+ std::fill_n(_displayChannel, _module->_core._nChannels, true);
+ }
+ }
+ ~AnalyzerDisplay() {
+ if (_module) {
+ if (_channelBinsReaders) {
+ for (int i = 0; i < _module->_core._nChannels; ++i) {
+ if (_channelBinsReaders) {
+ delete _channelBinsReaders[i];
+ }
+ }
+ delete[] _channelBinsReaders;
+ }
+
+ if (_displayChannel) {
+ delete[] _displayChannel;
+ }
+ }
}
+ void setChannelBinsReader(int channel, BinsReader* br);
+ void displayChannel(int channel, bool display);
void draw(const DrawArgs& args) override;
void drawBackground(const DrawArgs& args);
void drawHeader(const DrawArgs& args);
void drawYAxis(const DrawArgs& args, float strokeWidth, float rangeDb);
void drawXAxis(const DrawArgs& args, float strokeWidth, float rangeMinHz, float rangeMaxHz);
void drawXAxisLine(const DrawArgs& args, float hz, float rangeMinHz, float rangeMaxHz);
- void drawGraph(const DrawArgs& args, const float* bins, int binsN, NVGcolor color, float strokeWidth, float rangeMinHz, float rangeMaxHz, float rangeDb);
+ void drawGraph(const DrawArgs& args, BinsReader& bins, NVGcolor color, float strokeWidth, float rangeMinHz, float rangeMaxHz, float rangeDb);
void drawText(const DrawArgs& args, const char* s, float x, float y, float rotation = 0.0, const NVGcolor* color = NULL);
int binValueToHeight(float value, float rangeDb);
};
diff --git a/src/bogaudio.cpp b/src/bogaudio.cpp
@@ -75,6 +75,7 @@
#include "PolyMult.hpp"
#include "Pressor.hpp"
#include "Pulse.hpp"
+#include "Ranalyzer.hpp"
#include "Reftone.hpp"
#include "RGate.hpp"
#include "SampleHold.hpp"
@@ -191,9 +192,10 @@ void init(rack::Plugin *p) {
p->addModel(modelPgmr);
p->addModel(modelPgmrX);
+ p->addModel(modelVU);
p->addModel(modelAnalyzer);
p->addModel(modelAnalyzerXL);
- p->addModel(modelVU);
+ p->addModel(modelRanalyzer);
p->addModel(modelDetune);
p->addModel(modelStack);
diff --git a/src/dsp/oscillator.cpp b/src/dsp/oscillator.cpp
@@ -334,12 +334,14 @@ void ChirpOscillator::_sampleRateChanged() {
}
float ChirpOscillator::_next() {
- _time += _sampleTime;
_complete = false;
- if (_time >= _Time) {
+ if (_time > _Time) {
_time = 0.0f;
_complete = true;
}
+ else {
+ _time += _sampleTime;
+ }
if (_linear) {
_oscillator.setFrequency(_f1 + (_time / _Time) * (_f2 - _f1));
@@ -349,3 +351,59 @@ float ChirpOscillator::_next() {
}
return _oscillator.next();
}
+
+void ChirpOscillator::reset() {
+ _time = 0.0f;
+}
+
+
+void PureChirpOscillator::setParams(float frequency1, float frequency2, float time, bool linear) {
+ frequency1 = std::max(minFrequency, std::min(frequency1, 0.99f * 0.5f * _sampleRate));
+ frequency2 = std::max(minFrequency, std::min(frequency2, 0.99f * 0.5f * _sampleRate));
+ assert(time >= minTimeSeconds);
+
+ if (_f1 != frequency1 || _f2 != frequency2 || _Time != time || _linear != linear) {
+ _f1 = frequency1;
+ _f2 = frequency2;
+ _Time = time;
+ _linear = linear;
+ update();
+ }
+}
+
+void PureChirpOscillator::_sampleRateChanged() {
+ _sampleTime = 1.0f / _sampleRate;
+ update();
+}
+
+void PureChirpOscillator::update() {
+ _Time = std::max(2.0f * _sampleTime, _Time);
+ _c = (double)(_f2 - _f1) / (double)_Time;
+ _k = pow((double)(_f2 / _f1), 1.0f / (double)_Time);
+ _invlogk = 1.0 / log(_k);
+}
+
+float PureChirpOscillator::_next() {
+ _complete = false;
+ if (_time > _Time) {
+ _time = 0.0f;
+ _complete = true;
+ }
+ else {
+ _time += _sampleTime;
+ }
+
+ // formulas from https://en.wikipedia.org/wiki/Chirp
+ float phase = 0.0f;
+ if (_linear) {
+ phase = 2.0 * M_PI * (0.5 * _c * (double)(_time * _time) + (double)(_f1 * _time));
+ }
+ else {
+ phase = 2.0 * M_PI * (double)_f1 * ((pow(_k, (double)_time) - 1.0) * _invlogk);
+ }
+ return _phasor.nextForPhase(Phasor::radiansToPhase(phase));
+}
+
+void PureChirpOscillator::reset() {
+ _time = 0.0f;
+}
diff --git a/src/dsp/oscillator.hpp b/src/dsp/oscillator.hpp
@@ -364,6 +364,45 @@ struct ChirpOscillator : OscillatorGenerator {
void setParams(float frequency1, float frequency2, float time, bool linear);
void _sampleRateChanged() override;
float _next() override;
+ void reset();
+};
+
+struct PureChirpOscillator : OscillatorGenerator {
+ static constexpr float minFrequency = 1.0f;
+ static constexpr float minTimeSeconds = 0.025f;
+
+ TablePhasor _phasor;
+ float _f1 = -1.0f;
+ float _f2 = -1.0f;
+ float _Time = -1.0f;
+ bool _linear = false;
+
+ float _sampleTime = 0.0f;
+ float _time = 0.0f;
+ bool _complete = false;
+ double _c = 0.0;
+ double _k = 0.0;
+ double _invlogk = 0.0;
+
+ PureChirpOscillator(
+ float sampleRate = 1000.0f,
+ float frequency1 = 100.0f,
+ float frequency2 = 300.0f,
+ float time = 1.0f,
+ bool linear = true
+ )
+ : _phasor(StaticSineTable::table(), sampleRate, frequency1)
+ {
+ setParams(frequency1, frequency2, time, linear);
+ }
+
+ inline bool isCycleComplete() { return _complete; }
+ inline bool isCycleNearlyComplete(float seconds) { return _time > _Time - seconds; }
+ void setParams(float frequency1, float frequency2, float time, bool linear);
+ void _sampleRateChanged() override;
+ void update();
+ float _next() override;
+ void reset();
};
} // namespace dsp