midi-cli 0.2.0__tar.gz → 0.3.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.
- {midi_cli-0.2.0 → midi_cli-0.3.0}/ARCHITECTURE.md +16 -11
- {midi_cli-0.2.0 → midi_cli-0.3.0}/PKG-INFO +1 -1
- {midi_cli-0.2.0 → midi_cli-0.3.0}/midi_cli/app.py +112 -64
- {midi_cli-0.2.0 → midi_cli-0.3.0}/midi_cli/midi.py +3 -7
- {midi_cli-0.2.0 → midi_cli-0.3.0}/midi_cli/state.py +4 -2
- {midi_cli-0.2.0 → midi_cli-0.3.0}/midi_cli/ui.py +29 -25
- {midi_cli-0.2.0 → midi_cli-0.3.0}/pyproject.toml +1 -1
- {midi_cli-0.2.0 → midi_cli-0.3.0}/uv.lock +1 -1
- {midi_cli-0.2.0 → midi_cli-0.3.0}/.gitignore +0 -0
- {midi_cli-0.2.0 → midi_cli-0.3.0}/CLAUDE.md +0 -0
- {midi_cli-0.2.0 → midi_cli-0.3.0}/LICENSE +0 -0
- {midi_cli-0.2.0 → midi_cli-0.3.0}/README.md +0 -0
- {midi_cli-0.2.0 → midi_cli-0.3.0}/midi_cli/__init__.py +0 -0
- {midi_cli-0.2.0 → midi_cli-0.3.0}/midi_cli/audio.py +0 -0
- {midi_cli-0.2.0 → midi_cli-0.3.0}/midi_cli/config.py +0 -0
- {midi_cli-0.2.0 → midi_cli-0.3.0}/midi_cli/loop.py +0 -0
- {midi_cli-0.2.0 → midi_cli-0.3.0}/midi_cli/sampler.py +0 -0
|
@@ -30,12 +30,14 @@ Developer reference for midi-cli: modes, key behavior, and app flow.
|
|
|
30
30
|
Modes are stored in `state.mode`. The ordered list is defined in `state.MODES`:
|
|
31
31
|
|
|
32
32
|
```
|
|
33
|
-
synth → sample →
|
|
33
|
+
synth → sample → pitch → chromatic → trim → copy → loop → split
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
- **Tab** advances to the next mode (wraps)
|
|
37
37
|
- **Shift+Tab** goes to the previous mode
|
|
38
|
-
- Switching away from `
|
|
38
|
+
- Switching away from `trim`, `copy`, `split`, or `loop` resets that mode's transient state
|
|
39
|
+
- **`r`** is a global key toggle (arm/disarm recording) — not a mode
|
|
40
|
+
- **`c`** opens the config overlay — not a mode
|
|
39
41
|
|
|
40
42
|
### synth
|
|
41
43
|
Polyphonic additive synth (5 harmonics, piano-like two-stage decay).
|
|
@@ -46,10 +48,12 @@ Sustain pedal (CC 64) holds notes until pedal release.
|
|
|
46
48
|
Plays back recorded samples. Note on → sample voice added to `state.sample_voices`, plays from position 0.
|
|
47
49
|
Note off → voice set to `releasing` (5ms fade out). Sample plays to its natural end before being removed.
|
|
48
50
|
|
|
49
|
-
###
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
In keyboard mock mode: key press
|
|
51
|
+
### Record armed (`r` key)
|
|
52
|
+
`record_armed` is a boolean flag in state, not a mode. Press **r** to arm (yellow ARM indicator appears); press **r** again to disarm.
|
|
53
|
+
While armed: note on → `start_recording()`, note off → `stop_recording()`.
|
|
54
|
+
In keyboard mock mode: key press toggles recording on/off per-note via `keyboard_recording_notes`.
|
|
55
|
+
Tabbing away while armed automatically disarms and stops any active recording.
|
|
56
|
+
In chromatic mode, **r** means "reselect root" — it does not arm recording.
|
|
53
57
|
|
|
54
58
|
### pitch
|
|
55
59
|
Note on → plays the sample and sets `pitch_selected_note`.
|
|
@@ -76,7 +80,7 @@ Two-step note selection:
|
|
|
76
80
|
|
|
77
81
|
### loop
|
|
78
82
|
Loop state machine: `idle → recording → playing → overdub → playing → ...`
|
|
79
|
-
Triggered by **l** key
|
|
83
|
+
Triggered by **l** key.
|
|
80
84
|
Sample voices (from note-on) are mixed into the loop buffer during recording and overdub.
|
|
81
85
|
**Escape** resets to idle.
|
|
82
86
|
|
|
@@ -92,8 +96,10 @@ Two phases:
|
|
|
92
96
|
- **markers**: select a source note, use arrow keys to position cursor, **Enter** to place a marker. **a** advances to assign phase.
|
|
93
97
|
- **assign**: each note-on saves the current slice to that key and advances to the next. **Escape** steps back one slice (or back to markers phase).
|
|
94
98
|
|
|
95
|
-
###
|
|
96
|
-
|
|
99
|
+
### Config overlay (`c` key)
|
|
100
|
+
Press **c** from any mode to open the config overlay (rendered in the waveform area). All keys except **q** are swallowed while the overlay is open.
|
|
101
|
+
Up/Down navigates fields. Left/Right changes values. Fields: input device, output device, MIDI port, synesthesia toggle. Changes saved immediately on each arrow press.
|
|
102
|
+
Press **c** or **Escape** to close the overlay. Focus always resets to field 0 on open.
|
|
97
103
|
|
|
98
104
|
## Audio Callback
|
|
99
105
|
|
|
@@ -118,11 +124,10 @@ Metronome is mixed in at the end of blocks 3–5 when `metronome_enabled` and th
|
|
|
118
124
|
|
|
119
125
|
### Sustain pedal (CC 64)
|
|
120
126
|
- In **synth**: holds notes; releasing pedal fades all held notes
|
|
121
|
-
- In **loop**: cycles loop state (same as **l** key)
|
|
122
127
|
- Other modes: ignored
|
|
123
128
|
|
|
124
129
|
### Metronome
|
|
125
|
-
- **m** key toggles on/off in modes: `synth`, `sample`, `
|
|
130
|
+
- **m** key toggles on/off in modes: `synth`, `sample`, `loop`, `chromatic`
|
|
126
131
|
- Arrow keys adjust BPM (±1 / ±5) when metronome is on and no mode-specific arrow binding takes priority
|
|
127
132
|
- BPM range: 20–300
|
|
128
133
|
|
|
@@ -14,6 +14,26 @@ from midi_cli.midi import open_midi_input, trigger_keyboard_note, midi_callback
|
|
|
14
14
|
from midi_cli.ui import draw_ui, reset_trim_state, reset_copy_state
|
|
15
15
|
from midi_cli.loop import reset_loop_state, cycle_loop_state
|
|
16
16
|
|
|
17
|
+
def _read_escape_seq(fd):
|
|
18
|
+
seq = [os.read(fd, 1).decode("utf-8", errors="replace")]
|
|
19
|
+
if seq[0] == 'O':
|
|
20
|
+
r, _, _ = select.select([fd], [], [], 0.02)
|
|
21
|
+
if r:
|
|
22
|
+
seq.append(os.read(fd, 1).decode("utf-8", errors="replace"))
|
|
23
|
+
elif seq[0] == '[':
|
|
24
|
+
while True:
|
|
25
|
+
r, _, _ = select.select([fd], [], [], 0.02)
|
|
26
|
+
if not r:
|
|
27
|
+
break
|
|
28
|
+
b = os.read(fd, 1).decode("utf-8", errors="replace")
|
|
29
|
+
seq.append(b)
|
|
30
|
+
if b.isalpha() or b == '~':
|
|
31
|
+
break
|
|
32
|
+
if len(seq) > 16:
|
|
33
|
+
break
|
|
34
|
+
return "".join(seq)
|
|
35
|
+
|
|
36
|
+
|
|
17
37
|
def _stop_active_voices():
|
|
18
38
|
"""Clear all active voices on mode switch."""
|
|
19
39
|
with st.voices_lock:
|
|
@@ -66,6 +86,78 @@ def main():
|
|
|
66
86
|
ch = os.read(fd, 1).decode("utf-8", errors="replace")
|
|
67
87
|
if ch in ("q", "\x03"): # q or Ctrl+C
|
|
68
88
|
break
|
|
89
|
+
elif st.config_overlay_open:
|
|
90
|
+
if ch == "c":
|
|
91
|
+
st.config_overlay_open = False
|
|
92
|
+
elif ch == "\x1b":
|
|
93
|
+
esc_ready, _, _ = select.select([fd], [], [], 0.02)
|
|
94
|
+
if not esc_ready:
|
|
95
|
+
st.config_overlay_open = False
|
|
96
|
+
else:
|
|
97
|
+
seq = _read_escape_seq(fd)
|
|
98
|
+
if seq == "[Z": # Shift+Tab: ignore while overlay open
|
|
99
|
+
pass
|
|
100
|
+
elif seq == "[A": # Up: previous field
|
|
101
|
+
st.config_focused_field = (st.config_focused_field - 1) % 4
|
|
102
|
+
elif seq == "[B": # Down: next field
|
|
103
|
+
st.config_focused_field = (st.config_focused_field + 1) % 4
|
|
104
|
+
elif seq in ("[C", "[D"): # Right/Left: change value
|
|
105
|
+
direction = 1 if seq == "[C" else -1
|
|
106
|
+
field = st.config_focused_field
|
|
107
|
+
if field == 3: # Synesthesia toggle
|
|
108
|
+
st.synesthesia_enabled = not st.synesthesia_enabled
|
|
109
|
+
elif field == 0: # Input device
|
|
110
|
+
devices = get_input_devices()
|
|
111
|
+
if devices:
|
|
112
|
+
cur = st.input_device
|
|
113
|
+
idx = devices.index(cur) if cur in devices else 0
|
|
114
|
+
st.input_device = devices[(idx + direction) % len(devices)]
|
|
115
|
+
elif field == 1: # Output device
|
|
116
|
+
devices = get_output_devices()
|
|
117
|
+
if devices:
|
|
118
|
+
cur = st.output_device
|
|
119
|
+
idx = devices.index(cur) if cur in devices else 0
|
|
120
|
+
new_device = devices[(idx + direction) % len(devices)]
|
|
121
|
+
stream.stop()
|
|
122
|
+
stream.close()
|
|
123
|
+
st.output_device = new_device
|
|
124
|
+
stream = sd.OutputStream(
|
|
125
|
+
samplerate=st.SAMPLE_RATE,
|
|
126
|
+
blocksize=st.BLOCK_SIZE,
|
|
127
|
+
channels=1,
|
|
128
|
+
callback=audio_callback,
|
|
129
|
+
device=st.output_device,
|
|
130
|
+
)
|
|
131
|
+
stream.start()
|
|
132
|
+
elif field == 2: # MIDI port
|
|
133
|
+
ports = get_midi_ports()
|
|
134
|
+
options = [None] + ports
|
|
135
|
+
cur = st.midi_port
|
|
136
|
+
idx = options.index(cur) if cur in options else 0
|
|
137
|
+
new_port = options[(idx + direction) % len(options)]
|
|
138
|
+
if midi_in is not None:
|
|
139
|
+
midi_in.close_port()
|
|
140
|
+
midi_in.delete()
|
|
141
|
+
midi_in = None
|
|
142
|
+
st.midi_port = new_port
|
|
143
|
+
if new_port is None:
|
|
144
|
+
st.keyboard_mock_mode = True
|
|
145
|
+
st.ui_midi_port = ""
|
|
146
|
+
else:
|
|
147
|
+
midi_in, port_name = open_midi_input(new_port)
|
|
148
|
+
if midi_in is None:
|
|
149
|
+
st.keyboard_mock_mode = True
|
|
150
|
+
st.ui_midi_port = ""
|
|
151
|
+
else:
|
|
152
|
+
st.keyboard_mock_mode = False
|
|
153
|
+
st.ui_midi_port = port_name
|
|
154
|
+
save_config({
|
|
155
|
+
"input_device": st.input_device,
|
|
156
|
+
"output_device": st.output_device,
|
|
157
|
+
"midi_port": st.midi_port,
|
|
158
|
+
"synesthesia_enabled": st.synesthesia_enabled,
|
|
159
|
+
})
|
|
160
|
+
# all other keys are silently swallowed by the overlay
|
|
69
161
|
elif ch == "\r" and st.mode == "split" and st.split_selected_note is not None and st.split_phase == "markers":
|
|
70
162
|
with st.voices_lock:
|
|
71
163
|
st.split_markers.append(st.split_cursor_pos)
|
|
@@ -90,7 +182,8 @@ def main():
|
|
|
90
182
|
elif ch == "\r" and st.mode == "copy" and st.copy_confirm_pending:
|
|
91
183
|
execute_copy()
|
|
92
184
|
elif ch == "\t": # Tab: next mode
|
|
93
|
-
if st.
|
|
185
|
+
if st.record_armed:
|
|
186
|
+
st.record_armed = False
|
|
94
187
|
st.keyboard_recording_notes.clear()
|
|
95
188
|
if st.recording_note is not None:
|
|
96
189
|
threading.Thread(target=stop_recording, args=(st.recording_note,), daemon=True).start()
|
|
@@ -109,6 +202,17 @@ def main():
|
|
|
109
202
|
st.chromatic_selecting = st.chromatic_root_note is None
|
|
110
203
|
elif ch == "r" and st.mode == "chromatic" and not st.chromatic_selecting:
|
|
111
204
|
st.chromatic_selecting = True
|
|
205
|
+
elif ch == "r" and st.mode == "sample":
|
|
206
|
+
if st.record_armed:
|
|
207
|
+
st.record_armed = False
|
|
208
|
+
st.keyboard_recording_notes.clear()
|
|
209
|
+
if st.recording_note is not None:
|
|
210
|
+
threading.Thread(target=stop_recording, args=(st.recording_note,), daemon=True).start()
|
|
211
|
+
else:
|
|
212
|
+
st.record_armed = True
|
|
213
|
+
elif ch == "c":
|
|
214
|
+
st.config_overlay_open = True
|
|
215
|
+
st.config_focused_field = 0
|
|
112
216
|
elif ch == "\x1b": # escape sequence
|
|
113
217
|
# Use select on fd with timeout to distinguish bare Escape from sequences
|
|
114
218
|
esc_ready, _, _ = select.select([fd], [], [], 0.02)
|
|
@@ -144,9 +248,14 @@ def main():
|
|
|
144
248
|
else:
|
|
145
249
|
st.split_selected_note = None
|
|
146
250
|
else:
|
|
147
|
-
|
|
251
|
+
# Read a complete VT escape sequence. F5-F10 send CSI
|
|
252
|
+
# sequences like \x1b[15~ (5 bytes); reading only 2
|
|
253
|
+
# bytes would leak digits into the next iteration and
|
|
254
|
+
# trigger unintended notes via KEYBOARD_NOTE_MAP.
|
|
255
|
+
seq = _read_escape_seq(fd)
|
|
148
256
|
if seq == "[Z": # Shift+Tab: previous mode
|
|
149
|
-
if st.
|
|
257
|
+
if st.record_armed:
|
|
258
|
+
st.record_armed = False
|
|
150
259
|
st.keyboard_recording_notes.clear()
|
|
151
260
|
if st.recording_note is not None:
|
|
152
261
|
threading.Thread(target=stop_recording, args=(st.recording_note,), daemon=True).start()
|
|
@@ -208,67 +317,6 @@ def main():
|
|
|
208
317
|
st.split_cursor_pos = min(sample_len - 1, st.split_cursor_pos + coarse)
|
|
209
318
|
elif seq == "[B": # Down: move cursor back coarse
|
|
210
319
|
st.split_cursor_pos = max(0, st.split_cursor_pos - coarse)
|
|
211
|
-
elif st.mode == "config":
|
|
212
|
-
if seq == "[A": # Up: previous field
|
|
213
|
-
st.config_focused_field = (st.config_focused_field - 1) % 4
|
|
214
|
-
elif seq == "[B": # Down: next field
|
|
215
|
-
st.config_focused_field = (st.config_focused_field + 1) % 4
|
|
216
|
-
elif seq in ("[C", "[D"): # Right/Left: change value
|
|
217
|
-
direction = 1 if seq == "[C" else -1
|
|
218
|
-
field = st.config_focused_field
|
|
219
|
-
if field == 3: # Synesthesia toggle
|
|
220
|
-
st.synesthesia_enabled = not st.synesthesia_enabled
|
|
221
|
-
elif field == 0: # Input device
|
|
222
|
-
devices = get_input_devices()
|
|
223
|
-
if devices:
|
|
224
|
-
cur = st.input_device
|
|
225
|
-
idx = devices.index(cur) if cur in devices else 0
|
|
226
|
-
st.input_device = devices[(idx + direction) % len(devices)]
|
|
227
|
-
elif field == 1: # Output device
|
|
228
|
-
devices = get_output_devices()
|
|
229
|
-
if devices:
|
|
230
|
-
cur = st.output_device
|
|
231
|
-
idx = devices.index(cur) if cur in devices else 0
|
|
232
|
-
new_device = devices[(idx + direction) % len(devices)]
|
|
233
|
-
stream.stop()
|
|
234
|
-
stream.close()
|
|
235
|
-
st.output_device = new_device
|
|
236
|
-
stream = sd.OutputStream(
|
|
237
|
-
samplerate=st.SAMPLE_RATE,
|
|
238
|
-
blocksize=st.BLOCK_SIZE,
|
|
239
|
-
channels=1,
|
|
240
|
-
callback=audio_callback,
|
|
241
|
-
device=st.output_device,
|
|
242
|
-
)
|
|
243
|
-
stream.start()
|
|
244
|
-
elif field == 2: # MIDI port
|
|
245
|
-
ports = get_midi_ports()
|
|
246
|
-
options = [None] + ports
|
|
247
|
-
cur = st.midi_port
|
|
248
|
-
idx = options.index(cur) if cur in options else 0
|
|
249
|
-
new_port = options[(idx + direction) % len(options)]
|
|
250
|
-
if midi_in is not None:
|
|
251
|
-
midi_in.close_port()
|
|
252
|
-
midi_in.delete()
|
|
253
|
-
midi_in = None
|
|
254
|
-
st.midi_port = new_port
|
|
255
|
-
if new_port is None:
|
|
256
|
-
st.keyboard_mock_mode = True
|
|
257
|
-
st.ui_midi_port = ""
|
|
258
|
-
else:
|
|
259
|
-
midi_in, port_name = open_midi_input(new_port)
|
|
260
|
-
if midi_in is None:
|
|
261
|
-
st.keyboard_mock_mode = True
|
|
262
|
-
st.ui_midi_port = ""
|
|
263
|
-
else:
|
|
264
|
-
st.keyboard_mock_mode = False
|
|
265
|
-
st.ui_midi_port = port_name
|
|
266
|
-
save_config({
|
|
267
|
-
"input_device": st.input_device,
|
|
268
|
-
"output_device": st.output_device,
|
|
269
|
-
"midi_port": st.midi_port,
|
|
270
|
-
"synesthesia_enabled": st.synesthesia_enabled,
|
|
271
|
-
})
|
|
272
320
|
elif st.metronome_enabled and st.mode in st.METRONOME_MODES:
|
|
273
321
|
if seq == "[C": # Right: +1 BPM
|
|
274
322
|
st.bpm = min(st.BPM_MAX, st.bpm + 1)
|
|
@@ -19,7 +19,7 @@ def midi_callback(event, data):
|
|
|
19
19
|
st.seen_notes.add(note)
|
|
20
20
|
|
|
21
21
|
if msg_type == 0x90 and velocity > 0:
|
|
22
|
-
if st.
|
|
22
|
+
if st.record_armed:
|
|
23
23
|
threading.Thread(target=start_recording, args=(note,), daemon=True).start()
|
|
24
24
|
return
|
|
25
25
|
|
|
@@ -99,7 +99,7 @@ def midi_callback(event, data):
|
|
|
99
99
|
st.voices[note] = {"phase": 0.0, "gain": 0.0, "releasing": False, "age": 0, "faded_in": False}
|
|
100
100
|
st.sustained_notes.discard(note)
|
|
101
101
|
elif msg_type == 0x80 or (msg_type == 0x90 and velocity == 0):
|
|
102
|
-
if st.
|
|
102
|
+
if st.record_armed:
|
|
103
103
|
threading.Thread(target=stop_recording, args=(note,), daemon=True).start()
|
|
104
104
|
return
|
|
105
105
|
if st.mode in ("trim", "copy", "split"):
|
|
@@ -121,10 +121,6 @@ def midi_callback(event, data):
|
|
|
121
121
|
elif msg_type == 0xB0 and note == 64:
|
|
122
122
|
pedal_on = velocity >= 64
|
|
123
123
|
st.ui_sustain = pedal_on
|
|
124
|
-
if pedal_on and st.mode == "loop":
|
|
125
|
-
with st.voices_lock:
|
|
126
|
-
cycle_loop_state()
|
|
127
|
-
return
|
|
128
124
|
with st.voices_lock:
|
|
129
125
|
st.sustain = pedal_on
|
|
130
126
|
if not pedal_on:
|
|
@@ -135,7 +131,7 @@ def midi_callback(event, data):
|
|
|
135
131
|
|
|
136
132
|
|
|
137
133
|
def trigger_keyboard_note(note):
|
|
138
|
-
if st.
|
|
134
|
+
if st.record_armed:
|
|
139
135
|
if note in st.keyboard_recording_notes:
|
|
140
136
|
st.keyboard_recording_notes.discard(note)
|
|
141
137
|
threading.Thread(target=stop_recording, args=(note,), daemon=True).start()
|
|
@@ -76,7 +76,7 @@ sustain = False
|
|
|
76
76
|
sustained_notes = set()
|
|
77
77
|
|
|
78
78
|
# Sampler state
|
|
79
|
-
MODES = ["synth", "sample", "
|
|
79
|
+
MODES = ["synth", "sample", "pitch", "chromatic", "trim", "copy", "loop", "split"]
|
|
80
80
|
mode = "synth"
|
|
81
81
|
samples = {} # note_number -> numpy array (mono float64)
|
|
82
82
|
sample_voices = {} # note -> {"pos": int, "releasing": bool, "gain": float}
|
|
@@ -86,6 +86,7 @@ recording_note = None # int: MIDI note currently recording, or None
|
|
|
86
86
|
recording_chunks = [] # list of np.ndarray frames from InputStream callback
|
|
87
87
|
recording_stream = None # sd.InputStream instance, or None
|
|
88
88
|
keyboard_recording_notes = set() # notes with in-progress keyboard-toggle recordings
|
|
89
|
+
record_armed = False # True when 'r' has been pressed; next note-on triggers recording
|
|
89
90
|
|
|
90
91
|
# Pitch mode state
|
|
91
92
|
pitch_offsets = {} # note_number -> {"semitones": int, "cents": int}
|
|
@@ -141,7 +142,7 @@ loop_pos = 0 # current playback position (int)
|
|
|
141
142
|
loop_record_chunks = [] # list of np.ndarray, accumulated during recording
|
|
142
143
|
loop_layer_count = 0 # 1 after first recording, increments each overdub completion
|
|
143
144
|
|
|
144
|
-
METRONOME_MODES = {"synth", "sample", "
|
|
145
|
+
METRONOME_MODES = {"synth", "sample", "loop", "chromatic"}
|
|
145
146
|
|
|
146
147
|
# Metronome state
|
|
147
148
|
metronome_enabled = False
|
|
@@ -157,6 +158,7 @@ copy_confirm_pending = False # True when awaiting overwrite confirmation
|
|
|
157
158
|
|
|
158
159
|
# Config mode state
|
|
159
160
|
config_focused_field = 0 # 0=input, 1=output, 2=midi, 3=synesthesia
|
|
161
|
+
config_overlay_open = False # True when config overlay is visible
|
|
160
162
|
|
|
161
163
|
# UI settings
|
|
162
164
|
synesthesia_enabled = False
|
|
@@ -296,7 +296,12 @@ def draw_ui():
|
|
|
296
296
|
return s
|
|
297
297
|
|
|
298
298
|
# Waveform area — always exactly waveform_block lines so the console never moves
|
|
299
|
-
if st.
|
|
299
|
+
if st.config_overlay_open:
|
|
300
|
+
config_lines = render_config_panel()
|
|
301
|
+
for cl in config_lines:
|
|
302
|
+
out += _line(cl)
|
|
303
|
+
out += _line()
|
|
304
|
+
elif st.mode == "trim" and st.trim_selected_note is not None:
|
|
300
305
|
waveform_lines = render_waveform(st.trim_selected_note)
|
|
301
306
|
for wl in waveform_lines:
|
|
302
307
|
out += _line(wl)
|
|
@@ -311,11 +316,6 @@ def draw_ui():
|
|
|
311
316
|
for wl in waveform_lines:
|
|
312
317
|
out += _line(wl)
|
|
313
318
|
out += _line()
|
|
314
|
-
elif st.mode == "config":
|
|
315
|
-
config_lines = render_config_panel()
|
|
316
|
-
for cl in config_lines:
|
|
317
|
-
out += _line(cl)
|
|
318
|
-
out += _line()
|
|
319
319
|
else:
|
|
320
320
|
pad_lines = render_pad_strip(sounding_notes)
|
|
321
321
|
for pl in pad_lines:
|
|
@@ -411,6 +411,8 @@ def draw_ui():
|
|
|
411
411
|
line1 += f" {st.YELLOW}{st.BOLD}[DUB]{st.RST} {st.DIM}{pos_m}:{pos_s:04.1f} / {buf_secs:.1f} x{l_layers} {layer_word}{st.RST}"
|
|
412
412
|
else:
|
|
413
413
|
line1 += f" {st.DIM}idle{st.RST}"
|
|
414
|
+
elif st.mode == "sample" and st.record_armed and not ui_rec:
|
|
415
|
+
line1 += f" {st.YELLOW}{st.BOLD}\u25cf ARM{st.RST}"
|
|
414
416
|
elif ui_rec:
|
|
415
417
|
elapsed = time.time() - ui_rec["start_time"]
|
|
416
418
|
line1 += f" {st.RED}{st.BOLD}\u25cf REC {ui_rec['note']} {elapsed:.1f}s{st.RST}"
|
|
@@ -436,47 +438,49 @@ def draw_ui():
|
|
|
436
438
|
out += _line(line2)
|
|
437
439
|
|
|
438
440
|
# Line 3: help keys
|
|
439
|
-
if st.
|
|
441
|
+
if st.config_overlay_open:
|
|
442
|
+
line3 = f" {st.DIM}[\u2191\u2193] field [\u2190\u2192] change [c] close [q] quit{st.RST}"
|
|
443
|
+
elif st.mode == "trim":
|
|
440
444
|
if st.trim_selected_note is None:
|
|
441
|
-
line3 = f" {st.DIM}[Tab] mode [play] select [q] quit{st.RST}"
|
|
445
|
+
line3 = f" {st.DIM}[Tab] mode [c] config [play] select [q] quit{st.RST}"
|
|
442
446
|
else:
|
|
443
|
-
line3 = f" {st.DIM}[\u2190\u2192] fine [\u2191\u2193] coarse [enter] set [esc] cancel [q] quit{st.RST}"
|
|
447
|
+
line3 = f" {st.DIM}[Tab] mode [c] config [\u2190\u2192] fine [\u2191\u2193] coarse [enter] set [esc] cancel [q] quit{st.RST}"
|
|
444
448
|
elif st.mode == "pitch":
|
|
445
|
-
line3 = f" {st.DIM}[Tab] mode [\u2191\u2193] semitone [\u2190\u2192] cents [q] quit{st.RST}"
|
|
449
|
+
line3 = f" {st.DIM}[Tab] mode [c] config [\u2191\u2193] semitone [\u2190\u2192] cents [q] quit{st.RST}"
|
|
446
450
|
elif st.mode == "chromatic":
|
|
447
451
|
if st.chromatic_selecting:
|
|
448
|
-
line3 = f" {st.DIM}[Tab] mode [play] select [q] quit{st.RST}"
|
|
452
|
+
line3 = f" {st.DIM}[Tab] mode [c] config [play] select [q] quit{st.RST}"
|
|
449
453
|
elif st.metronome_enabled:
|
|
450
|
-
line3 = f" {st.DIM}[Tab] mode [m] metro off [\u2190\u2192] bpm\u00b11 [\u2191\u2193] bpm\u00b15 [r] reselect [q] quit{st.RST}"
|
|
454
|
+
line3 = f" {st.DIM}[Tab] mode [c] config [m] metro off [\u2190\u2192] bpm\u00b11 [\u2191\u2193] bpm\u00b15 [r] reselect [q] quit{st.RST}"
|
|
451
455
|
else:
|
|
452
|
-
line3 = f" {st.DIM}[Tab] mode [r] reselect [m] metro [q] quit{st.RST}"
|
|
456
|
+
line3 = f" {st.DIM}[Tab] mode [c] config [r] reselect [m] metro [q] quit{st.RST}"
|
|
453
457
|
elif st.mode == "copy":
|
|
454
458
|
if st.copy_confirm_pending:
|
|
455
|
-
line3 = f" {st.DIM}[enter] confirm [esc] cancel [q] quit{st.RST}"
|
|
459
|
+
line3 = f" {st.DIM}[Tab] mode [c] config [enter] confirm [esc] cancel [q] quit{st.RST}"
|
|
456
460
|
elif st.copy_source_note is not None:
|
|
457
|
-
line3 = f" {st.DIM}[Tab] mode [play] paste [esc] cancel [q] quit{st.RST}"
|
|
461
|
+
line3 = f" {st.DIM}[Tab] mode [c] config [play] paste [esc] cancel [q] quit{st.RST}"
|
|
458
462
|
else:
|
|
459
|
-
line3 = f" {st.DIM}[Tab] mode [play] select [q] quit{st.RST}"
|
|
463
|
+
line3 = f" {st.DIM}[Tab] mode [c] config [play] select [q] quit{st.RST}"
|
|
460
464
|
elif st.mode == "split":
|
|
461
465
|
if st.split_selected_note is None:
|
|
462
|
-
line3 = f" {st.DIM}[Tab] mode [play] select [q] quit{st.RST}"
|
|
466
|
+
line3 = f" {st.DIM}[Tab] mode [c] config [play] select [q] quit{st.RST}"
|
|
463
467
|
elif st.split_phase == "markers":
|
|
464
|
-
line3 = f" {st.DIM}[\u2190\u2192] fine [\u2191\u2193] coarse [enter] mark [a] assign [esc] undo [q] quit{st.RST}"
|
|
468
|
+
line3 = f" {st.DIM}[Tab] mode [c] config [\u2190\u2192] fine [\u2191\u2193] coarse [enter] mark [a] assign [esc] undo [q] quit{st.RST}"
|
|
465
469
|
else:
|
|
466
|
-
line3 = f" {st.DIM}[play] assign slice [esc] undo [q] quit{st.RST}"
|
|
470
|
+
line3 = f" {st.DIM}[Tab] mode [c] config [play] assign slice [esc] undo [q] quit{st.RST}"
|
|
467
471
|
elif st.mode == "loop":
|
|
468
472
|
if st.metronome_enabled:
|
|
469
|
-
line3 = f" {st.DIM}[Tab] mode [m] metro off [\u2190\u2192] bpm\u00b11 [\u2191\u2193] bpm\u00b15 [q] quit{st.RST}"
|
|
473
|
+
line3 = f" {st.DIM}[Tab] mode [c] config [m] metro off [\u2190\u2192] bpm\u00b11 [\u2191\u2193] bpm\u00b15 [q] quit{st.RST}"
|
|
470
474
|
else:
|
|
471
|
-
line3 = f" {st.DIM}[
|
|
472
|
-
elif st.mode == "config":
|
|
473
|
-
line3 = f" {st.DIM}[\u2191\u2193] field [\u2190\u2192] change [Tab] mode [q] quit{st.RST}"
|
|
475
|
+
line3 = f" {st.DIM}[Tab] mode [c] config [l] record/play/dub [esc] clear [m] metro [q] quit{st.RST}"
|
|
474
476
|
else:
|
|
477
|
+
# synth or sample
|
|
478
|
+
rec_hint = " [r] rec" if st.mode == "sample" else ""
|
|
475
479
|
if st.metronome_enabled and st.mode in st.METRONOME_MODES:
|
|
476
|
-
line3 = f" {st.DIM}[Tab] mode [m] metro off [\u2190\u2192] bpm\u00b11 [\u2191\u2193] bpm\u00b15 [q] quit{st.RST}"
|
|
480
|
+
line3 = f" {st.DIM}[Tab] mode [c] config{rec_hint} [m] metro off [\u2190\u2192] bpm\u00b11 [\u2191\u2193] bpm\u00b15 [q] quit{st.RST}"
|
|
477
481
|
else:
|
|
478
482
|
metro_hint = " [m] metro" if st.mode in st.METRONOME_MODES else ""
|
|
479
|
-
line3 = f" {st.DIM}[Tab] mode{metro_hint} [q] quit{st.RST}"
|
|
483
|
+
line3 = f" {st.DIM}[Tab] mode [c] config{rec_hint}{metro_hint} [q] quit{st.RST}"
|
|
480
484
|
out += _line(line3)
|
|
481
485
|
|
|
482
486
|
sys.stdout.write(out)
|
|
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
|