Last active
January 19, 2026 18:58
-
-
Save twobob/4097c3120b8910501eea282619244e82 to your computer and use it in GitHub Desktop.
I need some breaky beats. Now. no deps
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import struct | |
| import math | |
| import random | |
| import os | |
| # --- MIDI CONSTANTS --- | |
| PPQ = 960 | |
| # General MIDI Map | |
| KICK = 36 | |
| SNARE = 38 | |
| SNARE_RIM = 37 | |
| CL_HAT = 42 | |
| OP_HAT = 46 | |
| PEDAL_HAT = 44 | |
| CRASH = 49 | |
| SPLASH = 55 | |
| RIDE = 51 | |
| RIDE_BELL = 53 | |
| # Toms | |
| TOM_HI = 50 | |
| TOM_MID = 47 | |
| TOM_LO = 43 | |
| def get_variable_length_number(n): | |
| """Convert integer to MIDI variable length bytes.""" | |
| if n < 128: | |
| return bytes([n]) | |
| output = [] | |
| output.append(n & 0x7F) | |
| n >>= 7 | |
| while n > 0: | |
| output.append((n & 0x7F) | 0x80) | |
| n >>= 7 | |
| return bytes(reversed(output)) | |
| def create_midi_event(delta_time, status, data1, data2=None): | |
| """Pack MIDI event data.""" | |
| output = get_variable_length_number(delta_time) | |
| output += bytes([status]) | |
| output += bytes([data1]) | |
| if data2 is not None: | |
| output += bytes([data2]) | |
| return output | |
| def resolve_directory(target_path): | |
| """ | |
| Attempts to create/access the target path. | |
| If it fails (e.g. drive doesn't exist), falls back to local directory. | |
| """ | |
| try: | |
| os.makedirs(target_path, exist_ok=True) | |
| return target_path | |
| except OSError: | |
| print(f"WARNING: Could not write to '{target_path}'. Defaulting to local folder.") | |
| return "." | |
| class FractalGroove: | |
| def __init__(self, bpm=174, swing_pct=58, quant_pct=95, jazziness=0.3, style_mode="random"): | |
| self.bpm = bpm | |
| self.swing_pct = swing_pct | |
| self.quant_pct = quant_pct | |
| self.jazziness = jazziness | |
| # --- PATTERN STYLES --- | |
| styles = { | |
| "Amen-ish": { | |
| "kick": [0, 10], # 1, 3-and | |
| "snare": [4, 12] # 2, 4 | |
| }, | |
| "2-Step": { | |
| "kick": [0, 10, 11], # 1, 3-and, 3-a | |
| "snare": [4, 12] | |
| }, | |
| "Rolling": { | |
| "kick": [0, 7, 10], # 1, 2-a, 3-and | |
| "snare": [4, 12] | |
| }, | |
| "Tech-Step": { | |
| "kick": [0, 2, 10], # 1, 1-and, 3-and | |
| "snare": [4, 12] | |
| } | |
| } | |
| if style_mode == "random": | |
| self.style_name = random.choice(list(styles.keys())) | |
| else: | |
| self.style_name = style_mode if style_mode in styles else "Amen-ish" | |
| self.base_kick = styles[self.style_name]["kick"] | |
| self.base_snare = styles[self.style_name]["snare"] | |
| def get_tick(self, bar, step, current_jitter_profile, is_fill=False): | |
| """Calculates absolute tick with swing and quantize logic.""" | |
| quarter_note = math.floor(step / 4) | |
| step_in_beat = step % 4 | |
| ticks_per_8th = PPQ / 2 | |
| # Absolute start | |
| bar_offset = bar * 4 * PPQ | |
| beat_offset = quarter_note * PPQ | |
| # Swing Logic (MPC Style) | |
| ratio = self.swing_pct / 100.0 | |
| swing_offset = 0 | |
| if step_in_beat == 1: # e | |
| swing_offset = ticks_per_8th * ratio | |
| elif step_in_beat == 2: # & | |
| swing_offset = ticks_per_8th | |
| elif step_in_beat == 3: # a | |
| swing_offset = ticks_per_8th + (ticks_per_8th * ratio) | |
| perfect_tick = bar_offset + beat_offset + swing_offset | |
| # Quantize / Jitter Logic | |
| effective_quant = self.quant_pct + 10 if is_fill else self.quant_pct | |
| effective_quant = min(effective_quant, 100) | |
| deviation_amount = (100 - effective_quant) * 2 | |
| jitter = random.uniform(-deviation_amount, deviation_amount) | |
| final_tick = int(perfect_tick + jitter) | |
| return max(0, final_tick) | |
| def generate_bar_pattern(self, bar_index): | |
| notes = [] | |
| # --- FRACTAL ANALYSIS --- | |
| check = bar_index + 1 | |
| is_4_end = (check % 4 == 0) | |
| is_8_end = (check % 8 == 0) | |
| is_16_end = (check % 16 == 0) | |
| is_32_end = (check % 32 == 0) | |
| is_64_end = (check % 64 == 0) | |
| intensity = 0 | |
| if is_4_end: intensity = 1 | |
| if is_8_end: intensity = 2 | |
| if is_16_end: intensity = 3 | |
| if is_32_end: intensity = 4 | |
| if is_64_end: intensity = 5 | |
| # --- BASE GRID (Hats & Rides) --- | |
| for step in range(16): | |
| cymbal = CL_HAT | |
| if intensity >= 4: cymbal = RIDE | |
| if intensity == 5 and step % 4 == 0: cymbal = CRASH | |
| hat_vel = 80 + (intensity * 4) if step % 2 == 0 else 50 | |
| if step % 2 != 0 and random.random() > 0.3: | |
| notes.append((cymbal, step, hat_vel, False)) | |
| elif step % 2 == 0: | |
| notes.append((cymbal, step, hat_vel, False)) | |
| # --- CORE GROOVE (Kick/Snare) --- | |
| for step in range(16): | |
| if step in self.base_kick: | |
| notes.append((KICK, step, 120, False)) | |
| if step in self.base_snare: | |
| if not (intensity >= 4 and step == 12): | |
| notes.append((SNARE, step, 127, False)) | |
| # --- FILL & GHOST ENGINE --- | |
| max_extras = 2 + int(self.jazziness * 6) | |
| extras_count = 0 | |
| # 1. FILL LOGIC | |
| if is_4_end: | |
| fill_type = 'ghosts' | |
| roll = random.random() | |
| if intensity >= 3 and roll < 0.7: fill_type = 'toms' | |
| elif intensity >= 2 and roll < 0.4: fill_type = 'cymbals' | |
| if fill_type == 'toms': | |
| notes = [n for n in notes if n[1] < 12] | |
| pat = [TOM_HI, TOM_MID, TOM_LO, KICK] | |
| if intensity == 5: pat = [TOM_HI, TOM_HI, TOM_MID, TOM_LO] | |
| for i, inst in enumerate(pat): | |
| notes.append((inst, 12 + i, 115, True)) | |
| elif fill_type == 'cymbals': | |
| positions = [10, 13, 15] | |
| for p in positions: | |
| if random.random() < 0.8: | |
| inst = SPLASH if random.random() < 0.5 else CRASH | |
| notes.append((inst, p, 100, True)) | |
| if intensity >= 3: | |
| notes.append((KICK, p, 90, True)) | |
| else: # Ghosts | |
| possible_steps = [x for x in range(16) if x not in self.base_kick and x not in self.base_snare] | |
| random.shuffle(possible_steps) | |
| for s in possible_steps: | |
| if extras_count >= max_extras: break | |
| if intensity < 2 and (s == 0 or s == 15): continue | |
| if random.random() < self.jazziness: | |
| inst = KICK if random.random() > 0.5 else SNARE | |
| vel = 60 if inst == KICK else 40 | |
| notes.append((inst, s, vel, True)) | |
| extras_count += 1 | |
| # 2. STANDARD BAR GHOSTING | |
| else: | |
| ghost_spots = [2, 3, 8, 9, 11, 14, 15] | |
| for s in ghost_spots: | |
| if extras_count >= (max_extras * 0.5): break | |
| occupied = any(n[1] == s and n[0] in [KICK, SNARE] for n in notes) | |
| if not occupied and random.random() < (self.jazziness * 0.4): | |
| notes.append((SNARE, s, 35, True)) | |
| extras_count += 1 | |
| return notes | |
| def render(self, output_path, filename_prefix, batch_id): | |
| description = f"[{batch_id}] {self.style_name} Sw:{self.swing_pct}% Q:{self.quant_pct}% Jz:{self.jazziness:.1f}" | |
| filename = f"{filename_prefix}_{batch_id:02d}_{self.style_name.replace(' ','')}.mid" | |
| # SAFE PATH RESOLUTION | |
| safe_path = resolve_directory(output_path) | |
| full_path = os.path.join(safe_path, filename) | |
| all_events = [] | |
| print(f"Generating -> {full_path}") | |
| print(f" Details: {description}") | |
| for bar in range(128): | |
| bar_notes = self.generate_bar_pattern(bar) | |
| for (note, step, vel, is_fill) in bar_notes: | |
| start_tick = self.get_tick(bar, step, 0, is_fill) | |
| duration = 60 | |
| all_events.append({'tick': start_tick, 'type': 'on', 'note': note, 'vel': vel}) | |
| all_events.append({'tick': start_tick + duration, 'type': 'off', 'note': note, 'vel': 0}) | |
| all_events.sort(key=lambda x: x['tick']) | |
| # Build Track | |
| track_data = bytearray() | |
| # Track Name | |
| track_name_bytes = description.encode('ascii', 'ignore') | |
| track_data += b'\x00\xFF\x03' + get_variable_length_number(len(track_name_bytes)) + track_name_bytes | |
| # Tempo | |
| mspqn = int(60000000 / self.bpm) | |
| track_data += b'\x00\xFF\x51\x03' + mspqn.to_bytes(3, 'big') | |
| last_tick = 0 | |
| for e in all_events: | |
| delta = e['tick'] - last_tick | |
| last_tick = e['tick'] | |
| status = 0x99 if e['type'] == 'on' else 0x89 | |
| track_data += create_midi_event(delta, status, e['note'], e['vel']) | |
| track_data += b'\x00\xFF\x2F\x00' | |
| header = b'MThd' + (6).to_bytes(4, 'big') + (0).to_bytes(2, 'big') + \ | |
| (1).to_bytes(2, 'big') + PPQ.to_bytes(2, 'big') | |
| chunk = b'MTrk' + len(track_data).to_bytes(4, 'big') + track_data | |
| with open(full_path, 'wb') as f: | |
| f.write(header) | |
| f.write(chunk) | |
| # --- EXECUTION --- | |
| if __name__ == "__main__": | |
| # Requested default path | |
| target_dir = r"E:\SONGS\code Project" | |
| print("--- STARTING BATCH GENERATION (16 FILES) ---") | |
| for i in range(1, 17): | |
| rnd_bpm = random.randint(160, 178) | |
| rnd_swing = random.randint(50, 66) | |
| rnd_quant = random.randint(75, 98) | |
| rnd_jazz = round(random.uniform(0.2, 0.7), 2) | |
| gen = FractalGroove( | |
| bpm=rnd_bpm, | |
| swing_pct=rnd_swing, | |
| quant_pct=rnd_quant, | |
| jazziness=rnd_jazz, | |
| style_mode="random" | |
| ) | |
| gen.render(target_dir, "fractal_batch", i) | |
| print("--- BATCH COMPLETE ---") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment