Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

La biblioteca: estilos y ritmos como datos

Archivos: crates/lineup-core/src/library/, crates/lineup-core/src/sources.rs, crates/lineup-app/src/library_browser.rs y crates/lineup-app/src/app/library.rs

Un músico que se sienta a tocar una bossa nova sabe cuatro cosas: los acordes, cómo los toca la guitarra, qué hace el bajo y el patrón de la batería. Nada de eso es código. Es conocimiento, y la mejor manera de meter conocimiento en un programa es escribirlo como datos, en una forma que una persona pueda leer y comprobar, y que el código que lo usa sea pequeño.

La biblioteca hace eso: 215 estilos armónicos, 139 ritmos y 6 rellenos, cada uno unas pocas líneas en una tabla, más los estilos dodecafónicos y de micromodos de los capítulos de teoría y las canciones de Cómo se escribe una demostración. Este capítulo recorre las notaciones y después sigue un estilo desde la tabla hasta una canción: qué canal toca qué, cómo cambia de estilo una canción entera, cómo cada parte de la canción recuerda de dónde vino, y la pestaña Biblioteca que lo lista todo. Cómo se usa la Biblioteca está en el capítulo del manual Los estilos y la Biblioteca.

Cifrados: acordes compás por compás

Una progresión escrita con nombres de acordes, «C G Am F», solo sirve en Do. Escrita con números romanos, «I V vi IV», sirve en cualquier tonalidad (ver Leer un número romano). Un cifrado escribe los números compás por compás, como una hoja guía, con | entre compases:

I | IV V | vi | ii7 V7

Los acordes de un compás se lo reparten por igual, así que «IV V» es medio compás cada uno. Un acorde que repite el anterior se sostiene, no se vuelve a tocar: «I | I» son dos compases de un mismo acorde. chart::parse_chart(text, bar) convierte un cifrado en ChordSpans, en pasos de un compás de bar pasos:

#![allow(unused)]
fn main() {
pub struct ChordSpan {
    pub chord: RomanChord,
    pub start: usize,
    pub length: usize,
    /// Whether the band plays it early (see [`Push`]).
    pub push: Option<Push>,
    /// Whether the band stops playing its style here (see [`Stop`]).
    pub stop: Option<Stop>,
}
}

Devuelve None cuando no se puede leer un acorde, un compás está vacío o un compás no se puede repartir por igual entre sus acordes: una entrada de la tabla que está mal hace fallar una prueba en lugar de tocar algo sin sentido. push y stop son las marcas de Golpes, sostenidos, silencios y anticipaciones.

El comping: cómo se tocan los acordes

Los mismos cuatro acordes pueden ser una balada, un reggae o un tema de house. Lo que cambia es el ritmo con que se golpean los acordes, lo que los músicos de jazz llaman comping (de accompanying, acompañar), y lo que hace el bajo. Cada manera de hacer comping es una tabla corta de golpes: cuándo empieza una nota, cuánto dura, qué notas del acorde toca y con qué fuerza.

#![allow(unused)]
fn main() {
struct Hit<T> {
    /// When it starts, in steps from the start of the pattern's period.
    at: usize,
    /// How long it lasts, in steps.
    len: usize,
    what: T,
    velocity: u8,
}
}

Para los acordes, what es un Voices: All, todas las notas; Upper, todas menos la más grave, el golpe ascendente más suave de un rasgueo; o Nth(n), una nota contada desde abajo, para los arpegios. Para el bajo es un Tone: la fundamental, la quinta, la octava, la tercera, la séptima, la fundamental del acorde siguiente tocada antes (una anticipación) o la nota a un semitono de ella, que conduce a la siguiente. Un único struct genérico sirve para los dos, con T puesto dos veces.

#![allow(unused)]
fn main() {
Comping::Pulse => (4, &[hit!(0, 2, All, 96), hit!(2, 2, All, 78)]),
Comping::Quarters => (4, &[hit!(0, 2, All, 100)]),
Comping::Offbeat => (4, &[hit!(2, 1, All, 92)]),
}

El primer número es el período: los golpes se repiten cada 4 pasos, un pulso. Pulse son corcheas parejas, la segunda más suave; Offbeat es el skank del reggae, un acorde corto en la segunda corchea de cada pulso. El largo, corto, largo, largo de la habanera ocupa 8 pasos:

#![allow(unused)]
fn main() {
Comping::Habanera => (
    8,
    &[
        hit!(0, 3, All, 96),
        hit!(3, 1, Upper, 74),
        hit!(4, 2, All, 86),
        hit!(6, 2, All, 84),
    ],
),
}

macro_rules!, y por qué no una función

hit! es una pequeña macro declarativa:

#![allow(unused)]
fn main() {
macro_rules! hit {
    ($at:expr, $len:expr, $what:expr, $velocity:expr) => {
        Hit {
            at: $at,
            len: $len,
            what: $what,
            velocity: $velocity,
        }
    };
}
}

Una macro es código que escribe código. macro_rules! compara lo que recibe con un patrón, aquí cuatro expresiones ($at:expr y las demás), y las pega en la plantilla de la derecha. hit!(0, 2, All, 96) se convierte en Hit { at: 0, len: 2, what: All, velocity: 96 } antes de que el compilador compruebe nada.

¿Por qué no una const fn hit(...) -> Hit? Hay que mirar lo que devuelve el match: &'static [Hit<Voices>], un slice prestado que vive todo el programa. Escribir &[ ... ] con valores simples da exactamente eso: el compilador ve una expresión hecha solo de constantes, la pone en los datos de solo lectura del programa y deja que el préstamo viva para siempre. Eso se llama promoción de constantes. Solo ocurre con valores simples: una llamada a función dentro de los corchetes, aunque sea una const fn, no se promueve, y el compilador dice que el temporal prestado se descarta al final de la sentencia. La macro hace que la tabla sea corta de escribir y siga hecha de valores simples. El comentario sobre la macro lo dice, para que nadie la «simplifique» convirtiéndola en una función.

Estilos: una tabla en un static

Un estilo lo junta todo: un cifrado en un modo, una manera de hacer comping, una línea de bajo, un tempo habitual y el ritmo que lo acompaña.

#![allow(unused)]
fn main() {
pub struct Style {
    pub name: &'static str,
    pub genre: Genre,
    /// The mode the chords are written in: the project's scale becomes
    /// this mode when the style is used.
    pub mode: ScaleKind,
    pub time: TimeSignature,
    /// The progression, as a chord chart (see [`chart::parse_chart`]):
    /// numerals counted from the tonic, bars separated by `|`.
    pub chart: &'static str,
    pub comping: Comping,
    pub bass: Bass,
    /// The tempo the style is usually played at, in BPM.
    pub tempo: f32,
    /// The name of the rhythm in [`super::RHYTHMS`] that goes with it, or
    /// "" for none.
    pub rhythm: &'static str,
    /// One or two sentences on what to listen for.
    pub about: &'static str,
}
}

Cada campo es un &'static str, algo Copy o un enum pequeño, así que un Style es Copy y la tabla no necesita reservar memoria. Una entrada se lee como una frase:

#![allow(unused)]
fn main() {
style(
    "Four chords",
    Pop,
    Major,
    "I | V | vi | IV",
    Comping::Pulse,
    Bass::Pulse,
    110.0,
    "Pop beat",
    "The four chords behind hundreds of hits: home, the dominant, the sad relative minor, \
     and the subdominant.",
),
}

style es una const fn que pone 4/4, con #[allow(clippy::too_many_arguments)] porque nueve argumentos en una fila de tabla son más claros que nueve nombres de campo. En la aplicación, este estilo se llama «Cuatro acordes» y su ritmo «Ritmo pop»: las tablas guardan los nombres en inglés y la interfaz los traduce al mostrarlos. Los estilos en otro compás añaden .in_time(THREE_FOUR), otra const fn, que usa la sintaxis de actualización de structs:

#![allow(unused)]
fn main() {
const fn in_time(self, time: TimeSignature) -> Style {
    Style { time, ..self }
}
}

..self quiere decir «todos los demás campos como en self». La tabla misma es un static:

#![allow(unused)]
fn main() {
pub static STYLES: &[Style] = &[
}

Un const se pega en cada lugar que lo usa, como una macro; un static es un único valor en una única dirección durante todo el programa. Para una tabla grande que se usa en muchos lugares, static guarda una sola copia, y un &'static Style que apunta a ella es tan barato de pasar como un número y nunca puede quedar colgando.

Style::render(tonic) toca un estilo en cualquier tonalidad: el cifrado se convierte en tramos, cada número romano en un acorde de la tonalidad, los acordes se disponen uno tras otro moviéndose lo menos posible, y el comping y el bajo los tocan. Ese es el trabajo propio de la banda, en Cómo toca la banda una hoja de acordes.

Ritmos, una línea por tambor

Los bateristas y las cajas de ritmos piensan un ritmo como una cuadrícula: una fila por tambor, una columna por paso. La biblioteca escribe exactamente eso, como texto:

#![allow(unused)]
fn main() {
rhythm(
    "Pop beat",
    Pop,
    110.0,
    &[
        (Kick, "X... .... X.x. ...."),
        (Snare, ".... X... .... X..."),
        (HiHat, "x.x. x.x. x.x. x.x."),
    ],
    "Snare roll",
    "The backbeat: kick on 1 and 3, snare on 2 and 4, hi-hat on every eighth note.",
),
}

Cada carácter es un paso. X es un golpe fuerte, x uno normal, o una nota fantasma (ghost note) suave y . silencio. h y l tocan el tambor cinco semitonos más agudo o más grave, para los tonos abierto y apagado de una conga o para dos toms. Los espacios solo sirven para ver bien los pulsos, y | separa compases, en los ritmos de más de uno. «Snare roll» (el «Redoble de caja» de la aplicación) nombra el relleno, un corte de un compás que cierra una sección.

Un carácter se convierte en un golpe:

#![allow(unused)]
fn main() {
fn hit(character: char) -> Option<Option<Hit>> {
    match character {
        '.' => Some(None),
        'X' => Some(Some((120, 0))),
        'x' => Some(Some((96, 0))),
        'o' => Some(Some((56, 0))),
        'h' => Some(Some((96, 5))),
        'l' => Some(Some((96, -5))),
        _ => None,
    }
}
}

Un Option dentro de un Option dice dos cosas distintas: el None de fuera es «este no es un carácter que un ritmo pueda tener», así que una línea con una errata se rechaza; el None de dentro es «un paso de silencio», una respuesta perfectamente válida. Aquí Hit es un alias de type, (u8, i32): una intensidad y un desplazamiento en semitonos.

Las mismas dos letras escriben los usul turcos de los ritmos balcánicos. En la darbuka, l es el golpe grave, düm, y h el agudo, tek, y los comentarios citan de dónde viene cada patrón:

#![allow(unused)]
fn main() {
rhythm(
    "Aksak",
    Balkan,
    90.0,
    &[
        (Kick, "X... .... X... ......"),
        (Conga, "l... h.h. l... h...h."),
        (Shaker, "x.o. x.o. x.o. x.o.o."),
    ],
    "",
    "The Turkish aksak, 9/8 counted 2 + 2 + 2 + 3, on the darbuka: düm, te-ke, \
     düm, tek-tek, the low düm on the first and third beats, the long last beat \
     split two and one.",
)
.in_time(TimeSignature::of_groups(&[2, 2, 2, 3], 8)),
}

Cuando un ritmo entra en un patrón, se reparte compás por compás en el compás del patrón:

#![allow(unused)]
fn main() {
for start in (0..length).step_by(bar) {
    let steps = &bars[(start / bar) % bars.len()];
    for (step, hit) in steps.iter().enumerate().take(bar) {
}

step_by(bar) da el comienzo de cada compás, y take(bar) corta un compás largo en la barra de compás, así que un ritmo de 4/4 en un patrón de 3/4 pierde su último pulso en cada compás en lugar de desplazarse por encima de las barras. Unos pocos ritmos, los marcados follows_the_meter, se escriben de nuevo para el compás propio de cada compás, para la música cuyo compás cambia todo el tiempo (ver Una orquesta para música que cambia de compás).

Un ritmo para cualquier compás

Cuando cambias a mano el compás de una canción, hace falta un ritmo en el nuevo. Rhythm::for_time pregunta, por orden, hasta que algo responde:

#![allow(unused)]
fn main() {
RHYTHMS
    .iter()
    .filter(in_time)
    .find(same_kind)
    .or_else(|| {
        USUAL
            .iter()
            .filter_map(|name| Rhythm::named(name))
            .find(in_time)
    })
    .or_else(|| RHYTHMS.iter().find(in_time))
    .or_else(|| Rhythm::grouped_for(time, same_kind))
    .unwrap_or_else(|| Rhythm::basic(time))
}

Un ritmo del mismo tipo en el compás nuevo (un tango en 3/4 es un vals de tango); si no, el habitual (un vals para 3/4); si no, cualquiera en ese compás; si no, uno con el mismo número de las mismas notas por compás, agrupadas a su manera (7/8 se toca como 2+2+3/8, una rachenitsa); y si no, un ritmo sencillo hecho para ese compás. Cada or_else solo se ejecuta si todo lo anterior no encontró nada. Cómo pasan las notas de la canción al nuevo compás está en Pasar una canción a otro compás.

Ese último ritmo sencillo tiene un problema: un Rhythm guarda líneas &'static str, y 5/4 no está en ninguna tabla. Rhythm::basic hace el texto la primera vez que se lo piden, y lo guarda mientras dure el programa:

#![allow(unused)]
fn main() {
pub fn basic(time: TimeSignature) -> &'static Rhythm {
    static MADE: Mutex<Vec<&'static Rhythm>> = Mutex::new(Vec::new());
    let mut made = MADE.lock().unwrap_or_else(PoisonError::into_inner);
    if let Some(beat) = made.iter().find(|beat| beat.time == time) {
        return beat;
    }
    let lines: Vec<(Drum, &'static str)> = basic_lines(time)
        .into_iter()
        .map(|(drum, line)| (drum, &*String::leak(line)))
        .collect();
}

String::leak, Vec::leak y Box::leak renuncian a propósito a su memoria: nunca se libera, y a cambio se obtiene una referencia &'static a ella. Filtrar memoria cada vez crecería sin fin, así que los ritmos hechos se guardan en un static Mutex<Vec<...>> y cada compás se hace una sola vez: un puñado de cadenas pequeñas durante la vida del programa. Mutex::new es una const fn, y eso es lo que deja que un static guarde uno. Un candado envenenado (un hilo entró en pánico mientras lo tenía) sigue guardando datos válidos aquí, así que unwrap_or_else(PoisonError::into_inner) los toma igualmente.

Adónde van las notas

Las tablas no saben nada de canales. library/apply.rs decide adónde van sus notas: los acordes a un sintetizador llamado «Keys» (o al canal que tengas seleccionado), la línea de bajo a un sintetizador llamado «Bass», la melodía de una canción a «Lead» y cada tambor al canal que lo toca. Los canales que faltan se añaden, con un piano para Keys y un bajo eléctrico tocado con los dedos para Bass como sonidos General MIDI.

#![allow(unused)]
fn main() {
pub fn chords_channel(project: &mut Project, selected: Option<usize>) -> Option<usize> {
    let free = |(index, channel): (usize, &Channel)| {
        plays_notes(channel)
            && channel.name != BASS
            && channel.name != LEAD
            && !project.is_locked(index)
    };
    if let Some(index) = selected
        && project
            .channels()
            .get(index)
            .is_some_and(|channel| free((index, channel)))
    {
        return Some(index);
    }
    let other = project
        .channels()
        .iter()
        .enumerate()
        .position(|(index, channel)| channel.kind == ChannelKind::Synth && free((index, channel)));
    find_synth(project, KEYS)
        .or(other)
        .or_else(|| synth_named(project, KEYS))
}
}

free es un closure guardado en una variable, para usarlo dos veces. Un canal bloqueado nunca se elige para los acordes (ver Forma, secciones, variaciones y bloqueos). La cadena del final muestra la diferencia entre or y or_else: other ya está calculado, así que or lo toma como valor; synth_named añade un canal, así que solo debe ejecutarse si no se encontró nada más, y va en un closure con or_else.

Los tambores se encuentran por lo que son, no por su nombre: el canal cuyo tipo es Sampler(SampleSource::Builtin(drum)). Un bombo renombrado «Bombo» sigue siendo el bombo. Un sintetizador llamado «Bass» que se envía por MIDI a un sintetizador externo sigue siendo el bajo, porque plays_notes acepta también los canales de salida MIDI (ver MIDI).

Tocar una canción en otro estilo

Probar un estilo en un patrón propio es fácil. Cambiar el estilo de una canción entera es más difícil: «hagamos esta como un reggae» quiere decir los mismos acordes, tocados de otra manera por todos. Pero un patrón no guarda sus acordes; guarda notas. Así que la biblioteca primero los oye, con el segundo oído de Oír la armonía, y después los vuelve a tocar.

Un ritmo dice cómo le gusta que se toquen los acordes a través del estilo del que es la batería:

#![allow(unused)]
fn main() {
pub fn feel(rhythm: &Rhythm) -> (Comping, Bass) {
    let same_time = |style: &&Style| style.time == rhythm.time;
    STYLES
        .iter()
        .find(|style| style.rhythm == rhythm.name)
        .or_else(|| {
            STYLES
                .iter()
                .filter(same_time)
                .find(|style| style.genre == rhythm.genre)
        })
        .or_else(|| STYLES.iter().find(same_time))
        .map_or((Comping::Pulse, Bass::Pulse), |style| {
            (style.comping, style.bass)
        })
}
}

El ritmo «Salsa» es la batería del estilo «Salsa montuno» (el «Montuno de salsa» de la aplicación), así que toca sus acordes como un montuno sobre un bajo de tumbao. Un ritmo que ningún estilo usa toma prestado un estilo de su género en su compás, así que un ritmo en 3/4 nunca recibe una manera de tocar pensada para 4/4. ¿Por qué &&Style? STYLES.iter() da &Style, y filter le pasa a su closure una referencia a cada elemento: una referencia a una referencia.

recomp vuelve a tocar un patrón: oye los acordes de su canal de acordes sobre su bajo, los dispone y deja que el nuevo comping y el nuevo bajo toquen cada tramo. Los tramos donde no se oyó ningún acorde conservan sus notas, así que una introducción sin acordes queda como estaba. play_song lo hace con cada patrón, y deja tres cosas en paz a propósito: la melodía, los patrones de la banda (que siguen la hoja de acordes, ver Cómo toca la banda una hoja de acordes) y los finales, un patrón cuyas notas empiezan todas en su primer paso, como un acorde final tenido con un platillo. Donde la batería anterior tenía un relleno en su último compás, va el relleno del ritmo nuevo. Un relleno es un último compás que no suena como ningún compás anterior:

#![allow(unused)]
fn main() {
// A fill is a last bar that plays like no bar before it.
let fill = length >= 2 * bar && {
    let last = drums_in(project, pattern, length - bar, bar);
    (0..length - bar)
        .step_by(bar)
        .all(|start| drums_in(project, pattern, start, bar) != last)
};
}

En Rust un bloque { ... } es una expresión, cuyo valor es su última línea, así que puede ir después de &&; y como && se detiene en cuanto su lado izquierdo es false, el bloque solo se ejecuta para patrones de dos compases o más. Un ritmo en otro compás primero cambia el compás de toda la canción, compás por compás, así que una canción en 4/4 tocada como vals pasa a ser una canción en 3/4.

Qué partes tomar

La ventana que abre un estilo en la aplicación pregunta qué partes tomar, en tres grupos: bajo Acompañamiento, Batería, Partes de acordes y bajo y Voces / instrumentos; bajo Armonía, Progresión de acordes; bajo Tiempo, Compás, Tempo y Swing. Son un struct pequeño:

#![allow(unused)]
fn main() {
pub struct StyleParts {
    pub rhythm: bool,
    pub instruments: bool,
    pub time_signature: bool,
    pub progression: bool,
    /// Take the preset's usual tempo where the meter stays the same. Off
    /// by default: the song keeps its tempo, so its own notes last as
    /// long as before.
    pub tempo: bool,
    pub swing: bool,
    /// Copy the preset's chord, bass and other accompaniment parts (a
    /// rhythm's are those of its matching style). Off, the song's own
    /// stay, as its drums do with `rhythm` off.
    pub matching_style: bool,
}
}

rhythm es la casilla Batería y matching_style la casilla Partes de acordes y bajo. Cada casilla va por su cuenta: lo que queda sin marcar, la canción lo conserva. En un patrón, cada casilla es un paso de write_style_parts, y cada paso toca solo sus propias notas:

#![allow(unused)]
fn main() {
if parts.time_signature {
    meter(project, index, style.time);
}
if parts.matching_style {
    recomp(project, index, style.comping, style.bass);
}
if parts.progression {
    progression(project, index, style);
}
// ...
if parts.rhythm
    && let Some(rhythm) = Rhythm::named(style.rhythm)
{
    write_rhythm(project, index, rhythm);
}
if parts.instruments {
    style_instruments(project, style, selected);
}
}

recomp vuelve a tocar los canales de acordes y de bajo y deja la batería; write_rhythm escribe los carriles de batería y deja el resto. Así una canción puede tomar la batería de un estilo y las partes de acordes y bajo de otro, en cualquier orden. Una banda guarda esa misma separación en un campo propio, drums (ver Cómo toca la banda un cifrado). La línea if parts.rhythm && let Some(rhythm) = ... es una cadena de let (let chain): el bloque se ejecuta solo cuando la casilla está marcada y se encuentra el ritmo del estilo, con rhythm ligado dentro de él.

Voces / instrumentos da los sonidos del estilo a los canales que tocan sus partes: Teclados y Bajo, o el canal elegido en el rack. Una canción importada de un archivo MIDI no tiene ninguno de los dos; sus canales llevan los nombres que les dio el archivo, como «PIANO MELODIA», «BANDONEON» o «CONTRABAJO». Entonces own_players_instruments da los sonidos del estilo a los músicos propios de la canción, a cada uno según la familia del instrumento que tenía, y deja la melodía en paz:

#![allow(unused)]
fn main() {
let program = match channel.program? {
    general_midi::CONTRABASS => band.bass_program(),
    // Violins to tremolo strings, and the string ensembles.
    40..=44 | 48..=51 => extras.strings.as_ref()?.program,
    program => match general_midi::FAMILIES[general_midi::family(program)] {
        "Piano" => piano,
        "Organ" => extras.organ.as_ref().map_or(piano, |organ| organ.program),
        "Guitar" => extras
            .guitar
            .as_ref()
            .map_or(piano, |guitar| guitar.program),
        "Bass" => band.bass_program(),
        _ => return None,
    },
};
}

El código está dentro de un cierre que se pasa a filter_map, que devuelve un Option, así que cada ? y el return None significan «este canal conserva su sonido»: un canal sin programa General MIDI, las cuerdas cuando el estilo no tiene, un coro o un bandoneón, que ningún estilo tiene. Un contrabajo está en la familia de las cuerdas de General MIDI, así que se reconoce primero, por su número, y toma el bajo. La melodía es cualquier canal cuyo nombre lo diga (Lead, Melody, Melodía, Voice, Voz, Canto), o si no el que mostraría la partitura de melodía.

Dos métodos pequeños corrigen la elección según el tipo de preajuste, cada uno tomando self por valor y devolviendo una copia cambiada: for_rhythm apaga la progresión, que un ritmo no tiene; y following mantiene los compases y el tempo de la canción para un estilo que toca cada compás en su propio compás. Se encadenan: parts.for_rhythm().following(rhythm.follows_the_meter).

La elección se guarda entre ejecuciones, en la memoria de egui, con el nombre apply-options. Hasta que la cambies, es todo menos la progresión de acordes, así que la canción conserva sus propios acordes:

#![allow(unused)]
fn main() {
const FIRST_OPTIONS: StyleParts = StyleParts {
    rhythm: true,
    instruments: true,
    time_signature: true,
    progression: false,
    tempo: true,
    swing: true,
    matching_style: true,
};
}

#[serde(default)] en el struct hace que un campo añadido más tarde se lea con su valor por defecto desde una copia guardada más antigua. Una const de un struct se escribe entera allí donde se usa, así que FIRST_OPTIONS no cuesta nada al ejecutarse.

El mismo estilo, la misma canción

Prueba Bluegrass, después Celtic, después Bluegrass otra vez: la tercera vez debería sonar exactamente como la primera. Un estilo no debe aplicarse encima del anterior, con el compás y el tempo del anterior, o cada clic alejaría un poco más la canción de sí misma. Así que la aplicación guarda, junto a cada versión de la canción que hizo la Biblioteca, la canción tal como vino, en su propio compás:

#![allow(unused)]
fn main() {
pub(super) struct SongSource {
    original: Project,
    library: Tried,
    /// Latest preset components, applied without changing the source meter.
    reference: Project,
}
}

Cada clic vuelve a empezar desde reference, y pregunta si el compás del estilo es distinto del de esa canción, no del que le dejó el último estilo:

#![allow(unused)]
fn main() {
let changes_meter = !lineup_core::remeter::is_in(&source.reference, time);
}

El tempo sigue la misma idea. Tempo pone el tempo del estilo donde suena la mayor parte de la canción, con take_tempo (ver La franja de tempo): una canción que empieza con un compás lento y luego toca a 121 BPM, con un estilo hecho para 180, toca a 180 donde tocaba a 121, y su compás lento sigue siendo igual de más lento que antes. Dar el tempo del estilo al primer compás de la canción haría que un archivo MIDI con una entrada lenta se disparara.

De dónde vino cada parte

Tomar solo algunas partes significa que una canción puede tener su batería de un estilo, sus partes de acordes y bajo de otro, su tempo de un tercero y sus acordes de la demostración con la que empezó. La Biblioteca lo muestra, una línea por parte: Tempo «Tango marcato (estilo)», Acordes «El Choclo (demostración)», y editado, era … en cuanto cambias tú esa parte. sources.rs lleva ese registro.

Las nueve partes son un enum, en el orden en que las lista la Biblioteca: la batería, las partes de acordes y bajo, la melodía, los instrumentos, el compás, la tonalidad, los acordes, el tempo y el swing. Un conjunto de ellas son nueve bits de un u16, un bit por parte:

#![allow(unused)]
fn main() {
pub struct PartSet(u16);
}
#![allow(unused)]
fn main() {
pub fn contains(self, part: SongPart) -> bool {
    self.0 & part.bit() != 0
}
}

Todo cambio que trae partes pasa por una única función, que compara la canción antes y después:

#![allow(unused)]
fn main() {
pub fn bring_in<R>(
    &mut self,
    source: &Source,
    taken: PartSet,
    edited: PartSet,
    change: impl FnOnce(&mut Project) -> R,
) -> R {
    let before = SongPart::ALL.map(|part| self.part_fingerprint(part));
    let result = change(self);
    for (part, before) in SongPart::ALL.into_iter().zip(before) {
        let now = self.part_fingerprint(part);
        // A key that changed with what was taken came with it: a
        // minor progression puts the song in minor, a row style puts
        // it in its row.
        let came = taken.contains(part)
            || (part == SongPart::Key && now != before && taken != PartSet::NONE);
        if came && !edited.contains(part) {
            self.sources.set(
                part,
                Some(PartSource {
                    source: source.clone(),
                    print: Some(now),
                }),
            );
        } else if now != before && !edited.contains(part) {
            self.follow_part(part, before, now);
        }
    }
    result
}
}

Las partes tomadas ahora vienen de source, aunque no hayan cambiado: un estilo en 4/4 aplicado a una canción que ya está en 4/4 le da igual su compás. Una parte que cambió solo junto con ellas, como unos acordes movidos a un compás nuevo, las sigue: sigue viniendo de donde venía, y está como vino si lo estaba antes. La tonalidad es la única parte que ninguna casilla nombra: viene con lo que la trajo, así que la tonalidad de una progresión menor viene de su estilo.

El print es una huella: un hash u64 de la parte tal como vino, hecho con std::hash::DefaultHasher a partir de sus notas, su tempo, sus acordes. Si una parte está editada no se guarda en ningún sitio; se pregunta cada vez, comparando la huella con la parte tal como está ahora:

#![allow(unused)]
fn main() {
edited: came.print != Some(self.part_fingerprint(part)),
}

Así, editar el tempo lo deja «editado, era …», y volverlo a poner como estaba, o deshacer, lo deja otra vez como vino, sin código que mantenga al día un indicador. El archivo del proyecto guarda solo la fuente y un indicador edited, nunca el hash: DefaultHasher puede cambiar de algoritmo en otra versión de Rust, y un hash guardado llamaría entonces editada a cada parte de cada archivo antiguo. Al cargar, la huella se vuelve a calcular a partir de la parte tal como está.

Tres de las partes son notas de los mismos patrones: la batería, la melodía y las partes de acordes y bajo, que son todo lo demás. Se distinguen por canal, con un único ayudante que calcula el hash de las notas de los canales que elige un cierre:

#![allow(unused)]
fn main() {
SongPart::Melody => self.hash_notes(&mut hasher, is_lead),
}

y las partes de acordes y bajo pasan |channel| !is_drum(channel) && !is_lead(channel). Con una banda, la batería y las partes de acordes y bajo son lo que toca la banda, así que sus huellas calculan en cambio el hash de los nombres de los estilos de los que vienen.

Las versiones anteriores conocían solo seis de las partes, y rechazarían un archivo que nombrara una que no conocen. Así que la batería, la melodía y la tonalidad se guardan en un campo propio, more_sources, que esas versiones se saltan. Se escribe siempre que alguna parte vino de algún sitio, aunque esté vacío, así que un archivo sin él es de antes; para un archivo así, sources_of_an_older_file deduce de dónde vinieron esas tres: la batería con los patrones, la melodía de una demostración con la demostración, y la tonalidad con los acordes.

Un detalle más. El registro de fuentes no es la música, y dos canciones que suenan igual deberían ser iguales: la aplicación compara proyectos para saber si hay cambios sin guardar. Así que su PartialEq dice que dos registros cualesquiera son iguales:

#![allow(unused)]
fn main() {
impl PartialEq for Sources {
    fn eq(&self, _: &Self) -> bool {
        true
    }
}
}

Un impl escrito a mano es la manera en que un tipo participa en un PartialEq derivado (el del Project) sin cambiar la respuesta.

La pestaña Biblioteca

La Biblioteca lista los estilos armónicos, los ritmos y tus propios estilos en un único catálogo, ordenado por los nombres tal como se muestran en el idioma elegido:

#![allow(unused)]
fn main() {
pub enum Preset<'a> {
    Style(&'static Style),
    Rhythm(&'static Rhythm),
    User(usize, &'a UserStyle),
}
}

Dos tipos de entrada toman prestado de tablas que viven para siempre; tus propios estilos viven en la aplicación, así que Preset tiene un tiempo de vida, 'a, que dice que no puede vivir más que la lista de la que toma prestado. Cada pregunta que hace la lista (el nombre, el compás, el tempo, si trae una progresión) es un método con un único match, así que el resto del navegador nunca pregunta de qué tipo es. Un ritmo que es justo la batería de un estilo con el mismo nombre, tempo y aire se lista una sola vez, como el estilo.

Las casillas bajo la búsqueda dejan solo los preajustes parecidos a la canción, «En 4/4» (en su compás) o «Cerca de 120 BPM» (cerca de su tempo), o los que tienen lo que buscas: «Con acordes» (una progresión), «Swing» (una batería con swing) o un instrumento elegido en un menú, «Cualquier instrumento». Lo que miran en un preajuste es un struct Copy pequeño,

#![allow(unused)]
fn main() {
struct Traits {
    time: TimeSignature,
    /// The speed it was made for, in beats a minute.
    tempo: f32,
    /// Whether it plays every bar in its own meter, so it fits any time
    /// signature.
    any_meter: bool,
    /// Whether it brings a chord progression.
    chords: bool,
    /// Whether its drums swing.
    swing: bool,
}
}

y la decisión es un único método:

#![allow(unused)]
fn main() {
fn keeps(self, traits: Traits, instruments: impl FnOnce() -> Vec<Instrument>) -> bool {
    let filters = self.filters;
    // 7/8 as 2+2+3 is still 7/8.
    let same_time = traits.any_meter || traits.time.ungrouped() == self.time.ungrouped();
    let ratio = traits.tempo / self.tempo;
    let near = ratio.max(1.0 / ratio) <= NEAR_SPEED;
    (!filters.time || same_time)
        && (!filters.speed || near)
        && (!filters.chords || traits.chords)
        && (!filters.swing || traits.swing)
        && filters
            .instrument
            .is_none_or(|instrument| instruments().contains(&instrument))
}
}

Las líneas se leen como reglas unidas por «y»: !filters.time || same_time es «o la casilla no está marcada, o el compás es el mismo». ratio.max(1.0 / ratio) convierte «la mitad de rápido» (0,5) y «el doble de rápido» (2,0) en el mismo número, 2, así que más rápido y más lento se juzgan igual; con NEAR_SPEED en 1,15, una canción a 130 BPM conserva los preajustes hechos para 113 a 149,5. Comparar proporciones en lugar de una diferencia en BPM trata igual las canciones lentas y las rápidas: ±15 BPM sería mucho a 60 y poco a 180. keeps toma self por valor: el struct es Copy y ocupa unos pocos bytes, así que no cuesta más que una referencia.

Los instrumentos son la única pregunta que cuesta algo: los de un preajuste son los de la banda que lo tocaría, calculados a partir de su estilo. Así que llegan como un cierre, instruments, al que se llama solo al final, cuando hay un instrumento elegido. Option::is_none_or es true para None, ningún instrumento elegido, y si no pregunta al cierre por el que hay dentro. && se detiene en el primer false, así que un preajuste que una casilla anterior ya dejó fuera nunca llega a calcular sus instrumentos.

La lista se agrupa por género, un egui::CollapsingHeader por género con su cuenta y tus propios estilos primero bajo Estilos de usuario, salvo que Ajustes > Biblioteca de estilos diga Lista alfabética. Una búsqueda abre cada grupo que tiene una coincidencia. Cuando la canción cambia de estilo, el grupo del nuevo se abre durante medio segundo, lo justo para que egui desplace su fila iluminada hasta la vista, y luego deja los grupos como los tenías.

Probar, cambiar y deshacer

Lo que hace un clic depende de la canción. Con una banda, como en una canción de Archivo > Nuevo o en una demostración, el estilo pasa a ser la manera en que la banda toca la hoja de acordes (ver Cómo toca la banda una hoja de acordes). Mientras la canción suena sin banda, el clic cambia la canción entera, así que la música sigue. Si no, escribe el estilo en un patrón de prueba propio y lo toca ahí.

Hacer clic en otro estilo debería reemplazar la prueba, no ir apilando patrones, para poder hojear la biblioteca. Pero en cuanto empiezas a editar ese patrón, es tuyo. La aplicación distingue los dos casos guardando una copia de lo que escribió:

#![allow(unused)]
fn main() {
pub(super) struct Audition {
    pattern: usize,
    /// The pattern as the library left it. If it still looks like this,
    /// the next click may rewrite it.
    written: Pattern,
    style: Option<&'static Style>,
    rhythm: Option<&'static Rhythm>,
    row_style: Option<&'static RowStyle>,
    micromode_style: Option<&'static MicromodeStyle>,
}
}
#![allow(unused)]
fn main() {
fn unchanged_audition(&self) -> Option<&Audition> {
    let pattern = self.project.pattern(self.pattern)?;
    self.auditions
        .iter()
        .rev()
        .find(|audition| audition.pattern == self.pattern && &audition.written == pattern)
}
}

== entre dos Patterns compara cada nota, gracias al PartialEq derivado: en lugar de seguir cada edición, se guarda una copia y se compara, la misma idea que con los cambios sin guardar. La aplicación guarda las últimas 64 pruebas, no solo la más nueva, y busca desde la más nueva con rev(). Eso es por el deshacer: después de que Ctrl+Z traiga de vuelta el estilo anterior, el patrón coincide con una copia más antigua, y el siguiente clic lo sigue cambiando en el mismo lugar.

Los estilos armónicos y los ritmos toman las partes que marcaste (ver Qué partes tomar). Los estilos de series y los de micromodos no tienen esa elección: se prueban de la misma manera, y solo se diferencian en las notas que escriben. Así que sus pasos comunes son un único método, y el paso distinto es un closure que se le pasa:

#![allow(unused)]
fn main() {
fn write_pattern_style(
    &mut self,
    name: &'static str,
    tempo: f32,
    rhythm: Option<&'static Rhythm>,
    write: impl FnOnce(&mut Project, usize),
) -> Option<(usize, Pattern)> {
}

impl FnOnce(&mut Project, usize) acepta cualquier closure que se pueda llamar una vez con esos dos argumentos. El compilador hace una copia del método para cada closure que se le pasa, así que la llamada no cuesta más que el código escrito a mano. FnOnce es el que menos pide de los tres traits de closure (Fn, FnMut, FnOnce): todo closure es al menos FnOnce, así que pedirlo los acepta todos.

Cada escritura pasa por edit, así que una prueba es un solo paso de deshacer, el hilo de audio se entera por el diff de siempre, y guardar funciona sin más. Dentro, bring_in registra de dónde vinieron las partes.

Cómo se prueba

Las tablas son datos, y los datos pueden estar mal tan fácilmente como el código, sin que nada se rompa. Así que las pruebas leen la biblioteca entera: every_style_parses_and_fits_in_a_pattern, names_are_unique_and_rhythms_exist, every_genre_has_styles, styles_play_in_any_key, y para los ritmos every_line_is_whole_bars_of_known_characters, names_are_unique_and_fills_exist y congas_play_high_and_low. a_new_time_signature_keeps_the_kind_of_rhythm, an_odd_meter_gets_a_rhythm_grouped_its_way y a_time_signature_with_no_rhythm_gets_a_basic_beat siguen a for_time por su cadena.

apply.rs comprueba adónde van las notas (styles_go_to_keys_and_bass, chords_go_around_a_locked_channel), que recomping_keeps_the_chords, que una canción entera toma un ritmo o un estilo nuevos, que un vals pone la canción en 3/4 y, la más amplia, every_demo_takes_every_kind_of_library_click. sources.rs comprueba una parte que llega, se edita y se deshace:

#![allow(unused)]
fn main() {
#[test]
fn a_part_brought_in_is_edited_once_changed_and_not_after_undoing_it() {
    let mut project = Project::demo();
    project.bring_in_all(&demo());
    assert_eq!(from(&project, SongPart::Tempo), Some(("El Choclo", false)));
    let tempo = project.tempo;
    project.scale_tempo(Tempo::new(tempo.bpm() + 10.0));
    assert_eq!(from(&project, SongPart::Tempo), Some(("El Choclo", true)));
    assert_eq!(from(&project, SongPart::Swing), Some(("El Choclo", false)));
    project.scale_tempo(tempo);
    assert_eq!(from(&project, SongPart::Tempo), Some(("El Choclo", false)));
}
}

Para las casillas, a_styles_sounds_go_to_an_imported_songs_players_by_family_but_the_tune arma una canción como un tango importado, con un «PIANO MELODIA», un bandoneón, una guitarra, un contrabajo, cuerdas y un coro, y comprueba qué canales toman los sonidos de Tango y de Bossa nova y cuáles conservan los suyos; a_songs_keys_and_bass_still_take_a_styles_sounds_alone comprueba que una canción con Teclados y Bajo cambia solo ahí. En band.rs, drums_and_chord_parts_come_on_their_own_from_builtin_and_saved_styles da a una banda de Rock ride primero la batería de Bossa nova, del estilo incorporado y de una copia guardada de él, y comprueba que el piano y el bajo quedan como estaban, que el archivo se vuelve a leer igual y que, cuando llegan también las partes de acordes y bajo, la banda vuelve a ser simplemente Bossa nova, en el formato que necesitaba antes; a_styles_chord_parts_can_come_without_its_drums toma las partes de acordes y bajo de Bossa nova sobre la batería de Rock ride, lo que necesita el formato 29. a_style_tempo_goes_where_most_of_the_song_is comprueba take_tempo en una canción con un primer compás lento.

En la aplicación, the_library_tick_boxes_keep_only_presets_like_the_song cuenta los preajustes listados a medida que se marcan las casillas; the_same_style_brings_the_same_tempo_whatever_was_tried_before pulsa Bluegrass, Celtic, Bluegrass y Celtic en una canción en 2/4 y en 4/4, y comprueba que cada estilo trae el mismo tempo cada vez; y trying_row_and_micromode_styles_rewrites_one_pattern_while_it_is_unchanged comprueba que las pruebas caen en un único patrón y que cada una es un solo paso de deshacer.

Los ejercicios sobre la biblioteca están en Ejercicios: la banda.