From 59355909f2de9236af8168a26c70bcf6caa3b285 Mon Sep 17 00:00:00 2001 From: Elvis Claros Castro Date: Sat, 26 Sep 2026 20:21:47 -0300 Subject: Import video pipeline as it was --- scripts/music.py | 166 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 scripts/music.py (limited to 'scripts/music.py') diff --git a/scripts/music.py b/scripts/music.py new file mode 100644 index 0000000..a3aee16 --- /dev/null +++ b/scripts/music.py @@ -0,0 +1,166 @@ +# -*- 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] -- cgit v1.2.3