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 traza de reproducción

Archivos: crates/lineup-audio/src/trace.rs, crates/lineup-audio/src/engine.rs, crates/lineup-app/src/app/trace.rs y crates/lineup-app/src/main.rs

La pantalla muestra el proyecto; no muestra lo que el reproductor hizo con él. Una canción pasada a otro compás puede verse bien, cada compás redibujado en 3/4, y aun así tocar sus compases al ritmo de antes, porque las notas se apretaron y el tempo se hizo más lento, y lo uno compensa lo otro. Solo el sonido dice la verdad. Pero escuchar es lento, y dice que algo está mal, no qué.

Una traza es una lista de cada nota que toca el reproductor, anotada por el hilo de audio en el momento en que la toca. No se calcula de nuevo a partir del proyecto: es lo que les llega a los altavoces, después del swing, los cambios de tempo, los desplazamientos y la soltura, con la canción desplegada como la toca la lista de reproducción y sin las pistas silenciadas. Así que una traza puede leerse en lugar de escucharse, por ti o por un test. Muchos de los tests del programa leen trazas, y también las lee quien comprueba que una demostración toca lo que dice su partitura.

Probarla

Desde una terminal, con un archivo de canción o el nombre de una demostración:

ferrous-lineup --trace song.lineup --out song.tsv
ferrous-lineup --trace "Twelve-bar blues"
ferrous-lineup --demos

--trace toca la canción como lo hace Archivo > Exportar WAV…, de principio a fin, pero sin ventana, sin tarjeta de sonido y sin archivo de sonido: obtienes las notas de la exportación, frame a frame, en uno o unos pocos segundos. --out las escribe en un archivo; sin él van a la terminal. --rate toca a otra frecuencia de muestreo que los 44.100 de la exportación. --demos lista los nombres que entiende --trace, que son los nombres en inglés de las demostraciones («Twelve-bar blues» para la que la aplicación en español llama «Blues de doce compases»). Las mayúsculas no importan, y basta con parte de un nombre cuando solo una demostración lo tiene.

En Windows, usa --out: el programa ya compilado es un programa de ventana y no tiene terminal en la que escribir.

En la aplicación, Edición > Ajustes… > Reproducción > Registro tiene una casilla, Escribir un registro mientras suena. Mientras está marcada, cada reproducción, de Reproducir a Detener, añade una tabla a playback-trace.tsv, en la carpeta donde la aplicación guarda sus ajustes; el ajuste muestra la ruta completa. Empieza sin marcar cada vez que se abre la aplicación, para que una traza olvidada nunca llene el disco. La casilla está desactivada cuando no hay ninguna tarjeta de sonido abierta, ya que entonces no suena nada. El manual sugiere adjuntar este archivo al informar de un problema con la reproducción.

Leer la tabla

Las líneas que empiezan con # dicen qué contiene la tabla, y después enumeran el tempo y el compás de cada parte de la canción. Estas son las primeras de Für Elise («Para Elisa» en la aplicación en español):

# Ferrous Lineup playback trace: Für Elise
# Every note the player started, in the order they start. Times are in seconds from the start of playback;
# steps are sixteenth notes of the loop played (the song, or the pattern in pattern mode), counted from 0.
# sample rate: 44100 Hz
# tempo: 0.0000 s, step 0, bar 1: 144.00 BPM
# tempo: 123.7500 s, step 1188, bar 100: 216.00 BPM
# tempo: 130.0000 s, step 1278, bar 105: 144.00 BPM
# tempo: 156.2500 s, step 1530, bar 126: 142.80 BPM
# tempo: 156.3551 s, step 1531, bar 126: 141.60 BPM
...
# time: step 0, bar 1: 3/4
# time: step 1188, bar 100: 9/8
# time: step 1278, bar 105: 3/4

El tempo cambia como está escrito en los compases 100 y 105, y después baja deslizándose desde el compás 126: un ritardando, que el reproductor toca paso a paso, cada uno un poco más lento, así que la traza tiene una línea por cada paso.

Después viene una línea por nota, en el orden en que empiezan, con un tabulador entre columnas. Estas son las primeras notas del bajo y del charles en la demostración Blues de doce compases, sin algunas columnas:

start   length  bar beat into_beat step   steps bpm    name pitch note velocity late_ms end
0.0000  0.4000  1   1    0.000     0.000  2.667 100.00 Bass 36    C2   104      0.0     gate
0.0000  0.1500  1   1    0.000     0.000  1.000 100.00 Hat  60    C4   96       0.0     gate
0.4000  0.0500  1   1    2.667     2.667  0.333 100.00 Hat  60    C4   96       100.0   gate
0.4000  0.2000  1   1    2.667     2.667  1.333 100.00 Bass 40    E2   90       100.0   gate
0.6000  0.4000  1   2    0.000     4.000  2.667 100.00 Bass 43    G2   96       0.0     gate
0.6000  0.1500  1   2    0.000     4.000  1.000 100.00 Hat  60    C4   96       0.0     gate

El swing está ahí mismo. A 100 BPM un pulso dura 0,6 segundos y tiene cuatro pasos. Escrita, la segunda corchea del pulso está en el paso 2, a la mitad. Tocada, llega 2,667 pasos dentro del pulso, 100 ms tarde: a dos tercios del camino, la tercera nota de un tresillo, que es lo que significa el swing completo. Y la nota del bajo anterior dura 2,667 pasos en lugar de 2, larga antes que corta. (Cómo el swing mueve las notas está en Swing.)

ColumnaQué dice
start, lengthCuándo empieza la nota y cuánto se sostiene, en segundos desde el inicio de la reproducción
bar, beatSu compás y su pulso, contados desde 1, en el compás vigente
into_beatCuántos pasos después del inicio de su pulso empieza
step, stepsEl paso en que empieza (contado desde 0, con la fracción con la que llega tarde) y cuántos pasos se sostiene
bpm, timeEl tempo de su paso y el compás de su compás
channel, name, program, instrumentSu fila en el rack de canales (desde 1), el nombre del canal, su programa General MIDI y qué lo toca
pitch, noteEl número de nota MIDI y su nombre, escrito en la tonalidad del momento
velocityCon qué fuerza se toca, de 1 a 127
late_msCuánto después de su paso empieza: su desplazamiento, el swing y la soltura
endPor qué terminó: gate cuando se acabó su duración, cut cuando se cortó antes (Detener, un salto, su pista silenciada, su clip quitado), again cuando la misma nota volvió a empezar en una Salida MIDI o una SoundFont, que tocan una de cada nota a la vez
patternSu patrón y el paso del patrón del que viene

Tablas así están hechas para grep y awk. Las notas del bajo del compás 12, o cada nota más fuerte que 110:

ferrous-lineup --trace song.lineup | awk -F'\t' '$11 == "Bass" && $3 == 12'
ferrous-lineup --trace song.lineup | awk -F'\t' '!/^#/ && $16+0 > 110'

Una hoja de cálculo también abre el archivo: dile que las columnas están separadas por tabuladores y que las líneas que empiezan con # son comentarios. La tabla está siempre en inglés y nombra las notas con letras, muestre lo que muestre la ventana, así que una traza se lee igual para todos.

Lo que una traza no dice

Una traza sigue las notas, no el sonido. Una muestra de batería suena hasta su final sea cual sea la duración de la nota, el release de un sintetizador sigue apagándose después de que la nota termina, y cuando todas las voces del Lineup Osc están ocupadas, la más vieja se toma para la nota nueva. Nada de eso está en la traza. Para un problema dentro de un instrumento, escuchar (o mirar las muestras) sigue siendo el camino.

Escribir en el hilo de audio

El hilo de audio tiene reglas estrictas: nunca debe esperar, nunca reservar memoria, nunca escribir en un archivo. Escribir una traza es justo lo que no puede hacer. Así que hace lo que ya hace con la Salida MIDI: entrega mensajes pequeños a un ring buffer, y otro hilo, que puede tomarse su tiempo, los lee.

Los mensajes son un enum Copy, así que meter uno copia unos pocos bytes y no reserva nada:

#![allow(unused)]
fn main() {
pub enum TraceEvent {
    /// Playback started on `step`: of the song, or of pattern `pattern`
    /// when `song` is false. After a count-in, when the count is over.
    Play {
        frame: u64,
        step: usize,
        song: bool,
        pattern: usize,
    },
    /// Playback jumped to `step` while playing.
    Jump { frame: u64, step: usize },
    /// Playback stopped.
    Stop { frame: u64 },
    /// A step started, at `tempo`.
    Step {
        frame: u64,
        step: usize,
        tempo: Tempo,
    },
    /// A note started on channel `channel` (its row in the channel rack),
    /// `late` frames after its step (nudge and loose timing), to be held
    /// for `gate` frames. `id` pairs it with its [`TraceEvent::NoteOff`].
    NoteOn {
        frame: u64,
        id: u32,
        channel: usize,
        origin: Origin,
        pitch: Note,
        velocity: u8,
        gate: u32,
        late: u32,
    },
    /// The note `id` ended, as `end` says.
    NoteOff { frame: u64, id: u32, end: End },
}
}

frame es el reloj propio del hilo de audio: los frames que ha tocado desde que arrancó. Cada nota recibe un id, para poder emparejar después su NoteOff con su NoteOn. Los ids cuentan hacia arriba con wrapping_add(1), que vuelve a 0 después de u32::MAX en lugar de entrar en pánico en una compilación de depuración; cuatro mil millones de notas después, las primeras ya terminaron hace mucho.

El hilo de audio guarda el trazador como un Option<Box<Tracer>>. Cuando la traza está apagada es None, y cada lugar que anota es un if let Some(trace) = &mut self.trace, que no cuesta casi nada. El trazador se crea en otro hilo, donde se puede reservar su memoria:

#![allow(unused)]
fn main() {
/// A tracer for the audio thread and the reader of what it writes.
pub fn trace_queue() -> (Tracer, TraceReader) {
    let (events, consumer) = rtrb::RingBuffer::new(TRACE_QUEUE_SIZE);
    let lost = Arc::new(AtomicUsize::new(0));
    let tracer = Tracer {
        events,
        lost: Arc::clone(&lost),
        open: Vec::with_capacity(OPEN_NOTES),
        next_id: 0,
        next_end: u64::MAX,
    };
    let reader = TraceReader {
        events: consumer,
        lost,
    };
    (tracer, reader)
}
}

y viaja al hilo de audio dentro de un comando, igual que un instrumento: Command::SetTrace(Some(Box::new(tracer))). La cola guarda 65.536 eventos (1 << 16), y la lista de notas que suenan se reserva para 4.096, así que nada crece en el hilo de audio. Un trazador viejo vuelve al hilo de la interfaz como basura, para liberarse allí.

Perdidos a propósito

Si el ring buffer está lleno, porque nadie lo leyó durante mucho tiempo, el evento no se mete y sube un contador de eventos perdidos:

#![allow(unused)]
fn main() {
fn push(&mut self, event: TraceEvent) {
    if self.events.push(event).is_err() {
        self.lost.fetch_add(1, Ordering::Relaxed);
    }
}
}

La cabecera de la tabla dice entonces cuántos eventos se perdieron, en lugar de que falten notas en silencio. Las colas que llevan la música hacen lo contrario: un note-off MIDI que encuentra su cola llena espera a que haya sitio, porque uno perdido dejaría una nota sonando para siempre en un sintetizador (ver Cuando la tarjeta de sonido desaparece). Una traza es un informe, no la música. Hacer esperar al hilo de audio por ella pondría el informe antes que el sonido.

Dónde empiezan las notas

Una nota de la canción empieza en uno de dos lugares: en start_pattern_notes, cuando llega su paso, o en start_pending, unos milisegundos después, para una nota desplazada o con soltura. Ambos se lo dicen al trazador en el frame en que la nota empieza de verdad, por eso una nota con swing muestra sus 100 ms:

#![allow(unused)]
fn main() {
if let Some(trace) = &mut self.trace {
    trace.note_on(
        self.now,
        note.channel,
        note.origin,
        note.pitch,
        note.velocity,
        note.gate,
        note.late,
        one_per_pitch(&channel.instrument),
    );
}
}

one_per_pitch dice si el instrumento es una Salida MIDI o una SoundFont, que tocan una de cada nota a la vez: en esos, una nota que vuelve a empezar termina la que suena, y la traza escribe again para ella.

Cuánto dura una nota

Cuando una nota empieza, su duración se convierte en un número de frames, el gate. Si el tempo cambia mientras suena, cada instrumento estira los gates de sus notas con la misma función, stretched, para que la nota siga terminando en su paso (ver Cada paso a su propio tempo). El trazador lleva su propia lista de las notas que suenan, open, y las estira con esa misma función:

#![allow(unused)]
fn main() {
pub(crate) fn tempo_changed(&mut self, now: u64, next_step: f64, stretch: f64) {
    for note in &mut self.open {
        if note.end != u64::MAX {
            let left = note.end.saturating_sub(now) as f64;
            note.end = now + stretched(left, next_step, stretch) as u64;
        }
    }
    self.find_next_end();
}
}

Una nota con el gate que nunca se acaba, HELD (u32::MAX), no tiene un final que el trazador pueda saber, y lo guarda como u64::MAX.

El trazador también se entera de todo lo que termina notas antes de tiempo: Detener, un salto, un silenciado, una canción enviada mientras suena que ya no tiene un clip, un canal quitado. El caso de la canción cambiada muestra un pequeño detalle de Rust. El hilo de audio comprueba cada nota que suena con un closure, still_played, que toma prestada la canción. Lo necesitan tanto los instrumentos como el trazador:

#![allow(unused)]
fn main() {
for channel in &mut self.channels {
    channel.instrument.release_unless(still_played);
}
if let Some(trace) = &mut self.trace {
    trace.cut(self.now, None, still_played);
}
}

still_played se pasa por valor, una vez por cada canal y una vez más. Eso funciona porque un closure que solo captura referencias compartidas es Copy, como las propias referencias: pasarlo entrega una copia y el original sigue usable.

Solo cuando una nota termina

Para encontrar las notas cuya duración se acabó, el trazador tendría que mirar cada una en cada frame, 44.100 veces por segundo. En cambio, recuerda el final más temprano, next_end, y no hace nada hasta que llega ese frame:

#![allow(unused)]
fn main() {
/// Ends the notes whose length is over by frame `now`. Called once
/// a frame, before anything else happens on it.
pub(crate) fn frame(&mut self, now: u64) {
    if now < self.next_end {
        return;
    }
    // The fields are borrowed one by one: `open` to go through, the
    // queue and the counter to write to.
    let events = &mut self.events;
    let lost = &self.lost;
    self.open.retain(|note| {
        if note.end > now {
            return true;
        }
        let event = TraceEvent::NoteOff {
            frame: note.end,
            id: note.id,
            end: End::Gate,
        };
        if events.push(event).is_err() {
            lost.fetch_add(1, Ordering::Relaxed);
        }
        false
    });
    self.find_next_end();
}
}

retain conserva los elementos para los que el closure devuelve true y suelta los demás, en una sola pasada y en el mismo sitio. El closure necesita meter eventos en la cola mientras retain tiene self.open prestado como mutable. Llamar a self.push dentro de él tomaría prestado todo self por segunda vez, y el compilador lo rechaza. Tomar prestados antes los otros dos campos en variables locales (events, lost) da tres préstamos de tres campos distintos, que no se solapan.

Convertir los eventos en notas

Trace::notes lee los eventos después, en un hilo normal. Los divide en reproducciones (runs) en cada Play, y guarda los eventos Step de cada una en una lista, en el orden en que se tocaron. Para saber dónde está un frame, busca el último paso que empezó antes, con partition_point:

#![allow(unused)]
fn main() {
fn position(&self, frame: u64, sample_rate: u32) -> Option<(f64, f64, Tempo)> {
    let index = self.steps.partition_point(|played| played.frame <= frame);
    let played = self.steps.get(index.checked_sub(1)?)?;
    let length = match self.steps.get(index) {
        Some(next) => (next.frame - played.frame) as f64,
        None => frames_per_step(played.tempo, sample_rate),
    };
    let fraction = (frame - played.frame) as f64 / length.max(1.0);
    Some((
        played.step as f64 + fraction,
        played.count as f64 + fraction,
        played.tempo,
    ))
}
}

partition_point es una búsqueda binaria: dada una lista ordenada y una prueba que es verdadera para una primera parte y falsa para el resto, devuelve dónde empieza la parte falsa. Aquí es el primer paso que empezó después de frame; el anterior es el paso que se está tocando. La fracción es cuánto ha avanzado frame entre ese paso y el siguiente. index.checked_sub(1)? devuelve None para un frame anterior al primer paso, donde index - 1 daría la vuelta por debajo de cero.

Cada paso tiene además un count de los pasos tocados antes que él, que sigue creciendo cuando un patrón vuelve a empezar o el cursor salta. La duración de una nota en pasos es la diferencia de esos contadores: se mide a lo largo de los pasos tocados de verdad, sean cuales sean los números que les da el bucle. Una nota sostenida a través del final de un bucle de un compás, del paso 14 al paso 2 de la vuelta siguiente, dura 4 pasos, no -12.

Un NoteOff encuentra su NoteOn por id en una pequeña lista de las notas aún abiertas en la reproducción. swap_remove lo saca moviendo el último elemento a su lugar, lo que es rápido y aquí está bien, ya que el orden de esa lista no importa.

Escribir la tabla

write_table escribe la cabecera y una línea por nota. Los nombres vienen de Labels: el título de la canción, el proyecto (para los nombres de los canales y patrones), los compases y las tonalidades, todo en los pasos del bucle que se tocó:

#![allow(unused)]
fn main() {
/// For a trace of song mode playing `arrangement`.
pub fn song(title: &'a str, project: &'a Project, arrangement: &Arrangement) -> Self {
    Labels {
        title,
        project,
        meter: project.played_meter(arrangement),
        keys: project.played_keys(arrangement),
    }
}
}

played_meter y played_keys despliegan los compases y las tonalidades igual que el arreglo despliega los clips, así que una sección en 3/4 que suena dos veces está en 3/4 las dos veces. Cada nota se escribe en la tonalidad de su paso: un Fa♯ en Sol mayor, un Sol♭ en Re♭ mayor (en la tabla, siempre con letras: F♯, G♭).

El nombre de un canal o de un patrón podría contener un tabulador o un salto de línea, que partirían la tabla. cell los cambia por espacios:

#![allow(unused)]
fn main() {
/// Text for a table cell: no tabs or line breaks, which would split it.
fn cell(text: &str) -> String {
    text.replace(['\t', '\n', '\r'], " ")
}
}

str::replace recibe un patrón, y un array de caracteres lo es: coincide con cualquiera de ellos.

Una nota que seguía sonando cuando terminó la traza no tiene duración; la tabla escribe ? para ella, y la cabecera dice cuántas hay.

Una traza mientras tocas

La traza del ajuste es un LiveTrace, que guarda la aplicación. Una vez por frame, check_trace mueve a él lo que entregó el hilo de audio. Cada reproducción se escribe como una tabla propia cuando termina, al Detener o cuando empieza otra (cambiar entre el modo canción y el modo patrón mientras suena empieza otra). Trace::take_finished saca las reproducciones terminadas y deja la que sigue en marcha.

Una tabla necesita los nombres del proyecto, pero puedes renombrar un canal mientras suena una canción. Así que en cada Play, check_trace guarda una copia del proyecto tal como estaba, y la tabla se escribe con los nombres que tenía esa reproducción:

#![allow(unused)]
fn main() {
for event in &live.trace.events[before..] {
    if let TraceEvent::Play { song, pattern, .. } = *event {
        live.playing.push_back(Playing {
            title: self
                .file
                .song_name()
                .unwrap_or_else(|| "Untitled".to_string()),
            project: self.project.clone(),
            song,
            pattern,
        });
    }
}
}

Un VecDeque es una cola que puede añadir por un extremo y sacar por el otro: las copias entran por detrás a medida que empiezan las reproducciones, y salen por delante a medida que se escriben sus tablas.

La ventana muestra los nombres de las notas en letras o en solfeo y las palabras en inglés o en español, pero la tabla no debe cambiar con ellos. Así que write fija ambos mientras escribe, con las funciones with de los ajustes por hilo:

#![allow(unused)]
fn main() {
let mut table = Vec::new();
lineup_core::i18n::Language::English.with(|| {
    lineup_core::NoteNaming::Letters.with(|| write_table(&mut table, trace, &labels))
})?;
}

Desmarcar la casilla, o cerrar la aplicación, suelta el LiveTrace. Su Drop escribe la reproducción que sigue en marcha, para que no se pierdan las últimas notas. Si la tarjeta de sonido desaparece y vuelve, resume_trace escribe lo que dejó el hilo de audio viejo y le da un trazador nuevo al nuevo, y la traza sigue en el mismo archivo.

La línea de comandos

main mira su primer argumento antes de abrir una ventana (ver main.rs). Después de --trace, un pequeño bucle lee el resto:

#![allow(unused)]
fn main() {
while let Some(arg) = args.next() {
    match arg.to_str() {
        Some("--out" | "-o") => out = args.next().map(PathBuf::from),
        Some("--rate") => match args.next().and_then(|hz| hz.to_str()?.parse().ok()) {
            Some(hz) if (8_000..=192_000).contains(&hz) => rate = hz,
            _ => {
                eprintln!("--rate takes a sample rate in Hz, such as 44100");
                return 2;
            }
        },
        _ if source.is_none() => source = Some(arg),
        _ => {
            eprintln!("{USAGE}");
            return 2;
        }
    }
}
}

args es un iterador, y el bucle le saca elementos de dos maneras: uno por vuelta con while let, y uno más con args.next() dentro del cuerpo, para el valor de una opción. Los brazos muestran tres tipos de patrón: "--out" | "-o" coincide con cualquiera de los dos textos, Some(hz) if ... añade una guarda, y _ if source.is_none() coincide con cualquier cosa, pero solo mientras no se haya dado una canción. La función devuelve el código de salida con el que termina el programa: 0 cuando se escribió la traza, 1 cuando no se pudo, 2 por un error en los argumentos. Los scripts lo comprueban.

trace_table construye la aplicación entera sin ventana ni tarjeta de sonido, como hacen los tests (Audio::Unavailable), abre el archivo o la demostración, y pide el mismo renderizado de la canción que hace la exportación WAV, con una traza activada:

#![allow(unused)]
fn main() {
pub(super) fn trace_song(&mut self, sample_rate: u32) -> Trace {
    let mut render = self.song_render_traced(sample_rate);
    render.renderer.skip(render.frames);
    render.renderer.take_trace().unwrap_or_default()
}
}

skip toca todos los frames y tira el sonido. La traza es la de la exportación, porque es el mismo camino de código (ver Exportar).

El nombre de una demostración se busca en tres intentos: exacto, después sin mirar las mayúsculas, después como parte de un nombre. El último intento puede encontrar ninguna, una o varias, y un patrón de slice dice cuál es el caso:

#![allow(unused)]
fn main() {
match matching.as_slice() {
    [demo] => Ok(*demo),
    [] => Err(format!(
        "There is no file or demo called \"{name}\" (--demos lists the demos)."
    )),
    several => {
        let names: Vec<&str> = several.iter().map(|demo| demo.name()).collect();
        Err(format!("\"{name}\" could be any of: {}.", names.join(", ")))
    }
}
}

Tests que leen en lugar de escuchar

Los tests usan las trazas como lo haría una persona. En lineup-audio, sobre un renderer pequeño:

  • notes_start_on_their_step_and_last_their_length, y a_nudged_note_starts_late_by_its_nudge.
  • a_slower_tempo_stretches_a_held_note: una nota que suena a través de un cambio a la mitad del tempo dura sus pasos, en más segundos.
  • stopping_and_muting_cut_the_notes_sounding, y a_midi_out_ends_a_note_that_starts_again.
  • a_looping_pattern_counts_its_steps_on: las duraciones se miden a lo largo de los pasos tocados, a través del bucle.

Y en la aplicación, sobre canciones enteras:

  • the_trace_plays_every_note_of_the_export: cada nota que tiene el proyecto, clip a clip, está en la traza exactamente una vez, y todas terminan cuando se acaba su duración.
  • swing_plays_the_off_beats_late: con swing, las únicas notas que empiezan tarde son las segundas corcheas de cada pulso.
  • a_tempo_marker_changes_the_seconds_not_the_steps: una marca de tempo en el paso 16 cambia cuándo empiezan las notas que vienen después, en segundos, pero no sus pasos.
  • re_metering_keeps_each_bar_as_long_in_seconds: una canción pasada de 4/4 a 3/4 empieza cada compás en el mismo segundo que antes. Es el error del principio de este capítulo, el que solo el sonido podía mostrar, escrito como un test.
  • a_trace_while_playing_writes_a_table_for_each_playback.

Los tests de otros capítulos también usan trazas, como el que lleva El Choclo por siete estilos y comprueba que cada nota de la melodía sigue empezando y durando como antes (Tempo, compás, swing y sensación).

Los ejercicios sobre trazas están en Ejercicios: el sonido.