Problem
This is a random frequency audio generator made with the Web Audio API by Mozilla. The end-goal is an audio frequency trainer: the website plays an audio frequency and the user tries to guess which frequency it is. The frequencies will be limited to those found on 31 band graphical equalisers, and I’ll include multiple difficulties.
This is my first step: the generator for the ‘easy’ difficulty (4 frequencies).
// create web audio api context
var audioCtx = new AudioContext();
// create oscillator (tone) and gain (volume) node
var tone = audioCtx.createOscillator();
var volume = audioCtx.createGain();
// create array of frequency values
var frequencies = ["100", "400", "1600", "6300"];
// pick a random frequency
var frequency = frequencies[Math.floor(Math.random() * frequencies.length)];
// set oscillator type (sine, square, sawtooth or triangle)
tone.type = 'sine';
// set oscillator frequency in Hz
tone.frequency.value = frequency;
// set gain volume (above 1 will clip)
volume.gain.value = 0.5;
// set tone destination to go through volume
tone.connect(volume);
// set volume destination to output
volume.connect(audioCtx.destination);
// start oscillator x seconds after timestamp
tone.start(1);
// stop oscillator x seconds after timestamp
tone.stop(4);
Don’t mind the excessive commentation, I’ve only just started using JavaScript and don’t trust myself to remember what every line of code does after a few days.
Solution
Reusable code
You code is not very flexible, and the frequencies somewhat arbitrary and spanning a large 6 octaves.
You can encapsulate the code in a function that creates an object you use to play the random tone as needed, allowing you to add features as you need.
Note friendly
Consider using standard musical notes rather than random frequencies.
To get the frequency of a note you use the expression
freq = 2 ** (note / 12) * 440;
where note is the number of semi tones from A4 440Hz. So to find C4 which is 3 notes up 2 ** (3 / 12) * 440;
To get the closest note to a frequency you use
note = Math.round(Math.log2(freq / 440) * 12);
Basic example
The example creates that object randomNotes
and plays musical notes rather than random tones.
const randomNotes= (() => {
/* Create the objects needed to play a tone */
const audio = new AudioContext();
const volume = audio.createGain();
volume.gain.value = 0.5;
volume.connect(audio.destination);
/* Function to calculate frequency of a note starting at A4 and stepping semi tones*/
const freq = note => 2 ** (note / 12) * 440; // 440 is the frequency of A4
const randomNote = () => notes[Math.random() * notes.length | 0]; // the bitwise Or does the same as Math.floor
const notes = [-25, -2, 22, 46]; // Close to your 100, 400, 1600 and 6300
/* Create and return the object that is the randomNote */
return {
play(startIn = 1, playFor = 4) { // startIn and playFor is time in seconds
const now = audio.currentTime;
const oscillator = audio.createOscillator();
oscillator.type = "sine";
oscillator.connect(volume);
oscillator.frequency.value = freq(randomNote());
oscillator.start(now + startIn);
oscillator.stop(now + startIn + playFor);
}
};
})();
Then to play a random note just call
randomNotes.play();
or playing two tones
randomNotes.play();
setTimeout(randomNotes.play, 6000);
or overlapping tones
randomNotes.play();
setTimeout(randomNotes.play, 3000); // starts half way through the first
Reusing the code
The Advantages of using this approch is that it becomes very easy to extend the functionality. Say you want to add volume control, or be able to add notes to the random list, just add a setter to the object returned,
// from above snippet
/* Create and return the object that is the randomNote */
return {
set volume(value) { volume.gain.value = value },
set note(value) { notes.push(value) }
// continue on from above snippet
play(startIn = 1, playFor = 4) { ...
You can then set the volume in a more abstract way and add notes to the random set with ease.
randomNotes.note = 12; // Add note A5 to the random set
randomNotes.volume = 0.8; // Increase the volume
More sound.
You will notice that there is a new oscillator created each time, and that the audio context’s current time continues to tick. It is much more efficient to create one oscillator and change its frequency as needed and using timers to change the volume to start stop the sound. An oscillator can not restart after it has been stopped.
But this is not the best either as the audio context can chew some serious power and CPU even when not playing sound (On some audio hardware). Ideally you suspend the audio context and resume it as needed, in conjunction to using the gain control to start and stop the oscillator, as the audio context can sometimes be a little slow when suspending .
Personally I would create this type of thing using an audio buffer and just write the waveform directly to the buffer then play it.
You said…
Don’t mind the excessive commentation, I’ve only just started using JavaScript and don’t trust myself to remember what every line of code does after a few days.
But commenting every line is a bad habit for a beginner to form. Some of the comments are really very redundant and annoying:
// create web audio api context var audioCtx = new AudioContext(); // create oscillator (tone) and gain (volume) node var tone = audioCtx.createOscillator(); var volume = audioCtx.createGain();
Comments should be used judiciously. I think that this amount of commenting would be plenty, even for a beginner:
// Pick one of these frequencies (in Hz) randomly
var frequencies = ["100", "400", "1600", "6300"];
var frequency = frequencies[(Math.random() * frequencies.length) | 0];
var audioCtx = new AudioContext();
var toneGen = audioCtx.createOscillator();
toneGen.type = 'sine'; // could be sine, square, sawtooth or triangle
toneGen.frequency.value = frequency;
var amplifier = audioCtx.createGain();
amplifier.gain.value = 0.5; // setting gain above 1 would clip
// Connect nodes: toneGen -> amplifier -> output
toneGen.connect(amplifier);
amplifier.connect(audioCtx.destination);
// Start playing tone at 1 sec, stop at 2 sec
toneGen.start(1);
toneGen.stop(2);
For clarity, I’ve renamed tone
→ toneGen
(to make it clear that it acts as a sound source), and volume
→ amplifier
(because that node is a component; tone.connect(volume)
makes no sense).