rosabeats 0.1.3__tar.gz → 0.2.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.
- {rosabeats-0.1.3/rosabeats.egg-info → rosabeats-0.2.0}/PKG-INFO +8 -30
- {rosabeats-0.1.3 → rosabeats-0.2.0}/README.md +3 -21
- {rosabeats-0.1.3 → rosabeats-0.2.0}/pyproject.toml +8 -5
- {rosabeats-0.1.3 → rosabeats-0.2.0}/rosabeats/__init__.py +1 -1
- rosabeats-0.2.0/rosabeats/__main__.py +59 -0
- {rosabeats-0.1.3 → rosabeats-0.2.0}/rosabeats/beatrecipe_processor.py +63 -46
- {rosabeats-0.1.3 → rosabeats-0.2.0}/rosabeats/beatswitch.py +29 -13
- rosabeats-0.2.0/rosabeats/downbeat.py +207 -0
- {rosabeats-0.1.3 → rosabeats-0.2.0}/rosabeats/rosabeats.py +575 -543
- rosabeats-0.2.0/rosabeats/rosabeats_shell.py +494 -0
- {rosabeats-0.1.3 → rosabeats-0.2.0}/rosabeats/segment_song.py +100 -31
- {rosabeats-0.1.3 → rosabeats-0.2.0/rosabeats.egg-info}/PKG-INFO +8 -30
- {rosabeats-0.1.3 → rosabeats-0.2.0}/rosabeats.egg-info/SOURCES.txt +9 -4
- {rosabeats-0.1.3 → rosabeats-0.2.0}/rosabeats.egg-info/requires.txt +3 -6
- {rosabeats-0.1.3 → rosabeats-0.2.0}/rosabeats.egg-info/top_level.txt +1 -1
- rosabeats-0.2.0/tests/__init__.py +1 -0
- rosabeats-0.2.0/tests/conftest.py +131 -0
- rosabeats-0.2.0/tests/test_beatrecipe_processor.py +193 -0
- rosabeats-0.2.0/tests/test_downbeat.py +149 -0
- rosabeats-0.2.0/tests/test_rosabeats.py +234 -0
- rosabeats-0.2.0/tests/test_segment_song.py +120 -0
- rosabeats-0.2.0/tests/test_shell.py +305 -0
- rosabeats-0.1.3/docs/beatrecipe_docs.txt +0 -80
- rosabeats-0.1.3/rosabeats/rosabeats_shell.py +0 -387
- rosabeats-0.1.3/scripts/reverse_beats_in_bars_rosa.py +0 -48
- rosabeats-0.1.3/scripts/shuffle_bars_rosa.py +0 -35
- rosabeats-0.1.3/scripts/shuffle_beats_rosa.py +0 -29
- {rosabeats-0.1.3 → rosabeats-0.2.0}/LICENSE.md +0 -0
- {rosabeats-0.1.3 → rosabeats-0.2.0}/rosabeats.egg-info/dependency_links.txt +0 -0
- {rosabeats-0.1.3 → rosabeats-0.2.0}/rosabeats.egg-info/entry_points.txt +0 -0
- {rosabeats-0.1.3 → rosabeats-0.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rosabeats
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Audio beat detection, segmentation, and remixing library using librosa
|
|
5
5
|
Author: John Fleming
|
|
6
6
|
License-Expression: ISC
|
|
@@ -8,15 +8,13 @@ Project-URL: Homepage, https://github.com/jbff/rosabeats
|
|
|
8
8
|
Project-URL: Repository, https://github.com/jbff/rosabeats
|
|
9
9
|
Keywords: audio,beat,detection,segmentation,remixing,librosa,music
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
13
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
14
|
Classifier: Operating System :: OS Independent
|
|
17
15
|
Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
|
|
18
16
|
Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion
|
|
19
|
-
Requires-Python: >=3.
|
|
17
|
+
Requires-Python: >=3.11
|
|
20
18
|
Description-Content-Type: text/markdown
|
|
21
19
|
License-File: LICENSE.md
|
|
22
20
|
Requires-Dist: librosa<1.0,>=0.11.0
|
|
@@ -27,11 +25,9 @@ Requires-Dist: scipy<2.0,>=1.15
|
|
|
27
25
|
Requires-Dist: scikit-learn<2.0,>=1.5
|
|
28
26
|
Provides-Extra: ffms2
|
|
29
27
|
Requires-Dist: ffms2; extra == "ffms2"
|
|
30
|
-
Provides-Extra:
|
|
31
|
-
Requires-Dist:
|
|
32
|
-
|
|
33
|
-
Requires-Dist: ffms2; extra == "all"
|
|
34
|
-
Requires-Dist: vamp; extra == "all"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
35
31
|
Dynamic: license-file
|
|
36
32
|
|
|
37
33
|
# rosabeats
|
|
@@ -48,9 +44,7 @@ I wrote most of this code in 2018-2019 and made some additions through 2021. I c
|
|
|
48
44
|
|
|
49
45
|
- **Beat Detection**: Automatically detect beats and tempo in audio files
|
|
50
46
|
- **Bar Analysis**: Group beats into bars with configurable beats-per-bar
|
|
51
|
-
- **Audio Segmentation**: Segment songs into structural parts using
|
|
52
|
-
- Laplacian segmentation (powered by librosa)
|
|
53
|
-
- Segmentino (using Vamp plugins)
|
|
47
|
+
- **Audio Segmentation**: Segment songs into structural parts using Laplacian spectral clustering
|
|
54
48
|
- **Audio Remixing**: Create remixes by manipulating beats and bars:
|
|
55
49
|
- Play individual beats or ranges of beats
|
|
56
50
|
- Play entire bars or ranges of bars
|
|
@@ -66,7 +60,7 @@ I wrote most of this code in 2018-2019 and made some additions through 2021. I c
|
|
|
66
60
|
|
|
67
61
|
### Prerequisites
|
|
68
62
|
|
|
69
|
-
- Python 3.
|
|
63
|
+
- Python 3.11+ (Tested most recently with Python 3.11.12 and 3.13.2)
|
|
70
64
|
- ffms2 libraries (On Fedora: `dnf install ffms2`; tested most recently with ffms-5.0)
|
|
71
65
|
|
|
72
66
|
### Installing from PyPI
|
|
@@ -105,32 +99,17 @@ pip install -e .
|
|
|
105
99
|
|
|
106
100
|
3. Installing optional dependencies:
|
|
107
101
|
|
|
108
|
-
rosabeats has optional dependencies that can be installed based on your needs:
|
|
109
|
-
|
|
110
102
|
```bash
|
|
111
|
-
# Install with Vamp plugin support
|
|
112
|
-
pip install .[vamp]
|
|
113
|
-
|
|
114
103
|
# Install with ffms2 support for additional audio formats
|
|
115
104
|
pip install .[ffms2]
|
|
116
|
-
|
|
117
|
-
# Install with all optional dependencies
|
|
118
|
-
pip install .[all]
|
|
119
105
|
```
|
|
120
106
|
|
|
121
107
|
This will install the package and its dependencies, and make the following command-line tools available:
|
|
122
108
|
- `beatrecipe-processor`: Process beat recipes
|
|
123
|
-
- `segment-song`: Segment songs
|
|
109
|
+
- `segment-song`: Segment songs and track beats
|
|
124
110
|
- `beatswitch`: Generate beat recipes with alternating patterns
|
|
125
111
|
- `rosabeats-shell`: Interactive shell for beat manipulation
|
|
126
112
|
|
|
127
|
-
### Optional: Vamp Plugins for Segmentino
|
|
128
|
-
|
|
129
|
-
If you want to use the Segmentino-based segmentation:
|
|
130
|
-
|
|
131
|
-
1. Download and install the Vamp plugin SDK
|
|
132
|
-
2. Install the Segmentino plugin
|
|
133
|
-
|
|
134
113
|
### Using rosabeats
|
|
135
114
|
|
|
136
115
|
The package can be imported in Python as:
|
|
@@ -153,6 +132,5 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE
|
|
|
153
132
|
## Acknowledgments
|
|
154
133
|
|
|
155
134
|
- [librosa](https://librosa.org/) for audio analysis
|
|
156
|
-
- [Vamp plugins](https://www.vamp-plugins.org/) for audio segmentation
|
|
157
135
|
- [Amen](https://github.com/algorithmic-music-exploration/amen) was the original inspiration
|
|
158
136
|
|
|
@@ -12,9 +12,7 @@ I wrote most of this code in 2018-2019 and made some additions through 2021. I c
|
|
|
12
12
|
|
|
13
13
|
- **Beat Detection**: Automatically detect beats and tempo in audio files
|
|
14
14
|
- **Bar Analysis**: Group beats into bars with configurable beats-per-bar
|
|
15
|
-
- **Audio Segmentation**: Segment songs into structural parts using
|
|
16
|
-
- Laplacian segmentation (powered by librosa)
|
|
17
|
-
- Segmentino (using Vamp plugins)
|
|
15
|
+
- **Audio Segmentation**: Segment songs into structural parts using Laplacian spectral clustering
|
|
18
16
|
- **Audio Remixing**: Create remixes by manipulating beats and bars:
|
|
19
17
|
- Play individual beats or ranges of beats
|
|
20
18
|
- Play entire bars or ranges of bars
|
|
@@ -30,7 +28,7 @@ I wrote most of this code in 2018-2019 and made some additions through 2021. I c
|
|
|
30
28
|
|
|
31
29
|
### Prerequisites
|
|
32
30
|
|
|
33
|
-
- Python 3.
|
|
31
|
+
- Python 3.11+ (Tested most recently with Python 3.11.12 and 3.13.2)
|
|
34
32
|
- ffms2 libraries (On Fedora: `dnf install ffms2`; tested most recently with ffms-5.0)
|
|
35
33
|
|
|
36
34
|
### Installing from PyPI
|
|
@@ -69,32 +67,17 @@ pip install -e .
|
|
|
69
67
|
|
|
70
68
|
3. Installing optional dependencies:
|
|
71
69
|
|
|
72
|
-
rosabeats has optional dependencies that can be installed based on your needs:
|
|
73
|
-
|
|
74
70
|
```bash
|
|
75
|
-
# Install with Vamp plugin support
|
|
76
|
-
pip install .[vamp]
|
|
77
|
-
|
|
78
71
|
# Install with ffms2 support for additional audio formats
|
|
79
72
|
pip install .[ffms2]
|
|
80
|
-
|
|
81
|
-
# Install with all optional dependencies
|
|
82
|
-
pip install .[all]
|
|
83
73
|
```
|
|
84
74
|
|
|
85
75
|
This will install the package and its dependencies, and make the following command-line tools available:
|
|
86
76
|
- `beatrecipe-processor`: Process beat recipes
|
|
87
|
-
- `segment-song`: Segment songs
|
|
77
|
+
- `segment-song`: Segment songs and track beats
|
|
88
78
|
- `beatswitch`: Generate beat recipes with alternating patterns
|
|
89
79
|
- `rosabeats-shell`: Interactive shell for beat manipulation
|
|
90
80
|
|
|
91
|
-
### Optional: Vamp Plugins for Segmentino
|
|
92
|
-
|
|
93
|
-
If you want to use the Segmentino-based segmentation:
|
|
94
|
-
|
|
95
|
-
1. Download and install the Vamp plugin SDK
|
|
96
|
-
2. Install the Segmentino plugin
|
|
97
|
-
|
|
98
81
|
### Using rosabeats
|
|
99
82
|
|
|
100
83
|
The package can be imported in Python as:
|
|
@@ -117,6 +100,5 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE
|
|
|
117
100
|
## Acknowledgments
|
|
118
101
|
|
|
119
102
|
- [librosa](https://librosa.org/) for audio analysis
|
|
120
|
-
- [Vamp plugins](https://www.vamp-plugins.org/) for audio segmentation
|
|
121
103
|
- [Amen](https://github.com/algorithmic-music-exploration/amen) was the original inspiration
|
|
122
104
|
|
|
@@ -8,15 +8,13 @@ dynamic = ["version"]
|
|
|
8
8
|
description = "Audio beat detection, segmentation, and remixing library using librosa"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "ISC"
|
|
11
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
12
|
authors = [
|
|
13
13
|
{ name = "John Fleming" }
|
|
14
14
|
]
|
|
15
15
|
keywords = ["audio", "beat", "detection", "segmentation", "remixing", "librosa", "music"]
|
|
16
16
|
classifiers = [
|
|
17
17
|
"Programming Language :: Python :: 3",
|
|
18
|
-
"Programming Language :: Python :: 3.9",
|
|
19
|
-
"Programming Language :: Python :: 3.10",
|
|
20
18
|
"Programming Language :: Python :: 3.11",
|
|
21
19
|
"Programming Language :: Python :: 3.12",
|
|
22
20
|
"Programming Language :: Python :: 3.13",
|
|
@@ -35,8 +33,7 @@ dependencies = [
|
|
|
35
33
|
|
|
36
34
|
[project.optional-dependencies]
|
|
37
35
|
ffms2 = ["ffms2"]
|
|
38
|
-
|
|
39
|
-
all = ["ffms2", "vamp"]
|
|
36
|
+
dev = ["pytest>=8.0", "pytest-cov>=4.0"]
|
|
40
37
|
|
|
41
38
|
[project.scripts]
|
|
42
39
|
beatrecipe-processor = "rosabeats.beatrecipe_processor:main"
|
|
@@ -56,3 +53,9 @@ where = ["."]
|
|
|
56
53
|
|
|
57
54
|
[tool.setuptools.package-data]
|
|
58
55
|
rosabeats = ["docs/*"]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
59
|
+
python_files = ["test_*.py"]
|
|
60
|
+
python_functions = ["test_*"]
|
|
61
|
+
addopts = "-v --tb=short"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Entry point for running rosabeats as a module.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python -m rosabeats [command]
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
shell Launch interactive shell (default)
|
|
10
|
+
segment Segment audio file and track beats
|
|
11
|
+
beatswitch Generate beat recipe with alternating patterns
|
|
12
|
+
recipe Process beat recipe files
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
python -m rosabeats # Launch interactive shell
|
|
16
|
+
python -m rosabeats shell # Same as above
|
|
17
|
+
python -m rosabeats segment song.wav # Segment audio file
|
|
18
|
+
python -m rosabeats beatswitch song.wav # Generate beat recipe
|
|
19
|
+
python -m rosabeats recipe file.br # Process beat recipe
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main():
|
|
26
|
+
"""Main entry point for python -m rosabeats."""
|
|
27
|
+
if len(sys.argv) < 2:
|
|
28
|
+
# Default to shell
|
|
29
|
+
from rosabeats.rosabeats_shell import main as shell_main
|
|
30
|
+
sys.exit(shell_main())
|
|
31
|
+
|
|
32
|
+
command = sys.argv[1]
|
|
33
|
+
|
|
34
|
+
# Remove the command from argv so subcommands see correct args
|
|
35
|
+
sys.argv = [f"rosabeats {command}"] + sys.argv[2:]
|
|
36
|
+
|
|
37
|
+
if command in ("shell", "sh"):
|
|
38
|
+
from rosabeats.rosabeats_shell import main as shell_main
|
|
39
|
+
sys.exit(shell_main())
|
|
40
|
+
elif command in ("segment", "seg"):
|
|
41
|
+
from rosabeats.segment_song import main as segment_main
|
|
42
|
+
sys.exit(segment_main() or 0)
|
|
43
|
+
elif command in ("beatswitch", "bs"):
|
|
44
|
+
from rosabeats.beatswitch import main as beatswitch_main
|
|
45
|
+
sys.exit(beatswitch_main() or 0)
|
|
46
|
+
elif command in ("recipe", "br"):
|
|
47
|
+
from rosabeats.beatrecipe_processor import main as recipe_main
|
|
48
|
+
sys.exit(recipe_main() or 0)
|
|
49
|
+
elif command in ("-h", "--help", "help"):
|
|
50
|
+
print(__doc__)
|
|
51
|
+
sys.exit(0)
|
|
52
|
+
else:
|
|
53
|
+
print(f"Unknown command: {command}")
|
|
54
|
+
print("Use 'python -m rosabeats --help' for usage information.")
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
main()
|
|
@@ -4,6 +4,7 @@ import re, sys, os.path, random, logging
|
|
|
4
4
|
|
|
5
5
|
from rosabeats import rosabeats
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
class beatrecipe_processor(rosabeats):
|
|
8
9
|
@staticmethod
|
|
9
10
|
def ilist(indices):
|
|
@@ -65,6 +66,13 @@ class beatrecipe_processor(rosabeats):
|
|
|
65
66
|
def parse_error(self, error_info):
|
|
66
67
|
self.log.error("%s" % error_info)
|
|
67
68
|
|
|
69
|
+
def parse_command(self, cmd):
|
|
70
|
+
"""Split a command line into (verb, args). args is a list of remaining tokens."""
|
|
71
|
+
parts = cmd.split()
|
|
72
|
+
if not parts:
|
|
73
|
+
return None, []
|
|
74
|
+
return parts[0], parts[1:]
|
|
75
|
+
|
|
68
76
|
def read_beatrecipe(self):
|
|
69
77
|
self.recipe_lines = list()
|
|
70
78
|
with open(self.recipe, "r") as f:
|
|
@@ -75,11 +83,18 @@ class beatrecipe_processor(rosabeats):
|
|
|
75
83
|
|
|
76
84
|
for cmd in lines:
|
|
77
85
|
self.recipe_lines.append(cmd)
|
|
86
|
+
# Load macro definitions at read time so macros are available after init
|
|
87
|
+
if cmd.strip().startswith("def "):
|
|
88
|
+
try:
|
|
89
|
+
_, rest = cmd.split(maxsplit=1)
|
|
90
|
+
name, value = rest.split(maxsplit=1)
|
|
91
|
+
self.define_macro(name, value)
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
78
94
|
|
|
79
95
|
def define_macro(self, name, value):
|
|
80
96
|
if self.macros.get(name) is not None:
|
|
81
|
-
self.
|
|
82
|
-
return False
|
|
97
|
+
self.log.debug("macro %s redefined" % name)
|
|
83
98
|
self.macros[name] = value
|
|
84
99
|
return True
|
|
85
100
|
|
|
@@ -114,7 +129,7 @@ class beatrecipe_processor(rosabeats):
|
|
|
114
129
|
return None
|
|
115
130
|
|
|
116
131
|
# skip lines that start with hash
|
|
117
|
-
if line.startswith(
|
|
132
|
+
if line.startswith("#"):
|
|
118
133
|
return None
|
|
119
134
|
|
|
120
135
|
# remove same line comments but process rest of line
|
|
@@ -127,7 +142,7 @@ class beatrecipe_processor(rosabeats):
|
|
|
127
142
|
|
|
128
143
|
# split multiple commands separated by semicolons
|
|
129
144
|
lines = []
|
|
130
|
-
for l in line.split(
|
|
145
|
+
for l in line.split(";"):
|
|
131
146
|
lines.append(l.strip())
|
|
132
147
|
|
|
133
148
|
return lines
|
|
@@ -339,9 +354,7 @@ class beatrecipe_processor(rosabeats):
|
|
|
339
354
|
return True
|
|
340
355
|
else:
|
|
341
356
|
raise e
|
|
342
|
-
print(
|
|
343
|
-
"[*] (def seg: %s = %s)" % (name, value)
|
|
344
|
-
)
|
|
357
|
+
print("[*] (def seg: %s = %s)" % (name, value))
|
|
345
358
|
|
|
346
359
|
self.define_macro(name, value)
|
|
347
360
|
print("defined %s => %s" % (name, value))
|
|
@@ -357,7 +370,6 @@ class beatrecipe_processor(rosabeats):
|
|
|
357
370
|
print("%15s %15s" % (name, value))
|
|
358
371
|
|
|
359
372
|
elif self.interactive and verb == "ls":
|
|
360
|
-
|
|
361
373
|
print(", ".join(list(self.macros.keys())))
|
|
362
374
|
|
|
363
375
|
elif self.interactive and verb == "quit":
|
|
@@ -392,12 +404,11 @@ class beatrecipe_processor(rosabeats):
|
|
|
392
404
|
def process_interactive(self):
|
|
393
405
|
KeepProcessing = True
|
|
394
406
|
while KeepProcessing:
|
|
395
|
-
|
|
396
407
|
print("")
|
|
397
408
|
print("enter beatrecipe command => ", end="", flush=True)
|
|
398
409
|
|
|
399
410
|
lines = self.preprocess(input())
|
|
400
|
-
|
|
411
|
+
|
|
401
412
|
for cmd in lines:
|
|
402
413
|
print(cmd)
|
|
403
414
|
KeepProcessing = self.execute_command(cmd)
|
|
@@ -413,8 +424,8 @@ class beatrecipe_processor(rosabeats):
|
|
|
413
424
|
except Exception:
|
|
414
425
|
verb = line
|
|
415
426
|
args = ""
|
|
416
|
-
# self.parse_error("could not split line into verb and args")
|
|
417
|
-
# sys.exit(1)
|
|
427
|
+
# self.parse_error("could not split line into verb and args")
|
|
428
|
+
# sys.exit(1)
|
|
418
429
|
|
|
419
430
|
if verb == "file":
|
|
420
431
|
filename = str(args)
|
|
@@ -433,10 +444,12 @@ class beatrecipe_processor(rosabeats):
|
|
|
433
444
|
# if we know our audio source, load it, analyze it, and init outputs
|
|
434
445
|
if self.sourcefile:
|
|
435
446
|
print("One moment as we track your beats for you, madame...")
|
|
436
|
-
self.track_beats(beatsper=per,
|
|
447
|
+
self.track_beats(beatsper=per, downbeat=first)
|
|
437
448
|
self.init_outputs()
|
|
438
449
|
else:
|
|
439
|
-
self.parse_error(
|
|
450
|
+
self.parse_error(
|
|
451
|
+
"a file directive must come before a beats_bar directive"
|
|
452
|
+
)
|
|
440
453
|
sys.exit(1)
|
|
441
454
|
|
|
442
455
|
elif verb == "def":
|
|
@@ -457,46 +470,49 @@ class beatrecipe_processor(rosabeats):
|
|
|
457
470
|
|
|
458
471
|
|
|
459
472
|
def main(args=None):
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
473
|
+
import argparse
|
|
474
|
+
|
|
475
|
+
parser = argparse.ArgumentParser(
|
|
476
|
+
description='Process beat recipe files (.br) to create audio remixes',
|
|
477
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
478
|
+
epilog='''
|
|
479
|
+
Examples:
|
|
480
|
+
beatrecipe-processor -p recipe.br Play the recipe
|
|
481
|
+
beatrecipe-processor -s recipe.br Save output to WAV
|
|
482
|
+
beatrecipe-processor -p -s recipe.br Play and save
|
|
483
|
+
beatrecipe-processor -b recipe.br Write beats output file
|
|
484
|
+
'''
|
|
485
|
+
)
|
|
486
|
+
parser.add_argument('recipes', nargs='+', metavar='RECIPE',
|
|
487
|
+
help='Beat recipe files (.br) to process')
|
|
488
|
+
parser.add_argument('-p', '--play', action='store_true',
|
|
489
|
+
help='Enable audio playback')
|
|
490
|
+
parser.add_argument('-s', '--save', action='store_true',
|
|
491
|
+
help='Save output to WAV file')
|
|
492
|
+
parser.add_argument('-b', '--beats', action='store_true',
|
|
493
|
+
help='Write beats output file')
|
|
494
|
+
parser.add_argument('-d', '--debug', action='store_true',
|
|
495
|
+
help='Enable debug mode')
|
|
496
|
+
|
|
497
|
+
parsed = parser.parse_args(args)
|
|
498
|
+
|
|
499
|
+
output_play = parsed.play
|
|
500
|
+
output_save = parsed.save
|
|
501
|
+
output_beats = parsed.beats
|
|
502
|
+
loglevel = logging.DEBUG if parsed.debug else logging.INFO
|
|
503
|
+
recipes = parsed.recipes
|
|
489
504
|
|
|
490
505
|
if not (output_play or output_save or output_beats):
|
|
491
|
-
|
|
492
|
-
sys.exit(1)
|
|
506
|
+
parser.error("must specify one or more of --play, --save, or --beats for output")
|
|
493
507
|
|
|
494
508
|
for recipe in recipes:
|
|
495
509
|
print("processing %s..." % recipe, flush=True)
|
|
496
510
|
|
|
497
511
|
stub, ext = os.path.splitext(os.path.basename(recipe))
|
|
498
512
|
|
|
499
|
-
p = beatrecipe_processor(
|
|
513
|
+
p = beatrecipe_processor(
|
|
514
|
+
recipe, interactive=False, loglevel=loglevel, debug=True
|
|
515
|
+
)
|
|
500
516
|
|
|
501
517
|
if output_play:
|
|
502
518
|
p.enable_output_play()
|
|
@@ -519,5 +535,6 @@ def main(args=None):
|
|
|
519
535
|
print("error processing %s: %s" % (recipe, e), flush=True)
|
|
520
536
|
raise e
|
|
521
537
|
|
|
538
|
+
|
|
522
539
|
if __name__ == "__main__":
|
|
523
540
|
main()
|
|
@@ -21,16 +21,16 @@ class BeatSwitcher(rosabeats.rosabeats):
|
|
|
21
21
|
Attributes:
|
|
22
22
|
infile (str): Path to input audio file
|
|
23
23
|
outfile (str): Path to output beat recipe file (.br)
|
|
24
|
-
|
|
24
|
+
downbeat (int): Beat index of first downbeat
|
|
25
25
|
fmin (int): Minimum number of forward beats
|
|
26
26
|
fmax (int): Maximum number of forward beats
|
|
27
27
|
bmin (int): Minimum number of backward beats
|
|
28
28
|
bmax (int): Maximum number of backward beats
|
|
29
29
|
"""
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
def __init__(self, infile, debug=False):
|
|
32
32
|
"""Initialize the BeatSwitcher.
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
Args:
|
|
35
35
|
infile (str): Path to input audio file
|
|
36
36
|
debug (bool, optional): Enable debug mode
|
|
@@ -43,26 +43,26 @@ class BeatSwitcher(rosabeats.rosabeats):
|
|
|
43
43
|
|
|
44
44
|
# Initialize instance variables
|
|
45
45
|
self.outfile = None
|
|
46
|
-
self.
|
|
46
|
+
self._downbeat_arg = None
|
|
47
47
|
self.fmin = None
|
|
48
48
|
self.fmax = None
|
|
49
49
|
self.bmin = None
|
|
50
50
|
self.bmax = None
|
|
51
51
|
|
|
52
|
-
def setup(self, outfile,
|
|
52
|
+
def setup(self, outfile, downbeat=None):
|
|
53
53
|
"""Set up the beat recipe configuration.
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
Args:
|
|
56
56
|
outfile (str): Path to output beat recipe file (.br)
|
|
57
|
-
|
|
57
|
+
downbeat (int, optional): Beat index of first downbeat (auto-detected if None)
|
|
58
58
|
"""
|
|
59
59
|
# Configure output file
|
|
60
60
|
self.outfile = outfile
|
|
61
61
|
self.enable_output_beats(self.outfile)
|
|
62
62
|
|
|
63
63
|
# Configure beat tracking
|
|
64
|
-
self.
|
|
65
|
-
self.track_beats(
|
|
64
|
+
self._downbeat_arg = downbeat
|
|
65
|
+
self.track_beats(downbeat=downbeat)
|
|
66
66
|
|
|
67
67
|
# Disable WAV output and playback
|
|
68
68
|
self.disable_output_save()
|
|
@@ -90,7 +90,7 @@ class BeatSwitcher(rosabeats.rosabeats):
|
|
|
90
90
|
self.gen_beat_samples()
|
|
91
91
|
|
|
92
92
|
# Start from first full bar
|
|
93
|
-
curr_beat = self.
|
|
93
|
+
curr_beat = self.downbeat
|
|
94
94
|
song_over = False
|
|
95
95
|
direction = "r" # Start with reverse direction
|
|
96
96
|
|
|
@@ -215,8 +215,12 @@ def parse_args():
|
|
|
215
215
|
help="Maximum number of backward beats"
|
|
216
216
|
)
|
|
217
217
|
parser.add_argument(
|
|
218
|
-
"--
|
|
219
|
-
help="
|
|
218
|
+
"--downbeat", type=int, default=None,
|
|
219
|
+
help="Beat index of first downbeat (default: 0)"
|
|
220
|
+
)
|
|
221
|
+
parser.add_argument(
|
|
222
|
+
"--auto-downbeat", action="store_true",
|
|
223
|
+
help="Auto-detect downbeat using DBN"
|
|
220
224
|
)
|
|
221
225
|
parser.add_argument(
|
|
222
226
|
"--debug", action="store_true",
|
|
@@ -247,7 +251,19 @@ def main():
|
|
|
247
251
|
|
|
248
252
|
# Create and run beat switcher
|
|
249
253
|
bs = BeatSwitcher(args.input_file, debug=args.debug)
|
|
250
|
-
|
|
254
|
+
|
|
255
|
+
# Determine downbeat value
|
|
256
|
+
if args.downbeat is not None:
|
|
257
|
+
downbeat = args.downbeat
|
|
258
|
+
elif args.auto_downbeat:
|
|
259
|
+
# Auto-detect using DBN approach
|
|
260
|
+
bs.track_beats(downbeat=0)
|
|
261
|
+
print("Auto-detecting downbeat using DBN...")
|
|
262
|
+
downbeat = bs.detect_downbeat_dbn(bs.beatsperbar)
|
|
263
|
+
else:
|
|
264
|
+
downbeat = 0
|
|
265
|
+
|
|
266
|
+
bs.setup(output_file, downbeat)
|
|
251
267
|
bs.set_parameters(args.fmin, args.fmax, args.bmin, args.bmax)
|
|
252
268
|
bs.run()
|
|
253
269
|
|