4
\$\begingroup\$

I'm making an audio frequency trainer as a learning exercise. It could be useful for audio technicians who need to recognize audio frequencies (eg. when ringing out feedback during a live concert or soundcheck).

A few days ago I posted the humble beginnings. I got very useful feedback and went further. Here is a working example on JSFiddle & here is the full code on GitHub. In case anyone prefers the devtools in their own browser, here is a working example online.

Before I continue adding more functions, I would like to know what you guys think of this so far. Any feedback is welcome!

let toneContext = null;
let toneGenerator = null;
let toneAmplifier = null;

function startFrequencyTrainer(difficultyMode, previousFrequency) {
    let frequencies = null;
    let frequency = null;

    // Create objects
    toneContext = new(window.AudioContext || window.webkitAudioContext)();
    toneAmplifier = toneContext.createGain();

    // Pick a frequency
    frequencies = getFrequencies(difficultyMode);
    frequency = getNewFrequency(frequencies, previousFrequency);

    return {
        frequencies,
        frequency
    };
}

function stopFrequencyTrainer() {
    toneContext.close();
}

function startToneGenerator(frequency, volumeControl, startTimer, stopTimer) {
    // Create and configure the oscillator
    toneGenerator = toneContext.createOscillator();
    toneGenerator.type = 'sine'; // could be sine, square, sawtooth or triangle
    toneGenerator.frequency.value = frequency;

    // Connect toneGenerator -> toneAmplifier -> output
    toneGenerator.connect(toneAmplifier);
    toneAmplifier.connect(toneContext.destination);

    // Set the gain volume
    toneAmplifier.gain.value = volumeControl.value / 100;

    // Fire up the toneGenerator
    toneGenerator.start(toneContext.currentTime + startTimer);
    toneGenerator.stop(toneContext.currentTime + startTimer + stopTimer);
}

function stopToneGenerator() {
    if (toneGenerator) {
        toneGenerator.disconnect();
    }
}

function changeVolume(volumeControl) {
    toneAmplifier.gain.value = volumeControl.value / 100;
}

function getFrequencies(difficultyMode) {
    let frequencies = null;

    if (difficultyMode === 'easy') {
        frequencies = ["250", "800", "2500", "8000"];
    } else if (difficultyMode === 'normal') {
        frequencies = ["100", "200", "400", "800", "1600", "3150", "6300", "12500"];
    } else if (difficultyMode === 'hard') {
        frequencies = ["80", "125", "160", "250", "315", "500", "630", "1000", "1250", "2000", "2500", "4000", "5000", "8000", "10000", "16000"];
    } else if (difficultyMode === 'pro') {
        frequencies = ["20", "25", "31.5", "40", "50", "63", "80", "100", "125", "160", "200", "250", "315", "400", "500", "630", "800", "1000", "1250", "1600", "2000", "2500", "3150", "4000", "5000", "6300", "8000", "10000", "12500", "16000", "20000"];
    }

    return frequencies;
}

function getNewFrequency(frequencies, previousFrequency) {
    let newFrequency = null;

    newFrequency = frequencies[Math.floor(Math.random() * frequencies.length)];
    // Avoid getting the same frequency twice in a row
    while (newFrequency === previousFrequency) {
        newFrequency = frequencies[Math.floor(Math.random() * frequencies.length)];
    }

    return newFrequency;
}

function frequencyFormatter(frequency) {
    let frequencyFormatted = null;

    if (frequency > 999) {
        frequencyFormatted = frequency / 1000 + ' k';
    } else {
        frequencyFormatted = frequency + ' ';
    }

    return frequencyFormatted;
}
<!DOCTYPE html>
<html lang="en">
<head>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat:900" />
</head>
<body>
    <div class="body">
        <div class="title">
            <h1>Frequency Trainer</h1>
        </div>
        <div class="controls">
            <br />
            <button type="button" id="start-button" class="control-button">Start</button>
            <button type="button" id="stop-button" class="control-button">Stop</button>
            <button type="button" id="next-button" class="control-button">Next</button><br />
            <br />
            Volume:<br />
            <input type="range" id="volume-control" class="volume-control" min="0" max="20" value="2" step="0.1" /><br />
            <br />
            <button type="button" id="difficulty-easy" class="difficulty-button" data-difficulty="easy">Easy</button>
            <button type="button" id="difficulty-normal" class="difficulty-button" data-difficulty="normal">Normal</button>
            <button type="button" id="difficulty-hard" class="difficulty-button" data-difficulty="hard">Hard</button>
            <button type="button" id="difficulty-pro" class="difficulty-button" data-difficulty="pro">Pro</button><br />
            <br />
        </div>
        <div class="grid">
        </div>
    </div>

    <script>
    (function () {
        let difficultyMode = 'easy'; // default difficulty mode
        let frequencyTrainer = startFrequencyTrainer(difficultyMode, null);
        let frequency = frequencyTrainer.frequency;
        let frequencyContainers = null;

        // Control buttons
        let startButton = document.getElementById('start-button');
        startButton.onclick = function () {
            stopToneGenerator();
            startToneGenerator(frequency, volumeControl, 0, 3);
        };
        let stopButton = document.getElementById('stop-button');
        stopButton.onclick = function () {
            stopToneGenerator();
        };
        let nextButton = document.getElementById('next-button');
        nextButton.onclick = function () {
            stopToneGenerator();
            stopFrequencyTrainer();
            frequency = startFrequencyTrainer(difficultyMode, frequency).frequency;
            startToneGenerator(frequency, volumeControl, 0.05, 3);
        };
        let volumeControl = document.getElementById('volume-control');
        volumeControl.oninput = function () {
            changeVolume(volumeControl);
        };

        function fillFrequencyGrid(frequencies) {
            let frequencyFormatted = null;
            let frequencyGrid = document.getElementsByClassName('grid')[0];

            frequencyGrid.innerHTML = '';
            frequencies.forEach(function (frequency) {
                frequencyFormatted = frequencyFormatter(frequency);
                frequencyGrid.insertAdjacentHTML('beforeend', '<div class="frequency-container" data-frequency="' + frequency + '">' + frequencyFormatted + 'Hz</div>');
            });
        }

        function makeFrequencyGridInteractive() {
            frequencyContainers = document.getElementsByClassName('frequency-container');
            Array.prototype.forEach.call(frequencyContainers, function (frequencyContainer) {
                frequencyContainer.onclick = function () {
                    let frequencyChosen = frequencyContainer.getAttribute('data-frequency');
                    let frequencyChosenFormatted = frequencyFormatter(frequencyChosen);

                    stopToneGenerator();
                    if (frequencyChosen === frequency) {
                        if (window.confirm(frequencyChosenFormatted + 'Hz is correct!\nLet\'s try another one!')) {
                            stopFrequencyTrainer();
                            frequency = startFrequencyTrainer(difficultyMode, frequency).frequency;
                            startToneGenerator(frequency, volumeControl, 0.05, 3);
                        }
                    } else {
                        window.alert(frequencyChosenFormatted + 'Hz is not correct.\nPlease try again.');
                        startToneGenerator(frequency, volumeControl, 0.05, 3);
                    }
                };
            });
        }

        // Generate frequency grid
        fillFrequencyGrid(frequencyTrainer.frequencies);
        makeFrequencyGridInteractive();

        // Difficulty buttons
        let difficultyButtons = document.getElementsByClassName('difficulty-button');
        Array.prototype.forEach.call(difficultyButtons, function (difficultyButton) {
            difficultyButton.onclick = function () {
                stopToneGenerator();
                stopFrequencyTrainer();
                difficultyMode = difficultyButton.getAttribute('data-difficulty');
                frequencyTrainer = startFrequencyTrainer(difficultyMode, frequency);
                frequency = frequencyTrainer.frequency;
                fillFrequencyGrid(frequencyTrainer.frequencies);
                makeFrequencyGridInteractive();
            };
        });
    }());
    </script>
</body>
</html>
\$\endgroup\$
1
+50
\$\begingroup\$

General Feedback

This looks like a neat little program. I admit I hadn't explored the audio APIs before reading your first post and as a musician I enjoy seeing technology connect audio and visual elements. The colors on the frequency contains is a nice array. Good work! There are a few things mentioned below that can help optimize memory as well as the coding style. The feedback comes both from my experience as well as reading articles like this one.

Suggestions

Event delegation

Use event delegation instead of manually setting the onclick handlers.

For example, instead of assigning a callback function to the onclick attribute of each frequency button, use EventTarget.addEventListener() for the click event, and inspect event.target to see if it has the class frequency-container:

frequencyGrid.addEventListener('click', function(event) {
    const target = event.target;
    if (target.classList.contains('frequency-container')) {
        let frequencyChosen = target.getAttribute('data-frequency');

Not only does it allow the click handler to be set once on a container but also it can avoid memory leaks (since the frequencies can be removed from the DOM). See answers to DOM: why is this a memory leak? for more information.

Strict Mode

Many of the functions in the javascript section have 'use strict'; at the first line, while the IIFE on the page has that directive at the start. Why not make the Javascript section more like the IIFE?

Use const as default instead of let

It is advisable to use const instead of let for any variable that doesn't need to be re-assigned. Then when you determine re-assignment is necessary, use let. That way there will be less chance of overwriting a value.

On that same topic, there is little point in assigning a value of null shortly before assigning an actual value - e.g. the following lines come in startFrequencyTrainer():

let frequencies = null;
let frequency = null;

// Create objects
toneContext = new(window.AudioContext || window.webkitAudioContext)();
toneAmplifier = toneContext.createGain();

// Pick a frequency
frequencies = getFrequencies(difficultyMode);
frequency = getNewFrequency(frequencies, previousFrequency);

Instead frequencies and frequency can be declared as a const when assigned.

// Create objects
toneContext = new(window.AudioContext || window.webkitAudioContext)();
toneAmplifier = toneContext.createGain();

// Pick a frequency
const frequencies = getFrequencies(difficultyMode);
const frequency = getNewFrequency(frequencies, previousFrequency);

The same is true for newFrequency in getNewFrequency():

let newFrequency = null;
newFrequency = frequencies[Math.floor(Math.random() * frequencies.length)];

This can be simplified to a single line, since there is no point in the value being null before it is assigned:

let newFrequency = frequencies[Math.floor(Math.random() * frequencies.length)];

Use spread operator

Use the spread operator for things like putting the HTMLElement collections into an array instead of using Array.prototype.forEach.call.

So lines like these:

let difficultyButtons = document.getElementsByClassName('difficulty-button');
Array.prototype.forEach.call(difficultyButtons, function (difficultyButton) {

Can be changed to this:

let difficultyButtons = document.getElementsByClassName('difficulty-button');
[...difficultyButtons].forEach( function (difficultyButton) {

Selecting frequencies by difficulty

There isn't anything wrong with the function getFrequencies() but it could be shortened by declaring a mapping outside the function:

const frequenciesByDifficulty = {
'easy': ["250", "800", "2500", "8000"],
  'normal': ["100", "200", "400", "800", "1600", "3150", "6300", "12500"],
  'hard': ["80", "125", "160", "250", "315", "500", "630", "1000", "1250", "2000", "2500", "4000", "5000", "8000", "10000", "16000"],
  'pro': ["20", "25", "31.5", "40", "50", "63", "80", "100", "125", "160", "200", "250", "315", "400", "500", "630", "800", "1000", "1250", "1600", "2000", "2500", "3150", "4000", "5000", "6300", "8000", "10000", "12500", "16000", "20000"]
};

Then that function can simply look up frequencies from that mapping:

function getFrequencies(difficultyMode) {
    if (difficultyMode in frequenciesByDifficulty) {
      return frequenciesByDifficulty[difficultyMode];
    }
    //fallback
    return null;
}

That way the frequencies can be altered without having to modify the function.

formatter function

This is probably just a personal preference - the function formatterFunction() could be simplified by removing frequencyFormatted and simply return the values as soon as they are ready:

function frequencyFormatter(frequency) {
    if (frequency > 999) {
        return frequency / 1000 + ' k';
    } 
    return frequency + ' ';    
}

And if you wanted, that could be simplifed using an arrow function (as could all functions):

const frequencyFormatter = frequency => frequency > 999 ? frequency / 1000 + ' k' : frequency + ' ';

Use a function partial for the volume change callback

Use Function.bind() to create a partially applied function for the oninput callback:

volumeControl.oninput = changeVolume.bind(null, volumeControl);

Updated code

See snippet below. I didn't change all the functions to arrow functions but could...

'use strict';
let toneContext = null;
let toneGenerator = null;
let toneAmplifier = null;

function startFrequencyTrainer(difficultyMode, previousFrequency) {
  // Create objects
  toneContext = new(window.AudioContext || window.webkitAudioContext)();
  toneAmplifier = toneContext.createGain();

  // Pick a frequency
  const frequencies = getFrequencies(difficultyMode);
  const frequency = getNewFrequency(frequencies, previousFrequency);

  return {
    frequencies,
    frequency
  };
}

function stopFrequencyTrainer() {
  toneContext.close();
}

function startToneGenerator(frequency, volumeControl, startTimer, stopTimer) {
  // Create and configure the oscillator
  toneGenerator = toneContext.createOscillator();
  toneGenerator.type = 'sine'; // could be sine, square, sawtooth or triangle
  toneGenerator.frequency.value = frequency;

  // Connect toneGenerator -> toneAmplifier -> output
  toneGenerator.connect(toneAmplifier);
  toneAmplifier.connect(toneContext.destination);

  // Set the gain volume
  toneAmplifier.gain.value = volumeControl.value / 100;

  // Fire up the toneGenerator
  toneGenerator.start(toneContext.currentTime + startTimer);
  toneGenerator.stop(toneContext.currentTime + startTimer + stopTimer);
}

function stopToneGenerator() {
  if (toneGenerator) {
    toneGenerator.disconnect();
  }
}

function changeVolume(volumeControl) {
  toneAmplifier.gain.value = volumeControl.value / 100;
}
const frequenciesByDifficulty = {
  'easy': ["250", "800", "2500", "8000"],
  'normal': ["100", "200", "400", "800", "1600", "3150", "6300", "12500"],
  'hard': ["80", "125", "160", "250", "315", "500", "630", "1000", "1250", "2000", "2500", "4000", "5000", "8000", "10000", "16000"],
  'pro': ["20", "25", "31.5", "40", "50", "63", "80", "100", "125", "160", "200", "250", "315", "400", "500", "630", "800", "1000", "1250", "1600", "2000", "2500", "3150", "4000", "5000", "6300", "8000", "10000", "12500", "16000", "20000"]
};

function getFrequencies(difficultyMode) {
  if (difficultyMode in frequenciesByDifficulty) {
    return frequenciesByDifficulty[difficultyMode];
  }
  //fallback
  return null;
}

function getNewFrequency(frequencies, previousFrequency) {
  let newFrequency = frequencies[Math.floor(Math.random() * frequencies.length)];
  // Avoid getting the same frequency twice in a row
  while (newFrequency === previousFrequency) {
    newFrequency = frequencies[Math.floor(Math.random() * frequencies.length)];
  }
  return newFrequency;
}

function frequencyFormatter(frequency) {
  if (frequency > 999) {
    return frequency / 1000 + ' k';
  }
  return frequency + ' ';
}

(function() {
  'use strict';
  let difficultyMode = 'easy'; // default difficulty mode
  let frequencyTrainer = startFrequencyTrainer(difficultyMode);
  let frequency = frequencyTrainer.frequency;
  let frequencyContainers = null;
  const frequencyGrid = document.getElementsByClassName('grid')[0];
  const controls = document.getElementsByClassName('controls')[0];

  // Control buttons
  const startButton = document.getElementById('start-button');
  startButton.onclick = function() {
    stopToneGenerator();
    startToneGenerator(frequency, volumeControl, 0, 3);
  };
  const stopButton = document.getElementById('stop-button');
  stopButton.onclick = function() {
    stopToneGenerator();
  };
  const nextButton = document.getElementById('next-button');
  nextButton.onclick = function() {
    stopToneGenerator();
    stopFrequencyTrainer();
    frequency = startFrequencyTrainer(difficultyMode, frequency).frequency;
    startToneGenerator(frequency, volumeControl, 0.05, 3);
  };
  let volumeControl = document.getElementById('volume-control');
  volumeControl.oninput = changeVolume.bind(null, volumeControl);

  function fillFrequencyGrid(frequencies) {
    let frequencyFormatted = null;

    frequencyGrid.innerHTML = '';
    frequencies.forEach(function(frequency) {
      frequencyFormatted = frequencyFormatter(frequency);
      frequencyGrid.insertAdjacentHTML('beforeend', '<div class="frequency-container" data-frequency="' + frequency + '">' + frequencyFormatted + 'Hz</div>');
    });
  }
  frequencyGrid.addEventListener('click', function(event) {
    const target = event.target;
    if (target.classList.contains('frequency-container')) {
      let frequencyChosen = target.getAttribute('data-frequency');
      let frequencyChosenFormatted = frequencyFormatter(frequencyChosen);

      stopToneGenerator();
      if (frequencyChosen === frequency) {
        if (window.confirm(frequencyChosenFormatted + 'Hz is correct!\nLet\'s try another one!')) {
          stopFrequencyTrainer();
          frequency = startFrequencyTrainer(difficultyMode, frequency).frequency;
          startToneGenerator(frequency, volumeControl, 0.05, 3);
        }
      } else {
        window.alert(frequencyChosenFormatted + 'Hz is not correct.\nPlease try again.');
        startToneGenerator(frequency, volumeControl, 0.05, 3);
      }
    }
  })


  // Generate frequency grid
  fillFrequencyGrid(frequencyTrainer.frequencies);

  // Difficulty buttons
  controls.addEventListener('click', event => {
    if (event.target.classList.contains('difficulty-button')) {
      event.stopPropagation();
      stopToneGenerator();
      stopFrequencyTrainer();
      difficultyMode = event.target.getAttribute('data-difficulty');
      frequencyTrainer = startFrequencyTrainer(difficultyMode, frequency);
      frequency = frequencyTrainer.frequency;
      fillFrequencyGrid(frequencyTrainer.frequencies);
    }
  }, false);
}());
body {
  font-family: 'Montserrat', sans-serif;
  text-align: center;
  padding-top: 10px;
}

h1 {
  margin: 0 auto;
  font-size: 30px;
  text-decoration: underline;
}

h2 {
  margin: 0;
  font-size: 25px;
}

a {
  color: #0000BB;
}

a:hover {
  color: #000000;
}

button {
  font-family: 'Montserrat', sans-serif;
  text-align: center;
  font-size: calc(10px + 1vw);
}

.body {
  max-width: 1500px;
  border: 1px solid black;
  width: 95%;
  margin: 0 auto;
}

.title {
  padding: 10px 0 0 0;
  margin: 0 auto;
  width: 95%;
}

.content {
  padding: 30px 0 0 0;
  margin: 0 auto;
  width: 95%;
}

.controls {
  padding: 0;
  margin: 0 auto;
  width: 95%;
}

.volume-control {
  padding: 0;
  margin: 0 auto;
  min-width: 200px;
  width: 80%;
}

.footer {
  padding: 20px 0 10px 0;
  margin: 0 auto;
  width: 95%;
}

.grid {
  margin: 0 auto;
  width: 95%;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
}

.frequency-container {
  margin: 2px;
  border: 1px solid black;
  padding: 0;
  min-width: 80px;
  min-height: 80px;
  max-width: 300px;
  max-height: 300px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: calc(30px + 0.2vw);
  text-shadow: 0 0 25px white;
}

.frequency-container:before {
  content: '';
  padding-top: 100%;
  float: left;
}

[data-frequency="20"] {
  background: #CC2828;
}

[data-frequency="25"] {
  background: #CC3028;
}

[data-frequency="31.5"] {
  background: #CC3928;
}

[data-frequency="40"] {
  background: #CC4128;
}

[data-frequency="50"] {
  background: #CC4928;
}

[data-frequency="63"] {
  background: #CC5128;
}

[data-frequency="80"] {
  background: #CC5928;
}

[data-frequency="100"] {
  background: #CC6128;
}

[data-frequency="125"] {
  background: #CC6A28;
}

[data-frequency="160"] {
  background: #CC7228;
}

[data-frequency="200"] {
  background: #CC7A28;
}

[data-frequency="250"] {
  background: #CC8228;
}

[data-frequency="315"] {
  background: #CC8A28;
}

[data-frequency="400"] {
  background: #CC9228;
}

[data-frequency="500"] {
  background: #CC9B28;
}

[data-frequency="630"] {
  background: #CCAB28;
}

[data-frequency="800"] {
  background: #CCBB28;
}

[data-frequency="1000"] {
  background: #CCCC28;
}

[data-frequency="1250"] {
  background: #BBCC28;
}

[data-frequency="1600"] {
  background: #ABCC28;
}

[data-frequency="2000"] {
  background: #9BCC28;
}

[data-frequency="2500"] {
  background: #8ACC28;
}

[data-frequency="3150"] {
  background: #7ACC28;
}

[data-frequency="4000"] {
  background: #6ACC28;
}

[data-frequency="5000"] {
  background: #59CC28;
}

[data-frequency="6300"] {
  background: #49CC28;
}

[data-frequency="8000"] {
  background: #39CC28;
}

[data-frequency="10000"] {
  background: #28CC28;
}

[data-frequency="12500"] {
  background: #28CC39;
}

[data-frequency="16000"] {
  background: #28CC49;
}

[data-frequency="20000"] {
  background: #28CC59;
}
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat:900" />
<div class="body">
  <div class="title">
    <h1>Frequency Trainer</h1>
  </div>
  <div class="controls">
    <br />
    <button type="button" id="start-button" class="control-button">Start</button>
    <button type="button" id="stop-button" class="control-button">Stop</button>
    <button type="button" id="next-button" class="control-button">Next</button><br />
    <br /> Volume:
    <br />
    <input type="range" id="volume-control" class="volume-control" min="0" max="20" value="2" step="0.1" /><br />
    <br />
    <button type="button" id="difficulty-easy" class="difficulty-button" data-difficulty="easy">Easy</button>
    <button type="button" id="difficulty-normal" class="difficulty-button" data-difficulty="normal">Normal</button>
    <button type="button" id="difficulty-hard" class="difficulty-button" data-difficulty="hard">Hard</button>
    <button type="button" id="difficulty-pro" class="difficulty-button" data-difficulty="pro">Pro</button><br />
    <br />
  </div>
  <div class="grid">
  </div>
  <div class="footer">
    <a href="https://github.com/MaxVMH/frequency-trainer/tree/v.0.0.4-alpha">v.0.0.4</a>
  </div>
</div>

Or with Arrow functions:

'use strict';
let toneContext = null;
let toneGenerator = null;
let toneAmplifier = null;

const startFrequencyTrainer = (difficultyMode, previousFrequency) => {
  // Create objects
  toneContext = new(window.AudioContext || window.webkitAudioContext)();
  toneAmplifier = toneContext.createGain();

  // Pick a frequency
  const frequencies = getFrequencies(difficultyMode);
  const frequency = getNewFrequency(frequencies, previousFrequency);

  return {
    frequencies,
    frequency
  };
}

const stopFrequencyTrainer = _ => toneContext.close();

const startToneGenerator = (frequency, volumeControl, startTimer, stopTimer) => {
  // Create and configure the oscillator
  toneGenerator = toneContext.createOscillator();
  toneGenerator.type = 'sine'; // could be sine, square, sawtooth or triangle
  toneGenerator.frequency.value = frequency;

  // Connect toneGenerator -> toneAmplifier -> output
  toneGenerator.connect(toneAmplifier);
  toneAmplifier.connect(toneContext.destination);

  // Set the gain volume
  toneAmplifier.gain.value = volumeControl.value / 100;

  // Fire up the toneGenerator
  toneGenerator.start(toneContext.currentTime + startTimer);
  toneGenerator.stop(toneContext.currentTime + startTimer + stopTimer);
}

const stopToneGenerator = _ => {
  if (toneGenerator) {
    toneGenerator.disconnect();
  }
}

const changeVolume = volumeControl => toneAmplifier.gain.value = volumeControl.value / 100;

const frequenciesByDifficulty = {
  'easy': ["250", "800", "2500", "8000"],
  'normal': ["100", "200", "400", "800", "1600", "3150", "6300", "12500"],
  'hard': ["80", "125", "160", "250", "315", "500", "630", "1000", "1250", "2000", "2500", "4000", "5000", "8000", "10000", "16000"],
  'pro': ["20", "25", "31.5", "40", "50", "63", "80", "100", "125", "160", "200", "250", "315", "400", "500", "630", "800", "1000", "1250", "1600", "2000", "2500", "3150", "4000", "5000", "6300", "8000", "10000", "12500", "16000", "20000"]
};
const getFrequencies = difficultyMode => {
  if (difficultyMode in frequenciesByDifficulty) {
    return frequenciesByDifficulty[difficultyMode];
  }
  //fallback
  return null;
}

const getNewFrequency = (frequencies, previousFrequency) => {
  let newFrequency = frequencies[Math.floor(Math.random() * frequencies.length)];
  // Avoid getting the same frequency twice in a row
  while (newFrequency === previousFrequency) {
    newFrequency = frequencies[Math.floor(Math.random() * frequencies.length)];
  }
  return newFrequency;
}

const frequencyFormatter = frequency => {
  if (frequency > 999) {
    return frequency / 1000 + ' k';
  }
  return frequency + ' ';
}

(function() {
  'use strict';
  let difficultyMode = 'easy'; // default difficulty mode
  let frequencyTrainer = startFrequencyTrainer(difficultyMode);
  let frequency = frequencyTrainer.frequency;
  let frequencyContainers = null;
  const frequencyGrid = document.getElementsByClassName('grid')[0];
  const controls = document.getElementsByClassName('controls')[0];

  // Control buttons
  const startButton = document.getElementById('start-button');
  startButton.onclick = function() {
    stopToneGenerator();
    startToneGenerator(frequency, volumeControl, 0, 3);
  };
  const stopButton = document.getElementById('stop-button');
  stopButton.onclick = function() {
    stopToneGenerator();
  };
  const nextButton = document.getElementById('next-button');
  nextButton.onclick = function() {
    stopToneGenerator();
    stopFrequencyTrainer();
    frequency = startFrequencyTrainer(difficultyMode, frequency).frequency;
    startToneGenerator(frequency, volumeControl, 0.05, 3);
  };
  let volumeControl = document.getElementById('volume-control');
  volumeControl.oninput = changeVolume.bind(null, volumeControl);

  const fillFrequencyGrid = frequencies => {
    frequencyGrid.innerHTML = '';
    frequencies.forEach(function(frequency) {
      const frequencyFormatted = frequencyFormatter(frequency);
      frequencyGrid.insertAdjacentHTML('beforeend', '<div class="frequency-container" data-frequency="' + frequency + '">' + frequencyFormatted + 'Hz</div>');
    });
  };
  frequencyGrid.addEventListener('click', event => {
    const target = event.target;
    if (target.classList.contains('frequency-container')) {
      event.stopPropagation();
      let frequencyChosen = target.getAttribute('data-frequency');
      let frequencyChosenFormatted = frequencyFormatter(frequencyChosen);

      stopToneGenerator();
      if (frequencyChosen === frequency) {
        if (window.confirm(frequencyChosenFormatted + 'Hz is correct!\nLet\'s try another one!')) {
          stopFrequencyTrainer();
          frequency = startFrequencyTrainer(difficultyMode, frequency).frequency;
          startToneGenerator(frequency, volumeControl, 0.05, 3);
        }
      } else {
        window.alert(frequencyChosenFormatted + 'Hz is not correct.\nPlease try again.');
        startToneGenerator(frequency, volumeControl, 0.05, 3);
      }
    }
  }, true);


  // Generate frequency grid
  fillFrequencyGrid(frequencyTrainer.frequencies);
  controls.addEventListener('click', event => {
    if (event.target.classList.contains('difficulty-button')) {
      event.stopPropagation();
      stopToneGenerator();
      stopFrequencyTrainer();
      difficultyMode = event.target.getAttribute('data-difficulty');
      frequencyTrainer = startFrequencyTrainer(difficultyMode, frequency);
      frequency = frequencyTrainer.frequency;
      fillFrequencyGrid(frequencyTrainer.frequencies);
    }
  }, false);
}());
body {
  font-family: 'Montserrat', sans-serif;
  text-align: center;
  padding-top: 10px;
}

h1 {
  margin: 0 auto;
  font-size: 30px;
  text-decoration: underline;
}

h2 {
  margin: 0;
  font-size: 25px;
}

a {
  color: #0000BB;
}

a:hover {
  color: #000000;
}

button {
  font-family: 'Montserrat', sans-serif;
  text-align: center;
  font-size: calc(10px + 1vw);
}

.body {
  max-width: 1500px;
  border: 1px solid black;
  width: 95%;
  margin: 0 auto;
}

.title {
  padding: 10px 0 0 0;
  margin: 0 auto;
  width: 95%;
}

.content {
  padding: 30px 0 0 0;
  margin: 0 auto;
  width: 95%;
}

.controls {
  padding: 0;
  margin: 0 auto;
  width: 95%;
}

.volume-control {
  padding: 0;
  margin: 0 auto;
  min-width: 200px;
  width: 80%;
}

.footer {
  padding: 20px 0 10px 0;
  margin: 0 auto;
  width: 95%;
}

.grid {
  margin: 0 auto;
  width: 95%;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
}

.frequency-container {
  margin: 2px;
  border: 1px solid black;
  padding: 0;
  min-width: 80px;
  min-height: 80px;
  max-width: 300px;
  max-height: 300px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: calc(30px + 0.2vw);
  text-shadow: 0 0 25px white;
}

.frequency-container:before {
  content: '';
  padding-top: 100%;
  float: left;
}

[data-frequency="20"] {
  background: #CC2828;
}

[data-frequency="25"] {
  background: #CC3028;
}

[data-frequency="31.5"] {
  background: #CC3928;
}

[data-frequency="40"] {
  background: #CC4128;
}

[data-frequency="50"] {
  background: #CC4928;
}

[data-frequency="63"] {
  background: #CC5128;
}

[data-frequency="80"] {
  background: #CC5928;
}

[data-frequency="100"] {
  background: #CC6128;
}

[data-frequency="125"] {
  background: #CC6A28;
}

[data-frequency="160"] {
  background: #CC7228;
}

[data-frequency="200"] {
  background: #CC7A28;
}

[data-frequency="250"] {
  background: #CC8228;
}

[data-frequency="315"] {
  background: #CC8A28;
}

[data-frequency="400"] {
  background: #CC9228;
}

[data-frequency="500"] {
  background: #CC9B28;
}

[data-frequency="630"] {
  background: #CCAB28;
}

[data-frequency="800"] {
  background: #CCBB28;
}

[data-frequency="1000"] {
  background: #CCCC28;
}

[data-frequency="1250"] {
  background: #BBCC28;
}

[data-frequency="1600"] {
  background: #ABCC28;
}

[data-frequency="2000"] {
  background: #9BCC28;
}

[data-frequency="2500"] {
  background: #8ACC28;
}

[data-frequency="3150"] {
  background: #7ACC28;
}

[data-frequency="4000"] {
  background: #6ACC28;
}

[data-frequency="5000"] {
  background: #59CC28;
}

[data-frequency="6300"] {
  background: #49CC28;
}

[data-frequency="8000"] {
  background: #39CC28;
}

[data-frequency="10000"] {
  background: #28CC28;
}

[data-frequency="12500"] {
  background: #28CC39;
}

[data-frequency="16000"] {
  background: #28CC49;
}

[data-frequency="20000"] {
  background: #28CC59;
}
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat:900" />
<div class="body">
  <div class="title">
    <h1>Frequency Trainer</h1>
  </div>
  <div class="controls">
    <br />
    <button type="button" id="start-button" class="control-button">Start</button>
    <button type="button" id="stop-button" class="control-button">Stop</button>
    <button type="button" id="next-button" class="control-button">Next</button><br />
    <br /> Volume:
    <br />
    <input type="range" id="volume-control" class="volume-control" min="0" max="20" value="2" step="0.1" /><br />
    <br />
    <button type="button" id="difficulty-easy" class="difficulty-button" data-difficulty="easy">Easy</button>
    <button type="button" id="difficulty-normal" class="difficulty-button" data-difficulty="normal">Normal</button>
    <button type="button" id="difficulty-hard" class="difficulty-button" data-difficulty="hard">Hard</button>
    <button type="button" id="difficulty-pro" class="difficulty-button" data-difficulty="pro">Pro</button><br />
    <br />
  </div>
  <div class="grid">
  </div>
  <div class="footer">
    <a href="https://github.com/MaxVMH/frequency-trainer/tree/v.0.0.4-alpha">v.0.0.4</a>
  </div>
</div>

\$\endgroup\$
  • \$\begingroup\$ Thanks for the extensive feedback, I’ll have more time after the weekend to look at your review more closely so I might have some questions then but for now everything looks pretty straight-forward :) \$\endgroup\$ – Max Sep 20 '18 at 6:16
  • 1
    \$\begingroup\$ Alrighty- I expanded the section on let and const variables \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Sep 21 '18 at 17:33

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.