| |
| """ |
| FACTORY DEFAULT RESONANCE ENGINE (Isolated Pure Gradio Factory) |
| This is the ORIGINAL working 4-layer sonification engine, completely separate from LYGO/LDQ. |
| |
| - NO LDQ imports, NO use_ldq, NO sidechain, NO genre manifold, NO perceptual, NO fingerprint. |
| - Pure image-driven stereo soundscapes using only the classic layers: |
| 1. Texture Floor (filtered noise from edge density) |
| 2. Drones (saw from structural lines) |
| 3. Melody (sine from contours) |
| 4. Glitch (short tones from FAST keypoints) |
| |
| Goal: Reliable "industry quality beats / musical" output as the default factory experience. |
| This file is the drop-in clean factory. The LYGO tab continues to use the other engine. |
| |
| Based on the clean original implementations that produced good musical results (historical working versions). |
| """ |
|
|
| import cv2 |
| import numpy as np |
| import soundfile as sf |
| import math |
| from pathlib import Path |
| from typing import Optional, Dict, Any |
|
|
| __version__ = "factory-1.0.0-isolated" |
|
|
| |
| |
| PRESETS = { |
| "raw": { |
| "noise_vol": 0.06, |
| "drone_vol": 0.07, |
| "note_vol": 0.12, |
| "glitch_vol": 0.02, |
| "noise_lowpass_hz": 800, |
| }, |
| "ambient": { |
| "noise_vol": 0.04, |
| "drone_vol": 0.09, |
| "note_vol": 0.10, |
| "glitch_vol": 0.008, |
| "drone_attack": 5.0, |
| "drone_decay": 6.0, |
| "note_decay": 0.55, |
| "noise_lowpass_hz": 450, |
| }, |
| "glitch": { |
| "noise_vol": 0.07, |
| "drone_vol": 0.05, |
| "note_vol": 0.09, |
| "glitch_vol": 0.06, |
| "max_glitches": 25, |
| "noise_lowpass_hz": 2200, |
| }, |
| "ethereal": { |
| "noise_vol": 0.03, |
| "drone_vol": 0.08, |
| "note_vol": 0.11, |
| "glitch_vol": 0.01, |
| "note_decay": 0.6, |
| "noise_lowpass_hz": 380, |
| }, |
| "cinematic": { |
| "noise_vol": 0.05, |
| "drone_vol": 0.08, |
| "note_vol": 0.13, |
| "glitch_vol": 0.025, |
| "drone_attack": 4.0, |
| "drone_decay": 5.5, |
| "note_attack": 0.05, |
| "note_decay": 0.38, |
| "max_glitches": 18, |
| "noise_lowpass_hz": 720, |
| "root_freq_range": (28, 72), |
| }, |
| |
| "musical": { |
| "noise_vol": 0.045, |
| "drone_vol": 0.085, |
| "note_vol": 0.135, |
| "glitch_vol": 0.022, |
| "noise_lowpass_hz": 680, |
| "drone_attack": 3.8, |
| "drone_decay": 5.2, |
| "note_decay": 0.36, |
| "max_notes": 9, |
| "max_glitches": 16, |
| }, |
| } |
|
|
|
|
| class FactoryResonanceEngine: |
| """Completely isolated factory default engine. No LYGO/LDQ dependencies.""" |
|
|
| def __init__(self, config: Optional[Dict[str, Any]] = None): |
| self.config = { |
| "sr": 44100, |
| "duration": 15.0, |
| "global_fade": 0.65, |
| "soft_clip": True, |
| "soft_clip_amount": 1.35, |
| "max_drones": 5, |
| "max_notes": 8, |
| "max_glitches": 14, |
| "noise_vol": 0.05, |
| "drone_vol": 0.07, |
| "note_vol": 0.12, |
| "glitch_vol": 0.018, |
| "root_freq_range": (32, 68), |
| "theta_lock_range": (4.8, 10.5), |
| "drone_attack": 3.8, |
| "drone_decay": 5.0, |
| "note_attack": 0.045, |
| "note_decay": 0.34, |
| "glitch_attack": 0.004, |
| "glitch_decay": 0.014, |
| "noise_lowpass_hz": 650, |
| "random_seed": None, |
| "verbose": True, |
| "export_stems": False, |
| "export_midi": False, |
| } |
| if config: |
| self.config.update(config) |
|
|
| def _log(self, msg: str): |
| if self.config.get("verbose", True): |
| print(msg) |
|
|
| def analyze_image(self, image_path: str) -> Dict[str, Any]: |
| """Standard computer vision analysis β unchanged from original working versions.""" |
| 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) |
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) |
| h, w = gray.shape |
| avg_blue, avg_green, avg_red, _ = cv2.mean(img) |
| edges = cv2.Canny(gray, 50, 150) |
| edge_density = np.sum(edges > 0) / (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) |
| return { |
| "width": w, "height": h, |
| "avg_red": avg_red, "avg_green": avg_green, "avg_blue": avg_blue, |
| "edge_density": edge_density, |
| "contours": contours, |
| "lines": lines if lines is not None else [], |
| "keypoints": keypoints, |
| } |
|
|
| 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.58).astype(np.float32) |
| elif wave_type == "noise": |
| return np.random.uniform(-0.32, 0.32, 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 synthesize(self, features: Dict[str, Any], output_path: str): |
| cfg = self.config |
| if cfg["random_seed"] is not None: |
| np.random.seed(cfg["random_seed"]) |
|
|
| 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) |
| root = np.interp(features["avg_red"], [0, 255], cfg["root_freq_range"]) |
| theta = np.interp(features["avg_green"], [0, 255], cfg["theta_lock_range"]) |
| w, h = features["width"], features["height"] |
|
|
| self._log("π΅ FACTORY ENGINE β Pure 4-layer (no LYGO/LDQ)") |
|
|
| |
| 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.006: |
| noise = self._generate_tone(0, duration, "noise") |
| if cfg.get("noise_lowpass_hz", 650) > 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.35, cfg["noise_vol"]) |
| stereo_noise = self._stereo_pan(noise, 0.0) * vol |
| audio += stereo_noise |
| audio_noise += stereo_noise |
| self._log(f" Layer1 noise vol={vol:.3f} lowpass={cfg.get('noise_lowpass_hz')}") |
|
|
| |
| for i, line in enumerate(features["lines"][:cfg["max_drones"]]): |
| x1, _, x2, _ = line[0] |
| length = math.hypot(x2 - x1, 0) |
| detune = (i * 0.65) if cfg["random_seed"] is not None else 0 |
| freq = root + (max(1, int(length / 46)) * theta * 0.58) + detune |
| tone = self._generate_tone(freq, duration, "sawtooth") |
| tone = self._apply_envelope(tone, cfg["drone_attack"], cfg["drone_decay"]) |
| pan = (x1 / 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 85 < cv2.contourArea(c) < (w * h * 0.62)] |
| valid.sort(key=lambda c: cv2.boundingRect(c)[0]) |
|
|
| 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.65) + (verts * theta * 1.55) |
| dur = min(2.8, 0.18 + (area / 12800)) |
| 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) |
| 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)) |
|
|
| |
| for i, kp in enumerate(features["keypoints"][:cfg["max_glitches"]]): |
| x, y = kp.pt |
| freq = root * 13.2 + (y % 82) * 1.35 |
| tone = self._generate_tone(freq, 0.048, "sine") |
| tone = self._apply_envelope(tone, cfg["glitch_attack"], cfg["glitch_decay"]) |
| start = (y / h) * (duration - 0.048) |
| 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.965 |
|
|
| sf.write(output_path, audio, sr) |
| self._log(f"β FACTORY Saved: {output_path} | Peak: {peak:.3f}") |
|
|
| |
| 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")]: |
| mv = np.max(np.abs(stem)) |
| if mv > 0: |
| stem = stem / mv * 0.965 |
| sf.write(f"{base}_{name}.wav", stem, sr) |
| self._log(f"β Stem: {base}_{name}.wav") |
|
|
| |
| if cfg.get("export_midi") and melody_events: |
| try: |
| from mido import MidiFile, MidiTrack, Message |
| mid = MidiFile() |
| track = MidiTrack() |
| mid.tracks.append(track) |
| ticks_per_beat = 480 |
| tempo = 128 |
| tick_offset = 0 |
| for freq, dur, start in melody_events: |
| midi_note = max(0, min(127, int(12 * math.log2(max(20, freq) / 440) + 69))) |
| 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=62, time=start_ticks - tick_offset)) |
| track.append(Message('note_off', note=midi_note, velocity=62, time=duration_ticks)) |
| tick_offset = start_ticks + duration_ticks |
| mid_path = output_path.replace(".wav", ".mid") |
| mid.save(mid_path) |
| self._log(f"β MIDI: {mid_path}") |
| except Exception as e: |
| self._log(f"MIDI export skipped: {e}") |
|
|
| def process(self, image_path: str, output_path: str): |
| self._log(f"\nββββββββββββββββββββββββββββββββββββββββββββββ") |
| self._log(f"β FACTORY DEFAULT RESONANCE ENGINE v{__version__} β") |
| self._log(f"β Pure image β musical 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(): |
| import argparse |
| parser = argparse.ArgumentParser() |
| parser.add_argument("image") |
| 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("--quiet", action="store_true") |
| args = parser.parse_args() |
|
|
| config = { |
| "duration": args.duration, |
| "random_seed": args.seed, |
| "verbose": not args.quiet, |
| "export_stems": args.stems, |
| "export_midi": args.midi, |
| } |
| if args.noise_filter is not None: |
| config["noise_lowpass_hz"] = args.noise_filter |
|
|
| preset = PRESETS.get(args.style, {}) |
| config.update(preset) |
|
|
| out_path = args.output or f"factory_{Path(args.image).stem}.wav" |
| engine = FactoryResonanceEngine(config) |
| engine.process(args.image, out_path) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|