0

I did a pure CSS/JS piano keyboard using the AudioContext object but I have two problems related to the playTone function:

  1. On Chrome/android (v.89.0.4389.105/Android 10) it seems that the volume is halved at every key pressed: after a few notes played the volume is not audible anymore.

  2. On Firefox (v.88/MacOS 10.15.7) I hear a crackle at the end of every key pressed.

On latest Chrome/MacOS, for comparison, it sounds good.

const noteFrequencies = {
   'C1' : 261.63,
   'C1#': 277.18,
   'D1' : 293.66,
   'D1#': 311.13,
   'E1' : 329.63,
   'F1' : 349.23,
   'F1#': 369.99,
   'G1' : 392.00,
   'G1#': 415.30,
   'A1' : 440.00,
   'A1#': 466.16,
   'B1' : 493.88
}


function playTone(note, duration = 1) {

   let ac = new AudioContext();
   let oscl = ac.createOscillator();
   let gain = ac.createGain();

   gain.connect(ac.destination);
   oscl.connect(gain);
   
   oscl.frequency.value = noteFrequencies[note];
   oscl.type = "sine"
   gain.gain.linearRampToValueAtTime(.0001, ac.currentTime + duration);
   oscl.start();
   oscl.stop(ac.currentTime + duration);
}


let notelist = document.querySelector('.piano');

notelist.addEventListener('click', (ev) => {
  let tgt = ev.target;
  let tone;
  if (tgt.matches('[data-tone]')) {
     tone = tgt.getAttribute('data-tone');
     playTone(tone);   
  }
});
* {
   box-sizing: border-box;
   font-family: "Lobster Two";
   color: #555666;
}

h1 {
   text-align: center;
   font-size: 5rem;
   margin-bottom: 4rem;
}

.piano {
   position   : relative;
   display    : flex;
   width      : max-content;
   list-style : none;
   margin     : 0 auto;
   padding    : 0;
   align-items: flex-start;
}

.piano li {      
   --ar   : .2;   
   display: inherit;
   cursor : pointer;
   color  : transparent;
   user-select : none;
   aspect-ratio: var(--ar);
}

@supports (not (aspect-ratio: 1)) {
  .piano li::before {
      content: "";
      padding-top: calc(100% / var(--ar));
   }
}


li:not(.diesis) {
  width         : max(50px, 4vw);
  border-left   : 1px solid #c4c4c8;
  border-bottom : 1px solid #c4c4c8;
  border-radius : 0 0 4px 4px;
  background    : linear-gradient(to bottom, #f2f2f5, #fff);
  box-shadow    : 0 0 5px #ccc inset;
}


li:not(.diesis):active {
  border-top    : 1px solid #bbb;
  border-left   : 1px solid #ccc;
  border-bottom : 1px solid #ccc;
  background    : linear-gradient(to bottom, #fff, #f2f2f5);
  box-shadow    :
     2px 0 3px rgba(0,0,0,0.1) inset,
     -4px 2px 10px rgba(0,0,0,.02) inset; 
}


li.diesis {   
  position      : relative;
  z-index       : 1;
   
  --w           : max(30px, 2.5vw);
  width         : var(--w);
  margin        : 0 calc(var(--w) / -2);
  border        : 1px solid #131313;
  border-radius : 0 0 4px 4px;
  background    : linear-gradient(40deg, #222, #555);
  box-shadow    :
     0 -1px 1px 2px rgba(0,0,0, .5) inset,
     0 1px 2px rgba(0,0,0, .5);
   
}

li.diesis:active {
  background: linear-gradient(100deg, #505050, #131313);
  box-shadow: 
     -1px -1px 2px rgba(255,255,255,0.2) inset,
     0 -2px 2px 3px rgba(0,0,0,0.6) inset,
     0 1px 2px rgba(0,0,0,0.5);
}
<ul class="piano">
   <li data-tone="C1">C1</li>
   <li data-tone="C1#" class="diesis">C1#</li>
   <li data-tone="D1">D1</li>
   <li data-tone="D1#" class="diesis">D1#</li>
   <li data-tone="E1">E1</li>
   <li data-tone="F1">F1</li>
   <li data-tone="F1#" class="diesis">F1#</li>
   <li data-tone="G1">G1</li>
   <li data-tone="G1#" class="diesis">G1#</li>
   <li data-tone="A1">A1</li>
   <li data-tone="A1#" class="diesis">A1#</li>
   <li data-tone="B1">B1</li>
</ul>

A full demo on codepen is available too, if it can help the debug.

Thank you

Fabrizio Calderan
  • 120,726
  • 26
  • 164
  • 177
  • 2
    Just tried on Windows10 and confirm Edge/Chrome sound good but FF gives a sort of terminating 'crackle'. – A Haworth Apr 26 '21 at 07:49

1 Answers1

1

The problem with the volume in Chrome can be solved by using only one global AudioContext which then needs to be resumed in the click handler.

The crackling in Firefox can be removed by adding an explicit value to the automation timeline to start with.

const currentTime = ac.currentTime;

gain.gain.setValueAtTime(1, currentTime);
gain.gain.linearRampToValueAtTime(.0001, currentTime + duration);

The updated JavaScript code of your CodePen would then look like this:

const ac = new AudioContext();
const noteFrequencies = {
   'C1' : 261.63,
   'C1#': 277.18,
   'D1' : 293.66,
   'D1#': 311.13,
   'E1' : 329.63,
   'F1' : 349.23,
   'F1#': 369.99,
   'G1' : 392.00,
   'G1#': 415.30,
   'A1' : 440.00,
   'A1#': 466.16,
   'B1' : 493.88,
   'C2' : 523.25,
   'C2#': 554.37,
   'D2' : 587.33,
   'D2#': 622.25,
   'E2' : 659.25
}


function playTone(note, duration = 1) {
   const currentTime = ac.currentTime;

   let oscl = ac.createOscillator();
   let gain = ac.createGain();

   gain.connect(ac.destination);
   oscl.connect(gain);

   oscl.frequency.value = noteFrequencies[note];
   oscl.type = "sine"
   gain.gain.setValueAtTime(1, currentTime);
   gain.gain.linearRampToValueAtTime(.0001, currentTime + duration);
   oscl.start();
   oscl.stop(currentTime + duration);
}


let notelist = document.querySelector('.piano');

notelist.addEventListener('click', (ev) => {
  ac.resume();

  let tgt = ev.target;
  let tone;
  if (tgt.matches('[data-tone]')) {
     tone = tgt.getAttribute('data-tone');
     playTone(tone);   
  }
});
chrisguttandin
  • 7,025
  • 15
  • 21
  • Thanks for the answer, I tried to use a global audioContext object and it solves the volume issue but now, if you play two notes together something goes wrong on Chrome when the notes are overlapping, while on Firefox is ok. – Fabrizio Calderan Apr 26 '21 at 10:02
  • If I recall correctly Chrome clamps the signal to be between -1 and 1 before handing it over to the OS. When two oscillators play at the same time there is a good chance that the signal exceeds these values and gets clamped. A conservative way to solve this would be to only allow one oscillator to be played at the same time. Or you could allow two and level both with a gain of `0.5`. A more flexible way would be to add a limiter at the end of your chain. – chrisguttandin Apr 26 '21 at 10:24
  • How can I add a limiter? something like this https://webaudiotech.com/2016/01/21/should-your-web-audio-app-have-a-limiter/ ? – Fabrizio Calderan Apr 26 '21 at 10:30
  • Yes, I can recommend that. – chrisguttandin Apr 26 '21 at 11:02