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.
tests/test_shell.py ADDED
@@ -0,0 +1,305 @@
1
+ """Tests for rosabeats_shell module."""
2
+
3
+ import pytest
4
+ from io import StringIO
5
+ import sys
6
+
7
+ from rosabeats.rosabeats_shell import (
8
+ parse_range,
9
+ parse_int,
10
+ parse_float,
11
+ rosabeats_shell,
12
+ )
13
+
14
+
15
+ class TestParseRange:
16
+ """Tests for parse_range function."""
17
+
18
+ def test_single_number(self):
19
+ """Should parse single number as single-element list."""
20
+ result = parse_range("5")
21
+ assert result == [5]
22
+
23
+ def test_ascending_range(self):
24
+ """Should parse ascending range correctly."""
25
+ result = parse_range("3-7")
26
+ assert result == [3, 4, 5, 6, 7]
27
+
28
+ def test_descending_range(self):
29
+ """Should parse descending range correctly."""
30
+ result = parse_range("7-3")
31
+ assert result == [7, 6, 5, 4, 3]
32
+
33
+ def test_same_number_range(self):
34
+ """Should handle range where start equals end."""
35
+ result = parse_range("5-5")
36
+ assert result == [5]
37
+
38
+ def test_whitespace_handling(self):
39
+ """Should handle leading/trailing whitespace."""
40
+ result = parse_range(" 3-5 ")
41
+ assert result == [3, 4, 5]
42
+
43
+ def test_empty_string(self):
44
+ """Should return None for empty string."""
45
+ result = parse_range("")
46
+ assert result is None
47
+
48
+ def test_invalid_format(self, capsys):
49
+ """Should return None and print error for invalid format."""
50
+ result = parse_range("abc")
51
+ assert result is None
52
+ captured = capsys.readouterr()
53
+ assert "invalid range" in captured.out
54
+
55
+ def test_negative_numbers(self):
56
+ """Should handle negative numbers."""
57
+ result = parse_range("-3-2")
58
+ assert result == [-3, -2, -1, 0, 1, 2]
59
+
60
+
61
+ class TestParseInt:
62
+ """Tests for parse_int function."""
63
+
64
+ def test_valid_integer(self):
65
+ """Should parse valid integer string."""
66
+ assert parse_int("42") == 42
67
+
68
+ def test_negative_integer(self):
69
+ """Should parse negative integer."""
70
+ assert parse_int("-5") == -5
71
+
72
+ def test_whitespace(self):
73
+ """Should handle whitespace."""
74
+ assert parse_int(" 10 ") == 10
75
+
76
+ def test_invalid_integer(self, capsys):
77
+ """Should return None for invalid integer."""
78
+ result = parse_int("abc")
79
+ assert result is None
80
+ captured = capsys.readouterr()
81
+ assert "must be an integer" in captured.out
82
+
83
+ def test_float_string(self, capsys):
84
+ """Should return None for float string."""
85
+ result = parse_int("3.14")
86
+ assert result is None
87
+
88
+ def test_custom_name(self, capsys):
89
+ """Should use custom name in error message."""
90
+ parse_int("abc", "beat_count")
91
+ captured = capsys.readouterr()
92
+ assert "beat_count must be an integer" in captured.out
93
+
94
+
95
+ class TestParseFloat:
96
+ """Tests for parse_float function."""
97
+
98
+ def test_valid_float(self):
99
+ """Should parse valid float string."""
100
+ assert parse_float("3.14") == pytest.approx(3.14)
101
+
102
+ def test_integer_as_float(self):
103
+ """Should parse integer string as float."""
104
+ assert parse_float("42") == 42.0
105
+
106
+ def test_negative_float(self):
107
+ """Should parse negative float."""
108
+ assert parse_float("-2.5") == -2.5
109
+
110
+ def test_whitespace(self):
111
+ """Should handle whitespace."""
112
+ assert parse_float(" 1.5 ") == 1.5
113
+
114
+ def test_invalid_float(self, capsys):
115
+ """Should return None for invalid float."""
116
+ result = parse_float("abc")
117
+ assert result is None
118
+ captured = capsys.readouterr()
119
+ assert "must be a number" in captured.out
120
+
121
+
122
+ class TestRosabeatsShell:
123
+ """Tests for rosabeats_shell class."""
124
+
125
+ @pytest.fixture
126
+ def shell(self):
127
+ """Create a shell instance for testing."""
128
+ s = rosabeats_shell()
129
+ # Don't enable audio output for tests
130
+ s.output_play = False
131
+ s.output_save = False
132
+ s.output_beats = False
133
+ return s
134
+
135
+ def test_init(self, shell):
136
+ """Shell should initialize with empty macros."""
137
+ assert shell.macros == {}
138
+
139
+ def test_define_macro(self, shell):
140
+ """Should define a macro."""
141
+ shell.define_macro("test", "beats 0-7")
142
+ assert "test" in shell.macros
143
+ assert shell.macros["test"] == "beats 0-7"
144
+
145
+ def test_redefine_macro(self, shell, capsys):
146
+ """Should allow redefining a macro."""
147
+ shell.define_macro("test", "beats 0-7")
148
+ shell.define_macro("test", "bars 0-3")
149
+ assert shell.macros["test"] == "bars 0-3"
150
+ captured = capsys.readouterr()
151
+ assert "redefining" in captured.out
152
+
153
+ def test_do_def_basic(self, shell, capsys):
154
+ """do_def should define a macro from command."""
155
+ shell.do_def("intro beats 0-15")
156
+ assert "intro" in shell.macros
157
+ assert shell.macros["intro"] == "beats 0-15"
158
+
159
+ def test_do_def_strips_comments(self, shell):
160
+ """do_def should strip inline comments."""
161
+ shell.do_def("intro beats 0-15 # this is the intro")
162
+ assert shell.macros["intro"] == "beats 0-15"
163
+
164
+ def test_do_def_missing_args(self, shell, capsys):
165
+ """do_def should show usage if missing arguments."""
166
+ shell.do_def("onlyname")
167
+ captured = capsys.readouterr()
168
+ assert "usage:" in captured.out
169
+
170
+ def test_do_ls_empty(self, shell, capsys):
171
+ """do_ls should show message when no macros defined."""
172
+ shell.do_ls("")
173
+ captured = capsys.readouterr()
174
+ assert "no macros defined" in captured.out
175
+
176
+ def test_do_ls_with_macros(self, shell, capsys):
177
+ """do_ls should list macro names."""
178
+ shell.define_macro("intro", "beats 0-7")
179
+ shell.define_macro("verse", "bars 0-3")
180
+ shell.do_ls("")
181
+ captured = capsys.readouterr()
182
+ assert "intro" in captured.out
183
+ assert "verse" in captured.out
184
+
185
+ def test_do_lsdef_shows_definitions(self, shell, capsys):
186
+ """do_lsdef should show macro names and definitions."""
187
+ shell.define_macro("intro", "beats 0-7")
188
+ shell.do_lsdef("")
189
+ captured = capsys.readouterr()
190
+ assert "intro" in captured.out
191
+ assert "beats 0-7" in captured.out
192
+
193
+ def test_do_file_sets_sourcefile(self, shell, temp_audio_file):
194
+ """do_file should set the source file."""
195
+ shell.do_file(temp_audio_file)
196
+ assert shell.sourcefile is not None
197
+ assert temp_audio_file in shell.sourcefile
198
+
199
+ def test_do_file_missing_arg(self, shell, capsys):
200
+ """do_file should show usage if no argument."""
201
+ shell.do_file("")
202
+ captured = capsys.readouterr()
203
+ assert "usage:" in captured.out
204
+
205
+ def test_do_beats_bar_missing_file(self, shell, capsys):
206
+ """do_beats_bar should error if no file loaded."""
207
+ shell.do_beats_bar("4 0")
208
+ captured = capsys.readouterr()
209
+ assert "load a file first" in captured.out
210
+
211
+ def test_do_quit_returns_true(self, shell):
212
+ """do_quit should return True to exit cmdloop."""
213
+ # Mock shutdown to avoid errors
214
+ shell.shutdown = lambda: None
215
+ result = shell.do_quit("")
216
+ assert result is True
217
+
218
+ def test_default_unknown_command(self, shell, capsys):
219
+ """default should print error for unknown command."""
220
+ shell.default("unknowncommand")
221
+ captured = capsys.readouterr()
222
+ assert "unknown command or macro" in captured.out
223
+
224
+ def test_default_plays_macro(self, shell, capsys):
225
+ """default should play a macro if name matches."""
226
+ shell.define_macro("test", "ls") # ls is a safe command
227
+ shell.default("test")
228
+ captured = capsys.readouterr()
229
+ # Should attempt to play the macro
230
+ assert "test" in captured.out
231
+
232
+ def test_emptyline_does_nothing(self, shell, capsys):
233
+ """emptyline should not produce output or execute anything."""
234
+ shell.emptyline()
235
+ captured = capsys.readouterr()
236
+ assert captured.out == ""
237
+
238
+
239
+ class TestShellLoadRecipeFile:
240
+ """Tests for load_recipe_file functionality."""
241
+
242
+ @pytest.fixture
243
+ def shell(self):
244
+ """Create a shell instance for testing."""
245
+ s = rosabeats_shell()
246
+ s.output_play = False
247
+ s.output_save = False
248
+ s.output_beats = False
249
+ return s
250
+
251
+ def test_load_recipe_file_defines_macros(self, shell, tmp_path, sample_bri_content):
252
+ """load_recipe_file should define macros from file."""
253
+ filepath = tmp_path / "test.bri"
254
+ filepath.write_text(sample_bri_content)
255
+
256
+ # Mock setfile to avoid actual file loading
257
+ shell.setfile = lambda x: setattr(shell, 'sourcefile', x)
258
+ # Mock track_beats
259
+ shell.track_beats = lambda **kwargs: None
260
+ shell.total_beats = 100
261
+ shell.total_bars = 24
262
+ shell.beatsperbar = 4
263
+ shell.downbeat = 0
264
+ shell.init_outputs = lambda: None
265
+
266
+ shell.load_recipe_file(str(filepath))
267
+
268
+ # Check that macros were defined
269
+ assert "A" in shell.macros
270
+ assert "B" in shell.macros
271
+ assert "A2" in shell.macros
272
+ assert "A_beats" in shell.macros
273
+
274
+ def test_load_recipe_file_strips_comments(self, shell, tmp_path):
275
+ """load_recipe_file should handle inline comments in defs."""
276
+ content = """def test beats 0-7 # inline comment
277
+ """
278
+ filepath = tmp_path / "test.br"
279
+ filepath.write_text(content)
280
+
281
+ shell.load_recipe_file(str(filepath))
282
+
283
+ assert "test" in shell.macros
284
+ assert shell.macros["test"] == "beats 0-7"
285
+
286
+ def test_load_recipe_file_not_found(self, shell, capsys):
287
+ """load_recipe_file should handle missing file gracefully."""
288
+ result = shell.load_recipe_file("/nonexistent/path.bri")
289
+ assert result is False
290
+ captured = capsys.readouterr()
291
+ assert "not found" in captured.out
292
+
293
+ def test_load_recipe_file_skips_comments(self, shell, tmp_path):
294
+ """load_recipe_file should skip comment lines."""
295
+ content = """# This is a comment
296
+ def test beats 0-7
297
+ # Another comment
298
+ """
299
+ filepath = tmp_path / "test.br"
300
+ filepath.write_text(content)
301
+
302
+ shell.load_recipe_file(str(filepath))
303
+
304
+ assert "test" in shell.macros
305
+ assert len(shell.macros) == 1 # Only one macro defined
docs/beatrecipe_docs.txt DELETED
@@ -1,80 +0,0 @@
1
- every beat recipe must contain these two lines, at the top:
2
- file <filename.wav>
3
- beats_bar # #
4
-
5
- the two arguments to beats_bar specify the number of beats pe bar, and the absolute beat
6
- number in the file that starts the first bar (this is often 0, first beat = first bar,
7
- but is also often not, depending on track and file characteristics; using control_beats.py
8
- to experiment is best way to discover if this should be non-zero for a given track)
9
-
10
- These two lines are necessary to specify source and in order to calculate where bars are
11
- in the file. After them, everything else is optional and can occur as many or few times as
12
- you like, in any order
13
-
14
- beats #
15
- bars #
16
- beats #-#
17
- bars #-#
18
-
19
- These are the simplest. You can play one or more beats, or one or more bars, by specifying
20
- the beats or bars you wish to play. All of these are valid syntax:
21
-
22
- beats 3
23
- beats 3-3
24
- beats 3-8
25
- beats 8-3
26
- bars 4
27
- bars 4-4
28
- bars 4-12
29
- bars 12-4
30
-
31
- If the first and last # specified are the same, it will be like specifying a single
32
- beat/bar. If the first # specified is greater than the last, the beats/bars will be
33
- played in reverse order, i.e. bars 4-1 will play bar 4, bar 3, bar 2, then bar 1.
34
-
35
- rest #
36
-
37
- Will insert silence. The argument specifies number of beats of silence. "rest 1" will
38
- insert 1 beat of silence. Argument is floating point, so you can add fractional beats
39
- of silence, e.g. "rest 0.5" will add half a beat of silence.
40
-
41
- beats_shuf #
42
- beats_shuf #-#
43
- bars_shuf #
44
- bars_shuf #-#
45
-
46
- These two act much like beats and bars covered previously, except instead of playing
47
- the specified beats or bars in the sequence specified, they will be randomly shuffled
48
- before being played. If only a single beat or bar is given after these specifiers,
49
- it will be the same as using "beats" or "bars" as shuffling a single item will always
50
- result in the output of that one item (i.e. "bars_shuf 8" is same as "bars 8").
51
-
52
-
53
- bars_rev #
54
- bars_rev #-#
55
-
56
- This is like bars, except it will play the beats in the bar in reverse order. That is,
57
- if "bars 0" played beats 0, 1, 2, 3, 4, 5, 6, and 7, "bars_rev 0" would play beats
58
- 7, 6, 5, 4, 3, 2, 1, and 0.
59
-
60
- beat_div # # #
61
- This will divide a beat into equal segments and play a certain number of them. The
62
- first argument is the beat #. The second argument is how many pieces to divide the beat
63
- into. THe third argument is how many times to play that subdivided beat. For example,
64
- "beat_div 540 2 2" will play the first half of beat 540 twice; "beat_div 300 3 12"
65
- will play the first third of beat 300 12 times (this will sound like 4 beats of triplets).
66
-
67
- def name bars #
68
- def name bars #-#
69
- def name beats #
70
- def name beats #-#
71
-
72
- This defines a segment that can later be played with the play command (below). Right now
73
- it can only handle beats and bars specifications (per above) but the plan is to expand it
74
- to represent any valid command.
75
-
76
- play name
77
-
78
- This will play any defined segment defined with "def" (above). If previously you had said
79
- "def seg1 bars 0-7" later when you say "play seg1" it will actually play bars 0-7.
80
- 1
@@ -1,16 +0,0 @@
1
- docs/beatrecipe_docs.txt,sha256=2WOPDt_8Av27vKV5Sq2yYK0PhByJH4nU6JS6rnlD4PM,3355
2
- rosabeats/__init__.py,sha256=egpPGyTlEcFz8g4Fxlmzx3R2ntFXoVUc0ttzFOyPOFs,841
3
- rosabeats/beatrecipe_processor.py,sha256=fAMJugNVfYvEza2U3C4s488d9KT4LejvglAlBBTBjJk,16230
4
- rosabeats/beatswitch.py,sha256=VOO00lHM8TYrPHBL-a0SkJ5jvFtSUq6LpyID3WhZfZ8,8483
5
- rosabeats/rosabeats.py,sha256=BHYD01biiw3z-vtoGGs9gK-ZURk9velt6Or0_-Qom1c,36264
6
- rosabeats/rosabeats_shell.py,sha256=Giq1sdHw96TOK86ze5QUfnOuyKsFzddfWP5fTztJt4w,10718
7
- rosabeats/segment_song.py,sha256=VJ4poSXVfXZGFLkn8HP92M3bCjk-wMxeumBwAh2svec,6703
8
- rosabeats-0.1.3.dist-info/licenses/LICENSE.md,sha256=aPJeloE50MoONg47hPe8CtvglnNcGO3tsS-S5os4WiA,771
9
- scripts/reverse_beats_in_bars_rosa.py,sha256=g1fRRTTko7BrDnmGQOhIq_rMsVYQznUwgTolRSXFDpI,1167
10
- scripts/shuffle_bars_rosa.py,sha256=m3zLkQ8A0tgApeVUni6calh6xwHLwzJlkG_qdbyJ3bU,799
11
- scripts/shuffle_beats_rosa.py,sha256=USaHpWgv7B8rQKVAOXqoia91FLBYyn1N03dIQOcpyBM,683
12
- rosabeats-0.1.3.dist-info/METADATA,sha256=L8ztM95XsgvMYKD5Ae-Zevize47RmoHpmkzQzDMIe5U,6987
13
- rosabeats-0.1.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
- rosabeats-0.1.3.dist-info/entry_points.txt,sha256=4psFMIAY5kogJyT8RMdFeTpAnRHmZpaFPnlwI3tntuk,208
15
- rosabeats-0.1.3.dist-info/top_level.txt,sha256=XbOrAF6RZpVfzXdkX9tFRtJFB3RUGXodl7RzoYIfz7A,23
16
- rosabeats-0.1.3.dist-info/RECORD,,
@@ -1,3 +0,0 @@
1
- docs
2
- rosabeats
3
- scripts
@@ -1,48 +0,0 @@
1
- #!/usr/bin/env python
2
- # shuffle bars in a song with rosabeats
3
-
4
- import sys
5
-
6
- from rosabeats import rosabeats
7
-
8
- if __name__ == "__main__":
9
- try:
10
- infile = str(sys.argv[1])
11
- except:
12
- print("must specify input and output file", flush=True)
13
- sys.exit(1)
14
- try:
15
- outfile = str(sys.argv[2])
16
- except:
17
- print("must specify input and output file", flush=True)
18
- sys.exit(1)
19
- try:
20
- first_bar = int(sys.argv[3])
21
- except:
22
- first_bar = 0
23
- try:
24
- beats_per = int(sys.argv[4])
25
- except:
26
- beats_per = 8
27
-
28
- song = rosabeats(infile, debug=True)
29
- song.divide_bars()
30
- song.track_beats()
31
-
32
- bar_count = int(song.total_beats / beats_per)
33
-
34
- barlist = [x for x in range(bar_count + 1)]
35
-
36
- song.enable_output_play()
37
- song.setup_playback()
38
-
39
- song.enable_output_save(outfile)
40
- song.reset_remix()
41
-
42
- song.play_beats([x for x in range(first_bar)])
43
- for count, bar in enumerate(barlist):
44
- first_b = (count * 8) + first_bar
45
- beatlist = [x for x in range(first_b, first_b + beats_per)]
46
- beatlist.reverse()
47
- song.play_beats(beatlist)
48
- song.save_remix()
@@ -1,35 +0,0 @@
1
- #!/usr/bin/env python
2
- # shuffle bars in a song with rosabeats
3
-
4
- import sys, random
5
- from rosabeats import rosabeats
6
-
7
- if __name__ == "__main__":
8
- if len(sys.argv) < 2:
9
- print("require one argument that is a file containing music", flush=True)
10
- sys.exit(1)
11
-
12
- infile = sys.argv[1]
13
- outfile = infile[0:-4] + "_barshuf.wav"
14
-
15
- song = rosabeats(infile, debug=True)
16
- song.divide_bars(8, 2)
17
- song.track_beats()
18
-
19
- bar_count = int(song.total_beats / 8)
20
-
21
- barlist = [x for x in range(1, bar_count)]
22
-
23
- random.shuffle(barlist)
24
-
25
- song.enable_output_play()
26
- song.setup_playback()
27
-
28
- song.enable_output_save(outfile)
29
- song.reset_remix()
30
-
31
- song.play_beats([0, 1])
32
- song.play_bar(0)
33
- song.play_bars(barlist)
34
- song.play_bar(bar_count)
35
- song.save_remix()
@@ -1,29 +0,0 @@
1
- #!/usr/bin/env python
2
- # shuffle beats in a song with rosabeats
3
-
4
- import sys, random
5
- from rosabeats import rosabeats
6
-
7
- if __name__ == "__main__":
8
- if len(sys.argv) < 2:
9
- print("require one argument that is a file containing music", flush=True)
10
- sys.exit(1)
11
-
12
- infile = sys.argv[1]
13
- outfile = infile[0:-4] + "_shuf.wav"
14
-
15
- song = rosabeats(infile, debug=True)
16
- song.divide_bars(8, 2)
17
- song.track_beats()
18
-
19
- beatlist = [x for x in range(song.total_beats)]
20
- random.shuffle(beatlist)
21
-
22
- song.enable_output_play()
23
- song.setup_playback()
24
-
25
- song.enable_output_save(outfile)
26
- song.reset_remix()
27
-
28
- song.play_beats(beatlist)
29
- song.save_remix()