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.
- rosabeats/__init__.py +1 -1
- rosabeats/__main__.py +59 -0
- rosabeats/beatrecipe_processor.py +63 -46
- rosabeats/beatswitch.py +29 -13
- rosabeats/downbeat.py +207 -0
- rosabeats/rosabeats.py +575 -543
- rosabeats/rosabeats_shell.py +391 -284
- rosabeats/segment_song.py +100 -31
- {rosabeats-0.1.3.dist-info → rosabeats-0.2.0.dist-info}/METADATA +8 -30
- rosabeats-0.2.0.dist-info/RECORD +21 -0
- rosabeats-0.2.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/conftest.py +131 -0
- tests/test_beatrecipe_processor.py +193 -0
- tests/test_downbeat.py +149 -0
- tests/test_rosabeats.py +234 -0
- tests/test_segment_song.py +120 -0
- tests/test_shell.py +305 -0
- docs/beatrecipe_docs.txt +0 -80
- rosabeats-0.1.3.dist-info/RECORD +0 -16
- rosabeats-0.1.3.dist-info/top_level.txt +0 -3
- scripts/reverse_beats_in_bars_rosa.py +0 -48
- scripts/shuffle_bars_rosa.py +0 -35
- scripts/shuffle_beats_rosa.py +0 -29
- {rosabeats-0.1.3.dist-info → rosabeats-0.2.0.dist-info}/WHEEL +0 -0
- {rosabeats-0.1.3.dist-info → rosabeats-0.2.0.dist-info}/entry_points.txt +0 -0
- {rosabeats-0.1.3.dist-info → rosabeats-0.2.0.dist-info}/licenses/LICENSE.md +0 -0
rosabeats/rosabeats_shell.py
CHANGED
|
@@ -1,386 +1,493 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
|
|
3
|
-
import re
|
|
4
|
-
import
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
import os.path
|
|
6
|
+
import random
|
|
7
|
+
import cmd
|
|
8
|
+
import rosabeats
|
|
5
9
|
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return
|
|
14
|
+
Returns list of ints, or None on error.
|
|
15
|
+
"""
|
|
16
|
+
arg = arg.strip()
|
|
17
|
+
if not arg:
|
|
18
|
+
return None
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
28
|
+
if start > stop:
|
|
29
|
+
step = -1
|
|
30
|
+
else:
|
|
31
|
+
step = 1
|
|
23
32
|
|
|
24
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
125
|
-
# no second arg so 1 by default
|
|
126
|
-
if len(self.cmd_args) < 2:
|
|
127
|
-
return 1
|
|
88
|
+
# --- Commands ---
|
|
128
89
|
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return 1
|
|
96
|
+
filename = arg.strip()
|
|
97
|
+
print(f"loading {filename}")
|
|
98
|
+
self.setfile(filename)
|
|
135
99
|
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
|
144
|
-
return
|
|
110
|
+
if beatsper is None or downbeat is None:
|
|
111
|
+
return
|
|
145
112
|
|
|
146
|
-
|
|
113
|
+
if not self.sourcefile:
|
|
114
|
+
print("error: load a file first with 'file <path>'")
|
|
115
|
+
return
|
|
147
116
|
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
|
|
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("
|
|
140
|
+
print(f"[*] {times} x beats {beats[0]}-{beats[-1]}")
|
|
162
141
|
else:
|
|
163
|
-
print("
|
|
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
|
|
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
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return
|
|
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("
|
|
165
|
+
print(f"[*] {times} x bars {bars[0]}-{bars[-1]}")
|
|
208
166
|
else:
|
|
209
|
-
print("
|
|
167
|
+
print(f"[*] {times} x bar {bars[0]}")
|
|
210
168
|
|
|
211
|
-
for
|
|
169
|
+
for _ in range(times):
|
|
212
170
|
self.play_bars(bars)
|
|
213
171
|
|
|
214
172
|
def do_bars_rev(self, arg):
|
|
215
|
-
bars
|
|
216
|
-
|
|
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("
|
|
190
|
+
print(f"[*] {times} x bars_rev {bars[0]}-{bars[-1]}")
|
|
224
191
|
else:
|
|
225
|
-
print("
|
|
192
|
+
print(f"[*] {times} x bar_rev {bars[0]}")
|
|
226
193
|
|
|
227
|
-
for
|
|
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
|
|
232
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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("[*]
|
|
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
|
|
239
|
+
for _ in range(times):
|
|
241
240
|
random.shuffle(bars)
|
|
242
241
|
self.play_bars(bars)
|
|
243
242
|
|
|
244
|
-
def
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
|
|
274
|
+
rest_beats = parse_float(args[0], "beats")
|
|
275
|
+
if rest_beats is None:
|
|
276
|
+
return
|
|
252
277
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
263
|
-
|
|
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
|
-
|
|
266
|
-
|
|
296
|
+
name = args[0]
|
|
297
|
+
value = args[1]
|
|
267
298
|
|
|
268
|
-
|
|
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
|
|
304
|
+
print(f"defined {name} => {value}")
|
|
272
305
|
|
|
273
|
-
def
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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("
|
|
335
|
+
print(f" {name}: {value}")
|
|
282
336
|
|
|
283
|
-
def
|
|
284
|
-
|
|
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
|
-
|
|
287
|
-
self.shutdown()
|
|
288
|
-
sys.exit(0)
|
|
343
|
+
filename = arg.strip()
|
|
289
344
|
|
|
290
|
-
|
|
291
|
-
|
|
345
|
+
if not self.sourcefile:
|
|
346
|
+
print("error: no audio file loaded")
|
|
347
|
+
return
|
|
292
348
|
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
first = self.arg2_int()
|
|
363
|
+
filename = arg.strip()
|
|
364
|
+
self.load_recipe_file(filename)
|
|
302
365
|
|
|
303
|
-
|
|
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
|
-
|
|
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("
|
|
309
|
-
|
|
310
|
-
print("
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
|
317
|
-
|
|
318
|
-
print("
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
326
|
-
|
|
327
|
-
self.
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
481
|
+
shell.cmdloop()
|
|
482
|
+
except KeyboardInterrupt:
|
|
483
|
+
print("\ninterrupted")
|
|
484
|
+
finally:
|
|
351
485
|
try:
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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__":
|