Skip to main content

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.

IDE preview

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:

FieldDescription
enableEnables or disables the audio system
backendOutput backend: i2s, buzzer, or null
sample_rate22050 (default) or 44100; other values produce a warning and 22050 is used
block_framesMix 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_portI2S port used for output
mclk, bclk, ws, dout, dinI2S bus pins
buzzer_pinPin used by the buzzer backend
stereoDuplicates the mono mix into two channels when enabled
task_core, task_priority, task_stackAudio task configuration on ESP32

Main Functions

FunctionDescription
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
Fractional frequency

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.

Per-channel LFOs (vibrato, pwm and tremolo)

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")
info

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 CalledResult
PlayingJumps to ms and keeps playing live
PausedRemains paused at ms; resumeMusic() continues from there
Stopped, loaded but not startedIs 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
info

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().