tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
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}