#!/usr/bin/env python3 """ STANDARD BEAT TOOLS ENGINE – Human Groove Edition Adds swing, velocity variation, fills, complexity control, and mix balance. """ import numpy as np from scipy import signal import soundfile as sf import cv2 from pathlib import Path import hashlib import math # ================================================================ # SAFE ADDITION LAYER # ================================================================ def safe_add(dest, src, start, end): if src is None or len(src) == 0: return dest if src.ndim == 2: src = np.mean(src, axis=1) target_len = end - start if target_len <= 0: return dest if len(src) < target_len: padded = np.zeros(target_len, dtype=np.float32) padded[:len(src)] = src src = padded elif len(src) > target_len: src = src[:target_len] dest[start:end] += src return dest # ================================================================ # BEAT STYLES & HUMANIZATION PRESETS # ================================================================ BEAT_STYLES = { "cinematic": {"base_pattern": "default", "bpm_mod": 0, "energy_mod": 1.0, "density_mod": 1.0, "complexity_mod": 1.0}, "house": {"base_pattern": "four_on_floor", "bpm_mod": 5, "energy_mod": 1.1, "density_mod": 1.2, "complexity_mod": 1.2}, "techno": {"base_pattern": "four_on_floor", "bpm_mod": 10, "energy_mod": 1.3, "density_mod": 1.1, "complexity_mod": 1.1}, "breakbeat": {"base_pattern": "breakbeat", "bpm_mod": -5, "energy_mod": 1.2, "density_mod": 1.4, "complexity_mod": 1.4}, "trap": {"base_pattern": "trap", "bpm_mod": -10, "energy_mod": 1.4, "density_mod": 1.5, "complexity_mod": 1.5}, "lo-fi": {"base_pattern": "default", "bpm_mod": -15, "energy_mod": 0.6, "density_mod": 0.7, "complexity_mod": 0.6}, "dubstep": {"base_pattern": "dubstep", "bpm_mod": -20, "energy_mod": 1.6, "density_mod": 1.3, "complexity_mod": 1.3}, "drum_and_bass": {"base_pattern": "drum_and_bass", "bpm_mod": 30, "energy_mod": 1.5, "density_mod": 1.6, "complexity_mod": 1.6}, "ambient": {"base_pattern": "ambient", "bpm_mod": -20, "energy_mod": 0.4, "density_mod": 0.5, "complexity_mod": 0.4}, } HUMAN_FEELS = { "straight": {"swing_amount": 0.0, "micro_shift": 0.0}, "funk": {"swing_amount": 0.33, "micro_shift": 0.02}, "shuffle": {"swing_amount": 0.4, "micro_shift": 0.03}, "swing": {"swing_amount": 0.25, "micro_shift": 0.015}, "pocket": {"swing_amount": 0.12, "micro_shift": 0.01}, } def generate_standard_audio(image_path, style, seed, duration, noise_filter, bpm, prompt_text, export_stems, export_midi, mix_kick, mix_snare, mix_hats, mix_bass, mix_melody, complexity, density, human_feel, swing_amount, fill_frequency): """ Generate a professional, humanized beat with full mix control. """ # === SEED LOCKING === with open(image_path, 'rb') as f: img_hash = int(hashlib.sha256(f.read()).hexdigest(), 16) if seed and seed != 0: final_seed = int(seed) + (img_hash % 1000000) else: final_seed = img_hash % 1000000 np.random.seed(final_seed) import random random.seed(final_seed) # === IMAGE FEATURES === img = cv2.imread(str(image_path)) if img is None: return None, [], "❌ Could not read image" gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) brightness = float(np.mean(gray)) / 255.0 edge_density = float(np.mean(cv2.Canny(gray, 50, 150) > 0)) avg_color = np.mean(img, axis=(0, 1)) color_r, color_g, color_b = avg_color / 255.0 contrast = float(np.std(gray)) / 255.0 # === BASE PARAMETERS === sr = 44100 duration = float(duration) n = int(sr * duration) t = np.linspace(0, duration, n, dtype=np.float32) # Beat style beat_style = style if style in BEAT_STYLES else "cinematic" style_config = BEAT_STYLES[beat_style] base_pattern = style_config["base_pattern"] bpm = max(60, min(180, int(bpm + style_config["bpm_mod"]))) base_energy = style_config["energy_mod"] * (0.6 + brightness * 0.8) * (0.8 + color_r * 0.4) base_density = style_config["density_mod"] * (0.5 + edge_density * 3.0) * (0.8 + color_g * 0.4) # Apply user complexity/density sliders energy = base_energy * complexity density = base_density * density # === AGGRESSIVE TEXT PROMPT (simplified) === prompt = (prompt_text or "").lower().strip() changes = [] if "heavy" in prompt: energy = min(2.5, energy * 1.5); changes.append("Heavy mode") if "light" in prompt: energy = max(0.3, energy * 0.6); changes.append("Light mode") if "swing" in prompt: human_feel = "swing"; changes.append("Swing groove") # === HUMANIZATION SETUP === if human_feel in HUMAN_FEELS: swing = HUMAN_FEELS[human_feel]["swing_amount"] micro_shift = HUMAN_FEELS[human_feel]["micro_shift"] else: swing = 0.0 micro_shift = 0.0 # Apply user swing slider swing = max(0.0, min(0.5, swing + swing_amount)) # === STRUCTURE === section_durations = [] num_sections = max(3, int(complexity * 4)) for i in range(num_sections): if i == 0: section_durations.append(duration * 0.1) # Intro elif i == num_sections - 1: section_durations.append(duration * 0.1) # Outro else: section_durations.append(duration * (0.8 / (num_sections - 2))) section_durations = [d * (duration / sum(section_durations)) for d in section_durations] # === INSTRUMENT SELECTION === melodic_instruments = ["piano", "guitar", "trumpet", "synth_lead", "pad", "flute", "strings", "marimba", "organ", "bell", "vocal_oh", "brass_stab"] melodic_patterns = ["arpeggio", "chord_stab", "lead_line", "motif", "evolving_pad"] percussion_families = ["standard", "clap", "shaker", "toms", "cymbals", "rimshot", "cowbell", "glitch"] idx_mel = (final_seed + int(brightness * 50) + int(color_g * 30)) % len(melodic_instruments) melodic_instrument = melodic_instruments[idx_mel] idx_pattern = (final_seed + int(contrast * 30)) % len(melodic_patterns) melodic_pattern = melodic_patterns[idx_pattern] idx_perc = (final_seed + int(edge_density * 40)) % len(percussion_families) percussion_family = percussion_families[idx_perc] changes.append(f"Melody: {melodic_instrument}") changes.append(f"Pattern: {melodic_pattern}") changes.append(f"Percussion: {percussion_family}") # === BUILDER FUNCTIONS === beat_duration = 60.0 / bpm num_beats = int(duration / beat_duration) + 2 # Helper to apply swing/micro-shift def apply_groove(beat_index, subdivision, total_subdivisions): """Return shifted time offset based on swing and micro-shift.""" shift = 0.0 # Swing: shift every other 16th note if swing > 0 and subdivision % 2 == 0: beat_pos = subdivision // 2 shift += swing * beat_duration / 8 # Micro-shift: small random offset within a tight range shift += np.random.uniform(-micro_shift, micro_shift) * beat_duration / 16 return shift def create_kick(start, length, energy): length = max(0.001, length) k_len = max(64, int(length * sr)) k_t = np.linspace(0, length, k_len, dtype=np.float32) pitch_start = 80 + np.random.uniform(-10, 10) pitch_end = 30 + np.random.uniform(-5, 5) pitch = np.linspace(pitch_start, pitch_end, k_len) wave = np.sin(2 * np.pi * pitch * k_t) env = np.exp(-k_t * 25) return wave * env * 0.8 * energy def create_snare(start, length, energy, family): length = max(0.001, length) s_len = max(64, int(length * sr)) s_t = np.linspace(0, length, s_len, dtype=np.float32) noise = np.random.normal(0, 1, s_len).astype(np.float32) if family == "clap": b, a = signal.butter(2, [500, 3000], btype='band', fs=sr) env = np.exp(-s_t * 8) elif family == "shaker": b, a = signal.butter(2, [2000, 8000], btype='band', fs=sr) env = np.exp(-s_t * 15) elif family == "toms": b, a = signal.butter(2, [150, 500], btype='band', fs=sr) env = np.exp(-s_t * 12) elif family == "cymbals": b, a = signal.butter(2, [4000, 12000], btype='band', fs=sr) env = np.exp(-s_t * 25) elif family == "rimshot": b, a = signal.butter(2, [1000, 3000], btype='band', fs=sr) env = np.exp(-s_t * 6) elif family == "cowbell": b, a = signal.butter(2, [800, 1200], btype='band', fs=sr) env = np.exp(-s_t * 12) elif family == "glitch": b, a = signal.butter(2, [2000, 6000], btype='band', fs=sr) env = np.exp(-s_t * 5) + 0.2 * np.random.uniform(0, 1, s_len) else: # standard b, a = signal.butter(2, [200, 2000], btype='band', fs=sr) env = np.exp(-s_t * 18) filtered = signal.lfilter(b, a, noise) return filtered * env * 0.5 * energy def create_hat(start, length, density, energy): length = max(0.001, length) h_len = max(64, int(length * sr)) h_t = np.linspace(0, length, h_len, dtype=np.float32) noise = np.random.normal(0, 1, h_len).astype(np.float32) cutoff = noise_filter if noise_filter > 0 else 5000 b, a = signal.butter(2, cutoff, btype='high', fs=sr) filtered = signal.lfilter(b, a, noise) env = np.exp(-h_t * 60) return filtered * env * 0.3 * density * energy def create_bass(start, length, brightness, energy, style): length = max(0.001, length) b_len = max(64, int(length * sr)) b_t = np.linspace(0, length, b_len, dtype=np.float32) base_freq = 30 + (brightness * 80) detune = np.random.uniform(-10, 10) bass_freq = max(20, base_freq + detune) if style == "sine": wave = np.sin(2 * np.pi * bass_freq * b_t) elif style == "saw": wave = (2 * (b_t * bass_freq - np.floor(b_t * bass_freq + 0.5))) elif style == "square": wave = np.sign(np.sin(2 * np.pi * bass_freq * b_t)) elif style == "pluck": wave = np.sin(2 * np.pi * bass_freq * b_t); env = np.exp(-b_t * 12); wave = wave * env elif style == "sub": wave = np.sin(2 * np.pi * bass_freq * b_t) + 0.5 * np.sin(2 * np.pi * bass_freq * 0.5 * b_t) elif style == "wobble": lfo = 0.5 + 0.5 * np.sin(2 * np.pi * 2 * b_t) wave = np.sin(2 * np.pi * bass_freq * b_t) * lfo elif style == "fm": carrier = np.sin(2 * np.pi * bass_freq * b_t) modulator = np.sin(2 * np.pi * bass_freq * 3 * b_t) wave = np.sin(2 * np.pi * bass_freq * b_t + 2 * modulator) else: wave = np.sin(2 * np.pi * bass_freq * b_t) env = np.exp(-b_t * 4) return wave * env * 0.4 * energy def create_melody(length, brightness, energy, instrument, pattern, section_index): length = max(0.001, length) if instrument in ["piano", "guitar", "marimba", "bell"]: note_pool = np.array([261.6, 293.7, 329.6, 349.2, 392.0, 440.0, 493.9]) elif instrument in ["synth_lead", "pad", "organ"]: note_pool = np.array([261.6, 293.7, 329.6, 392.0, 440.0, 523.3, 587.3]) elif instrument in ["trumpet", "brass_stab", "vocal_oh"]: note_pool = np.array([261.6, 329.6, 392.0, 523.3, 659.3, 784.0]) else: note_pool = np.array([261.6, 293.7, 329.6, 349.2, 392.0, 440.0, 493.9]) if pattern == "arpeggio": num_notes = 8; note_seq = [note_pool[i % len(note_pool)] for i in range(num_notes)]; durations = np.ones(num_notes) * 0.125 elif pattern == "chord_stab": num_notes = 4; root = note_pool[0]; third = note_pool[2] * 0.95; fifth = note_pool[4] * 0.95 note_seq = [root, third, fifth, root]; durations = np.ones(num_notes) * 0.25 elif pattern == "lead_line": num_notes = 12; note_seq = [note_pool[int(np.random.choice(len(note_pool)))] for _ in range(num_notes)] durations = np.random.uniform(0.125, 0.5, num_notes) elif pattern == "motif": num_notes = 6; note_seq = [note_pool[0], note_pool[2], note_pool[4], note_pool[2], note_pool[0], note_pool[3]] durations = np.ones(num_notes) * 0.25 else: # evolving_pad num_notes = 16; note_seq = [note_pool[int((i * 0.618) % len(note_pool))] for i in range(num_notes)] durations = np.ones(num_notes) * 0.5 total_dur = np.sum(durations) durations = durations * (length / total_dur) melody = np.zeros(int(length * sr), dtype=np.float32) current_time = 0.0 for note, dur in zip(note_seq, durations): start = int(current_time * sr) end = min(start + int(dur * sr), len(melody)) t_note = np.linspace(0, dur, end-start, dtype=np.float32) if instrument in ["piano", "marimba", "bell"]: wave = np.sin(2 * np.pi * note * t_note) * np.exp(-t_note * 2.5) elif instrument in ["synth_lead", "organ"]: wave = (2 * (t_note * note - np.floor(t_note * note + 0.5))) * 0.5 elif instrument in ["trumpet", "brass_stab"]: wave = np.sin(2 * np.pi * note * t_note) * (1 + 0.3 * np.sin(2 * np.pi * note * 3 * t_note)) elif instrument in ["vocal_oh"]: wave = np.sin(2 * np.pi * note * t_note) * 0.5 + 0.3 * np.sin(2 * np.pi * note * 2 * t_note) elif instrument in ["flute"]: wave = np.sin(2 * np.pi * note * t_note) * (1 + 0.1 * np.sin(2 * np.pi * note * 2 * t_note)) * np.exp(-t_note * 3) else: wave = np.sin(2 * np.pi * note * t_note) * (1 + 0.2 * np.sin(2 * np.pi * note * 1.5 * t_note)) section_factor = 0.8 + 0.2 * np.sin(2 * np.pi * section_index * 2) melody[start:end] += wave * energy * section_factor * 0.5 current_time += dur return melody # === MAIN PATTERN ENGINE WITH HUMANIZATION === audio = np.zeros((n, 2), dtype=np.float32) kick = np.zeros(n, dtype=np.float32) snare = np.zeros(n, dtype=np.float32) hats = np.zeros(n, dtype=np.float32) bass = np.zeros(n, dtype=np.float32) melody = np.zeros(n, dtype=np.float32) current_time = 0.0 total_beats_played = 0 for section_idx, section_dur in enumerate(section_durations): section_end = current_time + section_dur section_length = section_end - current_time num_beats_section = int(section_length / beat_duration) + 2 # Section energy modulation if section_idx == 0: # Intro section_energy = energy * 0.4 section_density = density * 0.3 elif section_idx == len(section_durations) - 1: # Outro section_energy = energy * 0.3 section_density = density * 0.2 elif section_idx % 2 == 0: # Chorus section_energy = energy * 1.2 section_density = density * 1.1 else: # Verse section_energy = energy * 0.8 section_density = density * 0.8 # Check for fill (every fill_frequency bars) fill_mode = False if fill_frequency > 0 and total_beats_played > 0 and total_beats_played % (fill_frequency * 4) < 4: fill_mode = True for beat in range(num_beats_section): absolute_beat = total_beats_played + beat start = int((current_time + beat * beat_duration) * sr) if start >= n: break # ----- KICK ----- kick_vel = 0.8 * section_energy if fill_mode: # In a fill, kick might be sparse or hit on non-standard beats if np.random.random() < 0.4: kick_segment = create_kick(start, 0.18, kick_vel) safe_add(kick, kick_segment, start, min(start + int(0.18 * sr), n)) elif base_pattern in ["four_on_floor", "trap", "dubstep"]: if beat % 2 == 0: kick_segment = create_kick(start, 0.18, kick_vel) safe_add(kick, kick_segment, start, min(start + int(0.18 * sr), n)) elif base_pattern == "breakbeat": if beat % 2 == 0: kick_segment = create_kick(start, 0.18, kick_vel) safe_add(kick, kick_segment, start, min(start + int(0.18 * sr), n)) elif base_pattern == "drum_and_bass": if beat % 4 in [0, 3]: kick_segment = create_kick(start, 0.16, kick_vel * 1.2) safe_add(kick, kick_segment, start, min(start + int(0.16 * sr), n)) else: if beat % 2 == 0 and np.random.random() < 0.8: kick_segment = create_kick(start, 0.18, kick_vel) safe_add(kick, kick_segment, start, min(start + int(0.18 * sr), n)) # ----- SNARE ----- snare_vel = 0.7 * section_energy if fill_mode: if np.random.random() < 0.6: snare_segment = create_snare(start, 0.12, snare_vel, percussion_family) safe_add(snare, snare_segment, start, min(start + int(0.12 * sr), n)) elif base_pattern in ["four_on_floor", "trap", "dubstep"]: if beat % 2 == 1: snare_segment = create_snare(start, 0.12, snare_vel, percussion_family) safe_add(snare, snare_segment, start, min(start + int(0.12 * sr), n)) elif base_pattern == "breakbeat": if beat % 8 == 3 or beat % 8 == 7: snare_segment = create_snare(start, 0.12, snare_vel, percussion_family) safe_add(snare, snare_segment, start, min(start + int(0.12 * sr), n)) elif base_pattern == "drum_and_bass": if beat % 8 == 2 or beat % 8 == 6: snare_segment = create_snare(start, 0.10, snare_vel * 1.1, percussion_family) safe_add(snare, snare_segment, start, min(start + int(0.10 * sr), n)) else: if beat % 4 == 2 and np.random.random() < 0.8: snare_segment = create_snare(start, 0.12, snare_vel * 0.5, percussion_family) safe_add(snare, snare_segment, start, min(start + int(0.12 * sr), n)) # ----- HATS ----- hat_vel = 0.3 * section_density * section_energy if fill_mode: # Fills often have more hats hat_subdivision = 4 else: hat_subdivision = 2 if base_pattern == "drum_and_bass" else 2 for tick in range(hat_subdivision): pos = current_time + beat * beat_duration + tick * (beat_duration / hat_subdivision) # Apply swing and micro-shift to hi-hat timing shift = apply_groove(total_beats_played + beat, tick, hat_subdivision) pos += shift start_hat = int(pos * sr) if start_hat >= n: break # Velocity variation (human feel) var_vel = hat_vel * (0.7 + 0.3 * np.random.random()) hat_segment = create_hat(start_hat, 0.04, var_vel, section_energy) safe_add(hats, hat_segment, start_hat, min(start_hat + int(0.04 * sr), n)) # ----- BASS ----- bass_vel = 0.8 * section_energy # Bass on every beat, but with variation if not fill_mode or np.random.random() < 0.7: bass_style = ["sine", "saw", "square", "pluck", "sub", "wobble", "fm"][ (final_seed + section_idx * 3 + int(brightness * 20)) % 7 ] bass_segment = create_bass(start, 0.25, brightness, bass_vel, bass_style) safe_add(bass, bass_segment, start, min(start + int(0.25 * sr), n)) # ----- MELODY ----- # Melody in verses and choruses, not in intro/outro if section_idx not in [0, len(section_durations)-1] and np.random.random() < 0.9: melody_length = section_dur * 0.6 melody_start = current_time + section_dur * 0.2 + np.random.uniform(-0.05, 0.05) melody_segment = create_melody(melody_length, brightness, section_energy, melodic_instrument, melodic_pattern, section_idx) # Apply velocity variation to melody melody_segment *= (0.7 + 0.3 * np.random.random()) melody_start_sample = int(melody_start * sr) melody_end_sample = min(melody_start_sample + len(melody_segment), n) safe_add(melody, melody_segment, melody_start_sample, melody_end_sample) current_time = section_end total_beats_played += num_beats_section # === STEREO CONVERSION & MIX === def stereo_embed(mono, pan=0.0): if mono is None or len(mono) == 0: return np.zeros((n, 2), dtype=np.float32) left = math.cos((pan + 1) * math.pi / 4) right = math.sin((pan + 1) * math.pi / 4) return np.column_stack((mono * left, mono * right)).astype(np.float32) # Apply mix sliders audio += stereo_embed(kick, 0.0) * mix_kick audio += stereo_embed(snare, 0.2) * mix_snare audio += stereo_embed(hats, -0.2) * mix_hats audio += stereo_embed(bass, 0.0) * mix_bass audio += stereo_embed(melody, 0.1) * mix_melody # Final polish b, a = signal.butter(2, 10000, btype='low', fs=sr) audio = signal.lfilter(b, a, audio, axis=0) audio = np.tanh(audio * 1.5) / 1.5 peak = np.max(np.abs(audio)) if peak > 0.001: audio = audio / peak * 0.95 # Write output out_path = f"standard_{Path(image_path).stem}.wav" sf.write(str(out_path), audio, sr) downloadable = [out_path] if export_stems: base = out_path.replace(".wav", "") for comp, name in [(kick, "kick"), (snare, "snare"), (hats, "hats"), (bass, "bass"), (melody, "melody")]: cs = stereo_embed(comp, 0.0) cpk = np.max(np.abs(cs)) if cpk > 0.001: cs = cs / cpk * 0.95 sp = f"{base}_{name}.wav" sf.write(str(sp), cs, sr) downloadable.append(sp) if export_midi: try: from mido import MidiFile, MidiTrack, Message mid = MidiFile() track = MidiTrack() mid.tracks.append(track) ticks_per_beat = 480 tempo = bpm tick_offset = 0 for beat in range(num_beats): start_ticks = int(beat * beat_duration * ticks_per_beat * (tempo / 60)) track.append(Message('note_on', note=36, velocity=80, time=start_ticks - tick_offset)) track.append(Message('note_off', note=36, velocity=80, time=int(0.15 * ticks_per_beat * (tempo / 60)))) tick_offset = start_ticks + int(0.15 * ticks_per_beat * (tempo / 60)) if beat % 2 == 1: track.append(Message('note_on', note=38, velocity=70, time=0)) track.append(Message('note_off', note=38, velocity=70, time=int(0.1 * ticks_per_beat * (tempo / 60)))) track.append(Message('note_on', note=28, velocity=60, time=0)) track.append(Message('note_off', note=28, velocity=60, time=int(0.25 * ticks_per_beat * (tempo / 60)))) mid_path = out_path.replace(".wav", ".mid") mid.save(mid_path) downloadable.append(mid_path) except ImportError: pass log = [ f"✅ Standard Beat Tools – Human Groove", f"Style: {beat_style} | Feel: {human_feel}", f"BPM: {bpm} | Duration: {duration}s | Seed: {final_seed}", f"Kick: {mix_kick:.2f} | Snare: {mix_snare:.2f} | Hats: {mix_hats:.2f} | Bass: {mix_bass:.2f} | Melody: {mix_melody:.2f}", f"Complexity: {complexity:.1f} | Density: {density:.1f} | Fill Freq: {fill_frequency} bars", f"Melody: {melodic_instrument} ({melodic_pattern}) | Percussion: {percussion_family}", ] if changes: log.extend(changes) return out_path, downloadable, "\n".join(log)