#!/usr/bin/env python3 """ LDQ Percussion Engine v2.1 Professional drum synthesis – FIXED: broadcasting, truthiness, and shape mismatches. """ import numpy as np import math import cv2 from typing import Dict, Any def generate_kick(features: Dict[str, Any], sr: int, duration: float = 0.5) -> np.ndarray: """Generate a punchy kick drum. Output shape: (sr*duration,).""" n = int(sr * duration) if n <= 0: return np.zeros(1) contours = features.get("contours") if contours is None or len(contours) == 0: # default kick t = np.linspace(0, duration, n) pitch = np.linspace(60, 30, n) kick = np.sin(2 * np.pi * pitch * t) env = np.exp(-t * 25) return (kick * env).astype(np.float32) largest = max(contours, key=cv2.contourArea) area = cv2.contourArea(largest) perimeter = cv2.arcLength(largest, True) f0 = 80 - (area / 10000) * 30 # 50–80 Hz f1 = 30 + (perimeter / 100) * 5 # 30–35 Hz t = np.linspace(0, duration, n) pitch = np.linspace(f0, f1, n) kick = np.sin(2 * np.pi * pitch * t).astype(np.float32) env = np.exp(-t * 20).astype(np.float32) kick = kick * env # click transient click = np.zeros(n, dtype=np.float32) click_len = min(int(sr * 0.003), n) click[:click_len] = np.random.normal(0, 0.3, click_len) click[:click_len] *= np.linspace(1, 0, click_len) kick += click * 0.2 kick = np.tanh(kick * 1.0) / np.tanh(1.0) return kick def generate_snare(features: Dict[str, Any], sr: int, duration: float = 0.15) -> np.ndarray: """Generate snare. Output shape: (sr*duration,).""" n = int(sr * duration) if n <= 0: return np.zeros(1) t = np.linspace(0, duration, n) # body body_freq = 180.0 body = np.sin(2 * np.pi * body_freq * t).astype(np.float32) body_env = np.exp(-t * 15).astype(np.float32) body = body * body_env # crack noise = np.random.normal(0, 1, n).astype(np.float32) # simple high‑pass via convolution (very short kernel) kernel = np.array([1, -1], dtype=np.float32) noise = np.convolve(noise, kernel, mode='same') crack_env = np.exp(-t * 30).astype(np.float32) crack = noise * crack_env snare = body * 0.5 + crack * 0.5 snare = snare * 0.8 # optional angle modulation lines = features.get("lines") if lines is not None and len(lines) > 0: angles = [] for line in lines[:10]: x1, y1, x2, y2 = line[0] angles.append(math.degrees(math.atan2(y2 - y1, x2 - x1)) % 180) if angles: avg_angle = np.mean(angles) detune = (avg_angle / 180) * 20 body2 = np.sin(2 * np.pi * (body_freq + detune) * t).astype(np.float32) body2 = body2 * body_env snare = body2 * 0.5 + crack * 0.5 return (snare * 0.8).astype(np.float32) def generate_hihats(features: Dict[str, Any], sr: int, bpm: float, duration: float = 2.0) -> np.ndarray: """Generate hi‑hats. Output shape: (sr*duration,).""" n = int(sr * duration) if n <= 0: return np.zeros(1) edge_density = features.get("edge_density", 0.02) brightness = features.get("average_brightness", 0.5) if edge_density < 0.03: subdivision = 8 elif edge_density < 0.06: subdivision = 16 else: subdivision = 32 beats = duration * (bpm / 60) n_ticks = int(beats * subdivision) hihat = np.zeros(n, dtype=np.float32) amp = 0.05 + brightness * 0.1 for i in range(n_ticks): pos = int(i * sr * 60 / (bpm * subdivision)) if pos >= n: break if i % 4 == 0: level = amp elif i % 8 == 1: level = amp * 0.7 else: level = amp * 0.4 pulse_len = int(sr * 0.015) end = min(pos + pulse_len, n) noise = np.random.normal(0, level, pulse_len).astype(np.float32) env = np.linspace(1, 0, pulse_len, dtype=np.float32) hihat[pos:end] += noise * env hihat = np.tanh(hihat * 0.7) / np.tanh(0.7) return hihat