Created
October 10, 2024 10:54
-
-
Save amb/d243633de14b99f0e409008223f71e1a to your computer and use it in GitHub Desktop.
Launchpad X midi Game of Life
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import time | |
import mido | |
import random | |
import numpy as np | |
# List all available MIDI ports | |
print("Available MIDI ports:") | |
for port in mido.get_input_names(): | |
print(port) | |
for port in mido.get_output_names(): | |
print(port) | |
# Create a MIDI output port | |
outport = mido.open_output("Launchpad X LPX MIDI In") | |
inport = mido.open_input("Launchpad X LPX MIDI Out") | |
# Open virtual MIDI ports | |
# This is Mac OS specific, on Windows you can use the loopMIDI software | |
virtual_out = mido.open_output("IAC Driver Bus 1") | |
previous_color_grid = np.ones((8, 8, 3), dtype=np.uint8) | |
def set_colors(grid, full_refresh=False): | |
difference = grid != previous_color_grid | |
if not np.any(difference) and not full_refresh: | |
return | |
previous_color_grid[:] = grid | |
sysex_data = [0x00, 0x20, 0x29, 0x02, 0x0C, 0x03] | |
for row in range(1, 9): | |
for col in range(1, 9): | |
if not full_refresh and not np.any(difference[row - 1, col - 1]): | |
continue | |
led_index = row * 10 + col | |
C = grid[row - 1, col - 1] | |
sysex_data.extend([0x03, led_index, *C]) | |
outport.send(mido.Message("sysex", data=sysex_data)) | |
def value_to_color(value): | |
c = (float(value) / 127.0) ** 1.5 * 127 | |
return np.array([c, c, c], dtype=np.uint8) | |
def set_colors_mono(grid): | |
stacked_pixels = np.stack([grid, grid, grid], axis=-1) * 127 | |
set_colors(stacked_pixels) | |
return stacked_pixels | |
pixels = np.random.randint(0, 2, (8, 8), dtype=np.uint8) | |
stacked_pixels = np.stack([pixels, pixels, pixels], axis=-1) * 127 | |
color_grid = stacked_pixels[:] | |
set_colors(stacked_pixels, full_refresh=True) | |
try: | |
while True: | |
# Run Game of Life on pixels | |
new_pixels = np.zeros_like(pixels) | |
pixels = np.where(color_grid[:, :, 0] > 0, 1, 0) | |
for i in range(8): | |
for j in range(8): | |
neighbors = pixels[i - 1 : i + 2, j - 1 : j + 2].sum() - pixels[i, j] | |
if pixels[i, j] == 1 and neighbors in [2, 3]: | |
new_pixels[i, j] = 1 | |
elif pixels[i, j] == 0 and neighbors == 3: | |
new_pixels[i, j] = 1 | |
pixels = new_pixels | |
color_grid = set_colors_mono(pixels) | |
# Output pixel grid as MIDI notes | |
for i in range(8): | |
for j in range(8): | |
note = i * 10 + j + 1 | |
velocity = 127 if pixels[i, j] else 0 | |
virtual_out.send(mido.Message("note_on", note=note, velocity=velocity)) | |
current_time = time.time() | |
while time.time() - current_time < 0.1: | |
midi_message = inport.poll() | |
if midi_message: | |
print(midi_message) | |
if not hasattr(midi_message, "note"): | |
continue | |
row = midi_message.note // 10 - 1 | |
col = midi_message.note % 10 - 1 | |
# Polyphonic aftertouch | |
if midi_message.type == "polytouch": | |
color_grid[row, col] = value_to_color(midi_message.value) | |
set_colors(color_grid) | |
# Note on | |
if midi_message.type == "note_on" and midi_message.velocity > 0: | |
color_grid[row, col] = value_to_color(midi_message.velocity) | |
set_colors(color_grid) | |
# Note off | |
if midi_message.type == "note_on" and midi_message.velocity == 0: | |
color_grid[row, col] = np.array([0, 0, 0], dtype=np.uint8) | |
set_colors(color_grid) | |
except KeyboardInterrupt: | |
pass | |
# Close the MIDI port | |
print("Closing MIDI ports") | |
outport.close() | |
inport.close() | |
virtual_out.close() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment