| |
| """ |
| 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 |
|
|
| |
| |
| |
| 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 = { |
| "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. |
| """ |
| |
| 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) |
|
|
| |
| 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 |
|
|
| |
| sr = 44100 |
| duration = float(duration) |
| n = int(sr * duration) |
| t = np.linspace(0, duration, n, dtype=np.float32) |
|
|
| |
| 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) |
|
|
| |
| energy = base_energy * complexity |
| density = base_density * density |
|
|
| |
| 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") |
|
|
| |
| 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 |
| |
| swing = max(0.0, min(0.5, swing + swing_amount)) |
|
|
| |
| section_durations = [] |
| num_sections = max(3, int(complexity * 4)) |
| for i in range(num_sections): |
| if i == 0: |
| section_durations.append(duration * 0.1) |
| elif i == num_sections - 1: |
| section_durations.append(duration * 0.1) |
| else: |
| section_durations.append(duration * (0.8 / (num_sections - 2))) |
| section_durations = [d * (duration / sum(section_durations)) for d in section_durations] |
|
|
| |
| 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}") |
|
|
| |
| beat_duration = 60.0 / bpm |
| num_beats = int(duration / beat_duration) + 2 |
|
|
| |
| def apply_groove(beat_index, subdivision, total_subdivisions): |
| """Return shifted time offset based on swing and micro-shift.""" |
| shift = 0.0 |
| |
| if swing > 0 and subdivision % 2 == 0: |
| beat_pos = subdivision // 2 |
| shift += swing * beat_duration / 8 |
| |
| 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: |
| 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: |
| 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 |
|
|
| |
| 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 |
|
|
| |
| if section_idx == 0: |
| section_energy = energy * 0.4 |
| section_density = density * 0.3 |
| elif section_idx == len(section_durations) - 1: |
| section_energy = energy * 0.3 |
| section_density = density * 0.2 |
| elif section_idx % 2 == 0: |
| section_energy = energy * 1.2 |
| section_density = density * 1.1 |
| else: |
| section_energy = energy * 0.8 |
| section_density = density * 0.8 |
|
|
| |
| 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_vel = 0.8 * section_energy |
| if fill_mode: |
| |
| 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_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)) |
|
|
| |
| hat_vel = 0.3 * section_density * section_energy |
| if fill_mode: |
| |
| 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) |
| |
| shift = apply_groove(total_beats_played + beat, tick, hat_subdivision) |
| pos += shift |
| start_hat = int(pos * sr) |
| if start_hat >= n: break |
| |
| 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_vel = 0.8 * section_energy |
| |
| 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)) |
|
|
| |
| |
| 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) |
| |
| 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 |
|
|
| |
| 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) |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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) |