tu propia melodía para una caja de música / Sudo Null IT News

Una de mis cosas favoritas de la programación es cuando puedo utilizar mis habilidades no sólo en mi trabajo diario, sino también en algún área temática nueva e inesperada, facilitando el trabajo de otra persona y resolviendo problemas que no se han automatizado antes. La mayoría de las veces, mi esposa se convierte en la fuente de inspiración en estos asuntos: una de las veces anteriores tuve que escribir mucho código cuando estábamos escribiendo una tesis sobre hidrología juntos (y todavía no he podido escribir un artículo sobre it), pero esta vez la tarea era hacer una caja de música con una melodía única.

Esta vez escribí un script que automatiza la creación de un modelo para imprimir en 3D una caja de música directamente desde un archivo MIDI con una melodía.

Mecanismo de caja de música con melodía personalizada.

Mecanismo de caja de música con melodía personalizada.

El mecanismo de una caja de música es simple: consta de un mecanismo de cuerda con resorte, un peine con dientes sintonizados a ciertas frecuencias de sonido y un tambor, que es un cilindro del que sobresalen “pasadores” que, cuando el tambor gira, tiran los dientes del peine.

Empecé buscando información en Internet: quería comprobar qué tan factible es la tarea de crear tu propio tambor mediante impresión 3D (teniendo en cuenta las características de los materiales de impresión tradicionales). En los comentarios de Reddit logramos encontrar no solo historias de éxito, sino también un script listo para usar para OpenSCAD que genera un modelo de tambor. Sin embargo, todavía era necesario finalizarlo porque sus capacidades eran limitadas y, posteriormente, la mayor parte fue reescrita.

Asignación de tareas

Teniendo un archivo MIDI con partituras para una caja de música, necesitamos tener listo un archivo STL para usarlo como modelo para impresión 3D. Descompongamos un poco el problema:

  1. Análisis de un archivo MIDI: debe tener en cuenta las limitaciones físicas de la caja de música y convertir el archivo MIDI de la forma más sencilla posible en un conjunto de notas, divididas en intervalos de tiempo en los que deben “tocarse”.

  2. Comparación de notas y los números correspondientes de los “dientes” del peine.

  3. Creando un modelo de tambor junto con pines ubicados en los lugares correctos.

Analizando un archivo MIDI

En realidad, esta es la parte más fácil. Usé Python y la biblioteca mido (ya que mi idioma principal es Python).

El formato de archivo MIDI determina la posibilidad de que existan varios pistasque a su vez constan de mensajes. Los mensajes pueden tener tipo, nota y tiempo, el cual debe pasar del mensaje anterior a la activación de éste. Además, hay metamensajes que establecen varias configuraciones del instrumento musical (como el tempo).

Como sólo tenemos un instrumento (nuestra caja de música) y este instrumento no “sabe” tocar notas de diferente duración o cambiar el tempo del juego, se siguen una serie de simplificaciones:

  1. Todas las pistas se pueden procesar secuencialmente, ignorando mensajesque seleccionan un instrumento, establecen el tempo de reproducción, etc.

  2. Puede ignorar diferentes duraciones de sonido y tomar como base, por ejemplo, la duración de la primera nota del archivo como un “cuanto de tiempo”.

Elegí una matriz bidimensional (o lista de listas en Python) como mi estructura de datos de destino. Entonces, la primera lista es un conjunto de momentos en el tiempo en los que se deben tocar las notas. Las listas que contiene son en realidad las notas tocadas en un punto determinado. Ejemplo:

music_score = (
  (),             # Ничего не играть в первый момент времени
  (),             # И во второй момент
  ('C#4', 'A6'),  # Одновременно сыграть C4 в 4-й октаве и A в 6-й.
  ('G5'),         # Затем сразу G в 5-й.
)

En realidad, el código para dicha conversión se ve así:

...
score = (())
for track in midi_file.tracks:
    for message in track:
        if message.type not in ('note_on', 'note_off'):  # Нас интересуют только ноты
            continue
        elapsed = message.time // time_quant
        score.extend(() for _ in range(elapsed))  # Добавляем промежутки, если с момента последнего сообщения должно пройти время
        if message.type == 'note_on':
            current_frame = score(-1)
            note = number_to_note(message.note)
            current_frame.append(note)

Aquí puedes ver la llamada a la función. number_to_notepero no es ciencia espacial: la especificación MIDI define los números de notas, dado que el formato es binario, los convertimos en texto para mayor comodidad:

NOTES = ('C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B')
OCTAVES = list(range(11))
NOTES_IN_OCTAVE = len(NOTES)

def number_to_note(number: int) -> str:
    octave = number // NOTES_IN_OCTAVE - 1
    assert octave in OCTAVES, 'Invalid octave number'
    assert 0 <= number <= 127, 'Invalid MIDI note number'
    note = NOTES(number % NOTES_IN_OCTAVE)

    return f'{note}{octave}'

Notas a juego con el peine de la caja.

Parecería que C es 1, C# es 2, D es 3 y así sucesivamente… Pero no, hay un par de puntos interesantes.

En primer lugar, estos mecanismos chinos, que han inundado todos los mercados y no tienen análogos accesibles, tienen una serie de notas impredecibles. Tenemos que desmontar el mecanismo, coger un papel, un bolígrafo y un teléfono con una aplicación de sintonizador para poder escribir las notas que realmente tenemos a nuestra disposición. Y luego añadir una lista al script y, en un solo paso, comprobar que tenemos todas las notas del archivo MIDI “disponibles”.

En segundo lugar, el mecanismo tiene una limitación: en algunas melodías, la misma nota puede estar demasiado cerca en una fila, y luego el diente del peine se clava en la siguiente clavija del tambor y suena desagradablemente. Tenemos que cambiar la melodía para adaptarnos a esta limitación. Sin embargo, hay un lado positivo: un conjunto de notas impredecibles puede tener repeticiones, es decir, una nota se puede tocar con varios dientes de peine diferentes. Esto nos permite eludir parcialmente la limitación si cada vez que nos encontramos con una nota de este tipo utilizamos un diente diferente al que utilizamos la última vez. Para hacer esto, se escribió el siguiente código, que se utiliza para hacer coincidir cada nota siguiente con el número de diente:

class NoteNumberGetter:
    def __init__(self, available_notes: list(str)) -> None:
        self._notes_mapping = {
            note: (_indices(available_notes, note, start=1), -1)  # функция _indices возвращает все индексы, по которым в списке найден указанный элемент.
            for note in available_notes
        }

    def get_note_number(self, note: str) -> int:
        note_numbers, last_used_index = self._notes_mapping(note)
        last_used_index = (last_used_index + 1) % len(note_numbers)
        note_number = note_numbers(last_used_index)
        self._notes_mapping(note) = (note_numbers, last_used_index)
        return note_number

Generación de modelos

Para crear modelos 3D paramétricos, cuya construcción se basa en un algoritmo que acepta datos de entrada arbitrarios, quizás exista una solución popular y probada: OpenSCAD. Afortunadamente, para un comienzo rápido, obtuve un script listo para usar que se puede tomar como base, pero aún así tuvo que reescribirse y refactorizarse significativamente, teniendo en cuenta el hecho de que puedo preparar los datos automáticamente y no escribirlos. se realizaba manualmente y el guión original no permitía tocar dos notas en el mismo momento. En base a esto, vale la pena considerar todo el proceso de escritura paso a paso.

Podemos dividir condicionalmente la construcción de un modelo de tambor para una caja de música en dos etapas: la creación real del tambor y el llenado de su superficie con alfileres de acuerdo con una melodía determinada.

La base del carrete se crea usando el siguiente código:

module generateBody() {
	difference() {
		cylinder(d=cylinderDiameter-cylinderTolerance, h=cylinderHeight, $fn=100, center=false);

		cylinder(d=cylinderDiameter-cylinderTolerance-cylinderThickness*2, h=cylinderHeight, $fn=100, center=false);

		// top hole
		translate((cylinderDiameter/-2,cylinderTopHoleWidth/-2, cylinderHeight-cylinderTopHoleHeight))
		cube((cylinderDiameter, cylinderTopHoleWidth, cylinderTopHoleHeight));
	}
	// bottom
	translate((0, 0, -cylinderBottomHeight))
	difference() {
		cylinder(d=cylinderBottomDiameter, h=cylinderBottomHeight, $fn=64, center=false);
		cylinder(d1=cylinderBottomHoleD1,d2=cylinderBottomHoleD2, h=cylinderBottomHeight, $fn=64, center=false);
	}
}

Aquí se crea un cilindro de paredes delgadas con ranuras en la parte superior y un orificio para un tornillo de montaje en la parte inferior. Las dimensiones se toman con un calibre del tambor “original” de la caja. Tiene un engranaje en la parte superior que se quita e instala fácilmente en el tambor impreso, y los huecos ayudan a asegurarlo para que el engranaje no gire. Esto elimina la necesidad de desarrollar e imprimir el equipo desde cero, lo que simplifica todo enormemente.

El modelo de tambor resultante.

El modelo de tambor resultante.

Los pines se crean según el siguiente algoritmo:

  1. Crea un cono truncado y colócalo horizontalmente.

  2. Gírelo y muévalo en la dirección deseada.

    1. El ángulo de rotación se determina de forma muy sencilla: necesitamos mapear nuestra “pista sonora” en un círculo, para ello simplemente multiplicamos el índice del momento correspondiente a la nota para la que se crea este pin por 360º; la longitud total de la composición.

    2. El desplazamiento a lo largo de los ejes X e Y se determina con la misma facilidad: es necesario multiplicar el radio del tambor por el coseno y el seno del ángulo de rotación, respectivamente.

    3. El desplazamiento del eje Z se define como la distancia entre el centro del primer diente del peine desde la coordenada cero + el número de diente multiplicado por la distancia entre los centros de los dientes adyacentes.

Este algoritmo se ejecuta en bucle sobre una composición determinada, previamente convertida a un formato adecuado.

module generatePinsFromScore(score) {
    scoreLength = len(score);
    offsetAngle = 360 / scoreLength;

    for (i = (0:scoreLength - 1)) {
        notes = score(i);
        if (len(notes) - 1 > 0) {
            for (noteIndex = (0:len(notes) - 1)) {
                toothId = notes(noteIndex);
                angle = (isCounterclockwise ? -1 : 1) * (offsetAngle * i);
                rOffset = 0.3;  // How deep pins would protrude the cylinder
                radius = cylinderDiameter / 2 - rOffset;
                x = radius * cos(angle);
                y = radius * sin(angle);
                z = firstPinPosition + pinOffset * (isTopFirst ? (tonesTotalNumber - toothId) : toothId);
                translate((x, y, z))
                    rotate((0, 0, angle))
                        rotate((0, 90, 0))
                            cylinder(
                            d1 = pinBaseDiameter,
                            d2 = pinDiameter,
                            h = pinHeight + rOffset,
                            $fn = 20,
                            center = false
                            );
            }
        }
    }
}
Modelo de batería confeccionado con melodía.

Modelo de batería confeccionado con melodía.

El script Python utiliza una plantilla de archivo para OpenSCAD y, cuando se ejecuta, llena la plantilla con datos basados ​​en la melodía MIDI. El archivo resultante sólo se puede abrir en OpenSCAD, exportar a STL e imprimir.

Sello

Imprimimos el modelo resultante en una impresora SLA. Es mejor utilizar para esto una resina con mayor resistencia. Por lo general, esto se llama resina resistente a los rayos UV o resina similar al ABS. El modelo no requiere soportes y está impreso con suficiente calidad. También en las fotos del autor del sprint original se puede ver el tambor impreso en una impresora FDM, pero todavía no he intentado imprimir esto en FDM.

¿Qué se puede mejorar?

  1. Sería bueno modificar el modelo de tambor para que en su interior haya nervaduras de refuerzo que lo protejan de deformaciones, especialmente al apretar el perno de fijación.

  2. El script se puede modificar para que funcione adecuadamente con archivos MIDI en los que la duración y los intervalos de las notas son diferentes. Ahora, para simplificar, la duración de la primera nota en el archivo se toma como base para el cuanto de tiempo, y puede buscar el máximo común divisor de todos los intervalos de tiempo (para no crear demasiadas líneas vacías en la lista de notas que se transfiere al script SCAD).

  3. Me gustaría tomarme el tiempo para adjuntar una interfaz web al script para que sea más fácil para los usuarios que no tienen conocimientos técnicos.

Un repositorio de scripts

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *