Simple WebAudioWorklet Generating Choppy Audio - javascript

I'm on Firefox 84.0.1, Windows 10, x86_64. I have a very basic WebAudioWorklet synthesiser that maps keys to the frequencies of musical notes. It is generating very choppy audio when a key is held down. This makes me think that there is not enough audio samples being queued for the speaker to play, hence the audio dropping in and out. However, in audio processing terms, I'm performing a very low-intensive task. As a result, I feel like the default Worklet setup should be able to handle this. Here is my code:
syn.js
(async() => {
let a2_hertz = 110.0;
let twelfth_root_of_two = Math.pow(2.0, 1.0 / 12.0);
let audio_cxt = new AudioContext();
await audio_cxt.audioWorklet.addModule("syn-worklet.js", {credentials: "omit"});
let audio_worklet_options = {
numberOfInputs: 0,
numberOfOutputs: 1,
outputChannelCount: [audio_cxt.destination.channelCount]
};
let audio_worklet = new AudioWorkletNode(audio_cxt, "synthesiser", audio_worklet_options);
audio_worklet.connect(audio_cxt.destination);
document.addEventListener("keydown", (evt) => {
for (let key = 0; key < 12; ++key) {
if (evt.code == "Key" + "QWERTYUIOPAS"[key]) {
audio_worklet.port.postMessage(a2_hertz * Math.pow(twelfth_root_of_two, key));
}
}
});
document.addEventListener("keyup", (evt) => {
audio_worklet.port.postMessage(0.0);
});
})();
syn-worklet.js
function angular_frequency(hertz) {
return hertz * 2 * Math.PI;
}
let OSC_TYPES = {"sine": 0, "square": 1, "triangle": 2};
function oscillator(hertz, osc_type) {
switch (osc_type) {
case OSC_TYPES.sine: {
return Math.sin(angular_frequency(hertz) * currentTime);
} break;
case OSC_TYPES.square: {
return Math.sin(angular_frequency(hertz) * currentTime) > 0.0 ? 1.0 : -1.0;
} break;
case OSC_TYPES.triangle: {
return Math.asin(Math.sin(angular_frequency(hertz) * currentTime)) * (2.0 / Math.PI);
} break;
default: {
return 0.0;
}
}
}
class Synthesiser extends AudioWorkletProcessor {
constructor() {
super();
this.hertz = 0.0;
this.port.onmessage = (evt) => {
this.hertz = evt.data;
};
}
process(inputs, outputs) {
let channels = outputs[0];
let num_samples_per_channel = channels[0].length;
for (let pcm_i = 0; pcm_i < num_samples_per_channel; ++pcm_i) {
let volume = 0.1;
let pcm_value = volume * oscillator(this.hertz, OSC_TYPES.sine);
for (let channel_i = 0; channel_i < channels.length; ++channel_i) {
channels[channel_i][pcm_i] = pcm_value;
}
}
return true;
}
}
registerProcessor("synthesiser", Synthesiser);

I think the problem is that currentTime seems to be the only thing which influences the output of your oscillator() function. But currentTime doesn't change during the invocation of the process() function.
I would recommend using currentFrame instead. It will give you an integer value which represents the currentTime in frames. If you combine that with pcm_i you get the actual index of the sample that you're processing.
const currentSample = currentFrame + pcm_i;
const currentSampleInSeconds = (currentFrame + pcm_i) / sampleRate;

Related

P5 JS - Saving an edited soundfile

let play_button = document.querySelector('#play_button');
let reverb_button = document.querySelector('#reverb_button');
let save_button = document.querySelector("#save_button");
let song, reverb, speed_slider, recorder;
let reverb_on = false;
function preload() {
song = loadSound('music/Mood.mp3');
}
function setup() {
speed_slider = createSlider(0.6, 1.5, 1, 0.1); // min, max, start, step
reverb = new p5.Reverb();
reverb.process(song, 10, 10); // 10 seconds duration, 10% decay
}
function draw() {
let val = 0;
if (reverb_on) val = 1;
reverb.drywet(val); // 1 is all reverb
song.rate(speed_slider.value());
}
function togglePlay() {
if (!song.isPlaying()) {
song.play();
play_button.innerText = "Stop"
} else {
song.pause();
play_button.innerText = "Play"
}
}
function toggleReverb() {
if (!reverb_on) {
reverb_button.innerText = 'off';
reverb_on = true;
} else {
reverb_button.innerText = 'on';
reverb_on = false
}
}
function saveSong() {
saveSound(song, 'mood')
}
Above is my code, trying to give the user an option to save the soundfile after they added different effects to it, such as reverb and speed control.
No matter what method I tried that was on the p5 documentation, including:
saveSound() -> https://p5js.org/reference/#/p5/saveSound
save() -> https://p5js.org/reference/#/p5.SoundFile/save
p5.SoundRecorder passing in the sound file itself.
None of them work and only save the original sound; doesn't save any of the 'changes' you make.

react decibel meter and use

i need to make a decibel meter
i try to use 2 libary but they wont work and they very old
i found a function that make this but it wont work as expected
first of all the issue is
inside this function i get the
" let decibelNumString = decibelNum.toString().substring(0, 2); "
which is get me the number of the decibel and make it to integear
but
i want to make the minNumber and MaxNumber as state because it should re render after the changes happen
the probme it make a lot of re render becuase every 100ms it change the numbers
i have also the current decibel so it need to be render every 100ms
and it not good for perfomance
// const [minNumber, setMinNumber] = useState(25);
// const [maxNumber, setMaxNumber] = useState(30);
let arr = [];
let volumeCallback = null;
let volumeInterval = null;
(async () => {
// Initialize
try {
const audioStream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
},
});
const audioContext = new AudioContext();
const audioSource = audioContext.createMediaStreamSource(audioStream);
const analyser = audioContext.createAnalyser();
analyser.fftSize = 512;
analyser.minDecibels = -127;
analyser.maxDecibels = 0;
analyser.smoothingTimeConstant = 0.4;
audioSource.connect(analyser);
const volumes = new Uint8Array(analyser.frequencyBinCount);
volumeCallback = () => {
analyser.getByteFrequencyData(volumes);
let volumeSum = 0;
for (const volume of volumes) volumeSum += volume;
const averageVolume = volumeSum / volumes.length;
// Value range: 127 = analyser.maxDecibels - analyser.minDecibels;
let decibelNum = (averageVolume * 100) / 127;
let decibelNumString = decibelNum.toString().substring(0, 2);
if (Number(decibelNumString) > maxNumber) {
// (maxNumber = Number(decibelNum))
// setMaxNumber((prev) => {
// return (prev = Number(decibelNumString));
// });
}
if (Number(decibelNumString) < minNumber) {
// setMinNumber((prev) => {
// return (prev = Number(decibelNumString));
// });
}
console.log(decibelNumString);
// console.log(minNumber + "min");
// console.log(maxNumber + "max");
arr.push(Number(decibelNumString));
// console.log(arr)
};
} catch (e) {
console.error("Failed to initialize volume visualizer, simulating instead...", e);
// Simulation
//TODO remove in production!
let lastVolume = 50;
volumeCallback = () => {
const volume = Math.min(Math.max(Math.random() * 100, 0.8 * lastVolume), 1.2 * lastVolume);
lastVolume = volume;
};
}
// Use
// start();
})();
const stop = () => {
clearInterval(volumeInterval);
// volumeInterval = null;
};
const start = () => {
// Updating every 100ms (should be same as CSS transition speed)
if (volumeCallback !== null && volumeInterval === null) volumeInterval = setInterval(volumeCallback, 100);
};

Dynamically creating oscillators causes memory issues in p5 Sound

Within my code (the code below is a very shortened version), I am creating oscillators (https://p5js.org/reference/#/p5.Oscillator) in draw. Eventually the audio starts crackling and being all weird, and I assume this is because the oscillators aren't being garbage collected? When looking at the Memory tab in Inspect Element, it continously goes up, and the audio goes crackly eventually.
I tried to dereference the oscillators using the two functions in my disconnect variable/function. This sets the oscillator to null after a specific amount of time I hope. I wasn't sure how to set a reference argument to null.
Maybe the p5 sound library still keeps references for them?
function createOsc(grid) {
let osc = new p5.Oscillator();
if (grids[grid].type == 1) {
osc.setType("triangle");
} else if (grids[grid].type == 2) {
osc.setType("sawtooth");
} else if (grids[grid].type == 3) {
osc.setType("square");
}
return osc;
}
function createEnv(grid) {
let env = new p5.Envelope();
env.set(grids[grid].ADSR.attack, grids[grid].vol, grids[grid].ADSR.decay, grids[grid].ADSR.sustain * grids[grid].vol, grids[grid].ADSR.release, 0);
return env;
}
let disconnect = {
list: [],
osc: function(osc, time) {
this.list.push([osc, time, 0]);
},
update: function() {
for (i = 0; i < this.list.length; i++) {
this.list[i][2] += 1 / 60.0;
if (this.list[i][2] >= this.list[i][1]) {
this.list[i][0].a.disconnect();
this.list[i][0].a = null;
this.list.splice(i, 1);
i += -1;
}
}
}
}
function draw() {
let osc = createOsc(0);
let env = createEnv(0);
osc.start();
osc.freq(420);
env.triggerAttack(osc);
env.triggerRelease(osc, env.aTime + env.dTime);
osc.stop(env.aTime + env.dTime + env.rTime);
disconnect.osc({a: osc}, env.aTime + env.dTime + env.rTime);
}
I attempted to use the PolySynth (https://p5js.org/reference/#/p5.PolySynth) object, but I couldn't figure out how to change the waveforms or anything due to lack of clear documentation.
Edit:
This new standalone code produces the same crackling audio after a while. I am also now using Millis() as suggested. Same issue with memory constantly increasing.
let disconnect = {
list: [],
osc: function(osc, time) {
this.list.push([osc, time, millis()]);
},
update: function() {
for (i = 0; i < this.list.length; i++) {
if (millis() - this.list[i][2] >= this.list[i][1]) {
this.list[i][0].a.disconnect();
this.list[i][0].a = null;
this.list.splice(i, 1);
i += -1;
}
}
}
}
function createOsc() {
let osc = new p5.Oscillator();
osc.setType("triangle");
return osc;
}
function createEnv() {
let env = new p5.Envelope();
env.set(0.1, 1, 0.1, 0.8, 0.1, 0);
return env;
}
function setup() {
frameRate(60);
}
function draw() {
disconnect.update();
print(disconnect.list.length);
if (frameCount % 2 == 0) {
let osc = createOsc();
let env = createEnv();
osc.start();
osc.freq(420);
env.triggerAttack(osc);
env.triggerRelease(osc, 0.2);
osc.stop(0.3);
disconnect.osc({a: osc}, 300);
}
}

Play sounds from array every 0.5 sec

I am building a Simon game, but I get stucked on some point.
I have the array with the currentSequence that contains random sounds from another array. I created methods and properties in order to push random sounds inthe array and play them each 0.5 seconds.
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var game = function() {
this.power = 0;
this.start = false;
this.strict = false;
this.level = 1;
this.sounds = [
"https://s3.amazonaws.com/freecodecamp/simonSound1.mp3",
"https://s3.amazonaws.com/freecodecamp/simonSound2.mp3",
"https://s3.amazonaws.com/freecodecamp/simonSound3.mp3",
"https://s3.amazonaws.com/freecodecamp/simonSound4.mp3"
];
this.wrongMatch = "https://static.mezgrman.de/downloads/wwm/falsch.mp3";
this.currentSequence = [];
this.playerSequence = [];
this.addLevel = function() {
this.level++;
}
this.getcurrentSequence = function() {
var tot = this.level;
for (var i = 0; i <= tot; i++) {
var index = getRandomInt(0, tot);
this.currentSequence.push(this.sounds[index]);
}
console.log(this.currentSequence);
}
this.playCurrentSequence = function() {
for (var i = 0; i < this.currentSequence.length; i++) {
var audio = new Audio(this.currentSequence[i]);
audio.play();
}
}
this.reset = function() {
this.power = 0;
this.start = false;
this.strict = false;
this.level = 1;
this.currentSequence = [];
this.playerSequence = [];
}
}
var simon = new game();
$(document).ready(function() {
simon.level = 4;
simon.getcurrentSequence();
simon.playCurrentSequence();
The last 3 lines are for test.
I think I should use a setInterval but now i got stuck and i can't see the solution
Where
tl;dr jsFiddle
I can see you're new, so I rewrote your code to give you better idea on how to write this stuff. Key highlights:
Use ES6 - If you're writing a game, you don't need to use 10 years old syntax
Use audio events instead of timeouts - audio emits ended event when it's done playing. See all useful events here: Media Events - MDN
Cache created audios - once you create an audio, put it in some array. You can rewind old audio using Audio.currentTime = 0
class Game {
constructor() {
this.power = 0;
this.start = false;
this.strict = false;
this.level = 1;
/** #type {Array<string>} */
this.sounds = [
"https://s3.amazonaws.com/freecodecamp/simonSound1.mp3",
"https://s3.amazonaws.com/freecodecamp/simonSound2.mp3",
"https://s3.amazonaws.com/freecodecamp/simonSound3.mp3",
"https://s3.amazonaws.com/freecodecamp/simonSound4.mp3"
];
/** #type {Array<HTMLAudioElement>} cached audios so that we don't create them every time */
this.audios = [];
this.audios.length = this.sounds.length;
/** #type {HTMLAudioElement} */
this.currentAudio = null;
/** #type {number} timeout ID returned by setTimeout*/
this.nextSoundTimeout = -1;
this.shouldPlaySounds = true;
}
startRandomSounds() {
this.shouldPlaySounds = true;
this.soundDone();
}
stopRandomSounds() {
this.shouldPlaySounds = false;
if (this.currentAudio) {
this.currentAudio.pause();
}
}
/**
* Called when sound is done playing, next sound will start after that.
*/
soundDone() {
if (this.shouldPlaySounds) {
const soundIndex = Math.floor(this.sounds.length*Math.random());
/** #type {HTMLAudioElement} */
var audio = null;
/// Audio created alread
if (this.audios[soundIndex] instanceof HTMLAudioElement) {
audio = this.audios[soundIndex];
}
/// Audio not created so create it
else {
console.info("Create audio for sound ", this.sounds[soundIndex]);
audio = new Audio(this.sounds[soundIndex]);
/// listen when audio is done
audio.addEventListener("ended", () => { console.info("Done playing"); this.soundDone(); });
this.audios[soundIndex] = audio;
}
this.currentAudio = audio;
audio.currentTime = 0;
audio.play();
}
}
}
const game = new Game();
game.startRandomSounds();
Try you function like this:
this.playCurrentSequence = function() {
for (var i = 0; i < this.currentSequence.length; i++) {
var audio = new Audio(this.currentSequence[i]);
setTimeout(function(){audio.play();}, 500); // This is the change I did.
}
}
You can use the audio ended event so it fires as soon as the previous audio file is done. This would allow you to increase the difficulty of the game with each level and speed up the game and have the next audio fire at the correct time.
this.playCurrentSequence = function(index) {
var that = this;
let audio = new Audio(this.currentSequence[index]);
audio.onended = function(){
if(that.currentSequence.length > index)
that.playCurrentSequence(++index);
}
audio.play();
}

Totally destroy an instance

I am making a game with rounds that last either 100 minutes OR the time required for totalItemAmount to reach 500.
If "addPlayer" gets triggered when the game has ended, I want to push that player to next round.
I want to be able to destroy it/create new ones flawlessly.
First game always runs well.
Following games go wrong. To sum it up, I feel like the first game ran isn't being properly destroyed after it ends. Obviously, my code is much much larger than what you see down here, but let me ask you, is there any problem with my structure?
function Game(duration, playersForThisRound) {
console.log('NEW GAME')
this.playersForNextRound = [];
this.id = Math.random();
this.finished = 0;
this.players = {};
this.duration = 100;
this.totalItemAmount = 0;
this.endTimeout = setTimeout(() => {
this.end(this);
return
}, this.duration * 60 * 1000);
if (playersForThisRound && playersForThisRound.length > 0) {
Object.keys(this.playersForNextRound).forEach(function(key) {
this.addPlayer(this, key.player, key.items)
});
}
return
}
Game.prototype.addPlayer = function(game, player) {
if (this.finished || this.totalItemAmount >= 500) {
var playerIsInNextRound = 0;
Object.keys(this.playersForNextRound).forEach(function(key) {
if (key.id == player.id) {
playerIsInNextRound = 1
key.items.push(items)
}
})
if (!playerIsInNextRound)
this.playersForNextRound.push({
"id": player.id,
"player": player,
"items": items
});
if (game.totalItemAmount >= 500) {
clearTimeout(game.endTimeout)
this.end(this);
}
return
}
if (!game.players[player.id]) {
game.players[player.id] = new Player(game, player, items);
game.players[player.id].addItems(game, items, function(added) {
game.jackpot += added;
Object.keys(game.players).forEach(function(player) {
game.players[player].chance = game.players[player].getChance(game.jackpot)
});
})
}
Game.prototype.end = function(game) {
game.finished = 1;
clearTimeout(game.endTimeout)
setTimeout(() => {
game = new Game(10, this.playersForNextRound);
}, 1000 * 60 * 10);
}
//To start first game
var game = new Game();
module.exports = game;
Now in another file:
let game = require("./lib/games/roundbased.js");
let test = setInterval(() => {
for (var i = 0; i < 100; i++) {
game.addPlayer(game, {
id: '12345' + i,
nick: "player" + i
},

Categories

Resources