Sound Module
The Sound module provides basic synthesis, PCM sample playback, and music playback for JARU programs. On Windows it uses SDL Audio. On ESP32, ESP32S3, and ESP32P4 it can use the backend configured in boot.cfg, usually i2s, buzzer, or null.
Usage
use Sound
Before playing sound, the audio system must be initialized:
Sound.init()
When finished, Sound.close() stops audio output and releases the backend resources:
Sound.close()
Sample Rate
The JARU mixer can run at 22050 Hz (default) or 44100 Hz. The rate is chosen per board, in the audio.sample_rate field of the hardware profile (boot.cfg): two boards in the same project can use different rates. The effective value is available in the constant:
Sound.SAMPLE_RATE
{
"audio": {
"enable": true,
"backend": "i2s",
"sample_rate": 44100
}
}
If the configured value is neither 22050 nor 44100, the VM shows a warning at startup and continues at 22050 Hz.
The same song or sound effect sounds identical at both rates: note pitch, tempo, envelopes, and the timbre of the noise channel do not depend on the sample rate (the noise generator's clock is pinned to 22050 Hz, like the master clock of a classic sound chip). Choosing 44100 Hz only improves fidelity: less aliasing on tone harmonics and better PCM sample playback, at the cost of twice the mixing work per second.
The IDE's music editor plays its preview at the rate of the active board profile, so what you hear in the tracker is what that board will play.
ESP32 Audio Configuration
The most common fields inside boot.cfg.audio are:
| Field | Description |
|---|---|
enable | Enables or disables the audio system |
backend | Output backend: i2s, buzzer, or null |
sample_rate | 22050 (default) or 44100; other values produce a warning and 22050 is used |
block_frames | Mix block size used by the audio task (64..512, default 256). Smaller blocks = lower SFX latency but less underrun cushion: 128 recommended for games at 22050 Hz (256 at 44100 Hz) |
i2s_port | I2S port used for output |
mclk, bclk, ws, dout, din | I2S bus pins |
buzzer_pin | Pin used by the buzzer backend |
stereo | Duplicates the mono mix into two channels when enabled |
task_core, task_priority, task_stack | Audio task configuration on ESP32 |
Main Functions
| Function | Description |
|---|---|
init() | Initializes the mixer and starts the audio backend |
close() | Stops the backend and releases resources |
tone(channel, freq, volume [, waveform, durationMs, priority]) | Plays a tone |
pulse(channel, freq, volume, duty [, durationMs, priority]) | Plays a pulse wave |
noise(volume, period [, mode, durationMs, priority]) | Plays noise |
fade(channel, targetVolume, durationMs) | Slides a channel volume toward targetVolume |
slideFreq(channel, targetFreq, durationMs) | Slides a channel frequency (portamento) |
slideDuty(channel, targetDuty, durationMs) | Slides the pulse duty toward targetDuty (1..99) |
slideNoise(targetPeriod, durationMs) | Slides the noise channel period |
setRelease(channel, releaseMs) | Release tail applied when a note is stopped (channels 0-4) |
envelope(channel, attackMs, decayMs, sustainVol, releaseMs) | Persistent ADSR envelope for the channel (0-4) |
vibrato(channel, depth, rateTenthsHz [, delayMs]) | Pitch LFO on channels 0-3; depth 0..255 (0 turns it off), rate in tenths of Hz |
pwm(channel, depth, rateTenthsHz [, delayMs]) | Duty LFO on channels 0-3, audible on the PULSE wave; full depth sweeps ±40 duty points |
tremolo(channel, depth, rateTenthsHz [, delayMs]) | Amplitude LFO on channels 0-4 (tones and noise); full depth dips to silence |
stop(channel) | Stops one channel |
stopAll() | Stops all channels |
setVolume(channel, volume) | Changes a channel volume |
setMasterVolume(volume) | Changes the master volume |
isPlaying(channel) | Indicates whether a channel is active |
loadSample(id, bytes, originalFrequency) | Loads a PCM sample |
playSample(channel, id, playbackFrequency, loop) | Plays a sample |
unloadSample(id) | Unloads a sample |
loadMusic(bytes | path) | Loads music data from a buffer or file |
playMusic([loop]) | Plays the loaded music from the beginning |
seekMusic(ms) | Moves the playback head to ms milliseconds from the start |
pauseMusic() | Pauses the music |
resumeMusic() | Resumes the music |
stopMusic() | Stops the music |
setMusicVolume(volume) | Global music volume (0..255) |
isMusicPlaying() | Indicates whether music is playing |
unloadMusic() | Unloads the music |
The freq parameter of tone and pulse and the targetFreq of slideFreq accept decimals in hertz (e.g. tone(0, 65.406, 200) or slideFreq(0, 27.5, 300)) for sub-hertz tuning. Integers keep working exactly as before.
vibrato, pwm and tremolo are persistent channel configuration: they affect the sounding note and every following one until changed (depth 0 turns them off). Each new note re-arms the LFO phase and its optional delayMs onset. playMusic() clears this configuration, since songs carry their own per-instrument settings.
Loading Music From a File
loadMusic() accepts JARU's own binary music format. The file must contain exactly the same bytes that were previously passed to Sound.loadMusic(bytes): version header, flags, tempo/volume, and compact sequencer events.
The current music format is .jmu v1, which is the only supported version (tone-slide targets travel in milli-hertz, with the same precision as notes). The tracker can emit noise-channel-specific controls: white/periodic/short/dark mode, percussion decay in ticks, period slide, and immediate period changes.
use Sound
Sound.init()
if (Sound.loadMusic("Music/theme.jmu")) then
Sound.playMusic(true)
end
It can also be loaded from a specific drive:
Sound.loadMusic("flash:Music/theme.jmu")
Sound.loadMusic("sd:Music/theme.jmu")
This function does not decode WAV, MP3, OGG, or other audio formats. For music, use JARU's compact sequencer format. For raw PCM, keep using loadSample(id, bytes, originalFrequency).
Playing Music From a Position
playMusic() always starts from the beginning. To start from, or jump to, a specific point, use seekMusic(ms), passing the position in milliseconds from the start of the song.
seekMusic() reconstructs the sequencer state up to that point, including per-channel instrument, tempo, transpose, volume, envelopes, and vibrato, and preserves the transport state:
| State When Called | Result |
|---|---|
| Playing | Jumps to ms and keeps playing live |
| Paused | Remains paused at ms; resumeMusic() continues from there |
| Stopped, loaded but not started | Is armed as paused at ms; resumeMusic() starts from there |
use Sound
Sound.init()
Sound.loadMusic("Music/theme.jmu")
// Start the song from second 15
Sound.seekMusic(15000)
Sound.resumeMusic()
// Jump while playing, for example to the chorus
Sound.playMusic(true)
Sound.seekMusic(30000) // jumps to 0:30 and continues
seekMusic() is measured in milliseconds, matching the rest of the module, and respects intermediate tempo changes in the song. If ms exceeds the duration: if the song loops, it wraps around; otherwise, it stays at the end. After the jump, channels remain silent until the next note at the destination point. Calling playMusic() after seekMusic() intentionally restarts from the beginning; to start from the selected point, use resumeMusic().