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.
@@ -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 → record → pitch → chromatic → trim → copy → loop → split → config
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 `record`, `trim`, `copy`, `split`, or `loop` resets that mode's transient state
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
- ### record
50
- Note on `start_recording()` (opens `sd.InputStream`, accumulates chunks).
51
- Note off → `stop_recording()` (closes stream, trims silence, saves WAV).
52
- In keyboard mock mode: key press **toggles** recording on/off (press once to start, again to stop).
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 or sustain pedal (CC 64).
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
- ### config
96
- Up/Down navigates fields. Left/Right changes values. Fields: input device, output device, MIDI port, synesthesia toggle. Changes saved immediately on each key press.
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`, `record`, `loop`, `chromatic`
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: midi-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A MIDI sampler and looper in your terminal
5
5
  Project-URL: Homepage, https://github.com/mnbpdx/midi-cli
6
6
  Project-URL: Repository, https://github.com/mnbpdx/midi-cli
@@ -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.mode == "record":
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
- seq = os.read(fd, 2).decode("utf-8", errors="replace")
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.mode == "record":
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.mode == "record":
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.mode == "record":
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.mode == "record":
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", "record", "pitch", "chromatic", "trim", "copy", "loop", "split", "config"]
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", "record", "loop", "chromatic"}
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.mode == "trim" and st.trim_selected_note is not None:
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.mode == "trim":
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}[sustain/l] record/play/dub [esc] clear [play] sample [m] metro [q] quit{st.RST}"
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)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "midi-cli"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "A MIDI sampler and looper in your terminal"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -74,7 +74,7 @@ wheels = [
74
74
 
75
75
  [[package]]
76
76
  name = "midi-cli"
77
- version = "0.1.3"
77
+ version = "0.2.0"
78
78
  source = { editable = "." }
79
79
  dependencies = [
80
80
  { name = "numpy" },
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