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

Series dodecafónicas y sus reglas

Archivos: crates/lineup-core/src/harmony/row.rs, crates/lineup-core/src/serial.rs, crates/lineup-core/src/library/rows.rs, crates/lineup-core/src/harmony/voice_leading/serial.rs y crates/lineup-app/src/twelve_tone.rs

Casi todo Ferrous Lineup supone música en una tonalidad: una nota que es la casa, acordes que se inclinan hacia ella, teclas de armonía numeradas de I a vii°. Una gran parte de la música del siglo XX no tiene tonalidad, y mucha se escribió con series dodecafónicas (dodecafonismo; en inglés, twelve-tone rows): las doce notas en un orden del que está hecha toda la obra. En la aplicación, serie dodecafónica es una de las clases de tonalidad; Serie…, en el piano roll, abre una ventana para escribir, dibujar o elegir una serie, ver su matriz de 48 formas y escribir cualquier forma en el patrón; la Biblioteca tiene un grupo de estilos de series, y la revisión de Conducción de voces conoce las reglas con las que escribían los compositores dodecafónicos. Este capítulo trata las cuatro cosas, y el Rust que hay detrás: un newtype que siempre es una serie de verdad, aritmética con doce números, backtracking, un builder que corre al compilar, conjuntos de bits y programación dinámica.

La música, en pocas palabras

En Do mayor, Do es la casa. Sol tira hacia ella, Si tira fuerte, y una obra que termina en Do suena terminada. Hacia 1921, Arnold Schoenberg buscó una manera de escribir música en la que ninguna nota fuera la casa. Su respuesta fue usar las doce notas por igual, en un orden fijo que elegía para cada obra: la serie. Ninguna nota vuelve hasta que hayan sonado las otras once, así que ninguna llega a ser el centro.

Esta es la serie de la Suite para piano op. 25 de Schoenberg, de 1925, una de las primeras obras construidas enteras a partir de una:

Mi  Fa  Sol  Do♯  Fa♯  Re♯  Sol♯  Re  Si  Do  La  La♯

Una serie sería aburrida si solo pudiera tocarse de una manera, así que viene en cuatro:

formacómola serie de Schoenberg sobre Mi
P, originalcomo está escritaMi Fa Sol Do♯ Fa♯ Re♯ Sol♯ Re Si Do La La♯
I, inversióncada intervalo dado vuelta: donde P sube un semitono, I baja unoMi Re♯ Do♯ Sol Re Fa Do Fa♯ La Sol♯ Si La♯
R, retrogradaciónP de atrás hacia adelanteLa♯ La Do Si Re Sol♯ Re♯ Fa♯ Do♯ Sol Fa Mi
RI, retrogradación de la inversiónI de atrás hacia adelanteLa♯ Si Sol♯ La Fa♯ Do Fa Re Sol Do♯ Re♯ Mi

Cada una de las cuatro puede empezar en cualquiera de las doce notas, lo que da 48 formas. Se nombran con una letra y un número: el número es la nota en que empieza la forma, con Do como 0, Do♯ como 1, hasta Si como 11. Así, la serie de arriba es P4 (empieza en Mi), e I0 es su inversión empezando en Do. Una retrogradación toma el número de la forma que da vuelta: R4 es P4 al revés, así que termina en Mi.

Algunos libros numeran las formas desde la primera nota de la propia serie, de modo que la serie tal como está escrita sea siempre P0. Entonces la serie de Schoenberg es P0, y lo que Ferrous Lineup llama I11 allí es I7. Las formas son las mismas; solo se mueven sus números. Ferrous Lineup numera una forma por la nota en que empieza, porque así una etiqueta nombra una nota que puedes encontrar en el teclado.

La matriz

Las 48 formas caben en un cuadrado de 12×12. La serie va arriba, su inversión desde la misma nota baja por el lado izquierdo, y cada fila i es la serie movida para empezar en la nota que está a la izquierda de esa fila:

      I4   I5   I7   I1   I6   I3   I8   I2   I11  I0   I9   I10
P4    Mi   Fa   Sol  Do♯  Fa♯  Re♯  Sol♯ Re   Si   Do   La   La♯    R4
P3    Re♯  Mi   Fa♯  Do   Fa   Re   Sol  Do♯  La♯  Si   Sol♯ La     R3
P1    Do♯  Re   Mi   La♯  Re♯  Do   Fa   Si   Sol♯ La   Fa♯  Sol    R1
P7    Sol  Sol♯ La♯  Mi   La   Fa♯  Si   Fa   Re   Re♯  Do   Do♯    R7
...
      RI4  RI5  RI7  RI1  ...

Una fila leída de izquierda a derecha es una forma P; de derecha a izquierda, una forma R. Una columna leída de arriba abajo es una forma I; de abajo arriba, una forma RI. Las etiquetas alrededor del cuadrado dicen cuál: P a la izquierda, R a la derecha, I arriba, RI abajo. En la ventana Serie dodecafónica, haz clic en cualquier etiqueta para elegir esa forma, y su fila o su columna se ilumina.

La diagonal desde arriba a la izquierda hasta abajo a la derecha es la misma nota de punta a punta (Mi, aquí): una manera rápida de comprobar que una matriz está bien hecha.

Los grupos de notas tomados de una serie, como sus cuatro grupos de tres, son conjuntos de clases de altura, y la teoría que les pone nombre tiene un capítulo propio: Conjuntos de clases de altura, y una armonía con cuatro nombres.

Una serie en Rust: ToneRow

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct ToneRow([u8; 12]);
}

Una serie son doce números, los semitonos de sus notas por encima de Do. Es un newtype alrededor de un array: el array es privado del módulo, así que las únicas maneras de hacer un ToneRow son funciones que lo comprueban:

#![allow(unused)]
fn main() {
pub fn new(semitones: [u8; 12]) -> Option<Self> {
    let set: PitchSet = PitchSet::from_semitones(&semitones);
    (set.len() == 12 && semitones.iter().all(|&n| n < 12)).then_some(ToneRow(semitones))
}
}

Se juntan los doce números en un conjunto de clases de altura, y si el conjunto tiene doce miembros, ninguna nota se repitió. Todo ToneRow del programa es, entonces, una serie de verdad: el invariante se comprueba una vez, aquí, y nada más tiene que preocuparse por él.

Archivos: try_from e into

Los dos atributos de serde del struct dicen: «para guardar un ToneRow, conviértelo en un String; para leer uno, lee un String e intenta convertirlo en un ToneRow». Las conversiones son implementaciones de traits comunes:

#![allow(unused)]
fn main() {
impl TryFrom<String> for ToneRow {
    type Error = String;

    fn try_from(text: String) -> Result<Self, String> {
        ToneRow::parse(&text)
    }
}

impl From<ToneRow> for String {
    fn from(row: ToneRow) -> Self {
        let numbers: Vec<String> = row.0.iter().map(u8::to_string).collect();
        numbers.join(" ")
    }
}
}

Así, un archivo de proyecto guarda "4 5 7 1 6 3 8 2 11 0 9 10", que una persona puede leer, en lugar de una lista de doce números entre corchetes. Guardar no puede fallar, así que es un From simple; leer sí puede, así que es un TryFrom con un error. Como la lectura pasa por parse, un archivo con una nota escrita dos veces se rechaza con un mensaje, en vez de dar una serie rota. PitchSet tiene el mismo par, así que un conjunto se guarda como "0 3 6 9".

parse también lee el cuadro de arriba de la ventana Serie dodecafónica. Acepta nombres de notas en letras (E F G Db Gb Eb Ab D B C A Bb) o en solfeo (Mi Fa Sol Reb Solb Mib Lab Re Si Do La Sib), números (4 5 7 1 …) o la forma compacta de los libros de dodecafonismo, un carácter por nota con T y E para diez y once (45716382E09T). Cuando una nota viene dos veces, dice cuál, y qué nota falta en su lugar.

Un nombre se lee primero en letras, y en solfeo cuando eso falla. Ninguna sílaba es también un nombre en letras, porque ninguna empieza con una letra de la A a la G salvo Do y Fa, cuya segunda letra no es un sostenido ni un bemol, así que las dos nunca chocan. Antes de leer solfeo, parse_name escribe el nombre con una mayúscula y el resto en minúsculas, así que SOL# y sib se leen como Sol# y Sib, donde la b minúscula es un bemol:

#![allow(unused)]
fn main() {
fn parse_name(token: &str) -> Result<u8, String> {
    parse_letters(token).or_else(|error| {
        let mut chars = token.chars();
        let first = chars.next().map(|c| c.to_uppercase().collect::<String>());
        let written = first.unwrap_or_default() + &chars.as_str().to_lowercase();
        NoteName::read_as(&written, NoteNaming::Solfege)
            .filter(|(_, rest)| rest.is_empty())
            .map(|(name, _)| name.pitch_class().semitone())
            .ok_or(error)
    })
}
}

or_else corre su closure solo ante un error, y ok_or(error) devuelve el error de las letras cuando también falla el solfeo, así que un nombre equivocado se explica en los términos que más gente escribe.

Desde las notas lee una serie del piano roll: las doce primeras clases de altura distintas entre las notas seleccionadas (o todas), en el orden en que empiezan. ToneRow::from_notes se salta una nota cuya clase de altura ya está en la serie, así que puedes dibujar libremente y repetir notas, mientras aparezcan las doce. Las dos terminan en una tercera conversión, desde un Vec<u8>:

#![allow(unused)]
fn main() {
impl TryFrom<Vec<u8>> for ToneRow {
    type Error = String;

    fn try_from(semitones: Vec<u8>) -> Result<Self, String> {
        let array: [u8; 12] = semitones
            .try_into()
            .map_err(|v: Vec<u8>| trf("a row has 12 notes, not {}", &[&v.len()]))?;
        ToneRow::new(array).ok_or_else(|| tr("a row has each of the 12 notes once").to_string())
    }
}
}

Un Vec puede tener cualquier cantidad de notas y un array exactamente doce, así que el try_into de la biblioteca estándar de uno al otro puede fallar. Cuando falla, devuelve el vector sin cambios como error, y map_err lo convierte en un mensaje que dice cuántas notas había. Después, ok_or_else convierte también el None de new en un error. Los closures solo corren cuando algo está mal, así que una serie correcta nunca arma un mensaje.

Todas las formas desde una función

#![allow(unused)]
fn main() {
pub fn form(self, label: RowLabel) -> ToneRow {
    let first = self.0[0];
    let n = label.number;
    let mut notes = self.0.map(|note| {
        let up = (note + 12 - first) % 12;
        if label.form.is_inverted() {
            (n + 12 - up) % 12
        } else {
            (n + up) % 12
        }
    });
    if label.form.is_backwards() {
        notes.reverse();
    }
    ToneRow(notes)
}
}

Primero se mide cada nota como up, su distancia por encima de la primera nota de la serie (de 0 a 11). Una forma P la pone a la misma distancia por encima del nuevo comienzo n; una forma I, a la misma distancia por debajo. Eso es todo lo que es una inversión. El + 12 antes de cada % evita que los números bajen de cero: son u8, y 4 - 7 no daría la vuelta a la octava, sino que detendría el programa en una compilación de depuración.

self.0.map(...) es el map de un array, no de un iterador: llama al closure con cada uno de los doce elementos y devuelve un [u8; 12] nuevo, sin Vec y sin collect. La longitud es parte del tipo, así que el resultado sigue siendo exactamente doce notas.

Con form, la matriz son dos líneas:

#![allow(unused)]
fn main() {
pub fn matrix(self) -> [[u8; 12]; 12] {
    let inversion = self.form(RowLabel::new(RowForm::Inversion, self.0[0]));
    inversion
        .0
        .map(|start| self.starting_on(PitchClass::from_semitone(start)).0)
}
}

La inversión desde la primera nota de la serie es la columna de la izquierda; cada una de sus notas empieza una fila.

En el otro sentido, «¿qué forma de esta serie es esa?» prueba las 48 hasta que una coincide:

#![allow(unused)]
fn main() {
pub fn label_of(self, other: ToneRow) -> Option<RowLabel> {
    RowForm::ALL.into_iter().find_map(|form| {
        (0..12)
            .map(|n| RowLabel::new(form, n))
            .find(|&label| self.form(label) == other)
    })
}
}

find_map se detiene en la primera forma que da Some, y find en el primer número que coincide, así que la búsqueda termina en cuanto encuentra la forma.

Qué hace interesante a una serie

Debajo de la matriz, la ventana Serie dodecafónica dice qué tiene de especial la serie. Cada línea es una función corta.

Los pasos. intervals() da los once pasos de nota a nota, en semitonos hacia arriba. Para la serie de Schoenberg son 1 2 6 5 9 5 6 9 1 9 1. Se construye con std::array::from_fn, que hace un array llamando a un closure con cada índice:

#![allow(unused)]
fn main() {
pub fn intervals(self) -> [u8; 11] {
    std::array::from_fn(|i| (self.0[i + 1] + 12 - self.0[i]) % 12)
}
}

El tipo de retorno dice once, así que from_fn llama al closure once veces, e i + 1 nunca pasa de la duodécima nota.

Series de todos los intervalos. Cuando los once pasos son todos distintos, cada intervalo, de un semitono a una séptima mayor, aparece exactamente una vez. La Suite lírica de Alban Berg (1927) empieza con una serie así: Fa Mi Do La Sol Re Sol♯ Do♯ Re♯ Fa♯ La♯ Si, con el tritono justo en el medio. Son raras: de las 39.916.800 series que empiezan en Do, solo 3.856 son de todos los intervalos. Así que el botón 🎲 Todos los intervalos no puede limitarse a barajar notas hasta dar con una; construye la serie nota a nota:

#![allow(unused)]
fn main() {
fn all_interval_from(notes: &mut Vec<u8>, random: &mut Random) -> Option<Vec<u8>> {
    if notes.len() == 12 {
        return Some(notes.clone());
    }
    let used: Vec<u8> = notes.windows(2).map(|w| (w[1] + 12 - w[0]) % 12).collect();
    let mut intervals: Vec<u8> = (1..12).filter(|i| !used.contains(i)).collect();
    for i in (1..intervals.len()).rev() {
        intervals.swap(i, random.below(i + 1));
    }
    let last = *notes.last()?;
    for interval in intervals {
        let next = (last + interval) % 12;
        if notes.contains(&next) {
            continue;
        }
        notes.push(next);
        if let Some(row) = all_interval_from(notes, random) {
            return Some(row);
        }
        notes.pop();
    }
    None
}
}

Esto es backtracking (vuelta atrás): probar cada paso todavía no usado, en un orden al azar; si la nota en la que cae está libre, añadirla y seguir desde ahí (la función se llama a sí misma); si eso no lleva a ninguna parte, quitar la nota (pop) y probar el paso siguiente. Cuando ningún paso sirve, devolver None para que quien llamó quite su nota. windows(2) entrega cada par de vecinas, [a, b], que es justo lo que es un paso. Todas las llamadas comparten un vector a través de &mut, así que probar una nota cuesta un push y un pop, no una copia.

Series al azar. El botón 🎲 Al azar baraja las doce notas con el algoritmo de Fisher-Yates, el mismo bucle que baraja los intervalos de arriba: recorrer desde el último lugar hasta el primero, e intercambiar cada uno con un lugar elegido al azar entre los que aún no se han hecho. Así todos los órdenes son igual de probables. Los números al azar vienen de un generador diminuto, SplitMix64, escrito en unas pocas líneas en vez de añadir una biblioteca:

#![allow(unused)]
fn main() {
pub(crate) fn next(&mut self) -> u64 {
    self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
    let mut z = self.0;
    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
    z ^ (z >> 31)
}
}

wrapping_add y wrapping_mul dicen «aquí desbordar está bien, que dé la vuelta»: un + normal que desborda detendría una compilación de depuración. La misma semilla da siempre la misma serie, lo que hace que las pruebas se puedan repetir. pub(crate) deja que otros módulos de lineup-core usen el generador (el entrenamiento auditivo elige sus preguntas con él) sin que forme parte de la API pública del crate.

Grupos de notas. Los compositores suelen tocar una serie en grupos: cuatro tricordios (tres notas cada uno), tres tetracordios, dos hexacordios. segments(size) corta la serie con chunks_exact, que entrega porciones de exactamente size notas y deja fuera una última más corta, y la ventana nombra la clase de conjunto de cada grupo (ver Conjuntos de clases de altura). Los tetracordios de la serie de Schoenberg son tres clases distintas, 4-12, 4-Z15 y 4-1; los tricordios de la serie Cuatro tríadas son todos 3-11, las tríadas mayores y menores.

Combinatoriedad. Toma las seis primeras notas de una serie. Puede que otra forma de la misma serie empiece exactamente con las otras seis. Toca las dos formas a la vez, y cada mitad de la música tiene las doce notas, sin que ninguna vuelva antes de tiempo. Schoenberg construyó sus obras tardías sobre esto, y se llama combinatoriedad hexacordal:

#![allow(unused)]
fn main() {
pub fn combinatorial_forms(self) -> Vec<RowLabel> {
    let missing = PitchSet::from_semitones(&self.0[..6]).complement();
    [
        RowForm::Prime,
        RowForm::Inversion,
        RowForm::RetrogradeInversion,
    ]
    .into_iter()
    .flat_map(|form| (0..12).map(move |n| RowLabel::new(form, n)))
    .filter(|&label| PitchSet::from_semitones(&self.form(label).0[..6]) == missing)
    .collect()
}
}

La retrogradación queda fuera porque siempre funciona (su primera mitad es la segunda mitad de la serie), así que no dice nada de la serie. La serie de la op. 25 de Schoenberg es combinatoria con I11: sus seis primeras notas, Mi Fa Sol Do♯ Fa♯ Re♯, y las seis primeras de I11, Si La♯ Sol♯ Re La Do, forman las doce. La Serie de Lineup, escrita para Ferrous Lineup, es combinatoria con I3.

El move de move |n| hace falta porque flat_map saca el closure de dentro fuera del de afuera, y ese closure usa form: move hace que se lleve su propia copia de form en vez de tomar prestada una variable que está a punto de desaparecer.

Las series para empezar

El menú Series famosas y de ejemplo tiene ocho series. Cuatro vienen de obras famosas, y cuatro se escribieron para Ferrous Lineup para mostrar lo que puede hacer una serie. ROW_PRESETS es un array const de ellas, cada una con un nombre, de dónde viene y una frase sobre ella.

seriedequé muestra
Mi Fa Sol Do♯ Fa♯ Re♯ Sol♯ Re Si Do La La♯Schoenberg, Suite para piano op. 25 (1925)una de las primeras obras dodecafónicas; sus cuatro últimas notas, Si Do La La♯, deletrean BACH al revés con los nombres alemanes de las notas, donde B es Si♭ y H es Si
Fa Mi Do La Sol Re Sol♯ Do♯ Re♯ Fa♯ La♯ SiBerg, Suite lírica (1927)una serie de todos los intervalos, con el tritono en el medio
La Fa♯ Sol Sol♯ Mi Fa Si La♯ Re Do♯ Do Re♯Webern, Sinfonía op. 21 (1929)un palíndromo: al revés, es la misma serie un tritono más arriba (R9 es P3)
Si La♯ Re Re♯ Sol Fa♯ Sol♯ Mi Fa Do Do♯ LaWebern, Concierto op. 24 (1934)cuatro grupos de tres notas, todos del mismo micromodo, Menor 3
Do Re Fa♯ Sol♯ Fa Mi La Do♯ Si Re♯ Sol La♯Serie de Lineupcombinatoria por hexacordios con I3
Do Mi Sol Re Fa♯ La La♯ Do♯ Fa Si Re♯ Sol♯Cuatro tríadascuatro tríadas que aun así usan cada nota una vez
Do Mi Re♯ Sol Sol♯ Si Fa Re Do♯ La La♯ Fa♯Serie espejoun palíndromo, como la de Webern
Do Re Fa Mi Si La Re♯ Sol Sol♯ Do♯ La♯ Fa♯Todos los intervalosuna serie de todos los intervalos

Las obras de 1925 a 1929 son de dominio público en los Estados Unidos (95 años después de su publicación), y sus compositores murieron en 1951, 1935 y 1945, así que también lo son donde los derechos de autor duran 70 años después de la muerte del autor. El Concierto de Webern se publicó en 1948, así que es de dominio público donde los derechos duran 70 años después de la muerte del autor. Solo se usan las series: doce notas cada una.

Algunas series no tienen 48 formas distintas. Como la R9 de Webern es su P3, cada forma R de esa serie es también una forma P, y cada forma RI una forma I: solo 24 son distintas. Con la serie de Berg y la Serie espejo pasa lo mismo: leída al revés, cada una es ella misma un tritono más arriba. label_of prueba primero las formas P, así que una forma así se nombra como la forma P que también es.

De una serie a notas: el serialismo

Una serie dice qué nota viene después, y nada más. Para hacer música con ella, alguien todavía tiene que decidir cuánto dura cada nota, qué tan fuerte suena, en qué octava, y si algunas notas suenan juntas. RowWriting guarda esas decisiones, y los controles de debajo de la matriz las fijan:

#![allow(unused)]
fn main() {
pub struct RowWriting {
    /// The form to start with, such as `P0`.
    pub label: RowLabel,
    pub chain: Chain,
    /// How many notes of the row sound together: 1 for a melody, 3 to
    /// play the row as four three-note chords (trichords), 6 for two
    /// hexachords.
    pub together: usize,
    /// How long each note (or chord) lasts, in steps, when the durations
    /// do not follow the row.
    pub step: usize,
    pub register: Register,
    /// The lowest note the row is written from.
    pub low: Note,
    /// Whether the durations follow the row (see the module documentation).
    pub durations: bool,
    /// Whether the velocities follow the row's inversion.
    pub dynamics: bool,
    /// Whether the octaves follow the row's retrograde.
    pub octaves: bool,
}
}

Después (chain) dice qué viene tras la forma hasta llenar el patrón: nada (Una vez), la misma forma otra vez (Repetir), o las cuatro formas por turno con el mismo número (P, R, I, RI). Juntas es together, y Cada es step. Los tres registros (el menú Octavas) deciden las octavas cuando la serie no lo hace:

  • Cerrado pone cada nota en la octava por encima de la nota más grave;
  • Suave pone cada nota tan cerca como pueda de la anterior, lo que da una línea cantable (un tritono, justo a mitad de camino, sube);
  • Amplio manda las notas grave, aguda, media, grave, aguda, media por tres octavas, las líneas a saltos de Anton Webern.

Serialismo integral

Hacia 1950, Olivier Messiaen, Pierre Boulez y Milton Babbitt llevaron la idea más lejos: si las alturas siguen una serie, ¿por qué no también las duraciones, las dinámicas y las octavas? Eso es el serialismo integral (o total). Las obras reales traducen las series a duraciones y dinámicas de muchas maneras; Ferrous Lineup usa tres sencillas, cada una leyendo una forma distinta de la serie para que las series no avancen a la par:

  • Duraciones: los números de la serie (la distancia de cada nota por encima de la primera, de 0 a 11) se vuelven duraciones. La nota k-ésima dura tantos pasos como el número k-ésimo, más uno, así que de 1 a 12 semicorcheas.
  • Dinámicas: los números de la inversión se vuelven doce velocidades, de muy suave a muy fuerte: 28 + 9 * level, de 28 a 127.
  • Octavas: los números de la retrogradación, contados de tres en tres (% 3), eligen una de tres octavas.

Para la serie de Schoenberg los números son 0 1 3 9 2 11 4 10 7 8 5 6, así que con Duraciones marcada sus notas duran 1, 2, 4, 10, 3, 12… pasos. Dentro de write_row, cada nota hace las tres preguntas por turno:

#![allow(unused)]
fn main() {
let lasts = if writing.durations {
    usize::from(numbers[place % 12]) + 1
} else {
    step
};
// ...
let octave = if writing.octaves {
    // The retrograde's numbers, in threes.
    Some(numbers[11 - k % 12] % 3)
} else if writing.register == Register::Wide {
    // Low, high, middle, and round again, against the row.
    Some([0, 2, 1][k % 3])
} else {
    None
};
// ...
let velocity = if writing.dynamics {
    // The inversion's numbers.
    dynamic((12 - numbers[k % 12]) % 12)
} else {
    crate::DEFAULT_VELOCITY
};
}

place cuenta notas a través de las formas, así que con P, R, I, RI la serie de duraciones sigue de una forma a la siguiente en vez de volver a empezar. k es place más el lugar de la nota en su acorde, así que cada nota de un acorde tiene su propia octava y su propia intensidad.

write_row es una función pura: entra una serie, las decisiones y la duración de un patrón; sale una lista de NoteEvent. La ventana la llama en cada fotograma para avisar cuando el patrón es demasiado corto para toda la serie («El patrón solo tiene lugar para 9 de las 12 notas»), y Escribir P0 en el patrón actual manda las mismas notas al piano roll, que reemplaza las notas del canal con la edición de siempre, así que Ctrl+Z trae de vuelta las anteriores. Guardar en un patrón nuevo las pone en un patrón nuevo.

Una serie como tonalidad

serie dodecafónica en los menús de tonalidad, o Usar como tonalidad en la ventana Serie dodecafónica para la forma que elegiste, hace de la serie la tonalidad. En el código es una clase más de tonalidad, ScaleKind::Row(ToneRow), que lleva su serie consigo (Escalas, modos y tonalidades muestra cómo la trata el resto del código). La serie se guarda empezando en Do, y la fundamental de la tonalidad dice dónde empieza de verdad, así que P4 de la serie de Schoenberg se guarda así (el archivo escribe las notas siempre con letras):

scale: (root: E, kind: Row("0 1 3 9 2 11 4 10 7 8 5 6")),

Con una serie como tonalidad, el piano roll numera sus notas del 1 al 12 en el teclado, en el orden de la serie, así que puedes dibujar la serie siguiendo los números. Las teclas de armonía tocan los grupos de la serie en lugar de acordes, como arman sus acordes los compositores dodecafónicos: 3 notas da cuatro teclas para los tricordios, 1–3, 4–6, 7–9 y 10–12; 4 notas da tres teclas, y 6 notas, dos. Salen de cortar la serie en orden:

#![allow(unused)]
fn main() {
ScaleKind::Row(_) => {
    let group = row_group(size);
    Some(
        notes
            .chunks(group)
            .enumerate()
            .map(|(i, chunk)| {
                let label = format!("{}–{}", i * group + 1, i * group + chunk.len());
                GroupKey::new(scale, Some(label), chunk.to_vec())
            })
            .collect(),
    )
}
}

Cambiar la tonalidad de Do mayor a una serie no mueve ninguna nota, porque no hay un lugar sensato al que pueda ir Mi en una serie; cambiar a la misma serie sobre otra nota las mueve todas, que es transportar. Pasar de una serie a conjunto de clases de alturas se queda con las tres primeras notas de la serie como conjunto, que son las que tocaban las teclas de armonía (ver Una tonalidad como conjunto: las mismas notas, otro nombre).

Estilos y demostraciones dodecafónicos

El grupo Series dodecafónicas de la Biblioteca tiene siete estilos:

estiloqué hace
Melodía de la seriela Serie de Lineup como melodía en corcheas, por P, R, I y RI
Coral de tríadasla serie Cuatro tríadas como acordes, sobre su retrogradación en el bajo: música dodecafónica que suena casi tonal
Pads de hexacordiosmedia serie como acorde, las otras seis notas desde I3 como melodía: cada compás tiene las doce notas
Puntillismonotas sueltas que saltan por tres octavas, con duraciones y dinámicas de la serie, como en Webern
Serialismo integralalturas, duraciones, dinámicas y octavas, todas de la serie de todos los intervalos, sobre su retrogradación de la inversión en acordes de dos notas
Lo-fi dodecafónicolos tricordios de la serie sobre un ritmo lo-fi, con su forma original en el bajo: una serie también puede tener groove
Los tricordios de Webernla serie del Concierto de Webern, con su melodía saltando por tres octavas sobre sus grupos de tres como acordes

Un estilo armónico es una progresión de acordes y una manera de tocarla. Un estilo de serie no tiene progresión, así que es una serie y una lista de voces, cada una diciendo cómo toca la serie un instrumento. Como el resto de la Biblioteca, son datos:

#![allow(unused)]
fn main() {
RowStyle {
    name: "Triad chorale",
    // ...
    row: "Four triads",
    tempo: 72.0,
    time: TimeSignature::COMMON,
    bars: 4,
    voices: &[
        voice(KEYS, RowForm::Prime)
            .chain(Chain::FourForms)
            .chords(3, 4)
            .from(Register::Nearest, 55),
        voice(BASS, RowForm::Retrograde)
            .step(8)
            .from(Register::Close, 36),
    ],
    rhythm: "",
},
}

voice hace una voz simple (una melodía en corcheas desde el Do central), y cada método cambia una cosa y devuelve la voz nueva:

#![allow(unused)]
fn main() {
impl RowVoice {
    const fn offset(self, offset: u8) -> Self {
        RowVoice { offset, ..self }
    }
    const fn chain(self, chain: Chain) -> Self {
        RowVoice { chain, ..self }
    }
    // ...
}
}

Tomar self por valor y devolver una copia cambiada con la sintaxis de actualización de structs (..self) deja encadenar las llamadas, como un builder. Y como son const fn, toda la tabla puede ser un static: pub static ROW_STYLES: [RowStyle; 7] se construye al compilar, sin código que corra al arrancar.

Aritmética de formas

Las demostraciones de Archivo > Demostraciones > Dodecafónico tocan cada estilo cuatro veces, como las secciones de una canción: P: la serie, R: al revés, I: dada vuelta y RI: las dos, y luego P una vez más para terminar. Con un ritmo, la batería entra después de la primera sección, con un fill antes de la última. Pero las voces de un estilo ya usan formas distintas: el bajo del Coral de tríadas toca R. ¿Qué toca el bajo en la sección R? R de R, que es la serie hacia adelante otra vez: P.

Las cuatro formas se comportan como dos interruptores, «dada vuelta» y «al revés», y combinar dos formas cambia cada interruptor una vez por cada forma que lo tiene encendido:

#![allow(unused)]
fn main() {
pub fn combine(section: RowForm, voice: RowForm) -> RowForm {
    let flags = |form| match form {
        RowForm::Prime => (false, false),
        RowForm::Inversion => (true, false),
        RowForm::Retrograde => (false, true),
        RowForm::RetrogradeInversion => (true, true),
    };
    let (a, b) = (flags(section), flags(voice));
    match (a.0 != b.0, a.1 != b.1) {
        (false, false) => RowForm::Prime,
        (true, false) => RowForm::Inversion,
        (false, true) => RowForm::Retrograde,
        (true, true) => RowForm::RetrogradeInversion,
    }
}
}

a.0 != b.0 sobre dos bool es un o exclusivo: verdadero cuando exactamente uno lo es. Los matemáticos llaman a este pequeño sistema el grupo de Klein: cada forma se deshace a sí misma (I de I es P), y el orden no importa. Un detalle más: la melodía de Pads de hexacordios toca I3, tres semitonos por encima de la primera nota de la serie. Dada vuelta, «tres por encima» pasa a ser «tres por debajo», así que en las secciones I y RI ese desplazamiento pasa a ser 12 − 3 = 9:

#![allow(unused)]
fn main() {
let offset = if inverted {
    (12 - self.offset % 12) % 12
} else {
    self.offset % 12
};
}

Tocar una canción entera como una serie

Como con los demás estilos, un estilo de serie se puede probar de dos maneras: parado, se escribe en un patrón propio; mientras suena la canción, se adueña de toda la canción. use_row_style recorre cada patrón que tiene acordes, una línea de bajo o una melodía, y escribe el estilo allí, leyendo los patrones por turno como P, R, I y RI para que la canción no se repita. La tonalidad pasa a ser la serie, empezando en la tónica de la canción, y los finales quedan como están.

Una regla decide qué pasa con las partes propias de la canción: el estilo se adueña de la armonía. Acordes tonales o un bajo tonal bajo una serie solo chocan, así que los canales de acordes y de bajo que el estilo no usa se callan en ese patrón, y los acordes del estilo van al canal que tocaba los acordes de la canción. La melodía se queda, salvo que el estilo tenga una propia:

#![allow(unused)]
fn main() {
if chords.is_some() || plays(project, bass) || melody {
    let before = project.pattern(pattern).cloned();
    let quiet: Vec<usize> = [(KEYS, chords), (BASS, bass)]
        .into_iter()
        .filter(|(name, _)| !uses(name))
        .filter_map(|(_, channel)| channel)
        .collect();
    // ... empty the `quiet` channels, write the voices ...
    if project.pattern(pattern) != before.as_ref() {
        written += 1;
        change.harmony += 1;
    }
}
}

Para saber si un patrón cambió de verdad, guarda una copia de antes (cloned() convierte el Option<&Pattern> en un Option<Pattern>) y compara al final. as_ref() vuelve a convertir la copia en un Option<&Pattern>, para que los dos lados de != tengan el mismo tipo. Esta función, take_over_harmony, recibe la escritura como un closure, así que los estilos de micromodos la usan también.

Una ventana que se abre delante

egui pone delante de las demás una ventana que se abre. Pero la ventana cuyo botón Serie… la abrió recibió un clic en el mismo fotograma, y un clic sube la ventana donde cae, así que las dos quieren estar arriba, y la nueva puede quedar detrás. Front mantiene delante una ventana nueva durante sus primeros fotogramas:

#![allow(unused)]
fn main() {
#[derive(Default)]
pub(crate) struct Front {
    /// The frame the window was last drawn in.
    drawn_in: u64,
    /// The frame it opened in.
    opened_in: u64,
}

impl Front {
    /// Called each frame the window `id` is drawn, before it is.
    pub(crate) fn keep(&mut self, ctx: &egui::Context, id: egui::Id) {
        let frame = ctx.cumulative_pass_nr();
        if self.drawn_in + 1 < frame {
            self.opened_in = frame;
        }
        self.drawn_in = frame;
        if frame - self.opened_in < 3 {
            ctx.move_to_top(egui::LayerId::new(egui::Order::Middle, id));
            ctx.request_repaint();
        }
    }
}
}

cumulative_pass_nr cuenta los fotogramas que egui ha dibujado. Una ventana que no se dibujó en el fotograma anterior acaba de abrirse, así que recuerda ese fotograma; durante sus tres primeros fotogramas sube su propia capa arriba de todo y pide otro fotograma, de modo que los tres llegan enseguida en vez de esperar a que se mueva el ratón. Para encontrar su capa, la ventana necesita su id, así que las ventanas Serie dodecafónica, Conjuntos de clases de alturas y Micromodos ponen cada una el suyo con .id(...), hecho con egui::Id::unique (unique porque nombra una sola cosa en todo el programa, como debe hacerlo el id de una ventana). La lista de reproducción hace lo mismo cuando un doble clic abre un clip en el piano roll (Doble clic en un clip para editarlo).

La ventana Serie dodecafónica es tan ancha como la matriz y se desplaza cuando la pantalla es baja, para que sus botones de abajo sigan a mano: .resizable([false, true]) la deja hacerse más alta o más baja pero no más ancha, y .constrain_to(area) la mantiene dentro de la ventana principal.

Las reglas de la escritura dodecafónica

La revisión de la conducción de voces marca lo que evita la escritura a varias voces clásica: quintas paralelas, una sensible que no sube, una séptima que no baja. Cada una de esas reglas tiene que ver con una tonalidad. En la música escrita con una serie no hay tónica, así que allí no significan nada. La música dodecafónica tiene reglas propias. Schoenberg escribía con ellas, y también sus alumnos Berg y Webern; los cursos de composición todavía las enseñan. Así que cuando la tonalidad es una serie dodecafónica, la casilla Conducción de voces revisa esas reglas en su lugar.

Lo que ves

  • Una línea Serie debajo de los nombres de los acordes. Nombra cada forma de la serie que la revisión encuentra en las notas, como P4 o P10, encima de las notas que la tocan. Pasa el puntero por encima para leer las notas de la forma: «P4: la serie que empieza en Mi: Mi Fa Sol Do# Fa# Mib Lab Re Si Do La Sib.»
  • La línea Voces, debajo, marca lo que evita la escritura dodecafónica: orden para una nota fuera del orden de la serie, rep. para una nota repetida antes de tiempo, hueco para una forma sin terminar, 8va para una nota duplicada a la octava, tonal para un acorde que suena a armonía tonal.
  • Contornos alrededor de las notas en cuestión. Dos notas duplicadas se unen con una línea, y también las notas de un acorde tonal. Un contorno punteado muestra la nota que la serie pedía en su lugar.
  • La línea de resumen, encima de las filas de acordes, lo resume todo: «Reglas dodecafónicas, P4, P10, a través de todas las pistas juntas: fuera del orden de la serie (2), duplicación a la octava (1), suena tonal (1).» Pasa el puntero por un paso con un hallazgo y la línea dice qué pasa allí; pasa el puntero por la línea para leer por qué.

Con un conjunto de clases de alturas como tonalidad, no se revisa nada. Un conjunto es una colección de notas sin orden y sin reglas propias, así que no hay nada contra qué revisar, y la línea de debajo de los acordes lo dice.

Las cinco reglas

La serie fija el orden. Una obra avanza por formas de la serie, cada una tocada nota tras nota, en su orden, para que todo lo que oyes venga de la serie. Notas vecinas en la serie pueden sonar juntas como acorde, en cualquier orden de abajo arriba, y una nota puede repetirse enseguida, o hacer un trino con la nota anterior: sigue siendo la misma nota de la serie. Dos errores se cuentan con cuidado. Si intercambias dos notas, o dejas una fuera, eso es una nota fuera de lugar, no todas las que vienen después: con Si tocado antes que Re en P4, la revisión marca el Si («Melodía toca Si4 donde P4 pide Re, su nota 8») y encuentra el resto de la serie en orden otra vez.

Ninguna nota vuelve antes de tiempo. Antes de que una nota vuelva, suenan las otras once. La serie se hizo para eso: ninguna nota se oye más que las demás, así que ninguna se vuelve tónica. Una nota que vuelve después de otras notas, antes de que la forma haya hecho sonar las doce, se marca con rep.

Una forma se toca entera. Cuando una forma se corta y empieza la siguiente, las notas que dejó fuera faltan entre las doce. La revisión marca la última nota de una forma así con hueco y nombra lo que nunca sonó: «Serie sin terminar: P4 se detiene después de 8 de sus 12 notas, con Melodía en Re5; Si, Do, La y Sib nunca suenan antes de que empiece la forma siguiente.»

Sin duplicaciones a la octava. Duplicar una nota a la octava le da peso. Como explicó Schoenberg, una nota con peso puede oírse como fundamental, o incluso como tónica. La revisión marca dos partes que tocan la misma nota a la vez, y también una parte que toca una nota mientras otra parte todavía la sostiene. Una parte que duplica a otra de principio a fin, una línea en octavas, se dice una sola vez, donde empieza.

Sin acordes de armonía tonal. Una tríada mayor o menor, o una séptima de dominante, le recuerda al oído la música tonal, y entonces el oído espera que la música siga como seguiría la música tonal. Schoenberg y Webern los evitaban. La revisión marca un momento en que todo lo que suena a la vez forma uno de esos acordes, y lo nombra. Berg los usaba a propósito: la serie de su Concierto para violín (1935) está hecha de tríadas, Sol menor, Re mayor, La menor y Mi mayor. Así que lee tonal como «esto suena a una tonalidad», no como «esto está mal».

Leer la serie como lo haría un analista

Nada en las notas dice qué forma de la serie suena, ni dónde termina una forma y empieza la siguiente. Un estudiante que analiza una partitura tiene que averiguarlo, y la revisión también. Corta las notas en grupos, notas que empiezan juntas, prueba luego todas las maneras de repartir los grupos entre formas de la serie, y se queda con la que tiene menos problemas: cada nota fuera de orden, cada nota repetida antes de tiempo y cada nota que una forma deja fuera cuenta uno. Algunas cosas acercan la lectura a lo que hacen de verdad los compositores:

  • Las formas pueden solaparse en la unión. Una forma puede empezar en la nota, o el acorde, en que terminó la forma anterior, de modo que las dos la comparten: una elisión. Si P4 termina con el acorde La Si♭ Do y P10 empieza en Si♭, P10 toma el Si♭ de ese acorde y sigue con Si.
  • Un acorde puede compartir notas con el anterior. Esas notas cuentan para la forma nueva si las necesita, o como notas repetidas si no.
  • Una serie, o una por pista. Todas las pistas pueden compartir una forma, como suelen hacerlo una melodía y los acordes que la acompañan, o cada pista puede tocar formas propias, como en un canon. La revisión lee la serie de las dos maneras, se queda con la lectura con menos problemas, y la línea de resumen dice cuál eligió.
  • Los patrones se enlazan. Cada patrón se revisa por separado, pero una serie puede seguir de un patrón al siguiente. Así, la primera forma puede haber empezado en el patrón anterior, siempre que sus notas de aquí estén en orden: la línea Serie dice entonces «…P4», y el resumen «P4 desde la nota 3». La última forma puede seguir en el patrón siguiente, así que nunca se marca como sin terminar.
  • Empates. Cuando dos lecturas tienen la misma cantidad de problemas, la revisión prefiere formas que empiezan en su primera nota. Si dos formas siguen empatadas, nombra la primera en el orden P, I, R, RI. Los acordes pueden hacer que dos formas no se puedan distinguir: la serie del Concierto de Webern son cuatro grupos de tres notas, y sus formas R0 e I1 tienen las mismas tres notas en cada grupo, en otro orden. En el estilo Los tricordios de Webern, la melodía y los acordes tocan los dos R0 después de P0. Las notas de la melodía llegan de a una, así que solo pueden ser R0; los acordes hacen sonar cada grupo de una vez, así que encajan igual de bien con R0 que con I1, y la revisión los nombra I1, que va primero. Cualquiera de los dos nombres es correcto.

La revisión sigue las reglas clásicas del método de Schoenberg. No juzga la música atonal libre, escrita sin ninguna serie (para eso elige un conjunto de clases de alturas como tonalidad); el serialismo más allá de las alturas, si las duraciones, las dinámicas o los registros siguen también una serie; ni la combinatoriedad: en el estilo Pads de hexacordios lee la forma propia de cada pista, lo cual es correcto, pero no dice que las dos mitades juntas forman las doce notas.

Cómo lee la serie la revisión

Los lugares de una forma en dieciséis bits

Walk recorre una forma de la serie, grupo por grupo, y recuerda cuáles de los doce lugares han sonado. Son doce respuestas de sí o no, así que las guarda como los bits de un u16, un bit por lugar:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy)]
struct Walk {
    /// One bit for each place in the form, from the first.
    used: u16,
    /// The place after the furthest one sounded.
    next: usize,
    errors: usize,
}

impl Walk {
    fn from(from: usize) -> Walk {
        Walk {
            used: (1 << from) - 1,
            next: from,
            errors: 0,
        }
    }

    fn heard(self) -> usize {
        self.used.count_ones() as usize
    }

    fn has(self, place: usize) -> bool {
        self.used & (1 << place) != 0
    }
    // ...
}
}

Walk::from(3) empieza un recorrido cuyos tres primeros lugares ya sonaron, en el patrón anterior o como las notas compartidas de una elisión: (1 << 3) - 1 es 0b111. Walk es Copy, dos palabras de máquina y un conjunto de bits, así que probar una forma no cuesta nada de preparar. count_ones cuenta los lugares oídos en una sola instrucción del procesador en la mayoría de las máquinas. Los conjuntos de clases de altura usan la misma idea, con doce bits para las doce notas.

¿Cuándo están en orden las notas nuevas?

Un grupo de n notas nuevas está en orden cuando llena los n lugares siguientes de la forma, en cualquier orden entre ellas. «Los lugares siguientes» puede querer decir dos cosas, y window prueba las dos:

#![allow(unused)]
fn main() {
fn open(self, places: std::ops::Range<usize>, count: usize) -> u16 {
    places
        .filter(|&place| !self.has(place))
        .take(count)
        .fold(0, |bits, place| bits | 1 << place)
}

fn window(self, taken: u16, count: usize) -> u16 {
    let first_open = self.open(0..12, count);
    let ahead = self.open(self.next..12, count);
    match (taken & ahead).count_ones() > (taken & first_open).count_ones() {
        true => ahead,
        false => first_open,
    }
}
}

open arma un conjunto de lugares con un fold que añade con OR un bit por lugar. first_open son los primeros lugares todavía abiertos, lo que recupera una nota saltada justo antes. ahead son los lugares después de la nota más lejana oída, lo que sigue después de un salto. Gana el que tiene más notas del grupo.

Toma P4 con Re y Si intercambiadas: Mi Fa Sol Do♯ Fa♯ Mi♭ La♭ Si Re Do La Si♭. En Si, la forma pide Re, así que Si está fuera de lugar. En Re, el primer lugar abierto es el de Re, así que Re está en orden, y también todo lo que sigue.

Ahora toma P4 sin el Re: Mi Fa Sol Do♯ Fa♯ Mi♭ La♭ Si Do La Si♭. Si está otra vez fuera de lugar. En Do, el primer lugar abierto sigue siendo el de Re, que nunca llega. Pero el lugar después de la nota más lejana oída, Si, es el de Do, así que Do está en orden, y también La y Si♭. Solo se marca Si. Con first_open solo, cada nota después del Re que falta estaría fuera de lugar.

Un recorrido, dos trabajos

take recibe un grupo y encuentra lo que está fuera de lugar:

#![allow(unused)]
fn main() {
fn take(&mut self, form: &Form, group: &Group, mut tell: impl FnMut(Misplaced)) {
    // ...
    for &played in &group.notes {
        let place = place_of(&played);
        let kind = if repeat(&played) {
            continue;
        } else if self.has(place) {
            SlipKind::EarlyRepeat
        } else if window & (1 << place) == 0 {
            SlipKind::RowOrder
        } else {
            continue;
        };
        self.errors += 1;
        tell(Misplaced {
            kind,
            played,
            place,
            wanted,
            heard: self.heard(),
        });
    }
    self.used |= fresh;
    if fresh != 0 {
        self.next = self.next.max(16 - fresh.leading_zeros() as usize);
    }
}
}

let kind = if ... { continue; } else ... funciona porque continue nunca da un valor: su tipo es !, «nunca», que encaja en lugar de cualquier tipo, así que las otras ramas deciden que kind es un SlipKind. Una nota que solo repite la nota recién tocada se salta de esta manera, y también una nota en su lugar. Al final, 16 - fresh.leading_zeros() es el lugar que sigue al bit más alto de fresh, el lugar más lejano al que llegó este grupo.

El mismo take cuenta los problemas mientras se comparan las lecturas, y los informa para la lectura que gana. Hace las dos cosas con el closure tell, al que llama por cada nota fuera de lugar. Al comparar, el closure no hace nada: walk.take(form, &groups[end - 1], |_| {}). Al informar, convierte cada nota en un hallazgo: walk.take(form, group, |misplaced| slips.push(misplaced.slip(form, key))). Ese segundo closure cambia slips, un vector que toma prestado, así que es un FnMut, no solo un Fn. Como take es genérica sobre el closure (impl FnMut), el compilador hace una copia de take para cada uno, y la vacía no cuesta nada.

La mejor lectura: programación dinámica

read tiene que elegir dónde empieza y termina cada forma, y qué forma es. best[end] es la menor cantidad de problemas en los primeros end grupos, y came_from[end] es el último tramo en el camino hasta ahí:

#![allow(unused)]
fn main() {
for start in 0..n {
    if best[start] == usize::MAX {
        continue;
    }
    // ...
    for from in 0..12 {
        for (index, form) in forms.iter().enumerate() {
            // ... may this form start at its place `from` here? ...
            let mut walk = Walk::from(from);
            for end in start + 1..=reach {
                walk.take(form, &groups[end - 1], |_| {});
                // ...
                let missing = if end == n { 0 } else { 12 - walk.heard() };
                let cost = best[start] + walk.errors + missing;
                if cost < best[end] {
                    best[end] = cost;
                    came_from[end] = Stretch {
                        start,
                        end,
                        form: index,
                        from,
                    };
                }
            }
        }
    }
}
}

Cada tramo se prueba una vez por forma. El recorrido sigue de un end al siguiente en vez de volver a empezar, así que una forma probada sobre veinte grupos cuesta veinte pasos, no doscientos. Una forma puede empezar en un lugar posterior (from mayor que 0) solo al principio de todo, traída del patrón anterior, o donde las notas anteriores son las que compartió el grupo anterior (una elisión). Las formas desde su primera nota se prueban primero, y solo un costo estrictamente menor reemplaza una lectura: así se llevan los empates. Recorrer came_from hacia atrás desde el último grupo da las formas en orden.

forms_of hace la lista de formas a probar, saltando una forma que tenga las mismas notas que otra ya incluida, así que el palíndromo de Webern se lee con 24 formas, no 48. Y read_rows corre read dos veces, una con todas las notas y otra por pista, y se queda con la más barata:

#![allow(unused)]
fn main() {
if apart.iter().map(|(_, reading)| reading.cost).sum::<usize>() < together.cost {
}

La misma idea, guardar la mejor manera de llegar a cada punto, reparte las notas de un acorde entre las partes en la revisión clásica (Conducción de voces y movimiento de las partes).

Notas que empiezan juntas: chunk_by

Los grupos salen de las notas ordenadas por comienzo, cortadas donde cambia el comienzo:

#![allow(unused)]
fn main() {
for chunk in played.chunk_by(|a, b| a.note.start_tick() == b.note.start_tick()) {
}

chunk_by entrega porciones de vecinas para las que el closure dice «mismo grupo», sin copiar nada.

Cómo se prueba

row.rs comprueba la teoría contra los libros: las cuatro formas de Schoenberg, nota por nota, contra las publicadas; cada una de las 48 formas de la Serie de Lineup, nombrada otra vez por label_of; cada fila de la matriz de Berg una forma P y cada columna una forma I, con la primera nota de la serie a lo largo de toda la diagonal; la serie de Webern un palíndromo (R9 es P3); la serie de Berg, la serie Todos los intervalos y cincuenta series al azar de todos los intervalos, de todos los intervalos; la Serie de Lineup combinatoria solo con I3; series leídas desde nombres, números y la forma compacta, y las equivocadas rechazadas con el mensaje correcto; la misma semilla dando la misma serie al azar; y una serie con una nota escrita dos veces rechazada al leerla de un archivo:

#![allow(unused)]
fn main() {
#[test]
fn webern_is_a_palindrome() {
    let webern = RowPreset::named("Webern's Symphony").unwrap().row;
    let r9 = webern.form(RowLabel::new(RowForm::Retrograde, 9));
    assert_eq!(webern.label_of(r9).unwrap().to_string(), "P3");
    let mirror = RowPreset::named("Mirror row").unwrap().row;
    let backwards = mirror.form(RowLabel::new(RowForm::Retrograde, 0));
    assert_eq!(mirror.label_of(backwards).unwrap().to_string(), "P6");
}
}

serial.rs escribe una melodía en corcheas, las cuatro formas por turno llenando el patrón, tricordios como acordes, las tres series, y comprueba que ninguna nota pasa del final del patrón. library/rows.rs comprueba que cada estilo usa una serie de las predefinidas y escribe sus voces en cualquier tonalidad; que Pads de hexacordios tiene las doce notas en cada compás; que una canción toma un estilo de serie, y que los acordes del estilo reemplazan a los de la canción en su propio canal en vez de sonar encima. Una prueba une las dos mitades de este capítulo: escribe cada estilo, sobre Do y sobre Fa♯, y le pasa la revisión de la conducción de voces, que tiene que leer cada uno como formas de su serie sin ninguna nota fuera de orden, así que los estilos y la revisión siguen las mismas reglas.

Las pruebas de las reglas en voice_leading/serial.rs escriben cada regla y sus excepciones como notas sobre la serie de Schoenberg: una serie en orden, acordes de notas vecinas, dos notas intercambiadas, una nota que falta, repeticiones y trinos, una elisión, una forma sin terminar, una melodía y unos acordes que comparten una forma, un canon, duplicaciones a la octava, acordes tonales, y las reglas clásicas apagadas en series y conjuntos. Sus nombres se leen como las reglas: two_notes_swapped_are_one_note_out_of_order, a_note_may_be_repeated_or_trilled_but_not_brought_back_later, one_form_may_end_on_the_note_the_next_begins_with.

Los ejercicios sobre series están en Ejercicios: la teoría musical en código.