circuitpython-synthtools 0.6.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.
Files changed (138) hide show
  1. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/.gitignore +7 -0
  2. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/.pre-commit-config.yaml +1 -1
  3. {circuitpython_synthtools-0.6.0/circuitpython_synthtools.egg-info → circuitpython_synthtools-0.8.0}/PKG-INFO +16 -15
  4. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/README.rst +15 -14
  5. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0/circuitpython_synthtools.egg-info}/PKG-INFO +16 -15
  6. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/circuitpython_synthtools.egg-info/SOURCES.txt +19 -3
  7. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/conftest.py +3 -1
  8. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/docs/api.rst +6 -0
  9. circuitpython_synthtools-0.8.0/docs/examples.rst +90 -0
  10. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/synth_setup.py +8 -1
  11. circuitpython_synthtools-0.8.0/examples/synthtools_bassline_accent_demo.py +219 -0
  12. circuitpython_synthtools-0.8.0/examples/synthtools_bassline_filter_demo.py +248 -0
  13. circuitpython_synthtools-0.8.0/examples/synthtools_bassline_fx_demo.py +240 -0
  14. circuitpython_synthtools-0.8.0/examples/synthtools_harmony_demo.py +71 -0
  15. circuitpython_synthtools-0.6.0/examples/step_sequencer_demo.py → circuitpython_synthtools-0.8.0/examples/synthtools_stepsequencer_demo.py +2 -1
  16. circuitpython_synthtools-0.8.0/examples/synthtools_wavetable_chords.py +88 -0
  17. circuitpython_synthtools-0.8.0/examples/synthtools_wavetable_simple.py +58 -0
  18. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/trig_sequencer_demo.py +5 -3
  19. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/wavetables/PLAITS02.WAV +0 -0
  20. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/pyproject.toml +1 -1
  21. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/__init__.py +7 -1
  22. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/audio_fx.py +47 -10
  23. circuitpython_synthtools-0.8.0/synthtools/bassline_synth.py +1159 -0
  24. circuitpython_synthtools-0.8.0/synthtools/harmony.py +198 -0
  25. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/trig_sequencer.py +7 -6
  26. circuitpython_synthtools-0.8.0/synthtools/wavetable.py +89 -0
  27. circuitpython_synthtools-0.8.0/synthtools/wavetable_synth.py +215 -0
  28. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/README.md +18 -4
  29. circuitpython_synthtools-0.8.0/tests/hw/test_trig_sequencer_timing.py +123 -0
  30. circuitpython_synthtools-0.8.0/tests/hw/test_wavetable_cost.py +210 -0
  31. circuitpython_synthtools-0.8.0/tests/render/FINDINGS.md +153 -0
  32. circuitpython_synthtools-0.8.0/tests/render/README.md +79 -0
  33. circuitpython_synthtools-0.8.0/tests/render/analyze_renders.py +488 -0
  34. circuitpython_synthtools-0.8.0/tests/render/check_wavetable.py +108 -0
  35. circuitpython_synthtools-0.8.0/tests/render/check_wavetable_synth.py +168 -0
  36. circuitpython_synthtools-0.8.0/tests/render/render_chords.py +263 -0
  37. circuitpython_synthtools-0.8.0/tests/render/run.sh +51 -0
  38. circuitpython_synthtools-0.8.0/tests/render/wavhdr.py +48 -0
  39. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/run_tests.sh +4 -0
  40. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/test_audio_fx.py +106 -10
  41. circuitpython_synthtools-0.8.0/tests/test_harmony.py +139 -0
  42. circuitpython_synthtools-0.8.0/tests/test_mono.py +867 -0
  43. circuitpython_synthtools-0.8.0/tests/test_trig_sequencer.py +144 -0
  44. circuitpython_synthtools-0.6.0/docs/examples.rst +0 -58
  45. circuitpython_synthtools-0.6.0/examples/step_sequencer_bassdemo.py +0 -85
  46. circuitpython_synthtools-0.6.0/examples/synthtools_bassline_demo.py +0 -161
  47. circuitpython_synthtools-0.6.0/examples/synthtools_wavetable_simple.py +0 -29
  48. circuitpython_synthtools-0.6.0/synthtools/bassline_synth.py +0 -696
  49. circuitpython_synthtools-0.6.0/synthtools/wavetable.py +0 -61
  50. circuitpython_synthtools-0.6.0/synthtools/wavetable_synth.py +0 -303
  51. circuitpython_synthtools-0.6.0/tests/test_mono.py +0 -494
  52. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/.gitattributes +0 -0
  53. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md +0 -0
  54. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/.github/workflows/build.yml +0 -0
  55. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/.github/workflows/failure-help-text.yml +0 -0
  56. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/.github/workflows/release_gh.yml +0 -0
  57. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/.github/workflows/release_pypi.yml +0 -0
  58. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/.pylintrc +0 -0
  59. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/.readthedocs.yaml +0 -0
  60. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/CODE_OF_CONDUCT.md +0 -0
  61. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/LICENSE +0 -0
  62. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/LICENSES/CC-BY-4.0.txt +0 -0
  63. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/LICENSES/MIT.txt +0 -0
  64. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/LICENSES/Unlicense.txt +0 -0
  65. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/README.rst.license +0 -0
  66. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/circuitpython_synthtools.egg-info/dependency_links.txt +0 -0
  67. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/circuitpython_synthtools.egg-info/requires.txt +0 -0
  68. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/circuitpython_synthtools.egg-info/top_level.txt +0 -0
  69. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/docs/_static/custom.css +0 -0
  70. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/docs/_static/favicon.ico +0 -0
  71. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/docs/_static/favicon.ico.license +0 -0
  72. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/docs/api.rst.license +0 -0
  73. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/docs/conf.py +0 -0
  74. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/docs/examples.rst.license +0 -0
  75. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/docs/index.rst +0 -0
  76. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/docs/index.rst.license +0 -0
  77. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/docs/requirements.txt +0 -0
  78. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/REUSE.toml +0 -0
  79. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/arpeggiator_demo.py +0 -0
  80. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/synthinst_code1.py +0 -0
  81. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/synthinst_fenv_demo.py +0 -0
  82. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/synthinst_pad_demo.py +0 -0
  83. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/synthinst_pitch_demo.py +0 -0
  84. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/synthinst_wave_demo.py +0 -0
  85. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/synthtools_ahr_envelope_bend.py +0 -0
  86. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/synthtools_fm_demo.py +0 -0
  87. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/synthtools_midi_demo.py +0 -0
  88. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/synthtools_simpletest.py +0 -0
  89. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/synthtools_swarm_demo.py +0 -0
  90. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/ui/synthtools_gaugecluster_test.py +0 -0
  91. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/ui/synthtools_paramscaler_demo.py +0 -0
  92. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/wavetables/BRAIDS02.WAV +0 -0
  93. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/00_909kick4.wav +0 -0
  94. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/01_909snare2.wav +0 -0
  95. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/02_909hatclosed2a.wav +0 -0
  96. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/03_909hatopen5.wav +0 -0
  97. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/04_909clap1.wav +0 -0
  98. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/05_909tommed.wav +0 -0
  99. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/06_909blip.wav +0 -0
  100. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/07_909cym2.wav +0 -0
  101. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/examples/wavs/kit0_909/readme.txt +0 -0
  102. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/optional_requirements.txt +0 -0
  103. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/requirements.txt +0 -0
  104. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/ruff.toml +0 -0
  105. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/setup.cfg +0 -0
  106. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/ahr_envelope.py +0 -0
  107. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/arpeggiator.py +0 -0
  108. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/blocks.py +0 -0
  109. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/fm_synth.py +0 -0
  110. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/gauge_cluster.py +0 -0
  111. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/param_scaler.py +0 -0
  112. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/paramset.py +0 -0
  113. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/patch.py +0 -0
  114. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/pitch_glider.py +0 -0
  115. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/step_sequencer.py +0 -0
  116. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/subtractive_synth.py +0 -0
  117. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/swarm_synth.py +0 -0
  118. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/synth.py +0 -0
  119. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/utils.py +0 -0
  120. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/synthtools/waves.py +0 -0
  121. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/hw/run_on_device.py +0 -0
  122. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/hw/test_device.py +0 -0
  123. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/hw/test_display_cost.py +0 -0
  124. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/plot_env.py +0 -0
  125. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/stubs/synthio.py +0 -0
  126. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/stubs/ulab/__init__.py +0 -0
  127. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/stubs/ulab/numpy.py +0 -0
  128. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/stubs_fx/audiodelays.py +0 -0
  129. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/stubs_fx/audiofilters.py +0 -0
  130. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/test_arpeggiator.py +0 -0
  131. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/test_env_shapes.py +0 -0
  132. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/test_fm_synth.py +0 -0
  133. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/test_param_scaler.py +0 -0
  134. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/test_paramset.py +0 -0
  135. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/test_patch.py +0 -0
  136. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/test_pytest_smoke.py +0 -0
  137. {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.8.0}/tests/test_swarm_synth.py +0 -0
  138. {circuitpython_synthtools-0.6.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/
@@ -19,4 +19,4 @@ repos:
19
19
  rev: v6.2.0
20
20
  hooks:
21
21
  - id: reuse
22
- exclude: 'docs/.*|tests/.*'
22
+ exclude: 'docs/.*|tests/.*|temp/.*'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: circuitpython-synthtools
3
- Version: 0.6.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
- with ``mono`` mode for a single-voice synth with ``glide_time`` portamento
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 live audio-rate bend modulator --
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,23 +145,23 @@ 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
- * ``Patch`` -- inert, JSON-able patch data; save/load with
148
- ``save_patches()`` / ``load_patches()``
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``)
158
+ * ``Scale`` / ``chord`` -- scales and diatonic/chromatic chords for
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
164
165
  * ``RollingAverage`` -- moving-average smoothing for noisy knob reads
165
166
 
166
167
  Documentation
@@ -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
- with ``mono`` mode for a single-voice synth with ``glide_time`` portamento
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 live audio-rate bend modulator --
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,23 +125,23 @@ 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
- * ``Patch`` -- inert, JSON-able patch data; save/load with
128
- ``save_patches()`` / ``load_patches()``
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``)
138
+ * ``Scale`` / ``chord`` -- scales and diatonic/chromatic chords for
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
144
145
  * ``RollingAverage`` -- moving-average smoothing for noisy knob reads
145
146
 
146
147
  Documentation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: circuitpython-synthtools
3
- Version: 0.6.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
- with ``mono`` mode for a single-voice synth with ``glide_time`` portamento
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 live audio-rate bend modulator --
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,23 +145,23 @@ 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
- * ``Patch`` -- inert, JSON-able patch data; save/load with
148
- ``save_patches()`` / ``load_patches()``
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``)
158
+ * ``Scale`` / ``chord`` -- scales and diatonic/chromatic chords for
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
164
165
  * ``RollingAverage`` -- moving-average smoothing for noisy knob reads
165
166
 
166
167
  Documentation
@@ -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,11 +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/synthtools_bassline_demo.py
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
+ examples/synthtools_harmony_demo.py
52
53
  examples/synthtools_midi_demo.py
53
54
  examples/synthtools_simpletest.py
55
+ examples/synthtools_stepsequencer_demo.py
54
56
  examples/synthtools_swarm_demo.py
57
+ examples/synthtools_wavetable_chords.py
55
58
  examples/synthtools_wavetable_simple.py
56
59
  examples/trig_sequencer_demo.py
57
60
  examples/ui/synthtools_gaugecluster_test.py
@@ -75,6 +78,7 @@ synthtools/bassline_synth.py
75
78
  synthtools/blocks.py
76
79
  synthtools/fm_synth.py
77
80
  synthtools/gauge_cluster.py
81
+ synthtools/harmony.py
78
82
  synthtools/param_scaler.py
79
83
  synthtools/paramset.py
80
84
  synthtools/patch.py
@@ -95,16 +99,28 @@ tests/test_arpeggiator.py
95
99
  tests/test_audio_fx.py
96
100
  tests/test_env_shapes.py
97
101
  tests/test_fm_synth.py
102
+ tests/test_harmony.py
98
103
  tests/test_mono.py
99
104
  tests/test_param_scaler.py
100
105
  tests/test_paramset.py
101
106
  tests/test_patch.py
102
107
  tests/test_pytest_smoke.py
103
108
  tests/test_swarm_synth.py
109
+ tests/test_trig_sequencer.py
104
110
  tests/test_wiring.py
105
111
  tests/hw/run_on_device.py
106
112
  tests/hw/test_device.py
107
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
108
124
  tests/stubs/synthio.py
109
125
  tests/stubs/ulab/__init__.py
110
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
- collect_ignore_glob = ["examples/*", "tests/hw/*"]
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/*"]
@@ -82,6 +82,12 @@ Waves
82
82
  .. automodule:: synthtools.waves
83
83
  :members:
84
84
 
85
+ Harmony
86
+ =======
87
+
88
+ .. automodule:: synthtools.harmony
89
+ :members:
90
+
85
91
  Arpeggiator
86
92
  ===========
87
93
 
@@ -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.25 # 0.25 usually better for headphones, 1.0 for speakers
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