Portada del disco de DALL·E 3
En este artículo, compartiré cómo hice un proyecto durante un fin de semana para lanzar mi álbum ( https://evhaevla.netlify.app/ ). No soy un músico ni un compositor capacitado, pero a veces me vienen a la mente melodías. Los anoto y luego dejo que la computadora los reproduzca.
En 2021, lancé mi álbum titulado "Todos están felices, todos están riendo". Es un álbum sencillo de un "compositor" desconocido: ese soy yo.
No sólo me gusta la música; También soy desarrollador y recientemente me centré principalmente en el trabajo frontend. Pensé, ¿por qué no combinar estos dos amores? Entonces, me propuse diseñar un sitio web para presentar visualmente mi álbum.
Este artículo no profundizará en todos los detalles técnicos; sería demasiado extenso y podría no ser del agrado de todos. En lugar de ello, destacaré los conceptos centrales y los obstáculos que encontré. Para aquellos interesados, todo el código se puede encontrar en GitHub .
Mi álbum está compuesto para piano, por lo que la decisión es sencilla. Imagínese rectángulos descendiendo sobre las teclas del piano. Cualquiera que tenga inclinaciones musicales probablemente haya encontrado numerosos vídeos en YouTube que muestran notas de esta manera. Un rectángulo toca una tecla, iluminándola, indicando el momento preciso para tocar la nota.
No estoy seguro del origen de este estilo visual, pero una búsqueda rápida en Google arroja predominantemente capturas de pantalla de Synthesia.
En YouTube hay creadores que logran producir efectos visualmente impresionantes. Ver vídeos de este tipo es un placer, tanto desde el punto de vista estético como musical. Mira esto o esto .
¿Qué necesitaremos implementar?
Abordemos cada punto y pongámoslos todos en acción.
Al principio supuse que implementar las claves plantearía el mayor desafío. Sin embargo, una búsqueda rápida en línea reveló una gran cantidad de ejemplos y guías sobre cómo hacer precisamente eso. Buscando un diseño con un toque de elegancia, opté por un ejemplo creado por Philip Zastrow .
Todo lo que me quedaba por hacer era replicar las teclas varias veces y establecer una cuadrícula para que las notas se deslizaran. Utilicé Vue.js como marco de interfaz y a continuación se muestra el código del componente.
<template> <ul style="transform: translate3d(0, 0, 0)"> <li :id="`key_${OFFSET - 1}`" style="display: none"></li> <template v-for="key in keys" :key="key.number"> <li :class="`${key.color} ${key.name}`" :id="`key_${key.number}`"></li> </template> </ul> </template> <script setup lang="ts"> import { ref } from 'vue' import type { Key } from '@/components/types' const OFFSET = 24 const template = [ { color: 'white', name: 'c' // Do }, { color: 'black', name: 'cs' // Do-diez }, { color: 'white', name: 'd' // Re }, /* ... */ ] const keys = ref<Key[]>([]) for (let i = 0; i < 72; i++) { keys.value.push({ ...template[i % 12], number: i + OFFSET }) } </script>
Me gustaría mencionar que agregué un atributo id
a cada clave, que será esencial al iniciar sus animaciones.
Si bien este puede parecer el segmento más simple, algunos desafíos se esconden a simple vista.
¿Cómo se puede lograr el efecto de notas descendentes?
¿Es necesario mantener una estructura que pueda consultarse para recuperar las notas actuales?
¿Cuál es el mejor enfoque para representar los resultados de tales consultas?
Cada pregunta presenta un obstáculo que hay que sortear para lograr el efecto deseado sin problemas.
No me detendré en cada pregunta, sino que iré directo al grano. Dados los innumerables desafíos asociados con el enfoque dinámico, es aconsejable prestar atención a la navaja de Occam y optar por una solución estática.
Así es como lo abordé: rendericé las 6215 notas simultáneamente en un único lienzo expansivo. Este lienzo está alojado dentro de un contenedor con el estilo overflow: hidden
. Lograr el efecto de notas que caen es simplemente una cuestión de animar la scrollTop
de este contenedor.
Sin embargo, queda una pregunta pendiente: ¿cómo obtengo las coordenadas de cada nota?
Afortunadamente, tengo un archivo MIDI donde se archivan todas estas notas, una comodidad que me brinda ser el compositor del álbum. Todo se reduce a renderizar las notas utilizando los datos extraídos del archivo MIDI.
Dado que el archivo MIDI está en formato binario y no tenía intención de analizarlo yo mismo, solicité la ayuda de la biblioteca de archivos midi .
La biblioteca midi-file
es eficaz para extraer datos sin procesar de archivos MIDI, pero para mis necesidades, eso no es suficiente. Mi objetivo es transformar estos datos en un formato más accesible y fácil de usar para facilitar la representación perfecta dentro de la aplicación.
En un archivo MIDI, no estoy tratando con notas en el sentido habitual, sino con eventos. Hay una gran variedad de estos eventos, pero me centro principalmente en dos tipos: 'noteOn', que se activa cuando se presiona una tecla, y 'noteOff', cuando se suelta la tecla.
Tanto los eventos 'noteOn' como 'noteOff' especifican el número de nota particular que se presionó o soltó, respectivamente. El tiempo, en el sentido convencional, está ausente en MIDI. En cambio, tenemos "garrapatas". El número de ticks por tiempo se detalla en el encabezado del archivo MIDI.
De hecho, hay más que considerar. También está presente una pista de tempo que contiene eventos 'setTempo' que son parte integral del proceso, considerando que el tempo puede cambiar durante la reproducción. Mi enfoque inicial implicó ajustar la velocidad de animación de la propiedad scrollTop
del contenedor para alinearla con el tempo.
Sin embargo, pronto me di cuenta de que esto no produciría el resultado esperado debido a la acumulación excesiva de errores. Una extensión de tiempo "lineal" resultó más efectiva para animar el scrollTop
.
Incluso con el aspecto de la animación ordenado, el tempo aún requería incorporación. Resolví esto ajustando las longitudes de los rectángulos de las notas. Si bien no es la solución óptima (lo ideal sería manipular la velocidad), este método aseguró un funcionamiento más fluido.
Esta solución no es perfecta, principalmente porque asocio un evento de tempo con un evento de nota en función de si tienen el mismo o menos tiempo. Esto significa que si se produce otro evento de tempo mientras una nota todavía está sonando, simplemente se ignorará.
Potencialmente, esto podría introducir un error, especialmente si una nota es muy larga y se produce un cambio de tempo dramático durante su tiempo de reproducción. Es una compensación. He aceptado este pequeño defecto porque estoy concentrado en un desarrollo rápido.
Hay casos en los que la velocidad prima y es más pragmático no enredarse en cada detalle.
Entonces, estamos equipados con la siguiente información:
Con estos detalles a mano, puedo señalar las coordenadas exactas de cada nota en el lienzo. El número de tecla determina el eje X, mientras que el comienzo de la pulsación de tecla es el eje Y. La longitud de la prensa dicta la altura del rectángulo.
Al utilizar un elemento div estándar y establecer su posición en "absoluta", logré con éxito el efecto deseado.
No tenía intención de crear un sintetizador para piano, ya que me habría llevado mucho tiempo. En su lugar, utilicé un archivo OGG existente que ya había sido "renderizado" y seleccioné The Grandeur de Native Instruments para la biblioteca de sonidos.
Personalmente, creo que es el mejor instrumento de piano VST disponible.
Incrusté el archivo OGG resultante en un elemento de audio estándar. Mi tarea principal entonces era sincronizar el audio con la animación scrollTop
de mi lienzo de notas.
Antes de poder abordar la sincronización, primero había que establecer la animación. La animación del lienzo es bastante sencilla: animo el scrollTop
desde un valor infinito hasta cero, usando interpolación lineal. La duración de esta animación coincide con la duración del álbum.
Cuando una nota desciende sobre una tecla, esa tecla se ilumina. Esto significa que para el descenso de cada nota, necesito "activar" la tecla correspondiente, y una vez que la nota complete su recorrido, desactivarla.
Con un total de 6215 notas, esto equivale a la friolera de 12,430 animaciones de activación y desactivación de notas.
Además, mi objetivo era brindar a los usuarios la capacidad de rebobinar el audio, permitiéndoles navegar a cualquier lugar dentro del álbum. Para implementar una característica de este tipo, es esencial una solución sólida.
Y cuando me enfrento a la necesidad de una solución confiable que "simplemente funcione", mi opción es siempre la plataforma de animación GreenSock .
Mira la cantidad de código que se necesita para crear todas las animaciones para cada una de las claves. Usar id
para animar componentes no es la mejor práctica para aplicaciones de una sola página. Sin embargo, este método supone un verdadero ahorro de tiempo. ¿Recuerda la id
que mencioné para cada clave? Aquí es donde entran en juego.
const keysTl = gsap.timeline() notes.value.forEach((note) => { const keySelector = `#note_${note.noteNumber}` keysTl .set(keySelector, KEY_ACTIVE_STATE, note.positionSeconds) .set(keySelector, KEY_INACTIVE_STATE, note.positionSeconds + note.durationSeconds - 0.02) })
Básicamente, el código de sincronización establece una conexión a través de eventos entre el audio y la línea de tiempo global de GSAP.
audioRef.value?.addEventListener('timeupdate', () => { const time = audioRef.value?.currentTime ?? 0 globalTl.time(time) }) audioRef.value?.addEventListener('play', () => { globalTl.play() }) audioRef.value?.addEventListener('playing', () => { globalTl.play() }) audioRef.value?.addEventListener('waiting', () => { globalTl.pause() }) audioRef.value?.addEventListener('pause', () => { globalTl.pause() })
Justo cuando tenía ganas de terminar, me vino a la mente una idea intrigante. ¿Qué pasaría si le añadiera un toque único al álbum? Originalmente no estaba en mi lista de tareas pendientes, pero sentí que el proyecto no brillaría realmente sin esta característica. Entonces, decidí incorporarlo también.
Cada vez que me sumerjo en una canción, me encuentro reflexionando sobre sus significados más profundos. ¿Qué mensaje intentaba transmitir el compositor? Consideremos, por ejemplo, un segmento del "Nightbook" de Ludovico Einaudi. El piano resuena en el oído izquierdo, mientras que las cuerdas resuenan en el derecho.
Crea un ambiente de diálogo que se desarrolla entre los dos. Se siente como si las teclas del piano estuvieran sondeando: "¿Estás de acuerdo?" Las cuerdas responden afirmativamente. "¿Es esa la pregunta?" Las cuerdas hacen eco de su afirmación. La secuencia culmina con ambos instrumentos convergiendo, simbolizando la realización de la unidad y la armonía. ¿No es una experiencia fascinante?
Es imperativo mencionar que esta es puramente mi interpretación personal. Una vez tuve la oportunidad de asistir a un concierto de Ludovico en Milán. Después de la actuación, me acerqué a él y le pregunté si realmente había tenido la intención de incorporar la noción de diálogo en ese segmento en particular.
Su respuesta fue esclarecedora: "Nunca lo pensé de esa manera, pero ciertamente posees una imaginación vívida".
A partir de esa experiencia, reflexioné: ¿y si integro subtítulos en la partitura? A medida que se reproducen segmentos específicos, los comentarios podrían materializarse en la pantalla, proporcionando ideas o interpretaciones sobre la intención del compositor.
Esta característica podría ofrecer a los oyentes una comprensión más profunda o una nueva perspectiva sobre "¿qué quiso decir realmente el autor?"
Fue una suerte elegir GSAP como mi herramienta de animación. Me permitió integrar sin esfuerzo otra línea de tiempo, específicamente encargada de animar el comentario. Esta adición simplificó el proceso e hizo que la implementación de mi idea fuera mucho más sencilla.
Me sentí inclinado a introducir los comentarios mediante marcado HTML. Para lograr esto, creé un componente que presenta la animación durante el evento onMounted
.
<template> <div :class="$style.comment" ref="commentRef"> <slot></slot> </div> </template> <script setup lang="ts"> /* ... */ onMounted(() => { if (!commentRef.value) return props.timeline .fromTo( commentRef.value, { autoAlpha: 0 }, { autoAlpha: 1, duration: 0.5 }, props.time ? parseTime(props.time) : props.delay ? `+=${props.delay}` : '+=1' ) .to( commentRef.value, { autoAlpha: 0, duration: 0.5 }, props.time ? parseTime(props.time) + props.duration : `+=${props.duration}` ) }) </script>
El uso de este componente sería el siguiente.
<template> <div> <Comment time="0:01" :duration="5" :timeline="commentsTl"> <h1>A title for a track</h1> </Comment> <Comment :delay="1" :duration="13" :timeline="commentsTl"> I would like to say... </Comment>
Con todos los elementos en su lugar, el siguiente paso fue alojar el sitio. Opté por Netlify. Ahora los invito a experimentar el álbum y ver la presentación final.
Realmente espero que haya otros desarrolladores amantes del piano y deseosos de mostrar sus álbumes de una manera tan única. Si eres uno de ellos, no dudes en bifurcar el proyecto.