rosabeats 0.1.3__py3-none-any.whl → 0.2.0__py3-none-any.whl

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.
@@ -1,386 +1,493 @@
1
1
  #!/usr/bin/env python
2
2
 
3
- import re, sys, os.path, random
4
- import cmd, rosabeats
3
+ import re
4
+ import sys
5
+ import os.path
6
+ import random
7
+ import cmd
8
+ import rosabeats
5
9
 
6
10
 
7
- class rosabeats_shell(cmd.Cmd, rosabeats.rosabeats):
8
- intro = "welcome to the rosabeats shell"
9
- prompt = "R@; "
11
+ def parse_range(arg):
12
+ """Parse a range argument like '8' or '8-16' or '-3-2' into a list of integers.
10
13
 
11
- def define_macro(self, name, value):
12
- if self.is_defined_macro(name):
13
- print("redefining macro %s" % name)
14
- self.macros[name] = value
15
- return True
14
+ Returns list of ints, or None on error.
15
+ """
16
+ arg = arg.strip()
17
+ if not arg:
18
+ return None
16
19
 
17
- def play_macro(self, name, times):
18
- if not self.is_defined_macro(name):
19
- print("macro %s not defined" % name)
20
- return False
20
+ # Support negative numbers: match optional minus, digits, optional -stop
21
+ m = re.match(r"^(-?\d+)(?:-(-?\d+))?$", arg)
22
+ if not m:
23
+ print(f"invalid range: '{arg}' - use e.g. '8' or '8-16' or '-3-2'")
24
+ return None
25
+ start = int(m.group(1))
26
+ stop = int(m.group(2)) if m.group(2) is not None else start
21
27
 
22
- macro = self.macros[name]
28
+ if start > stop:
29
+ step = -1
30
+ else:
31
+ step = 1
23
32
 
24
- print("[*] %d * %s [%s]" % (times, name, macro))
33
+ return list(range(start, (stop + 1) if step >= 0 else (stop - 1), step))
25
34
 
26
- for x in range(times):
27
- print("-> %s" % macro)
28
- self.onecmd(self.precmd(macro))
29
35
 
30
- def is_defined_macro(self, name):
31
- if self.macros.get(name, None) is not None:
32
- return True
33
- else:
34
- return False
36
+ def parse_int(arg, name="argument"):
37
+ """Parse an integer argument. Returns int or None on error."""
38
+ try:
39
+ return int(arg.strip())
40
+ except (ValueError, AttributeError):
41
+ print(f"{name} must be an integer")
42
+ return None
35
43
 
36
- def arg1_parse_range(self):
37
- start = None
38
- stop = None
39
- step = None
40
44
 
41
- try:
42
- if "-" in self.cmd_args[0]:
43
- start, stop = self.cmd_args[0].split("-", maxsplit=1)
44
- start = int(start)
45
- stop = int(stop)
46
-
47
- else:
48
- start = int(self.cmd_args[0])
49
- stop = start
50
- except:
51
- print("first argument must be valid range, e.g. '8' or '8-10'")
52
- return None
45
+ def parse_float(arg, name="argument"):
46
+ """Parse a float argument. Returns float or None on error."""
47
+ try:
48
+ return float(arg.strip())
49
+ except (ValueError, AttributeError):
50
+ print(f"{name} must be a number")
51
+ return None
53
52
 
54
- if start > stop:
55
- step = -1
56
- else:
57
- step = 1
58
53
 
59
- return [x for x in range(start, (stop + 1) if step >= 0 else (stop - 1), step)]
54
+ class rosabeats_shell(cmd.Cmd, rosabeats.rosabeats):
55
+ intro = "Welcome to the rosabeats shell. Type 'help' for commands."
56
+ prompt = "R> "
60
57
 
61
- # constructor
62
58
  def __init__(self):
63
59
  cmd.Cmd.__init__(self)
64
60
  rosabeats.rosabeats.__init__(self)
65
- # super(cmd.Cmd, self)
66
- # super(rosabeats.rosabeats, self)
67
- # super().__init__()
68
- self.macros = dict()
69
- self.cmd_args = None
70
-
71
- # pre & post hooks
72
- def precmd(self, line):
73
- if line.strip() != '':
74
- print("precmd: saving cmd as %s" % line)
75
- self.prev_cmd = line
76
- self.cmd_args = line.split()[1:]
77
- return line
61
+ self.macros = {}
78
62
 
79
63
  def preloop(self):
80
64
  self.enable_output_play()
81
65
  self.disable_output_beats()
82
66
  self.disable_output_save()
83
67
 
84
- def arg1_float(self):
85
- try:
86
- arg1 = float(self.cmd_args[0])
87
- except:
88
- print("arg1 must be float")
89
- return None
90
- return arg1
68
+ # --- Macro management ---
91
69
 
92
- def arg1_string(self):
93
- try:
94
- arg1 = str(self.cmd_args[0])
95
- except:
96
- print("arg1 must be string")
97
- return None
98
- return arg1
70
+ def define_macro(self, name, value):
71
+ if name in self.macros:
72
+ print(f"redefining macro '{name}'")
73
+ self.macros[name] = value
99
74
 
100
- def arg1_int(self):
101
- try:
102
- arg1 = int(self.cmd_args[0])
103
- except:
104
- print("arg1 must be int")
105
- return None
106
- return arg1
75
+ def play_macro(self, name, times=1):
76
+ if name not in self.macros:
77
+ print(f"macro '{name}' not defined")
78
+ return False
107
79
 
108
- def arg2_int(self):
109
- try:
110
- arg1 = int(self.cmd_args[1])
111
- except:
112
- print("arg2 must be int")
113
- return None
114
- return arg1
80
+ macro = self.macros[name]
81
+ print(f"[*] {times} x {name} [{macro}]")
115
82
 
116
- def arg3_int(self):
117
- try:
118
- arg3 = int(self.cmd_args[2])
119
- except:
120
- print("arg3 must be int")
121
- return None
122
- return arg3
83
+ for _ in range(times):
84
+ print(f"-> {macro}")
85
+ self.onecmd(macro)
86
+ return True
123
87
 
124
- def arg2_valid_repeat(self):
125
- # no second arg so 1 by default
126
- if len(self.cmd_args) < 2:
127
- return 1
88
+ # --- Commands ---
128
89
 
129
- return self.arg2_int()
90
+ def do_file(self, arg):
91
+ """Load an audio file: file <path>"""
92
+ if not arg.strip():
93
+ print("usage: file <path>")
94
+ return
130
95
 
131
- def arg3_valid_repeat(self):
132
- # no third arg so 1 by default
133
- if len(self.cmd_args) < 3:
134
- return 1
96
+ filename = arg.strip()
97
+ print(f"loading {filename}")
98
+ self.setfile(filename)
135
99
 
136
- return self.arg3_int()
100
+ def do_beats_bar(self, arg):
101
+ """Set beats per bar and downbeat: beats_bar <beats_per_bar> <downbeat>"""
102
+ args = arg.split()
103
+ if len(args) < 2:
104
+ print("usage: beats_bar <beats_per_bar> <downbeat>")
105
+ return
137
106
 
138
- # command handlers
139
- def do_rest(self, arg):
140
- rest = self.arg1_float()
141
- times = self.arg2_valid_repeat()
107
+ beatsper = parse_int(args[0], "beats_per_bar")
108
+ downbeat = parse_int(args[1], "downbeat")
142
109
 
143
- if rest is None or times is None:
144
- return False
110
+ if beatsper is None or downbeat is None:
111
+ return
145
112
 
146
- print("[*] %d * %g beat(s) of rest" % (times, rest))
113
+ if not self.sourcefile:
114
+ print("error: load a file first with 'file <path>'")
115
+ return
147
116
 
148
- for x in range(times):
149
- self.rest(rest)
117
+ print("Tracking beats...")
118
+ self.track_beats(beatsper=beatsper, downbeat=downbeat)
119
+ print(f"Found {self.total_beats} beats in {self.total_bars} bars")
120
+ self.init_outputs()
150
121
 
151
122
  def do_beats(self, arg):
152
- beats = self.arg1_parse_range()
153
- times = self.arg2_valid_repeat()
154
-
155
- if beats is None or times is None:
156
- return False
157
-
158
- print("[*] %d * " % times, end="", flush=True)
123
+ """Play beats: beats <N> or beats <N-M> [times]"""
124
+ args = arg.split()
125
+ if not args:
126
+ print("usage: beats <N> or beats <N-M> [times]")
127
+ return
128
+
129
+ beats = parse_range(args[0])
130
+ if beats is None:
131
+ return
132
+
133
+ times = 1
134
+ if len(args) > 1:
135
+ times = parse_int(args[1], "times")
136
+ if times is None:
137
+ return
159
138
 
160
139
  if len(beats) > 1:
161
- print("(beats %d-%d) " % (beats[0], beats[-1]))
140
+ print(f"[*] {times} x beats {beats[0]}-{beats[-1]}")
162
141
  else:
163
- print("(beat %d) " % beats[0])
164
-
165
- for x in range(times):
166
- self.play_beats(beats)
167
-
168
- def do_beat_div(self, arg):
169
- beat = self.arg1_int()
170
- divisor = self.arg2_int()
171
- times = self.arg3_valid_repeat()
172
-
173
- if beats is None or divisor is None or times is None:
174
- return False
175
-
176
- print("[*] %d * (1/%d beats) " % (times, divisor), flush=True)
142
+ print(f"[*] {times} x beat {beats[0]}")
177
143
 
178
- for x in range(times):
179
- print("%d/%d " % (beat, divisor), end="", flush=True)
180
- self.play_beat(beat, divisor=divisor, silent=True)
181
-
182
- print(flush=True)
183
-
184
- def do_beats_shuf(self, arg):
185
- beats = self.arg1_parse_range()
186
- times = self.arg2_valid_repeat()
187
-
188
- if beats is None or times is None:
189
- return False
190
-
191
- print("[*] %d * " % times, end="", flush=True)
192
- print("(shuffled beats %d-%d] " % (beats[0], beats[-1]), flush=True)
193
-
194
- for x in range(times):
195
- random.shuffle(beats)
144
+ for _ in range(times):
196
145
  self.play_beats(beats)
197
146
 
198
147
  def do_bars(self, arg):
199
- bars = self.arg1_parse_range()
200
- times = self.arg2_valid_repeat()
201
-
202
- if bars is None or times is None:
203
- return False
148
+ """Play bars: bars <N> or bars <N-M> [times]"""
149
+ args = arg.split()
150
+ if not args:
151
+ print("usage: bars <N> or bars <N-M> [times]")
152
+ return
153
+
154
+ bars = parse_range(args[0])
155
+ if bars is None:
156
+ return
157
+
158
+ times = 1
159
+ if len(args) > 1:
160
+ times = parse_int(args[1], "times")
161
+ if times is None:
162
+ return
204
163
 
205
- print("[*] %d * " % times, end="", flush=True)
206
164
  if len(bars) > 1:
207
- print("(bars %d-%d) " % (bars[0], bars[-1]), flush=True)
165
+ print(f"[*] {times} x bars {bars[0]}-{bars[-1]}")
208
166
  else:
209
- print("(bar %d) " % bars[0], flush=True)
167
+ print(f"[*] {times} x bar {bars[0]}")
210
168
 
211
- for x in range(times):
169
+ for _ in range(times):
212
170
  self.play_bars(bars)
213
171
 
214
172
  def do_bars_rev(self, arg):
215
- bars = self.arg1_parse_range()
216
- times = self.arg2_valid_repeat()
173
+ """Play bars with beats reversed: bars_rev <N-M> [times]"""
174
+ args = arg.split()
175
+ if not args:
176
+ print("usage: bars_rev <N-M> [times]")
177
+ return
178
+
179
+ bars = parse_range(args[0])
180
+ if bars is None:
181
+ return
182
+
183
+ times = 1
184
+ if len(args) > 1:
185
+ times = parse_int(args[1], "times")
186
+ if times is None:
187
+ return
217
188
 
218
- if bars is None or times is None:
219
- return False
220
-
221
- print("[*] %d * " % times, end="", flush=True)
222
189
  if len(bars) > 1:
223
- print("(rev bars %d-%d) " % (bars[0], bars[-1]), flush=True)
190
+ print(f"[*] {times} x bars_rev {bars[0]}-{bars[-1]}")
224
191
  else:
225
- print("(rev bar %d) " % bars[0], flush=True)
192
+ print(f"[*] {times} x bar_rev {bars[0]}")
226
193
 
227
- for x in range(times):
194
+ for _ in range(times):
228
195
  self.play_bars(bars, reverse=True)
229
196
 
197
+ def do_beats_shuf(self, arg):
198
+ """Play beats in shuffled order: beats_shuf <N-M> [times]"""
199
+ args = arg.split()
200
+ if not args:
201
+ print("usage: beats_shuf <N-M> [times]")
202
+ return
203
+
204
+ beats = parse_range(args[0])
205
+ if beats is None:
206
+ return
207
+
208
+ times = 1
209
+ if len(args) > 1:
210
+ times = parse_int(args[1], "times")
211
+ if times is None:
212
+ return
213
+
214
+ print(f"[*] {times} x beats_shuf {beats[0]}-{beats[-1]}")
215
+
216
+ for _ in range(times):
217
+ random.shuffle(beats)
218
+ self.play_beats(beats)
219
+
230
220
  def do_bars_shuf(self, arg):
231
- bars = self.arg1_parse_range()
232
- times = self.arg2_valid_repeat()
221
+ """Play bars in shuffled order: bars_shuf <N-M> [times]"""
222
+ args = arg.split()
223
+ if not args:
224
+ print("usage: bars_shuf <N-M> [times]")
225
+ return
233
226
 
234
- if bars is None or times is None:
235
- return False
227
+ bars = parse_range(args[0])
228
+ if bars is None:
229
+ return
230
+
231
+ times = 1
232
+ if len(args) > 1:
233
+ times = parse_int(args[1], "times")
234
+ if times is None:
235
+ return
236
236
 
237
- print("[*] %d * " % times, end="", flush=True)
238
- print("(shuffled bars %d-%d) " % (bars[0], bars[-1]), flush=True)
237
+ print(f"[*] {times} x bars_shuf {bars[0]}-{bars[-1]}")
239
238
 
240
- for x in range(times):
239
+ for _ in range(times):
241
240
  random.shuffle(bars)
242
241
  self.play_bars(bars)
243
242
 
244
- def do_play(self, arg):
245
- name = self.arg1_string()
246
- times = self.arg2_valid_repeat()
243
+ def do_beat_div(self, arg):
244
+ """Play subdivided beat: beat_div <beat> <divisor> [times]"""
245
+ args = arg.split()
246
+ if len(args) < 2:
247
+ print("usage: beat_div <beat> <divisor> [times]")
248
+ return
249
+
250
+ beat = parse_int(args[0], "beat")
251
+ divisor = parse_int(args[1], "divisor")
252
+ if beat is None or divisor is None:
253
+ return
254
+
255
+ times = 1
256
+ if len(args) > 2:
257
+ times = parse_int(args[2], "times")
258
+ if times is None:
259
+ return
260
+
261
+ print(f"[*] {times} x beat {beat}/{divisor}")
262
+
263
+ for _ in range(times):
264
+ self.play_beat(beat, divisor=divisor, silent=True)
265
+ print()
247
266
 
248
- if name is None or times is None:
249
- return False
267
+ def do_rest(self, arg):
268
+ """Insert silence: rest <beats> [times]"""
269
+ args = arg.split()
270
+ if not args:
271
+ print("usage: rest <beats> [times]")
272
+ return
250
273
 
251
- self.play_macro(name, times)
274
+ rest_beats = parse_float(args[0], "beats")
275
+ if rest_beats is None:
276
+ return
252
277
 
253
- def args_valid_value(self):
254
- try:
255
- args = " ".join(self.cmd_args[1:])
256
- except:
257
- print("this cmd requires the value to assign")
258
- return None
259
- return args
278
+ times = 1
279
+ if len(args) > 1:
280
+ times = parse_int(args[1], "times")
281
+ if times is None:
282
+ return
283
+
284
+ print(f"[*] {times} x rest({rest_beats})")
285
+
286
+ for _ in range(times):
287
+ self.rest(rest_beats)
260
288
 
261
289
  def do_def(self, arg):
262
- name = self.arg1_string()
263
- value = self.args_valid_value()
290
+ """Define a macro: def <name> <command>"""
291
+ args = arg.split(None, 1) # Split into name and rest
292
+ if len(args) < 2:
293
+ print("usage: def <name> <command>")
294
+ return
264
295
 
265
- if name is None or value is None:
266
- return False
296
+ name = args[0]
297
+ value = args[1]
267
298
 
268
- print("[*] (def seg: %s = %s)" % (name, value))
299
+ # Strip inline comments
300
+ if "#" in value:
301
+ value = value.split("#")[0].strip()
269
302
 
270
303
  self.define_macro(name, value)
271
- print("defined %s => %s" % (name, value))
304
+ print(f"defined {name} => {value}")
272
305
 
273
- def do_help(self, arg):
274
- print("valid commands are:")
275
- print(" beats, bars, beats_shuf, bars_shuf, beat_div, bars_rev, rest")
276
- print(" for predefined macros: ls, lsdef, play, or just type the def name")
277
- print(" and finally: save, help, quit")
306
+ def do_play(self, arg):
307
+ """Play a macro: play <name> [times]"""
308
+ args = arg.split()
309
+ if not args:
310
+ print("usage: play <name> [times]")
311
+ return
312
+
313
+ name = args[0]
314
+ times = 1
315
+ if len(args) > 1:
316
+ times = parse_int(args[1], "times")
317
+ if times is None:
318
+ return
319
+
320
+ self.play_macro(name, times)
321
+
322
+ def do_ls(self, arg):
323
+ """List defined macros"""
324
+ if self.macros:
325
+ print(", ".join(self.macros.keys()))
326
+ else:
327
+ print("(no macros defined)")
278
328
 
279
329
  def do_lsdef(self, arg):
330
+ """List macros with their definitions"""
331
+ if not self.macros:
332
+ print("(no macros defined)")
333
+ return
280
334
  for name, value in self.macros.items():
281
- print("%15s %15s" % (name, value))
335
+ print(f" {name}: {value}")
282
336
 
283
- def do_ls(self, arg):
284
- print(", ".join(list(self.macros.keys())))
337
+ def do_save(self, arg):
338
+ """Save current session to a .br file: save <filename>"""
339
+ if not arg.strip():
340
+ print("usage: save <filename>")
341
+ return
285
342
 
286
- def do_quit(self, arg):
287
- self.shutdown()
288
- sys.exit(0)
343
+ filename = arg.strip()
289
344
 
290
- def do_file(self, arg):
291
- filename = self.arg1_string()
345
+ if not self.sourcefile:
346
+ print("error: no audio file loaded")
347
+ return
292
348
 
293
- if filename is None:
294
- return False
349
+ print(f"saving to {filename}")
350
+ with open(filename, "w") as f:
351
+ f.write(f"file {self.sourcefile}\n")
352
+ f.write(f"beats_bar {self.beatsperbar} {self.downbeat}\n")
353
+ for name, value in self.macros.items():
354
+ f.write(f"def {name} {value}\n")
355
+ print("saved")
295
356
 
296
- print("setting filename to %s" % filename)
297
- self.setfile(filename)
357
+ def do_load(self, arg):
358
+ """Load a .br or .bri file: load <filename>"""
359
+ if not arg.strip():
360
+ print("usage: load <filename>")
361
+ return
298
362
 
299
- def do_beats_bar(self, arg):
300
- per = self.arg1_int()
301
- first = self.arg2_int()
363
+ filename = arg.strip()
364
+ self.load_recipe_file(filename)
302
365
 
303
- if per is None or first is None:
366
+ def load_recipe_file(self, filename):
367
+ """Load and execute commands from a .br or .bri file."""
368
+ try:
369
+ with open(filename, "r") as f:
370
+ lines = f.readlines()
371
+ except FileNotFoundError:
372
+ print(f"error: file not found: {filename}")
373
+ return False
374
+ except Exception as e:
375
+ print(f"error reading {filename}: {e}")
304
376
  return False
305
377
 
306
- # if we know our audio source, load it, analyze it, and init outputs
378
+ print(f"loading {filename}...")
379
+ for line in lines:
380
+ line = line.strip()
381
+ # Skip empty lines and comments
382
+ if not line or line.startswith("#"):
383
+ continue
384
+ # Strip inline comments (but not inside the def command - that's handled separately)
385
+ # Execute the command
386
+ self.onecmd(line)
387
+
388
+ print(f"loaded {filename}")
307
389
  if self.sourcefile:
308
- print("One moment as we track your beats for you, madame...")
309
- self.track_beats(beatsper=per, firstfull=first)
310
- print("%d beats, %d bars" % (self.total_beats, self.total_bars))
311
- self.init_outputs()
312
- else:
313
- print("specify file before beats_bar")
314
- return False
390
+ print(f" audio: {self.sourcefile}")
391
+ if self.total_beats:
392
+ print(f" {self.total_beats} beats, {self.total_bars} bars")
393
+ if self.macros:
394
+ print(f" {len(self.macros)} macros defined")
395
+ return True
315
396
 
316
- def do_save(self, arg):
317
- filename = self.arg1_string()
318
- print("saving %s" % filename)
319
- with open(filename, "w") as f:
320
- print("file %s" % self.sourcefile, file=f)
321
- print("beats_bar %d %d" % (self.beatsperbar, self.firstfullbar), file=f)
322
- for name, value in self.macros.items():
323
- print("def %s %s" % (name, value), file=f)
397
+ def do_quit(self, arg):
398
+ """Exit the shell"""
399
+ print("goodbye")
400
+ self.shutdown()
401
+ return True # Signals cmd.Cmd to exit
324
402
 
325
- def default(self, line):
326
- self.cmd_args = line.split()
327
- self.do_play(line)
403
+ def do_exit(self, arg):
404
+ """Exit the shell"""
405
+ return self.do_quit(arg)
406
+
407
+ def do_EOF(self, arg):
408
+ """Handle Ctrl+D"""
409
+ print()
410
+ return self.do_quit(arg)
328
411
 
412
+ def default(self, line):
413
+ """Try to play a macro if command not recognized"""
414
+ args = line.split()
415
+ if not args:
416
+ return
417
+
418
+ name = args[0]
419
+ if name in self.macros:
420
+ times = 1
421
+ if len(args) > 1:
422
+ times = parse_int(args[1], "times")
423
+ if times is None:
424
+ return
425
+ self.play_macro(name, times)
426
+ else:
427
+ print(f"unknown command or macro: '{name}'")
329
428
 
330
- # def emptyline(self):
331
- # print("repeating: %s" % self.prev_cmd)
332
- # self.onecmd(self.precmd(self.prev_cmd))
429
+ def emptyline(self):
430
+ """Do nothing on empty line (don't repeat last command)"""
431
+ pass
333
432
 
334
433
 
335
434
  def main():
336
- """
337
- Main function for rosabeats-shell command-line tool.
338
-
339
- Usage: rosabeats-shell [audio_file [beats_per_bar first_full_bar]]
340
-
341
- Args:
342
- audio_file: Optional audio file to analyze
343
- beats_per_bar: Number of beats per bar (if audio_file is provided)
344
- first_full_bar: First full bar number (if audio_file is provided)
345
-
346
- Returns:
347
- Exit code (0 for success, non-zero for errors)
348
- """
435
+ """Main entry point for rosabeats-shell."""
436
+ import argparse
437
+
438
+ parser = argparse.ArgumentParser(
439
+ description='Interactive shell for beat manipulation',
440
+ formatter_class=argparse.RawDescriptionHelpFormatter,
441
+ epilog='''
442
+ Examples:
443
+ rosabeats-shell Start empty shell
444
+ rosabeats-shell song.wav Load audio file
445
+ rosabeats-shell song.wav 8 0 Load with 8 beats/bar, downbeat at 0
446
+ rosabeats-shell recipe.br Load beat recipe file
447
+ rosabeats-shell --debug song.wav Enable debug mode
448
+ '''
449
+ )
450
+ parser.add_argument('file', nargs='?', metavar='FILE',
451
+ help='Audio file (.wav, .ogg) or recipe file (.br, .bri) to load')
452
+ parser.add_argument('beatsper', nargs='?', type=int, default=8, metavar='BEATSPER',
453
+ help='Beats per bar (default: 8)')
454
+ parser.add_argument('downbeat', nargs='?', type=int, default=0, metavar='DOWNBEAT',
455
+ help='Downbeat offset (default: 0)')
456
+ parser.add_argument('-d', '--debug', action='store_true',
457
+ help='Enable debug mode')
458
+
459
+ args = parser.parse_args()
460
+
461
+ # Set debug mode before creating shell
462
+ if args.debug:
463
+ rosabeats.rosabeats.debug = True
464
+
465
+ shell = rosabeats_shell()
466
+ shell.preloop()
467
+
468
+ # Handle file argument
469
+ if args.file:
470
+ filename = args.file
471
+
472
+ # Check if it's a recipe file (.br or .bri) or an audio file
473
+ if filename.endswith(".br") or filename.endswith(".bri"):
474
+ shell.load_recipe_file(filename)
475
+ else:
476
+ # Treat as audio file
477
+ shell.onecmd(f"file {filename}")
478
+ shell.onecmd(f"beats_bar {args.beatsper} {args.downbeat}")
479
+
349
480
  try:
350
- load_file = False
481
+ shell.cmdloop()
482
+ except KeyboardInterrupt:
483
+ print("\ninterrupted")
484
+ finally:
351
485
  try:
352
- filename = sys.argv[1]
353
- beats_bar = " ".join(sys.argv[2:])
354
- load_file = True
355
- print(f"Audio file: {filename}")
356
- print(f"Beats/bar settings: {beats_bar}")
357
- except IndexError:
358
- # No command line arguments is valid
486
+ shell.shutdown()
487
+ except:
359
488
  pass
360
489
 
361
- s = rosabeats_shell()
362
- if load_file:
363
- s.preloop()
364
- s.onecmd(s.precmd(f"file {filename}"))
365
- s.onecmd(s.precmd(f"beats_bar {beats_bar}"))
366
-
367
- s.cmdloop()
368
- return 0
369
- except KeyboardInterrupt:
370
- print("\nShutting down...")
371
- if 's' in locals() and hasattr(s, 'shutdown'):
372
- s.shutdown()
373
- return 0
374
- except Exception as e:
375
- print(f"Error: {str(e)}")
376
- return 1
377
- finally:
378
- # Ensure we clean up resources even if there's an error
379
- if 's' in locals() and hasattr(s, 'shutdown'):
380
- try:
381
- s.shutdown()
382
- except:
383
- pass
490
+ return 0
384
491
 
385
492
 
386
493
  if __name__ == "__main__":