Módulo Sound
El módulo Sound proporciona síntesis básica, reproducción de samples PCM y reproducción de música para los programas JARU. En Windows usa SDL Audio. En ESP32, ESP32S3 y ESP32P4 puede usar el backend configurado en boot.cfg, normalmente i2s, buzzer o null.
Uso
use Sound
Antes de reproducir sonido se debe iniciar el sistema de audio:
Sound.init()
Al terminar, Sound.close() detiene la salida de audio y libera los recursos del backend:
Sound.close()
Frecuencia de muestreo
El mixer de JARU puede trabajar a 22050 Hz (por defecto) o a 44100 Hz. La frecuencia se elige por placa, en el campo audio.sample_rate del perfil de hardware (boot.cfg): dos placas del mismo proyecto pueden usar frecuencias distintas. El valor efectivo está disponible en la constante:
Sound.SAMPLE_RATE
{
"audio": {
"enable": true,
"backend": "i2s",
"sample_rate": 44100
}
}
Si el valor configurado no es 22050 ni 44100, la VM muestra un aviso al arrancar y continúa a 22050 Hz.
Una misma canción o efecto suena igual a ambas frecuencias: el tono de las notas, el tempo, las envolventes y el timbre del canal de ruido no dependen de la frecuencia de muestreo (el reloj del generador de ruido está fijado a 22050 Hz, como el reloj maestro de un chip de sonido clásico). Elegir 44100 Hz solo mejora la fidelidad: menos aliasing en los armónicos de los tonos y mejor reproducción de samples PCM, a cambio del doble de trabajo de mezcla por segundo.
El editor de música del IDE reproduce el preview a la frecuencia del perfil de placa activo en ese momento, así que lo que oyes en el tracker es lo que sonará en esa placa.
Configuración de audio en ESP32
Los campos más habituales dentro de boot.cfg.audio son:
| Campo | Descripción |
|---|---|
enable | Activa o desactiva el sistema de audio |
backend | Backend de salida: i2s, buzzer o null |
sample_rate | 22050 (defecto) o 44100; otros valores generan un aviso y se usa 22050 |
block_frames | Tamaño del bloque de mezcla de la tarea de audio (64..512, defecto 256). Bloques menores = menos latencia de los SFX pero menos colchón anti-cortes: 128 recomendado para juegos a 22050 Hz (256 a 44100 Hz) |
i2s_port | Puerto I2S que se usará para la salida |
mclk, bclk, ws, dout, din | Pines del bus I2S |
buzzer_pin | Pin usado por el backend buzzer |
stereo | Duplica la mezcla mono en dos canales cuando está activado |
task_core, task_priority, task_stack | Configuración de la tarea de audio en ESP32 |
Funciones principales
| Función | Descripción |
|---|---|
init() | Inicializa el mixer y arranca el backend de audio |
close() | Detiene el backend y libera recursos |
tone(channel, freq, volume [, waveform, durationMs, priority]) | Reproduce un tono |
pulse(channel, freq, volume, duty [, durationMs, priority]) | Reproduce una onda de pulso |
noise(volume, period [, mode, durationMs, priority]) | Reproduce ruido |
fade(channel, targetVolume, durationMs) | Desliza el volumen de un canal hasta targetVolume |
slideFreq(channel, targetFreq, durationMs) | Desliza la frecuencia de un canal (portamento) |
slideDuty(channel, targetDuty, durationMs) | Desliza el duty del pulso hacia targetDuty (1..99) |
slideNoise(targetPeriod, durationMs) | Desliza el periodo del canal de ruido |
setRelease(channel, releaseMs) | Cola de release al parar una nota (canales 0-4) |
envelope(channel, attackMs, decayMs, sustainVol, releaseMs) | Envolvente ADSR persistente del canal (0-4) |
vibrato(channel, depth, rateTenthsHz [, delayMs]) | LFO de tono en canales 0-3; depth 0..255 (0 lo apaga), rate en décimas de Hz |
pwm(channel, depth, rateTenthsHz [, delayMs]) | LFO del duty en canales 0-3, audible con onda PULSE; a fondo barre ±40 puntos de duty |
tremolo(channel, depth, rateTenthsHz [, delayMs]) | LFO de amplitud en canales 0-4 (tonos y ruido); a fondo baja hasta el silencio |
stop(channel) | Detiene un canal |
stopAll() | Detiene todos los canales |
setVolume(channel, volume) | Cambia el volumen de un canal |
setMasterVolume(volume) | Cambia el volumen maestro |
isPlaying(channel) | Indica si un canal está activo |
loadSample(id, bytes, originalFrequency) | Carga un sample PCM |
playSample(channel, id, playbackFrequency, loop) | Reproduce un sample |
unloadSample(id) | Descarga un sample |
loadMusic(bytes | ruta) | Carga datos de música desde un buffer o desde un fichero |
playMusic([loop]) | Reproduce la música cargada desde el principio |
seekMusic(ms) | Mueve el cabezal a ms milisegundos desde el inicio |
pauseMusic() | Pausa la música |
resumeMusic() | Reanuda la música |
stopMusic() | Detiene la música |
setMusicVolume(volume) | Volumen global de la música (0..255) |
isMusicPlaying() | Indica si la música está sonando |
unloadMusic() | Descarga la música |
Los parámetros freq de tone y pulse y targetFreq de slideFreq admiten decimales en hercios (por ejemplo tone(0, 65.406, 200) o slideFreq(0, 27.5, 300)) para afinar con precisión sub-hercio. Los enteros siguen funcionando igual que siempre.
vibrato, pwm y tremolo son configuración persistente del canal: afectan a la nota que suena y a las siguientes hasta que se cambian (depth 0 los apaga). Cada nota nueva re-arma la fase del LFO y su retardo opcional delayMs. playMusic() limpia esta configuración, porque la música trae la suya propia por instrumento.
Cargar música desde fichero
loadMusic() acepta el formato binario propio de música de JARU. El fichero debe contener exactamente los mismos bytes que antes se pasaban a Sound.loadMusic(bytes): cabecera de versión, flags, tempo/volumen y eventos compactos del secuenciador.
El formato actual de música es .jmu v1 (única versión soportada; el destino de los slides de tono viaja en mili-hercios, con la misma precisión que las notas). El tracker puede emitir controles
específicos para el canal de ruido: modo blanco/periódico/corto/oscuro, decay de
percusión en ticks, slide de periodo y cambio inmediato de periodo.
use Sound
Sound.init()
if (Sound.loadMusic("Music/theme.jmu")) then
Sound.playMusic(true)
end
También se puede cargar desde una unidad concreta:
Sound.loadMusic("flash:Music/theme.jmu")
Sound.loadMusic("sd:Music/theme.jmu")
Esta función no decodifica WAV, MP3, OGG ni otros formatos de audio. Para música usa el formato compacto del secuenciador de JARU. Para PCM crudo sigue usando loadSample(id, bytes, originalFrequency).
Reproducir música desde una posición
playMusic() siempre arranca desde el principio. Para empezar (o saltar) a un punto
concreto se usa seekMusic(ms), indicando la posición en milisegundos desde el
inicio de la canción.
seekMusic() reconstruye el estado del secuenciador hasta ese punto (instrumento por
canal, tempo, transpose, volumen, envolventes y vibrato) y conserva el estado de
transporte:
| Estado al llamar | Resultado |
|---|---|
| Sonando | Salta a ms y sigue sonando (seek en vivo) |
| Pausada | Se queda en ms pausada; resumeMusic() continúa desde ahí |
| Parada (cargada sin arrancar) | Queda armada en pausa en ms; resumeMusic() la arranca desde ahí |
use Sound
Sound.init()
Sound.loadMusic("Music/theme.jmu")
// Empezar la canción a partir del segundo 15
Sound.seekMusic(15000)
Sound.resumeMusic()
// Saltar mientras suena (por ejemplo, ir al estribillo)
Sound.playMusic(true)
Sound.seekMusic(30000) // salta a 0:30 y continúa
seekMusic() se mide en milisegundos (coherente con el resto del módulo) y respeta los
cambios de tempo intermedios de la canción. Si ms supera la duración: si la canción
tiene bucle, da la vuelta; si no, queda al final. Tras el salto, los canales permanecen
en silencio hasta la siguiente nota del punto de destino. Llamar a playMusic() después
de seekMusic() reinicia desde el principio a propósito; para arrancar desde el punto
buscado usa resumeMusic().