circuitpython-synthtools 0.6.0__tar.gz → 0.7.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.6.0/circuitpython_synthtools.egg-info → circuitpython_synthtools-0.7.0}/PKG-INFO +3 -1
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/README.rst +2 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0/circuitpython_synthtools.egg-info}/PKG-INFO +3 -1
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/circuitpython_synthtools.egg-info/SOURCES.txt +3 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/docs/api.rst +6 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/docs/examples.rst +10 -0
- circuitpython_synthtools-0.7.0/examples/synthtools_harmony_demo.py +71 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/pyproject.toml +1 -1
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/__init__.py +7 -1
- circuitpython_synthtools-0.7.0/synthtools/harmony.py +198 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/run_tests.sh +2 -0
- circuitpython_synthtools-0.7.0/tests/test_harmony.py +139 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/.gitattributes +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/.github/workflows/build.yml +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/.github/workflows/failure-help-text.yml +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/.github/workflows/release_gh.yml +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/.github/workflows/release_pypi.yml +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/.gitignore +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/.pre-commit-config.yaml +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/.pylintrc +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/.readthedocs.yaml +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/CODE_OF_CONDUCT.md +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/LICENSE +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/LICENSES/CC-BY-4.0.txt +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/LICENSES/MIT.txt +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/LICENSES/Unlicense.txt +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/README.rst.license +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/circuitpython_synthtools.egg-info/dependency_links.txt +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/circuitpython_synthtools.egg-info/requires.txt +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/circuitpython_synthtools.egg-info/top_level.txt +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/conftest.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/docs/_static/custom.css +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/docs/_static/favicon.ico +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/docs/_static/favicon.ico.license +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/docs/api.rst.license +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/docs/conf.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/docs/examples.rst.license +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/docs/index.rst +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/docs/index.rst.license +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/docs/requirements.txt +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/REUSE.toml +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/arpeggiator_demo.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/step_sequencer_bassdemo.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/step_sequencer_demo.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synth_setup.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthinst_code1.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthinst_fenv_demo.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthinst_pad_demo.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthinst_pitch_demo.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthinst_wave_demo.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthtools_ahr_envelope_bend.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthtools_bassline_demo.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthtools_fm_demo.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthtools_midi_demo.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthtools_simpletest.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthtools_swarm_demo.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthtools_wavetable_simple.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/trig_sequencer_demo.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/ui/synthtools_gaugecluster_test.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/ui/synthtools_paramscaler_demo.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/wavetables/BRAIDS02.WAV +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/wavetables/PLAITS02.WAV +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/wavs/kit0_909/00_909kick4.wav +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/wavs/kit0_909/01_909snare2.wav +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/wavs/kit0_909/02_909hatclosed2a.wav +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/wavs/kit0_909/03_909hatopen5.wav +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/wavs/kit0_909/04_909clap1.wav +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/wavs/kit0_909/05_909tommed.wav +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/wavs/kit0_909/06_909blip.wav +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/wavs/kit0_909/07_909cym2.wav +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/wavs/kit0_909/readme.txt +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/optional_requirements.txt +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/requirements.txt +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/ruff.toml +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/setup.cfg +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/ahr_envelope.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/arpeggiator.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/audio_fx.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/bassline_synth.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/blocks.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/fm_synth.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/gauge_cluster.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/param_scaler.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/paramset.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/patch.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/pitch_glider.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/step_sequencer.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/subtractive_synth.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/swarm_synth.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/synth.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/trig_sequencer.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/utils.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/waves.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/wavetable.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/wavetable_synth.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/README.md +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/hw/run_on_device.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/hw/test_device.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/hw/test_display_cost.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/plot_env.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/stubs/synthio.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/stubs/ulab/__init__.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/stubs/ulab/numpy.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/stubs_fx/audiodelays.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/stubs_fx/audiofilters.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/test_arpeggiator.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/test_audio_fx.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/test_env_shapes.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/test_fm_synth.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/test_mono.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/test_param_scaler.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/test_paramset.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/test_patch.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/test_pytest_smoke.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/test_swarm_synth.py +0 -0
- {circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/test_wiring.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: circuitpython-synthtools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -162,6 +162,8 @@ What's Included
|
|
|
162
162
|
``synthio.Note`` graphs (the engines above have their own portamento,
|
|
163
163
|
via ``mono`` + ``glide_time``)
|
|
164
164
|
* ``RollingAverage`` -- moving-average smoothing for noisy knob reads
|
|
165
|
+
* ``Scale`` / ``chord`` -- scales and diatonic/chromatic chords for
|
|
166
|
+
mapping pads and MIDI onto pitch
|
|
165
167
|
|
|
166
168
|
Documentation
|
|
167
169
|
=============
|
|
@@ -142,6 +142,8 @@ What's Included
|
|
|
142
142
|
``synthio.Note`` graphs (the engines above have their own portamento,
|
|
143
143
|
via ``mono`` + ``glide_time``)
|
|
144
144
|
* ``RollingAverage`` -- moving-average smoothing for noisy knob reads
|
|
145
|
+
* ``Scale`` / ``chord`` -- scales and diatonic/chromatic chords for
|
|
146
|
+
mapping pads and MIDI onto pitch
|
|
145
147
|
|
|
146
148
|
Documentation
|
|
147
149
|
=============
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: circuitpython-synthtools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -162,6 +162,8 @@ What's Included
|
|
|
162
162
|
``synthio.Note`` graphs (the engines above have their own portamento,
|
|
163
163
|
via ``mono`` + ``glide_time``)
|
|
164
164
|
* ``RollingAverage`` -- moving-average smoothing for noisy knob reads
|
|
165
|
+
* ``Scale`` / ``chord`` -- scales and diatonic/chromatic chords for
|
|
166
|
+
mapping pads and MIDI onto pitch
|
|
165
167
|
|
|
166
168
|
Documentation
|
|
167
169
|
=============
|
|
@@ -49,6 +49,7 @@ examples/synthinst_wave_demo.py
|
|
|
49
49
|
examples/synthtools_ahr_envelope_bend.py
|
|
50
50
|
examples/synthtools_bassline_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
|
|
54
55
|
examples/synthtools_swarm_demo.py
|
|
@@ -75,6 +76,7 @@ synthtools/bassline_synth.py
|
|
|
75
76
|
synthtools/blocks.py
|
|
76
77
|
synthtools/fm_synth.py
|
|
77
78
|
synthtools/gauge_cluster.py
|
|
79
|
+
synthtools/harmony.py
|
|
78
80
|
synthtools/param_scaler.py
|
|
79
81
|
synthtools/paramset.py
|
|
80
82
|
synthtools/patch.py
|
|
@@ -95,6 +97,7 @@ tests/test_arpeggiator.py
|
|
|
95
97
|
tests/test_audio_fx.py
|
|
96
98
|
tests/test_env_shapes.py
|
|
97
99
|
tests/test_fm_synth.py
|
|
100
|
+
tests/test_harmony.py
|
|
98
101
|
tests/test_mono.py
|
|
99
102
|
tests/test_param_scaler.py
|
|
100
103
|
tests/test_paramset.py
|
|
@@ -53,6 +53,16 @@ A bright DX-style FM bell using FMSynth, with a live fm_ratio / fm_index sweep
|
|
|
53
53
|
:caption: examples/synthtools_fm_demo.py
|
|
54
54
|
:linenos:
|
|
55
55
|
|
|
56
|
+
Harmony demo
|
|
57
|
+
------------
|
|
58
|
+
|
|
59
|
+
Walk scales and play diatonic chords with harmony.Scale, cycling through a
|
|
60
|
+
short chord progression
|
|
61
|
+
|
|
62
|
+
.. literalinclude:: ../examples/synthtools_harmony_demo.py
|
|
63
|
+
:caption: examples/synthtools_harmony_demo.py
|
|
64
|
+
:linenos:
|
|
65
|
+
|
|
56
66
|
|
|
57
67
|
|
|
58
68
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
#
|
|
4
|
+
# Play scales and diatonic chords with synthtools.harmony.
|
|
5
|
+
#
|
|
6
|
+
# A Scale is a root note plus a set of semitone offsets; index it by
|
|
7
|
+
# degree and it wraps with octave shifts, so `scale.degree(i)` maps a row
|
|
8
|
+
# of 8 pads straight onto any scale. `scale.chord(degree, shape)` stacks
|
|
9
|
+
# the chord in-key, so the quality follows the mode for free: the triad
|
|
10
|
+
# on degree 0 of a major scale is major, on degree 1 it is minor.
|
|
11
|
+
#
|
|
12
|
+
# It prints every note and chord it plays, so it also reads as a plain
|
|
13
|
+
# console demo with the mixer down.
|
|
14
|
+
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
from synth_setup import synth as engine
|
|
18
|
+
|
|
19
|
+
from synthtools import Patch, SubtractiveSynth
|
|
20
|
+
from synthtools.harmony import DIATONIC_SHAPE_NAMES, Scale, note_name
|
|
21
|
+
|
|
22
|
+
# fmt: off
|
|
23
|
+
patch = Patch(name="harmony demo", wave="TRI",
|
|
24
|
+
amp_env=[0.01, 0.15, 0.5, 0.25],
|
|
25
|
+
filt_type="LPF", filt_f=2600, filt_q=0.7)
|
|
26
|
+
# fmt: on
|
|
27
|
+
synth = SubtractiveSynth(engine, patch)
|
|
28
|
+
|
|
29
|
+
# (key note, scale name) -- MIDI 48 is C3
|
|
30
|
+
PROGRESSION = (
|
|
31
|
+
(48, "major"),
|
|
32
|
+
(45, "minor"),
|
|
33
|
+
(50, "dorian"),
|
|
34
|
+
(43, "mixolydian"),
|
|
35
|
+
(48, "major_pentatonic"),
|
|
36
|
+
)
|
|
37
|
+
CHORD_SHAPES = ("triad", "7th", "9th", "spread")
|
|
38
|
+
# roman numeral for the degree the chord is built on
|
|
39
|
+
DEGREES = ((0, "I"), (3, "IV"), (4, "V"), (5, "vi"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def play(notes, secs):
|
|
43
|
+
for n in notes:
|
|
44
|
+
synth.note_on(n, velocity=100)
|
|
45
|
+
time.sleep(secs)
|
|
46
|
+
for n in notes:
|
|
47
|
+
synth.note_off(n)
|
|
48
|
+
time.sleep(0.06)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
while True:
|
|
52
|
+
for root, scale_name in PROGRESSION:
|
|
53
|
+
scale = Scale(root=root, name=scale_name)
|
|
54
|
+
print("\n--- %s %s ---" % (note_name(root, with_octave=False), scale.name))
|
|
55
|
+
|
|
56
|
+
# walk the scale, one note per pad
|
|
57
|
+
line = " ".join(note_name(scale.degree(i)) for i in range(scale.size + 1))
|
|
58
|
+
print("scale:", line)
|
|
59
|
+
for i in range(scale.size + 1):
|
|
60
|
+
play([scale.degree(i)], 0.16)
|
|
61
|
+
time.sleep(0.3)
|
|
62
|
+
|
|
63
|
+
# diatonic chords on a few degrees, through a few voicings
|
|
64
|
+
for degree, roman in DEGREES:
|
|
65
|
+
for shape in CHORD_SHAPES:
|
|
66
|
+
notes = scale.chord(degree, shape)
|
|
67
|
+
print(" %-3s %-6s %s" % (roman, shape, [note_name(n) for n in notes]))
|
|
68
|
+
play(notes, 0.4)
|
|
69
|
+
time.sleep(0.15)
|
|
70
|
+
|
|
71
|
+
print("\n(all %d DIATONIC_SHAPES: %s)" % (len(DIATONIC_SHAPE_NAMES), DIATONIC_SHAPE_NAMES))
|
|
@@ -13,7 +13,7 @@ requires = [
|
|
|
13
13
|
[project]
|
|
14
14
|
name = "circuitpython-synthtools"
|
|
15
15
|
description = "CircuitPython helper library to do help doing synthio"
|
|
16
|
-
version = "0.
|
|
16
|
+
version = "0.7.0"
|
|
17
17
|
readme = "README.rst"
|
|
18
18
|
authors = [
|
|
19
19
|
{name = "Tod Kurt", email = "tod@todbot.com"}
|
|
@@ -36,7 +36,7 @@ Anything not listed below is still importable by its module path::
|
|
|
36
36
|
|
|
37
37
|
import sys
|
|
38
38
|
|
|
39
|
-
__version__ = "0.
|
|
39
|
+
__version__ = "0.7.0"
|
|
40
40
|
__repo__ = "https://github.com/todbot/CircuitPython_SynthTools.git"
|
|
41
41
|
|
|
42
42
|
#: Public name -> the submodule that defines it. Every entry is loaded on
|
|
@@ -49,6 +49,12 @@ _LAZY = {
|
|
|
49
49
|
"BasslineSynth": "bassline_synth",
|
|
50
50
|
"FMSynth": "fm_synth",
|
|
51
51
|
"GaugeCluster": "gauge_cluster",
|
|
52
|
+
"Scale": "harmony",
|
|
53
|
+
"chord": "harmony",
|
|
54
|
+
"note_name": "harmony",
|
|
55
|
+
"SCALES": "harmony",
|
|
56
|
+
"CHORDS": "harmony",
|
|
57
|
+
"DIATONIC_SHAPES": "harmony",
|
|
52
58
|
"ParamScaler": "param_scaler",
|
|
53
59
|
"Patch": "patch",
|
|
54
60
|
"load_patches": "patch",
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""
|
|
4
|
+
``harmony``
|
|
5
|
+
================================================================================
|
|
6
|
+
|
|
7
|
+
Scales and chords for mapping controllers onto musical pitch.
|
|
8
|
+
|
|
9
|
+
A ``Scale`` is a root MIDI note plus a set of semitone offsets. Index it
|
|
10
|
+
by scale degree to get MIDI notes; degrees past the end (or negative)
|
|
11
|
+
wrap with octave shifts, so a row of pads maps straight onto the scale
|
|
12
|
+
without bounds checks. ``Scale.chord()`` stacks in-key chords by degree;
|
|
13
|
+
the module-level ``chord()`` builds chromatic chords from an interval
|
|
14
|
+
table.
|
|
15
|
+
|
|
16
|
+
Pure Python, no dependencies. Part of synthtools.
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# The *_NAMES tuples are written out rather than `tuple(SCALES)` because
|
|
21
|
+
# MicroPython does not guarantee dict iteration order (CPython 3.7+ does),
|
|
22
|
+
# and a UI that builds a selector from the order and stores an index needs
|
|
23
|
+
# it stable across interpreters. Keep each tuple in step with its dict.
|
|
24
|
+
|
|
25
|
+
#: name -> ascending semitone offsets from the root, one octave. The root
|
|
26
|
+
#: (0) is always the first entry; the octave (12) is implied, not listed.
|
|
27
|
+
SCALES = {
|
|
28
|
+
"major": (0, 2, 4, 5, 7, 9, 11),
|
|
29
|
+
"minor": (0, 2, 3, 5, 7, 8, 10), # natural minor / aeolian
|
|
30
|
+
"dorian": (0, 2, 3, 5, 7, 9, 10),
|
|
31
|
+
"phrygian": (0, 1, 3, 5, 7, 8, 10),
|
|
32
|
+
"lydian": (0, 2, 4, 6, 7, 9, 11),
|
|
33
|
+
"mixolydian": (0, 2, 4, 5, 7, 9, 10),
|
|
34
|
+
"locrian": (0, 1, 3, 5, 6, 8, 10),
|
|
35
|
+
"harmonic_minor": (0, 2, 3, 5, 7, 8, 11),
|
|
36
|
+
"melodic_minor": (0, 2, 3, 5, 7, 9, 11),
|
|
37
|
+
"major_pentatonic": (0, 2, 4, 7, 9),
|
|
38
|
+
"minor_pentatonic": (0, 3, 5, 7, 10),
|
|
39
|
+
"blues": (0, 3, 5, 6, 7, 10),
|
|
40
|
+
"whole_tone": (0, 2, 4, 6, 8, 10),
|
|
41
|
+
"chromatic": (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
SCALE_NAMES = (
|
|
45
|
+
"major",
|
|
46
|
+
"minor",
|
|
47
|
+
"dorian",
|
|
48
|
+
"phrygian",
|
|
49
|
+
"lydian",
|
|
50
|
+
"mixolydian",
|
|
51
|
+
"locrian",
|
|
52
|
+
"harmonic_minor",
|
|
53
|
+
"melodic_minor",
|
|
54
|
+
"major_pentatonic",
|
|
55
|
+
"minor_pentatonic",
|
|
56
|
+
"blues",
|
|
57
|
+
"whole_tone",
|
|
58
|
+
"chromatic",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
#: name -> semitone offsets from a root note. Chromatic, key-independent.
|
|
62
|
+
CHORDS = {
|
|
63
|
+
"1": (0,),
|
|
64
|
+
"5": (0, 7),
|
|
65
|
+
"oct": (0, 12),
|
|
66
|
+
"maj": (0, 4, 7),
|
|
67
|
+
"min": (0, 3, 7),
|
|
68
|
+
"dim": (0, 3, 6),
|
|
69
|
+
"aug": (0, 4, 8),
|
|
70
|
+
"sus2": (0, 2, 7),
|
|
71
|
+
"sus4": (0, 5, 7),
|
|
72
|
+
"6": (0, 4, 7, 9),
|
|
73
|
+
"min6": (0, 3, 7, 9),
|
|
74
|
+
"maj7": (0, 4, 7, 11),
|
|
75
|
+
"min7": (0, 3, 7, 10),
|
|
76
|
+
"dom7": (0, 4, 7, 10),
|
|
77
|
+
"dim7": (0, 3, 6, 9),
|
|
78
|
+
"add9": (0, 4, 7, 14),
|
|
79
|
+
"maj9": (0, 4, 7, 11, 14),
|
|
80
|
+
"min9": (0, 3, 7, 10, 14),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
CHORD_NAMES = (
|
|
84
|
+
"1",
|
|
85
|
+
"5",
|
|
86
|
+
"oct",
|
|
87
|
+
"maj",
|
|
88
|
+
"min",
|
|
89
|
+
"dim",
|
|
90
|
+
"aug",
|
|
91
|
+
"sus2",
|
|
92
|
+
"sus4",
|
|
93
|
+
"6",
|
|
94
|
+
"min6",
|
|
95
|
+
"maj7",
|
|
96
|
+
"min7",
|
|
97
|
+
"dom7",
|
|
98
|
+
"dim7",
|
|
99
|
+
"add9",
|
|
100
|
+
"maj9",
|
|
101
|
+
"min9",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
#: name -> scale-degree offsets (not semitones). Stacked in the current
|
|
105
|
+
#: scale, so quality follows the key: "triad" on degree 0 of a major
|
|
106
|
+
#: scale is major, on degree 1 it is minor, and so on.
|
|
107
|
+
DIATONIC_SHAPES = {
|
|
108
|
+
"root": (0,),
|
|
109
|
+
"oct": (0, 7),
|
|
110
|
+
"5th": (0, 4),
|
|
111
|
+
"triad": (0, 2, 4),
|
|
112
|
+
"7th": (0, 2, 4, 6),
|
|
113
|
+
"9th": (0, 2, 4, 6, 8),
|
|
114
|
+
"sus": (0, 3, 4),
|
|
115
|
+
"spread": (0, 4, 9),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
DIATONIC_SHAPE_NAMES = ("root", "oct", "5th", "triad", "7th", "9th", "sus", "spread")
|
|
119
|
+
|
|
120
|
+
NOTE_NAMES = ("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def note_name(midi, with_octave=True):
|
|
124
|
+
"""``"C4"``, ``"F#3"`` ... MIDI 60 is C4 here. Software that calls
|
|
125
|
+
middle C "C3" should subtract 1 from the octave."""
|
|
126
|
+
n = NOTE_NAMES[int(midi) % 12]
|
|
127
|
+
return "%s%d" % (n, int(midi) // 12 - 1) if with_octave else n
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def chord(root, name):
|
|
131
|
+
"""MIDI notes of chromatic chord ``name`` (see ``CHORDS``) on ``root``."""
|
|
132
|
+
return [root + i for i in CHORDS[name]]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Scale:
|
|
136
|
+
"""A scale: a root MIDI note and a set of semitone offsets.
|
|
137
|
+
|
|
138
|
+
:param root: MIDI note of scale degree 0
|
|
139
|
+
:param name: a key of ``SCALES``, or an explicit sequence of ascending
|
|
140
|
+
semitone offsets starting at 0
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def __init__(self, root=60, name="major"):
|
|
144
|
+
self.root = root
|
|
145
|
+
self.set_scale(name)
|
|
146
|
+
|
|
147
|
+
def set_scale(self, name):
|
|
148
|
+
"""Switch scale in place, keeping the root. ``name`` is a ``SCALES``
|
|
149
|
+
key or an explicit offset sequence; an unknown name falls back to
|
|
150
|
+
major rather than raising, so a knob mapped past the list is safe."""
|
|
151
|
+
if isinstance(name, str):
|
|
152
|
+
self.name = name if name in SCALES else "major"
|
|
153
|
+
self.steps = SCALES[self.name]
|
|
154
|
+
else:
|
|
155
|
+
self.name = "custom"
|
|
156
|
+
self.steps = tuple(name)
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def size(self):
|
|
160
|
+
"""Notes per octave in this scale."""
|
|
161
|
+
return len(self.steps)
|
|
162
|
+
|
|
163
|
+
def degree(self, n):
|
|
164
|
+
"""MIDI note for 0-based scale degree ``n``. ``n`` may be any
|
|
165
|
+
integer: it wraps with octave shifts, so ``degree(size)`` is the
|
|
166
|
+
octave above the root and ``degree(-1)`` the step below it."""
|
|
167
|
+
octaves, i = divmod(int(n), len(self.steps))
|
|
168
|
+
return self.root + 12 * octaves + self.steps[i]
|
|
169
|
+
|
|
170
|
+
def __getitem__(self, n):
|
|
171
|
+
return self.degree(n)
|
|
172
|
+
|
|
173
|
+
def chord(self, degree, shape="triad"):
|
|
174
|
+
"""Diatonic chord as a list of MIDI notes: the degree offsets of
|
|
175
|
+
``shape`` (a ``DIATONIC_SHAPES`` key or an explicit offset
|
|
176
|
+
sequence) added to ``degree``, each taken through the scale so the
|
|
177
|
+
chord stays in key."""
|
|
178
|
+
offsets = DIATONIC_SHAPES[shape] if isinstance(shape, str) else shape
|
|
179
|
+
return [self.degree(degree + o) for o in offsets]
|
|
180
|
+
|
|
181
|
+
def contains(self, midi):
|
|
182
|
+
"""Whether ``midi`` is a note of this scale, in any octave."""
|
|
183
|
+
return (int(midi) - self.root) % 12 in self.steps
|
|
184
|
+
|
|
185
|
+
def snap(self, midi):
|
|
186
|
+
"""The scale note nearest ``midi`` (ties round down). Useful for
|
|
187
|
+
quantizing a chromatic source -- MIDI input, an LFO -- onto the
|
|
188
|
+
scale."""
|
|
189
|
+
rel = int(round(midi)) - self.root
|
|
190
|
+
octaves, semis = divmod(rel, 12)
|
|
191
|
+
best = self.steps[0]
|
|
192
|
+
for step in self.steps:
|
|
193
|
+
if abs(step - semis) < abs(best - semis):
|
|
194
|
+
best = step
|
|
195
|
+
return self.root + 12 * octaves + best
|
|
196
|
+
|
|
197
|
+
def __repr__(self):
|
|
198
|
+
return "Scale(root=%d, name=%r)" % (self.root, self.name)
|
|
@@ -41,6 +41,7 @@ if command -v python3 >/dev/null 2>&1; then
|
|
|
41
41
|
run python3 tests/test_arpeggiator.py
|
|
42
42
|
run python3 tests/test_paramset.py
|
|
43
43
|
run python3 tests/test_param_scaler.py
|
|
44
|
+
run python3 tests/test_harmony.py
|
|
44
45
|
else
|
|
45
46
|
echo "python3 not found, skipping the CPython tier"
|
|
46
47
|
status=1
|
|
@@ -57,6 +58,7 @@ if command -v micropython >/dev/null 2>&1; then
|
|
|
57
58
|
run micropython tests/test_arpeggiator.py
|
|
58
59
|
run micropython tests/test_paramset.py
|
|
59
60
|
run micropython tests/test_param_scaler.py
|
|
61
|
+
run micropython tests/test_harmony.py
|
|
60
62
|
else
|
|
61
63
|
printf '\nmicropython not found, skipping the portability tier.\n'
|
|
62
64
|
printf 'Install it to catch the MicroPython-only bugs: brew install micropython\n'
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""harmony: scales, chords, and the degree-wrap arithmetic.
|
|
4
|
+
|
|
5
|
+
harmony.py imports nothing, so this runs on a bare interpreter and is a
|
|
6
|
+
portability canary alongside test_patch.py / test_paramset.py: the whole
|
|
7
|
+
reason Scale.degree() uses divmod is that a pad row indexes past the end
|
|
8
|
+
of the scale, and negative divmod has to wrap the same way on MicroPython.
|
|
9
|
+
|
|
10
|
+
python3 tests/test_harmony.py
|
|
11
|
+
micropython tests/test_harmony.py
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
_D = __file__.rsplit("/", 1)[0] if "/" in __file__ else "."
|
|
17
|
+
sys.path.insert(0, _D + "/../synthtools") # direct: skips synthtools/__init__
|
|
18
|
+
|
|
19
|
+
from harmony import ( # noqa: E402
|
|
20
|
+
CHORD_NAMES,
|
|
21
|
+
CHORDS,
|
|
22
|
+
DIATONIC_SHAPE_NAMES,
|
|
23
|
+
DIATONIC_SHAPES,
|
|
24
|
+
SCALE_NAMES,
|
|
25
|
+
SCALES,
|
|
26
|
+
Scale,
|
|
27
|
+
chord,
|
|
28
|
+
note_name,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
fails = []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def ck(cond, msg):
|
|
35
|
+
if not cond:
|
|
36
|
+
fails.append(msg)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# --- the portability canary -------------------------------------------
|
|
40
|
+
# Python floors towards negative infinity, so divmod(-1, 7) == (-1, 6):
|
|
41
|
+
# degree(-1) is the top note of the octave below. MicroPython must agree.
|
|
42
|
+
ck(divmod(-1, 7) == (-1, 6), "negative divmod does not wrap: %r" % (divmod(-1, 7),))
|
|
43
|
+
ck(divmod(-8, 7) == (-2, 6), "negative divmod does not wrap over an octave")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# --- Scale.degree ----------------------------------------------------
|
|
47
|
+
s = Scale(60, "major")
|
|
48
|
+
ck(
|
|
49
|
+
[s.degree(n) for n in range(8)] == [60, 62, 64, 65, 67, 69, 71, 72],
|
|
50
|
+
"C major degrees 0..7 wrong: %r" % [s.degree(n) for n in range(8)],
|
|
51
|
+
)
|
|
52
|
+
ck(s.degree(7) == s.root + 12, "degree(size) is not the octave: %d" % s.degree(7))
|
|
53
|
+
ck(s.degree(-1) == 59, "degree(-1) is not the leading tone below: %d" % s.degree(-1))
|
|
54
|
+
ck(s.degree(14) == 84, "two octaves up wrong: %d" % s.degree(14))
|
|
55
|
+
ck(s[3] == s.degree(3), "__getitem__ disagrees with degree()")
|
|
56
|
+
ck(s.size == 7, "major scale size should be 7, got %d" % s.size)
|
|
57
|
+
|
|
58
|
+
# a pentatonic keeps climbing past its 5 notes
|
|
59
|
+
p = Scale(57, "minor_pentatonic")
|
|
60
|
+
ck(
|
|
61
|
+
[p.degree(n) for n in range(6)] == [57, 60, 62, 64, 67, 69],
|
|
62
|
+
"A minor pentatonic wrong: %r" % [p.degree(n) for n in range(6)],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# --- chromatic chord() --------------------------------------------------
|
|
67
|
+
ck(chord(60, "maj") == [60, 64, 67], "C maj wrong: %r" % chord(60, "maj"))
|
|
68
|
+
ck(chord(60, "min") == [60, 63, 67], "C min wrong: %r" % chord(60, "min"))
|
|
69
|
+
ck(chord(60, "dom7") == [60, 64, 67, 70], "C7 wrong: %r" % chord(60, "dom7"))
|
|
70
|
+
ck(chord(48, "maj9") == [48, 52, 55, 59, 62], "Cmaj9 wrong: %r" % chord(48, "maj9"))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# --- Scale.chord: diatonic quality falls out of the key --------------
|
|
74
|
+
c = Scale(60, "major")
|
|
75
|
+
ck(c.chord(0, "triad") == [60, 64, 67], "I is not major: %r" % c.chord(0, "triad"))
|
|
76
|
+
ck(c.chord(1, "triad") == [62, 65, 69], "ii is not minor: %r" % c.chord(1, "triad"))
|
|
77
|
+
ck(c.chord(6, "triad") == [71, 74, 77], "vii is not diminished-shaped")
|
|
78
|
+
ck(c.chord(4, "7th") == [67, 71, 74, 77], "V7 is not dominant-shaped: %r" % c.chord(4, "7th"))
|
|
79
|
+
ck(len(c.chord(0, "9th")) == 5, "9th is not 5 notes")
|
|
80
|
+
ck(c.chord(0, (0, 4)) == [60, 67], "explicit degree-offset shape ignored")
|
|
81
|
+
ck(c.chord(0, "spread")[-1] > c.chord(0, "spread")[0] + 12, "spread does not open past an octave")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# --- set_scale ------------------------------------------------------
|
|
85
|
+
q = Scale(60, "major")
|
|
86
|
+
q.set_scale("bogus_scale")
|
|
87
|
+
ck(q.name == "major" and q.steps == SCALES["major"], "unknown scale did not fall back to major")
|
|
88
|
+
q.set_scale((0, 3, 7))
|
|
89
|
+
ck(q.name == "custom" and q.steps == (0, 3, 7), "explicit offsets not accepted")
|
|
90
|
+
ck(q.degree(0) == 60 and q.degree(1) == 63 and q.degree(3) == 72, "custom scale degrees wrong")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --- contains / snap --------------------------------------------------
|
|
94
|
+
cm = Scale(60, "major")
|
|
95
|
+
ck(cm.contains(64) and cm.contains(64 + 12) and cm.contains(64 - 24), "E should be in C major")
|
|
96
|
+
ck(not cm.contains(61) and not cm.contains(61 + 12), "C# should not be in C major")
|
|
97
|
+
ck(cm.snap(61) == 60, "snap C#4 -> C4 expected, got %d" % cm.snap(61))
|
|
98
|
+
ck(cm.snap(66) == 65, "snap F#4 -> F4 expected, got %d" % cm.snap(66))
|
|
99
|
+
ck(cm.snap(67) == 67, "snap of an in-scale note must be a no-op, got %d" % cm.snap(67))
|
|
100
|
+
ck(cm.snap(72) == 72, "snap of the octave must be a no-op, got %d" % cm.snap(72))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# --- note_name -----------------------------------------------------
|
|
104
|
+
ck(note_name(60) == "C4", "MIDI 60 is C4 here, got %s" % note_name(60))
|
|
105
|
+
ck(note_name(69) == "A4", "MIDI 69 is A4, got %s" % note_name(69))
|
|
106
|
+
ck(note_name(61, False) == "C#", "note_name without octave wrong: %s" % note_name(61, False))
|
|
107
|
+
ck(note_name(48) == "C3" and note_name(72) == "C5", "octave numbering off")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# --- table invariants -----------------------------------------------
|
|
111
|
+
for name, steps in SCALES.items():
|
|
112
|
+
ck(isinstance(steps, tuple) and steps[0] == 0, "scale %s must be a tuple starting at 0" % name)
|
|
113
|
+
ck(list(steps) == sorted(steps), "scale %s not ascending" % name)
|
|
114
|
+
ck(all(0 <= x <= 11 for x in steps), "scale %s has an out-of-octave step" % name)
|
|
115
|
+
for name, ivals in CHORDS.items():
|
|
116
|
+
ck(isinstance(ivals, tuple) and ivals[0] == 0, "chord %s must be a tuple starting at 0" % name)
|
|
117
|
+
for name, offs in DIATONIC_SHAPES.items():
|
|
118
|
+
ck(isinstance(offs, tuple) and offs[0] == 0, "shape %s must be a tuple starting at 0" % name)
|
|
119
|
+
|
|
120
|
+
# The *_NAMES tuples are hand-written (MicroPython dict order is not
|
|
121
|
+
# stable) so a UI selector index means the same thing everywhere: check
|
|
122
|
+
# they cover their dicts exactly, and that the fixed order holds.
|
|
123
|
+
for names, table, what in (
|
|
124
|
+
(SCALE_NAMES, SCALES, "SCALE_NAMES"),
|
|
125
|
+
(CHORD_NAMES, CHORDS, "CHORD_NAMES"),
|
|
126
|
+
(DIATONIC_SHAPE_NAMES, DIATONIC_SHAPES, "DIATONIC_SHAPE_NAMES"),
|
|
127
|
+
):
|
|
128
|
+
ck(set(names) == set(table), "%s does not cover its dict exactly" % what)
|
|
129
|
+
ck(len(names) == len(table), "%s has a duplicate or a gap" % what)
|
|
130
|
+
ck(SCALE_NAMES[0] == "major", "major must stay first in SCALE_NAMES (the default)")
|
|
131
|
+
ck(DIATONIC_SHAPE_NAMES[3] == "triad", "DIATONIC_SHAPE_NAMES order shifted")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if fails:
|
|
135
|
+
print("FAIL (%d)" % len(fails))
|
|
136
|
+
for f in fails:
|
|
137
|
+
print(" -", f)
|
|
138
|
+
sys.exit(1)
|
|
139
|
+
print("test_harmony: ok")
|
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/.github/workflows/build.yml
RENAMED
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/.github/workflows/release_gh.yml
RENAMED
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/.github/workflows/release_pypi.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/docs/_static/favicon.ico.license
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/arpeggiator_demo.py
RENAMED
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/step_sequencer_demo.py
RENAMED
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthinst_code1.py
RENAMED
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthinst_fenv_demo.py
RENAMED
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthinst_pad_demo.py
RENAMED
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthinst_pitch_demo.py
RENAMED
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthinst_wave_demo.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthtools_fm_demo.py
RENAMED
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthtools_midi_demo.py
RENAMED
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthtools_simpletest.py
RENAMED
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/synthtools_swarm_demo.py
RENAMED
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/trig_sequencer_demo.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/wavetables/BRAIDS02.WAV
RENAMED
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/wavetables/PLAITS02.WAV
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/examples/wavs/kit0_909/readme.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/ahr_envelope.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/bassline_synth.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/gauge_cluster.py
RENAMED
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/param_scaler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/pitch_glider.py
RENAMED
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/step_sequencer.py
RENAMED
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/subtractive_synth.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/trig_sequencer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/synthtools/wavetable_synth.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/hw/test_display_cost.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/stubs/ulab/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/stubs_fx/audiodelays.py
RENAMED
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/stubs_fx/audiofilters.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/test_param_scaler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{circuitpython_synthtools-0.6.0 → circuitpython_synthtools-0.7.0}/tests/test_pytest_smoke.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|