tawasuyu/takiy
> takiy (Quechua: to sing). Music in Rust — synthesis, sequencing, playback — on Llimphi.
⎇ 5802bf9ee1f162194bcbfdc797293f2d20d829ae
02_ruway/takiy/takiy-acordes/src/render.rs
History
Who touched what
raw
chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0)
1//! De la progresión a un [`Score`] — el momento en que la idea suena.
2//!
3//! Determinista: la misma progresión con los mismos patrones y ajustes da
4//! exactamente las mismas notas. Lo que sale es un `Score` normal y
5//! corriente, así que sigue de largo por lo que ya existe: se reproduce
6//! (`takiy-playback`), se edita nota por nota en el piano roll, se exporta
7//! a MIDI (`takiy-midi`) o a WAV.
8
9use serde::{Deserialize, Serialize};
10use takiy_core::{Pitch, Score, ScoreNote, Track};
11use takiy_genesis::rng::Rng;
12use takiy_genesis::{Brief, Style};
13
14use crate::banco::Banco;
15use crate::patron::{Direccion, Golpe, Patron};
16use crate::progresion::{Progresion, Ranura};
17
18/// Cómo se baja la progresión a notas.
19#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
20pub struct Ajustes {
21 pub bpm: f32,
22 /// Tiempos por compás — 4 para 4/4, 3 para un vals.
23 pub beats_por_compas: f32,
24 /// El patrón que usan las ranuras que no eligieron uno propio.
25 pub patron: usize,
26 /// Pista de bajo con la fundamental del acorde.
27 pub bajo: bool,
28 pub octava_bajo: i32,
29 /// Pista de batería. Sólo se agrega en compás de 4 tiempos — el groove
30 /// que reusamos de `takiy-genesis` está escrito en 4/4.
31 pub bateria: bool,
32 /// Estilo del groove de batería.
33 pub estilo: Style,
34 /// Semilla del jitter de la batería (el resto es determinista sin ella).
35 pub semilla: u64,
36 /// Swing `0.0..0.5`: cuánto se atrasa cada paso impar, en fracción de
37 /// paso. `0.0` = recto.
38 pub swing: f32,
39}
40
41impl Default for Ajustes {
42 fn default() -> Self {
43 Self {
44 bpm: 100.0,
45 beats_por_compas: 4.0,
46 patron: 2, // «Rasgueo pop»
47 bajo: true,
48 octava_bajo: 2,
49 bateria: false,
50 estilo: Style::Pop,
51 semilla: 1,
52 swing: 0.0,
53 }
54 }
55}
56
57/// Las alturas concretas de una ranura: el acorde en su octava, con la
58/// inversión aplicada (cada inversión sube la voz más grave una octava).
59pub fn voces_de(ranura: &Ranura) -> Vec<Pitch> {
60 let mut voces = ranura.acorde.voicing(ranura.octava);
61 if voces.is_empty() {
62 return voces;
63 }
64 for _ in 0..ranura.inversion.min(4) {
65 let grave = voces.remove(0);
66 match grave.transpose(12) {
67 Some(p) => voces.push(p),
68 None => {
69 voces.insert(0, grave);
70 break;
71 }
72 }
73 }
74 voces.sort_by_key(|p| p.midi());
75 voces
76}
77
78/// La progresión hecha `Score`.
79pub fn render(prog: &Progresion, banco: &Banco, aj: &Ajustes) -> Score {
80 let mut score = Score::new(aj.bpm);
81 let bpc = aj.beats_por_compas.max(1.0);
82
83 let mut acordes = Track::new("Acordes");
84 let mut bajo = Track::new("Bajo");
85
86 for (ranura, inicio_compas) in prog.ranuras().iter().zip(prog.inicios()) {
87 let inicio = inicio_compas * bpc;
88 let largo = ranura.compases.max(0.05) * bpc;
89 let patron = banco.get_o_primero(ranura.patron.unwrap_or(aj.patron));
90 tocar_ranura(&mut acordes, ranura, patron, inicio, largo, bpc, aj.swing);
91 if aj.bajo {
92 tocar_bajo(&mut bajo, ranura, inicio, largo, bpc);
93 }
94 }
95
96 score.add_track(acordes);
97 if aj.bajo && !bajo.is_empty() {
98 score.add_track(bajo);
99 }
100
101 // Batería: se reusa el groove de `takiy-genesis` (perfil por estilo,
102 // patrones euclidianos). Está escrito en 4/4, así que fuera de 4/4 no
103 // se agrega — antes que sonar corrida, no suena.
104 if aj.bateria && (bpc - 4.0).abs() < f32::EPSILON {
105 let compases = prog.compases().ceil().max(1.0) as usize;
106 let brief = Brief { bars: compases, style: aj.estilo, tempo_bpm: aj.bpm, ..Brief::default() };
107 let mut rng = Rng::new(aj.semilla);
108 let bateria = takiy_genesis::rhythm::drum_section(
109 &brief, compases, 0, 1.0, false, false, false, &mut rng,
110 );
111 if !bateria.is_empty() {
112 score.add_track(bateria);
113 }
114 }
115
116 score
117}
118
119/// Un acorde tocado con su patrón, desde `inicio` y por `largo` beats.
120fn tocar_ranura(
121 track: &mut Track,
122 ranura: &Ranura,
123 patron: &Patron,
124 inicio: f32,
125 largo: f32,
126 bpc: f32,
127 swing: f32,
128) {
129 let voces = voces_de(ranura);
130 if voces.is_empty() {
131 return;
132 }
133 let n = patron.pasos_por_compas();
134 let paso_beats = bpc / n as f32;
135 // Cuántos pasos entran en la ranura (una ranura de medio compás toca
136 // la primera mitad del patrón, no el patrón entero apretado).
137 let total = (largo / paso_beats).round().max(1.0) as usize;
138 let fin = inicio + largo;
139
140 for j in 0..total {
141 let Some(paso) = patron.paso(j % n) else { continue };
142 if !paso.golpe.suena() {
143 continue;
144 }
145 let atraso = if j % 2 == 1 { swing.clamp(0.0, 0.49) * paso_beats } else { 0.0 };
146 let t = inicio + j as f32 * paso_beats + atraso;
147 if t >= fin {
148 break;
149 }
150 // Nada suena después del cambio de acorde: la ligadura se corta.
151 let dur = (paso.largo * paso_beats).min(fin - t).max(0.05);
152
153 match paso.golpe {
154 Golpe::Silencio => {}
155 Golpe::Fundamental => {
156 track.add(ScoreNote::new(voces[0], t, dur, paso.velocidad));
157 }
158 Golpe::Voz(i) => {
159 let v = voces[(i as usize) % voces.len()];
160 track.add(ScoreNote::new(v, t, dur, paso.velocidad));
161 }
162 Golpe::Rasgueo(dir) => {
163 let sep = patron.ancho_rasgueo.max(0.0) * paso_beats;
164 for (k, &v) in voces.iter().enumerate() {
165 // Hacia abajo la púa entra por la voz grave; hacia
166 // arriba, por la aguda.
167 let orden = match dir {
168 Direccion::Abajo => k,
169 Direccion::Arriba => voces.len() - 1 - k,
170 };
171 let t_v = t + orden as f32 * sep;
172 if t_v >= fin {
173 continue;
174 }
175 // Las voces que entran después pierden un poco de
176 // fuerza: es lo que hace que un rasgueo no suene a
177 // acorde de máquina.
178 let vel = (paso.velocidad as i32 - 3 * orden as i32).clamp(1, 127) as u8;
179 let dur_v = (dur - (t_v - t)).max(0.05);
180 track.add(ScoreNote::new(v, t_v, dur_v, vel));
181 }
182 }
183 }
184 }
185}
186
187/// Bajo: la fundamental en el 1 y la quinta a mitad de compás cuando la
188/// ranura dura un compás entero o más.
189fn tocar_bajo(track: &mut Track, ranura: &Ranura, inicio: f32, largo: f32, bpc: f32) {
190 let voces = voces_de(ranura);
191 let Some(root) = Pitch::from_class_octave(ranura.acorde.root, ranura.octava - 2)
192 .or_else(|| voces.first().copied())
193 else {
194 return;
195 };
196 if largo >= bpc {
197 let mitad = largo / 2.0;
198 track.add(ScoreNote::new(root, inicio, mitad.min(bpc / 2.0), 96));
199 let quinta = root.transpose(7).unwrap_or(root);
200 track.add(ScoreNote::new(quinta, inicio + mitad, largo - mitad, 84));
201 } else {
202 track.add(ScoreNote::new(root, inicio, largo, 96));
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use crate::paleta::{Paleta, TipoTonalidad};
210 use crate::patron::{Direccion, Golpe, Paso};
211 use takiy_core::{Chord, ChordQuality, PitchClass};
212
213 fn do_mayor() -> Paleta {
214 Paleta::nueva(PitchClass::C, TipoTonalidad::Mayor)
215 }
216
217 fn ajustes() -> Ajustes {
218 Ajustes { bajo: false, bateria: false, ..Ajustes::default() }
219 }
220
221 #[test]
222 fn el_render_es_determinista() {
223 let p = Progresion::cadencia(&do_mayor(), 0);
224 let b = Banco::nuevo();
225 let a = Ajustes { bajo: true, bateria: true, ..Ajustes::default() };
226 let uno = render(&p, &b, &a);
227 let dos = render(&p, &b, &a);
228 assert_eq!(uno.tracks().len(), dos.tracks().len());
229 for (t1, t2) in uno.tracks().iter().zip(dos.tracks()) {
230 assert_eq!(t1.notes(), t2.notes(), "pista «{}»", t1.name);
231 }
232 }
233
234 #[test]
235 fn cada_acorde_cae_en_su_compas() {
236 let p = Progresion::cadencia(&do_mayor(), 0); // C G Am F
237 let mut b = Banco::nuevo();
238 // Patrón de un solo golpe al 1 para que la cuenta sea limpia.
239 let solo = b.agregar(Patron::desde(
240 "uno",
241 0.0,
242 vec![Paso::nuevo(Golpe::Fundamental, 100, 4.0)],
243 ));
244 let score = render(&p, &b, &Ajustes { patron: solo, ..ajustes() });
245 let notas = score.track(0).unwrap().notes();
246 assert_eq!(notas.len(), 4);
247 let inicios: Vec<f32> = notas.iter().map(|n| n.start).collect();
248 assert_eq!(inicios, vec![0.0, 4.0, 8.0, 12.0]);
249 let clases: Vec<_> = notas.iter().map(|n| n.pitch.class()).collect();
250 assert_eq!(
251 clases,
252 vec![PitchClass::C, PitchClass::G, PitchClass::A, PitchClass::F]
253 );
254 }
255
256 #[test]
257 fn el_rasgueo_abre_las_voces_en_el_tiempo() {
258 let mut prog = Progresion::nueva();
259 prog.agregar(Chord::new(PitchClass::C, ChordQuality::Major));
260 let mut b = Banco::nuevo();
261 let i = b.agregar(Patron::desde(
262 "rasgueo",
263 0.25,
264 vec![Paso::nuevo(Golpe::Rasgueo(Direccion::Abajo), 100, 4.0)],
265 ));
266 let score = render(&prog, &b, &Ajustes { patron: i, ..ajustes() });
267 let notas = score.track(0).unwrap().notes();
268 assert_eq!(notas.len(), 3, "las tres voces de la tríada");
269 // Grave primero, y separadas: es un rasgueo, no un bloque.
270 assert!(notas[0].start < notas[1].start && notas[1].start < notas[2].start);
271 assert!(notas[0].pitch.midi() < notas[1].pitch.midi());
272 // Y hacia arriba entra primero la aguda.
273 let j = b.agregar(Patron::desde(
274 "arriba",
275 0.25,
276 vec![Paso::nuevo(Golpe::Rasgueo(Direccion::Arriba), 100, 4.0)],
277 ));
278 let score = render(&prog, &b, &Ajustes { patron: j, ..ajustes() });
279 let notas = score.track(0).unwrap().notes();
280 let primera = notas.iter().min_by(|a, b| a.start.total_cmp(&b.start)).unwrap();
281 let aguda = notas.iter().max_by_key(|n| n.pitch.midi()).unwrap();
282 assert_eq!(primera.pitch, aguda.pitch);
283 }
284
285 #[test]
286 fn ninguna_nota_pisa_el_acorde_siguiente() {
287 let p = Progresion::cadencia(&do_mayor(), 0);
288 let b = Banco::nuevo();
289 // «Redonda» deja sonar el compás entero: el corte tiene que ser exacto.
290 let score = render(&p, &b, &Ajustes { patron: 0, ..ajustes() });
291 for n in score.track(0).unwrap().notes() {
292 let compas = (n.start / 4.0).floor();
293 assert!(
294 n.end() <= (compas + 1.0) * 4.0 + 1e-3,
295 "una nota de {} se estiró hasta {}",
296 n.start,
297 n.end()
298 );
299 }
300 }
301
302 #[test]
303 fn una_ranura_de_medio_compas_toca_media_grilla() {
304 let mut prog = Progresion::nueva();
305 prog.agregar(Chord::new(PitchClass::C, ChordQuality::Major));
306 prog.ranura_mut(0).unwrap().compases = 0.5;
307 let mut b = Banco::nuevo();
308 // Ocho pasos de una nota cada uno: en medio compás tienen que
309 // sonar los primeros cuatro, no los ocho apretados.
310 let i = b.agregar(Patron::desde(
311 "ocho",
312 0.0,
313 (0..8).map(|_| Paso::nuevo(Golpe::Fundamental, 90, 1.0)).collect(),
314 ));
315 let score = render(&prog, &b, &Ajustes { patron: i, ..ajustes() });
316 let inicios: Vec<f32> = score
317 .track(0)
318 .unwrap()
319 .notes()
320 .iter()
321 .map(|n| n.start)
322 .collect();
323 assert_eq!(inicios, vec![0.0, 0.5, 1.0, 1.5]);
324 }
325
326 #[test]
327 fn la_inversion_sube_la_voz_grave_una_octava() {
328 let mut r = Ranura::nueva(Chord::new(PitchClass::C, ChordQuality::Major));
329 let fundamental = voces_de(&r);
330 r.inversion = 1;
331 let primera = voces_de(&r);
332 assert_eq!(fundamental.len(), primera.len());
333 assert_eq!(primera[0].class(), PitchClass::E, "ahora abajo está la tercera");
334 assert_eq!(
335 primera.last().unwrap().midi(),
336 fundamental[0].midi() + 12,
337 "la fundamental subió una octava"
338 );
339 }
340
341 #[test]
342 fn el_swing_atrasa_solo_los_pasos_impares() {
343 let mut prog = Progresion::nueva();
344 prog.agregar(Chord::new(PitchClass::C, ChordQuality::Major));
345 let mut b = Banco::nuevo();
346 let i = b.agregar(Patron::desde(
347 "ocho",
348 0.0,
349 (0..8).map(|_| Paso::nuevo(Golpe::Fundamental, 90, 1.0)).collect(),
350 ));
351 let recto = render(&prog, &b, &Ajustes { patron: i, ..ajustes() });
352 let swing = render(&prog, &b, &Ajustes { patron: i, swing: 0.3, ..ajustes() });
353 let a: Vec<f32> = recto.track(0).unwrap().notes().iter().map(|n| n.start).collect();
354 let s: Vec<f32> = swing.track(0).unwrap().notes().iter().map(|n| n.start).collect();
355 assert_eq!(a.len(), s.len());
356 for (k, (x, y)) in a.iter().zip(&s).enumerate() {
357 if k % 2 == 0 {
358 assert_eq!(x, y, "el paso {k} es a tiempo");
359 } else {
360 assert!(y > x, "el paso {k} tiene que atrasarse");
361 }
362 }
363 }
364
365 #[test]
366 fn el_bajo_y_la_bateria_entran_como_pistas_aparte() {
367 let p = Progresion::cadencia(&do_mayor(), 0);
368 let b = Banco::nuevo();
369 let score = render(&p, &b, &Ajustes { bajo: true, bateria: true, ..Ajustes::default() });
370 let nombres: Vec<&str> = score.tracks().iter().map(|t| t.name.as_str()).collect();
371 assert_eq!(nombres, vec!["Acordes", "Bajo", "Batería"]);
372 assert!(!score.track(1).unwrap().is_empty());
373 assert!(!score.track(2).unwrap().is_empty());
374 }
375
376 #[test]
377 fn en_vals_no_hay_bateria_de_cuatro_cuartos() {
378 let p = Progresion::cadencia(&do_mayor(), 0);
379 let b = Banco::nuevo();
380 let score = render(
381 &p,
382 &b,
383 &Ajustes { beats_por_compas: 3.0, bateria: true, bajo: false, ..Ajustes::default() },
384 );
385 let nombres: Vec<&str> = score.tracks().iter().map(|t| t.name.as_str()).collect();
386 assert_eq!(nombres, vec!["Acordes"]);
387 }
388
389 #[test]
390 fn una_progresion_vacia_da_un_score_sin_notas() {
391 let score = render(&Progresion::nueva(), &Banco::nuevo(), &Ajustes::default());
392 assert!(score.tracks().iter().all(|t| t.is_empty()));
393 }
394}