tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Sutichaykuna

⎇ main llimphi-voxel-app/src/soundtrack.rs Rust · 107 siq'ikuna · 4.4 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

1//! Banda sonora de la película: compone un `Score` con `takiy-core` y lo
2//! sintetiza a un WAV con `takiy-synth`, para muxearlo al video (`--film`).
3//! Cierra el último ingrediente del "director": la peli **con sonido**.
4//!
5//! Es contenido (la "partitura"), igual que `screenplay()` es la dirección: una
6//! progresión cálida I–V–vi–IV (do–sol–lam–fa) con pads, bajo y una melodía que
7//! sube hacia el momento del gesto. El sintetizador es el de osciladores (feo a
8//! propósito), suavizado con un poco de reverb.
9
10use takiy_core::{Chord, ChordQuality, Pitch, PitchClass, ReverbParams, Score, ScoreNote, Track};
11use takiy_synth::{write_wav, Adsr, OscRenderer, Renderer, Waveform};
12
13/// Tempo de la banda (BPM). 86 → ~0.70 s/beat → 8 negras ≈ 5.6 s.
14const BPM: f32 = 86.0;
15
16/// Compone y sintetiza la banda sonora a `path` (WAV PCM 16-bit). Devuelve la
17/// duración en segundos del audio generado (para informar).
18///
19/// `accents` son los **beats del guion** (segundos): cortes de cámara y gestos de los
20/// actores (ver `Sequence::beat_times`). En cada uno se coloca un **acento** — una
21/// campana brillante (seno agudo, ataque corto) — para que la música caiga *sobre la
22/// acción* en vez de sólo compartir duración. Vacío = sin acentos (comportamiento viejo).
23pub fn render_to(path: &str, accents: &[f32]) -> f32 {
24    let mut score = Score::new(BPM); // ~0.70 s/beat → 8 beats ≈ 5.6 s
25    // Reverb suave: da aire sin tapar la mezcla.
26    score.master_reverb = Some(ReverbParams { room_size: 0.6, damping: 0.5, mix: 0.22 });
27
28    // Progresión I–V–vi–IV, dos beats por acorde.
29    let prog = [
30        (PitchClass::C, ChordQuality::Major),
31        (PitchClass::G, ChordQuality::Major),
32        (PitchClass::A, ChordQuality::Minor),
33        (PitchClass::F, ChordQuality::Major),
34    ];
35
36    // Pads: el acorde completo en octava 3, sostenido 2 beats.
37    let mut pads = Track::new("pads");
38    pads.volume = 0.5;
39    // Bajo: la fundamental en octava 2.
40    let mut bass = Track::new("bass");
41    bass.volume = 0.6;
42    for (i, &(root, quality)) in prog.iter().enumerate() {
43        let start = i as f32 * 2.0;
44        for p in Chord::new(root, quality).voicing(3) {
45            pads.add(ScoreNote::new(p, start, 2.0, 52));
46        }
47        if let Some(b) = Pitch::from_class_octave(root, 2) {
48            bass.add(ScoreNote::new(b, start, 2.0, 64));
49        }
50    }
51
52    // Melodía: una línea que se eleva hacia el beat 5 (≈ el gesto a cámara) y
53    // resuelve. Notas `(start_beat, midi, duración_beats)`.
54    let mut lead = Track::new("lead");
55    lead.volume = 0.85;
56    let melody = [
57        (0.0, 72, 1.0), // C5
58        (1.0, 76, 1.0), // E5
59        (2.0, 74, 1.0), // D5
60        (3.0, 79, 1.0), // G5
61        (4.0, 76, 1.0), // E5
62        (5.0, 81, 1.5), // A5 — el clímax
63        (6.5, 79, 0.5), // G5
64        (7.0, 77, 1.2), // F5 — resuelve
65    ];
66    for (start, midi, dur) in melody {
67        if let Some(p) = Pitch::from_midi(midi) {
68            lead.add(ScoreNote::new(p, start, dur, 78));
69        }
70    }
71
72    // Acentos: una campana aguda en cada beat del guion (corte de cámara o gesto). El
73    // tiempo en segundos se pasa a beats (la unidad del Score) dividiendo por s/beat.
74    // Una nota corta + una octava arriba refuerza el "ding". Volumen alto para que
75    // pinche sobre la mezcla cálida de abajo.
76    let sec_per_beat = 60.0 / BPM;
77    let mut hits = Track::new("acentos");
78    hits.volume = 0.9;
79    for &t in accents {
80        let beat = t / sec_per_beat;
81        // Campana = fundamental aguda (C6=84) + su octava (C7=96), bien corta.
82        for midi in [84, 96] {
83            if let Some(p) = Pitch::from_midi(midi) {
84                hits.add(ScoreNote::new(p, beat, 0.5, 90));
85            }
86        }
87    }
88
89    score.add_track(pads);
90    score.add_track(bass);
91    score.add_track(lead);
92    if !accents.is_empty() {
93        score.add_track(hits);
94    }
95
96    // Síntesis: triángulo (más cuerpo que el seno, menos áspero que la sierra)
97    // con una envolvente algo más dulce que la default.
98    let renderer = OscRenderer {
99        sample_rate: 44_100,
100        waveform: Waveform::Triangle,
101        envelope: Adsr { attack: 0.01, decay: 0.12, sustain: 0.7, release: 0.35 },
102    };
103    let buf = renderer.render(&score);
104    let secs = buf.duration_seconds();
105    write_wav(&buf, path).expect("escribir WAV de la banda sonora");
106    secs
107}