SPB Git forge

spb/admin-ka

Public
41commits 1branches 0releases
172.9 MBsize
maindefault branch
19 days agolast push
JavaScript 65.5% Python 17.8% CSS 13% HTML 3.7%
13.2 KB · 319 lines python
Raw Blame History
1#!/usr/bin/env python32"""Moteur musical procédural KA — compose un lit musical UNIQUE par reel.346 styles (house, lofi, synthwave, epic, funk, trap) × plusieurs progressions5d'accords × tonalité, ligne de basse, batterie et MÉLODIE générées à partir6d'une graine : deux reels n'ont jamais la même musique.78API : compose(dur, seed, out_wav, style=None) -> style utilisé9CLI : music_engine.py <dur> <graine> <out.wav> [style]10Sortie : WAV stéréo 44,1 kHz 16 bits (fondu d'entrée/sortie inclus).11"""12import sys, math, random, wave13import numpy as np1415SR = 441001617# ---------- filtres (magnitude ordre 1, via FFT — rapide et sans scipy) ----------18def _fftfilt(x, H_of_f):19    n = len(x)20    X = np.fft.rfft(x)21    f = np.fft.rfftfreq(n, 1.0/SR)22    return np.fft.irfft(X * H_of_f(f), n)2324def lowpass(x, fc):25    fc = max(30.0, fc)26    return _fftfilt(x, lambda f: 1.0/np.sqrt(1.0+(f/fc)**2))2728def highpass(x, fc):29    fc = max(30.0, fc)30    return _fftfilt(x, lambda f: (f/fc)/np.sqrt(1.0+(f/fc)**2))3132# ---------- oscillateurs / percussions ----------33def _t(n): return np.arange(n)/SR34def sine(f, n, ph=0.0): return np.sin(2*np.pi*f*_t(n)+ph)35def saw(f, n): return 2.0*((f*_t(n)) % 1.0)-1.036def tri(f, n): return 2.0*np.abs(2.0*((f*_t(n)) % 1.0)-1.0)-1.03738def expdec(n, tau): return np.exp(-_t(n)/max(1e-4, tau))3940def kick(dur=0.5, f0=150.0, f1=45.0, punch=1.0, rng=None):41    n = int(dur*SR); t = _t(n)42    f = f1+(f0-f1)*np.exp(-t*28.0)43    x = np.sin(2*np.pi*np.cumsum(f)/SR)*np.exp(-t*7.5)*punch44    nz = (rng or np.random).standard_normal(n)*np.exp(-t*400.0)45    return x + highpass(nz, 3500)*0.354647def sub808(f, dur, rng=None):48    n = int(dur*SR); t = _t(n)49    fr = f*(1.0+0.35*np.exp(-t*22.0))          # petit plongeon de hauteur50    x = np.sin(2*np.pi*np.cumsum(fr)/SR)51    x = np.tanh(x*2.2)*np.exp(-t*1.6)52    x[:int(0.004*SR)] *= np.linspace(0, 1, int(0.004*SR))53    return x5455def snare(dur=0.22, rng=None):56    n = int(dur*SR)57    nz = (rng or np.random).standard_normal(n)58    body = sine(190, n)*expdec(n, 0.035)*0.559    return highpass(nz, 1600)*expdec(n, 0.055) + body6061def clap(rng=None):62    n = int(0.30*SR)63    out = np.zeros(n)64    r = rng or np.random65    for i, off in enumerate((0.0, 0.012, 0.026)):66        o = int(off*SR); m = n-o67        out[o:] += highpass(r.standard_normal(m), 1200)*expdec(m, 0.05)*(0.8-0.15*i)68    return out6970def hat(dur=0.05, opened=False, rng=None):71    d = 0.30 if opened else dur72    n = int(max(d, 0.03)*SR)73    nz = (rng or np.random).standard_normal(n)74    return highpass(nz, 8200)*expdec(n, 0.11 if opened else 0.018)7576def bass_note(f, dur, kind="saw"):77    n = int(dur*SR)78    if kind == "sub":79        x = sine(f, n)+0.35*sine(2*f, n)80    else:81        x = lowpass(saw(f, n), f*4.0)*0.9 + sine(f, n)*0.582    e = np.minimum(1.0, _t(n)/0.006)*np.exp(-_t(n)/max(0.05, dur*0.7))83    return x*e8485def pluck(f, dur, bright=5.0):86    n = int(dur*SR)87    x = 0.6*saw(f, n)+0.4*tri(f, n)88    x = lowpass(x, f*bright)89    return x*expdec(n, max(0.04, dur*0.35))*np.minimum(1.0, _t(n)/0.003)9091def pad_chord(freqs, dur, detune=0.004, cutoff=1900.0):92    n = int(dur*SR)93    L = np.zeros(n); R = np.zeros(n)94    for f in freqs:95        L += saw(f*(1+detune), n)+0.5*sine(f, n)96        R += saw(f*(1-detune), n)+0.5*sine(f, n, ph=0.7)97    a = np.minimum(1.0, _t(n)/0.35)98    rel = np.minimum(1.0, (dur-_t(n))/0.25)99    e = a*np.clip(rel, 0, 1)100    return lowpass(L, cutoff)*e, lowpass(R, cutoff)*e101102# ---------- théorie : gammes, accords ----------103NOTE_HZ = {"F2": 87.31, "G2": 98.00, "A2": 110.00, "B2": 123.47, "C3": 130.81, "D3": 146.83, "E3": 164.81}104SCALES = {"minor": [0, 2, 3, 5, 7, 8, 10], "major": [0, 2, 4, 5, 7, 9, 11], "dorian": [0, 2, 3, 5, 7, 9, 10]}105106def deg_semi(scale, d):107    return scale[d % 7] + 12*(d//7)108109def chord_freqs(root_hz, scale, degree, spread=(0, 2, 4)):110    return [root_hz*2**(deg_semi(scale, degree+s)/12.0) for s in spread]111112# ---------- définition des styles ----------113# patterns = 16 pas (doubles-croches) par mesure 4/4 ; valeurs = vélocité114S = {115  "house": dict(116    bpm=(120, 126), scale="minor", roots=["A2", "G2", "F2", "C3"],117    progs=[[0, 5, 3, 4], [0, 5, 2, 6], [5, 3, 0, 4], [0, 2, 5, 6]],118    kick=[1, 0, 0, 0]*4, snare=None,119    clap=[0]*4+[1]+[0]*7+[1]+[0]*3,120    hats=[0, 0, .8, 0]*4, openhat=[0, 0, 1, 0]*4,121    bass="offbeat8", pad="sustain", mel_density=0.55, mel_oct=2,122    g=dict(kick=.95, clap=.5, hat=.35, bass=.62, pad=.30, mel=.42), swing=0.0),123  "lofi": dict(124    bpm=(78, 88), scale="dorian", roots=["F2", "G2", "A2"],125    progs=[[0, 3, 5, 4], [0, 2, 3, 4], [5, 4, 0, 3]],126    kick=[1]+[0]*6+[.7]+[0]*2+[.9]+[0]*5, snare=[0]*4+[.8]+[0]*7+[.8]+[0]*3, clap=None,127    hats=[.5, 0, .35, 0]*4, openhat=None,128    bass="half", pad="sustain", mel_density=0.35, mel_oct=1,129    g=dict(kick=.8, snare=.4, hat=.3, bass=.6, pad=.36, mel=.5), swing=0.12, vinyl=True),130  "synthwave": dict(131    bpm=(104, 112), scale="minor", roots=["A2", "C3", "G2"],132    progs=[[0, 5, 3, 6], [0, 6, 5, 4], [0, 3, 6, 4]],133    kick=[1, 0, 0, 0]*4, snare=[0]*4+[1]+[0]*7+[1]+[0]*3, clap=None,134    hats=[.35]*16, openhat=None,135    bass="arp16", pad="big", mel_density=0.4, mel_oct=2,136    g=dict(kick=.9, snare=.5, hat=.22, bass=.5, pad=.4, mel=.4), swing=0.0),137  "epic": dict(138    bpm=(96, 104), scale="minor", roots=["C3", "A2", "G2"],139    progs=[[0, 6, 3, 5], [0, 3, 6, 5], [0, 5, 6, 3]],140    kick=[1]+[0]*7+[0, 0, .9, 0]+[0]*4, snare=None, clap=[0]*12+[.6]+[0]*3,141    hats=None, openhat=None,142    bass="pulse8", pad="big", mel_density=0.45, mel_oct=2,143    g=dict(kick=.95, clap=.35, bass=.55, pad=.5, mel=.45), swing=0.0, swell=True),144  "funk": dict(145    bpm=(106, 114), scale="dorian", roots=["G2", "A2", "F2"],146    progs=[[0, 0, 3, 4], [0, 3, 0, 4], [0, 4, 3, 4]],147    kick=[1, 0, 0, 0, 0, 0, .8, 0, 0, .7, 0, 0, 1, 0, 0, 0],148    snare=[0]*4+[1]+[0]*7+[1]+[0, 0, .5], clap=None,149    hats=[.5, .2, .4, .2]*4, openhat=None,150    bass="synco", pad="stab", mel_density=0.5, mel_oct=1,151    g=dict(kick=.9, snare=.5, hat=.3, bass=.7, pad=.25, mel=.45), swing=0.08),152  "trap": dict(153    bpm=(136, 146), scale="minor", roots=["F2", "G2", "A2"],154    progs=[[0, 0, 5, 5], [0, 0, 6, 6], [0, 0, 3, 3]],155    kick=[1]+[0]*6+[.8]+[0]*8, snare=[0]*8+[1]+[0]*7, clap=None,156    hats=[.5, 0, .4, .4]*2+[.5, .3, .3, .3, .5, 0, .8, .4], openhat=None,157    bass="e808", pad="sustain", mel_density=0.3, mel_oct=2,158    g=dict(kick=.85, snare=.55, hat=.3, bass=.75, pad=.22, mel=.4), swing=0.0),159}160161# ---------- séquenceur ----------162def _place(dst, sig, i0, gain):163    if gain <= 0: return164    i1 = min(len(dst), i0+len(sig))165    if i1 <= i0: return166    dst[i0:i1] += sig[:i1-i0]*gain167168def _bass_events(kind, bar, rng):169    """-> liste (pas, longueur_en_pas, offset_degré, octave+)"""170    if kind == "offbeat8": return [(s, 2, 0, 0) for s in (2, 6, 10, 14)]171    if kind == "half":     return [(0, 8, 0, 0), (8, 8, 0, 0)]172    if kind == "arp16":    return [(s, 1, 0, (s % 4 == 2)) for s in range(16)]173    if kind == "pulse8":   return [(s, 2, 0, 0) for s in range(0, 16, 2)]174    if kind == "e808":     return [(0, 10, 0, 0), (10, 6, 0, 0)] if bar % 2 == 0 else [(0, 12, 0, 0), (13, 3, -2, 0)]175    if kind == "synco":176        pat = [(0, 2, 0, 0), (3, 1, 0, 1), (6, 2, 0, 0), (10, 1, 4, 0), (11, 1, 0, 1), (14, 2, 0, 0)]177        return pat if bar % 2 == 0 else pat[:-1]+[(14, 1, 0, 1), (15, 1, 0, 0)]178    return [(0, 4, 0, 0)]179180def _melody_bar(rng, density, prev_deg, chord_deg):181    """Rythme + hauteurs (degrés de gamme) pour une mesure. -> [(pas,long,deg)]"""182    events = []183    step = 0184    cur = prev_deg185    while step < 16:186        ln = rng.choice((2, 2, 2, 4, 4, 1, 3))187        if rng.random() < density:188            if rng.random() < 0.5:                       # note d'accord189                cur = chord_deg + rng.choice((0, 2, 4, 7))190            else:                                        # marche par degrés191                cur = max(chord_deg-1, min(chord_deg+9, cur+rng.choice((-2, -1, -1, 1, 1, 2))))192            events.append((step, ln, cur))193        step += ln194    return events, cur195196def compose(dur, seed, out_wav, style=None):197    rng = random.Random(str(seed))198    nrng = np.random.RandomState(abs(hash(str(seed))) % (2**31))199    name = style if style in S else rng.choice(sorted(S.keys()))200    st = S[name]201    bpm = rng.uniform(*st["bpm"])202    spb = 60.0/bpm203    stepd = spb/4.0204    scale = SCALES[st["scale"]]205    root = NOTE_HZ[rng.choice(st["roots"])]206    prog = rng.choice(st["progs"])207    swing = st.get("swing", 0.0)208209    total = int((dur+0.2)*SR)210    L = np.zeros(total); R = np.zeros(total)211    g = st["g"]212213    # percussions pré-rendues214    kick_s = kick(rng=nrng)215    snare_s = snare(rng=nrng) if st.get("snare") else None216    clap_s = clap(rng=nrng) if st.get("clap") else None217    hat_c = hat(rng=nrng); hat_o = hat(opened=True, rng=nrng)218219    nbars = int(math.ceil(dur/(4*spb)))+1220    prev_deg = 7221    for bar in range(nbars):222        bar_t = bar*4*spb223        cdeg = prog[bar % len(prog)]224        cfreqs = chord_freqs(root*2, scale, cdeg)225226        # pad / nappes227        if g.get("pad", 0) > 0:228            pl, pr = pad_chord(cfreqs, 4*spb*1.05,229                               detune=0.006 if st["pad"] == "big" else 0.003,230                               cutoff=2400 if st["pad"] == "big" else 1700)231            if st["pad"] == "stab":   # stabs funk : accord plaqué sur 2 offbeats232                for s in (2, 10):233                    n = int(spb*0.9*SR)234                    stab = sum(pluck(f, spb*0.9, bright=4) for f in cfreqs)235                    _place(L, stab, int((bar_t+s*stepd)*SR), g["pad"]*0.8)236                    _place(R, stab, int((bar_t+s*stepd)*SR), g["pad"]*0.8)237            else:238                _place(L, pl, int(bar_t*SR), g["pad"])239                _place(R, pr, int(bar_t*SR), g["pad"])240241        # batterie242        for s in range(16):243            tt = bar_t+s*stepd+(stepd*swing if s % 2 == 1 else 0.0)244            i0 = int(tt*SR)245            v = st["kick"][s] if st.get("kick") else 0246            if v: _place(L, kick_s, i0, g["kick"]*v); _place(R, kick_s, i0, g["kick"]*v)247            if snare_s is not None and st["snare"][s]:248                _place(L, snare_s, i0, g["snare"]*st["snare"][s]); _place(R, snare_s, i0, g["snare"]*st["snare"][s])249            if clap_s is not None and st.get("clap") and st["clap"][s]:250                _place(L, clap_s, i0, g["clap"]*st["clap"][s]); _place(R, clap_s, i0, g["clap"]*st["clap"][s])251            if st.get("hats") and st["hats"][s]:252                hs = hat_o if (st.get("openhat") and st["openhat"][s]) else hat_c253                vv = g["hat"]*st["hats"][s]*(0.8+0.4*rng.random())254                _place(L, hs, i0, vv*0.9); _place(R, hs, i0, vv*1.1)255256        # basse257        bkind = st["bass"]258        for (s, ln, doff, octu) in _bass_events(bkind, bar, rng):259            f = root*2**(deg_semi(scale, cdeg+doff)/12.0)*(2 if octu else 1)260            d = ln*stepd261            sig = sub808(f/2 if bkind == "e808" else f, d*1.1) if bkind == "e808" else \262                  bass_note(f, d*0.95, kind="sub" if bkind in ("half", "pulse8") else "saw")263            i0 = int((bar_t+s*stepd)*SR)264            _place(L, sig, i0, g["bass"]); _place(R, sig, i0, g["bass"])265266        # mélodie (graine → jamais deux fois pareille)267        if g.get("mel", 0) > 0:268            events, prev_deg = _melody_bar(rng, st["mel_density"], prev_deg, cdeg)269            for (s, ln, deg) in events:270                f = root*2**(deg_semi(scale, deg)/12.0)*(2**st["mel_oct"])271                d = ln*stepd272                sig = pluck(f, min(d*1.4, 1.2), bright=6.0)273                tt = bar_t+s*stepd+(stepd*swing if s % 2 == 1 else 0.0)274                i0 = int(tt*SR)275                pan = 0.15*math.sin(bar+s)          # léger mouvement stéréo276                _place(L, sig, i0, g["mel"]*(1-pan)); _place(R, sig, i0, g["mel"]*(1+pan))277278        # montée bruitée vers chaque 4e mesure (styles amples)279        if st.get("swell") and bar % 4 == 3:280            n = int(4*spb*SR)281            sw = highpass(nrng.standard_normal(n), 1200)*np.linspace(0, 1, n)**2.5282            _place(L, sw, int(bar_t*SR), 0.10); _place(R, sw, int(bar_t*SR), 0.10)283284    # texture vinyle (lofi)285    if st.get("vinyl"):286        hiss = lowpass(nrng.standard_normal(total), 4000)*0.006287        crackle = np.zeros(total)288        for _ in range(int(dur*7)):289            i = rng.randrange(total-200)290            crackle[i:i+120] += nrng.standard_normal(120)*expdec(120, 0.001)*0.05291        L += hiss+crackle; R += hiss+crackle292293    # écho discret sur le mix (profondeur)294    dly = int(0.375*spb/0.5*SR*0.5) or 1295    eL = np.zeros(total); eR = np.zeros(total)296    eL[dly:] = R[:-dly]*0.18; eR[dly:] = L[:-dly]*0.18   # ping-pong léger297    L += eL; R += eR298299    # master : drive doux, normalisation, fondus300    mix = np.stack([L, R])301    mix = np.tanh(mix*1.25)302    peak = np.max(np.abs(mix)) or 1.0303    mix = mix/peak*0.88304    nfi = int(0.5*SR); nfo = int(1.4*SR)305    mix[:, :nfi] *= np.linspace(0, 1, nfi)306    if total > nfo: mix[:, -nfo:] *= np.linspace(1, 0, nfo)307    mix = mix[:, :int(dur*SR)]308309    data = (np.clip(mix.T, -1, 1)*32767).astype(np.int16)310    with wave.open(out_wav, "wb") as w:311        w.setnchannels(2); w.setsampwidth(2); w.setframerate(SR)312        w.writeframes(data.tobytes())313    return name314315if __name__ == "__main__":316    dur = float(sys.argv[1]); seed = sys.argv[2]; out = sys.argv[3]317    style = sys.argv[4] if len(sys.argv) > 4 else None318    print(compose(dur, seed, out, style))319