From 57529ea2beb34342f5312c666a45c1c7c69378bf Mon Sep 17 00:00:00 2001 From: Zass Date: Mon, 6 Jan 2025 14:28:12 -0800 Subject: [PATCH 01/12] fix for issue where all notes were showing as 4th octave --- src/core.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core.py b/src/core.py index 930d754..f416368 100644 --- a/src/core.py +++ b/src/core.py @@ -2317,9 +2317,10 @@ def analyze(self, chord_notes): r = '' for i, note in enumerate(chord_notes): if note: - notes.append(NOTES[i % 12]) + n = mp.degree_to_note(i) + notes.append(n) if notes: - r = mp.alg.detect(mp.chord(','.join(notes))) + r = mp.alg.detect(mp.chord(notes)) # try: # r = r[0:self.chord.index(' sort')] # except ValueError: From 34e18c589f9e31aef602399f21ddeed8b23b041c Mon Sep 17 00:00:00 2001 From: Zass Date: Mon, 6 Jan 2025 14:32:07 -0800 Subject: [PATCH 02/12] fixed typo in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 904b47e..7cc9e86 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ LinnStrument Community Discord: https://discord.gg/h2BcrzmTXe ## Advantages -- Notes that sound good together are closer together. Notes that sound worse are furthest apart. Mistakes will be less likely and less obvious! +- Notes that sound good together are closer together. Notes that sound worse are furthest apart. Mistakes will be less likely and more obvious! - Like the LinnStrument's layout, it is also isomorphic (the same chord and scale shapes can be played anywhere) - The most common chords and scales are far easier to play and remember than other layouts. - Extended range compared to standard +5 tuning, making room for using a split. From 7639c4f46f885f06ad56f74ce4126546a75c3570 Mon Sep 17 00:00:00 2001 From: Zass Date: Tue, 7 Jan 2025 09:47:04 -0800 Subject: [PATCH 03/12] fixed bug where odd tonics display one octave off --- src/core.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/core.py b/src/core.py index f416368..23aca85 100644 --- a/src/core.py +++ b/src/core.py @@ -478,13 +478,6 @@ def mouse_hover(self, x, y): def get_octave(self, x, y): """Get octave for x, y""" y = self.board_h - y - 1 - # if self.flipped: - # if self.tonic % 2 == 0: - # y -= 1 - # octave = int(x + 4 + self.position.x + y * 2.5) // 6 - # else: - if self.tonic % 2: - y -= 1 octave = int(x + 4 + self.position.x + y * 2.5) // 6 return octave From b8ba0540945b386f3b6c488ff7a39818343ec583 Mon Sep 17 00:00:00 2001 From: Zass Date: Fri, 10 Jan 2025 12:53:24 -0800 Subject: [PATCH 04/12] fixed bug where keys on edge wouldn't light up after move pressed --- src/core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core.py b/src/core.py index 23aca85..80496e6 100644 --- a/src/core.py +++ b/src/core.py @@ -2017,7 +2017,7 @@ def mark(self, midinote, state, use_lights=False, only_row=None): y = 0 for row in rows: x = 0 - for x in range(len(row)): + for x in range(-1 * self.position.x, len(row)-self.position.x): #this band aids bug for 1 move right idx = self.get_note_index(x, y, transpose=False) # print(x, y, midinote%12, idx) if midinote % 12 == idx: @@ -2164,10 +2164,10 @@ def logic(self, dt): self.dirty = self.dirty_lights = True self.clear_marks(use_lights=False) elif ev.ui_element == self.btn_move_left: - self.move_board(-1) + self.move_board(1) self.clear_marks(use_lights=False) elif ev.ui_element == self.btn_move_right: - self.move_board(1) + self.move_board(-1) self.clear_marks(use_lights=False) # elif ev.ui_element == self.btn_mode: # # TODO: toggle mode From c108210c702e9df074a0070180401ec0e54269ea Mon Sep 17 00:00:00 2001 From: Zass Date: Fri, 10 Jan 2025 14:11:44 -0800 Subject: [PATCH 05/12] implemented sharps/flats toggle --- src/constants.py | 5 ++++- src/core.py | 23 +++++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/constants.py b/src/constants.py index a8c4b66..9c50940 100644 --- a/src/constants.py +++ b/src/constants.py @@ -2,7 +2,10 @@ TITLE = "midimech" # FOCUS = False -NOTES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] +#U+1D12C flat +#1D130 sharp +NOTES_SHARPS = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] +NOTES_FLATS = ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"] WHOLETONE = True FONT_SZ = 32 BRIGHTNESS = 0.4 diff --git a/src/core.py b/src/core.py index 80496e6..33c2e7d 100644 --- a/src/core.py +++ b/src/core.py @@ -93,6 +93,15 @@ def next_mode(self, ofs=1): self.set_mode((self.mode_index + ofs) % self.scale_notes.count('x')) self.dirty = self.dirty_lights = True + def toggle_sharps_flats(self, ofs=1): + if (self.use_sharps): + self.use_sharps = False + self.NOTES = NOTES_FLATS + else: + self.use_sharps = True + self.NOTES = NOTES_SHARPS + self.dirty = self.dirty_lights = True + def prev_scale(self, ofs=1): self.next_scale(-ofs) @@ -324,7 +333,7 @@ def get_note_index(self, x, y, transpose=True): y = self.board_h - y - 1 x += self.options.base_offset tr = self.tonic if transpose else 0 - return (row_offset * y + column_offset * x + tr) % len(NOTES) + return (row_offset * y + column_offset * x + tr) % len(self.NOTES) # ofs = (self.board_h - y) // 2 + BASE_OFFSET # step = 2 if WHOLETONE else 1 # tr = self.tonic if transpose else 0 @@ -335,7 +344,7 @@ def get_note_index(self, x, y, transpose=True): def get_note(self, x, y, transpose=True): """Get note name for x, y""" - return NOTES[self.get_note_index(x, y, transpose=transpose)] + return self.NOTES[self.get_note_index(x, y, transpose=transpose)] def get_color(self, x, y): """Get color for x, y""" @@ -1629,6 +1638,11 @@ def __init__(self): text='MOD>', manager=self.gui ) + self.btn_sharps_flats = pygame_gui.elements.UIButton( + relative_rect=pygame.Rect((bs.x * 16 + 2, y), (bs.x, bs.y)), + text='#/b', + manager=self.gui + ) # self.next_scale = pygame_gui.elements.UIButton( # relative_rect=pygame.Rect((bs.x * 11 + 2, y), (bs.x, bs.y)), # text='SCL>', @@ -1695,6 +1709,9 @@ def __init__(self): self.linn_out = None self.midi_out = None self.split_out = None + self.use_sharps = True + self.NOTES = NOTES_SHARPS + outnames = rtmidi2.get_out_ports() for i in range(len(outnames)): @@ -2223,6 +2240,8 @@ def logic(self, dt): self.next_mode() elif ev.ui_element == self.btn_prev_mode: self.prev_mode() + elif ev.ui_element == self.btn_sharps_flats: + self.toggle_sharps_flats() # elif ev.type == pygame_gui.UI_HORIZONTAL_SLIDER_MOVED: # if ev.ui_element == self.slider_velocity: # global self.options.velocity_curve From b4f9e14c8fb3e3036aed2d00a8d7edef85b0a1c2 Mon Sep 17 00:00:00 2001 From: Zass Date: Mon, 13 Jan 2025 15:49:14 -0800 Subject: [PATCH 06/12] updated readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7cc9e86..7081076 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ That being said, I hope you enjoy it and have fun! ### Windows -- [Download (Win)](https://github.com/flipcoder/midimech/releases) +- [Download (Win)](https://github.com/zass30/midimech/releases) After downloading, make sure to follow the instructions under `Setup`. @@ -87,7 +87,7 @@ After downloading, make sure to follow the instructions under `Setup`. - Download the project by typing the following commands in terminal: ``` -git clone https://github.com/flipcoder/midimech +git clone https://github.com/zass30/midimech ``` - Switch to the new project folder: From 879f2bbdb494e694e3c82f1492d74a7935a2e092 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Tue, 28 Jan 2025 19:54:57 +0000 Subject: [PATCH 07/12] Ensure MIDI CC & Pressure messages go to correct midi output (midi_out or split_out) --- src/core.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/core.py b/src/core.py index 33c2e7d..6be41c9 100644 --- a/src/core.py +++ b/src/core.py @@ -1056,11 +1056,8 @@ def cb_midi_in(self, data, timestamp, force_channel=None): # self.midi_write(self.split_out, data, timestamp) # else: # self.midi_write(self.midi_out, data, timestamp) - elif note and note.location is not None: - col = note.location.x - row = note.location.y - split_chan = self.channel_from_split(col, row) - if split_chan: + elif note and note.split is not None: + if note.split: self.midi_write(self.split_out, data, timestamp) else: self.midi_write(self.midi_out, data, timestamp) From f8da9dcbe9c47bcd6b8589b20d0525815190c39c Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Tue, 28 Jan 2025 19:56:00 +0000 Subject: [PATCH 08/12] Add Explicit Quit button Improve UX, to prompt user to explicitly quit and reset state of connected device --- src/core.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/core.py b/src/core.py index 6be41c9..eeaa1fb 100644 --- a/src/core.py +++ b/src/core.py @@ -1638,6 +1638,11 @@ def __init__(self): self.btn_sharps_flats = pygame_gui.elements.UIButton( relative_rect=pygame.Rect((bs.x * 16 + 2, y), (bs.x, bs.y)), text='#/b', + manager=self.gui + ) + self.btn_quit = pygame_gui.elements.UIButton( + relative_rect=pygame.Rect((bs.x * 17 + 2, y), (bs.x, bs.y)), + text='QUIT', manager=self.gui ) # self.next_scale = pygame_gui.elements.UIButton( @@ -2239,6 +2244,8 @@ def logic(self, dt): self.prev_mode() elif ev.ui_element == self.btn_sharps_flats: self.toggle_sharps_flats() + elif ev.ui_element == self.btn_quit: + self.quit() # elif ev.type == pygame_gui.UI_HORIZONTAL_SLIDER_MOVED: # if ev.ui_element == self.slider_velocity: # global self.options.velocity_curve From 71def6b7b1fa7ae08f82c3e151489c68f5036528 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Mon, 31 Mar 2025 00:07:36 -0700 Subject: [PATCH 09/12] Fixed suspend issue, Added swap/rotate lp options, Added configurable RPN delay, Added compose/decompose pitch bend for upcoming microtonal modes --- midimech.py | 6 +- requirements.txt | 1 + scripts/build_windows_package.ps1 | 2 +- src/articulation.py | 3 +- src/core.py | 129 +++++++++++++++++++++++------- src/launchpad.py | 10 ++- src/settings.py | 13 ++- src/util.py | 23 +++++- 8 files changed, 148 insertions(+), 39 deletions(-) diff --git a/midimech.py b/midimech.py index 2db8b2d..c991867 100755 --- a/midimech.py +++ b/midimech.py @@ -26,6 +26,11 @@ # print("The project dependencies have changed! Run the requirements setup command again!") # sys.exit(1) +def error(err): + print(err) + input() + sys.exit(1) + try: import launchpad_py as launchpad except ImportError: @@ -46,7 +51,6 @@ except ImportError: error("The project dependencies have changed! Run the requirements setup command again!") - def main(): core = None try: diff --git a/requirements.txt b/requirements.txt index f364c84..2df696a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ launchpad-py musicpy pyyaml webcolors +easygui diff --git a/scripts/build_windows_package.ps1 b/scripts/build_windows_package.ps1 index 5f8b3f2..fb7ef9f 100644 --- a/scripts/build_windows_package.ps1 +++ b/scripts/build_windows_package.ps1 @@ -1,3 +1,3 @@ cd .. -python -m PyInstaller --onefile midimech.py +python -m PyInstaller --icon=icon.ico --onefile midimech.py cd scripts diff --git a/src/articulation.py b/src/articulation.py index 16b8681..8c21109 100644 --- a/src/articulation.py +++ b/src/articulation.py @@ -1,6 +1,7 @@ -from enum import Enum + from src.util import * from src.constants import * +from enum import Enum class Articulation: State = Enum('state', 'off pre attack hold release') diff --git a/src/core.py b/src/core.py index 930d754..7454c17 100644 --- a/src/core.py +++ b/src/core.py @@ -17,6 +17,16 @@ from src.articulation import Articulation # from src.gamepad import Gamepad +GRADIENT = False + +try: + from easygui import msgbox +except ImportError: + print("The project dependencies have changed! Run the requirements setup command again!") + sys.exit(1) + +msgbox("test") + with open(os.devnull, "w") as devnull: # suppress pygame messages (to keep console output clean) stdout = sys.stdout @@ -26,25 +36,30 @@ sys.stdout = stdout import pygame_gui +def error(err): + msgbox(err) + sys.exit(1) + +def dependency_error(): + error("The project dependencies have changed! Run the requirements setup command again!") + try: import launchpad_py as launchpad except ImportError: try: import launchpad except ImportError: - error("The project dependencies have changed! Run the requirements setup command again!") + dependency_error() try: import yaml except ImportError: - error("The project dependencies have changed! Run the requirements setup command again!") - -# import mido + dependency_error() try: import musicpy as mp except ImportError: - error("The project dependencies have changed! Run the requirements setup command again!") + dependency_error() class Core: CORE = None @@ -150,8 +165,8 @@ def send_all_notes_off(self): return # for ch in range(0,15): ch = 0 - self.midi_write(self.midi_out, [0xb0 | ch, 120, 0], 0) - self.midi_write(self.midi_out, [0xb0 | ch, 123, 0], 0) + self.midi_write(self.midi_out, [0xb0 | ch, 120, 0], 0) # all sounds off + self.midi_write(self.midi_out, [0xb0 | ch, 123, 0], 0) # all notes off def ls_color(self, x, y, col): """Set LinnStrument pad color""" @@ -159,8 +174,21 @@ def ls_color(self, x, y, col): self.send_ls_cc(0, 20, x + 1) self.send_ls_cc(0, 21, self.board_h - y - 1) self.send_ls_cc(0, 22, col) + # time.sleep(self.options.rpn_delay) + + def transform_launchpad_coord(self, idx, x, y): + if self.launchpads[idx].rot: + x, y = y, x + x = (8-x-1) + return x, y - def set_light(self, x, y, col, index=None, mark=False): # col is [1,11], 0 resets + def untransform_launchpad_coord(self, idx, x, y): + if self.launchpads[idx].rot: + x = (8-x-1) + x, y = y, x + return x, y + + def set_light(self, x, y, col, index=None, mark=False, transform=True): # col is [1,11], 0 resets """Set light to color `col` at x, y if in range and connected""" if y < 0 or y >= self.board_h: return @@ -186,19 +214,23 @@ def set_light(self, x, y, col, index=None, mark=False): # col is [1,11], 0 rese lp_col = 0 else: lp_col = ivec3(0) - if 0 <= x < 8 and 0 <= y < 8: - if not self.is_macro_button(x, y): + if transform: + xx, yy = self.transform_launchpad_coord(lp.index, x, y) + else: + xx, yy = x, y + if 0 <= xx < 8 and 0 <= yy < 8: + if not self.is_macro_button(xx, yy): if self.options.launchpad_colors: - lp.out.LedCtrlXYByCode(x, y+1, lp_col) + lp.out.LedCtrlXYByCode(xx, yy+1, lp_col) else: - lp.out.LedCtrlXY(x, y+1, lp_col[0], lp_col[1], None if lp_col[2] == 0 else lp_col[2]) + lp.out.LedCtrlXY(xx, yy+1, lp_col[0], lp_col[1], None if lp_col[2] == 0 else lp_col[2]) else: if self.options.launchpad_colors: - lp.out.LedCtrlXYByCode(x, y+1, 3) + lp.out.LedCtrlXYByCode(xx, yy+1, 3) else: - lp.out.LedCtrlXY(x, y+1, 63, 63, 63) + lp.out.LedCtrlXY(xx, yy+1, 63, 63, 63) - def reset_light(self, x, y, reset_red=True): + def reset_light(self, x, y, reset_red=True, transform=True): """Reset the light at x, y""" note = self.get_note_index(x, y, transpose=False) @@ -223,10 +255,10 @@ def reset_light(self, x, y, reset_red=True): except IndexError: light_col = 7 - self.set_light(x, y, light_col, note) + self.set_light(x, y, light_col, note, transform=transform) self.mark_lights[y][x] = False - def reset_launchpad_light(self, x, y, launchpad=None): + def reset_launchpad_light(self, x, y, launchpad=None, transform=True): """Reset the launchpad light at x, y""" note = self.get_note_index(x, 8-y-1, transpose=False) # if self.is_split(): @@ -238,18 +270,23 @@ def reset_launchpad_light(self, x, y, launchpad=None): # else: # light_col = self.options.lights[note] for lp in ([launchpad] if launchpad else self.launchpads): - self.set_launchpad_light(x, y, note) + self.set_launchpad_light(x, y, note, transform=True) - def set_mark_light(self, x, y, state=True, launchpad=None): + # transform: rotate launchpad if in rotated mode + def set_mark_light(self, x, y, state=True, launchpad=None, transform=True): """Set launchpad light to touched color""" self.mark_lights[y][x] = state for lp in ([launchpad] if launchpad else self.launchpads): lp_col = self.options.mark_color if state: - lp.out.LedCtrlXY(x, y, lp_col[0], lp_col[1], lp_col[2]) + if transform: + xx, yy = self.transform_launchpad_coord(lp.index, x, y) + else: + xx, yy = x, y + lp.out.LedCtrlXY(xx, yy, lp_col[0], lp_col[1], lp_col[2]) # `color` below is an scale index (0, 1, 2...) - def set_launchpad_light(self, x, y, color, launchpad=None): + def set_launchpad_light(self, x, y, color, launchpad=None, transform=True): """Set launchpad light to color index""" if self.is_macro_button(x, 8 - y - 1): if self.options.launchpad_colors: @@ -275,10 +312,14 @@ def set_launchpad_light(self, x, y, color, launchpad=None): col = self.options.mark_color / 4 for lp in ([launchpad] if launchpad else self.launchpads): + if transform: + xx, yy = self.transform_launchpad_coord(lp.index, x, y) + else: + xx, yy = x, y if self.options.launchpad_colors: lp.out.LedCtrlXYByCode(x, 8-y, col) else: - lp.out.LedCtrlXY(x, 8-y, col[0], col[1], col[2]) + lp.out.LedCtrlXY(xx, 8-yy, col[0], col[1], col[2]) def setup_lights(self): """Set all lights""" @@ -549,7 +590,7 @@ def next_free_note(self): def is_mpe(self): return self.options.one_channel == 0 - def note_on(self, data, timestamp, width=None, curve=True, mpe=None, octave=0, transpose=0, force_channel=None): + def note_on(self, data, timestamp, width=None, curve=True, mpe=None, octave=0, transpose=0, force_channel=None, bend=0.0): # if mpe is None: # mpe = self.options.mpe d0 = data[0] @@ -709,6 +750,18 @@ def note_on(self, data, timestamp, width=None, curve=True, mpe=None, octave=0, t self.note_set.add(midinote) self.dirty_chord = True + if GRADIENT: + if x > 6: + bend = (x-6) - y/2 + bend *= (1/6) + try: + # print(bend) + pitch_lsb, pitch_msb = compose_pitch_bend(bend, 1/12) + # print(pitch_msb, pitch_lsb) + self.midi_write(self.midi_out, [0xE0 | ch, pitch_lsb, pitch_msb], timestamp) + except Exception as e: + print(e) + if self.is_split(): if split_chan == 0: # self.midi_out.write([[data, ev[1]]] @@ -1133,6 +1186,7 @@ def cb_launchpad_in(self, lp, event, timestamp=0): note = y * 8 + x note += 12 if not self.is_macro_button(x, 8 - y - 1): + # x, y = self.transform_launchpad_coord(lp.index, x, y) self.note_on([160, note, event[2]], timestamp, width=8, transpose=lp.transpose, octave=lp.get_octave(), force_channel=self.options.launchpad_channel) self.articulation.pressure(vel / 127) else: @@ -1141,8 +1195,10 @@ def cb_launchpad_in(self, lp, event, timestamp=0): x = event[0] y = 8 - event[1] if 0 <= x < 8 and 0 <= y < 8: - self.reset_launchpad_light(x, y, launchpad=lp) + # x2, y2 = self.untransform_launchpad_coord(lp.index, x, y) + # self.reset_launchpad_light(x2, y2, launchpad=lp, transform=False) if not self.is_macro_button(x, 8 - y - 1): + x, y = self.transform_launchpad_coord(lp.index, x, y) note = y * 8 + x self.note_off([128, note, event[2]], timestamp, width=8, transpose=lp.transpose, octave=lp.get_octave(), force_channel=self.options.launchpad_channel) else: @@ -1154,8 +1210,10 @@ def cb_launchpad_in(self, lp, event, timestamp=0): x = event[0] y = 8 - event[1] if 0 <= x < 8 and 0 <= y < 8: - self.set_launchpad_light(x, y, -1, launchpad=lp) + # x2, y2 = self.untransform_launchpad_coord(lp.index, x, y) + # self.set_launchpad_light(x2, y2, -1, launchpad=lp, transform=False) if not self.is_macro_button(x, 8 - y - 1): + x, y = self.transform_launchpad_coord(lp.index, x, y) note = y * 8 + x self.note_on([144, note, event[2]], timestamp, width=8, transpose=lp.transpose, octave=lp.get_octave(), force_channel=self.options.launchpad_channel) else: @@ -1300,6 +1358,9 @@ def __init__(self): self.options.colors = list(self.options.colors.split(",")) self.options.colors = list(map(lambda x: glm.ivec3(get_color(x)), self.options.colors)) + self.options.swap_launchpads = get_option(opts, "swap_launchpads", self.options.swap_launchpads) + self.options.rotate_launchpads = get_option(opts, "rotate_launchpads", self.options.rotate_launchpads) + self.options.launchpad_colors = get_option(opts, "launchpad_colors", DEFAULT_OPTIONS.launchpad_colors) if self.options.launchpad_colors: self.options.launchpad_colors = list(self.options.launchpad_colors.split(",")) @@ -1349,6 +1410,10 @@ def __init__(self): opts, "lite", DEFAULT_OPTIONS.lite ) + self.options.rpn_delay = get_option( + opts, "rpn_delay", DEFAULT_OPTIONS.rpn_delay + ) + # bend the velocity curve, examples: 0.5=sqrt, 1.0=default, 2.0=squared self.options.velocity_curve = get_option( opts, "velocity_curve", DEFAULT_OPTIONS.velocity_curve @@ -1544,6 +1609,7 @@ def __init__(self): pygame.display.set_caption(TITLE) self.icon = pygame.image.load('icon.png') pygame.display.set_icon(self.icon) + pygame.display.set_allow_screensaver(True) # if FOCUS: # pygame.mouse.set_visible(0) # pygame.event.set_grab(True) @@ -1780,8 +1846,11 @@ def __init__(self): self.launchpads += [Launchpad(self, lp, "lpx", num_launchpads, self.options.octave_separation)] num_launchpads += 1 - if self.launchpads: - print('Launchpads:', len(self.launchpads)) + # if self.launchpads: + # launchpad_count = len(self.launchpads) + # print('Launchpads:', launchpad_count) + # if launchpad_count >= 2 and self.options.swap_launchpads: + # self.launchpads = self.launchpads[::-1] self.done = False @@ -1823,6 +1892,8 @@ def __init__(self): self.setup_rpn() # self.test() + # msgbox("Welcome to midimech!\n\nThis project is still in development and some features are experimental. Feel free to play around and report any bugs to flipcoder. Thanks!", "midimech") + def midi_mode_rpn(self, on=True): if on: self.rpn(0, 1 if self.is_mpe() else 0) @@ -1875,7 +1946,7 @@ def rpn(self, num, value): self.midi_write(self.linn_out, [176, 38, value_lsb]) self.midi_write(self.linn_out, [176, 101, 127]) self.midi_write(self.linn_out, [176, 100, 127]) - time.sleep(0.05) + time.sleep(self.options.rpn_delay) def mpe_rpn(self, on=True): """Sets up MPE settings (except MIDI mode)""" @@ -2354,7 +2425,7 @@ def render(self): self.screen.surface.fill((0, 0, 0)) b = 2 # border - sz = self.screen_w / self.board_w + sz = self.scale.x y = 0 rad = int(sz // 2 - 8) diff --git a/src/launchpad.py b/src/launchpad.py index 7b5597c..d772428 100644 --- a/src/launchpad.py +++ b/src/launchpad.py @@ -8,11 +8,15 @@ def __init__(self, core, out, mode, index=0, octave_separation=0): self.mode = mode self.index = index self.octave_separation = octave_separation + self.rot = False + if core.options.rotate_launchpads: + if core.options.swap_launchpads: + self.rot = not bool(index) + else: + self.rot = bool(index) print("Launchpad", mode, 'Connected! (#' + str(index) + ")") - # self.pos = glm.ivec2(0, 0) - def button(self, x, y): # if self.mode == 'lpx': if y == -1: @@ -80,7 +84,7 @@ def button(self, x, y): self.core.prev_program() def set_lights(self): - # if self.mode == "lpx": + self.out.LedCtrlXY(0, 0, 0, 0, 63) self.out.LedCtrlXY(1, 0, 0, 0, 63) self.out.LedCtrlXY(2, 0, 63, 0, 63) diff --git a/src/settings.py b/src/settings.py index c6eef24..fcb0111 100644 --- a/src/settings.py +++ b/src/settings.py @@ -20,6 +20,12 @@ class Settings: # Color scheme using webcolors # split_colors: str = "cyan,blue,blue,blue,blue,blue,blue,blue,blue,blue,blue,blue" launchpad_colors: str = '5,1,9,1,13,122,1,37,45,94,1,95' + + # By default we'll rotate the second launchpad so we can fit them together better. + rotate_launchpads: bool = True + + # Use this if the wrong launchpad is rotated (left vs right) + swap_launchpads: bool = False colors: str = "red,darkred,orange,goldenrod,yellow,green,darkolivegreen,blue,darkslateblue,indigo,darkorchid,pink" # colors: str = "cyan,green,green,green,green,green,green,green,green,green,green,green" @@ -34,7 +40,7 @@ class Settings: # lite mode (no extra gfx, less processing) lite: bool = False - # Custom velocity curve exponent, ex: 0.5 = more sensitive + # Bend the velocity curve, examples: 0.5=sqrt, 1.0=default, 2.0=squared velocity_curve: float = 1.0 # Velocity curve exponent for foot controller curve bending @@ -84,7 +90,7 @@ class Settings: height: int = 8 # launchpad viberato method (off, mod, or pitch) - vibrato: str = 'mod' + vibrato: str = 'off' # Set scale based on left hand chord (not yet impl) jazz: bool = False @@ -106,5 +112,8 @@ class Settings: # octave splitting the linn and transposing octaves on the right side octave_split: int = 0 + # Delay between RPN message batches + rpn_delay: float = 0.1 + DEFAULT_OPTIONS = Settings() diff --git a/src/util.py b/src/util.py index 458ee54..869bf85 100644 --- a/src/util.py +++ b/src/util.py @@ -109,8 +109,27 @@ def decompose_pitch_bend(pitch_bend_bytes): def compose_pitch_bend(pitch_bend_norm): pitch_bend_value = int((pitch_bend_norm + 1.0) * 8192) - pitch_bend_bytes = [pitch_bend_value & 0x7F, (pitch_bend_value >> 7) & 0x7F] - return pitch_bend_bytes + return pitch_bend_value & 0x7F, (pitch_bend_value >> 7) & 0x7F + +def decompose_pitch_bend(pitch_bend_bytes): + pitch_bend_value = (pitch_bend_bytes[0] << 7) + pitch_bend_bytes[1] + pitch_bend_norm = (pitch_bend_value - 8192) / 8191.5 # Adjust for exact [0, 16383] range + return pitch_bend_norm + +def compose_pitch_bend(norm, scale): + # scale is the range in semitones that norm=1.0 represents + # Map [-1, 1] to [0, 16383], scaling norm by the desired range + # Assuming a default synth range of ±2 semitones (16383 = 2 semitones) + scale_factor = scale / 2.0 # Adjust relative to default ±2 semitones + scaled_norm = norm * scale_factor + # Clamp to [-1, 1] to stay within synth's max range + scaled_norm = max(-1.0, min(1.0, scaled_norm)) + # Convert to MIDI 14-bit value: [-1, 1] -> [0, 16383] + pitch_bend_value = int(((scaled_norm + 1.0) / 2.0) * 16383 + 0.5) + pitch_bend_value = max(0, min(16383, pitch_bend_value)) + lsb = pitch_bend_value & 0x7F + msb = (pitch_bend_value >> 7) & 0x7F + return lsb, msb def decode_value(value): lsb = value & 0x7F From 44ccfde02809c05d10865a42a3f6dbde10b944dd Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Mon, 31 Mar 2025 00:09:30 -0700 Subject: [PATCH 10/12] removed msgbox test --- src/core.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core.py b/src/core.py index 7454c17..6e33eb1 100644 --- a/src/core.py +++ b/src/core.py @@ -25,8 +25,6 @@ print("The project dependencies have changed! Run the requirements setup command again!") sys.exit(1) -msgbox("test") - with open(os.devnull, "w") as devnull: # suppress pygame messages (to keep console output clean) stdout = sys.stdout From c9ca4e995c0e0470670a69b0a151b79c2c63c401 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Mon, 31 Mar 2025 00:28:28 -0700 Subject: [PATCH 11/12] Reset links and added contributors (2) --- CONTRIBUTORS | 2 ++ README.md | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index e5501e3..02701c2 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -1,2 +1,4 @@ Grady O'Connell Andy Klise +zass30 +Richard Smith diff --git a/README.md b/README.md index 7081076..7cc9e86 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ That being said, I hope you enjoy it and have fun! ### Windows -- [Download (Win)](https://github.com/zass30/midimech/releases) +- [Download (Win)](https://github.com/flipcoder/midimech/releases) After downloading, make sure to follow the instructions under `Setup`. @@ -87,7 +87,7 @@ After downloading, make sure to follow the instructions under `Setup`. - Download the project by typing the following commands in terminal: ``` -git clone https://github.com/zass30/midimech +git clone https://github.com/flipcoder/midimech ``` - Switch to the new project folder: From 3bd577be391ef4aa626bbe67b87417d56986c156 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Mon, 14 Apr 2025 20:46:52 -0700 Subject: [PATCH 12/12] version bump and md changes --- CHANGELOG.md | 9 +++++++++ README.md | 15 +++++---------- VERSION | 2 +- 3 files changed, 15 insertions(+), 11 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..765222b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## 0.5.0 + +_First versioned release._ + +## 0.6.0-pre + +_Currently in development._ \ No newline at end of file diff --git a/README.md b/README.md index 7cc9e86..fc78b27 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ LinnStrument Community Discord: https://discord.gg/h2BcrzmTXe ## Advantages -- Notes that sound good together are closer together. Notes that sound worse are furthest apart. Mistakes will be less likely and more obvious! +- Notes that sound good together are closer together. Notes that sound worse are furthest apart. Mistakes will be less likely and won't sound as dissonant. - Like the LinnStrument's layout, it is also isomorphic (the same chord and scale shapes can be played anywhere) -- The most common chords and scales are far easier to play and remember than other layouts. +- The most common chords and scales are far easier to play and remember than those of other layouts. - Extended range compared to standard +5 tuning, making room for using a split. - Less finger stretching than other layouts when playing chords, which may help ergonomically. - Arpeggios are quite smooth, as you're simply walking stacked shapes. @@ -164,18 +164,13 @@ launchpad=false ### Vibrato -Midimech adds a cool feature to the Launchpad where it can detect wiggling a note to create a vibrato effect. This is enabled by default and mapped to CC0. If your synth supports CC0 vibrato, you should hear the vibrato activate by rocking your finger back and forth from left to right while pressing the note down. +Midimech adds a cool feature to the Launchpad where it can detect wiggling a note to create a vibrato effect. This is disabled by default and mapped to CC0. If your synth supports CC0 vibrato, you should hear the vibrato activate by rocking your finger back and forth from left to right while pressing the note down. -You can disable it in settings.ini using: +You can enable it in settings.ini using: ``` -vibrato=off +vibrato=mod ``` -There is also experimental support for pitch wheel vibrato using: - -``` -vibrato=pitch -``` ## Color Schemes diff --git a/VERSION b/VERSION index 8f0916f..577e58b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.0 +0.6.0-pre \ No newline at end of file