Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to mix multiple audio files on top of each other preferably with javascript

Tags:

javascript

I want to combine audio clips, layered on top of each other so that they play synchronously and are saved in a new audio file. Any help would be much appreciated. I've done some digging online, but couldn't find a definitive answer as to whether or not many of the tools available as far as Javascript audio editing librarys go (Mix.js for example) are capable.

like image 340
Ethan Stepanian Avatar asked Nov 13 '16 02:11

Ethan Stepanian


1 Answers

Yes, it is possible using OfflineAudioContext() or AudioContext.createChannelMerger() and creating a MediaStream. See Phonegap mixing audio files , Web Audio API.

You can use fetch() or XMLHttpRequest() to retrieve audio resource as an ArrayBuffer, AudioContext.decodeAudioData() to create an AudioBufferSourceNode from response; OfflineAudioContext() to render merged audio, AudioContext, AudioContext.createBufferSource(), AudioContext.createMediaStreamDestination() , MediaRecorder() to record stream; Promise.all(), Promise() constructor, .then() to process asynchronous requests to fetch(), AudioContext.decodeAudioData(), pass resulting mixed audio Blob at stop event of MediaRecorder.

Connect each AudioContext AudioBufferSourceNode to OfflineAudioContext.destination, call .start() on each node; call OfflineAudioContext.startRendering(); create new AudioContext node, connect renderedBuffer; call .createMediaStreamDestination() on AudioContext to create a MediaStream from merged audio buffers, pass .stream to MediaRecorder(), at stop event of MediaRecorder, create Blob URL of Blob of recorded audio mix with URL.createObjectURL(), which can be downloaded using <a> element with download attribute and href set to Blob URL.

var sources = ["https://upload.wikimedia.org/wikipedia/commons/b/be/"
               + "Hidden_Tribe_-_Didgeridoo_1_Live.ogg"
               , "https://upload.wikimedia.org/wikipedia/commons/6/6e/" 
               + "Micronesia_National_Anthem.ogg"];

var description = "HiddenTribeAnthem";
var context;
var recorder;
var div = document.querySelector("div");
var duration = 60000;
var chunks = [];
var audio = new AudioContext();
var mixedAudio = audio.createMediaStreamDestination();
var player = new Audio();
player.controls = "controls";

function get(src) {
  return fetch(src)
    .then(function(response) {
      return response.arrayBuffer()
    })
}

function stopMix(duration, ...media) {
  setTimeout(function(media) {
    media.forEach(function(node) {
      node.stop()
    })
  }, duration, media)
}

Promise.all(sources.map(get)).then(function(data) {
    var len = Math.max.apply(Math, data.map(function(buffer) {
      return buffer.byteLength
    }));
    context = new OfflineAudioContext(2, len, 44100);
    return Promise.all(data.map(function(buffer) {
        return audio.decodeAudioData(buffer)
          .then(function(bufferSource) {
            var source = context.createBufferSource();
            source.buffer = bufferSource;
            source.connect(context.destination);
            return source.start()
          })
      }))
      .then(function() {
        return context.startRendering()
      })
      .then(function(renderedBuffer) {
        return new Promise(function(resolve) {
          var mix = audio.createBufferSource();
          mix.buffer = renderedBuffer;
          mix.connect(audio.destination);
          mix.connect(mixedAudio);              
          recorder = new MediaRecorder(mixedAudio.stream);
          recorder.start(0);
          mix.start(0);
          div.innerHTML = "playing and recording tracks..";
          // stop playback and recorder in 60 seconds
          stopMix(duration, mix, recorder)

          recorder.ondataavailable = function(event) {
            chunks.push(event.data);
          };

          recorder.onstop = function(event) {
            var blob = new Blob(chunks,  {
              "type": "audio/ogg; codecs=opus"
            });
            console.log("recording complete");
            resolve(blob)
          };
        })
      })
      .then(function(blob) {
        console.log(blob);
        div.innerHTML = "mixed audio tracks ready for download..";
        var audioDownload = URL.createObjectURL(blob);
        var a = document.createElement("a");
        a.download = description + "." + blob.type.replace(/.+\/|;.+/g, "");
        a.href = audioDownload;
        a.innerHTML = a.download;
        document.body.appendChild(a);
        a.insertAdjacentHTML("afterend", "<br>");
        player.src = audioDownload;
        document.body.appendChild(player);
      })
  })
  .catch(function(e) {
    console.log(e)
  });
<!DOCTYPE html>
<html>

<head>
</head>

<body>
  <div>loading audio tracks.. please wait</div>
</body>

</html>

You can alternatively utilize AudioContext.createChannelMerger(), AudioContext.createChannelSplitter()

var sources = ["/path/to/audoi1", "/path/to/audio2"];    
var description = "mix";
var chunks = [];
var channels = [[0, 1], [1, 0]];
var audio = new AudioContext();
var player = new Audio();
var merger = audio.createChannelMerger(2);
var splitter = audio.createChannelSplitter(2);
var mixedAudio = audio.createMediaStreamDestination();
var duration = 60000;
var context;
var recorder;
var audioDownload;

player.controls = "controls";

function get(src) {
  return fetch(src)
    .then(function(response) {
      return response.arrayBuffer()
    })
}

function stopMix(duration, ...media) {
  setTimeout(function(media) {
    media.forEach(function(node) {
      node.stop()
    })
  }, duration, media)
}

Promise.all(sources.map(get)).then(function(data) {
    return Promise.all(data.map(function(buffer, index) {
        return audio.decodeAudioData(buffer)
          .then(function(bufferSource) {
            var channel = channels[index];
            var source = audio.createBufferSource();
            source.buffer = bufferSource;
            source.connect(splitter);
            splitter.connect(merger, channel[0], channel[1]);
            return source
          })
      }))
      .then(function(audionodes) {
        merger.connect(mixedAudio);
        merger.connect(audio.destination);
        recorder = new MediaRecorder(mixedAudio.stream);
        recorder.start(0);
        audionodes.forEach(function(node) {
          node.start(0)
        });

        stopMix(duration, ...audionodes, recorder);

        recorder.ondataavailable = function(event) {
          chunks.push(event.data);
        };

        recorder.onstop = function(event) {
          var blob = new Blob(chunks, {
            "type": "audio/ogg; codecs=opus"
          });
          audioDownload = URL.createObjectURL(blob);
          var a = document.createElement("a");
          a.download = description + "." + blob.type.replace(/.+\/|;.+/g, "");
          a.href = audioDownload;
          a.innerHTML = a.download;
          player.src = audioDownload;
          document.body.appendChild(a);
          document.body.appendChild(player);
        };
      })
  })
  .catch(function(e) {
    console.log(e)
  });
like image 141
guest271314 Avatar answered Oct 19 '22 03:10

guest271314