Skip to main content

Sound module

Sound is a small sound chip: four tone channels, one noise channel and two channels for recorded sounds (PCM samples), plus a sequencer that plays the music of the Music Tracker. Each order (a note, an effect) is mixed with what is already playing, without stopping your program.

use Sound

Call Sound.init() once, at the start: without it nothing sounds.

Platforms​

WindowsESP32 / ESP32-S3ESP32-P4ESP32-S31
Where it soundsThe PC's sound cardThe output of the board profile: I2S, buzzer or none (null)The output of the board profileThe output of the board profile
Sound.SAMPLE_RATE22050The profile's: 22050 or 44100The profile's: 22050 or 44100The profile's: 22050 or 44100

The output of each board, its pins and its sample rate are chosen in its profile: see Display, sound, SD and network. If the profile has sound turned off, Sound.init() returns false and nothing sounds. On Windows, SAMPLE_RATE is 44100 if the environment variable JARU_AUDIO_SAMPLE_RATE is 44100; any other value is ignored.

A song or an effect sounds the same at 22050 and at 44100: the pitch of the notes, the tempo, the envelopes and the noise do not depend on the sample rate. 44100 only gives a cleaner sound in the high notes and in the samples, at the cost of twice the mixing work.

With the buzzer (a speaker that plays a single tone), the module does less:

  • One channel sounds at a time: the lowest-numbered of those playing, from 0 to 4.
  • Every waveform sounds like a square one, with the frequency rounded to whole hertz. The volume, and the duty of pulse(), change how loud it sounds.
  • The noise sounds like a tone from 80 to 4000 Hz, depending on its period.
  • The ramps (fade, slideFreq, slideDuty, slideNoise) jump straight to the final value.
  • envelope, setRelease, vibrato, pwm and tremolo do nothing, although they return true.
  • There are no samples and no music: loadSample, playSample, unloadSample and the music functions return false.

With null nothing sounds: the functions accept the orders and return true, and isPlaying() and isMusicPlaying() return false.

The audio block of boot.cfg​

The output of the board is chosen in the Audio page of ESP32 Tools, which writes it in the audio block of boot.cfg (see Display, sound, SD and network). It is not used on Windows. These are its fields:

FieldWhat it isDefault
enableTurns the sound on. Off, Sound.init() returns false.false
backendThe output: i2s, buzzer or null.null
sample_rate22050 or 44100. With any other value, the VM warns at startup and uses 22050.22050
block_framesThe samples it mixes at a time, from 64 to 512 (outside that range it is adjusted). With fewer, the effects sound sooner, but there is less margin against dropouts.512 in ESP32 Tools (256 if it is missing from boot.cfg)
i2s_portThe I2S port.0
mclk, bclk, ws, dout, dinThe pins of the I2S bus; -1 is none.-1
buzzer_pinThe buzzer pin. With -1, it uses the dout one.-1
stereoSends the mix, which is mono, through both channels.true
mclk_multipleHow many times the sample rate the MCLK clock runs at: 256 or 384, plus 128 on the ESP32 and the S3, or 512 on the P4 and the S31. Any other value counts as 256.256
task_coreThe core of the task that mixes the sound: "0", "1" or "opposite", the one the VM is not on."opposite"
task_priority, task_stackThe priority of that task (0 counts as 5) and its stack, in bytes (at least 4096).5, 6144

The channels​

ChannelConstantWhat it playsIts functions
0 to 3Tones: square, triangle, saw, pulse and sinetone, pulse, slideFreq, slideDuty, vibrato, pwm, tremolo, fade, envelope, setRelease
4Noisenoise, slideNoise, tremolo, fade, envelope, setRelease
5 and 6S0, S1PCM samplesplaySample

stop, setVolume and isPlaying work on all seven. The music uses channels 0 to 4: see Music and effects.

Volume, duration and priority​

  • The volume goes from 0 to 255 (Sound.MAX_VOLUME). In tone, pulse, noise and the volume functions, a value outside is adjusted; in fade and envelope, it is an error.
  • The duration goes in milliseconds, from 0 to 65535. With 0, the default, the note sounds until stop(). It is counted in steps of 1/240 of a second, rounding up.
  • The priority goes from 0 to 255, and is 0 by default. A new note on a channel that is playing is dropped if the one playing has a higher priority; the function returns true all the same. When a note with a duration ends, or with stop(), the channel goes back to priority 0.

What the functions return​

Almost all of them return true if they accept the order and false if they cannot carry it out (no init(), no memory, with the buzzer what it does not have). With a wrong argument they do not throw an error: they return it, as a value:

  • with the wrong number of arguments, ERR_INVALID_ARG_NUMBER;
  • with an argument of another type, ERR_TYPE_MISMATCH;
  • with a channel, a frequency, a waveform, a duty, a period, a duration or a priority out of range, ERR_INVALID_VALUE.

try does not catch those errors, and in an if an error value counts as true. To know whether an order went well, compare it with true:

use Sound

Sound.init()
var ok = Sound.tone(9, 440, 200) // there is no channel 9
if (ok != true) then
println("It did not sound: ", ok) // It did not sound: Error 8
end

More on error values in Errors and exceptions.

Functions​

FunctionWhat it doesReturns
init()Starts the sound.true or false
close()Stops the sound and unloads the samples and the music.true
tone(channel, frequency, volume, ...)Plays a note.true
pulse(channel, frequency, volume, duty, ...)Plays a pulse-wave note.true
noise(volume, period, ...)Plays noise.true
stop(channel)Silences a channel.true
stopAll()Silences all of them, at once.true
setVolume(channel, volume)Changes the volume of what plays on a channel.true
setMasterVolume(volume)Changes the volume of everything.true
isPlaying(channel)Tells whether a channel is playing.true or false
fade(channel, volume, ms)Takes the volume of a channel to another one gradually.true
slideFreq(channel, frequency, ms)Slides the pitch of a channel.true
slideDuty(channel, duty, ms)Slides the duty of a pulse wave.true
slideNoise(period, ms)Slides the period of the noise.true
envelope(channel, attack, decay, sustain, release)Sets the envelope of a channel.true
setRelease(channel, ms)Sets only the release of a channel.true
vibrato(channel, depth, rate, delay)Makes the pitch wobble.true
pwm(channel, depth, rate, delay)Makes the duty of a pulse wave wobble.true
tremolo(channel, depth, rate, delay)Makes the volume wobble.true
loadSample(id, bytes, frequency)Loads a recorded sound.true or false
playSample(channel, id, frequency, loop)Plays a recorded sound.true or false
unloadSample(id)Unloads a recorded sound.true or false
loadMusic(file or bytes)Loads a song.true or false
playMusic(loop)Plays the song from the start.true or false
seekMusic(ms)Jumps to a point of the song.true or false
pauseMusic()Pauses the song.true
resumeMusic()Carries on with the song.true
stopMusic()Stops the song.true
setMusicVolume(volume)Changes the volume of the song.true
isMusicPlaying()Tells whether the song is playing.true or false
unloadMusic()Unloads the song.true or false

init​

init() starts the sound output and returns true. It returns false if it cannot: on the PC, with no sound card; on the board, with sound turned off in the profile or if the output does not start.

Without init() nothing sounds: the orders are kept, and when there are 128 of them waiting, the functions start returning false.

close​

close() stops the sound output and unloads the samples and the music. It returns true. To make it sound again, call init() again and load again what you need.

tone​

tone(channel, frequency, volume, waveform, duration, priority) plays a note on a tone channel. The last three arguments are optional.

use Sound

Sound.init()
Sound.tone(0, 440, 200) // an A that sounds until stop(0)
Sound.tone(1, 65.406, 180, Sound.TRIANGLE, 500) // a low C, half a second
Sound.tone(2, 880, 255, Sound.SQUARE, 100, 5) // a beep with priority 5
pause(600)
Sound.stop(0)
ParameterWhat it is
channelFrom 0 to 3.
frequencyIn hertz, from above 0 to 65535. It takes decimals, for fine tuning: 65.406.
volumeFrom 0 to 255. Outside that range it is adjusted.
waveformSQUARE (the default), TRIANGLE, SAW, PULSE (with a 50 % duty) or SINE.
durationIn milliseconds, from 0 to 65535. With 0, the default, it sounds until stop().
priorityFrom 0 to 255; 0 by default.

The note replaces the one playing on the channel, unless that one has a higher priority. If the channel has an envelope or LFOs, the note uses them.

pulse​

pulse(channel, frequency, volume, duty, duration, priority) plays a pulse-wave note. duty is the part of the period the wave stays high, as a percentage, from 1 to 99: with 50 it is a square wave, and the further it gets from 50, the thinner it sounds. The duration and the priority are optional, as in tone().

use Sound

Sound.init()
Sound.pulse(0, 330, 200, 12, 400) // 12 % duty, 400 ms
pause(500)

A duty outside 1 to 99 gives ERR_INVALID_VALUE.

noise​

noise(volume, period, mode, duration, priority) plays noise on channel 4. The last three arguments are optional.

use Sound

Sound.init()
Sound.noise(255, 6, Sound.NOISE_WHITE, 400) // a short explosion
pause(500)
ParameterWhat it is
volumeFrom 0 to 255. Outside that range it is adjusted.
periodFrom 1 to 65535. The noise changes every period cycles of a 22050 Hz clock (the same at 44100): the higher, the lower it sounds.
modeNOISE_WHITE (the default), NOISE_PERIODIC, NOISE_SHORT or NOISE_DARK: see Constants.
durationIn milliseconds, from 0 to 65535. With 0, it sounds until stop(4).
priorityFrom 0 to 255; 0 by default.

stop​

stop(channel) silences a channel, from 0 to 6. If the channel has a release (envelope or setRelease), the note fades out; if not, it stops at once.

stopAll​

stopAll() silences the seven channels, at once, with no release. The song does not stop: it carries on and sounds again at its next note. To stop it, stopMusic().

setVolume​

setVolume(channel, volume) changes the volume of what is playing on a channel, from 0 to 6. The volume goes from 0 to 255; outside that range it is adjusted. A channel that is not playing does not change, although the function returns true.

On the sample channels (S0, S1), the volume stays for the next sounds: a recorded sound plays at the volume its channel had, or at 255 if it was at 0 (the first time). Change it after playSample().

setMasterVolume​

setMasterVolume(volume) changes the volume of everything that sounds, music included, from 0 to 255. Outside that range it is adjusted.

isPlaying​

isPlaying(channel) returns true while the channel is playing: a note, the tail of its release, a sample or a note of the song. With a channel that does not exist, it returns false.

use Sound

Sound.init()
Sound.tone(0, 440, 200, Sound.SQUARE, 100)
pause(50)
println(Sound.isPlaying(0)) // true
pause(200)
println(Sound.isPlaying(0)) // false

The orders are applied in the next block of sound: right after tone(), isPlaying() can still give false for a few milliseconds.

fade​

fade(channel, volume, ms) takes the volume of what is playing on a channel, from 0 to 4, to volume (from 0 to 255) in ms milliseconds (from 0 to 65535).

use Sound

Sound.init()
Sound.tone(0, 220, 255)
Sound.fade(0, 0, 1000) // it fades out in one second
pause(1100)
Sound.stop(0) // fade does not silence the channel: it has to be stopped

A fade to 0 leaves the channel playing in silence: isPlaying() keeps giving true until stop(). On a channel that is not playing, it does nothing.

slideFreq​

slideFreq(channel, frequency, ms) slides the pitch of the note playing on a channel, from 0 to 3, up to frequency (from above 0 to 65535 Hz, with decimals) in ms milliseconds. It is the portamento.

use Sound

Sound.init()
Sound.tone(0, 1200, 200, Sound.SAW, 300)
Sound.slideFreq(0, 200, 300) // a laser shot that drops
pause(400)

The next note of the channel cancels the slide. On a channel that is not playing, it does nothing.

slideDuty​

slideDuty(channel, duty, ms) slides the duty of the pulse wave playing on a channel, from 0 to 3, up to duty (from 1 to 99) in ms milliseconds.

slideNoise​

slideNoise(period, ms) slides the period of the noise playing on channel 4 up to period (from 1 to 65535) in ms milliseconds.

envelope​

envelope(channel, attack, decay, sustain, release) sets the envelope of a channel, from 0 to 4: how the volume of each note rises, falls and dies away.

use Sound

Sound.init()
Sound.envelope(0, 10, 150, 128, 300) // quick attack, drops to half, 300 ms tail
Sound.tone(0, 440, 220, Sound.SQUARE, 400)
pause(800)
ParameterWhat it is
channelFrom 0 to 4.
attackMilliseconds the note takes to rise to its volume.
decayMilliseconds it takes to fall to the sustain level.
sustainThe level it stays at while the note lasts, from 0 to 255: a part of the note's volume (255 is all of it).
releaseMilliseconds it takes to die away when the note ends or with stop().

The times go from 0 to 65535. The envelope stays on the channel for the next notes, until you change it. With attack and decay at 0, the note plays at its volume until it ends.

setRelease​

setRelease(channel, ms) sets only the release of a channel, from 0 to 4: the milliseconds (from 0 to 65535) a note takes to die away when it ends or with stop(). It does not change the rest of the envelope.

vibrato​

vibrato(channel, depth, rate, delay) makes the pitch of a channel wobble, from 0 to 3.

use Sound

Sound.init()
Sound.vibrato(0, 80, 60, 150) // a third of the depth, 6 Hz, after 150 ms
Sound.tone(0, 523.25, 200, Sound.SINE, 800)
pause(900)
ParameterWhat it is
depthFrom 0 to 255. With 255, the pitch goes up and down a semitone; with 0, the vibrato is off.
rateIn tenths of a hertz, from 0 to 255: 60 is 6 wobbles per second.
delayOptional: the milliseconds (from 0 to 65535) each note waits before it starts to wobble.

pwm​

pwm(channel, depth, rate, delay) makes the duty of the pulse wave of a channel wobble, from 0 to 3. You only hear it with pulse() or with the PULSE waveform. With depth 255, the duty goes up and down 40 points; the other arguments are like those of vibrato().

tremolo​

tremolo(channel, depth, rate, delay) makes the volume of a channel wobble, from 0 to 4 (the noise too). With depth 255, the volume drops to silence in each wobble; it never goes above the note's volume. The other arguments are like those of vibrato().

LFOs stay on the channel

vibrato, pwm and tremolo are settings of the channel: they apply to the note playing and to the next ones, until you change them (with depth 0 they turn off). Each new note starts its wobble from the beginning, with its delay. playMusic() and seekMusic() clear them on channels 0 to 4, because the music brings its own in each instrument.

loadSample​

loadSample(id, bytes, frequency) loads a recorded sound into one of the 32 slots (Sound.MAX_SAMPLES).

ParameterWhat it is
idThe slot, from 0 to 31. If it already had a sound, it replaces it.
bytesA Bytes with the sound: one byte per sample, in signed 8-bit mono PCM. Each byte goes from -128 to 127.
frequencyThe samples per second of the sound: 8000, 11025, 22050... Greater than 0.

It returns true, or false if there is no memory left (or with the buzzer). It copies the bytes: afterwards you can reuse the Bytes. A slot outside 0 to 31, an empty Bytes or a frequency of 0 give ERR_INVALID_VALUE, and something that is not a Bytes, ERR_TYPE_MISMATCH.

The write(i, value) method of a Bytes keeps the low byte of the number, so a value from -128 to 127 stays as it is as a sample:

use Sound, Bytes

Sound.init()

// 0.2 s of sawtooth, at 8000 samples per second
var sample = Bytes.new(1600)
for (var i = 0; i < 1600; i++)
sample.write(i, (i % 32) * 8 - 128)
end
println(Sound.loadSample(0, sample, 8000)) // true

loadSample does not read WAV files or any other format: only the bytes you give it.

playSample​

playSample(channel, id, frequency, loop) plays the sound of a slot on a sample channel.

use Sound, Bytes

Sound.init()
var sample = Bytes.new(1600)
for (var i = 0; i < 1600; i++)
sample.write(i, (i % 32) * 8 - 128)
end
Sound.loadSample(0, sample, 8000)

Sound.playSample(Sound.S0, 0, 0, false) // at its frequency: 0.2 s
pause(300)
Sound.playSample(Sound.S1, 0, 16000, false) // at double: an octave higher and 0.1 s
pause(200)
ParameterWhat it is
channelSound.S0 (5) or Sound.S1 (6).
idThe slot, from 0 to 31.
frequencyThe samples per second it plays it at. With 0, those of loadSample. With double, it sounds an octave higher and lasts half as long.
looptrue to repeat it until stop(). An integer works too: 0 is false.

It returns true, or false if the slot is empty (or with the buzzer). A channel that is neither 5 nor 6, a slot outside 0 to 31 or a negative frequency give ERR_INVALID_VALUE. Whatever was playing on the channel is cut off.

unloadSample​

unloadSample(id) unloads the sound of a slot, from 0 to 31, and frees its memory. It returns true, or false if the slot was already empty.

loadMusic​

loadMusic(file) or loadMusic(bytes) loads a .jmu song, the format the Music Tracker generates when you compile. It does not play it: for that, playMusic().

use Sound

Sound.init()
if (Sound.loadMusic("Music/Theme.jmu")) then
Sound.playMusic(true)
end
  • The path is the song's in the project, with .jmu: Music/Theme.jms is loaded as "Music/Theme.jmu". A unit works too: "flash:Music/Theme.jmu", "sd:Music/Theme.jmu" (the units, in File).
  • With a Bytes, it loads its bytes, which have to be those of a .jmu. It copies them: afterwards you can reuse the Bytes.
  • It returns true, or false if the file does not exist, is larger than 65535 bytes, is not a .jmu of this version or there is no memory left (and with the buzzer). With another type, ERR_TYPE_MISMATCH.
  • Only one song is loaded: loading another stops and replaces the one there was.
  • It does not read WAV, MP3, OGG or other sound formats. For recorded sounds, loadSample.

playMusic​

playMusic() plays the loaded song from the start. Without an argument, it repeats if it has Loop song in the tracker; playMusic(true) and playMusic(false) decide it for you. With a loop, when it reaches the end it goes back to the song's loop point; without one, it stops.

It returns true, or false if there is no song loaded (or with the buzzer).

When it starts, it silences channels 0 to 4 (also the effects playing on them), clears their envelope and LFOs, and sets the song's volume again.

seekMusic​

seekMusic(ms) moves the song to the point ms milliseconds from the start. playMusic() always starts from the beginning: to start at (or jump to) another point, use seekMusic(). Copy ms (seekMusic), in the tracker, copies to the clipboard the time in milliseconds at which the cursor row starts.

seekMusic() rebuilds how the song would be at that point (the instrument of each channel, the tempo, the transpose, the volume, the envelopes and the vibrato) and keeps what it was doing:

When you call it, the song isResult
PlayingIt jumps to ms and keeps playing.
PausedIt stays at ms, paused; resumeMusic() carries on from there.
Stopped (loaded but not started)It stays at ms, paused; resumeMusic() starts it from there.
use Sound

Sound.init()
Sound.loadMusic("Music/Theme.jmu")

// Start the song at second 15
Sound.seekMusic(15000)
Sound.resumeMusic()
use Sound

Sound.init()
Sound.loadMusic("Music/Theme.jmu")

// Jump while it plays (for example, to the chorus)
Sound.playMusic(true)
Sound.seekMusic(30000) // jumps to 0:30 and carries on
  • It takes the tempo changes of the song into account.
  • If ms is longer than the song: if the song loops, it wraps around; if not, it stays at the end.
  • After the jump, the channels stay silent until the next note.
  • Calling playMusic() after seekMusic() starts again from the beginning. To start from the point you sought, resumeMusic().
  • It returns true, or false if there is no song. With a negative ms, ERR_INVALID_VALUE.

pauseMusic​

pauseMusic() pauses the song and silences its notes: with resumeMusic(), it carries on at its next note. It returns true.

resumeMusic​

resumeMusic() carries on with the paused song, or starts the one seekMusic() left ready. It returns true.

stopMusic​

stopMusic() stops the song and silences channels 0 to 4, also the effects playing on them. The song stays loaded: playMusic() starts it again. It returns true.

setMusicVolume​

setMusicVolume(volume) changes the volume of the song, from 0 to 255 (outside that range it is adjusted). It multiplies the volume of each channel of the song. It returns true.

  • You hear it from the next note of each channel, not in the one playing.
  • playMusic() and seekMusic() set the song's volume again, and the song also changes it if it has a volume command. Call setMusicVolume() after playMusic().

isMusicPlaying​

isMusicPlaying() returns true if the song is playing: neither stopped nor paused.

unloadMusic​

unloadMusic() stops the song and frees its memory. It returns true, or false if none was loaded.

Music and effects​

The song and your effects share channels 0 to 4. The sample channels, S0 and S1, are yours alone.

  • The song plays its notes with priority 0. An effect with a higher priority (and with a duration) keeps the channel while it plays, and the song comes back at its next note. An effect with priority 0 plays until the song's next note on that channel.
  • playMusic() and stopMusic() silence channels 0 to 4, with whatever effects they had.
  • The song sets its instruments: while it plays, an effect on a channel of the song uses the envelope and the LFOs of that channel's instrument. When it starts, playMusic() clears yours.

To keep the effects from fighting with the music:

  • play them on a channel the song does not use, or on S0 and S1 if they are recorded sounds;
  • or give them a priority and a duration: Sound.tone(3, 880, 255, Sound.SQUARE, 120, 10).

How the song is written, its channels and its instruments: Music Tracker.

Constants​

ConstantValueWhat it is
Sound.SQUARE0Square wave.
Sound.TRIANGLE1Triangle wave: soft.
Sound.SAW2Saw wave: bright.
Sound.PULSE3Pulse wave: with tone(), at 50 %; the duty is chosen with pulse().
Sound.SINE4Sine wave: the purest.
Sound.NOISE_WHITE0White noise.
Sound.NOISE_PERIODIC1Noise that repeats a short pattern: it sounds somewhat pitched.
Sound.NOISE_SHORT2Short-sequence noise: metallic.
Sound.NOISE_DARK3Filtered white noise: duller.
Sound.S05The first sample channel.
Sound.S16The second sample channel.
Sound.MAX_VOLUME255The highest volume.
Sound.MAX_SAMPLES32The slots of loadSample.
Sound.SAMPLE_RATE22050 or 44100The samples per second the sound is mixed at: see Platforms.

Full example​

A sound test: a tune with an envelope, a laser, an explosion and a recorded sound, one after another.

use Sound, Bytes

if (Sound.init() != true) then
println("No sound")
exit(0)
end

// A tune on channel 0, with a short attack and a tail
Sound.envelope(0, 5, 80, 160, 120)
var notes = [523.25, 659.25, 783.99, 1046.5]
foreach (var note in notes)
Sound.tone(0, note, 200, Sound.PULSE, 150)
pause(180)
end
println("Tune")

// A laser: a saw that drops
Sound.tone(1, 1500, 180, Sound.SAW, 250)
Sound.slideFreq(1, 150, 250)
pause(300)
println("Laser")

// An explosion: noise that fades out
Sound.noise(255, 8, Sound.NOISE_DARK)
Sound.fade(4, 0, 600)
pause(650)
Sound.stop(4)
println("Explosion")

// A recorded sound: 0.1 s of square wave at 8000 samples per second
var click = Bytes.new(800)
for (var i = 0; i < 800; i++)
if ((i div 10) % 2 == 0) then
click.write(i, 100)
else
click.write(i, -100)
end
end
Sound.loadSample(0, click, 8000)
Sound.playSample(Sound.S0, 0, 0, false)
pause(200)
println("Sample")

Sound.close()

What it prints:

Tune
Laser
Explosion
Sample

Pitfalls and known issues​

  • Without Sound.init() nothing sounds. The orders are kept, and at 128 the functions return false.
  • Errors are not thrown: they are returned. try does not catch them, and in an if an error value counts as true: compare with true (see What the functions return).
  • A note dropped because of its priority also returns true.
  • fade() to 0 does not silence the channel: isPlaying() stays true until stop().
  • playMusic() and stopMusic() cut off the effects of channels 0 to 4, and playMusic() clears their envelope and LFOs.
  • setMusicVolume() before playMusic() is lost.
  • close() unloads the samples and the music.
  • Ramps only act on what is playing: on a stopped channel, fade and the slide functions do nothing, although they return true. The LFOs and the envelope, on the other hand, stay for the next note.

All the known issues of JARU, together, are in Known issues.