Home > Blockchain >  creating single media stream from multiple <audio> elements?
creating single media stream from multiple <audio> elements?

Time:06-19

I have an SVG that produces audio based on keypresses, and I want to record the audio coming from it in my react app.

In the svg I have a group of audio elements like this:

<svg id="keyboard_svg">

*other svg stuff*

   <g id="keyboard">
     <audio src="https://arweave.net/<hash>/fileName1.flac"></audio
     <audio src="https://arweave.net/<hash>/fileName2.flac"></audio>
     <audio src="https://arweave.net/<hash>/fileName3.flac"></audio>
     <audio src="https://arweave.net/<hash>/fileName4.flac"></audio>
   </g>
</svg>

in my react app I get access to the audio elements like this:

const svg = document.getElementById("keyboard_svg")
const sound = svg.getElementById("Electric Piano")

so sound is the group of audio elements. I can iterate through them like this:

for(let i=0; i<sound.children.length; i  ) {
   //do something with sound.children[i]
}

the SVG is of a keyboard that users can use their keyboard to play. my goal is to record the audio from these elements in javascript. My understanding is that I can create a MediaStream object, add MediaStreamTracks to it, and then pass the MediaStream in the MediaRecorder constructor. But maybe Im misunderstanding how it works.

What I've tried:

let stream = new MediaStream()
for(let i=0; i<sound.children.length; i  ) {
    stream.addTrack(sound.children[i])
}

this doesnt work because audio elements are not of type MediaStreamTrack.

let audio = new Audio()

for(let i=0; i<sound.children.length; i  ) {
        let source = document.createElement("source")
        source.src = sound.children[i].src
        audio.appendChild(source)
    }

so audio is gives me this:

<audio>
   <source src="https://arweave.net/<hash>/fileName1.flac">
   <source src="https://arweave.net/<hash>/fileName2.flac">
   <source src="https://arweave.net/<hash>/fileName3.flac">
   <source src="https://arweave.net/<hash>/fileName4.flac">
</audio>

then I tried:

let stream = audio.captureStream()

but if I pass that into a MediaRecorder, i get:

Failed to construct 'MediaRecorder': parameter 1 is not of type 'MediaStream'

So thats where im stuck at basically. Im aware of these other functions

AudioContext.createMediaElementSource()
AudioContext.createMediaStreamSource()
AudioContext.createMediaStreamTrackSource()
MediaStreamTrackGenerator()

it sounds like theyre what I need but I cant figure out how to use them. Ive tried a bunch of other things but what I listed above is what I think makes most sense, but still doesnt work.

Any help would be great!

CodePudding user response:

For this you're going to need the Web Audio API, including some of the functions that you mentioned in your question.

Start of by creating a new AudioContext and select your <audio> elements. Loop over each <audio> element and use AudioContext.createMediaElementSource to create a playable source from the elements.

const audioContext = new AudioContext();
const audioElements = document.querySelectorAll('#keyboard audio');
const sources = Array.from(audioElements).map(
  audioElement => audioCtx.createMediaElementSource(audioElement)
);

From here you need to connect all the sources into the MediaRecorder. You can't do that directly, because the MediaRecorder only accepts a MediaStream instance as argument.

To connect the two, use a MediaStreamAudioDestinationNode. This node is able to both receive the inputs from the created sources and create an output in the form of a MediaStream.

Loop over the sources and connect them with the MediaStreamAudioDestinationNode. Then pass the stream property of MediaStreamAudioDestinationNode into the MediaRecorder constructor.

const mediaStreamDestination = audioContext.createMediaStreamDestination();

sources.forEach(source => {
  source.connect(mediaStreamDestination);
});

const mediaRecorder = new MediaRecorder(mediaStreamDestination.stream);

From here, all you have to do is hit record and play your notes.

let recordingData = [];

mediaRecorder.addEventListener('start', () => {
  recordingData.length = 0;
});

mediaRecorder.addEventListener('dataavailable', event => {
  recordingData.push(event.data);
});

mediaRecorder.addEventListener('stop', () => {
  const blob = new Blob(recordingData, { 
    'type': 'audio/mp3' 
  });
  
  // The blob is the result of the recording.
  // Handle the blob from here.
});

mediaRecorder.start();

Note 1: This solution is not written in React and should be modified to work with it. For example: document.querySelectorAll should never be used in a React app. Instead use the useRef hook to create a reference to the <audio> elements.

Note 2: The <audio> element is not a part of an SVG. You don't have to place the elements inside the SVG for them to work. Instead visually hide the <audio> elements and trigger them with, for example, an onClick prop.

CodePudding user response:

I found this article: https://mitya.uk/articles/concatenating-audio-pure-javascript

var uris = [];
for(let i=0; i<sound.children.length; i  ) {
    uris.push(sound.children[i].src);
}
let proms = uris.map(uri => fetch(uri).then(r => r.blob()));
Promise.all(proms).then(blobs => {
    let blob = new Blob([blobs[0], blobs[1]]),
        blobUrl = URL.createObjectURL(blob),
        audio = new Audio(blobUrl);
    audio.play();
});
  • Related