8BITSMUSIC / app.py
Marcepelaez's picture
app
fc2e2af verified
import numpy as np
import wave
import gradio as gr
import tempfile
from typing import List, Optional
import json
# Notas musicales comunes con sus frecuencias
NOTES = {
"C4": 261.63, "D4": 293.66, "E4": 329.63, "F4": 349.23,
"G4": 392.00, "A4": 440.00, "B4": 493.88, "C5": 523.25
}
# Melodías predefinidas
PREDEFINED_MELODIES = {
"Melodía Simple": {
"melody": "261.63:1, 329.63:0.5, 392.00:1, 440.00:0.5",
"counter_melody": "130.81:1, 164.81:0.5, 196.00:1, 220.00:0.5",
"wave_type": "square",
"rhythm_pattern": "1,0,1,0",
"tempo": 120,
"melody_subdivisions": 2,
"counter_subdivisions": 1
},
"Onda Triangular": {
"melody": "440.00:0.5, 493.88:0.5, 523.25:1, 493.88:0.5",
"counter_melody": "329.63:0.5, 349.23:0.5, 392.00:1, 349.23:0.5",
"wave_type": "triangle",
"rhythm_pattern": "1,1,0,0",
"tempo": 140,
"melody_subdivisions": 3,
"counter_subdivisions": 2
},
"Ritmo Complejo": {
"melody": "261.63:0.25, 329.63:0.25, 392.00:0.5, 440.00:0.25, 493.88:0.25",
"counter_melody": "130.81:0.25, 164.81:0.25, 196.00:0.5, 220.00:0.25, 246.94:0.25",
"wave_type": "sawtooth",
"rhythm_pattern": "1,0,1,1,0",
"tempo": 180,
"melody_subdivisions": 4,
"counter_subdivisions": 3
}
}
def generate_tone(frequency: float, duration: float, sample_rate: int,
amplitude: int, wave_type: str, subdivisions: int = 1) -> np.ndarray:
"""
Genera un tono con parámetros avanzados incluyendo submuestreo.
"""
total_samples = int(sample_rate * duration)
samples_per_subdivision = total_samples // subdivisions
t = np.linspace(0, duration, total_samples, endpoint=False)
try:
if wave_type == "square":
tone = amplitude * np.sign(np.sin(2 * np.pi * frequency * t))
elif wave_type == "sine":
tone = amplitude * np.sin(2 * np.pi * frequency * t)
elif wave_type == "triangle":
tone = amplitude * (2 * np.abs(2 * (frequency * t % 1) - 1) - 1)
elif wave_type == "sawtooth":
tone = amplitude * (2 * (frequency * t % 1) - 1)
else:
raise ValueError(f"Tipo de onda no válido: {wave_type}")
# Aplicar submuestreo
if subdivisions > 1:
subsampled_tone = np.zeros_like(tone)
for i in range(subdivisions):
start = i * samples_per_subdivision
end = (i + 1) * samples_per_subdivision
subsampled_tone[start:end] = tone[start:end] * (1 - abs(i - (subdivisions-1)/2) / ((subdivisions-1)/2))
tone = subsampled_tone
return tone.astype(np.int16)
except Exception as e:
raise ValueError(f"Error generando el tono: {str(e)}")
def apply_custom_rhythm(tone: np.ndarray, rhythm_pattern: List[int],
sample_rate: int, beat_duration: float) -> np.ndarray:
"""
Aplica un patrón rítmico personalizado binario.
"""
if not rhythm_pattern:
return tone
total_samples = len(tone)
samples_per_beat = int(sample_rate * beat_duration)
# Generar envolvente rítmica
rhythm_envelope = np.zeros(total_samples)
current_sample = 0
for beat in rhythm_pattern:
if current_sample >= total_samples:
break
beat_samples = samples_per_beat
if current_sample + beat_samples > total_samples:
beat_samples = total_samples - current_sample
if beat == 1:
# Añadir fade in/out
fade_length = int(beat_samples * 0.1)
fade_in = np.linspace(0, 1, fade_length)
fade_out = np.linspace(1, 0, fade_length)
beat_envelope = np.ones(beat_samples)
beat_envelope[:fade_length] *= fade_in
beat_envelope[-fade_length:] *= fade_out
rhythm_envelope[current_sample:current_sample+beat_samples] = beat_envelope
current_sample += samples_per_beat
return (tone * rhythm_envelope).astype(np.int16)
def generate_music(melody: str, counter_melody: str, wave_type: str,
tempo: int, melody_volume: int, counter_volume: int,
rhythm_pattern: str, melody_subdivisions: int,
counter_subdivisions: int) -> str:
"""
Genera una pieza musical con submuestreo y ritmo personalizado.
"""
try:
sample_rate = 44100
beat_duration = 60 / tempo
# Convertir patrón rítmico de texto a lista
try:
rhythm_list = [int(x) for x in rhythm_pattern.split(",") if x.strip() in ['0', '1']]
except:
rhythm_list = []
# Convertir cadenas de notas
melody_notes = [note.strip() for note in melody.split(",") if note.strip()]
counter_notes = [note.strip() for note in counter_melody.split(",") if note.strip()]
def validate_note(note: str) -> tuple:
try:
freq, beats = map(float, note.split(":"))
if freq <= 0 or beats <= 0:
raise ValueError
return freq, beats
except:
raise ValueError(f"Formato inválido de nota: {note}")
# Generar melodía principal
main_tone = []
for note in melody_notes:
freq, beats = validate_note(note)
tone = generate_tone(freq, beats * beat_duration, sample_rate,
melody_volume, wave_type, melody_subdivisions)
main_tone.append(tone)
main_tone = np.concatenate(main_tone)
# Aplicar ritmo personalizado
if rhythm_list:
main_tone = apply_custom_rhythm(main_tone, rhythm_list, sample_rate, beat_duration)
# Generar contramelodía
counter_tone = []
if counter_notes:
for note in counter_notes:
freq, beats = validate_note(note)
tone = generate_tone(freq, beats * beat_duration, sample_rate,
counter_volume, wave_type, counter_subdivisions)
counter_tone.append(tone)
counter_tone = np.concatenate(counter_tone)
# Aplicar ritmo personalizado
if rhythm_list:
counter_tone = apply_custom_rhythm(counter_tone, rhythm_list, sample_rate, beat_duration)
else:
counter_tone = np.zeros_like(main_tone)
# Asegurar misma longitud
max_length = max(len(main_tone), len(counter_tone))
main_tone = np.pad(main_tone, (0, max_length - len(main_tone)))
counter_tone = np.pad(counter_tone, (0, max_length - len(counter_tone)))
# Mezclar pistas
combined_audio = main_tone + counter_tone
combined_audio = np.clip(combined_audio, -32767, 32767).astype(np.int16)
# Guardar audio
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmpfile:
with wave.open(tmpfile.name, 'w') as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(sample_rate)
wf.writeframes(combined_audio.tobytes())
return tmpfile.name
except Exception as e:
raise gr.Error(f"Error generando la música: {str(e)}")
def create_note_buttons():
"""Crea botones para las notas predefinidas."""
return [gr.Button(note) for note in NOTES.keys()]
def add_note(note: str, current_melody: str) -> str:
"""Añade una nota al campo de melodía."""
freq = NOTES[note]
if current_melody:
return f"{current_melody}, {freq}:1"
return f"{freq}:1"
def load_predefined_melody(melody_name: str):
"""
Carga los parámetros de una melodía predefinida.
"""
melody_params = PREDEFINED_MELODIES.get(melody_name, {})
return (
melody_params.get('melody', ''),
melody_params.get('counter_melody', ''),
melody_params.get('wave_type', 'square'),
melody_params.get('tempo', 120),
melody_params.get('rhythm_pattern', ''),
melody_params.get('melody_subdivisions', 1),
melody_params.get('counter_subdivisions', 1)
)
# Interfaz de Gradio
with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo", secondary_hue="blue")) as ui:
gr.Markdown("""
# 🎵 Generador de Música Avanzado
Crea música con ritmos y submuestreo personalizados.
## Formato de Entrada
- Notas: `frecuencia:duración` (Ej: 440:1)
- Ritmo: Patrón binario de 0 y 1 (Ej: 1,0,1,0)
""")
with gr.Row():
with gr.Column(scale=2):
melody = gr.Textbox(
label="Melodía Principal",
placeholder="Ejemplo: 440:1, 494:0.5",
lines=3
)
counter_melody = gr.Textbox(
label="Contramelodía (opcional)",
placeholder="Ejemplo: 330:1, 349:0.5",
lines=3
)
with gr.Column(scale=1):
gr.Markdown("### Notas Predefinidas")
with gr.Row():
note_buttons = create_note_buttons()
for btn in note_buttons:
btn.click(fn=add_note, inputs=[btn, melody], outputs=melody)
with gr.Accordion("Melodías Predefinidas", open=False):
predefined_melody_dropdown = gr.Dropdown(
list(PREDEFINED_MELODIES.keys()),
label="Seleccionar Melodía Predefinida"
)
load_melody_btn = gr.Button("Cargar Melodía")
with gr.Row():
with gr.Column():
wave_type = gr.Radio(
["sine", "square", "triangle", "sawtooth"],
label="Tipo de Onda",
value="square"
)
rhythm_pattern = gr.Textbox(
label="Patrón Rítmico (0 y 1)",
placeholder="Ejemplo: 1,0,1,0",
value=""
)
with gr.Column():
melody_subdivisions = gr.Slider(
minimum=1, maximum=8, step=1,
label="Subdivisiones de Melodía",
value=1
)
counter_subdivisions = gr.Slider(
minimum=1, maximum=8, step=1,
label="Subdivisiones de Contramelodía",
value=1
)
tempo = gr.Slider(
minimum=60, maximum=180, step=5,
label="Tempo (BPM)",
value=120
)
volume_row = gr.Row()
with volume_row:
melody_volume = gr.Slider(
minimum=0, maximum=30000, step=1000,
label="Vol. Melodía", value=15000
)
counter_volume = gr.Slider(
minimum=0, maximum=30000, step=1000,
label="Vol. Contramelodía", value=10000
)
output_audio = gr.Audio(label="Audio Generado")
generate_btn = gr.Button("Generar Música", variant="primary")
# Conexión de eventos
load_melody_btn.click(
fn=load_predefined_melody,
inputs=predefined_melody_dropdown,
outputs=[
melody,
counter_melody,
wave_type,
tempo,
rhythm_pattern,
melody_subdivisions,
counter_subdivisions
]
)
generate_btn.click(
fn=generate_music,
inputs=[melody, counter_melody, wave_type, tempo,
melody_volume, counter_volume, rhythm_pattern,
melody_subdivisions, counter_subdivisions],
outputs=output_audio
)
gr.Markdown("""
### Características Avanzadas
- Ritmo personalizado con patrón binario
- Submuestreo para efectos de textura
- Control independiente de melodía y contramelodía
""")
if __name__ == "__main__":
ui.launch()
# requirements.txt content
# numpy==1.23.5
# gradio==3.50.2
# wave==0.0.2