Search code examples
javascriptweb-audio-apimediarecorderrecording

How to record directly from a webpage using javascript


I am working on making a little music app. I want to be able to record the sounds made in the browser, without relying on the microphone. So far, everything I have seen about the MediaRecorder api suggests that it relies on the mic. Ideally I'd like to achieve this goal without using an external library.

For reference, here is how I am making a really simple sound.

var congo = new Audio('http://www.denhaku.com/r_box/sr16/sr16perc/hi conga.wav');

var drumpad = document.getElementById('drumpad');

drumpad.addEventListener('click', function(){
    congo.play();
});

Thank you

Edit: To be clearer, how would I record the sound made by the code snippet I included, without relying on a computers built-in mic. For instance, lets say a user is making sounds with a drumpad and they are wearing headphones, the mic would be useless. Even if they aren't wearing headphones, they would still be picking up a lot of background noises. I want to isolate the sound being recorded, to just the music being made in the specific browser tab that a user has this application open in.


Solution

  • So far, everything I have seen about the MediaRecorder api suggests that it relies on the mic.

    No, MediaRecorder API does rely on MediaStreams, but these MediaStreams don't have to be LocalMediaStreams (i.e from gUM):

    You can get a MediaStream from MediaElement (<audio>, <video>)'s captureStream() method, if the media loaded complies with the same-origin policy.

    But this will return one MediaStream per MediaElement, and in your case it's probably not the best solution.

    Instead, make the great jump to the Web Audio API, which anyway is more suitable for such an application as a drum-pad. The Web Audio API does have a createMediaStreamDestination() method which will return a MediaStreamAudioDestinationNode, which will contain a MediaStream in its .stream property. Every other nodes you'll connect to this MediaStreamAudioDestinationNode will be aired in the MediaStream, and you'll be able to record it from a MediaRecorder.

    Let's recycle this drum-kit demo to include a recorder:

    (function myFirstDrumKit() {
    
      const db_url = 'https://dl.dropboxusercontent.com/s/'; // all our medias are stored on dropbox
    
      // we'll need to first load all the audios
      function initAudios() {
        const promises = drum.parts.map(part => {
          return fetch(db_url + part.audio_src) // fetch the file
            .then(resp => resp.arrayBuffer()) // as an arrayBuffer
            .then(buf => drum.a_ctx.decodeAudioData(buf)) // then decode its audio data
            .then(AudioBuf => {
              part.buf = AudioBuf; // store the audioBuffer (won't change)
              return Promise.resolve(part); // done
            });
        });
        return Promise.all(promises); // when all are loaded
      }
    
      function initImages() {
        // in this version we have only an static image,
        // but we could have multiple per parts, with the same logic as for audios
        var img = new Image();
        img.src = db_url + drum.bg_src;
        drum.bg = img;
        return new Promise((res, rej) => {
          img.onload = res;
          img.onerror = rej;
        });
      }
    
      let general_solo = false;
      let part_solo = false;
    
      const drum = {
        a_ctx: new AudioContext(),
        generate_sound: (part) => {
          // called each time we need to play a source
          const source = drum.a_ctx.createBufferSource();
          source.buffer = part.buf;
          source.connect(drum.gain);
          // to keep only one playing at a time
          // simply store this sourceNode, and stop the previous one
          if(general_solo){
            // stop all playing sources
            drum.parts.forEach(p => (p.source && p.source.stop(0)));
            }
          else if (part_solo && part.source) {
            // stop only the one of this part
            part.source.stop(0);
          }
          // store the source
          part.source = source;
          source.start(0);
        },
        parts: [{
            name: 'hihat',
            x: 90,
            y: 116,
            w: 160,
            h: 70,
            audio_src: 'kbgd2jm7ezk3u3x/hihat.mp3'
          },
          {
            name: 'snare',
            x: 79,
            y: 192,
            w: 113,
            h: 58,
            audio_src: 'h2j6vm17r07jf03/snare.mp3'
          },
          {
            name: 'kick',
            x: 80,
            y: 250,
            w: 200,
            h: 230,
            audio_src: '1cdwpm3gca9mlo0/kick.mp3'
          },
          {
            name: 'tom',
            x: 290,
            y: 210,
            w: 110,
            h: 80,
            audio_src: 'h8pvqqol3ovyle8/tom.mp3'
          }
        ],
        bg_src: '0jkaeoxls18n3y5/_drumkit.jpg?dl=0',
    //////////////////////
    /// The recording part
    //////////////////////    
        record: function record(e) {
        	const btn = document.getElementById('record');
        	const chunks = [];
        	// init a new MediaRecorder with our StreamNode's stream
        	const recorder = new MediaRecorder(drum.streamNode.stream);
        	// save every chunks
        	recorder.ondataavailable = e => chunks.push(e.data);
        	// once we're done recording
        	recorder.onstop = e => {
        		// export our recording
        		const blob = new Blob(chunks);
        		const url = URL.createObjectURL(blob);
        		// here in an <audio> element
        		const a = new Audio(url);
        		a.controls = true;
        		document.getElementById('records').appendChild(a);
        		// reset default click handler
        		btn.onclick = drum.record;
        		btn.textContent = 'record';
        	}
        	btn.onclick = function () {
        		recorder.stop();
        	};
        	// start recording
        	recorder.start();
        	btn.textContent = 'stop recording';
        }
      };
      drum.gain = drum.a_ctx.createGain();
      drum.gain.gain.value = .5;
      drum.gain.connect(drum.a_ctx.destination);
      // for recording
      drum.streamNode = drum.a_ctx.createMediaStreamDestination();
      drum.gain.connect(drum.streamNode);
      
      document.getElementById('record').onclick = drum.record;
    
    
    /////////////
    //Unrelated to current question
    ////////////
      function initCanvas() {
        const c = drum.canvas = document.createElement('canvas');
        const ctx = drum.ctx = c.getContext('2d');
        c.width = drum.bg.width;
        c.height = drum.bg.height;
        ctx.drawImage(drum.bg, 0, 0);
        document.body.appendChild(c);
        addEvents(c);
      }
    
      const isHover = (x, y) =>
        (drum.parts.filter(p => (p.x < x && p.x + p.w > x && p.y < y && p.y + p.h > y))[0] || false);
    
    
      function addEvents(canvas) {
        let mouse_hovered = false;
        canvas.addEventListener('mousemove', e => {
          mouse_hovered = isHover(e.pageX - canvas.offsetLeft, e.pageY - canvas.offsetTop)
          if (mouse_hovered) {
            canvas.style.cursor = 'pointer';
          } else {
            canvas.style.cursor = 'default';
          }
        })
        canvas.addEventListener('mousedown', e => {
          e.preventDefault();
          if (mouse_hovered) {
            drum.generate_sound(mouse_hovered);
          }
        });
        const checkboxes = document.querySelectorAll('input');
        checkboxes[0].onchange = function() {
          general_solo = this.checked;
          general_solo && (checkboxes[1].checked = part_solo = true);
        };
        checkboxes[1].onchange = function() {
          part_solo = this.checked;
          !part_solo && (checkboxes[0].checked = general_solo = false);
        };
      }
      Promise.all([initAudios(), initImages()])
        .then(initCanvas);
    
    })()
    label{float: right}
    <button id="record">record</button>
    <label>general solo<input type="checkbox"></label><br>
    <label>part solo<input type="checkbox"></label><br>
    <div id="records"></div>