LYGO-Resonance-Engine / resonance_engine.py
Justin Prince
fix: unique per-image style DNA + LYGO Protocol audio crash
71d80d2
Raw
History Blame Contribute Delete
27.7 kB
#!/usr/bin/env python3
"""
LYGO Resonance Engine v0.5.2
Image β†’ Living Stereo Soundscape
Full standard synthesis + LDQ percussion mode.
"""
import cv2
import numpy as np
import soundfile as sf
import math
import argparse
import sys
import hashlib
from pathlib import Path
from typing import Optional, Dict, Any
# mido is optional β€” only required when export_midi=True
try:
import mido
from mido import MidiFile, MidiTrack, Message
HAS_MIDO = True
except ImportError: # pragma: no cover
mido = None # type: ignore
MidiFile = MidiTrack = Message = None # type: ignore
HAS_MIDO = False
# LDQ imports are LAZY: only loaded inside the LDQ branch.
# This keeps the "Standard Beat Tools / Factory Default" path completely independent
# of any LDQ code so the default engine always works even if LDQ modules have issues.
__version__ = "0.5.3"
# Artistic Presets
PRESETS = {
"raw": {},
"ambient": {
"noise_vol": 0.03,
"drone_vol": 0.04,
"note_vol": 0.08,
"glitch_vol": 0.005,
"drone_attack": 6.0,
"drone_decay": 6.0,
"note_attack": 0.08,
"note_decay": 0.45,
"max_glitches": 5,
"noise_lowpass_hz": 350,
"root_freq_range": (40, 80),
"theta_lock_range": (5, 9),
},
# "musical" / factory-strong defaults (for Standard Beat Tools to produce nice non-static output
# matching historical working versions from LDQ protocol history + clean skill base).
# These ensure the 4-layer (noise + drones + melody + glitch) has energy + filtered texture.
"musical": {
"noise_vol": 0.09,
"drone_vol": 0.08,
"note_vol": 0.13,
"glitch_vol": 0.02,
"noise_lowpass_hz": 650,
"drone_attack": 4.0,
"drone_decay": 5.0,
"note_decay": 0.35,
},
"glitch": {
"noise_vol": 0.08,
"drone_vol": 0.04,
"note_vol": 0.06,
"glitch_vol": 0.03,
"max_notes": 6,
"max_glitches": 20,
"note_decay": 0.20,
"glitch_decay": 0.02,
"noise_lowpass_hz": 1800,
"root_freq_range": (30, 60),
"theta_lock_range": (4, 8),
},
"ethereal": {
"noise_vol": 0.02,
"drone_vol": 0.04,
"note_vol": 0.10,
"glitch_vol": 0.008,
"root_freq_range": (40, 95),
"theta_lock_range": (6, 13),
"note_attack": 0.10,
"note_decay": 0.55,
"noise_lowpass_hz": 300,
"max_glitches": 4,
},
"cinematic": {
"noise_vol": 0.04,
"drone_vol": 0.06,
"note_vol": 0.10,
"glitch_vol": 0.015,
"drone_attack": 4.5,
"drone_decay": 4.5,
"max_drones": 4,
"noise_lowpass_hz": 700,
"root_freq_range": (30, 70),
"theta_lock_range": (4.5, 10),
},
}
class ResonanceEngine:
def __init__(self, config: Optional[Dict[str, Any]] = None):
self.config = {
"sr": 44100,
"duration": 15.0,
"global_fade": 0.7,
"soft_clip": True,
"soft_clip_amount": 1.4,
"max_drones": 4,
"max_notes": 8,
"max_glitches": 15,
"noise_vol": 0.05,
"drone_vol": 0.04,
"note_vol": 0.10,
"glitch_vol": 0.015,
"root_freq_range": (30, 70),
"theta_lock_range": (4.5, 11),
"drone_attack": 3.5,
"drone_decay": 3.5,
"note_attack": 0.04,
"note_decay": 0.30,
"glitch_attack": 0.005,
"glitch_decay": 0.015,
"noise_lowpass_hz": 700,
"random_seed": None,
"verbose": True,
"export_stems": False,
"export_midi": False,
"use_ldq": False,
"genre_manifold": "None",
"percussion_mode": "standard",
"perceptual_polish": 0.5,
"tempo_bpm": 140,
"tempo_subdivision": 16,
"swing": 0.0,
}
if config:
self.config.update(config)
self.image_path = None
self.tempo_grid = None
def _log(self, msg: str):
if self.config.get("verbose", True):
print(msg)
def analyze_image(self, image_path: str) -> Dict[str, Any]:
img = cv2.imread(str(image_path))
if img is None:
raise FileNotFoundError(f"Could not load image: {image_path}")
if len(img.shape) == 2:
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
# Cap analysis size for speed/stability, keep full-res stats via scale
h0, w0 = img.shape[:2]
scale = 1.0
if max(h0, w0) > 960:
scale = 960.0 / max(h0, w0)
img = cv2.resize(img, (int(w0 * scale), int(h0 * scale)), interpolation=cv2.INTER_AREA)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
h, w = gray.shape
avg_blue, avg_green, avg_red, _ = cv2.mean(img)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
avg_hue = float(np.mean(hsv[:, :, 0]) * 2.0)
avg_sat = float(np.mean(hsv[:, :, 1]) / 255.0)
brightness = float(np.mean(gray) / 255.0)
contrast = float(np.std(gray) / 255.0)
edges = cv2.Canny(gray, 50, 150)
edge_density = float(np.sum(edges > 0) / max(1, h * w))
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
lines = cv2.HoughLinesP(edges, 1, np.pi / 180, 50, minLineLength=28, maxLineGap=12)
fast = cv2.FastFeatureDetector_create(threshold=38)
keypoints = fast.detect(gray, None)
# Content fingerprint β†’ deterministic unique RNG seed per photo
thumb = cv2.resize(gray, (64, 48), interpolation=cv2.INTER_AREA)
content_hash = hashlib.sha256(thumb.tobytes()).hexdigest()
image_seed = int(content_hash[:8], 16)
return {
"width": w, "height": h,
"avg_red": avg_red, "avg_green": avg_green, "avg_blue": avg_blue,
"avg_hue": avg_hue,
"avg_sat": avg_sat,
"brightness": brightness,
"contrast": contrast,
"edge_density": edge_density,
"contours": contours,
"lines": lines if lines is not None else [],
"keypoints": keypoints,
"content_hash": content_hash[:16],
"image_seed": image_seed,
"average_brightness": brightness, # LDQ hihat compat
}
def _generate_tone(self, freq: float, duration: float, wave_type: str = "sine") -> np.ndarray:
sr = self.config["sr"]
n = int(sr * duration)
if n <= 0:
return np.zeros(1, dtype=np.float32)
t = np.linspace(0, duration, n, dtype=np.float32)
if wave_type == "sine":
return np.sin(freq * t * 2 * np.pi).astype(np.float32)
elif wave_type == "sawtooth":
return (2 * (t * freq - np.floor(0.5 + t * freq)) * 0.6).astype(np.float32)
elif wave_type == "noise":
return np.random.uniform(-0.3, 0.3, n).astype(np.float32)
return np.zeros(n, dtype=np.float32)
def _apply_envelope(self, audio: np.ndarray, attack: float, decay: float) -> np.ndarray:
sr = self.config["sr"]
n = len(audio)
if n <= 0:
return audio
a = max(1, int(attack * sr))
d = max(1, int(decay * sr))
env = np.ones(n, dtype=np.float32)
if n > a + d:
env[:a] = np.linspace(0, 1, a, dtype=np.float32)
env[-d:] = np.linspace(1, 0, d, dtype=np.float32)
return (audio * env).astype(np.float32)
def _stereo_pan(self, mono: np.ndarray, pan: float) -> np.ndarray:
if mono.ndim == 1:
mono = mono[:, np.newaxis]
pan = max(-1.0, min(1.0, pan))
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)
def _fft_lowpass(self, audio: np.ndarray, cutoff_hz: float) -> np.ndarray:
if cutoff_hz <= 0 or len(audio) < 32:
return audio
sr = self.config["sr"]
n = len(audio)
fft = np.fft.rfft(audio)
freqs = np.fft.rfftfreq(n, 1.0 / sr)
fft[freqs > cutoff_hz] = 0
return np.fft.irfft(fft, n=n).real.astype(np.float32)
def _soft_limit(self, audio: np.ndarray) -> np.ndarray:
if self.config["soft_clip"]:
amt = self.config["soft_clip_amount"]
return (np.tanh(audio * amt) / np.tanh(amt)).astype(np.float32)
return audio
def _freq_to_midi(self, freq: float) -> int:
if freq <= 0:
return 0
return max(0, min(127, int(12 * math.log2(freq / 440) + 69)))
def synthesize(self, features: Dict[str, Any], output_path: str):
cfg = self.config
# Prefer explicit seed; else unique deterministic seed from image content
if cfg.get("random_seed") is not None:
rng_seed = int(cfg["random_seed"])
else:
rng_seed = int(features.get("image_seed") or 0)
np.random.seed(rng_seed)
self._log(f" LOG: RNG_SEED={rng_seed} content_hash={features.get('content_hash', '?')}")
sr = cfg["sr"]
duration = cfg["duration"]
n_total = int(sr * duration)
if n_total <= 0:
sf.write(output_path, np.zeros((1, 2)), sr)
return
audio = np.zeros((n_total, 2), dtype=np.float32)
# Multi-axis root/theta from color (more unique than red/green alone)
hue = float(features.get("avg_hue", features["avg_red"]))
sat = float(features.get("avg_sat", 0.4))
root = np.interp(features["avg_red"], [0, 255], cfg["root_freq_range"])
root *= 0.92 + 0.16 * (hue / 360.0) # hue detunes root per image
root *= 0.95 + 0.1 * sat
theta = np.interp(features["avg_green"], [0, 255], cfg["theta_lock_range"])
theta *= 0.9 + 0.2 * float(features.get("contrast", 0.2))
w, h = features["width"], features["height"]
# ---- LDQ Protocol Percussion Mode (LAZY imports + guarded) ----
# Enable when use_ldq True OR percussion_mode explicitly "ldq"
use_ldq_path = bool(cfg.get("use_ldq")) or str(cfg.get("percussion_mode", "")).lower() == "ldq"
if use_ldq_path:
try:
import ldq_fingerprint
import ldq_genre_manifold
import ldq_percussion
import ldq_perceptual_layer
import ldq_tempo_grid
import ldq_music_production
except Exception as imp_err:
self._log(f"⚠️ LDQ import failed ({imp_err}) β€” falling back to standard synthesis")
use_ldq_path = False
if use_ldq_path:
self.tempo_grid = ldq_tempo_grid.TempoGrid(
bpm=cfg.get("tempo_bpm", 140),
subdivision=cfg.get("tempo_subdivision", 16),
swing=cfg.get("swing", 0.0)
)
self._log("πŸ”¬ LDQ Percussion Mode active")
try:
if self.image_path is not None:
vgh = ldq_fingerprint.compute_vgh(self.image_path)
self._log(f"πŸ”‘ VGH: {vgh[:16]}...")
else:
vgh = None
if cfg.get("genre_manifold") and cfg["genre_manifold"] != "None":
self._log(f"🎡 Genre: {cfg['genre_manifold']}")
genre_params = ldq_genre_manifold.project_to_genre(features, cfg["genre_manifold"])
theta = theta * (1 + genre_params.get("swing_amount", 0))
if cfg["genre_manifold"] == "Dubstep":
cfg["tempo_bpm"] = 140
cfg["swing"] = 0.25
elif cfg["genre_manifold"] == "Phonk":
cfg["tempo_bpm"] = 100
cfg["swing"] = 0.33
elif cfg["genre_manifold"] == "Industrial":
cfg["tempo_bpm"] = 120
cfg["swing"] = 0.15
self.tempo_grid = ldq_tempo_grid.TempoGrid(
bpm=cfg["tempo_bpm"],
subdivision=cfg["tempo_subdivision"],
swing=cfg["swing"]
)
# One-shot drums placed on a tempo grid (not one long sweep)
bpm = float(cfg.get("tempo_bpm", 140))
beat = 60.0 / max(1.0, bpm)
kick_hit = ldq_percussion.generate_kick(features, sr, min(0.5, duration))
snare_hit = ldq_percussion.generate_snare(features, sr, min(0.2, duration))
hihats = ldq_percussion.generate_hihats(features, sr, bpm, duration)
kick_track = np.zeros(n_total, dtype=np.float32)
snare_track = np.zeros(n_total, dtype=np.float32)
# Kick on 1 & 3, snare on 2 & 4 β€” unique swing from image seed
swing = float(cfg.get("swing", 0.0))
n_beats = int(duration / beat) + 2
for bi in range(n_beats):
t0 = bi * beat
if swing > 0 and bi % 2 == 1:
t0 += swing * beat * 0.25
idx = int(t0 * sr)
if bi % 2 == 0:
end = min(idx + len(kick_hit), n_total)
if idx < n_total:
kick_track[idx:end] += kick_hit[: end - idx]
else:
end = min(idx + len(snare_hit), n_total)
if idx < n_total:
snare_track[idx:end] += snare_hit[: end - idx]
min_len = min(len(kick_track), len(snare_track), len(hihats), n_total)
audio[:min_len] += self._stereo_pan(kick_track[:min_len], 0.0) * 0.65
audio[:min_len] += self._stereo_pan(snare_track[:min_len], 0.15) * 0.45
audio[:min_len] += self._stereo_pan(hihats[:min_len], -0.2) * 0.35
# Layer standard tonal content under drums for richness unique to image
if features["edge_density"] > 0.005:
noise = self._generate_tone(0, duration, "noise")
if cfg.get("noise_lowpass_hz", 0) > 0:
noise = self._fft_lowpass(noise, cfg["noise_lowpass_hz"])
audio += self._stereo_pan(noise, 0.0) * min(0.04, cfg.get("noise_vol", 0.05))
try:
audio = ldq_music_production.apply_music_production(
audio, features, sr, bpm=int(cfg["tempo_bpm"]), sidechain=True
)
except Exception as mix_err:
self._log(f"⚠️ music_production skipped: {mix_err}")
if cfg.get("perceptual_polish", 0.0) > 0:
try:
audio = ldq_perceptual_layer.apply_perceptual_mixing(audio, features, sr)
except Exception as pol_err:
self._log(f"⚠️ perceptual polish skipped: {pol_err}")
if vgh is not None:
try:
audio = ldq_fingerprint.embed_fingerprint(audio, sr, vgh)
self._log("πŸ” Fingerprint embedded")
except Exception as fp_err:
self._log(f"⚠️ fingerprint skipped: {fp_err}")
audio = self._soft_limit(audio)
fade = int(cfg["global_fade"] * sr)
if fade > 0 and n_total > fade * 2:
audio[:fade] *= np.linspace(0, 1, fade)[:, np.newaxis]
audio[-fade:] *= np.linspace(1, 0, fade)[:, np.newaxis]
peak = float(np.max(np.abs(audio)))
if peak > 1e-6:
audio = audio / peak * 0.97
sf.write(output_path, audio, sr)
self._log(f"βœ“ Saved LDQ: {output_path} peak={peak:.3f}")
return
except Exception as ldq_err:
self._log(f"⚠️ LDQ path failed ({ldq_err}) β€” falling back to standard synthesis")
# fall through to standard 4-layer path
audio = np.zeros((n_total, 2), dtype=np.float32)
# ===== STANDARD SYNTHESIS (Original 4-layer system) =====
# FOUNDATION LOG: This is the rock-solid working base ("buzzing + clear sound").
# All advanced controls (BPM, swing, etc.) are mapped here lightly as STRICT modules
# so they enhance without pulling full LDQ (per rebuild plan + user testing).
self._log("🎡 Standard Synthesis active (FOUNDATION)")
# === DETAILED FOUNDATION LOGGING (for methodical debugging) ===
use_bpm_influence = True # strict module: light tempo effect even in pure factory
effective_bpm = cfg.get("tempo_bpm", 140)
effective_swing = cfg.get("swing", 0.0)
self._log(f" LOG: PATH=STANDARD | bpm={effective_bpm} | swing={effective_swing} | seed={cfg.get('random_seed')} | noise_lowpass={cfg.get('noise_lowpass_hz')}")
self._log(f" LOG: PRESET_VOLS noise={cfg.get('noise_vol'):.3f} drone={cfg.get('drone_vol'):.3f} note={cfg.get('note_vol'):.3f} glitch={cfg.get('glitch_vol'):.3f}")
# Collections for stems
audio_noise = np.zeros((n_total, 2), dtype=np.float32)
audio_drone = np.zeros((n_total, 2), dtype=np.float32)
audio_melody = np.zeros((n_total, 2), dtype=np.float32)
audio_glitch = np.zeros((n_total, 2), dtype=np.float32)
melody_events = []
# Layer 1: Texture Floor (Noise)
if features["edge_density"] > 0.007:
noise = self._generate_tone(0, duration, "noise")
if cfg["noise_lowpass_hz"] > 0:
noise = self._fft_lowpass(noise, cfg["noise_lowpass_hz"])
noise = self._apply_envelope(noise, cfg["drone_attack"], cfg["drone_decay"])
vol = min(features["edge_density"] * 1.0, cfg["noise_vol"])
stereo_noise = self._stereo_pan(noise, 0.0) * vol
audio += stereo_noise
audio_noise += stereo_noise
# Layer 2: Drones (Lines) β€” robust unpack for OpenCV Hough line shapes
raw_lines = features.get("lines")
if raw_lines is None:
lines = []
elif isinstance(raw_lines, np.ndarray):
lines = list(raw_lines)
else:
lines = list(raw_lines)
for i, line in enumerate(lines[: cfg["max_drones"]]):
try:
pts = np.array(line).reshape(-1)
if pts.size < 4:
continue
x1, y1, x2, y2 = float(pts[0]), float(pts[1]), float(pts[2]), float(pts[3])
except Exception:
continue
length = math.hypot(x2 - x1, y2 - y1)
detune = (i * 0.7) + (rng_seed % 11) * 0.05
freq = root + (max(1, int(length / 48)) * theta * 0.55) + detune
tone = self._generate_tone(freq, duration, "sawtooth")
tone = self._apply_envelope(tone, cfg["drone_attack"], cfg["drone_decay"])
pan = (x1 / max(1, w)) * 2 - 1
stereo_drone = self._stereo_pan(tone, pan) * cfg["drone_vol"]
audio += stereo_drone
audio_drone += stereo_drone
# Layer 3: Contours β†’ Melody
valid = [c for c in features["contours"] if 90 < cv2.contourArea(c) < (w * h * 0.6)]
valid.sort(key=lambda c: cv2.boundingRect(c)[0])
# STRICT BPM MODULE (light, foundation-safe): scale timing by tempo so BPM slider does audible work
# without full LDQ TempoGrid. This is the "mapped perfectly" enhancement.
tempo_factor = 120.0 / max(1, effective_bpm) # >1 for slower BPM = longer spacing
swing_amount = effective_swing
for i, cnt in enumerate(valid[:cfg["max_notes"]]):
area = cv2.contourArea(cnt)
verts = len(cv2.approxPolyDP(cnt, 0.04 * cv2.arcLength(cnt, True), True))
freq = (root * 3.7) + (verts * theta * 1.6)
dur = min(2.6, 0.22 + (area / 13500))
tone = self._generate_tone(freq, dur, "sine")
tone = self._apply_envelope(tone, cfg["note_attack"], cfg["note_decay"])
M = cv2.moments(cnt)
cx = int(M["m10"] / M["m00"]) if M["m00"] != 0 else cv2.boundingRect(cnt)[0]
start = (cx / w) * (duration - dur)
# Apply light BPM + swing module (deterministic but controllable)
if use_bpm_influence:
start = start * tempo_factor
if swing_amount > 0:
start += (i % 3 - 1) * swing_amount * 0.08 # micro humanizing swing
idx = int(start * sr)
end = min(idx + len(tone), n_total)
pan = (cx / w) * 2 - 1
stereo_note = self._stereo_pan(tone[:end-idx], pan) * cfg["note_vol"]
audio[idx:end] += stereo_note
audio_melody[idx:end] += stereo_note
melody_events.append((freq, dur, start))
if i == 0:
self._log(f" LOG: MELODY_MODULE first_event_start={start:.2f}s (bpm_factor={tempo_factor:.2f}, swing={swing_amount})")
# Layer 4: Glitch / Micro events
for i, kp in enumerate(features["keypoints"][:cfg["max_glitches"]]):
x, y = kp.pt
freq = root * 13.5 + (y % 85) * 1.4
tone = self._generate_tone(freq, 0.05, "sine")
tone = self._apply_envelope(tone, cfg["glitch_attack"], cfg["glitch_decay"])
start = (y / h) * (duration - 0.05)
idx = int(start * sr)
end = min(idx + len(tone), n_total)
pan = (x / w) * 2 - 1
stereo_glitch = self._stereo_pan(tone[:end-idx], pan) * cfg["glitch_vol"]
audio[idx:end] += stereo_glitch
audio_glitch[idx:end] += stereo_glitch
# Final polish
audio = self._soft_limit(audio)
fade = int(cfg["global_fade"] * sr)
if fade > 0 and n_total > fade * 2:
audio[:fade] *= np.linspace(0, 1, fade)[:, np.newaxis]
audio[-fade:] *= np.linspace(1, 0, fade)[:, np.newaxis]
peak = np.max(np.abs(audio))
if peak > 0:
audio = audio / peak * 0.97
sf.write(output_path, audio, sr)
self._log(f"βœ“ Saved: {output_path} | Peak: {peak:.3f}")
self._log(f" LOG_WORKING: STANDARD_FOUNDATION | effective_bpm={effective_bpm} | events={len(melody_events)} | peak={peak:.3f} | (bpm/swing now influence timing β€” test with extreme values)")
# Export Stems
if cfg.get("export_stems"):
base = output_path.replace(".wav", "")
for stem, name in [(audio_noise, "noise"), (audio_drone, "drone"),
(audio_melody, "melody"), (audio_glitch, "glitch")]:
max_val = np.max(np.abs(stem))
if max_val > 0:
stem = stem / max_val * 0.97
sf.write(f"{base}_{name}.wav", stem, sr)
self._log(f"βœ“ Stem saved: {base}_{name}.wav")
# Export MIDI (optional dependency)
if cfg.get("export_midi") and melody_events:
if not HAS_MIDO:
self._log("⚠️ export_midi requested but mido is not installed β€” skip MIDI")
else:
mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)
ticks_per_beat = 480
tempo = cfg.get("tempo_bpm", 120)
tick_offset = 0
for freq, dur, start in melody_events:
midi_note = self._freq_to_midi(freq)
duration_ticks = int(dur * ticks_per_beat * (tempo / 60))
start_ticks = int(start * ticks_per_beat * (tempo / 60))
track.append(Message('note_on', note=midi_note, velocity=64, time=max(0, start_ticks - tick_offset)))
track.append(Message('note_off', note=midi_note, velocity=64, time=max(1, duration_ticks)))
tick_offset = start_ticks + duration_ticks
mid_path = output_path.replace(".wav", ".mid")
mid.save(mid_path)
self._log(f"βœ“ MIDI saved: {mid_path}")
def process(self, image_path: str, output_path: str):
self.image_path = image_path
self._log(f"\n╔════════════════════════════════════════════╗")
self._log(f"β•‘ LYGO Resonance Engine v{__version__} β•‘")
self._log(f"β•‘ Image β†’ Living Stereo Soundscape β•‘")
self._log(f"β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•\n")
self._log(f"Analyzing: {image_path}")
features = self.analyze_image(image_path)
self.synthesize(features, output_path)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("image", help="Input image path")
parser.add_argument("-o", "--output", default=None)
parser.add_argument("--duration", type=float, default=15.0)
parser.add_argument("--style", choices=list(PRESETS.keys()), default="cinematic")
parser.add_argument("--seed", type=int, default=None)
parser.add_argument("--noise-filter", type=float, default=None)
parser.add_argument("--stems", action="store_true")
parser.add_argument("--midi", action="store_true")
parser.add_argument("--batch", action="store_true")
parser.add_argument("--quiet", action="store_true")
parser.add_argument("--ldq", action="store_true")
parser.add_argument("--genre", choices=["None", "Dubstep", "Phonk", "Industrial"], default="None")
parser.add_argument("--percussion", choices=["standard", "ldq"], default="standard")
parser.add_argument("--polish", type=float, default=0.5)
parser.add_argument("--bpm", type=int, default=140)
parser.add_argument("--swing", type=float, default=0.0)
args = parser.parse_args()
config = {
"duration": args.duration,
"random_seed": args.seed,
"verbose": not args.quiet,
"export_stems": args.stems,
"export_midi": args.midi,
"use_ldq": args.ldq,
"genre_manifold": args.genre,
"percussion_mode": args.percussion,
"perceptual_polish": args.polish,
"tempo_bpm": args.bpm,
"swing": args.swing,
}
if args.noise_filter is not None:
config["noise_lowpass_hz"] = args.noise_filter
preset = PRESETS.get(args.style, {})
config.update(preset)
if args.batch:
folder = Path(args.image)
if not folder.is_dir():
print("Error: --batch requires a folder path")
return
images = list(folder.glob("*.jpg")) + list(folder.glob("*.png")) + list(folder.glob("*.jpeg"))
if not images:
print("No images found")
return
for img in images:
print(f"\nProcessing: {img.name}")
out_path = f"resonance_{img.stem}.wav"
engine = ResonanceEngine(config)
engine.process(str(img), out_path)
return
out_path = args.output or f"resonance_{Path(args.image).stem}.wav"
engine = ResonanceEngine(config)
engine.process(args.image, out_path)
if __name__ == "__main__":
main()