| |
| """ |
| 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: |
| |
| 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 |
| f1 = 30 + (perimeter / 100) * 5 |
| 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 = 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_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 |
|
|
| |
| noise = np.random.normal(0, 1, n).astype(np.float32) |
| |
| 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 |
|
|
| |
| 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 |