LYGO-Resonance-Engine / standard_engine.py
DeepSeekOracle's picture
Update standard_engine.py
00767c1 verified
Raw
History Blame Contribute Delete
24.6 kB
#!/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)