5
\$\begingroup\$

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.

\$\endgroup\$
  • 1
    \$\begingroup\$ I mean, this is all look spretty straight forward. You should probably be using ES6 features, like const. \$\endgroup\$ – dwjohnston Sep 6 '18 at 2:06
  • \$\begingroup\$ @dwjohnston thanks for the tip, I had no idea that there were 'different versions' of JS. Away with var, let and const it is! \$\endgroup\$ – Max Sep 6 '18 at 21:15
  • 1
    \$\begingroup\$ @Max look into setting up ESLint (and I'd recommend the AirBnb style guide on top of it). It has helped me beyond measure with learning ES6 and coding to a very high standard \$\endgroup\$ – KayakinKoder Sep 6 '18 at 21:37
3
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ Thanks a ton for your remarks! I don't know enough about JavaScript to be able to form a quick opinion, but everything seems very useful and gives me a lot of inspiration on how to continue. About the tones/notes: the tool is meant to learn to recognize frequencies in live sound feedback situations, so I want to use frequencies that come on most GEQs. They span 6 octaves in 'easy mode' since it's easy mode, harder modes will fill the gaps in between. But if I were to make the tool for musicians, you are totally right about this, and I probably should have included this in the question! \$\endgroup\$ – Max Sep 6 '18 at 17:21
  • \$\begingroup\$ Hi, I went further with the code and mainly tried to focus on reusability. I didn't use the 'note friendly' remark since I'm trying to make it useful for sound technicians who work with a certain set of frequencies found on graphic equalisers. I tried to decently close the audio context to avoid Safari's limitation of only allowing a limited amount of open (6?) AudioContexts. Here it is, in case you want to further review the code: codereview.stackexchange.com/questions/203680/… Thanks again for the feedback you already gave! \$\endgroup\$ – Max Sep 15 '18 at 12:09
2
\$\begingroup\$

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 tonetoneGen (to make it clear that it acts as a sound source), and volumeamplifier (because that node is a component; tone.connect(volume) makes no sense).

\$\endgroup\$
  • \$\begingroup\$ You're totally right that commenting (almost) every line doesn't make sense. I was a bit insecure because I felt like I was way over my head as a first attempt, but it doesn't look that scary anymore after another day of trying to learn JavaScript. Also, I like your comments after a line of code for little remarks. And amplifier is a cool name for the volume, good suggestion! \$\endgroup\$ – Max Sep 6 '18 at 21:13
  • \$\begingroup\$ In case you are interested in following-up what I have so far - I've used your feedback: tried to use clearer names so I don't have to rely that hard on comments, and left out comments where it's clear what the code does. codereview.stackexchange.com/questions/203680/… Thanks for the feedback you already gave! \$\endgroup\$ – Max Sep 15 '18 at 12:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.