Edit a Compressor in a tab of its own, with knobs and levels (#378)
A Compressor had only its number rows in the node. Its numbers are
easier to set by feel on a dial, and what it does is easier to judge
when the level going in and the level going out are seen side by side.
- Knob is a new widget in the toolkit: a slider bent into a circle,
its name above and a number field below. A drag up or down turns
it, Shift a tenth as far, a notch of the wheel a fiftieth of the
turn, a double-click sets the default, the number takes a typed
value. A taper gives the low end more of the turn.
- The Compressor's tab opens on a double-click, one per node, like
the Parametric EQ's. It has a knob for each of the seven numbers,
greyed while its input is linked.
- Its curve shows level in to level out from -60 to 0 dB. The point
where it bends sets the threshold, the end of the curve sets the
ratio that ends there.
- Its levels show the last 4 s on both sides of the node. Where it
takes more than 0.5 dB away, the stretch below the level coming in
is tinted. The makeup is left out of that.
- The Compressor is tapped like the EQ. SpectrumTaps listens to more
than one node at once and reads the loudest sample of the last
25 ms on each side.
- The node tabs share one map in the editor (nodeTabs), so a third
kind needs no new plumbing.
Findings:
- CI task 614 for 23922ee was red on the memory check of the stream:
the tab's renderer grew by 94 MB with nothing decoded. Play mode
made garbage of its own and was read before a collection. Both
readings in play now come after one, and the check compares the
growth streamed with the growth decoded (3.7 MB against 59.8 MB).
- The ratio handle sat on the right edge of the picture, where a
widget's hit area ends. The curve is inset by the grab radius.
Tests:
- Gate (melektaus-atx): 282 files, 4338 tests (17 new)
- e2e 34 (melektaus-atx): ALL PASSED, 116 checks (6 new)
- Counter-checks: an inverted ratio search, stop() letting go of
every node and a drag without its least step each fail exactly
their test.
- Looked at the tab in a screenshot: a blip at -4.4 dB came out at
-16.7 dB, the gap tinted, the threshold line at -24.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
23922ee4a2
commit
1df38bd430
20 files changed
+1471
-103
No files matched your search
@@ -23,6 +23,12 @@ class FakeNode {
|
||||
getFloatFrequencyData(data: Float32Array): void {
|
||||
data.fill(this.context.made.length);
|
||||
}
|
||||
/** What the analyser heard last: `wave`, at the end of `data` like the newest samples. */
|
||||
wave: number[] = [];
|
||||
getFloatTimeDomainData(data: Float32Array): void {
|
||||
data.fill(0);
|
||||
data.set(this.wave, data.length - this.wave.length);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeContext {
|
||||
@@ -50,7 +56,7 @@ describe('the taps of the spectrum', () => {
|
||||
taps.hold(tap('fx:eq', 'in'), new FakeNode(context) as never);
|
||||
expect(taps.info()).toEqual({ listened: '', held: 1, feeding: 0 });
|
||||
expect(context.made).toHaveLength(0);
|
||||
expect(taps.read()).toBeNull();
|
||||
expect(taps.read('fx:eq')).toBeNull();
|
||||
});
|
||||
|
||||
it('feed the analyser of their side while listened to, a voice started before as well', () => {
|
||||
@@ -69,7 +75,7 @@ describe('the taps of the spectrum', () => {
|
||||
expect([...before.links]).toEqual([sumIn]);
|
||||
expect([...after.links]).toEqual([sumOut]);
|
||||
expect(other.links.size).toBe(0);
|
||||
expect(taps.read()?.binHz).toBeCloseTo(48000 / 8192, 9);
|
||||
expect(taps.read('fx:eq')?.binHz).toBeCloseTo(48000 / 8192, 9);
|
||||
});
|
||||
|
||||
it('let go of the analysers once nobody listens, or once the voice stops', () => {
|
||||
@@ -87,6 +93,41 @@ describe('the taps of the spectrum', () => {
|
||||
taps.stop();
|
||||
expect(bus.links.size).toBe(0);
|
||||
expect(taps.info()).toEqual({ listened: '', held: 1, feeding: 0 });
|
||||
expect(taps.read()).toBeNull();
|
||||
expect(taps.read('fx:eq')).toBeNull();
|
||||
});
|
||||
|
||||
it('listen to two nodes at once, and let one go without the other (#378)', () => {
|
||||
const context = new FakeContext();
|
||||
const taps = new SpectrumTaps();
|
||||
const eq = new FakeNode(context);
|
||||
const compressor = new FakeNode(context);
|
||||
taps.hold(tap('fx:eq', 'in'), eq as never);
|
||||
taps.hold(tap('fx:comp', 'in'), compressor as never);
|
||||
taps.listen('fx:eq');
|
||||
taps.listen('fx:comp');
|
||||
expect(taps.info()).toEqual({ listened: 'fx:eq fx:comp', held: 2, feeding: 2 });
|
||||
expect(eq.links.size).toBe(1);
|
||||
expect(compressor.links.size).toBe(1);
|
||||
taps.stop('fx:eq');
|
||||
expect(eq.links.size).toBe(0);
|
||||
expect(compressor.links.size).toBe(1);
|
||||
expect(taps.info()).toEqual({ listened: 'fx:comp', held: 2, feeding: 1 });
|
||||
expect(taps.read('fx:comp')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('give the loudest sample of the last stretch on each side, in dB (#378)', () => {
|
||||
const context = new FakeContext();
|
||||
const taps = new SpectrumTaps();
|
||||
expect(taps.levels('fx:comp', 0.01)).toBeNull();
|
||||
taps.listen('fx:comp');
|
||||
taps.hold(tap('fx:comp', 'in'), new FakeNode(context) as never);
|
||||
const [, analyserIn, , analyserOut] = context.made;
|
||||
// A peak of 1 long ago, then 0.5: a window of 10 ms (480 samples) only sees the 0.5.
|
||||
analyserIn!.wave = [1, ...new Array<number>(1000).fill(0), -0.5, 0.25];
|
||||
analyserOut!.wave = [];
|
||||
const levels = taps.levels('fx:comp', 0.01);
|
||||
expect(levels?.in).toBeCloseTo(20 * Math.log10(0.5), 9);
|
||||
expect(levels?.out).toBe(-Infinity);
|
||||
expect(taps.levels('fx:comp', 1)?.in).toBeCloseTo(0, 9);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user