bitcomposer 1.0.0rc1__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.
- bitcomposer/__init__.py +6 -0
- bitcomposer/__main__.py +5 -0
- bitcomposer/arp.py +192 -0
- bitcomposer/bass.py +173 -0
- bitcomposer/builder.py +394 -0
- bitcomposer/cli.py +266 -0
- bitcomposer/composer.py +705 -0
- bitcomposer/drums.py +291 -0
- bitcomposer/echo.py +154 -0
- bitcomposer/gui.py +122 -0
- bitcomposer/harmony.py +330 -0
- bitcomposer/instruments.py +216 -0
- bitcomposer/it_format.py +383 -0
- bitcomposer/melody.py +523 -0
- bitcomposer/mix.py +65 -0
- bitcomposer/names.py +158 -0
- bitcomposer/packs/__init__.py +30 -0
- bitcomposer/packs/evermore.py +361 -0
- bitcomposer/packs/ffvi.py +478 -0
- bitcomposer/packs/mana.py +376 -0
- bitcomposer/packs/megamanx.py +400 -0
- bitcomposer/pattern.py +511 -0
- bitcomposer/samples/__init__.py +386 -0
- bitcomposer/samples/chip.py +475 -0
- bitcomposer/samples/dsp.py +461 -0
- bitcomposer/samples/genesis.py +372 -0
- bitcomposer/samples/snes.py +244 -0
- bitcomposer/structure.py +399 -0
- bitcomposer/theory.py +258 -0
- bitcomposer-1.0.0rc1.dist-info/METADATA +190 -0
- bitcomposer-1.0.0rc1.dist-info/RECORD +35 -0
- bitcomposer-1.0.0rc1.dist-info/WHEEL +5 -0
- bitcomposer-1.0.0rc1.dist-info/entry_points.txt +3 -0
- bitcomposer-1.0.0rc1.dist-info/licenses/LICENSE +21 -0
- bitcomposer-1.0.0rc1.dist-info/top_level.txt +1 -0
bitcomposer/__init__.py
ADDED
bitcomposer/__main__.py
ADDED
bitcomposer/arp.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Arpeggio figures.
|
|
3
|
+
|
|
4
|
+
The arp used to be a direction — up, down, up-down or random — stepping
|
|
5
|
+
through the chord tones every eighth note for the whole song, one setting
|
|
6
|
+
per song, with random velocity jitter. That reads as a sequencer running in
|
|
7
|
+
the background. Game arps are *figures*: short rhythmic shapes with accents
|
|
8
|
+
and rests that recur with the section, climb, and leave room for the lead.
|
|
9
|
+
|
|
10
|
+
A figure is one bar of (row, degree, accent) steps. `degree` indexes a
|
|
11
|
+
ladder of chord tones that starts on the chord's root in octave 4 and
|
|
12
|
+
climbs through the chord — so 0 is the root, 1 the next chord tone up, and
|
|
13
|
+
for a triad 3 is the root an octave higher. Negative degrees reach below.
|
|
14
|
+
Steps are 16 rows to the bar; a figure can be sparser than that.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import random
|
|
18
|
+
|
|
19
|
+
from .pattern import ROWS_PER_BAR
|
|
20
|
+
|
|
21
|
+
# Accent levels, as a fraction of the section's arp volume.
|
|
22
|
+
S, N, W = 1.0, 0.8, 0.62 # strong, normal, weak
|
|
23
|
+
|
|
24
|
+
FIGURES = {
|
|
25
|
+
# name: {"steps": [(row, degree, accent), ...], "energy": (lo, hi), "climb": bool}
|
|
26
|
+
"sparse_pulse": {
|
|
27
|
+
"steps": [(0, 0, S), (8, 3, N)],
|
|
28
|
+
"energy": (0.0, 0.55), "climb": False},
|
|
29
|
+
"quarter_up": {
|
|
30
|
+
"steps": [(0, 0, S), (4, 1, W), (8, 2, N), (12, 3, W)],
|
|
31
|
+
"energy": (0.0, 0.7), "climb": False},
|
|
32
|
+
"classic_up": {
|
|
33
|
+
"steps": [(0, 0, S), (2, 1, W), (4, 2, N), (6, 3, W),
|
|
34
|
+
(8, 0, S), (10, 1, W), (12, 2, N), (14, 3, W)],
|
|
35
|
+
"energy": (0.4, 1.0), "climb": True},
|
|
36
|
+
"up_down": {
|
|
37
|
+
"steps": [(0, 0, S), (2, 1, W), (4, 2, N), (6, 3, W),
|
|
38
|
+
(8, 2, S), (10, 1, W), (12, 0, N), (14, 1, W)],
|
|
39
|
+
"energy": (0.4, 1.0), "climb": False},
|
|
40
|
+
"broken_octave": { # 1-5-8-5, the Mega Man figure
|
|
41
|
+
"steps": [(0, 0, S), (2, 2, W), (4, 3, N), (6, 2, W),
|
|
42
|
+
(8, 0, S), (10, 2, W), (12, 3, N), (14, 2, W)],
|
|
43
|
+
"energy": (0.5, 1.0), "climb": True},
|
|
44
|
+
"pedal_bounce": { # root pedal against the chord above it
|
|
45
|
+
"steps": [(0, 0, S), (2, 3, W), (4, 0, N), (6, 4, W),
|
|
46
|
+
(8, 0, S), (10, 3, W), (12, 0, N), (14, 5, W)],
|
|
47
|
+
"energy": (0.5, 1.0), "climb": False},
|
|
48
|
+
"sync_push": { # dotted-eighth push
|
|
49
|
+
"steps": [(0, 0, S), (3, 2, W), (6, 3, N), (8, 0, S), (11, 2, W), (14, 3, N)],
|
|
50
|
+
"energy": (0.5, 1.0), "climb": True},
|
|
51
|
+
"offbeat_stabs": { # chord tones on the "and"s, bass has the beat
|
|
52
|
+
"steps": [(2, 3, N), (6, 2, N), (10, 3, S), (14, 4, N)],
|
|
53
|
+
"energy": (0.5, 1.0), "climb": False},
|
|
54
|
+
"gallop": {
|
|
55
|
+
"steps": [(0, 0, S), (3, 0, W), (4, 2, N), (8, 0, S), (11, 0, W), (12, 3, N)],
|
|
56
|
+
"energy": (0.7, 1.0), "climb": False},
|
|
57
|
+
"run16": { # sixteenth-note run over two octaves
|
|
58
|
+
"steps": [(r, d, (S if r % 8 == 0 else (N if r % 4 == 0 else W)))
|
|
59
|
+
for r, d in zip(range(16), [0, 1, 2, 3, 4, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5])],
|
|
60
|
+
"energy": (0.8, 1.0), "climb": False},
|
|
61
|
+
"cascade": { # falling from the top of the chord
|
|
62
|
+
"steps": [(0, 5, S), (2, 4, W), (4, 3, N), (6, 2, W),
|
|
63
|
+
(8, 5, S), (10, 4, W), (12, 3, N), (14, 1, W)],
|
|
64
|
+
"energy": (0.5, 1.0), "climb": False},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def pick_figures(energy_pref: str, weights: dict[str, float] | None = None) -> dict[str, str]:
|
|
69
|
+
"""Choose a figure per section role for the song.
|
|
70
|
+
|
|
71
|
+
Verse and chorus always differ, so the chorus arp is part of the hook
|
|
72
|
+
rather than the same loop as everything else. The bridge gets a third
|
|
73
|
+
figure; intros and outros use the sparse pulse. A style pack may pass
|
|
74
|
+
weights over figure names; figures it weights at zero are out and the
|
|
75
|
+
chorus draw is weighted.
|
|
76
|
+
"""
|
|
77
|
+
if weights:
|
|
78
|
+
pool = [f for f in FIGURES if weights.get(f, 0) > 0]
|
|
79
|
+
chorus_pool = [f for f in pool if FIGURES[f]["energy"][0] >= 0.4] or pool
|
|
80
|
+
chorus = random.choices(chorus_pool, [weights[f] for f in chorus_pool], k=1)[0]
|
|
81
|
+
rest = [f for f in pool if f != chorus]
|
|
82
|
+
random.shuffle(rest)
|
|
83
|
+
return {"verse": rest[0], "chorus": chorus, "bridge": rest[1] if len(rest) > 1 else rest[0],
|
|
84
|
+
"intro": "sparse_pulse", "outro": "quarter_up"}
|
|
85
|
+
if energy_pref == "chill":
|
|
86
|
+
pool = ["quarter_up", "classic_up", "up_down", "pedal_bounce", "sparse_pulse"]
|
|
87
|
+
elif energy_pref == "intense":
|
|
88
|
+
pool = ["classic_up", "broken_octave", "sync_push", "gallop", "run16",
|
|
89
|
+
"offbeat_stabs", "cascade"]
|
|
90
|
+
else:
|
|
91
|
+
pool = ["classic_up", "up_down", "broken_octave", "pedal_bounce", "sync_push",
|
|
92
|
+
"offbeat_stabs", "cascade", "quarter_up"]
|
|
93
|
+
# The chorus runs at the top of the energy curve, so its figure must be
|
|
94
|
+
# one built for that; the verse and bridge can take anything in the pool.
|
|
95
|
+
chorus_pool = [f for f in pool if FIGURES[f]["energy"][0] >= 0.4]
|
|
96
|
+
chorus = random.choice(chorus_pool)
|
|
97
|
+
rest = [f for f in pool if f != chorus]
|
|
98
|
+
random.shuffle(rest)
|
|
99
|
+
return {"verse": rest[0], "chorus": chorus, "bridge": rest[1] if len(rest) > 1 else rest[0],
|
|
100
|
+
"intro": "sparse_pulse", "outro": "quarter_up"}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _ladder(chord_notes: list[int], base_octave_root: int = 48) -> list[int]:
|
|
104
|
+
"""Chord tones from the root placed in octave 4, climbing three octaves."""
|
|
105
|
+
pcs = sorted({n % 12 for n in chord_notes})
|
|
106
|
+
root_pc = chord_notes[0] % 12
|
|
107
|
+
pcs = pcs[pcs.index(root_pc):] + pcs[:pcs.index(root_pc)] # start on the root
|
|
108
|
+
root = base_octave_root + root_pc
|
|
109
|
+
while root >= base_octave_root + 12:
|
|
110
|
+
root -= 12
|
|
111
|
+
ladder = []
|
|
112
|
+
for octave in (-1, 0, 1, 2):
|
|
113
|
+
for pc in pcs:
|
|
114
|
+
n = root + ((pc - root_pc) % 12) + 12 * octave
|
|
115
|
+
ladder.append(n)
|
|
116
|
+
return ladder
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def colour_tones(chord_notes: list[int], scale_notes: list[int]) -> list[int]:
|
|
120
|
+
"""The 7th and the 9th above the chord's root, as the scale spells them.
|
|
121
|
+
|
|
122
|
+
Returned as pitch offsets from the root (10 or 11 for the 7th, 13 or 14
|
|
123
|
+
for the 9th, 15 means "none"), so they work in any octave. Game-music
|
|
124
|
+
arps on strings and harps nearly always carry one of these — it is what
|
|
125
|
+
makes a harp run sound like a score rather than a music box — but they
|
|
126
|
+
must come from the scale: a major 7th over a mixolydian chord, or a
|
|
127
|
+
natural 9th in phrygian, is a wrong note.
|
|
128
|
+
"""
|
|
129
|
+
pcs = {n % 12 for n in scale_notes}
|
|
130
|
+
root = chord_notes[0] % 12
|
|
131
|
+
out = []
|
|
132
|
+
for cands in ((10, 11), (14, 13)): # 7th, then 9th
|
|
133
|
+
for off in cands:
|
|
134
|
+
if (root + off) % 12 in pcs:
|
|
135
|
+
out.append(off)
|
|
136
|
+
break
|
|
137
|
+
return out
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def generate_arp(segments: list[tuple[int, int, list[int]]], figure_name: str,
|
|
141
|
+
energy: float, melody_rows: set[int] | None = None,
|
|
142
|
+
scale_notes: list[int] | None = None, colour: bool = False,
|
|
143
|
+
rows_per_bar: int = ROWS_PER_BAR) -> list[tuple[int, int, float]]:
|
|
144
|
+
"""Realise a figure over a chord timeline.
|
|
145
|
+
|
|
146
|
+
segments is [(start_row, end_row, chord_notes), ...]. Returns
|
|
147
|
+
[(row, midi_note, accent), ...]. A bar may rest at low energy (never at
|
|
148
|
+
high); a climbing figure plays its even bars an octave up; and where the
|
|
149
|
+
melody is busy in a bar (melody_rows carries its onsets) the figure's
|
|
150
|
+
weak steps are dropped so the arp fills gaps rather than fighting the lead.
|
|
151
|
+
|
|
152
|
+
With colour (the SNES palette), the figure's octave-root step becomes
|
|
153
|
+
the chord's 7th or 9th, alternating by bar, drawn from the scale via
|
|
154
|
+
colour_tones. Lower steps keep the triad, so the shimmer sits on top.
|
|
155
|
+
"""
|
|
156
|
+
fig = FIGURES[figure_name]
|
|
157
|
+
out = []
|
|
158
|
+
total = segments[-1][1] if segments else 0
|
|
159
|
+
bar_index = 0
|
|
160
|
+
for bar_start in range(0, total, rows_per_bar):
|
|
161
|
+
seg = next((s for s in segments if s[0] <= bar_start < s[1]), segments[0])
|
|
162
|
+
chord = seg[2]
|
|
163
|
+
ladder = _ladder(chord)
|
|
164
|
+
base = len(ladder) // 4 * 1 # index of the root in octave 4
|
|
165
|
+
per_octave = len(ladder) // 4
|
|
166
|
+
if energy < 0.8 and random.random() > energy + 0.25:
|
|
167
|
+
bar_index += 1
|
|
168
|
+
continue # this bar rests
|
|
169
|
+
shift = per_octave if (fig["climb"] and bar_index % 2 == 1) else 0
|
|
170
|
+
busy = False
|
|
171
|
+
if melody_rows:
|
|
172
|
+
onsets = sum(1 for r in melody_rows if bar_start <= r < bar_start + rows_per_bar)
|
|
173
|
+
busy = onsets >= 5
|
|
174
|
+
tints = colour_tones(chord, scale_notes) if (colour and scale_notes) else []
|
|
175
|
+
tint = tints[bar_index % len(tints)] if tints else None
|
|
176
|
+
for row_off, degree, accent in fig["steps"]:
|
|
177
|
+
if busy and accent == W:
|
|
178
|
+
continue
|
|
179
|
+
idx = base + degree + shift
|
|
180
|
+
if not (0 <= idx < len(ladder)):
|
|
181
|
+
continue
|
|
182
|
+
note = ladder[idx]
|
|
183
|
+
# The octave-root step (and any further octave of it) carries
|
|
184
|
+
# the colour tone, an octave below the root it replaces so the
|
|
185
|
+
# 7th/9th sits just above the triad rather than far on top.
|
|
186
|
+
if tint is not None and degree > 0 and degree % per_octave == 0:
|
|
187
|
+
note = ladder[idx - per_octave] + tint
|
|
188
|
+
row = bar_start + row_off
|
|
189
|
+
if row < total:
|
|
190
|
+
out.append((row, note, accent))
|
|
191
|
+
bar_index += 1
|
|
192
|
+
return out
|
bitcomposer/bass.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bass generation. (Arpeggios live in arp.py.)
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
from . import theory
|
|
8
|
+
from .pattern import ROWS_PER_BAR, ROWS_PER_BEAT
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Riffs: (row in bar, interval from the root) of a one-bar figure that
|
|
12
|
+
# repeats. Intervals stay chord-safe (root, fifth, octave, seventh), so a riff
|
|
13
|
+
# fits major and minor alike. One riff per song, drawn by the composer from
|
|
14
|
+
# the pack's weights; a stage theme has its own riff, not the game's.
|
|
15
|
+
RIFFS = {
|
|
16
|
+
"rock": [(0, 0), (2, 0), (4, 12), (6, 0), (8, 0), (10, 7), (12, 12), (14, 7)],
|
|
17
|
+
"gallop": [(0, 0), (3, 0), (4, 0), (6, 12), (8, 0), (11, 0), (12, 0), (14, 7)],
|
|
18
|
+
"funk": [(0, 0), (3, 12), (6, 0), (10, 7), (12, 0), (14, 10)],
|
|
19
|
+
"pedal_push": [(0, 0), (2, 0), (4, 0), (6, 0), (7, 12), (8, 0), (10, 0), (12, 0), (14, 12)],
|
|
20
|
+
"octaves": [(0, 0), (2, 12), (4, 0), (6, 12), (8, 0), (10, 12), (12, 7), (14, 12)],
|
|
21
|
+
"fifth_bounce": [(0, 0), (2, 7), (4, 0), (6, 7), (8, 12), (10, 7), (12, 0), (14, 7)],
|
|
22
|
+
"drop": [(0, 0), (2, 0), (6, 0), (8, 12), (10, 10), (12, 7), (14, 0)],
|
|
23
|
+
}
|
|
24
|
+
RIFF = RIFFS["rock"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# The lowest bass note written, as MIDI (28 is E1, 41.2 Hz, a bass guitar's
|
|
28
|
+
# low string). The windows below sit on it. Before the note conversion was
|
|
29
|
+
# corrected the whole engine sounded an octave low and the bass reached
|
|
30
|
+
# 13 Hz; the floor keeps any style's octave drop or cadence walk honest.
|
|
31
|
+
BASS_FLOOR = 28
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def floor_bass(notes: list[tuple[int, int]]) -> list[tuple[int, int]]:
|
|
35
|
+
"""Every note below BASS_FLOOR raised by octaves until it is not."""
|
|
36
|
+
out = []
|
|
37
|
+
for row, note in notes:
|
|
38
|
+
while 0 < note < BASS_FLOOR:
|
|
39
|
+
note += 12
|
|
40
|
+
out.append((row, note))
|
|
41
|
+
return out
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def generate_bass(root: int, chord_type: str,
|
|
45
|
+
rows: int, style: str = "steady",
|
|
46
|
+
weight: str = "heavy",
|
|
47
|
+
density: float = 1.0, riff: str | None = None,
|
|
48
|
+
scale_notes: list[int] | None = None) -> list[tuple[int, int]]:
|
|
49
|
+
"""Generate a bass line. Returns [(row, midi_note), ...].
|
|
50
|
+
|
|
51
|
+
density (0..1) thins the off-beat notes. The line previously played every
|
|
52
|
+
slot its style produced, unconditionally, for every pattern in the song —
|
|
53
|
+
the same "never rests" problem the arp had. Bar downbeats are always kept,
|
|
54
|
+
since the bass has to state the chord.
|
|
55
|
+
"""
|
|
56
|
+
# Register depends on weight, in MIDI (the conversion to the pattern is
|
|
57
|
+
# exact since 2026-10-17; before that everything sounded an octave low).
|
|
58
|
+
if weight == "light":
|
|
59
|
+
# MIDI 40-51 (82-165 Hz) — punchier, more melodic
|
|
60
|
+
bass_root = root
|
|
61
|
+
while bass_root >= 52:
|
|
62
|
+
bass_root -= 12
|
|
63
|
+
while bass_root < 40:
|
|
64
|
+
bass_root += 12
|
|
65
|
+
elif weight == "medium":
|
|
66
|
+
# MIDI 34-45 (58-110 Hz) — middle ground
|
|
67
|
+
bass_root = root
|
|
68
|
+
while bass_root >= 46:
|
|
69
|
+
bass_root -= 12
|
|
70
|
+
while bass_root < 34:
|
|
71
|
+
bass_root += 12
|
|
72
|
+
else:
|
|
73
|
+
# MIDI 28-39 (41-78 Hz) — deep and heavy
|
|
74
|
+
bass_root = root
|
|
75
|
+
while bass_root >= 40:
|
|
76
|
+
bass_root -= 12
|
|
77
|
+
while bass_root < 28:
|
|
78
|
+
bass_root += 12
|
|
79
|
+
|
|
80
|
+
chord = theory.build_chord(bass_root, chord_type)
|
|
81
|
+
notes = []
|
|
82
|
+
|
|
83
|
+
# Sparser step sizes for lighter weights
|
|
84
|
+
if weight == "light":
|
|
85
|
+
step_scale = 2 # half as many notes
|
|
86
|
+
elif weight == "medium":
|
|
87
|
+
step_scale = 1.5
|
|
88
|
+
else:
|
|
89
|
+
step_scale = 1
|
|
90
|
+
|
|
91
|
+
if style == "steady":
|
|
92
|
+
step = int(8 * step_scale)
|
|
93
|
+
for row in range(0, rows, step):
|
|
94
|
+
notes.append((row, bass_root))
|
|
95
|
+
elif style == "octave":
|
|
96
|
+
step = int(4 * step_scale)
|
|
97
|
+
for i, row in enumerate(range(0, rows, step)):
|
|
98
|
+
if i % 2 == 0:
|
|
99
|
+
notes.append((row, bass_root))
|
|
100
|
+
else:
|
|
101
|
+
notes.append((row, bass_root + 12))
|
|
102
|
+
elif style == "walking":
|
|
103
|
+
step = int(4 * step_scale)
|
|
104
|
+
for i, row in enumerate(range(0, rows, step)):
|
|
105
|
+
note = chord[i % len(chord)]
|
|
106
|
+
notes.append((row, note))
|
|
107
|
+
elif style == "driving":
|
|
108
|
+
step = int(2 * step_scale)
|
|
109
|
+
for row in range(0, rows, step):
|
|
110
|
+
notes.append((row, bass_root))
|
|
111
|
+
elif style == "drone":
|
|
112
|
+
# One note per chord, held for the whole segment; the composer slides
|
|
113
|
+
# between roots (porta) so the drone moves without restriking.
|
|
114
|
+
notes.append((0, bass_root))
|
|
115
|
+
elif style == "riff":
|
|
116
|
+
# A one-bar riff that repeats: root on the beat, the octave and the
|
|
117
|
+
# fifth pushing between, the way a rock bass carries a stage theme.
|
|
118
|
+
figure = RIFFS.get(riff or "rock", RIFF)
|
|
119
|
+
# A riff's seventh is only in key for minor, dorian, mixolydian and
|
|
120
|
+
# minor pentatonic; in a major key it sat a semitone under the
|
|
121
|
+
# melody's note. Snap every interval to the nearest scale tone at or
|
|
122
|
+
# below it, so the seventh becomes the sixth where that is the key's
|
|
123
|
+
# note and stays a seventh where it is.
|
|
124
|
+
classes = {n % 12 for n in scale_notes} if scale_notes else None
|
|
125
|
+
for bar in range(0, rows, ROWS_PER_BAR):
|
|
126
|
+
for off, iv in figure:
|
|
127
|
+
if bar + off >= rows:
|
|
128
|
+
continue
|
|
129
|
+
note = bass_root + iv
|
|
130
|
+
if classes and note % 12 not in classes:
|
|
131
|
+
while note % 12 not in classes and note > bass_root - 12:
|
|
132
|
+
note -= 1
|
|
133
|
+
notes.append((bar + off, note))
|
|
134
|
+
|
|
135
|
+
# A riff is a figure and repeats exactly; the thinning that lets the
|
|
136
|
+
# other styles rest would make it stumble from bar to bar.
|
|
137
|
+
if density < 1.0 and style != "riff":
|
|
138
|
+
notes = [
|
|
139
|
+
(row, note) for row, note in notes
|
|
140
|
+
if row % ROWS_PER_BAR == 0 or random.random() < density
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
return notes
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def pick_section_bass(bass_weight: str, section_energy: float,
|
|
147
|
+
bass_style: str) -> str:
|
|
148
|
+
"""Choose bass style for a section based on weight and energy."""
|
|
149
|
+
if bass_style in ("drone", "riff"):
|
|
150
|
+
return bass_style # a drone or a riff does not lift with the section
|
|
151
|
+
if bass_weight == "light":
|
|
152
|
+
if section_energy > 0.85:
|
|
153
|
+
return random.choice(["steady", "walking"])
|
|
154
|
+
return random.choice(["steady", "steady", "walking"])
|
|
155
|
+
elif section_energy < 0.5:
|
|
156
|
+
return random.choice(["steady", "steady"])
|
|
157
|
+
elif section_energy > 0.85:
|
|
158
|
+
return random.choice(["driving", "octave", bass_style])
|
|
159
|
+
return bass_style
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def generate_bass_timeline(segments: list[tuple[int, int, int, str]],
|
|
163
|
+
style: str = "steady", weight: str = "heavy",
|
|
164
|
+
density: float = 1.0,
|
|
165
|
+
riff: str | None = None,
|
|
166
|
+
scale_notes: list[int] | None = None) -> list[tuple[int, int]]:
|
|
167
|
+
"""generate_bass over [(start_row, end_row, root, chord_type), ...]."""
|
|
168
|
+
notes = []
|
|
169
|
+
for start, end, root, chord_type in segments:
|
|
170
|
+
part = generate_bass(root, chord_type, end - start, style, weight, density, riff=riff,
|
|
171
|
+
scale_notes=scale_notes)
|
|
172
|
+
notes.extend((start + row, note) for row, note in part)
|
|
173
|
+
return notes
|