rosabeats 0.1.3__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.
- docs/beatrecipe_docs.txt +80 -0
- rosabeats/__init__.py +23 -0
- rosabeats/beatrecipe_processor.py +523 -0
- rosabeats/beatswitch.py +256 -0
- rosabeats/rosabeats.py +996 -0
- rosabeats/rosabeats_shell.py +387 -0
- rosabeats/segment_song.py +181 -0
- rosabeats-0.1.3.dist-info/METADATA +158 -0
- rosabeats-0.1.3.dist-info/RECORD +16 -0
- rosabeats-0.1.3.dist-info/WHEEL +5 -0
- rosabeats-0.1.3.dist-info/entry_points.txt +5 -0
- rosabeats-0.1.3.dist-info/licenses/LICENSE.md +7 -0
- rosabeats-0.1.3.dist-info/top_level.txt +3 -0
- scripts/reverse_beats_in_bars_rosa.py +48 -0
- scripts/shuffle_bars_rosa.py +35 -0
- scripts/shuffle_beats_rosa.py +29 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import re, sys, os.path, random
|
|
4
|
+
import cmd, rosabeats
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class rosabeats_shell(cmd.Cmd, rosabeats.rosabeats):
|
|
8
|
+
intro = "welcome to the rosabeats shell"
|
|
9
|
+
prompt = "R@; "
|
|
10
|
+
|
|
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
|
|
16
|
+
|
|
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
|
|
21
|
+
|
|
22
|
+
macro = self.macros[name]
|
|
23
|
+
|
|
24
|
+
print("[*] %d * %s [%s]" % (times, name, macro))
|
|
25
|
+
|
|
26
|
+
for x in range(times):
|
|
27
|
+
print("-> %s" % macro)
|
|
28
|
+
self.onecmd(self.precmd(macro))
|
|
29
|
+
|
|
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
|
|
35
|
+
|
|
36
|
+
def arg1_parse_range(self):
|
|
37
|
+
start = None
|
|
38
|
+
stop = None
|
|
39
|
+
step = None
|
|
40
|
+
|
|
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
|
|
53
|
+
|
|
54
|
+
if start > stop:
|
|
55
|
+
step = -1
|
|
56
|
+
else:
|
|
57
|
+
step = 1
|
|
58
|
+
|
|
59
|
+
return [x for x in range(start, (stop + 1) if step >= 0 else (stop - 1), step)]
|
|
60
|
+
|
|
61
|
+
# constructor
|
|
62
|
+
def __init__(self):
|
|
63
|
+
cmd.Cmd.__init__(self)
|
|
64
|
+
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
|
|
78
|
+
|
|
79
|
+
def preloop(self):
|
|
80
|
+
self.enable_output_play()
|
|
81
|
+
self.disable_output_beats()
|
|
82
|
+
self.disable_output_save()
|
|
83
|
+
|
|
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
|
|
91
|
+
|
|
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
|
|
99
|
+
|
|
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
|
|
107
|
+
|
|
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
|
|
115
|
+
|
|
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
|
|
123
|
+
|
|
124
|
+
def arg2_valid_repeat(self):
|
|
125
|
+
# no second arg so 1 by default
|
|
126
|
+
if len(self.cmd_args) < 2:
|
|
127
|
+
return 1
|
|
128
|
+
|
|
129
|
+
return self.arg2_int()
|
|
130
|
+
|
|
131
|
+
def arg3_valid_repeat(self):
|
|
132
|
+
# no third arg so 1 by default
|
|
133
|
+
if len(self.cmd_args) < 3:
|
|
134
|
+
return 1
|
|
135
|
+
|
|
136
|
+
return self.arg3_int()
|
|
137
|
+
|
|
138
|
+
# command handlers
|
|
139
|
+
def do_rest(self, arg):
|
|
140
|
+
rest = self.arg1_float()
|
|
141
|
+
times = self.arg2_valid_repeat()
|
|
142
|
+
|
|
143
|
+
if rest is None or times is None:
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
print("[*] %d * %g beat(s) of rest" % (times, rest))
|
|
147
|
+
|
|
148
|
+
for x in range(times):
|
|
149
|
+
self.rest(rest)
|
|
150
|
+
|
|
151
|
+
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)
|
|
159
|
+
|
|
160
|
+
if len(beats) > 1:
|
|
161
|
+
print("(beats %d-%d) " % (beats[0], beats[-1]))
|
|
162
|
+
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)
|
|
177
|
+
|
|
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)
|
|
196
|
+
self.play_beats(beats)
|
|
197
|
+
|
|
198
|
+
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
|
|
204
|
+
|
|
205
|
+
print("[*] %d * " % times, end="", flush=True)
|
|
206
|
+
if len(bars) > 1:
|
|
207
|
+
print("(bars %d-%d) " % (bars[0], bars[-1]), flush=True)
|
|
208
|
+
else:
|
|
209
|
+
print("(bar %d) " % bars[0], flush=True)
|
|
210
|
+
|
|
211
|
+
for x in range(times):
|
|
212
|
+
self.play_bars(bars)
|
|
213
|
+
|
|
214
|
+
def do_bars_rev(self, arg):
|
|
215
|
+
bars = self.arg1_parse_range()
|
|
216
|
+
times = self.arg2_valid_repeat()
|
|
217
|
+
|
|
218
|
+
if bars is None or times is None:
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
print("[*] %d * " % times, end="", flush=True)
|
|
222
|
+
if len(bars) > 1:
|
|
223
|
+
print("(rev bars %d-%d) " % (bars[0], bars[-1]), flush=True)
|
|
224
|
+
else:
|
|
225
|
+
print("(rev bar %d) " % bars[0], flush=True)
|
|
226
|
+
|
|
227
|
+
for x in range(times):
|
|
228
|
+
self.play_bars(bars, reverse=True)
|
|
229
|
+
|
|
230
|
+
def do_bars_shuf(self, arg):
|
|
231
|
+
bars = self.arg1_parse_range()
|
|
232
|
+
times = self.arg2_valid_repeat()
|
|
233
|
+
|
|
234
|
+
if bars is None or times is None:
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
print("[*] %d * " % times, end="", flush=True)
|
|
238
|
+
print("(shuffled bars %d-%d) " % (bars[0], bars[-1]), flush=True)
|
|
239
|
+
|
|
240
|
+
for x in range(times):
|
|
241
|
+
random.shuffle(bars)
|
|
242
|
+
self.play_bars(bars)
|
|
243
|
+
|
|
244
|
+
def do_play(self, arg):
|
|
245
|
+
name = self.arg1_string()
|
|
246
|
+
times = self.arg2_valid_repeat()
|
|
247
|
+
|
|
248
|
+
if name is None or times is None:
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
self.play_macro(name, times)
|
|
252
|
+
|
|
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
|
|
260
|
+
|
|
261
|
+
def do_def(self, arg):
|
|
262
|
+
name = self.arg1_string()
|
|
263
|
+
value = self.args_valid_value()
|
|
264
|
+
|
|
265
|
+
if name is None or value is None:
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
print("[*] (def seg: %s = %s)" % (name, value))
|
|
269
|
+
|
|
270
|
+
self.define_macro(name, value)
|
|
271
|
+
print("defined %s => %s" % (name, value))
|
|
272
|
+
|
|
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")
|
|
278
|
+
|
|
279
|
+
def do_lsdef(self, arg):
|
|
280
|
+
for name, value in self.macros.items():
|
|
281
|
+
print("%15s %15s" % (name, value))
|
|
282
|
+
|
|
283
|
+
def do_ls(self, arg):
|
|
284
|
+
print(", ".join(list(self.macros.keys())))
|
|
285
|
+
|
|
286
|
+
def do_quit(self, arg):
|
|
287
|
+
self.shutdown()
|
|
288
|
+
sys.exit(0)
|
|
289
|
+
|
|
290
|
+
def do_file(self, arg):
|
|
291
|
+
filename = self.arg1_string()
|
|
292
|
+
|
|
293
|
+
if filename is None:
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
print("setting filename to %s" % filename)
|
|
297
|
+
self.setfile(filename)
|
|
298
|
+
|
|
299
|
+
def do_beats_bar(self, arg):
|
|
300
|
+
per = self.arg1_int()
|
|
301
|
+
first = self.arg2_int()
|
|
302
|
+
|
|
303
|
+
if per is None or first is None:
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
# if we know our audio source, load it, analyze it, and init outputs
|
|
307
|
+
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
|
|
315
|
+
|
|
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)
|
|
324
|
+
|
|
325
|
+
def default(self, line):
|
|
326
|
+
self.cmd_args = line.split()
|
|
327
|
+
self.do_play(line)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# def emptyline(self):
|
|
331
|
+
# print("repeating: %s" % self.prev_cmd)
|
|
332
|
+
# self.onecmd(self.precmd(self.prev_cmd))
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
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
|
+
"""
|
|
349
|
+
try:
|
|
350
|
+
load_file = False
|
|
351
|
+
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
|
|
359
|
+
pass
|
|
360
|
+
|
|
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
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
if __name__ == "__main__":
|
|
387
|
+
sys.exit(main())
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import os.path
|
|
5
|
+
import argparse
|
|
6
|
+
from rosabeats import rosabeats
|
|
7
|
+
|
|
8
|
+
# Check if terminal supports colors
|
|
9
|
+
def supports_color():
|
|
10
|
+
"""Check if the terminal supports color output"""
|
|
11
|
+
if sys.platform == 'win32':
|
|
12
|
+
return False
|
|
13
|
+
if not sys.stdout.isatty():
|
|
14
|
+
return False
|
|
15
|
+
try:
|
|
16
|
+
import curses
|
|
17
|
+
curses.setupterm()
|
|
18
|
+
return curses.tigetnum("colors") > 0
|
|
19
|
+
except:
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
# Check if terminal supports emojis
|
|
23
|
+
def supports_emojis():
|
|
24
|
+
"""Check if the terminal supports emoji output"""
|
|
25
|
+
if sys.platform == 'win32':
|
|
26
|
+
return False
|
|
27
|
+
if not sys.stdout.isatty():
|
|
28
|
+
return False
|
|
29
|
+
try:
|
|
30
|
+
# Try to print an emoji and check if it's displayed correctly
|
|
31
|
+
print("\U0001F44D", end='', flush=True)
|
|
32
|
+
return True
|
|
33
|
+
except:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
# ANSI color codes for terminal output
|
|
37
|
+
class Colors:
|
|
38
|
+
if supports_color():
|
|
39
|
+
HEADER = '\033[95m'
|
|
40
|
+
BLUE = '\033[94m'
|
|
41
|
+
CYAN = '\033[96m'
|
|
42
|
+
GREEN = '\033[92m'
|
|
43
|
+
YELLOW = '\033[93m'
|
|
44
|
+
RED = '\033[91m'
|
|
45
|
+
ENDC = '\033[0m'
|
|
46
|
+
BOLD = '\033[1m'
|
|
47
|
+
UNDERLINE = '\033[4m'
|
|
48
|
+
else:
|
|
49
|
+
HEADER = BLUE = CYAN = GREEN = YELLOW = RED = ENDC = BOLD = UNDERLINE = ''
|
|
50
|
+
|
|
51
|
+
# Emoji symbols
|
|
52
|
+
class Symbols:
|
|
53
|
+
if supports_emojis():
|
|
54
|
+
CHECK = '✓'
|
|
55
|
+
BULLET = '•'
|
|
56
|
+
WARNING = '⚠'
|
|
57
|
+
else:
|
|
58
|
+
CHECK = '[OK]'
|
|
59
|
+
BULLET = '*'
|
|
60
|
+
WARNING = '[!]'
|
|
61
|
+
|
|
62
|
+
def print_header(text):
|
|
63
|
+
"""Print a formatted header message"""
|
|
64
|
+
print(f"\n{Colors.BOLD}{Colors.BLUE}== {text} =={Colors.ENDC}\n")
|
|
65
|
+
|
|
66
|
+
def print_step(text):
|
|
67
|
+
"""Print a formatted step message"""
|
|
68
|
+
print(f"{Colors.CYAN}{Symbols.BULLET} {text}...{Colors.ENDC}")
|
|
69
|
+
|
|
70
|
+
def print_success(text):
|
|
71
|
+
"""Print a formatted success message"""
|
|
72
|
+
print(f"{Colors.GREEN}{Symbols.CHECK} {text}{Colors.ENDC}")
|
|
73
|
+
|
|
74
|
+
def print_warning(text):
|
|
75
|
+
"""Print a formatted warning message"""
|
|
76
|
+
print(f"{Colors.YELLOW}{Symbols.WARNING} {text}{Colors.ENDC}")
|
|
77
|
+
|
|
78
|
+
def main(args=None):
|
|
79
|
+
# Set up command line argument parsing
|
|
80
|
+
parser = argparse.ArgumentParser(
|
|
81
|
+
description='Segment audio file and track beats using laplacian, segmentino, or backtrack methods',
|
|
82
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument('audiofile', help='Audio file to process')
|
|
85
|
+
parser.add_argument('--method', choices=['laplacian', 'segmentino', 'backtrack'],
|
|
86
|
+
default='backtrack', help='Segmentation method to use')
|
|
87
|
+
parser.add_argument('--max-clusters', type=int, default=None,
|
|
88
|
+
help='Maximum number of clusters to use for laplacian segmentation')
|
|
89
|
+
parser.add_argument('--beatsper', type=int, default=8,
|
|
90
|
+
help='Number of beats per bar')
|
|
91
|
+
parser.add_argument('--firstfull', type=int, default=0,
|
|
92
|
+
help='First full bar number')
|
|
93
|
+
parser.add_argument('--output', help='Output file path (default: input filename with .bri extension)')
|
|
94
|
+
parser.add_argument('--debug', action='store_true',
|
|
95
|
+
help='Enable debug mode for detailed processing information')
|
|
96
|
+
|
|
97
|
+
args = parser.parse_args()
|
|
98
|
+
|
|
99
|
+
# Set output file if not specified
|
|
100
|
+
if args.output is None:
|
|
101
|
+
basename = os.path.basename(args.audiofile)
|
|
102
|
+
stub, _ = os.path.splitext(basename)
|
|
103
|
+
output = stub + ".bri"
|
|
104
|
+
else:
|
|
105
|
+
output = args.output
|
|
106
|
+
|
|
107
|
+
if args.method == "laplacian" and args.max_clusters is None:
|
|
108
|
+
print_warning("max-clusters is not specified for laplacian segmentation, using default of 48")
|
|
109
|
+
args.max_clusters = 48
|
|
110
|
+
|
|
111
|
+
if args.max_clusters is not None and args.method != "laplacian":
|
|
112
|
+
print_warning("max-clusters is specified for non-laplacian segmentation, this will be ignored")
|
|
113
|
+
|
|
114
|
+
# Print processing information
|
|
115
|
+
print_header("Audio Segmentation and Beat Tracking")
|
|
116
|
+
print(f"{Colors.BOLD}Input:{Colors.ENDC}")
|
|
117
|
+
print(f" Audio file: {args.audiofile}")
|
|
118
|
+
print(f" Method: {args.method}")
|
|
119
|
+
print(f" Beats per bar: {args.beatsper}")
|
|
120
|
+
print(f" First full bar: {args.firstfull}")
|
|
121
|
+
print(f" Output file: {output}")
|
|
122
|
+
if args.debug:
|
|
123
|
+
print_warning("Debug mode enabled - detailed processing information will be shown")
|
|
124
|
+
|
|
125
|
+
# Process the audio file
|
|
126
|
+
print_step("Loading audio file")
|
|
127
|
+
s = rosabeats(args.audiofile, debug=args.debug)
|
|
128
|
+
print_success(f"Loaded {os.path.basename(args.audiofile)}")
|
|
129
|
+
|
|
130
|
+
print_step("Tracking beats")
|
|
131
|
+
s.track_beats(args.beatsper, args.firstfull)
|
|
132
|
+
print_success(f"Found {s.total_beats} beats in {s.total_bars} bars")
|
|
133
|
+
|
|
134
|
+
print_step(f"Segmenting with {args.method} method")
|
|
135
|
+
s.segment(method=args.method, max_clusters=args.max_clusters)
|
|
136
|
+
print_success(f"Identified {s.total_segments} segments")
|
|
137
|
+
|
|
138
|
+
print_step("Assigning beats and bars to segments")
|
|
139
|
+
s.segmentize_beats()
|
|
140
|
+
print_success("Completed beat and bar assignment")
|
|
141
|
+
|
|
142
|
+
# Write results to output file
|
|
143
|
+
print_step(f"Writing results to {output}")
|
|
144
|
+
with open(output, "w") as f:
|
|
145
|
+
# Write header information
|
|
146
|
+
f.write("##BEATS## This was segmented and tracked using librosa's beat tracker\n\n")
|
|
147
|
+
f.write(f"file {s.sourcefile}\n")
|
|
148
|
+
f.write(f"beats_bar {s.beatsperbar} {s.firstfullbar}\n")
|
|
149
|
+
f.write(f"# total beats = {s.total_beats}\n")
|
|
150
|
+
f.write(f"# total bars = {s.total_bars} (beats {s.firstfullbar}-{s.firstfullbar + (s.beatsperbar * s.total_bars)})\n")
|
|
151
|
+
|
|
152
|
+
# Process and write segment information
|
|
153
|
+
bars_defs = []
|
|
154
|
+
beats_defs = []
|
|
155
|
+
for idx, seg in enumerate(s.segments):
|
|
156
|
+
# Print segment information to console
|
|
157
|
+
print(f"\n{Colors.BOLD}Segment {idx}:{Colors.ENDC}")
|
|
158
|
+
print(f" Label: {seg['label']}")
|
|
159
|
+
print(f" Duration: {seg['duration']:.2f} seconds")
|
|
160
|
+
|
|
161
|
+
if len(seg["bars"]) >= 1:
|
|
162
|
+
print(f" Bars: {seg['bars'][0]}-{seg['bars'][-1]}")
|
|
163
|
+
bars_defs.append(f"def {seg['label']}_{idx} bars {seg['bars'][0]}-{seg['bars'][-1]}")
|
|
164
|
+
|
|
165
|
+
if len(seg["beats"]) >= 1:
|
|
166
|
+
print(f" Beats: {seg['beats'][0]}-{seg['beats'][-1]}")
|
|
167
|
+
beats_defs.append(f"def {seg['label']}_{idx}_beats beats {seg['beats'][0]}-{seg['beats'][-1]}\t# dur = {seg['duration']:.2f}s, beats {seg['beats'][0]}-{seg['beats'][-1]}")
|
|
168
|
+
|
|
169
|
+
# Write segment definitions to file
|
|
170
|
+
for d in bars_defs:
|
|
171
|
+
f.write(f"{d}\n")
|
|
172
|
+
f.write("\n")
|
|
173
|
+
for d in beats_defs:
|
|
174
|
+
f.write(f"{d}\n")
|
|
175
|
+
f.write("\n")
|
|
176
|
+
|
|
177
|
+
print_success(f"Results written to {output}")
|
|
178
|
+
print_header("Processing Complete")
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
main()
|