Skip to content

Commit 11256b0

Browse files
bwhitmanclaude
andcommitted
Remove AMYboard patch-file support
Patch files (.patch wire-command dumps) are no longer used. Removes: - amyboard.py: load_patch_file(), save_patch_file() (the last caller of amy.get_synth_commands() in tulipcc), and the PatchSelector encoder app - spss.js: dead num_oscs_from_patch_file_content() and write_channel_dirty_patch_file() helpers, stale comments referencing restore_patch_state_from_files - server: patch_selector/PatchSelector from the display/encoder hardware-tag regexes and the matching test_hardware_tags check - docs: patch_selector() mention and example in accessories.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3c5860e commit 11256b0

5 files changed

Lines changed: 8 additions & 218 deletions

File tree

docs/amyboard/accessories.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ amyboard.monitor_encoders()
120120
```
121121

122122
- ![Adafruit QT Quad Rotary Encoder](img/accessory_adafruit_quad_encoder.jpg)
123-
[**Adafruit I2C QT Quad Rotary Encoder Breakout**](https://www.adafruit.com/product/5752) -- Four rotary encoders with built-in push buttons and one RGB NeoPixel per encoder on a single I2C breakout, running seesaw firmware. AMYboard has built-in support via `read_encoder()`, `init_buttons()`, `read_buttons()`, and `init_neopixels()`/`set_neopixel()`/`show_neopixels()`. With a display connected, you can use `patch_selector()` to scroll through your `.patch` files with one encoder and load them with a click.
123+
[**Adafruit I2C QT Quad Rotary Encoder Breakout**](https://www.adafruit.com/product/5752) -- Four rotary encoders with built-in push buttons and one RGB NeoPixel per encoder on a single I2C breakout, running seesaw firmware. AMYboard has built-in support via `read_encoder()`, `init_buttons()`, `read_buttons()`, and `init_neopixels()`/`set_neopixel()`/`show_neopixels()`.
124124

125125
```python
126126
import amyboard
@@ -141,11 +141,6 @@ amyboard.set_neopixel(1, 0, 64, 0) # encoder 1 -> dim green
141141
amyboard.set_neopixel(2, 0, 0, 64) # encoder 2 -> dim blue
142142
amyboard.set_neopixel(3, 32, 32, 0) # encoder 3 -> dim yellow
143143
amyboard.show_neopixels() # latch staged colors to the LEDs
144-
145-
# Patch selector: scroll through patches and load on click
146-
# (requires a display to be connected)
147-
amyboard.init_display()
148-
amyboard.patch_selector()
149144
```
150145

151146
- ![M5Stack 8-Encoder Unit](img/accessory_m5_8encoder.jpg)

tulip/amyboardweb/static/spss.js

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -762,19 +762,6 @@ function remove_current_environment_file_if_exists(filename) {
762762
return false;
763763
}
764764

765-
function num_oscs_from_patch_file_content(source) {
766-
var maxOsc = -1;
767-
var lines = String(source || "").split(/\r?\n/);
768-
for (var i = 0; i < lines.length; i++) {
769-
var m = lines[i].match(/^v(\d+)/);
770-
if (m) {
771-
var osc = parseInt(m[1], 10);
772-
if (osc > maxOsc) maxOsc = osc;
773-
}
774-
}
775-
return maxOsc < 0 ? 3 : maxOsc + 1;
776-
}
777-
778765
function get_wire_commands_for_juno_patch(patch) {
779766
// The new convention for AMYboard voice osc usage.
780767
const CTL_OSC = 0; // Osc with filter & env commands.
@@ -1053,8 +1040,6 @@ function get_wire_commands_for_channel(channel) {
10531040
return lines;
10541041
}
10551042

1056-
function write_channel_dirty_patch_file(channel) { /* no-op */ }
1057-
10581043
const pending_channel_knob_sync = new Array(17).fill(false);
10591044

10601045
function schedule_channel_knob_sync_after_ui_ready(channel) {
@@ -6200,9 +6185,9 @@ async function start_audio() {
62006185
await amy_live_start_web();
62016186
}
62026187
// Allow a few audio callbacks to fire so amy_execute_deltas processes
6203-
// any wire commands that were queued before audio started (e.g. from
6204-
// restore_patch_state_from_files during boot). Without this delay,
6205-
// yield_synth_commands would read stale default values for global effects.
6188+
// any wire commands that were queued before audio started. Without this
6189+
// delay, yield_synth_commands would read stale default values for global
6190+
// effects.
62066191
await new Promise(function(resolve) { setTimeout(resolve, 150); });
62076192
// Set up AnalyserNode to capture output waveform for Python
62086193
try {
@@ -6235,8 +6220,8 @@ async function start_audio() {
62356220
// to race a default run_sketch() against check_url_env_params's deferred load.
62366221
if (mp && !_url_env_pending) {
62376222
// Init synth 1 with default Juno patch before run_sketch applies knobs.
6238-
// On main, restore_patch_state_from_files did this. Without it, synth 1
6239-
// doesn't exist and _apply_knobs_text fails with "synth not defined".
6223+
// Without it, synth 1 doesn't exist and _apply_knobs_text fails with
6224+
// "synth not defined".
62406225
amy_add_message("i1K257iv6Z");
62416226
// Let AMY process the synth init before running Python.
62426227
await new Promise(function(resolve) { setTimeout(resolve, 100); });

tulip/server/amyboardworld_db_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1581,14 +1581,14 @@ def _unique_generator_filename(base: str, gen_id: int) -> str:
15811581
_HW_DISPLAY_RE = re.compile(
15821582
r"\binit_display\s*\(|\bdisplay_refresh\s*\("
15831583
r"|\bdisplay\s*\.\s*(?:text|fill|fill_rect|rect|line|hline|vline|pixel|scroll|show|refresh|clear|message)\b"
1584-
r"|\bmonitor_encoders\s*\(|\bdraw_waveform\s*\(|\bshow_midi_ccs\s*\(|\bpatch_selector\s*\(|\bPatchSelector\s*\("
1584+
r"|\bmonitor_encoders\s*\(|\bdraw_waveform\s*\(|\bshow_midi_ccs\s*\("
15851585
)
15861586
# Any rotary-encoder accessory: the unified amyboard.encoder() API, the legacy
15871587
# seesaw helpers, the legacy m5_8encoder module, and the helpers built on them.
15881588
_HW_ENCODER_RE = re.compile(
15891589
r"\bencoder\s*\(|\bamyboard\s*\.\s*Encoder\s*\(|\bread_encoder\s*\(|\bread_buttons\s*\(|\binit_buttons\s*\("
15901590
r"|\bset_neopixel\s*\(|\binit_neopixels\s*\(|\bshow_neopixels\s*\(|\bm5_8encoder\b"
1591-
r"|\bmonitor_encoders\s*\(|\bpatch_selector\s*\(|\bPatchSelector\s*\("
1591+
r"|\bmonitor_encoders\s*\("
15921592
)
15931593
# Evidence a sketch is written for the 8-encoder unit specifically: the legacy
15941594
# module, forcing type="m5stack", the m5stack-only toggle switch, touching an

tulip/server/test_hardware_tags.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ def tags(code: str) -> list[str]:
7171

7272
# --- combinations ---
7373
check("display + encoder", tags("import amyboard\namyboard.init_display()\nenc = amyboard.encoder()\ndef loop():\n amyboard.display.text(str(enc.read(0)),0,0,255)\n amyboard.display.show()") == ["display", "encoder"])
74-
check("patch_selector implies display + encoder", tags("import amyboard\namyboard.patch_selector()\ndef loop():\n pass") == ["display", "encoder"])
7574

7675
# --- against the bundled example sketches ---
7776
SKETCHES = Path(__file__).resolve().parents[1] / "amyboardweb" / "sketches"

tulip/shared/amyboard-py/amyboard.py

Lines changed: 0 additions & 189 deletions
Original file line numberDiff line numberDiff line change
@@ -257,46 +257,6 @@ def _clear_directory(path):
257257
_remove_tree(path.rstrip("/") + "/" + name)
258258

259259

260-
def load_patch_file(filename, synth=1, num_voices=6):
261-
"""Load a .patch file (list of AMY wire commands) into a given synth."""
262-
263-
def _num_oscs_for_patch(patch):
264-
"""Scan for how many oscs are used in a patch."""
265-
top_osc = 0
266-
for line in patch.split('\n'):
267-
if not line:
268-
continue
269-
if line[0] == 'v':
270-
osc_num = int(line[1]) # Cannot see osc nums > 9.
271-
if osc_num > top_osc:
272-
top_osc = osc_num
273-
return top_osc + 1
274-
275-
try:
276-
with open(filename, 'rb') as f:
277-
patch = f.read().decode('latin-1')
278-
except OSError:
279-
print("error reading", filename)
280-
return
281-
282-
# Reset global FX.
283-
amy.send_raw("V1x0,0,0M0,500,,0,0k0,320,0.5,0.5h0,0.85,0.5,3000")
284-
# Reset the synth.
285-
oscs_per_voice = _num_oscs_for_patch(patch)
286-
amy.send_raw('i%div%din%d' % (synth, num_voices, oscs_per_voice))
287-
# Execute the patch.
288-
for line in patch.split('\n'):
289-
if line and not line.startswith('#'):
290-
amy.send_raw('i%d%s' % (synth, line))
291-
292-
293-
def save_patch_file(filename, synth=1):
294-
"""Write the current state of a synth out to a patch file."""
295-
patch_commands = amy.get_synth_commands(synth)
296-
with open(filename, 'w') as f:
297-
f.write(patch_commands)
298-
299-
300260
def ensure_user_environment():
301261
import os
302262
user_base = tulip.root_dir() + "user"
@@ -1442,152 +1402,3 @@ def draw_waveform():
14421402
last_y = y
14431403
display.show()
14441404

1445-
1446-
# Patch Selector -
1447-
# If you have a display and a rotary encoder on I2C
1448-
# this function will let you scroll through your available patches
1449-
# and load each one by clicking the rotary encoder.
1450-
#
1451-
# You can launch it from sketch.py like this:
1452-
#
1453-
# import amyboard
1454-
# amyboard.patch_selector()
1455-
#
1456-
# or, for the 4-knob Adafruit rotary encoder:
1457-
#
1458-
# amyboard.patch_selector(seesaw_dev=0x49, button_pin=12)
1459-
1460-
class PatchSelector:
1461-
1462-
def __init__(
1463-
self, synth=1, seesaw_dev=0x36, encoder=0, button_pin=24,
1464-
patch_dir='/user/current', extension='.patch',
1465-
hold_max=30, exit_callback=lambda: True
1466-
):
1467-
"""Endless loop scrolling through patch files and installing on click. Long click rewrites the file."""
1468-
self.new_file_prefix = '* '
1469-
self.synth = synth
1470-
self.seesaw_dev = seesaw_dev
1471-
self.encoder = encoder
1472-
self.button_pin = button_pin
1473-
self.patch_dir = patch_dir
1474-
self.extension = extension
1475-
self.hold_max = hold_max # Count before "long press" action in ~ deciseconds
1476-
self.exit_callback = exit_callback # Not currently used.
1477-
# State
1478-
self.patches = self._list_patches()
1479-
self.index = 0 # within patches
1480-
self.button_state = False
1481-
self.hold_count = 0
1482-
# Make sure we always start at the top of the list.
1483-
self.last_encoder_value = 0
1484-
self.encoder_offset = -self.read_encoder()
1485-
# Initialize
1486-
display.clear()
1487-
display.message("PATCH SELECTOR", 0)
1488-
if self.patches:
1489-
display.message(self.patches[0], 2)
1490-
init_buttons(pins=(self.button_pin,), seesaw_dev=self.seesaw_dev)
1491-
1492-
def _list_patches(self):
1493-
"""List of the patch files."""
1494-
try:
1495-
patches = [f[:-len(self.extension)] for f in os.listdir(self.patch_dir) if f.endswith(self.extension)]
1496-
except OSError:
1497-
patches = []
1498-
for new_patch_num in range(100):
1499-
new_patch_name = '~NEW PATCH %02d' % new_patch_num
1500-
if new_patch_name not in patches:
1501-
patches.append(self.new_file_prefix + new_patch_name)
1502-
break
1503-
return patches
1504-
1505-
def patch_name(self, index, remove_star=False):
1506-
"""Return the index'th patch name from the stored list, removing "* " prefix if needed."""
1507-
name = self.patches[index % len(self.patches)]
1508-
if remove_star and name and name[:len(self.new_file_prefix)] == self.new_file_prefix:
1509-
name = name[len(self.new_file_prefix):]
1510-
return name
1511-
1512-
def read_encoder(self):
1513-
value = self.last_encoder_value
1514-
try:
1515-
value = read_encoder(self.encoder, seesaw_dev=self.seesaw_dev)
1516-
except OSError:
1517-
# Ignore I2C errors
1518-
pass
1519-
self.last_encoder_vale = value
1520-
return value
1521-
1522-
def read_button(self):
1523-
value = False
1524-
try:
1525-
value = read_buttons(pins=(self.button_pin,), seesaw_dev=self.seesaw_dev)[0]
1526-
except OSError:
1527-
# Ignore I2C errors
1528-
pass
1529-
return value
1530-
1531-
def update(self):
1532-
index = (self.read_encoder() + self.encoder_offset) % len(self.patches)
1533-
button_state = self.read_button()
1534-
if (index, button_state) != (self.index, self.button_state):
1535-
# Only rewrite display if it's out of sync.
1536-
display.message(self.patch_name(index), 2, inverse=button_state)
1537-
if button_state:
1538-
# Button is still down.
1539-
self.hold_count += 1
1540-
if self.hold_count == self.hold_max: # Only act once, at the exact count.
1541-
# Long hold - perform action (save)
1542-
name = self.patch_name(index, remove_star=True)
1543-
save_patch_file(self.patch_dir + '/' + name + self.extension, synth=self.synth)
1544-
# Flash non-inverse
1545-
display.message(name, 2, inverse=False)
1546-
time.sleep(0.1)
1547-
display.message(name, 2, inverse=True)
1548-
# Rescan the files (and add a new NEW PATCH if we just materialized one)
1549-
self.patches = self._list_patches()
1550-
# Update the offset so that we're still on NEW PATCH if we just wrote it.
1551-
self.encoder_offset = self.patches.index(name) - self.read_encoder()
1552-
else:
1553-
# Button is released.
1554-
if self.button_state and self.hold_count < self.hold_max:
1555-
# The button was just released *and* it wasn't a long-hold - perform laod.
1556-
load_patch_file(self.patch_dir + '/' + self.patch_name(index) + self.extension, synth=self.synth)
1557-
# Ensure count is reset
1558-
self.hold_count = 0
1559-
self.index = index
1560-
self.button_state = button_state
1561-
1562-
1563-
def patch_selector(synth=1, duration=30, seesaw_dev=0x36, encoder=0, button_pin=24,
1564-
patch_dir=None, extension='.patch'):
1565-
"""Run the PATCH SELECTOR app using Tulip sequencer."""
1566-
import sequencer
1567-
if not web():
1568-
if display is None or not display.available:
1569-
print("You need a rotary encoder and OLED display connected")
1570-
return
1571-
try:
1572-
read_encoder(encoder, seesaw_dev=seesaw_dev)
1573-
except OSError:
1574-
print("You need a rotary encoder and OLED display connected")
1575-
return
1576-
if patch_dir is None:
1577-
patch_dir = tulip.root_dir() + 'user/current'
1578-
seq = None
1579-
1580-
def exit_callback():
1581-
# Terminate the sequencing
1582-
if seq:
1583-
# Stop sequencer
1584-
seq.clear()
1585-
# Clear screen
1586-
display.clear()
1587-
1588-
# Initialize the patch selector
1589-
patchsel_obj = PatchSelector(synth=synth, seesaw_dev=seesaw_dev, encoder=encoder, button_pin=button_pin,
1590-
patch_dir=patch_dir, extension=extension, exit_callback=exit_callback)
1591-
# Arrange for it to be called every 32nd note
1592-
seq = sequencer.TulipSequence(32, lambda x: patchsel_obj.update())
1593-
return seq, patchsel_obj

0 commit comments

Comments
 (0)