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
| Windows | ESP32 / ESP32-S3 | ESP32-P4 | ESP32-S31 | |
|---|---|---|---|---|
| Where it sounds | The PC's sound card | The output of the board profile: I2S, buzzer or none (null) | The output of the board profile | The output of the board profile |
Sound.SAMPLE_RATE | 22050 | The profile's: 22050 or 44100 | The profile's: 22050 or 44100 | The 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,pwmandtremolodo nothing, although they returntrue.- There are no samples and no music:
loadSample,playSample,unloadSampleand the music functions returnfalse.
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:
| Field | What it is | Default |
|---|---|---|
enable | Turns the sound on. Off, Sound.init() returns false. | false |
backend | The output: i2s, buzzer or null. | null |
sample_rate | 22050 or 44100. With any other value, the VM warns at startup and uses 22050. | 22050 |
block_frames | The 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_port | The I2S port. | 0 |
mclk, bclk, ws, dout, din | The pins of the I2S bus; -1 is none. | -1 |
buzzer_pin | The buzzer pin. With -1, it uses the dout one. | -1 |
stereo | Sends the mix, which is mono, through both channels. | true |
mclk_multiple | How 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_core | The core of the task that mixes the sound: "0", "1" or "opposite", the one the VM is not on. | "opposite" |
task_priority, task_stack | The priority of that task (0 counts as 5) and its stack, in bytes (at least 4096). | 5, 6144 |
The channels
| Channel | Constant | What it plays | Its functions |
|---|---|---|---|
| 0 to 3 | Tones: square, triangle, saw, pulse and sine | tone, pulse, slideFreq, slideDuty, vibrato, pwm, tremolo, fade, envelope, setRelease | |
| 4 | Noise | noise, slideNoise, tremolo, fade, envelope, setRelease | |
| 5 and 6 | S0, S1 | PCM samples | playSample |
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). Intone,pulse,noiseand the volume functions, a value outside is adjusted; infadeandenvelope, 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
trueall the same. When a note with a duration ends, or withstop(), 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
| Function | What it does | Returns |
|---|---|---|
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)
| Parameter | What it is |
|---|---|
channel | From 0 to 3. |
frequency | In hertz, from above 0 to 65535. It takes decimals, for fine tuning: 65.406. |
volume | From 0 to 255. Outside that range it is adjusted. |
waveform | SQUARE (the default), TRIANGLE, SAW, PULSE (with a 50 % duty) or SINE. |
duration | In milliseconds, from 0 to 65535. With 0, the default, it sounds until stop(). |
priority | From 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)
| Parameter | What it is |
|---|---|
volume | From 0 to 255. Outside that range it is adjusted. |
period | From 1 to 65535. The noise changes every period cycles of a 22050 Hz clock (the same at 44100): the higher, the lower it sounds. |
mode | NOISE_WHITE (the default), NOISE_PERIODIC, NOISE_SHORT or NOISE_DARK: see Constants. |
duration | In milliseconds, from 0 to 65535. With 0, it sounds until stop(4). |
priority | From 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)
| Parameter | What it is |
|---|---|
channel | From 0 to 4. |
attack | Milliseconds the note takes to rise to its volume. |
decay | Milliseconds it takes to fall to the sustain level. |
sustain | The level it stays at while the note lasts, from 0 to 255: a part of the note's volume (255 is all of it). |
release | Milliseconds 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)
| Parameter | What it is |
|---|---|
depth | From 0 to 255. With 255, the pitch goes up and down a semitone; with 0, the vibrato is off. |
rate | In tenths of a hertz, from 0 to 255: 60 is 6 wobbles per second. |
delay | Optional: 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().
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).
| Parameter | What it is |
|---|---|
id | The slot, from 0 to 31. If it already had a sound, it replaces it. |
bytes | A Bytes with the sound: one byte per sample, in signed 8-bit mono PCM. Each byte goes from -128 to 127. |
frequency | The 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)
| Parameter | What it is |
|---|---|
channel | Sound.S0 (5) or Sound.S1 (6). |
id | The slot, from 0 to 31. |
frequency | The samples per second it plays it at. With 0, those of loadSample. With double, it sounds an octave higher and lasts half as long. |
loop | true 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.jmsis 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 theBytes. - It returns
true, orfalseif the file does not exist, is larger than 65535 bytes, is not a.jmuof 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 is | Result |
|---|---|
| Playing | It jumps to ms and keeps playing. |
| Paused | It 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
msis 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()afterseekMusic()starts again from the beginning. To start from the point you sought,resumeMusic(). - It returns
true, orfalseif there is no song. With a negativems,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()andseekMusic()set the song's volume again, and the song also changes it if it has a volume command. CallsetMusicVolume()afterplayMusic().
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()andstopMusic()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
S0andS1if 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
| Constant | Value | What it is |
|---|---|---|
Sound.SQUARE | 0 | Square wave. |
Sound.TRIANGLE | 1 | Triangle wave: soft. |
Sound.SAW | 2 | Saw wave: bright. |
Sound.PULSE | 3 | Pulse wave: with tone(), at 50 %; the duty is chosen with pulse(). |
Sound.SINE | 4 | Sine wave: the purest. |
Sound.NOISE_WHITE | 0 | White noise. |
Sound.NOISE_PERIODIC | 1 | Noise that repeats a short pattern: it sounds somewhat pitched. |
Sound.NOISE_SHORT | 2 | Short-sequence noise: metallic. |
Sound.NOISE_DARK | 3 | Filtered white noise: duller. |
Sound.S0 | 5 | The first sample channel. |
Sound.S1 | 6 | The second sample channel. |
Sound.MAX_VOLUME | 255 | The highest volume. |
Sound.MAX_SAMPLES | 32 | The slots of loadSample. |
Sound.SAMPLE_RATE | 22050 or 44100 | The 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 returnfalse. - Errors are not thrown: they are returned.
trydoes not catch them, and in anifan error value counts as true: compare withtrue(see What the functions return). - A note dropped because of its priority also returns
true. fade()to 0 does not silence the channel:isPlaying()staystrueuntilstop().playMusic()andstopMusic()cut off the effects of channels 0 to 4, andplayMusic()clears their envelope and LFOs.setMusicVolume()beforeplayMusic()is lost.close()unloads the samples and the music.- Ramps only act on what is playing: on a stopped channel,
fadeand theslidefunctions do nothing, although they returntrue. 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.