Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ jobs:
run: |
python -m pip install -q -i https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
"omnipython-audiodsp>=0.6.13.dev7" numpy pytest ruff==0.17.1
"omnipython-audiodsp>=0.6.13.dev8" numpy pytest ruff==0.17.1
python -m pip list 2>/dev/null | grep -iE "^(omnipython-audiodsp|numpy|pytest|ruff) "

- name: Lint
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-release-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
run: |
python -m pip install -q -i https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
"omnipython-audiodsp>=0.6.13.dev7" numpy pytest
"omnipython-audiodsp>=0.6.13.dev8" numpy pytest
python -m pip install -q --no-deps -e .
- name: The faces carry this version
run: |
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ All notable changes to `audioinstruments` are recorded here.
- StateVariableMono has twelve patches, from Fat Bass and Brass Stab to Notch Pad and
Squelch Zap, and `examples/state_variable_mono_demo.py` plays them. Patch 0 is the Init
patch as it was.
- StateVariableMono's eleven patches after Init are about 5 dB louder, to sit with the other
module voices.
- StateVariableMono's fast filter sweeps open as fast on the graph as on the rack, by setting
audiodsp's new `BIQUAD_RAMP` to one frame on its filters. Every patch's sound moves a little on
the graph, Init included, and most on the fast sweeps of Fat Bass, Brass Stab, Nasal Reed, Wobble Bass
and Squelch Zap. An audiodsp without the option plays as before.
- MonoOne has twelve patches, from Fat Bass and Sync Lead to Metal Bell and Wind,
and `examples/mono_one_demo.py` plays them. Patch 0 is the Init patch as it was.
- WestCoastSynth has twelve patches, from Soft Ping and Water Drops to Glass Chime and Wandering Fold,
Expand Down
3 changes: 0 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ Planned work, in rough order:
- Notes on their own frame with the graph on the audio pump. A note played in
a turn lands at the next block; with `audiograph` setting a value at a frame
inside a block, `schedulable` can be True and a sequencer can time notes.
- StateVariableMono's filter envelope opening as fast on the graph as on the rack, once
audiodsp's `BIQUAD` node can take a new cutoff at once instead of walking to it over
20 ms. The patches in `tests/test_module_patches.py`'s `FAST_SWEEP` then lose their waiver.
- Polyphony beyond the first cut: portamento between voices,
growing a pool past the voices it was built with, one pool shared by several
instruments, and notes queued on an `audiopump.Events` for frame-exact
Expand Down
27 changes: 16 additions & 11 deletions lib/audioinstruments/_rack/modules/state_variable_mono.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class StateVariableMono(Module):
"env1_sustain": 0.1,
"env2_decay": 0.3,
"env2_sustain": 0.92,
"volume": 0.99,
"volume": 1.081,
},
),
(
Expand All @@ -85,7 +85,7 @@ class StateVariableMono(Module):
"env2_attack": 0.03,
"env2_decay": 0.35,
"env2_sustain": 0.95,
"volume": 1.06,
"volume": 1.148,
},
),
(
Expand All @@ -106,7 +106,7 @@ class StateVariableMono(Module):
"env2_attack": 0.7,
"env2_decay": 1.2,
"env2_sustain": 1.0,
"volume": 1.01,
"volume": 1.093,
},
),
(
Expand All @@ -127,7 +127,7 @@ class StateVariableMono(Module):
"env2_attack": 0.3,
"env2_decay": 0.9,
"env2_sustain": 1.0,
"volume": 0.97,
"volume": 1.053,
},
(("lfo_tri", "vco1_pw"),),
),
Expand All @@ -147,7 +147,7 @@ class StateVariableMono(Module):
"env1_sustain": 0.5,
"env2_decay": 0.4,
"env2_sustain": 1.0,
"volume": 0.96,
"volume": 1.043,
},
),
(
Expand All @@ -165,7 +165,7 @@ class StateVariableMono(Module):
"env2_attack": 0.5,
"env2_decay": 1.0,
"env2_sustain": 1.0,
"volume": 1.0,
"volume": 1.083,
},
),
(
Expand All @@ -183,7 +183,7 @@ class StateVariableMono(Module):
"env1_sustain": 0.0,
"env2_decay": 0.25,
"env2_sustain": 0.55,
"volume": 1.1,
"volume": 1.18,
},
),
(
Expand All @@ -205,7 +205,7 @@ class StateVariableMono(Module):
"env2_attack": 0.02,
"env2_decay": 0.3,
"env2_sustain": 1.0,
"volume": 1.03,
"volume": 1.113,
},
),
(
Expand All @@ -224,7 +224,7 @@ class StateVariableMono(Module):
"env1_sustain": 0.7,
"env2_decay": 0.4,
"env2_sustain": 1.0,
"volume": 1.0,
"volume": 1.083,
},
),
(
Expand All @@ -243,7 +243,7 @@ class StateVariableMono(Module):
"env1_sustain": 1.0,
"env2_decay": 0.3,
"env2_sustain": 1.0,
"volume": 0.97,
"volume": 1.053,
},
),
(
Expand All @@ -260,7 +260,7 @@ class StateVariableMono(Module):
"env1_sustain": 0.0,
"env2_decay": 0.25,
"env2_sustain": 0.7,
"volume": 1.03,
"volume": 1.133,
},
),
)
Expand Down Expand Up @@ -544,6 +544,11 @@ def fragment(self, f):
f.set(node, ag.BIQUAD_Q, 0.7071)
f.set(node, ag.BIQUAD_GAIN_DB, 0.0)
f.set(node, ag.BIQUAD_MIX, 1.0)
# The rack's filter takes a new cutoff at once, and a fast envelope moves it every
# block: walk to each in one frame, so a sweep opens as fast here. An audiodsp too
# old to have this walks for 20 ms and plays the same notes a little duller.
if hasattr(ag, "BIQUAD_RAMP"):
f.set(node, ag.BIQUAD_RAMP, 0.0)
self._n_vcf = f.add(ag.MIX, (b_lp, b_hp, b_bp), f.output(self.vcf_out))

b_vcv = f.buffer()
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ classifiers = [
keywords = ["instruments", "synthesizer", "eurorack", "audio", "dsp", "micropython", "midi"]
dependencies = [
"numpy>=1.20",
"omnipython-audiodsp>=0.6.13.dev7",
"omnipython-audiodsp>=0.6.13.dev8",
]

[project.optional-dependencies]
Expand Down
4 changes: 2 additions & 2 deletions tests/test_graph_micropython.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
#: Voices whose filter cutoff goes through Python's pow every block: the most a sample may
#: differ between interpreters, and how many may (of 198144 a channel). The Poly80's moving
#: filters measure 3 counts on 5436 samples at the worst of its four phrases.
#: The StateVariableMono measures 1 count on 96 samples, with its VCA 6 dB down.
FLOAT_POW = {"state_variable_mono": (1.0, 128), "poly80": (4.0, 8000)}
#: The StateVariableMono measures 1 count on 144 samples, with its VCA 6 dB down.
FLOAT_POW = {"state_variable_mono": (1.0, 192), "poly80": (4.0, 8000)}

CASES = [
("hybrid_mono", 0),
Expand Down
54 changes: 46 additions & 8 deletions tests/test_graph_state_variable_mono.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,12 @@
graph's 16-bit wires: the VCO mix and the two biquad outputs round to a count
on the way into the morph mixer and the VCA.
* When Env 1 or the LFO moves the cutoff once a block as the rack does,
``synthio.Biquad`` in the rack steps direct-form coefficients instantly
without slewing, whereas ``audiograph.BIQUAD`` runs a 20 ms (1024-sample,
``AUDIODSP_BIQUAD_F32_RAMP_MS``) parameter walk through an internal SVF
topology whenever coefficients move. Because cutoff updates every block
(5.3 ms), the graph filter continuously slews across the 20 ms window.
At slow LFO rates (0.25 Hz) the difference is 19 counts rms (0.84% of
level); at 6 Hz it is 258 counts rms (11% of level) and converges when
modulation settles. Over the note it matches within 11% rms of level.
``synthio.Biquad`` in the rack takes each new cutoff at once, and the graph's
``BIQUAD`` walks to it in one frame (``BIQUAD_RAMP`` at 0). Across the moving
cases below that is 4.1 counts rms at the worst, 0.17% of level, and 257 counts
at the worst on the sample a move lands on. An audiodsp with no ``BIQUAD_RAMP``
walks for 20 ms and restarts the walk each block (5.3 ms), so the filter slews
behind the rack's, up to 11% rms of level, which is the bar here while it works.
* Over a phrase with voice steals the held chord matches to 32 counts and
1.5 counts rms.
* The LFO's triangle on ``lfo_tri_out`` matches within two counts and the
Expand Down Expand Up @@ -184,6 +182,46 @@ def test_an_audiograph_without_biquad_leaves_the_voice_on_the_rack(monkeypatch):
inst.deinit()


def filter_ramps():
"""Each voice's three biquads' BIQUAD_RAMP, on a fresh StateVariableMono's graph."""
import audiograph

inst = adapter.create(48000, voice="state_variable_mono", engine="graph", output="pull")
try:
assert inst.engine == "graph"
return [
module._fragment.get(node, audiograph.BIQUAD_RAMP)
for module in inst.voice_pool.modules("synth")
for node in (module._n_lp, module._n_hp, module._n_bp)
]
finally:
inst.deinit()


def test_the_filters_walk_for_one_frame_where_audiodsp_can():
import audiograph

if not hasattr(audiograph, "BIQUAD_RAMP"):
pytest.skip("this audiodsp has no BIQUAD_RAMP")
assert set(filter_ramps()) == {0.0}


def test_an_audiograph_without_the_ramp_still_compiles_the_voice(monkeypatch):
import audiograph

if not hasattr(audiograph, "BIQUAD_RAMP"):
pytest.skip("this audiodsp has no BIQUAD_RAMP to take away")
ramp = audiograph.BIQUAD_RAMP
monkeypatch.delattr(audiograph, "BIQUAD_RAMP")
inst = adapter.create(48000, voice="state_variable_mono", engine="auto", output="pull")
try:
assert inst.engine == "graph"
module = next(iter(inst.voice_pool.modules("synth")))
assert module._fragment.get(module._n_lp, ramp) == 20.0
finally:
inst.deinit()


# -- the voice --------------------------------------------------------------------------

CLEAN = [
Expand Down
18 changes: 3 additions & 15 deletions tests/test_module_patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,6 @@
"mod",
"pedal",
)
#: Patches whose filter envelope sweeps fast from a closed filter. The graph's biquad walks
#: to each new cutoff over 20 ms (``AUDIODSP_BIQUAD_F32_RAMP_MS``), and a cutoff that moves
#: every block starts the walk again, where the rack's ``synthio.Biquad`` takes the new cutoff
#: at once. The attack is duller for it, by more than LEVEL_DB. They compare overall level
#: only, until audiodsp's node can move at once.
FAST_SWEEP = (
("state_variable_mono", 1),
("state_variable_mono", 2),
("state_variable_mono", 8),
("state_variable_mono", 10),
("state_variable_mono", 11),
)


def create(name, engine):
Expand Down Expand Up @@ -197,9 +185,9 @@ def test_a_patch_is_the_same_level_on_rack_and_graph(name, index):
assert np.abs(graph).max() < adapter.PCM_SCALE
controls = get_module_class(adapter._MODULE_VOICES[name][0]).PRESETS[index][1]
noisy = any("noise" in param and value > 0.0 for param, value in controls.items())
if noisy or (name, index) in OVERALL_ONLY + RANDOM_CABLED + FAST_SWEEP:
# noise through a filter is another noise on each engine, and so is a random voltage,
# and a fast sweep opens slower on the graph's biquad: the overall level only
if noisy or (name, index) in OVERALL_ONLY + RANDOM_CABLED:
# noise through a filter is another noise on each engine, and so is a random voltage:
# the overall level only
assert abs(20.0 * np.log10(level / np.sqrt(np.mean(rack**2)))) <= LEVEL_DB
else:
assert worst_level_db(rack, graph) <= LEVEL_DB
Expand Down
Loading
Loading