# -*- coding: utf-8 -*- """Cama musical original hecha con las muestras de piano de keysound. Muestra NN => nota MIDI NN+20 (01-A-大字2组 = A0 = MIDI 21) Las progresiones son propias, para que el audio sea libre de derechos. """ import glob, os, wave import numpy as np PIANO_DIR = "/mnt/Data/GitHub/keysound/audio/piano" SR = 44100 _paths, _cache = {}, {} for _p in glob.glob(os.path.join(PIANO_DIR, "*.wav")): _paths[int(os.path.basename(_p).split("-")[0]) + 20] = _p NOTE = {"C": 0, "C#": 1, "D": 2, "D#": 3, "E": 4, "F": 5, "F#": 6, "G": 7, "G#": 8, "A": 9, "A#": 10, "B": 11} TRIAD = {"maj": [0, 4, 7], "min": [0, 3, 7]} def midi(name, octave): return 12 * (octave + 1) + NOTE[name] def load(m): m = max(21, min(108, m)) if m not in _cache: with wave.open(_paths[m], "rb") as w: d = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16) d = d.reshape(-1, w.getnchannels()).astype(np.float32) / 32768.0 if d.shape[1] == 1: d = np.repeat(d, 2, axis=1) _cache[m] = d return _cache[m] def chord(sym): """'Am' -> (bajo, triada). Soporta sostenidos y sufijo m.""" q = "min" if sym.endswith("m") else "maj" root = sym[:-1] if sym.endswith("m") else sym r = NOTE[root] bass = 12 * (2 + 1) + r # octava 2 triad = [12 * (4 + 1) + r + i for i in TRIAD[q]] return bass, triad # progresion + tempo por video BEDS = { "montyhall": (["Am", "F", "C", "G"], 92), "nueves": (["Em", "C", "G", "D"], 88), "cumple": (["C", "G", "Am", "F"], 96), "luna": (["Dm", "A#", "F", "C"], 82), "japones": (["G", "D", "Em", "C"], 100), "benford": (["Am", "Em", "F", "G"], 94), "hilbert": (["C", "Em", "Am", "F"], 80), "reuleaux": (["F", "C", "Dm", "A#"], 98), "regla72": (["G", "Em", "C", "D"], 104), "buffon": (["Am", "G", "F", "E"], 86), "collatz": (["Am", "Em", "Dm", "E"], 90), "simpson": (["Cm", "G#", "A#", "Gm"], 96), "cuerda": (["F", "Am", "Dm", "C"], 84), "gabriel": (["Em", "Bm", "C", "G"], 76), "mobius": (["D", "A", "Bm", "G"], 102), "bayes": (["Dm", "Gm", "A#", "A"], 86), "cicloide": (["A", "E", "F#m", "D"], 106), "dados": (["Em", "Am", "D", "G"], 94), "bolapeluda": (["C", "Am", "Dm", "G"], 100), "caos": (["Am", "F", "Dm", "E"], 88), "diferencial": (["Bm", "G", "D", "A"], 112, "motor"), "galton": (["D", "Bm", "G", "A"], 96), "bloques": (["E", "C#m", "A", "B"], 100), "domino": (["G", "D", "Em", "C"], 104, "motor"), } # melodias propias: por compas, (tiempo en negras, nota MIDI, duracion en negras) MELODIAS = { "motor": [ [(0, 78, 1.5), (1.5, 74, 0.5), (2, 76, 1), (3, 78, 1)], # Bm [(0, 79, 1.5), (1.5, 78, 0.5), (2, 76, 1), (3, 74, 1)], # G [(0, 78, 1), (1, 81, 1), (2, 78, 0.5), (2.5, 76, 0.5), (3, 74, 1)], # D [(0, 73, 2), (2, 76, 1), (3, 69, 1)], # A ], } def bed(name, total_s, gain=0.30, seed=0): prog, bpm = BEDS[name][:2] estilo = BEDS[name][2] if len(BEDS[name]) > 2 else None beat = 60.0 / bpm measure = 4 * beat eighth = beat / 2 n = int((total_s + 3.0) * SR) buf = np.zeros((n, 2), dtype=np.float32) rng = np.random.default_rng(seed) def hit(m, t, g, max_len=None): s = load(m) * g if max_len is not None: k = min(len(s), int(max_len * SR)) s = s[:k].copy() f = min(int(0.06 * SR), k) s[-f:] *= np.linspace(1, 0, f)[:, None] i = int(t * SR) if i >= n: return buf[i:i + len(s)] += s[:n - i] k = 0 t = 0.0 while t < total_s + 0.5: sym = prog[k % len(prog)] bass, triad = chord(sym) if estilo == "motor": # bajo en corcheas, como un motor; acorde en 1 y 3; melodia arriba for j in range(8): hit(bass + (12 if j % 2 else 0), t + j * eighth, 0.34 if j % 2 == 0 else 0.20, max_len=eighth * 0.9) for b_ in (0, 2): for nota in triad: hit(nota - 12, t + b_ * beat, 0.13, max_len=beat * 1.8) vuelta = (k // 4) % 2 # la segunda vuelta, una octava arriba for (b_, nota, d) in MELODIAS[estilo][k % 4]: hit(nota + 12 * vuelta, t + b_ * beat, 0.30, max_len=d * beat * 1.05) t += measure k += 1 continue hit(bass, t, 0.50, max_len=measure * 0.95) hit(bass + 12, t + 2 * beat, 0.26, max_len=beat * 1.6) pattern = [triad[0], triad[1], triad[2], triad[1], triad[2], triad[1], triad[2], triad[0]] for j, note in enumerate(pattern): g = 0.30 if j % 2 == 0 else 0.20 hit(note, t + j * eighth, g, max_len=beat * 1.15) if k % 4 == 3: # brillito al cerrar la vuelta hit(triad[2] + 12, t + 3.5 * beat, 0.16, max_len=beat) t += measure k += 1 buf = buf[:int(total_s * SR)] peak = np.abs(buf).max() + 1e-9 buf *= gain / peak # entrada y salida suaves fi, fo = int(1.2 * SR), int(2.0 * SR) buf[:fi] *= np.linspace(0, 1, fi)[:, None] buf[-fo:] *= np.linspace(1, 0, fo)[:, None] return buf def duck(music, speech, sr=SR, floor_db=-11.0, block=512, release=0.45): """Baja la musica cuando hay voz (sidechain simple).""" mono = np.abs(speech).mean(axis=1) if speech.ndim > 1 else np.abs(speech) nb = len(mono) // block env = mono[:nb * block].reshape(nb, block).max(axis=1) rel = np.exp(-block / (release * sr)) out = np.zeros(nb, dtype=np.float32) acc = 0.0 for i, v in enumerate(env): acc = v if v > acc else acc * rel + v * (1 - rel) out[i] = acc g_floor = 10 ** (floor_db / 20) active = np.clip(out / 0.03, 0, 1) gain = 1.0 - (1.0 - g_floor) * active idx = np.linspace(0, nb - 1, len(music)) g = np.interp(idx, np.arange(nb), gain).astype(np.float32) return music * g[:, None]