bitcomposer 1.0.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.
- bitcomposer/__init__.py +6 -0
- bitcomposer/__main__.py +5 -0
- bitcomposer/arp.py +192 -0
- bitcomposer/bass.py +173 -0
- bitcomposer/builder.py +403 -0
- bitcomposer/cli.py +284 -0
- bitcomposer/composer.py +715 -0
- bitcomposer/drums.py +291 -0
- bitcomposer/echo.py +154 -0
- bitcomposer/harmony.py +327 -0
- bitcomposer/instruments.py +216 -0
- bitcomposer/it_format.py +376 -0
- bitcomposer/melody.py +543 -0
- bitcomposer/mix.py +66 -0
- bitcomposer/names.py +158 -0
- bitcomposer/packs/__init__.py +30 -0
- bitcomposer/packs/evermore.py +358 -0
- bitcomposer/packs/ffvi.py +475 -0
- bitcomposer/packs/mana.py +373 -0
- bitcomposer/packs/megamanx.py +397 -0
- bitcomposer/pattern.py +474 -0
- bitcomposer/player/__init__.py +4 -0
- bitcomposer/player/__main__.py +4 -0
- bitcomposer/player/app.py +327 -0
- bitcomposer/player/audio.py +162 -0
- bitcomposer/player/autostart.py +93 -0
- bitcomposer/player/backend.py +479 -0
- bitcomposer/player/desktop.py +139 -0
- bitcomposer/player/engine.py +601 -0
- bitcomposer/player/fonts/JetBrainsMono-Bold.ttf +0 -0
- bitcomposer/player/fonts/JetBrainsMono-Regular.ttf +0 -0
- bitcomposer/player/fonts/LICENSE.txt +93 -0
- bitcomposer/player/icons/bitcomposer.svg +34 -0
- bitcomposer/player/mediakeys_win.py +47 -0
- bitcomposer/player/mpris.py +317 -0
- bitcomposer/player/openmpt.py +208 -0
- bitcomposer/player/qml/Main.qml +1764 -0
- bitcomposer/player/update.py +203 -0
- bitcomposer/player/window.py +113 -0
- bitcomposer/registers.py +78 -0
- bitcomposer/samples/__init__.py +385 -0
- bitcomposer/samples/chip.py +459 -0
- bitcomposer/samples/dsp.py +459 -0
- bitcomposer/samples/genesis.py +369 -0
- bitcomposer/samples/snes.py +242 -0
- bitcomposer/structure.py +382 -0
- bitcomposer/theory.py +248 -0
- bitcomposer-1.0.0.dist-info/METADATA +267 -0
- bitcomposer-1.0.0.dist-info/RECORD +53 -0
- bitcomposer-1.0.0.dist-info/WHEEL +5 -0
- bitcomposer-1.0.0.dist-info/entry_points.txt +3 -0
- bitcomposer-1.0.0.dist-info/licenses/LICENSE +21 -0
- bitcomposer-1.0.0.dist-info/top_level.txt +1 -0
bitcomposer/builder.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""One pattern at a time: the chord timeline, the key plan, and every layer
|
|
2
|
+
(melody, harmony, bass, arp, drums, echo returns) written into it."""
|
|
3
|
+
import random
|
|
4
|
+
import zlib
|
|
5
|
+
|
|
6
|
+
from . import theory
|
|
7
|
+
from .it_format import ITPattern, ITNote
|
|
8
|
+
from .pattern import CH_MELODY, CH_HARMONY, CH_HARMONY2, CH_HARMONY3, CH_BASS, CH_ARP, CH_KICK, CH_SNARE, CH_HIHAT, CH_TOM, CH_CRASH, CH_OPEN_HAT, CH_ECHO_ALL, NUM_CHANNELS, ROWS_PER_PATTERN, ROWS_PER_BAR, humanize_volume, apply_notes_to_pattern, apply_vibrato, apply_portamento, apply_harmony_fade, apply_fade, apply_echo_taps, apply_sustain_safety_net, end_note, apply_strum, STRUM_ROW, STRUM_TICKS
|
|
9
|
+
from .drums import render_drums
|
|
10
|
+
from .melody import generate_motif, generate_melody, generate_counter_melody, add_melody_cuts, MELODY_LOW, MELODY_HIGH
|
|
11
|
+
from .harmony import generate_harmony_timeline, anchors_for, window_for, stab_rows, harmony_level, HARMONY_VOICINGS
|
|
12
|
+
from .bass import generate_bass_timeline, floor_bass
|
|
13
|
+
from .arp import generate_arp
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# The channels each part writes, for the intro entries.
|
|
17
|
+
PART_CHANNELS = {
|
|
18
|
+
"melody": (CH_MELODY,), "harmony": (CH_HARMONY, CH_HARMONY2, CH_HARMONY3),
|
|
19
|
+
"bass": (CH_BASS,), "arp": (CH_ARP,),
|
|
20
|
+
"drums": (CH_KICK, CH_SNARE, CH_HIHAT, CH_TOM, CH_CRASH, CH_OPEN_HAT),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# How harmony notes decay after their note-on, per voicing: (slide, rows to
|
|
24
|
+
# hold first), or None for no slide. The chug, pulse and rhythmic hits are
|
|
25
|
+
# short, so a per-tick slide from the start is right. sustain is a pad: it
|
|
26
|
+
# holds for most of its 32-row interval and then takes a fine (per-row)
|
|
27
|
+
# slide down into the next hit (its hold is set per pattern so the slide,
|
|
28
|
+
# one per row, reaches zero at the cut whatever the written volume: at the
|
|
29
|
+
# old fixed 18 rows a quieter chord went silent early and re-attacked out
|
|
30
|
+
# of nothing). A stab takes no slide: it holds at level
|
|
31
|
+
# for the rows harmony.stab_rows gives the voice and ends on a note-off
|
|
32
|
+
# into the family's release. It used to take D02, which at volume 22 had
|
|
33
|
+
# the note silent eleven ticks after its attack whatever the cut row said.
|
|
34
|
+
HARMONY_FADE = {"stabs": None, "rhythmic": (0x03, 0), "sustain": (0xF1, None),
|
|
35
|
+
"ostinato": (0x03, 0), "pulse": (0x04, 0),
|
|
36
|
+
"chug_sync": (0x03, 0), "chug_offbeat": (0x03, 0), "chug_gallop": (0x04, 0)}
|
|
37
|
+
|
|
38
|
+
# The harmony's written volume per voice at full energy, before the family
|
|
39
|
+
# level and the voice-count scale (harmony.harmony_level). The distance
|
|
40
|
+
# under the lead is set by the harmony channels' volume in mix.py, so the
|
|
41
|
+
# note volumes keep their resolution and the humanisation stays small
|
|
42
|
+
# against them. The counter-line has its own level: a second melodic line,
|
|
43
|
+
# it sits a little under the lead, not at the chord's level.
|
|
44
|
+
HARMONY_VOL = 22
|
|
45
|
+
COUNTER_VOL = 32
|
|
46
|
+
|
|
47
|
+
# Chance that a pattern changes chord at its halfway point, by section type.
|
|
48
|
+
HALF_PATTERN_CHANGE = {
|
|
49
|
+
"intro": 0.2, "verse": 0.5, "chorus": 0.55, "bridge": 0.6, "outro": 0.3,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _content_seed(song_salt: int, group: str, chord_idx: int) -> int:
|
|
54
|
+
"""Stable per-(reuse group, chord) seed so repeats generate the same music.
|
|
55
|
+
|
|
56
|
+
crc32 rather than hash() because str hashing is salted per process, which
|
|
57
|
+
would make -s deterministic within a run but not across runs.
|
|
58
|
+
"""
|
|
59
|
+
return song_salt ^ zlib.crc32(f"{group}:{chord_idx}".encode())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _energy_tier(energy: float) -> int:
|
|
63
|
+
"""Bucket section energy to 0.2-wide tiers.
|
|
64
|
+
|
|
65
|
+
Sections a hair apart on the energy curve (chorus at 0.85 and 0.90) render
|
|
66
|
+
close enough to share a pattern; a final chorus at 1.0 lands in its own
|
|
67
|
+
tier and gets its own louder copy of the same music.
|
|
68
|
+
"""
|
|
69
|
+
return round(energy * 5)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _pick_section_motif(sec_type: str, use_phrased: bool,
|
|
73
|
+
chorus_motif, verse_motif, bridge_motif,
|
|
74
|
+
rng: random.Random | None = None, scale_size: int = 7) -> dict | None:
|
|
75
|
+
"""Pick the appropriate motif for a section type."""
|
|
76
|
+
if not use_phrased:
|
|
77
|
+
return None
|
|
78
|
+
if sec_type == "chorus":
|
|
79
|
+
return chorus_motif
|
|
80
|
+
elif sec_type == "verse":
|
|
81
|
+
return verse_motif
|
|
82
|
+
elif sec_type == "bridge":
|
|
83
|
+
return bridge_motif
|
|
84
|
+
elif sec_type == "intro":
|
|
85
|
+
return chorus_motif or verse_motif
|
|
86
|
+
else:
|
|
87
|
+
# Outro/tag: a fresh motif on the melody's own stream, so its draws
|
|
88
|
+
# never touch the arrangement.
|
|
89
|
+
r = rng or random
|
|
90
|
+
return generate_motif(length=r.choice([3, 4]), rng=rng, scale_size=scale_size)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Related keys a section can move to, as (semitone shift, scale family).
|
|
94
|
+
# "relative" is the relative major of a minor key or the relative minor of a
|
|
95
|
+
# major one — the move game composers reach for at a chorus or a bridge; the
|
|
96
|
+
# old +1 semitone chorus was the truck-driver key change and is gone.
|
|
97
|
+
RELATIVE = {
|
|
98
|
+
"natural_minor": (3, "major"), "harmonic_minor": (3, "major"), "dorian": (3, "major"),
|
|
99
|
+
"phrygian": (3, "major"), "minor_pentatonic": (3, "major_pentatonic"),
|
|
100
|
+
"major": (-3, "natural_minor"), "mixolydian": (-3, "natural_minor"),
|
|
101
|
+
"major_pentatonic": (-3, "minor_pentatonic"),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def plan_keys(scale_name: str, keys: str = "moving") -> dict[str, tuple[int, str]]:
|
|
106
|
+
"""Per section type: (semitone shift from the home key, scale name).
|
|
107
|
+
|
|
108
|
+
Verses and themes stay home. A chorus stays home half the time, goes to
|
|
109
|
+
the relative key a third of the time, or lifts to the subdominant. A
|
|
110
|
+
bridge most often moves — that is what a bridge is for — to the relative
|
|
111
|
+
key or the subdominant.
|
|
112
|
+
|
|
113
|
+
keys="home" pins every section to the home key. The moving plan trades
|
|
114
|
+
consistency for the chance of a striking song, which is right for a
|
|
115
|
+
generator you browse; a loop that has to sit under a game scene for ten
|
|
116
|
+
minutes, a track that must match a fixed-key stinger, or an A/B test of
|
|
117
|
+
anything else in the engine all want the stable version.
|
|
118
|
+
"""
|
|
119
|
+
rel_shift, rel_scale = RELATIVE.get(scale_name, (0, scale_name))
|
|
120
|
+
home = (0, scale_name)
|
|
121
|
+
# Draw either way, so a seed makes the same song with the keys pinned
|
|
122
|
+
# apart from the key moves themselves.
|
|
123
|
+
chorus = random.choices([home, (rel_shift, rel_scale), (5, scale_name)], weights=[50, 32, 18], k=1)[0]
|
|
124
|
+
bridge = random.choices([home, (rel_shift, rel_scale), (5, scale_name)], weights=[40, 35, 25], k=1)[0]
|
|
125
|
+
if keys == "home":
|
|
126
|
+
chorus = bridge = home
|
|
127
|
+
return {"verse": home, "chorus": chorus, "bridge": bridge, "intro": home, "outro": home}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _chord_segments(key: int, scale_name: str, scale_notes: list[int],
|
|
131
|
+
entries: list, next_entry, sec_type: str,
|
|
132
|
+
cadence_root: int | None = None) -> list[tuple]:
|
|
133
|
+
"""Chord timeline for one pattern: [(start_row, end_row, root, type, notes)].
|
|
134
|
+
|
|
135
|
+
`entries` holds one chord (normal harmonic rhythm) or two (compact, two
|
|
136
|
+
bars each). With one chord, HALF_PATTERN_CHANGE odds move the back two
|
|
137
|
+
bars to a related chord (theory.secondary_chord). With cadence_root set
|
|
138
|
+
— the root the next section opens on — the back half becomes that
|
|
139
|
+
root's dominant instead, so the section ends with a V that resolves into
|
|
140
|
+
the next one.
|
|
141
|
+
"""
|
|
142
|
+
def seg(start, end, e):
|
|
143
|
+
_, chord_type, _ = theory.unpack_chord(e)
|
|
144
|
+
root = theory.chord_root_for(key, scale_notes, e)
|
|
145
|
+
return (start, end, root, chord_type, theory.build_chord(root, chord_type))
|
|
146
|
+
|
|
147
|
+
half = ROWS_PER_PATTERN // 2
|
|
148
|
+
if cadence_root is not None:
|
|
149
|
+
dom = cadence_root + 7
|
|
150
|
+
while dom > key + 12:
|
|
151
|
+
dom -= 12
|
|
152
|
+
ctype = "power" if "pentatonic" in scale_name else "major"
|
|
153
|
+
return [seg(0, half, entries[0]), (half, ROWS_PER_PATTERN, dom, ctype, theory.build_chord(dom, ctype))]
|
|
154
|
+
if len(entries) >= 2:
|
|
155
|
+
return [seg(0, half, entries[0]), seg(half, ROWS_PER_PATTERN, entries[1])]
|
|
156
|
+
entry = entries[0]
|
|
157
|
+
if random.random() < HALF_PATTERN_CHANGE.get(sec_type, 0.4):
|
|
158
|
+
second = theory.secondary_chord(scale_name, entry, next_entry)
|
|
159
|
+
return [seg(0, half, entry), seg(half, ROWS_PER_PATTERN, second)]
|
|
160
|
+
return [seg(0, ROWS_PER_PATTERN, entry)]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _compose_pattern(*, scale_notes, segments, layers, sec_type, section_energy,
|
|
164
|
+
section_density, section_bass, sample_map,
|
|
165
|
+
melody_sample, use_phrased, section_motif, melody_rng,
|
|
166
|
+
chord_idx, is_answer,
|
|
167
|
+
voicing_style, num_harmony_voices,
|
|
168
|
+
vibrato_speed, vibrato_depth,
|
|
169
|
+
bass_weight, use_bass_porta, porta_speed,
|
|
170
|
+
arp_figure, arp_colour, groove, drum_fill, breakdown,
|
|
171
|
+
n_patterns, is_ending, ending_style,
|
|
172
|
+
swing_amount, speed, bpm, is_intro, echo_sends,
|
|
173
|
+
pickup_root=None, intro=None, counter_chance=0.3, hat_stroke=1.0,
|
|
174
|
+
crash_chance=1.0, fill_crash=True, kit_rng=None,
|
|
175
|
+
power_chords=False, bass_riff=None, harmony_family=None,
|
|
176
|
+
harmony_preset=None, harmony_onset_ms=0.0,
|
|
177
|
+
registers: dict | None = None) -> ITPattern:
|
|
178
|
+
"""Compose a single pattern with all layers."""
|
|
179
|
+
pat = ITPattern(rows=ROWS_PER_PATTERN, channels=NUM_CHANNELS)
|
|
180
|
+
melody = []
|
|
181
|
+
# Where the parts sit: the song's register plan (registers.py). Without
|
|
182
|
+
# one (a direct caller), the old window and the family's own register.
|
|
183
|
+
registers = registers or {}
|
|
184
|
+
lead_window = registers.get("lead_window", (MELODY_LOW, MELODY_HIGH))
|
|
185
|
+
lead_floor = registers.get("lead_floor")
|
|
186
|
+
harmony_window = registers.get("harmony_window", window_for(harmony_family))
|
|
187
|
+
|
|
188
|
+
# The timeline in the shapes each generator wants.
|
|
189
|
+
chord_notes = segments[0][4] # the pattern's main chord
|
|
190
|
+
melody_timeline = [(s, notes) for s, _, _, _, notes in segments]
|
|
191
|
+
harmony_segments = [(s, e, notes) for s, e, _, _, notes in segments]
|
|
192
|
+
if power_chords:
|
|
193
|
+
# Root, fifth, octave: the guitarist's chord, no third.
|
|
194
|
+
harmony_segments = [(s, e, [root, root + 7, root + 12]) for s, e, root, _, _ in segments]
|
|
195
|
+
bass_segments = [(s, e, root, ctype) for s, e, root, ctype, _ in segments]
|
|
196
|
+
|
|
197
|
+
# Scale volumes by section energy. The harmony's level is a property of
|
|
198
|
+
# the voice family and the voice count (harmony.harmony_level).
|
|
199
|
+
melody_vol = int(42 * section_energy)
|
|
200
|
+
arp_vol = int(36 * section_energy)
|
|
201
|
+
harmony_fade = HARMONY_FADE.get(voicing_style, (0x02, 0))
|
|
202
|
+
row_ms = 60000.0 / (bpm * 4) # four rows to a beat
|
|
203
|
+
# A stab's hold comes from the voice: its family's length or three times
|
|
204
|
+
# its attack, in rows at this tempo; every other mode keeps its own.
|
|
205
|
+
harmony_cut = None
|
|
206
|
+
if voicing_style == "stabs":
|
|
207
|
+
harmony_cut = stab_rows(harmony_family, harmony_onset_ms, row_ms,
|
|
208
|
+
HARMONY_VOICINGS["stabs"]["interval"])
|
|
209
|
+
harmony_hold_ms = (harmony_cut if harmony_cut is not None
|
|
210
|
+
else HARMONY_VOICINGS.get(voicing_style, HARMONY_VOICINGS["stabs"])["cut_after"]) * row_ms
|
|
211
|
+
|
|
212
|
+
# ── Melody ──
|
|
213
|
+
if layers["melody"]:
|
|
214
|
+
# The melody has its own random stream, seeded from the pattern's
|
|
215
|
+
# content seed, so the arrangement around it does not move when the
|
|
216
|
+
# melody code changes. Register follows section energy: verses low,
|
|
217
|
+
# choruses high.
|
|
218
|
+
melody = generate_melody(
|
|
219
|
+
scale_notes, chord_notes, ROWS_PER_PATTERN, section_density,
|
|
220
|
+
motif=section_motif, is_answer=is_answer, phrased=use_phrased,
|
|
221
|
+
chord_timeline=melody_timeline, rng=melody_rng,
|
|
222
|
+
register=0.2 + 0.7 * max(0.0, min(1.0, (section_energy - 0.5) / 0.5)),
|
|
223
|
+
window=lead_window, floor=lead_floor,
|
|
224
|
+
)
|
|
225
|
+
apply_notes_to_pattern(
|
|
226
|
+
pat, CH_MELODY, melody, melody_sample,
|
|
227
|
+
volume=melody_vol, humanize=True, rng=melody_rng,
|
|
228
|
+
)
|
|
229
|
+
apply_vibrato(pat, CH_MELODY, melody,
|
|
230
|
+
speed=vibrato_speed, depth=vibrato_depth)
|
|
231
|
+
|
|
232
|
+
# ── Harmony ──
|
|
233
|
+
if layers["harmony"]:
|
|
234
|
+
harmony_channels = [CH_HARMONY, CH_HARMONY2, CH_HARMONY3]
|
|
235
|
+
|
|
236
|
+
# The counter-line, when a chorus or bridge pattern draws one, has
|
|
237
|
+
# the third channel to itself; the chord keeps the first two. It
|
|
238
|
+
# used to take the first channel at the chord's level and with the
|
|
239
|
+
# chord's fade, so under a stab mode every counter note died like a
|
|
240
|
+
# stab, and the chord voice moved channel (and echo pan) between
|
|
241
|
+
# the patterns that had a counter and the ones that did not.
|
|
242
|
+
has_counter = bool(melody) and sec_type in ("chorus", "bridge") \
|
|
243
|
+
and random.random() < counter_chance
|
|
244
|
+
n_voices = min(2, num_harmony_voices) if has_counter else num_harmony_voices
|
|
245
|
+
|
|
246
|
+
# Register per family from the plan (the lead is placed above the
|
|
247
|
+
# chord, so the chord is not capped under the lead any more); the
|
|
248
|
+
# level per family and voice count.
|
|
249
|
+
voices = generate_harmony_timeline(
|
|
250
|
+
harmony_segments, voicing=voicing_style, num_voices=n_voices,
|
|
251
|
+
anchors=anchors_for(harmony_family, n_voices),
|
|
252
|
+
window=harmony_window, cut_after=harmony_cut,
|
|
253
|
+
)
|
|
254
|
+
harmony_vol = int(HARMONY_VOL * section_energy
|
|
255
|
+
* harmony_level(harmony_family, harmony_preset, n_voices, harmony_hold_ms))
|
|
256
|
+
for v_idx, voice_notes in enumerate(voices):
|
|
257
|
+
if v_idx >= len(harmony_channels):
|
|
258
|
+
break
|
|
259
|
+
ch = harmony_channels[v_idx]
|
|
260
|
+
apply_notes_to_pattern(
|
|
261
|
+
pat, ch, voice_notes,
|
|
262
|
+
sample_map["harmony"],
|
|
263
|
+
volume=humanize_volume(harmony_vol, 4),
|
|
264
|
+
humanize=True,
|
|
265
|
+
)
|
|
266
|
+
if voicing_style == "rhythmic" and v_idx > 0:
|
|
267
|
+
apply_strum(pat, ch, voice_notes, STRUM_ROW, STRUM_TICKS)
|
|
268
|
+
if harmony_fade is not None:
|
|
269
|
+
slide, hold = harmony_fade
|
|
270
|
+
if hold is None:
|
|
271
|
+
# Hold, then a one-per-row slide that lands on zero at the cut.
|
|
272
|
+
hold = max(0, HARMONY_VOICINGS[voicing_style]["cut_after"] - 1 - harmony_vol)
|
|
273
|
+
apply_harmony_fade(pat, ch, voice_notes, fade_rate=slide, hold_rows=hold)
|
|
274
|
+
|
|
275
|
+
if has_counter:
|
|
276
|
+
counter = generate_counter_melody(
|
|
277
|
+
scale_notes, chord_notes, melody, ROWS_PER_PATTERN, rng=melody_rng,
|
|
278
|
+
window=lead_window)
|
|
279
|
+
counter = add_melody_cuts(counter, ROWS_PER_PATTERN, max_sustain=8)
|
|
280
|
+
apply_notes_to_pattern(
|
|
281
|
+
pat, CH_HARMONY3, counter, sample_map.get("counter", sample_map["harmony"]),
|
|
282
|
+
volume=int(COUNTER_VOL * section_energy), humanize=True, rng=melody_rng,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# ── Bass ──
|
|
286
|
+
if layers["bass"]:
|
|
287
|
+
# Keep the bass present but not relentless: even at full energy a few
|
|
288
|
+
# off-beats drop out, and quiet sections thin further.
|
|
289
|
+
bass = generate_bass_timeline(bass_segments, section_bass,
|
|
290
|
+
weight=bass_weight,
|
|
291
|
+
density=0.6 + 0.35 * section_energy, riff=bass_riff,
|
|
292
|
+
scale_notes=scale_notes)
|
|
293
|
+
bass = floor_bass(bass)
|
|
294
|
+
# Volume-column values are 0-64. The old 150/100 for medium/light were
|
|
295
|
+
# not volumes at all: in the IT volume column 150 is a pan command and
|
|
296
|
+
# 100 a volume slide down, which left light basses nearly silent.
|
|
297
|
+
bass_vol = {"heavy": 255, "medium": 52, "light": 44}.get(bass_weight, 255)
|
|
298
|
+
if use_bass_porta and len(bass) > 1:
|
|
299
|
+
apply_portamento(
|
|
300
|
+
pat, CH_BASS, bass, sample_map["bass"],
|
|
301
|
+
volume=bass_vol, porta_speed=porta_speed, humanize=False,
|
|
302
|
+
)
|
|
303
|
+
else:
|
|
304
|
+
# A pack may carry a second bass sample for the top of the line
|
|
305
|
+
# (an octave pop on a heavy bass thins out on the low sample).
|
|
306
|
+
# Every note picks its sample by pitch; the channel is the same.
|
|
307
|
+
ceiling = {"heavy": 40, "medium": 46, "light": 52}.get(bass_weight, 40)
|
|
308
|
+
hi = sample_map.get("bass_hi")
|
|
309
|
+
groups = [(sample_map["bass"], [n for n in bass if hi is None or n[1] < ceiling])]
|
|
310
|
+
if hi is not None:
|
|
311
|
+
groups.append((hi, [n for n in bass if n[1] >= ceiling]))
|
|
312
|
+
for inst, notes_ in groups:
|
|
313
|
+
apply_notes_to_pattern(
|
|
314
|
+
pat, CH_BASS, notes_, inst,
|
|
315
|
+
volume=bass_vol if bass_vol < 255 else 255,
|
|
316
|
+
humanize=True,
|
|
317
|
+
)
|
|
318
|
+
if pickup_root is not None and bass:
|
|
319
|
+
# Walk up into the next section's root over the last two beats:
|
|
320
|
+
# the fifth below, a step, then the leading tone — the pickup a
|
|
321
|
+
# bass player plays into a chorus.
|
|
322
|
+
target = pickup_root
|
|
323
|
+
lo = min(n for _, n in bass)
|
|
324
|
+
while target - 12 >= lo:
|
|
325
|
+
target -= 12
|
|
326
|
+
while target < 36:
|
|
327
|
+
target += 12
|
|
328
|
+
walk = floor_bass([(ROWS_PER_PATTERN - 8, target - 5), (ROWS_PER_PATTERN - 4, target - 2),
|
|
329
|
+
(ROWS_PER_PATTERN - 2, target - 1)])
|
|
330
|
+
for row in range(ROWS_PER_PATTERN - 8, ROWS_PER_PATTERN):
|
|
331
|
+
pat.data[row][CH_BASS] = ITNote()
|
|
332
|
+
apply_notes_to_pattern(pat, CH_BASS, walk, sample_map["bass"],
|
|
333
|
+
volume=bass_vol if bass_vol < 255 else 255)
|
|
334
|
+
bass = [b for b in bass if b[0] < ROWS_PER_PATTERN - 8] + walk
|
|
335
|
+
if bass_weight in ("light", "medium"):
|
|
336
|
+
cut_after = 2 if bass_weight == "light" else 4
|
|
337
|
+
for row, _ in bass:
|
|
338
|
+
cut_row = row + cut_after
|
|
339
|
+
if cut_row < ROWS_PER_PATTERN:
|
|
340
|
+
if pat.data[cut_row][CH_BASS].note == 0:
|
|
341
|
+
pat.data[cut_row][CH_BASS] = ITNote(note=end_note(sample_map["bass"]))
|
|
342
|
+
|
|
343
|
+
# ── Arpeggio ──
|
|
344
|
+
if layers["arp"]:
|
|
345
|
+
# A figure with accents and rests, chosen per section role, that
|
|
346
|
+
# thins where the lead is busy; see arp.py.
|
|
347
|
+
arp = generate_arp(harmony_segments, arp_figure, section_energy,
|
|
348
|
+
melody_rows={r for r, n in melody if n != -1},
|
|
349
|
+
scale_notes=scale_notes, colour=arp_colour)
|
|
350
|
+
apply_notes_to_pattern(
|
|
351
|
+
pat, CH_ARP, [(r, n) for r, n, _ in arp], sample_map["arp"],
|
|
352
|
+
volume=arp_vol, humanize=False,
|
|
353
|
+
)
|
|
354
|
+
# Accents are the figure's own shape; no jitter on top, or every bar
|
|
355
|
+
# is unique again and the ear reads it as a sequencer.
|
|
356
|
+
for r, _, accent in arp:
|
|
357
|
+
pat.data[r][CH_ARP].volume = max(1, min(64, int(round(arp_vol * accent))))
|
|
358
|
+
|
|
359
|
+
# ── Drums ──
|
|
360
|
+
if layers["drums"]:
|
|
361
|
+
# A four-bar phrase from the section's groove, with its fill (if any)
|
|
362
|
+
# in the last bar, a crash on the first beat of every other pattern
|
|
363
|
+
# and on section starts; see drums.py.
|
|
364
|
+
render_drums(pat, groove, sample_map=sample_map, energy=section_energy,
|
|
365
|
+
bpm=bpm, swing=swing_amount, speed=speed, fill=drum_fill,
|
|
366
|
+
hat_stroke=hat_stroke, crash_chance=crash_chance,
|
|
367
|
+
fill_crash=fill_crash, kit_rng=kit_rng,
|
|
368
|
+
breakdown=breakdown,
|
|
369
|
+
crash_on_one=(chord_idx == 0 and sec_type in ("chorus", "bridge"))
|
|
370
|
+
or (chord_idx % 2 == 0 and section_energy >= 0.8))
|
|
371
|
+
|
|
372
|
+
# ── Intro entries ──
|
|
373
|
+
# In the first intro pattern each part waits for its bar (structure.INTROS);
|
|
374
|
+
# before the echo returns, so nothing is sent from a bar a part is not in.
|
|
375
|
+
if intro and chord_idx == 0:
|
|
376
|
+
for part, bar in intro["enter"].items():
|
|
377
|
+
for ch in PART_CHANNELS[part]:
|
|
378
|
+
for row in range(min(bar * ROWS_PER_BAR, ROWS_PER_PATTERN)):
|
|
379
|
+
pat.data[row][ch] = ITNote()
|
|
380
|
+
|
|
381
|
+
# ── Echo returns ──
|
|
382
|
+
# After every sent part is written: the lead per note, harmony and arp
|
|
383
|
+
# whole. The safety net below bounds the returns like any tonal channel.
|
|
384
|
+
if echo_sends:
|
|
385
|
+
for src, taps, hold, rest, accent in echo_sends:
|
|
386
|
+
if taps:
|
|
387
|
+
apply_echo_taps(pat, src, taps, hold, rest, accent)
|
|
388
|
+
|
|
389
|
+
# ── Sustain safety net ──
|
|
390
|
+
apply_sustain_safety_net(pat)
|
|
391
|
+
|
|
392
|
+
# ── Section transitions ──
|
|
393
|
+
if is_intro and chord_idx == 0 and (intro is None or intro["fade_in"]):
|
|
394
|
+
for ch in [CH_MELODY, CH_HARMONY, CH_HARMONY2, CH_HARMONY3, CH_BASS, CH_ARP,
|
|
395
|
+
*CH_ECHO_ALL]:
|
|
396
|
+
apply_fade(pat, ch, fade_in=True, rows_count=16)
|
|
397
|
+
|
|
398
|
+
if is_ending and chord_idx == n_patterns - 1:
|
|
399
|
+
if ending_style in ("fadeout", "tag"):
|
|
400
|
+
for ch in range(NUM_CHANNELS):
|
|
401
|
+
apply_fade(pat, ch, fade_out=True, rows_count=24)
|
|
402
|
+
|
|
403
|
+
return pat
|