 
                        
                    CREAR UN DRUM MACHINE CON PYTHON Y PYGAME
Introducción al desarrollo de un drum machine con Python
Crear un drum machine con Python y Pygame es un proyecto ideal para quienes desean combinar programación y música. Este tutorial te guiará paso a paso para construir una aplicación interactiva que permite crear, editar, reproducir y guardar ritmos musicales. Utilizando la biblioteca Pygame, aprenderás a manejar gráficos, sonidos y eventos del usuario, mientras aplicas conceptos de programación orientada a objetos y estructuras de datos. Este proyecto es accesible tanto para principiantes como para desarrolladores experimentados, y ofrece oportunidades para practicar bucles anidados, funciones y gestión de archivos de audio. A lo largo de este artículo, exploraremos cómo configurar el entorno, dibujar la interfaz, implementar sonidos y añadir funcionalidades avanzadas como guardar y cargar ritmos.
Configuración inicial del entorno
Para comenzar, necesitas instalar Python y la biblioteca Pygame. Puedes instalar Pygame utilizando pip, el administrador de paquetes de Python. Abre una terminal y ejecuta el siguiente comando:
pip install pygame
Asegúrate de tener un entorno de desarrollo configurado, como PyCharm o Visual Studio Code, que facilite la gestión de dependencias. Una vez instalado Pygame, importa los módulos necesarios y configura la inicialización básica de la aplicación. Define las dimensiones de la ventana, colores básicos y una fuente para los textos de la interfaz.
import pygame
pygame.init()
pygame.mixer.init()
width, height = 1400, 800
screen = pygame.display.set_mode([width, height])
pygame.display.set_caption("Drum Machine")
label_font = pygame.font.Font("freesansbold.ttf", 32)
black = (0, 0, 0)
white = (255, 255, 255)
gray = (128, 128, 128)
La ventana se establece en 1400x800 píxeles, adecuada para mostrar una cuadrícula de ritmos. La fuente “freesansbold.ttf” es una opción predeterminada en Pygame, pero puedes usar cualquier archivo .ttf personalizado, como “RobotoBold.ttf”, para personalizar el estilo.
Dibujando la interfaz de usuario
La interfaz del drum machine consta de una cuadrícula para los ritmos, un menú lateral para los instrumentos y un menú inferior para controles. Comienza definiendo una función draw_grid que dibuje los elementos estáticos. El menú lateral albergará los nombres de los instrumentos, mientras que el menú inferior contendrá botones para acciones como reproducir, pausar y guardar.
def draw_grid(screen, width, height):
    left_box = pygame.draw.rect(screen, gray, [0, 0, 200, height - 200], 5)
    bottom_box = pygame.draw.rect(screen, gray, [0, height - 200, width, 200], 5)
    return left_box, bottom_box
El menú lateral tiene 200 píxeles de ancho y se extiende hasta la parte superior del menú inferior. El menú inferior tiene 200 píxeles de alto y abarca todo el ancho de la pantalla. Ambos son rectángulos huecos con bordes de 5 píxeles para un diseño limpio.
Creando la cuadrícula de ritmos
La cuadrícula de ritmos es el núcleo de la aplicación, donde los usuarios seleccionan notas para cada instrumento. Define variables para el número de beats (por ejemplo, 8) y el número de instrumentos (por ejemplo, 6: hi-hat, snare, kick, crash, clap, floor tom). Usa bucles anidados para dibujar una cuadrícula de rectángulos, cada uno representando una posible nota.
beats = 8
instruments = 6
def draw_grid(screen, width, height, beats, instruments, clicked):
    boxes = []
    colors = [gray, white, gray]
    for i in range(beats):
        for j in range(instruments):
            rect = pygame.draw.rect(screen, gray, [i * ((width - 200) // beats) + 200, j * 100, ((width - 200) // beats), 100], 5)
            boxes.append((rect, (i, j)))
            if clicked[j][i] == 1:
                pygame.draw.rect(screen, (0, 255, 0), [i * ((width - 200) // beats) + 205, j * 100 + 5, ((width - 200) // beats) - 10, 90], 0, 5)
    return boxes
La variable clicked es una lista bidimensional que rastrea qué notas están activas (1) o inactivas (-1). Los rectángulos activos se rellenan de verde para indicar que están seleccionados. Esta estructura permite crear ritmos digitales de manera interactiva.
Gestionando la interacción del usuario
Para permitir que los usuarios activen o desactiven notas, implementa un sistema de detección de clics. En el bucle principal, verifica los eventos de clic del ratón y actualiza la lista clicked según corresponda.
clicked = [[-1 for _ in range(beats)] for _ in range(instruments)]
run = True
while run:
    timer.tick(60)
    screen.fill(black)
    boxes = draw_grid(screen, width, height, beats, instruments, clicked)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
        if event.type == pygame.MOUSEBUTTONDOWN:
            for i in range(len(boxes)):
                if boxes[i][0].collidepoint(event.pos):
                    coords = boxes[i][1]
                    clicked[coords[1]][coords[0]] *= -1
    pygame.display.flip()
pygame.quit()
Cuando el usuario hace clic en un rectángulo, su estado cambia entre activo (1) e inactivo (-1), permitiendo una edición intuitiva de los ritmos.
Añadiendo sonidos a los instrumentos
El drum machine necesita reproducir sonidos para cada instrumento. Descarga archivos .wav para hi-hat, snare, kick, crash, clap y floor tom, y guárdalos en una carpeta llamada “sounds”. Carga estos sonidos utilizando el módulo mixer de Pygame.
hi_hat = pygame.mixer.Sound("sounds/hi_hat.wav")
snare = pygame.mixer.Sound("sounds/snare.wav")
kick = pygame.mixer.Sound("sounds/kick.wav")
crash = pygame.mixer.Sound("sounds/crash.wav")
clap = pygame.mixer.Sound("sounds/clap.wav")
tom = pygame.mixer.Sound("sounds/tom.wav")
pygame.mixer.set_num_channels(instruments * 3)
Aumenta el número de canales de audio para evitar conflictos si varios sonidos se reproducen simultáneamente. Luego, crea una función play_notes que reproduzca los sonidos correspondientes a las notas activas en el beat actual.
def play_notes(clicked, active_beat):
    for i in range(len(clicked)):
        if clicked[i][active_beat] == 1:
            if i == 0:
                hi_hat.play()
            elif i == 1:
                snare.play()
            elif i == 2:
                kick.play()
            elif i == 3:
                crash.play()
            elif i == 4:
                clap.play()
            elif i == 5:
                tom.play()
Implementando el seguimiento de beats
Para que los ritmos se reproduzcan en secuencia, implementa un sistema que avance por los beats a un ritmo definido por los beats por minuto (BPM). Calcula la duración de cada beat en función del BPM y el framerate (60 FPS).
bpm = 240
beat_length = (60 * 60) // bpm
active_beat = 0
active_length = 0
beat_changed = True
playing = True
while run:
    timer.tick(60)
    screen.fill(black)
    boxes = draw_grid(screen, width, height, beats, instruments, clicked)
    if playing:
        if active_length < beat_length:
            active_length += 1
        else:
            active_length = 0
            if active_beat < beats - 1:
                active_beat += 1
                beat_changed = True
            else:
                active_beat = 0
                beat_changed = True
        if beat_changed:
            play_notes(clicked, active_beat)
            beat_changed = False
    for event in pygame.event.get():
        # Manejo de eventos
    pygame.display.flip()
pygame.quit()
La variable active_beat rastrea el beat actual, y beat_changed asegura que los sonidos se reproduzcan solo cuando se cambia de beat, manteniendo la sincronización.
Añadiendo controles de reproducción
Para mejorar la experiencia del usuario, implementa botones de play y pausa en el menú inferior. Dibuja un botón de reproducción/pausa y detecta clics para alternar el estado de playing.
def draw_grid(screen, width, height, beats, instruments, clicked, playing):
    boxes = []
    play_pause = pygame.draw.rect(screen, gray, [50, height - 150, 200, 100], 5, 5)
    play_text = label_font.render("Play/Pause", True, white)
    screen.blit(play_text, (70, height - 130))
    # Resto del código de draw_grid
    return boxes, play_pause
while run:
    timer.tick(60)
    screen.fill(black)
    boxes, play_pause = draw_grid(screen, width, height, beats, instruments, clicked, playing)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
        if event.type == pygame.MOUSEBUTTONDOWN:
            if play_pause.collidepoint(event.pos):
                playing = not playing
        # Resto del manejo de eventos
    pygame.display.flip()
pygame.quit()
Este código alterna entre reproducir y pausar el ritmo, permitiendo al usuario controlar la reproducción.
Ajustando el número de beats y BPM
Para hacer el drum machine más flexible, permite al usuario ajustar el número de beats y los BPM. Añade botones para incrementar o decrementar estas variables.
def draw_grid(screen, width, height, beats, instruments, clicked, playing, bpm):
    boxes = []
    bpm_add_rect = pygame.draw.rect(screen, gray, [300, height - 150, 100, 50], 5, 5)
    bpm_sub_rect = pygame.draw.rect(screen, gray, [450, height - 150, 100, 50], 5, 5)
    beats_add_rect = pygame.draw.rect(screen, gray, [600, height - 150, 100, 50], 5, 5)
    beats_sub_rect = pygame.draw.rect(screen, gray, [750, height - 150, 100, 50], 5, 5)
    bpm_text = label_font.render(f"BPM: {bpm}", True, white)
    beats_text = label_font.render(f"Beats: {beats}", True, white)
    screen.blit(bpm_text, (320, height - 130))
    screen.blit(beats_text, (620, height - 130))
    return boxes, play_pause, bpm_add_rect, bpm_sub_rect, beats_add_rect, beats_sub_rect
while run:
    timer.tick(60)
    beat_length = (60 * 60) // bpm
    screen.fill(black)
    boxes, play_pause, bpm_add_rect, bpm_sub_rect, beats_add_rect, beats_sub_rect = draw_grid(screen, width, height, beats, instruments, clicked, playing, bpm)
    for event in pygame.event.get():
        if event.type == pygame.MOUSEBUTTONDOWN:
            if bpm_add_rect.collidepoint(event.pos):
                bpm += 5
            if bpm_sub_rect.collidepoint(event.pos):
                bpm = max(60, bpm - 5)
            if beats_add_rect.collidepoint(event.pos):
                beats += 1
                clicked = [[-1 for _ in range(beats)] for _ in range(instruments)]
            if beats_sub_rect.collidepoint(event.pos):
                beats = max(1, beats - 1)
                clicked = [[-1 for _ in range(beats)] for _ in range(instruments)]
        # Resto del manejo de eventos
    pygame.display.flip()
pygame.quit()
Estos controles permiten al usuario personalizar la duración y velocidad del ritmo, ajustando la cuadrícula dinámicamente.
Activando y desactivando instrumentos
Añade la capacidad de activar o desactivar instrumentos individualmente. Crea una lista para rastrear el estado de cada instrumento y dibuja botones junto a sus nombres.
instrument_active = [True for _ in range(instruments)]
def draw_grid(screen, width, height, beats, instruments, clicked, playing, bpm, instrument_active):
    boxes = []
    for j in range(instruments):
        active_rect = pygame.draw.rect(screen, gray, [150, j * 100 + 20, 40, 40], 5, 5)
        if not instrument_active[j]:
            pygame.draw.rect(screen, (100, 100, 100), [150, j * 100 + 25, 30, 30], 0, 5)
        boxes.append((active_rect, (0, j)))
    # Resto del código de draw_grid
    return boxes, play_pause, bpm_add_rect, bpm_sub_rect, beats_add_rect, beats_sub_rect
while run:
    timer.tick(60)
    screen.fill(black)
    boxes, play_pause, bpm_add_rect, bpm_sub_rect, beats_add_rect, beats_sub_rect = draw_grid(screen, width, height, beats, instruments, clicked, playing, bpm, instrument_active)
    for event in pygame.event.get():
        if event.type == pygame.MOUSEBUTTONDOWN:
            for i in range(len(boxes)):
                if boxes[i][0].collidepoint(event.pos) and boxes[i][1][0] == 0:
                    instrument_active[boxes[i][1][1]] = not instrument_active[boxes[i][1][1]]
        # Resto del manejo de eventos
    pygame.display.flip()
pygame.quit()
Modifica la función play_notes para respetar el estado de los instrumentos:
def play_notes(clicked, active_beat, instrument_active):
    for i in range(len(clicked)):
        if clicked[i][active_beat] == 1 and instrument_active[i]:
            if i == 0:
                hi_hat.play()
            elif i == 1:
                snare.play()
            # Resto de las condiciones
Guardando y cargando ritmos
Para guardar ritmos, implementa un menú que permita al usuario ingresar un nombre y almacenar los datos en un archivo de texto. Crea un botón de “guardar” en el menú inferior.
save_menu = False
beat_name = ""
typing = False
def draw_save_menu(screen, width, height, beat_name, typing):
    pygame.draw.rect(screen, black, [0, 0, width, height])
    exit_btn = pygame.draw.rect(screen, gray, [width - 200, height - 100, 180, 90], 0, 5)
    exit_text = label_font.render("Close", True, white)
    screen.blit(exit_text, (width - 160, height - 70))
    saving_btn = pygame.draw.rect(screen, gray, [width // 2 - 200, height * 0.75, 400, 100], 0, 5)
    saving_text = label_font.render("Save Beat", True, white)
    screen.blit(saving_text, (width // 2 - 70, height * 0.75 + 30))
    entry_rect = pygame.draw.rect(screen, gray, [400, 200, 600, 200], 5)
    if typing:
        pygame.draw.rect(screen, (100, 100, 100), [400, 200, 600, 200], 0)
    entry_text = label_font.render(beat_name, True, white)
    screen.blit(entry_text, (430, 260))
    return exit_btn, saving_btn, entry_rect
Maneja los eventos de escritura y guardado en el bucle principal:
while run:
    timer.tick(60)
    screen.fill(black)
    if save_menu:
        exit_btn, saving_btn, entry_rect = draw_save_menu(screen, width, height, beat_name, typing)
    else:
        boxes, play_pause, bpm_add_rect, bpm_sub_rect, beats_add_rect, beats_sub_rect = draw_grid(screen, width, height, beats, instruments, clicked, playing, bpm, instrument_active)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
        if event.type == pygame.MOUSEBUTTONDOWN:
            if save_menu:
                if entry_rect.collidepoint(event.pos):
                    typing = not typing
                if saving_btn.collidepoint(event.pos):
                    with open("saved_beats.txt", "w") as file:
                        saved_beats.append(f"\nname: {beat_name}, beats: {beats}, bpm: {bpm}, selected: {clicked}")
                        for i in range(len(saved_beats)):
                            file.write(str(saved_beats[i]))
                    save_menu = False
                    typing = False
                    beat_name = ""
        if event.type == pygame.TEXTINPUT and typing:
            beat_name += event.text
        if event.type == pygame.KEYDOWN and typing:
            if event.key == pygame.K_BACKSPACE and len(beat_name) > 0:
                beat_name = beat_name[:-1]
    pygame.display.flip()
pygame.quit()
Implementando el menú de carga
El menú de carga permite al usuario seleccionar y cargar ritmos guardados. Lee el archivo saved_beats.txt al iniciar la aplicación y muestra los ritmos disponibles.
saved_beats = []
try:
    with open("saved_beats.txt", "r") as file:
        for line in file:
            saved_beats.append(line)
except FileNotFoundError:
    pass
def draw_load_menu(screen, width, height, index):
    loaded_info = [0, 0, []]
    pygame.draw.rect(screen, black, [0, 0, width, height])
    loaded_rect = pygame.draw.rect(screen, gray, [190, 90, 1000, 600], 5)
    for beat in range(len(saved_beats)):
        if beat < 10:
            row_text = label_font.render(f"{beat + 1}", True, white)
            screen.blit(row_text, (200, 100 + beat * 50))
            name_start = saved_beats[beat].index("name: ") + 6
            name_end = saved_beats[beat].index(", beats:")
            name_text = label_font.render(saved_beats[beat][name_start:name_end], True, white)
            screen.blit(name_text, (240, 100 + beat * 50))
            if 0 <= index < len(saved_beats) and beat == index:
                beat_index = saved_beats[beat].index(": ", name_end) + 2
                bpm_index = saved_beats[beat].index(": ", beat_index) + 2
                loaded_beats = int(saved_beats[beat][name_end + 8:beat_index])
                loaded_bpm = int(saved_beats[beat][beat_index + 6:bpm_index])
                loaded_clicks_string = saved_beats[beat][bpm_index + 14:-3]
                loaded_clicks_rows = list(loaded_clicks_string.split("], ["))
                loaded_clicks = []
                for row in range(len(loaded_clicks_rows)):
                    loaded_clicks_row = loaded_clicks_rows[row].split(", ")
                    for item in range(len(loaded_clicks_row)):
                        if loaded_clicks_row[item] in ["1", "-1"]:
                            loaded_clicks_row[item] = int(loaded_clicks_row[item])
                    loaded_clicks.append(loaded_clicks_row)
                loaded_info = [loaded_beats, loaded_bpm, loaded_clicks]
    return loaded_rect, loaded_info
Maneja la selección y carga de ritmos en el bucle principal:
load_menu = False
index = 100
while run:
    timer.tick(60)
    screen.fill(black)
    if load_menu:
        loaded_rect, loaded_info = draw_load_menu(screen, width, height, index)
    elif save_menu:
        exit_btn, saving_btn, entry_rect = draw_save_menu(screen, width, height, beat_name, typing)
    else:
        boxes, play_pause, bpm_add_rect, bpm_sub_rect, beats_add_rect, beats_sub_rect = draw_grid(screen, width, height, beats, instruments, clicked, playing, bpm, instrument_active)
    for event in pygame.event.get():
        if event.type == pygame.MOUSEBUTTONDOWN:
            if load_menu:
                if loaded_rect.collidepoint(event.pos):
                    index = (event.pos[1] - 100) // 50
                if loading_btn.collidepoint(event.pos):
                    if 0 <= index < len(saved_beats):
                        beats = loaded_info[0]
                        bpm = loaded_info[1]
                        clicked = loaded_info[2]
                        index = 100
                        load_menu = False
        # Resto del manejo de eventos
    pygame.display.flip()
pygame.quit()
Conclusiones
Construir un drum machine con Python y Pygame es un proyecto que combina creatividad y habilidades técnicas. A través de este tutorial, has aprendido a configurar un entorno de desarrollo, dibujar una interfaz gráfica, manejar eventos del usuario, reproducir sonidos y gestionar datos persistentes. Este proyecto no solo refuerza conceptos de programación en Python, sino que también introduce técnicas de diseño de interfaces y manipulación de audio. Puedes expandir esta aplicación añadiendo más instrumentos, efectos de sonido o incluso integración con MIDI. Experimenta con diferentes estilos visuales y sonidos para personalizar tu drum machine, y comparte tus creaciones con la comunidad de programadores.
 
                                
                                 
                                
                                 
                                
                                 
                                
                                