Back to Freecodecamp

Build a Drum Machine

curriculum/challenges/english/blocks/lab-drum-machine/6762ec275cef87635acc4fe3.md

latest15.2 KB
Original Source

--description--

Objective: Fulfill the user stories below and get all the tests to pass to complete the lab.

User Stories:

  1. You should have a div element with an id of drum-machine that contains all other elements.

  2. Inside the #drum-machine element you should have another div with an id of pad-bank.

  3. Inside the #drum-machine element you should have a p element with an id of display.

  4. Inside your #pad-bank element you should have nine clickable drum pad elements each with a class of drum-pad, a unique id that describes the audio clip the drum pad will be set up to trigger, and an inner text that corresponds to one of the following keys on the keyboard: Q, W, E, A, S, D, Z, X, C. The drum pads MUST be in this order.

  5. Each .drum-pad should have an audio element which has a class of clip, a src attribute that points to an audio clip, and an id corresponding to the inner text of its parent .drum-pad element (e.g. id="Q", id="W", id="E" etc.).

  6. When you click on a .drum-pad element, the audio clip contained in its child audio element should be triggered.

  7. When you press the trigger key associated with each .drum-pad, the audio clip contained in its child audio element should be triggered (e.g. pressing the Q key should trigger the drum pad which contains the string Q, pressing the W key should trigger the drum pad which contains the string W, etc.).

  8. When a .drum-pad is triggered, you should display a string describing the associated audio clip as the inner text of the #display element (each string must be unique).

Some audio samples you can use for your drum machine can be found at https://cdn.freecodecamp.org/curriculum/drum/<fileName>, where <fileName> is as follows:

drum namefileName
Heater 1Heater-1.mp3
Heater 2Heater-2.mp3
Heater 3Heater-3.mp3
Heater 4Heater-4_1.mp3
ClapHeater-6.mp3
Open-HHDsc_Oh.mp3
Kick-n'-HatKick_n_Hat.mp3
KickRP4_KICK_1.mp3
Closed-HHCev_H2.mp3

--hints--

You should have a div element with an id of drum-machine that contains all other elements.

js
assert.isNotNull(document.querySelector('div#drum-machine'));

Inside the #drum-machine element you should have another div with an id of pad-bank.

js
assert.isNotNull(document.querySelector('#drum-machine div#pad-bank'));

Inside the #drum-machine element you should have a p element with an id of display.

js
assert.isNotNull(document.querySelector('#drum-machine p#display'));

Inside your #pad-bank element you should have nine clickable drum pad elements each with a class of drum-pad.

js
assert.lengthOf(document.querySelectorAll('#pad-bank .drum-pad'), 9);

Each .drum-pad should be a button element.

js
const pads = document.querySelectorAll('.drum-pad');
pads.forEach(pad => {
  assert.equal(pad.tagName, 'BUTTON');
});

Each .drum-pad should have one of the following letters as innerText, in order: Q, W, E, A, S, D, Z, X, C.

js
const pads = document.querySelectorAll('.drum-pad');
const letters = [`Q`, `W`, `E`, `A`, `S`, `D`, `Z`, `X`, `C`];
for (let i = 0; i < 9; i++) {
  assert.equal(pads[i].innerText.trim(), letters[i]);
}

Each .drum-pad should have an audio element which has a class of clip, a src attribute that points to an audio clip, and an id corresponding to the inner text of its parent .drum-pad element (e.g. id="Q", id="W", id="E" etc.).

js
const letters = [`Q`, `W`, `E`, `A`, `S`, `D`, `Z`, `X`, `C`];
const audios = document.querySelectorAll('#drum-machine .drum-pad audio');

for (let i = 0; i < 9; i++) {
  assert.equal(audios[i].id, letters[i]);
  assert.isTrue(audios[i].classList.contains('clip'));
  assert.isTrue(audios[i].hasAttribute('src'));
}

When you click on a .drum-pad element, the audio clip contained in its child audio element should be triggered.

js
const pads = document.querySelectorAll('.drum-pad');
assert.isNotEmpty(pads);
pads.forEach(el => {
  const audio = el.querySelector('audio');
  el.click();
  assert.isFalse(audio.paused);
  audio.pause();
});

When you press one of the keys Q, W, E, A, S, D, Z, X, C on your keyboard, the corresponding audio element should play the corresponding sound.

js
const letters = [`Q`, `W`, `E`, `A`, `S`, `D`, `Z`, `X`, `C`];
const audios = document.querySelectorAll('#drum-machine .drum-pad audio');

for (let i = 0; i < 9; i++) {
  const audio = audios[i];
  document.dispatchEvent(
    new KeyboardEvent('keydown', { key: letters[i].toLowerCase() })
  );
  assert.isFalse(audio.paused);
  audio.pause();
}

When a .drum-pad is triggered, you should display a string describing the associated audio clip as the inner text of the #display element (each string must be unique).

js
const collection = new Set();
const disp = document.querySelector('#display');
const pads = document.querySelectorAll('.drum-pad');
assert.lengthOf(pads, 9);
pads.forEach(el => {
  const audio = el.querySelector('audio');
  el.click();
  collection.add(disp.innerText.trim());
  audio.pause();
});
assert.lengthOf(collection, 9);

--seed--

--seed-contents--

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Drum Machine</title>
  </head>
  <body></body>
</html>
css
js

--solutions--

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Drum Machine</title>
    <link rel="stylesheet" href="styles.css" />
  </head>

  <body>
    <div class="inner-container" id="drum-machine">
      <div id="pad-bank"></div>

      <div class="controls-container">
        <div class="control power-control">
          <label class="select">
            <span>Power: <span id="power-status">On</span></span>
            <span class="toggle-container">
              <input type="checkbox" id="power-toggle" checked />
              <span class="slider"></span>
            </span>
          </label>
        </div>

        <!-- Display for Drum Pad Name and Volume -->
        <p id="display"></p>
        
        <div class="volume-slider">
          <label for="volume-control">Volume</label>
          <input type="range" id="volume-control" min="0" max="1" step="0.01" value="0.3" />
        </div>

        <div class="control bank-control">
          <label class="select">
            <span>Bank: <span id="current-bank">Heater Kit</span></span>
            <span class="toggle-container">
              <input type="checkbox" id="bank-toggle" checked />
              <span class="slider"></span>
            </span>
          </label>
        </div>
      </div>
    </div>

    <script src="script.js"></script>
  </body>
</html>
css
/* General Reset */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: 'Arial', sans-serif;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background-color: #282c34;
  color: white;
}

.inner-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  background: #3b3b5f;
  border-radius: 8px;
  padding: 20px;
  width: 300px;
}

/* Drum Pad Styles */
#pad-bank {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin-bottom: 15px;
}

.drum-pad {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 70px;
  height: 70px;
  background: #666;
  border: none;
  border-radius: 5px;
  font-size: 1.2em;
  color: #fff;
  cursor: pointer;
  font-family: inherit;
}

.drum-pad:hover {
  background: #777;
}

.drum-pad:focus-visible {
  outline: 2px solid #0d6efd;
  outline-offset: 2px;
}

.drum-pad.active {
  background: #ffaa33;
  color: #000;
}

.controls-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
}

.control {
  display: flex;
  justify-content: space-between;
  width: 100%;
  margin: 10px 0;
}

.select {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
}

.toggle-container {
  position: relative;
  display: inline-flex;
  align-items: center;
  width: 50px;
  height: 24px;
  flex: 0 0 auto;
}

.toggle-container input[type="checkbox"] {
  opacity: 0;
  width: 0;
  height: 0;
  position: absolute;
}

.toggle-container .slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: #6c757d; /* default gray for inactive state */
  border-radius: 12px;
  transition: background 0.25s ease, box-shadow 0.25s ease;
}

.toggle-container .slider::before {
  position: absolute;
  content: "";
  width: 18px;
  height: 18px;
  left: 1px;
  bottom: 3px;
  background: #fff;
  border-radius: 50%;
  transition: transform 0.25s ease, background 0.25s ease;
  box-shadow: 0 1px 2px rgba(0,0,0,0.2);
}

/* move knob and set green background */
.toggle-container input:checked + .slider {
  background: #28a745;
}

.toggle-container input:checked + .slider::before {
  transform: translateX(26px);
}

/* add glow when active */
.toggle-container input#power-toggle:checked + .slider {
  box-shadow: 0 0 0 4px rgba(40,167,69,0.12);
}

/* Bank toggle: always show green and glow in both states */
.toggle-container input#bank-toggle + .slider {
  background: #28a745;
  box-shadow: 0 0 0 4px rgba(40,167,69,0.12);
}

.toggle-container input:focus-visible + .slider {
  outline: 2px solid #0d6efd;
  outline-offset: 2px;
}

#display {
  margin: 15px 0;
  text-align: center;
  font-size: 1em;
  padding: 20px;
  background: #444;
  border-radius: 5px;
  width: 100%;
}

.volume-slider {
  width: 100%;
}

.volume-slider label {
  display: block;
  margin-bottom: 5px;
  font-size: 0.9em;
}

.volume-slider input[type='range'] {
  width: 100%;
  -webkit-appearance: none;
  appearance: none;
  height: 4px;
  background: #ffaa33;
  border-radius: 2px;
  margin-bottom: 10px;
}

.volume-slider input[type='range']::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 12px;
  height: 12px;
  background: #fff;
  border-radius: 50%;
  cursor: pointer;
}

.volume-slider input[type='range']:focus-visible {
  outline: 2px solid #0d6efd;
  outline-offset: 2px;
}
js
const projectName = 'drum-machine';

const bankOne = [
  {
    keyCode: 81,
    keyTrigger: 'Q',
    id: 'Heater-1',
    url: 'https://cdn.freecodecamp.org/curriculum/drum/Heater-1.mp3'
  },
  {
    keyCode: 87,
    keyTrigger: 'W',
    id: 'Heater-2',
    url: 'https://cdn.freecodecamp.org/curriculum/drum/Heater-2.mp3'
  },
  {
    keyCode: 69,
    keyTrigger: 'E',
    id: 'Heater-3',
    url: 'https://cdn.freecodecamp.org/curriculum/drum/Heater-3.mp3'
  },
  {
    keyCode: 65,
    keyTrigger: 'A',
    id: 'Heater-4',
    url: 'https://cdn.freecodecamp.org/curriculum/drum/Heater-4_1.mp3'
  },
  {
    keyCode: 83,
    keyTrigger: 'S',
    id: 'Clap',
    url: 'https://cdn.freecodecamp.org/curriculum/drum/Heater-6.mp3'
  },
  {
    keyCode: 68,
    keyTrigger: 'D',
    id: 'Open-HH',
    url: 'https://cdn.freecodecamp.org/curriculum/drum/Dsc_Oh.mp3'
  },
  {
    keyCode: 90,
    keyTrigger: 'Z',
    id: "Kick-n'-Hat",
    url: 'https://cdn.freecodecamp.org/curriculum/drum/Kick_n_Hat.mp3'
  },
  {
    keyCode: 88,
    keyTrigger: 'X',
    id: 'Kick',
    url: 'https://cdn.freecodecamp.org/curriculum/drum/RP4_KICK_1.mp3'
  },
  {
    keyCode: 67,
    keyTrigger: 'C',
    id: 'Closed-HH',
    url: 'https://cdn.freecodecamp.org/curriculum/drum/Cev_H2.mp3'
  }
];

const bankTwo = [
  {
    keyCode: 81,
    keyTrigger: 'Q',
    id: 'Chord-1',
    url: 'https://cdn.freecodecamp.org/curriculum/drum/Chord_1.mp3'
  },
  {
    keyCode: 87,
    keyTrigger: 'W',
    id: 'Chord-2',
    url: 'https://cdn.freecodecamp.org/curriculum/drum/Chord_2.mp3'
  },
  {
    keyCode: 69,
    keyTrigger: 'E',
    id: 'Chord-3',
    url: 'https://cdn.freecodecamp.org/curriculum/drum/Chord_3.mp3'
  },
  {
    keyCode: 65,
    keyTrigger: 'A',
    id: 'Shaker',
    url: 'https://cdn.freecodecamp.org/curriculum/drum/Give_us_a_light.mp3'
  },
  {
    keyCode: 83,
    keyTrigger: 'S',
    id: 'Open-HH',
    url: 'https://cdn.freecodecamp.org/curriculum/drum/Dry_Ohh.mp3'
  },
  {
    keyCode: 68,
    keyTrigger: 'D',
    id: 'Closed-HH',
    url: 'https://cdn.freecodecamp.org/curriculum/drum/Bld_H1.mp3'
  },
  {
    keyCode: 90,
    keyTrigger: 'Z',
    id: 'Punchy-Kick',
    url: 'https://cdn.freecodecamp.org/curriculum/drum/punchy_kick_1.mp3'
  },
  {
    keyCode: 88,
    keyTrigger: 'X',
    id: 'Side-Stick',
    url: 'https://cdn.freecodecamp.org/curriculum/drum/side_stick_1.mp3'
  },
  {
    keyCode: 67,
    keyTrigger: 'C',
    id: 'Snare',
    url: 'https://cdn.freecodecamp.org/curriculum/drum/Brk_Snr.mp3'
  }
];

const state = {
  power: true,
  display: ' ',
  currentPadBank: bankOne,
  currentPadBankId: 'Heater Kit',
  sliderVal: 0.3
};

const display = document.getElementById('display');

const setDisplay = text => {
  state.display = text;
  display.innerText = text;
};

const adjustVolume = e => {
  if (state.power) {
    state.sliderVal = e.target.value;
    setDisplay(`Volume: ${Math.round(e.target.value * 100)}`);
    setTimeout(() => setDisplay(' '), 1000);
    document.querySelectorAll('.clip').forEach(sound => {
      sound.volume = state.sliderVal;
    });
  }
};

const activatePad = pad => {
  pad.classList.add('active');
  setTimeout(() => pad.classList.remove('active'), 100);
};

const playSound = (id, name) => {
  if (!state.power) return;

  const sound = document.getElementById(id);
  sound.currentTime = 0;
  sound.play();
  activatePad(sound.parentNode);
  setDisplay(name);
};

const renderPadBank = () => {
  const padBank = document.querySelector('#pad-bank');
  padBank.innerHTML = '';

  state.currentPadBank.forEach(drum => {
    const pad = document.createElement('button');
    pad.classList.add('drum-pad');
    pad.id = drum.id;
    pad.innerText = drum.keyTrigger;

    const audio = document.createElement('audio');
    audio.classList.add('clip');
    audio.id = drum.keyTrigger;
    audio.src = drum.url;
    pad.appendChild(audio);

    pad.addEventListener('click', () =>
      playSound(drum.keyTrigger, drum.id.replace(/-/g, ' '))
    );
    document.addEventListener('keydown', e => {
      if (e.key === drum.keyTrigger.toLowerCase())
        playSound(drum.keyTrigger, drum.id.replace(/-/g, ' '));
    });

    padBank.appendChild(pad);
  });
};

const powerControl = () => {
  state.power = !state.power;
  document.getElementById('power-status').textContent = state.power
    ? 'On'
    : 'Off';

  setDisplay('');
};

const selectBank = () => {
  if (!state.power) return;
  setDisplay(' ');
  state.currentPadBank =
    state.currentPadBankId === 'Heater Kit' ? bankTwo : bankOne;
  state.currentPadBankId =
    state.currentPadBankId === 'Heater Kit' ? 'Smooth Piano Kit' : 'Heater Kit';

  renderPadBank();
  document.getElementById('current-bank').textContent =
    state.currentPadBankId === 'Heater Kit' ? 'Heater Kit' : 'Smooth Piano Kit';
};

document
  .getElementById('power-toggle')
  .addEventListener('change', powerControl);
document
  .getElementById('bank-toggle')
  .addEventListener('change', selectBank);
document
  .getElementById('volume-control')
  .addEventListener('input', adjustVolume);

renderPadBank();