| |
| """ |
| 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 |
|
|
| |
| try: |
| import mido |
| from mido import MidiFile, MidiTrack, Message |
| HAS_MIDO = True |
| except ImportError: |
| mido = None |
| MidiFile = MidiTrack = Message = None |
| HAS_MIDO = False |
|
|
| |
| |
| |
|
|
| __version__ = "0.5.3" |
|
|
| |
| 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": { |
| "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) |
| |
| 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) |
| |
| 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, |
| } |
|
|
| 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 |
| |
| 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) |
| |
| 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) |
| 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"] |
|
|
| |
| |
| 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"] |
| ) |
|
|
| |
| 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) |
| |
| 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 |
|
|
| |
| 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") |
| |
| audio = np.zeros((n_total, 2), dtype=np.float32) |
|
|
| |
| |
| |
| |
| self._log("π΅ Standard Synthesis active (FOUNDATION)") |
|
|
| |
| use_bpm_influence = True |
| 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}") |
|
|
| |
| 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 = [] |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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]) |
|
|
| |
| |
| tempo_factor = 120.0 / max(1, effective_bpm) |
| 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) |
|
|
| |
| if use_bpm_influence: |
| start = start * tempo_factor |
| if swing_amount > 0: |
| start += (i % 3 - 1) * swing_amount * 0.08 |
|
|
| 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})") |
|
|
| |
| 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 |
|
|
| |
| 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)") |
|
|
| |
| 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") |
|
|
| |
| 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() |