circuitpython-synthtools 0.7.0__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/.gitignore +7 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/.pre-commit-config.yaml +1 -1
- {circuitpython_synthtools-0.7.0/circuitpython_synthtools.egg-info → circuitpython_synthtools-0.8.0}/PKG-INFO +15 -16
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/README.rst +14 -15
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0/circuitpython_synthtools.egg-info}/PKG-INFO +15 -16
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/circuitpython_synthtools.egg-info/SOURCES.txt +16 -3
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/conftest.py +3 -1
- circuitpython_synthtools-0.8.0/docs/examples.rst +90 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/synth_setup.py +8 -1
- circuitpython_synthtools-0.8.0/examples/synthtools_bassline_accent_demo.py +219 -0
- circuitpython_synthtools-0.8.0/examples/synthtools_bassline_filter_demo.py +248 -0
- circuitpython_synthtools-0.8.0/examples/synthtools_bassline_fx_demo.py +240 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/synthtools_harmony_demo.py +2 -2
- circuitpython_synthtools-0.7.0/examples/step_sequencer_demo.py → circuitpython_synthtools-0.8.0/examples/synthtools_stepsequencer_demo.py +2 -1
- circuitpython_synthtools-0.8.0/examples/synthtools_wavetable_chords.py +88 -0
- circuitpython_synthtools-0.8.0/examples/synthtools_wavetable_simple.py +58 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/trig_sequencer_demo.py +5 -3
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/wavetables/PLAITS02.WAV +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/pyproject.toml +1 -1
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/__init__.py +1 -1
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/audio_fx.py +47 -10
- circuitpython_synthtools-0.8.0/synthtools/bassline_synth.py +1159 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/trig_sequencer.py +7 -6
- circuitpython_synthtools-0.8.0/synthtools/wavetable.py +89 -0
- circuitpython_synthtools-0.8.0/synthtools/wavetable_synth.py +215 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/README.md +18 -4
- circuitpython_synthtools-0.8.0/tests/hw/test_trig_sequencer_timing.py +123 -0
- circuitpython_synthtools-0.8.0/tests/hw/test_wavetable_cost.py +210 -0
- circuitpython_synthtools-0.8.0/tests/render/FINDINGS.md +153 -0
- circuitpython_synthtools-0.8.0/tests/render/README.md +79 -0
- circuitpython_synthtools-0.8.0/tests/render/analyze_renders.py +488 -0
- circuitpython_synthtools-0.8.0/tests/render/check_wavetable.py +108 -0
- circuitpython_synthtools-0.8.0/tests/render/check_wavetable_synth.py +168 -0
- circuitpython_synthtools-0.8.0/tests/render/render_chords.py +263 -0
- circuitpython_synthtools-0.8.0/tests/render/run.sh +51 -0
- circuitpython_synthtools-0.8.0/tests/render/wavhdr.py +48 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/run_tests.sh +2 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/test_audio_fx.py +106 -10
- circuitpython_synthtools-0.8.0/tests/test_mono.py +867 -0
- circuitpython_synthtools-0.8.0/tests/test_trig_sequencer.py +144 -0
- circuitpython_synthtools-0.7.0/docs/examples.rst +0 -68
- circuitpython_synthtools-0.7.0/examples/step_sequencer_bassdemo.py +0 -85
- circuitpython_synthtools-0.7.0/examples/synthtools_bassline_demo.py +0 -161
- circuitpython_synthtools-0.7.0/examples/synthtools_wavetable_simple.py +0 -29
- circuitpython_synthtools-0.7.0/synthtools/bassline_synth.py +0 -696
- circuitpython_synthtools-0.7.0/synthtools/wavetable.py +0 -61
- circuitpython_synthtools-0.7.0/synthtools/wavetable_synth.py +0 -303
- circuitpython_synthtools-0.7.0/tests/test_mono.py +0 -494
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/.gitattributes +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/.github/workflows/build.yml +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/.github/workflows/failure-help-text.yml +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/.github/workflows/release_gh.yml +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/.github/workflows/release_pypi.yml +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/.pylintrc +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/.readthedocs.yaml +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/CODE_OF_CONDUCT.md +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/LICENSE +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/LICENSES/CC-BY-4.0.txt +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/LICENSES/MIT.txt +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/LICENSES/Unlicense.txt +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/README.rst.license +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/circuitpython_synthtools.egg-info/dependency_links.txt +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/circuitpython_synthtools.egg-info/requires.txt +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/circuitpython_synthtools.egg-info/top_level.txt +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/docs/_static/custom.css +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/docs/_static/favicon.ico +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/docs/_static/favicon.ico.license +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/docs/api.rst +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/docs/api.rst.license +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/docs/conf.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/docs/examples.rst.license +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/docs/index.rst +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/docs/index.rst.license +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/docs/requirements.txt +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/REUSE.toml +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/arpeggiator_demo.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/synthinst_code1.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/synthinst_fenv_demo.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/synthinst_pad_demo.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/synthinst_pitch_demo.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/synthinst_wave_demo.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/synthtools_ahr_envelope_bend.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/synthtools_fm_demo.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/synthtools_midi_demo.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/synthtools_simpletest.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/synthtools_swarm_demo.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/ui/synthtools_gaugecluster_test.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/ui/synthtools_paramscaler_demo.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/wavetables/BRAIDS02.WAV +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/00_909kick4.wav +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/01_909snare2.wav +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/02_909hatclosed2a.wav +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/03_909hatopen5.wav +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/04_909clap1.wav +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/05_909tommed.wav +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/06_909blip.wav +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/07_909cym2.wav +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/readme.txt +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/optional_requirements.txt +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/requirements.txt +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/ruff.toml +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/setup.cfg +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/ahr_envelope.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/arpeggiator.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/blocks.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/fm_synth.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/gauge_cluster.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/harmony.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/param_scaler.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/paramset.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/patch.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/pitch_glider.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/step_sequencer.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/subtractive_synth.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/swarm_synth.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/synth.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/utils.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/synthtools/waves.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/hw/run_on_device.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/hw/test_device.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/hw/test_display_cost.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/plot_env.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/stubs/synthio.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/stubs/ulab/__init__.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/stubs/ulab/numpy.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/stubs_fx/audiodelays.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/stubs_fx/audiofilters.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/test_arpeggiator.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/test_env_shapes.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/test_fm_synth.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/test_harmony.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/test_param_scaler.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/test_paramset.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/test_patch.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/test_pytest_smoke.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/test_swarm_synth.py +0 -0
- {circuitpython_synthtools-0.7.0 → circuitpython_synthtools-0.8.0}/tests/test_wiring.py +0 -0
|
@@ -50,6 +50,13 @@ _build
|
|
|
50
50
|
|
|
51
51
|
# todstuff
|
|
52
52
|
tmp
|
|
53
|
+
# scratch. Also what keeps `reuse` off it: that hook is pass_filenames:false,
|
|
54
|
+
# so .pre-commit-config.yaml's exclude never reaches it, but REUSE skips
|
|
55
|
+
# whatever git ignores.
|
|
56
|
+
temp/
|
|
53
57
|
|
|
54
58
|
# not ready for tracking/CI yet
|
|
55
59
|
docs/diagrams/
|
|
60
|
+
|
|
61
|
+
# regenerable render-harness output (FINDINGS.md is tracked)
|
|
62
|
+
tests/render/out/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: circuitpython-synthtools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: CircuitPython helper library to do help doing synthio
|
|
5
5
|
Author-email: Tod Kurt <tod@todbot.com>
|
|
6
6
|
License: MIT
|
|
@@ -125,16 +125,17 @@ Usage Example
|
|
|
125
125
|
What's Included
|
|
126
126
|
===============
|
|
127
127
|
|
|
128
|
+
* ``Patch`` -- inert, JSON-able patch data; save/load with
|
|
129
|
+
``save_patches()`` / ``load_patches()``
|
|
128
130
|
* ``Synth`` -- synth engine base: shared voice, patch, and modulation handling,
|
|
129
|
-
|
|
131
|
+
complex filter & pitch envelopes, and a ``mono`` mode for a single-voice
|
|
132
|
+
synth with ``glide_time`` portamento
|
|
130
133
|
* ``SubtractiveSynth`` -- subtractive two-oscillator synth w/ detune
|
|
134
|
+
* ``WavetableSynth`` -- wavetable-playback with adjustable LFO for wave_pos
|
|
131
135
|
* ``FMSynth`` -- two-operator phase-modulation voice: a carrier waveform
|
|
132
136
|
pre-rendered from ``sin(theta + fm_index*sin(fm_ratio*theta))``.
|
|
133
137
|
``fm_ratio`` must be an integer; ``fm_index`` is PM depth in radians,
|
|
134
|
-
0 = plain single-oscillator. (Not a
|
|
135
|
-
synthio's Math/LFO blocks only update every 256 samples, too slow for
|
|
136
|
-
that; see the module docstring for why.)
|
|
137
|
-
* ``WavetableSynth`` -- wavetable-playback with adjustable wave_pos
|
|
138
|
+
0 = plain single-oscillator. (Not a full FM synth, see module for details)
|
|
138
139
|
* ``BasslineSynth`` -- TB-303-style acid bassline: monophonic, one
|
|
139
140
|
oscillator, a decay-only filter sweep, per-step slide and accent. Can
|
|
140
141
|
own its own filter/distortion/echo effects chain via ``fx_*`` patch
|
|
@@ -144,26 +145,24 @@ What's Included
|
|
|
144
145
|
``tracking_filter()`` builds extra filter stages that follow the synth's
|
|
145
146
|
own cutoff and resonance for a steeper slope (needs ``audiofilters`` in
|
|
146
147
|
the build)
|
|
147
|
-
* ``
|
|
148
|
-
|
|
148
|
+
* ``Waves`` -- waveform factory (saw, square, sine, triangle, noise, and
|
|
149
|
+
"analog" variants)
|
|
149
150
|
* ``Wavetable`` -- loads a wavetable WAV file and lerps between frames
|
|
150
151
|
* ``AHREnvelope`` -- shared-block attack/release envelope, used for both
|
|
151
152
|
the filter and pitch envelopes
|
|
152
|
-
* ``Waves`` -- waveform factory (saw, square, sine, triangle, noise, and
|
|
153
|
-
"analog" variants)
|
|
154
153
|
* ``Arpeggiator`` / ``StepSequencer`` / ``TrigSequencer`` -- poll-based
|
|
155
154
|
sequencers with on/off callbacks
|
|
156
|
-
* ``Param`` / ``ParamSet`` -- knob-pickup and scaling for UIs with fewer
|
|
157
|
-
knobs than parameters
|
|
158
|
-
* ``ParamScaler`` -- proportional ("scale") knob takeover for a single
|
|
159
|
-
control, when you are not using ``ParamSet``
|
|
160
|
-
* ``GaugeCluster`` -- a bar-graph display of a parameter page
|
|
161
155
|
* ``Glider`` -- a standalone pitch-slide block for hand-built
|
|
162
156
|
``synthio.Note`` graphs (the engines above have their own portamento,
|
|
163
157
|
via ``mono`` + ``glide_time``)
|
|
164
|
-
* ``RollingAverage`` -- moving-average smoothing for noisy knob reads
|
|
165
158
|
* ``Scale`` / ``chord`` -- scales and diatonic/chromatic chords for
|
|
166
159
|
mapping pads and MIDI onto pitch
|
|
160
|
+
* ``Param`` / ``ParamSet`` -- (UI) knob-pickup and scaling for UIs with fewer
|
|
161
|
+
knobs than parameters
|
|
162
|
+
* ``ParamScaler`` -- (UI) proportional ("scale") knob takeover for a single
|
|
163
|
+
control, when you are not using ``ParamSet``
|
|
164
|
+
* ``GaugeCluster`` -- (UI) a bar-graph display of a parameter page
|
|
165
|
+
* ``RollingAverage`` -- moving-average smoothing for noisy knob reads
|
|
167
166
|
|
|
168
167
|
Documentation
|
|
169
168
|
=============
|
|
@@ -105,16 +105,17 @@ Usage Example
|
|
|
105
105
|
What's Included
|
|
106
106
|
===============
|
|
107
107
|
|
|
108
|
+
* ``Patch`` -- inert, JSON-able patch data; save/load with
|
|
109
|
+
``save_patches()`` / ``load_patches()``
|
|
108
110
|
* ``Synth`` -- synth engine base: shared voice, patch, and modulation handling,
|
|
109
|
-
|
|
111
|
+
complex filter & pitch envelopes, and a ``mono`` mode for a single-voice
|
|
112
|
+
synth with ``glide_time`` portamento
|
|
110
113
|
* ``SubtractiveSynth`` -- subtractive two-oscillator synth w/ detune
|
|
114
|
+
* ``WavetableSynth`` -- wavetable-playback with adjustable LFO for wave_pos
|
|
111
115
|
* ``FMSynth`` -- two-operator phase-modulation voice: a carrier waveform
|
|
112
116
|
pre-rendered from ``sin(theta + fm_index*sin(fm_ratio*theta))``.
|
|
113
117
|
``fm_ratio`` must be an integer; ``fm_index`` is PM depth in radians,
|
|
114
|
-
0 = plain single-oscillator. (Not a
|
|
115
|
-
synthio's Math/LFO blocks only update every 256 samples, too slow for
|
|
116
|
-
that; see the module docstring for why.)
|
|
117
|
-
* ``WavetableSynth`` -- wavetable-playback with adjustable wave_pos
|
|
118
|
+
0 = plain single-oscillator. (Not a full FM synth, see module for details)
|
|
118
119
|
* ``BasslineSynth`` -- TB-303-style acid bassline: monophonic, one
|
|
119
120
|
oscillator, a decay-only filter sweep, per-step slide and accent. Can
|
|
120
121
|
own its own filter/distortion/echo effects chain via ``fx_*`` patch
|
|
@@ -124,26 +125,24 @@ What's Included
|
|
|
124
125
|
``tracking_filter()`` builds extra filter stages that follow the synth's
|
|
125
126
|
own cutoff and resonance for a steeper slope (needs ``audiofilters`` in
|
|
126
127
|
the build)
|
|
127
|
-
* ``
|
|
128
|
-
|
|
128
|
+
* ``Waves`` -- waveform factory (saw, square, sine, triangle, noise, and
|
|
129
|
+
"analog" variants)
|
|
129
130
|
* ``Wavetable`` -- loads a wavetable WAV file and lerps between frames
|
|
130
131
|
* ``AHREnvelope`` -- shared-block attack/release envelope, used for both
|
|
131
132
|
the filter and pitch envelopes
|
|
132
|
-
* ``Waves`` -- waveform factory (saw, square, sine, triangle, noise, and
|
|
133
|
-
"analog" variants)
|
|
134
133
|
* ``Arpeggiator`` / ``StepSequencer`` / ``TrigSequencer`` -- poll-based
|
|
135
134
|
sequencers with on/off callbacks
|
|
136
|
-
* ``Param`` / ``ParamSet`` -- knob-pickup and scaling for UIs with fewer
|
|
137
|
-
knobs than parameters
|
|
138
|
-
* ``ParamScaler`` -- proportional ("scale") knob takeover for a single
|
|
139
|
-
control, when you are not using ``ParamSet``
|
|
140
|
-
* ``GaugeCluster`` -- a bar-graph display of a parameter page
|
|
141
135
|
* ``Glider`` -- a standalone pitch-slide block for hand-built
|
|
142
136
|
``synthio.Note`` graphs (the engines above have their own portamento,
|
|
143
137
|
via ``mono`` + ``glide_time``)
|
|
144
|
-
* ``RollingAverage`` -- moving-average smoothing for noisy knob reads
|
|
145
138
|
* ``Scale`` / ``chord`` -- scales and diatonic/chromatic chords for
|
|
146
139
|
mapping pads and MIDI onto pitch
|
|
140
|
+
* ``Param`` / ``ParamSet`` -- (UI) knob-pickup and scaling for UIs with fewer
|
|
141
|
+
knobs than parameters
|
|
142
|
+
* ``ParamScaler`` -- (UI) proportional ("scale") knob takeover for a single
|
|
143
|
+
control, when you are not using ``ParamSet``
|
|
144
|
+
* ``GaugeCluster`` -- (UI) a bar-graph display of a parameter page
|
|
145
|
+
* ``RollingAverage`` -- moving-average smoothing for noisy knob reads
|
|
147
146
|
|
|
148
147
|
Documentation
|
|
149
148
|
=============
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: circuitpython-synthtools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: CircuitPython helper library to do help doing synthio
|
|
5
5
|
Author-email: Tod Kurt <tod@todbot.com>
|
|
6
6
|
License: MIT
|
|
@@ -125,16 +125,17 @@ Usage Example
|
|
|
125
125
|
What's Included
|
|
126
126
|
===============
|
|
127
127
|
|
|
128
|
+
* ``Patch`` -- inert, JSON-able patch data; save/load with
|
|
129
|
+
``save_patches()`` / ``load_patches()``
|
|
128
130
|
* ``Synth`` -- synth engine base: shared voice, patch, and modulation handling,
|
|
129
|
-
|
|
131
|
+
complex filter & pitch envelopes, and a ``mono`` mode for a single-voice
|
|
132
|
+
synth with ``glide_time`` portamento
|
|
130
133
|
* ``SubtractiveSynth`` -- subtractive two-oscillator synth w/ detune
|
|
134
|
+
* ``WavetableSynth`` -- wavetable-playback with adjustable LFO for wave_pos
|
|
131
135
|
* ``FMSynth`` -- two-operator phase-modulation voice: a carrier waveform
|
|
132
136
|
pre-rendered from ``sin(theta + fm_index*sin(fm_ratio*theta))``.
|
|
133
137
|
``fm_ratio`` must be an integer; ``fm_index`` is PM depth in radians,
|
|
134
|
-
0 = plain single-oscillator. (Not a
|
|
135
|
-
synthio's Math/LFO blocks only update every 256 samples, too slow for
|
|
136
|
-
that; see the module docstring for why.)
|
|
137
|
-
* ``WavetableSynth`` -- wavetable-playback with adjustable wave_pos
|
|
138
|
+
0 = plain single-oscillator. (Not a full FM synth, see module for details)
|
|
138
139
|
* ``BasslineSynth`` -- TB-303-style acid bassline: monophonic, one
|
|
139
140
|
oscillator, a decay-only filter sweep, per-step slide and accent. Can
|
|
140
141
|
own its own filter/distortion/echo effects chain via ``fx_*`` patch
|
|
@@ -144,26 +145,24 @@ What's Included
|
|
|
144
145
|
``tracking_filter()`` builds extra filter stages that follow the synth's
|
|
145
146
|
own cutoff and resonance for a steeper slope (needs ``audiofilters`` in
|
|
146
147
|
the build)
|
|
147
|
-
* ``
|
|
148
|
-
|
|
148
|
+
* ``Waves`` -- waveform factory (saw, square, sine, triangle, noise, and
|
|
149
|
+
"analog" variants)
|
|
149
150
|
* ``Wavetable`` -- loads a wavetable WAV file and lerps between frames
|
|
150
151
|
* ``AHREnvelope`` -- shared-block attack/release envelope, used for both
|
|
151
152
|
the filter and pitch envelopes
|
|
152
|
-
* ``Waves`` -- waveform factory (saw, square, sine, triangle, noise, and
|
|
153
|
-
"analog" variants)
|
|
154
153
|
* ``Arpeggiator`` / ``StepSequencer`` / ``TrigSequencer`` -- poll-based
|
|
155
154
|
sequencers with on/off callbacks
|
|
156
|
-
* ``Param`` / ``ParamSet`` -- knob-pickup and scaling for UIs with fewer
|
|
157
|
-
knobs than parameters
|
|
158
|
-
* ``ParamScaler`` -- proportional ("scale") knob takeover for a single
|
|
159
|
-
control, when you are not using ``ParamSet``
|
|
160
|
-
* ``GaugeCluster`` -- a bar-graph display of a parameter page
|
|
161
155
|
* ``Glider`` -- a standalone pitch-slide block for hand-built
|
|
162
156
|
``synthio.Note`` graphs (the engines above have their own portamento,
|
|
163
157
|
via ``mono`` + ``glide_time``)
|
|
164
|
-
* ``RollingAverage`` -- moving-average smoothing for noisy knob reads
|
|
165
158
|
* ``Scale`` / ``chord`` -- scales and diatonic/chromatic chords for
|
|
166
159
|
mapping pads and MIDI onto pitch
|
|
160
|
+
* ``Param`` / ``ParamSet`` -- (UI) knob-pickup and scaling for UIs with fewer
|
|
161
|
+
knobs than parameters
|
|
162
|
+
* ``ParamScaler`` -- (UI) proportional ("scale") knob takeover for a single
|
|
163
|
+
control, when you are not using ``ParamSet``
|
|
164
|
+
* ``GaugeCluster`` -- (UI) a bar-graph display of a parameter page
|
|
165
|
+
* ``RollingAverage`` -- moving-average smoothing for noisy knob reads
|
|
167
166
|
|
|
168
167
|
Documentation
|
|
169
168
|
=============
|
|
@@ -38,8 +38,6 @@ docs/_static/favicon.ico
|
|
|
38
38
|
docs/_static/favicon.ico.license
|
|
39
39
|
examples/REUSE.toml
|
|
40
40
|
examples/arpeggiator_demo.py
|
|
41
|
-
examples/step_sequencer_bassdemo.py
|
|
42
|
-
examples/step_sequencer_demo.py
|
|
43
41
|
examples/synth_setup.py
|
|
44
42
|
examples/synthinst_code1.py
|
|
45
43
|
examples/synthinst_fenv_demo.py
|
|
@@ -47,12 +45,16 @@ examples/synthinst_pad_demo.py
|
|
|
47
45
|
examples/synthinst_pitch_demo.py
|
|
48
46
|
examples/synthinst_wave_demo.py
|
|
49
47
|
examples/synthtools_ahr_envelope_bend.py
|
|
50
|
-
examples/
|
|
48
|
+
examples/synthtools_bassline_accent_demo.py
|
|
49
|
+
examples/synthtools_bassline_filter_demo.py
|
|
50
|
+
examples/synthtools_bassline_fx_demo.py
|
|
51
51
|
examples/synthtools_fm_demo.py
|
|
52
52
|
examples/synthtools_harmony_demo.py
|
|
53
53
|
examples/synthtools_midi_demo.py
|
|
54
54
|
examples/synthtools_simpletest.py
|
|
55
|
+
examples/synthtools_stepsequencer_demo.py
|
|
55
56
|
examples/synthtools_swarm_demo.py
|
|
57
|
+
examples/synthtools_wavetable_chords.py
|
|
56
58
|
examples/synthtools_wavetable_simple.py
|
|
57
59
|
examples/trig_sequencer_demo.py
|
|
58
60
|
examples/ui/synthtools_gaugecluster_test.py
|
|
@@ -104,10 +106,21 @@ tests/test_paramset.py
|
|
|
104
106
|
tests/test_patch.py
|
|
105
107
|
tests/test_pytest_smoke.py
|
|
106
108
|
tests/test_swarm_synth.py
|
|
109
|
+
tests/test_trig_sequencer.py
|
|
107
110
|
tests/test_wiring.py
|
|
108
111
|
tests/hw/run_on_device.py
|
|
109
112
|
tests/hw/test_device.py
|
|
110
113
|
tests/hw/test_display_cost.py
|
|
114
|
+
tests/hw/test_trig_sequencer_timing.py
|
|
115
|
+
tests/hw/test_wavetable_cost.py
|
|
116
|
+
tests/render/FINDINGS.md
|
|
117
|
+
tests/render/README.md
|
|
118
|
+
tests/render/analyze_renders.py
|
|
119
|
+
tests/render/check_wavetable.py
|
|
120
|
+
tests/render/check_wavetable_synth.py
|
|
121
|
+
tests/render/render_chords.py
|
|
122
|
+
tests/render/run.sh
|
|
123
|
+
tests/render/wavhdr.py
|
|
111
124
|
tests/stubs/synthio.py
|
|
112
125
|
tests/stubs/ulab/__init__.py
|
|
113
126
|
tests/stubs/ulab/numpy.py
|
|
@@ -14,4 +14,6 @@
|
|
|
14
14
|
# alongside their sibling modules, not run from this tree.
|
|
15
15
|
# - tests/hw/test_device.py needs a real synthio: it only runs on
|
|
16
16
|
# actual hardware (see tests/README.md).
|
|
17
|
-
|
|
17
|
+
# - tests/render/* imports synthio/ulab and runs under a CircuitPython unix
|
|
18
|
+
# build via tests/render/run.sh, never pytest.
|
|
19
|
+
collect_ignore_glob = ["examples/*", "tests/hw/*", "tests/render/*"]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Simple test
|
|
2
|
+
------------
|
|
3
|
+
|
|
4
|
+
Simple bass synth showing Patch and SubtractiveSynth
|
|
5
|
+
|
|
6
|
+
.. literalinclude:: ../examples/synthtools_simpletest.py
|
|
7
|
+
:caption: examples/synthtools_simpletest.py
|
|
8
|
+
:linenos:
|
|
9
|
+
|
|
10
|
+
Synth filter envelope demo
|
|
11
|
+
--------------------------
|
|
12
|
+
|
|
13
|
+
Show how to use Synth's complex filter envelope using AHREnvelope
|
|
14
|
+
|
|
15
|
+
.. literalinclude:: ../examples/synthinst_fenv_demo.py
|
|
16
|
+
:caption: examples/synthtools_fenv_demo.py
|
|
17
|
+
:linenos:
|
|
18
|
+
|
|
19
|
+
Synth pitch modulation demo
|
|
20
|
+
---------------------------
|
|
21
|
+
|
|
22
|
+
Show how to use Synth's pitch modulation features
|
|
23
|
+
|
|
24
|
+
.. literalinclude:: ../examples/synthinst_pitch_demo.py
|
|
25
|
+
:caption: examples/synthtools_pitch_demo.py
|
|
26
|
+
:linenos:
|
|
27
|
+
|
|
28
|
+
Wavetable synth demo
|
|
29
|
+
--------------------
|
|
30
|
+
|
|
31
|
+
Demonstrate Wavetable use with an LFO on wave position
|
|
32
|
+
|
|
33
|
+
.. literalinclude:: ../examples/synthtools_wavetable_simple.py
|
|
34
|
+
:caption: examples/synthtools_wavetable_simple.py
|
|
35
|
+
:linenos:
|
|
36
|
+
|
|
37
|
+
Swarm synth demo
|
|
38
|
+
----------------
|
|
39
|
+
|
|
40
|
+
Like the Dewanatron Swarmatron: eight oscillators on one pitch, pulled apart
|
|
41
|
+
and back under a held drone.
|
|
42
|
+
|
|
43
|
+
.. literalinclude:: ../examples/synthtools_swarm_demo.py
|
|
44
|
+
:caption: examples/synthtools_swarm_demo.py
|
|
45
|
+
:linenos:
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
FM synth demo
|
|
49
|
+
-------------
|
|
50
|
+
|
|
51
|
+
A bright DX-style FM bell using FMSynth, with a live fm_ratio / fm_index sweep
|
|
52
|
+
|
|
53
|
+
.. literalinclude:: ../examples/synthtools_fm_demo.py
|
|
54
|
+
:caption: examples/synthtools_fm_demo.py
|
|
55
|
+
:linenos:
|
|
56
|
+
|
|
57
|
+
Acid bassline demos: filter/envelope/decay, accent/slide, fx
|
|
58
|
+
------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
Monophonic TB-303-style bassline using BasslineSynth, sweeping filter
|
|
61
|
+
cutoff, envelope depth (envmod), decay time, and the oscillator waveform
|
|
62
|
+
|
|
63
|
+
.. literalinclude:: ../examples/synthtools_bassline_filter_demo.py
|
|
64
|
+
:caption: examples/synthtools_bassline_filter_demo.py
|
|
65
|
+
:linenos:
|
|
66
|
+
|
|
67
|
+
The same bassline, sweeping the two per-step 303 flags: accent and slide
|
|
68
|
+
|
|
69
|
+
.. literalinclude:: ../examples/synthtools_bassline_accent_demo.py
|
|
70
|
+
:caption: examples/synthtools_bassline_accent_demo.py
|
|
71
|
+
:linenos:
|
|
72
|
+
|
|
73
|
+
The same bassline again, through BasslineSynth's owned effects chain:
|
|
74
|
+
an extra filter stage, distortion, and a tempo-synced echo
|
|
75
|
+
|
|
76
|
+
.. literalinclude:: ../examples/synthtools_bassline_fx_demo.py
|
|
77
|
+
:caption: examples/synthtools_bassline_fx_demo.py
|
|
78
|
+
:linenos:
|
|
79
|
+
|
|
80
|
+
Harmony demo
|
|
81
|
+
------------
|
|
82
|
+
|
|
83
|
+
Walk scales and play diatonic chords with harmony.Scale, cycling through a
|
|
84
|
+
short chord progression
|
|
85
|
+
|
|
86
|
+
.. literalinclude:: ../examples/synthtools_harmony_demo.py
|
|
87
|
+
:caption: examples/synthtools_harmony_demo.py
|
|
88
|
+
:linenos:
|
|
89
|
+
|
|
90
|
+
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
# part of todbot circuitpython synthio tutorial
|
|
6
6
|
# 10 Feb 2025 - @todbot / Tod Kurt
|
|
7
7
|
#
|
|
8
|
+
import sys
|
|
9
|
+
|
|
8
10
|
import analogio
|
|
9
11
|
import audiobusio
|
|
10
12
|
import audiomixer
|
|
@@ -15,6 +17,11 @@ import synthio
|
|
|
15
17
|
SAMPLE_RATE = 22050
|
|
16
18
|
CHANNEL_COUNT = 2
|
|
17
19
|
BUFFER_SIZE = 2048
|
|
20
|
+
if sys.platform == "RP2040":
|
|
21
|
+
SAMPLE_RATE = 22050
|
|
22
|
+
CHANNEL_COUNT = 1
|
|
23
|
+
BUFFER_SIZE = 4096
|
|
24
|
+
|
|
18
25
|
|
|
19
26
|
# what we have plugged into the breadboard or pico_test_synth
|
|
20
27
|
button_pins = (board.GP28,)
|
|
@@ -40,7 +47,7 @@ audio.play(mixer)
|
|
|
40
47
|
|
|
41
48
|
# plug the synth into the first 'voice' of the mixer
|
|
42
49
|
mixer.voice[0].play(synth)
|
|
43
|
-
mixer.voice[0].level = 0.
|
|
50
|
+
mixer.voice[0].level = 0.35 # 0.25 usually better for headphones, 1.0 for speakers
|
|
44
51
|
|
|
45
52
|
# more on this later, but makes it sound nicer
|
|
46
53
|
synth.envelope = synthio.Envelope(attack_time=0.0, release_time=0.6)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
#
|
|
4
|
+
# synthtools_bassline_accent_demo.py -- acid bassline: the two per-step
|
|
5
|
+
# 303 flags, accent and slide.
|
|
6
|
+
#
|
|
7
|
+
# Plays one 16-step pattern over and over, changing a single knob every few
|
|
8
|
+
# bars and printing what it changed, so you can hear each one on its own.
|
|
9
|
+
# One of three focused demos split out of a single, too-broad one; see
|
|
10
|
+
# synthtools_bassline_filter_demo.py (filter/envmod/decay) and
|
|
11
|
+
# synthtools_bassline_fx_demo.py (distortion/echo) for the rest, including
|
|
12
|
+
# why the patch fields below are set the way they are.
|
|
13
|
+
#
|
|
14
|
+
# slide glide the pitch into this step from the one before
|
|
15
|
+
# accent a louder, brighter, more resonant step
|
|
16
|
+
#
|
|
17
|
+
# A slide TIES the two steps into one held note, as the original does: the
|
|
18
|
+
# pitch glides and the envelopes keep running. That needs the gate held, so
|
|
19
|
+
# the loop below skips its note_off() whenever the NEXT step slides. Being
|
|
20
|
+
# monophonic, the synth steals its own sounding voice otherwise, so the loop
|
|
21
|
+
# still never has to track what is playing.
|
|
22
|
+
|
|
23
|
+
import microcontroller
|
|
24
|
+
|
|
25
|
+
# 200 MHz, up from the 125 MHz boot default. The main loop shares the CPU
|
|
26
|
+
# with the audio renderer, so the headroom shows up as steadier step
|
|
27
|
+
# timing, not just as spare cycles. Set BEFORE synth_setup is imported:
|
|
28
|
+
# rp2040's I2S clock is derived from sys_clk, so changing it afterwards
|
|
29
|
+
# would move the sample rate out from under the audio that is already
|
|
30
|
+
# running. Same placement as synthtools_wavetable_chords.py.
|
|
31
|
+
microcontroller.cpu.frequency = 200_000_000
|
|
32
|
+
|
|
33
|
+
import time
|
|
34
|
+
|
|
35
|
+
from synth_setup import mixer
|
|
36
|
+
from synth_setup import synth as engine
|
|
37
|
+
|
|
38
|
+
from synthtools import BasslineSynth, Patch
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
from supervisor import ticks_ms
|
|
42
|
+
except ImportError: # desktop CPython, for a syntax check
|
|
43
|
+
|
|
44
|
+
def ticks_ms():
|
|
45
|
+
"""stand-in for supervisor.ticks_ms"""
|
|
46
|
+
return time.monotonic_ns() // 1_000_000
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# --- the patch: same acid preset as synthtools_bassline_filter_demo.py --
|
|
50
|
+
patch = Patch(
|
|
51
|
+
name="acid",
|
|
52
|
+
synth_type="bassline",
|
|
53
|
+
wave="SAW",
|
|
54
|
+
filt_type="LPF",
|
|
55
|
+
filt_f=1200,
|
|
56
|
+
# filt_q up at 4.0: on this engine resonance also drives how DEEP an
|
|
57
|
+
# accent sweeps and how fast it arrives, and at 1.8 the lag is ~9ms
|
|
58
|
+
# and inaudible. accent_cutoff is scaled to match: the accent boost is
|
|
59
|
+
# multiplied by (0.35 + 0.65*resonance), so 5268 at this filt_q lands
|
|
60
|
+
# the same Hz that a flat 4000 used to.
|
|
61
|
+
filt_q=4.0,
|
|
62
|
+
envmod=0.75,
|
|
63
|
+
# Decay far longer than a step, so the level is essentially flat for as
|
|
64
|
+
# long as the gate is open. That is the stock 303 (its VCA envelope
|
|
65
|
+
# decay is fixed near 3.5s, untouched by the Decay knob) and it is what
|
|
66
|
+
# keeps a TIED note audible through its second step; at 0.25s a slide
|
|
67
|
+
# landed on a note that had already faded out.
|
|
68
|
+
amp_env=[0.003, 3.5, 0.0, 0.05],
|
|
69
|
+
fenv_attack=0.09,
|
|
70
|
+
fenv_release=0.05,
|
|
71
|
+
fenv_curve=3,
|
|
72
|
+
# what an accented step gets, on top of the resting patch above
|
|
73
|
+
accent=0.6,
|
|
74
|
+
accent_cutoff=5268, # Hz added at full accent
|
|
75
|
+
accent_q=0.8, # resonance added at full accent
|
|
76
|
+
amp_level=0.75, # un-accented level, so accents have room to be louder
|
|
77
|
+
slide_time=0.09,
|
|
78
|
+
transpose=0,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
synth = BasslineSynth(engine, patch)
|
|
82
|
+
# synthio directly, no fx chain: this demo is about the two per-step
|
|
83
|
+
# flags. The other two set fx_hpf_f and play synth.output instead.
|
|
84
|
+
mixer.voice[0].play(synth.synthio) # replaces synth_setup's direct hookup
|
|
85
|
+
|
|
86
|
+
# --- the pattern --------------------------------------------------------
|
|
87
|
+
# (midi_note, slide, accent), or None for a rest.
|
|
88
|
+
PATTERN = (
|
|
89
|
+
(36, False, True),
|
|
90
|
+
(36, False, False),
|
|
91
|
+
(48, True, False),
|
|
92
|
+
(36, False, False),
|
|
93
|
+
(39, False, True),
|
|
94
|
+
(36, True, False),
|
|
95
|
+
(43, False, False),
|
|
96
|
+
(36, False, False),
|
|
97
|
+
(36, False, True),
|
|
98
|
+
(41, True, True),
|
|
99
|
+
(36, False, False),
|
|
100
|
+
(48, False, True),
|
|
101
|
+
(36, True, False),
|
|
102
|
+
(34, False, False),
|
|
103
|
+
(36, False, False),
|
|
104
|
+
(43, False, True),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
BPM = 110
|
|
108
|
+
STEP_MS = int(60_000 / BPM / 4) # sixteenth notes, in ms
|
|
109
|
+
# GATE also has to leave the filter sweep room: the envelope only runs
|
|
110
|
+
# while the note is held, so GATE*step must stay LONGER than `decay` or the
|
|
111
|
+
# sweep is cut off partway and envmod does less than its number suggests.
|
|
112
|
+
GATE = 0.9 # fraction of a step a note is held for
|
|
113
|
+
|
|
114
|
+
# note_on() re-aims the shared glide node on EVERY note-on, slide or not
|
|
115
|
+
# (a non-slide step passes glide=0.0, which snaps in ~1ms), so slide_time
|
|
116
|
+
# can never exceed roughly one step (~136ms here) and be heard in full: the
|
|
117
|
+
# next note-on always cuts it off first. The "lazy slide" CHANGES entry
|
|
118
|
+
# below needs actual room, so it widens step_scale for its own window
|
|
119
|
+
# instead of raising slide_time past that ceiling. A mutable single-element
|
|
120
|
+
# list so the CHANGES closures can rebind it; the main loop reads it fresh
|
|
121
|
+
# every step.
|
|
122
|
+
step_scale = [1.0]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _lazy_slide_on():
|
|
126
|
+
synth.slide_time = 0.3
|
|
127
|
+
step_scale[0] = 3.0 # ~409ms/step: room for a slide_time of 0.3 to finish
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _lazy_slide_off():
|
|
131
|
+
synth.slide_time = 0.09
|
|
132
|
+
step_scale[0] = 1.0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# Each entry is (label, function), applied for BARS_PER_CHANGE bars each.
|
|
136
|
+
BARS_PER_CHANGE = 2
|
|
137
|
+
CHANGES = (
|
|
138
|
+
("accent 0.0, accented steps stop standing out", lambda: setattr(synth, "accent", 0.0)),
|
|
139
|
+
("accent 1.0, and now they really do", lambda: setattr(synth, "accent", 1.0)),
|
|
140
|
+
("accent 0.6", lambda: setattr(synth, "accent", 0.6)),
|
|
141
|
+
(
|
|
142
|
+
"slide_time 0.005, slides become almost instant",
|
|
143
|
+
lambda: setattr(synth, "slide_time", 0.005),
|
|
144
|
+
),
|
|
145
|
+
(
|
|
146
|
+
"slide_time 0.3, step time x3: room for a real lazy slide",
|
|
147
|
+
_lazy_slide_on,
|
|
148
|
+
),
|
|
149
|
+
("slide_time 0.09, step time normal again", _lazy_slide_off),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
print("bassline accent/slide demo: %d steps at %d bpm" % (len(PATTERN), BPM))
|
|
153
|
+
|
|
154
|
+
# The loop schedules off monotonic_ns and ACCUMULATES the step interval
|
|
155
|
+
# rather than sleeping for it. Sleeping drifts: the note_on/note_off work
|
|
156
|
+
# (~5.7ms on an rp2040) lands on top of every sleep, so a 113ms step ran at
|
|
157
|
+
# 119ms, about 5% slow, and nothing ever caught it back up. Accumulating
|
|
158
|
+
# step_at absorbs the work instead. The 1ms sleep keeps the poll from
|
|
159
|
+
# spinning flat out and competing with the audio render.
|
|
160
|
+
#
|
|
161
|
+
# It also has to be one flat loop rather than a for-loop over PATTERN: a
|
|
162
|
+
# step that TIES into the next one holds its gate open past its own step,
|
|
163
|
+
# so the note-off has to be scheduled independently of where the sequence
|
|
164
|
+
# has got to.
|
|
165
|
+
|
|
166
|
+
# Timing follows the house sequencer idiom (trig_sequencer.py): poll as
|
|
167
|
+
# fast as possible on ticks_ms, accumulate the step interval so the work
|
|
168
|
+
# in a step is absorbed rather than added to it, and RESYNC if a stall put
|
|
169
|
+
# us a whole step behind instead of burst-firing to catch up.
|
|
170
|
+
#
|
|
171
|
+
# ticks_ms, not time.monotonic_ns: past boot monotonic_ns exceeds 2**30, so
|
|
172
|
+
# every call allocates a bigint -- measured 21us and ~0.5 bytes a call
|
|
173
|
+
# against 13us and none, which at poll rates is real GC churn, and a GC
|
|
174
|
+
# pause lands as a late step.
|
|
175
|
+
#
|
|
176
|
+
# It also has to be one flat loop rather than a for-loop over PATTERN: a
|
|
177
|
+
# step that TIES into the next one holds its gate open past its own step,
|
|
178
|
+
# so the note-off has to be scheduled independently of where the sequence
|
|
179
|
+
# has got to.
|
|
180
|
+
|
|
181
|
+
bar = 0
|
|
182
|
+
i = 0
|
|
183
|
+
sounding = None
|
|
184
|
+
gate_off_at = None
|
|
185
|
+
next_step = ticks_ms()
|
|
186
|
+
|
|
187
|
+
while True:
|
|
188
|
+
now = ticks_ms()
|
|
189
|
+
|
|
190
|
+
if gate_off_at is not None and now - gate_off_at >= 0:
|
|
191
|
+
synth.note_off(sounding)
|
|
192
|
+
sounding = gate_off_at = None
|
|
193
|
+
|
|
194
|
+
if now - next_step >= 0:
|
|
195
|
+
step_millis = int(STEP_MS * step_scale[0])
|
|
196
|
+
if i == 0:
|
|
197
|
+
if bar % BARS_PER_CHANGE == 0:
|
|
198
|
+
label, apply = CHANGES[(bar // BARS_PER_CHANGE) % len(CHANGES)]
|
|
199
|
+
print(" %s" % label)
|
|
200
|
+
apply()
|
|
201
|
+
bar += 1
|
|
202
|
+
|
|
203
|
+
step = PATTERN[i]
|
|
204
|
+
if step is not None:
|
|
205
|
+
note, slide, accent = step
|
|
206
|
+
synth.note_on_step(note, slide=slide, accent=accent)
|
|
207
|
+
sounding = note
|
|
208
|
+
# A slid step TIES to the one before it, and can only do that
|
|
209
|
+
# while that one is still sounding. So hold the gate open across
|
|
210
|
+
# the whole step whenever the NEXT one slides; releasing would
|
|
211
|
+
# leave the tie with nothing to tie to and it would retrigger.
|
|
212
|
+
nxt = PATTERN[(i + 1) % len(PATTERN)]
|
|
213
|
+
hold = nxt is not None and nxt[1]
|
|
214
|
+
gate_off_at = None if hold else now + int(step_millis * GATE)
|
|
215
|
+
|
|
216
|
+
i = (i + 1) % len(PATTERN)
|
|
217
|
+
next_step += step_millis
|
|
218
|
+
if next_step < now: # a stall put us a whole step behind: resync
|
|
219
|
+
next_step = now + step_millis
|