Browse Source

Use a redundant copy of the js file in the exampe. A symlink generates too much confusion

master
Nils 3 months ago
parent
commit
8f9dd1f443
2 changed files with 433 additions and 1 deletions
  1. +0
    -1
      example/js/videomixer.js
  2. +433
    -0
      example/js/videomixer.js

+ 0
- 1
example/js/videomixer.js View File

@@ -1 +0,0 @@
../../videomixer.js

+ 433
- 0
example/js/videomixer.js View File

@@ -0,0 +1,433 @@
/*
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net )

This multichannel video audio mixer is free software: you can redistribute it and/or modify
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/agpl-3.0.en.html>.

Based on the web audio API
https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Using_Web_Audio_API

wavyjs Copyright (c) 2016 Chris Schalick , https://github.com/northeastnerd/wavyjs , License: MIT
imported via html
*/

//Stuff that didn't work, so we don't forget
// console.log(audioContext.sampleRate); //If not set in AudioContext(): our test that returned 44100 but video was 48k.
// console.log(videoElement.audioTracks); //undefined. Hence we supply our own list.


//We cannot programatically get the correct number of channels
//Tracknames is defined in the html document. Each track is a stereo channel
const sourceChannels = tracknames.length * 2;
const volumeGainNodes = {}; // monoChannelIndex:gain-node-object len == sourceChannels == number of tracks * 2
const volumeControls = {}; // HTML. len == sourceChannel / 2 == number of tracks
const live_audioContext = new AudioContext({
latencyHint: "interactive",
sampleRate: videoAudioSampleRate,
});

/*
* Create nodes. They are in a function for namespace clarity.
* live_audioContext stays global for persistancy
*/

function volumeFaderControl(eventParameterDict) {
var idx = parseInt( eventParameterDict["target"]["id"].substring("volume".length) );
//console.log(idx + "->" + this.value);
volumeGainNodes[idx*2].gain.value = this.value;
volumeGainNodes[idx*2+1].gain.value = this.value;
}

function createLiveMixingGraph() {
var videoElement = document.querySelector('video'); // Returns object of type HTMLMediaElement
var mediaElementAudioSourceNode = live_audioContext.createMediaElementSource(videoElement); //Returns type MediaElementAudioSourceNode
var splitterNode = live_audioContext.createChannelSplitter(sourceChannels);
var finalMergerNode = live_audioContext.createChannelMerger(2); //The final Stereo Mix.
finalMergerNode.channelInterpretation = "discrete";
mediaElementAudioSourceNode.connect(splitterNode); //now we have mono channels

var mixerStripsHTML = []; //HTML elements we write into a <div>
for (var i=0; i < sourceChannels/2; i++) {
//trackMixes[i] = live_audioContext.createChannelMerger(2);

//HTML Channels are counted in stereo, starting from 0. We present as 1-based though
//Push into a list, concat this list after the loop and write. volumeMap are the default values, set in HTML.
mixerStripsHTML.push('<input class="volume" type="range" orient="vertical" id="volume'+ i +'" min="0" max="2" value="' + volumeMap[i] + '" step="0.01">');
//Set HTML and label with a [number] shortcut.
mixerStripsHTML.push('<label class="volumeLabel" for="volume'+ i +'">' + tracknames[i] + ' - [' + (i+1) + ']</label></input>');
}

//Write mixer strips into HTML. From now on this innerHTML must remain static.
document.getElementById("mixerstrips").innerHTML = mixerStripsHTML.join(" "); // Empty string to get rid of the comma


for (var i=0; i < sourceChannels; i++) {
volumeGainNodes[i] = live_audioContext.createGain();
volumeGainNodes[i].channelCount=1;
volumeGainNodes[i].channelInterpretation = "discrete";
splitterNode.connect(volumeGainNodes[i], i, 0);
volumeGainNodes[i].connect(finalMergerNode, 0, i%2); //destination, outputIndex of source, inputIndex of dest
}

//we need yet another for loop. html elements are actually objets, we can't just rewrite innerHTML everyLoop. It will delete attached eventListeners
for (var i=0; i < sourceChannels/2; i++) {
//Connect html fader to javascript via event callback
volumeControls[i] = document.querySelector('#volume'+ i);
volumeControls[i].addEventListener('input', volumeFaderControl, false);
}

//Connect our stereo mixdown to the browser/user again
//Despite the analyzer and logs showing that the channelMerger only has 1 channel it *does* produce correct stereo output.
finalMergerNode.connect(live_audioContext.destination);
}
createLiveMixingGraph();
resetAllVolumeToDefault();

/*****************************************************
* Special Function to render the audio with current volume levels to a file for download
* Needs to be triggered (e.g. by a button press)
* Processing is done on the client computer.
*
* We do not connect to the existing live AudioContext / AudioNode but replicate everything based on the current
* fader values.
*****************************************************/




function renderOfflineAndDownload () {

var videoElement = document.querySelector('video'); // Returns object of type HTMLMediaElement
var videoURL = videoElement.currentSrc; //URL #TODO: is this cached?
var durationInSeconds = videoElement.duration; //Double. only available after video is loaded, which is true when this button can be clicked.

//audioContext is the global variable set at the beginning of this file.
var sampleRate = videoAudioSampleRate; //this was set manually on creation. Autodetection was wrong.

if ( !isFinite(durationInSeconds) || durationInSeconds <= 0.0 ) {
alert("Error: Couldn't get video duration. Download not possible.");
console.error("Error: Couldn't get video duration. Download not possible.");
return;
}

console.log("Starting down-mix for " + videoURL + " with duration " + durationInSeconds);

// https://www.mertakdut.com/blog/en/extracting-audio.html
// https://stackoverflow.com/questions/49140159/extracting-audio-from-a-video-file
var xhr = new XMLHttpRequest();
xhr.open('GET', videoURL, true);
xhr.responseType = 'blob'; // TODO: when was this implement in Chrome?
xhr.onload = function(e) {
if (this.status == 200) {
var videoBlob = this.response;
//Do the entire downmix in this local function
processOfflineData(videoBlob);
}
else {
alert("Download Mix not possible here");
console.error("Download Mix not possible here");
return;
}
};
xhr.send();

}

function processOfflineData(blob) {
var durationInSeconds = document.querySelector('video').duration; //Double. only available after video is loaded, which is true when this button can be clicked.
//videoAudioSampleRate is a global var from html, set manually.

var offline_audioContext = new OfflineAudioContext(2, (durationInSeconds+2)*videoAudioSampleRate , videoAudioSampleRate); //numberOfChannels, length in samples, sampleRate
var reader = new FileReader();
reader.readAsArrayBuffer(blob); // video file as blob, from parameter
reader.onload = function () {
var videoFileAsBuffer = reader.result; // ArrayBuffer
// https://www.mertakdut.com/blog/en/extracting-audio.html
// https://stackoverflow.com/questions/49140159/extracting-audio-from-a-video-file
//offlineAudioContext has no createMediaElementSource. We create a buffer through reading the file.
offline_audioContext.decodeAudioData(videoFileAsBuffer).then(function (decodedAudioData) {

/*
* Here starts the same process as live. Same chain, only for offline.
*/
var splitterNode = offline_audioContext.createChannelSplitter(sourceChannels); //sourceChannels is global
var finalMergerNode = offline_audioContext.createChannelMerger(2); //The final Stereo Mix.
finalMergerNode.channelInterpretation = "discrete";
var off_volumeGainNodes = {}; // monoChannelIndex:gain-node-object len == sourceChannels == number of tracks * 2

multiChannelSourceNode = offline_audioContext.createBufferSource(); //This does NOT take decodedAudioData as parameter...
multiChannelSourceNode.buffer = decodedAudioData; //...instead we need to write it in.
multiChannelSourceNode.connect(splitterNode);

//console.log(multiChannelSourceNode.buffer.getChannelData(0)); //This confirms there is data. We have samples.


for (var i=0; i < sourceChannels; i++) { //sourceChannels is a global var from HTML scope.
off_volumeGainNodes[i] = offline_audioContext.createGain();
off_volumeGainNodes[i].channelCount=1;
off_volumeGainNodes[i].channelInterpretation = "discrete";
splitterNode.connect(off_volumeGainNodes[i], i, 0);
off_volumeGainNodes[i].connect(finalMergerNode, 0, i%2); //destination, outputIndex of source, inputIndex of dest
}

//Set gain control to current html fader value
for (var i=0; i < sourceChannels; i++) {
// volumeControls is a global var from html scope
if (i%2==0) { // HTML Faders are in Stereo-Pairs, but still numbered 0,1,2,3. We want every 2nd and choose by /2 in the next line
volumeGainNodes[i].gain.value = volumeControls[i/2].value;
}
else {
volumeGainNodes[i].gain.value = volumeControls[(i-1)/2].value;
}
}

//Connect our stereo mixdown to the audio destination again.
//Despite the analyzer and logs showing that the channelMerger only has 1 channel it *does* produce correct stereo output.
//finalMergerNode.connect(offline_audioContext.destination);


/*
* Chain End.
*/

offline_audioContext.startRendering();
offline_audioContext.oncomplete = function(completeEvent) {
renderedBuffer = completeEvent.renderedBuffer;
console.log(renderedBuffer.getChannelData(0));
//createDownload(renderedBuffer);
}

});
};
}

function createDownload(renderedBuffer) {
// From https://www.russellgood.com/how-to-convert-audiobuffer-to-audio-file/
// wavyjs Copyright (c) 2016 Chris Schalick , https://github.com/northeastnerd/wavyjs , License: MIT
// wavyjs imported via html

//console.log(renderedBuffer);
/*
renderedBuffer.sampleRate //int
renderedBuffer.length //in samples
renderedBuffer.duration //in seconds
renderedBuffer.numberOfChannels // is 2.
*/


dl = new wavyjs;
dl.make(2, renderedBuffer.sampleRate, 32, renderedBuffer.length, 0x1); //32 bits, data type 0x1, whatever that is.

var len = renderedBuffer.length;
var channelL = renderedBuffer.getChannelData(0);
var channelR = renderedBuffer.getChannelData(1);

for(var x = 0; x < len; x++){
dl.set_sample(x, 0, channelL[x]);
}

for(var y = 0; y < len; y++){
dl.set_sample(y, 1, channelR[y]);
}

dl.save(dl.raw, "download.wav");

}




// Resume playback when user interacted with the page. Has nothing directly to do with our volume mixing.
// Needed because we interact with the video player and browser want you to confirm in an extra step.
document.querySelector('video').addEventListener('play', function() {
if (live_audioContext.state == "suspended"){
live_audioContext.resume().then(() => {
console.log('Playback resumed successfully');
});
}
});


/*****************************************************
* Various Functions for Buttons and Keyboar Shortcuts
*****************************************************/

function resetAllVolumeToDefault() {
//Reset all volume faders to the value they initially had
//Meant to be called by user interaction, e.g. a button press.

for (var i=0; i < sourceChannels; i++) {
if (i%2==0) { // HTML Faders are in Stereo-Pairs, but still numbered 0,1,2,3. We want every 2nd and choose by /2 in the next line
volumeControls[i/2].value = volumeMap[i/2];; //HTML Fader Setting it does not trigger node gain callback volumeFaderControl()
volumeGainNodes[i].gain.value = volumeMap[i/2]; // Actual Volume
}
else {
volumeGainNodes[i].gain.value = volumeMap[(i-1)/2]; // Actual Volume
}
}
}

function setAllVolumeToZero() {
//Mute everything
//Meant to be called by user interaction, e.g. a button press.
for (var i=0; i < sourceChannels; i++) {
volumeGainNodes[i].gain.value = 0.0 // Actual Volume
if (i%2==0) { // HTML Faders are in Stereo-Pairs, but still numbered 0,1,2,3. We want every 2nd and choose by /2 in the next line
volumeControls[i/2].value = 0.0; //HTML Fader Setting it does not trigger node gain callback volumeFaderControl()
}
}
}

function toggleFader(trackNumber) {
//Track Numbers are 0 based.
if (trackNumber >= tracknames.length) {
//No such channel
return
}

if (volumeControls[trackNumber].value > 0) { //Comparison with html fader is indirect, but easier to write than with actual gainnode values
volumeControls[trackNumber].value = 0.0; //HTML Fader
volumeGainNodes[trackNumber*2].gain.value = 0; // Actual Volume Left
volumeGainNodes[trackNumber*2+1].gain.value = 0; // Actual Volume Right
}
else {
volumeControls[trackNumber].value = 1.0; //HTML Fader
volumeGainNodes[trackNumber*2].gain.value = 1; // Actual Volume Left
volumeGainNodes[trackNumber*2+1].gain.value = 1; // Actual Volume Right
}
}

function halfFader(trackNumber) {
//Track Numbers are 0 based.
if (trackNumber >= tracknames.length) {
//No such channel
return
}
volumeControls[trackNumber].value = 0.5; //HTML Fader
subMixGainControllers['volume' + (trackNumber)].gain.value = 0.5; //Actual Volume

}

function setAllVolumeToOne() {
//Set all volume to the original values of the multitrack video
//This is not strictly the same as resetAllVolumeToDefault, because the volume map from our
//generator can override the videos channel volume.
//Meant to be called by user interaction, e.g. a button press.

for (var i=0; i < sourceChannels; i++) {
volumeGainNodes[i].gain.value = 1.0 // Actual Volume
if (i%2==0) { // HTML Faders are in Stereo-Pairs, but still numbered 0,1,2,3. We want every 2nd and choose by /2 in the next line
volumeControls[i/2].value = 1.0; //HTML Fader Setting it does not trigger node gain callback volumeFaderControl()
}
}
}

function playPause() {
var videoPlayer = document.querySelector('video');
if (videoPlayer.paused)
videoPlayer.play();
else
videoPlayer.pause();
}

function seek(amount) {
//Reminder: this will not work on the local php-interpreter based test server because it does not allow Chromium to seek, at all. Nothing to do with this function.
document.querySelector('video').currentTime += Math.round(amount);

}

function fasterPlaybackSpeed() {
var newSpeed = document.querySelector('video').playbackRate + 0.05;
newSpeed = Math.round((newSpeed + Number.EPSILON) * 100) / 100
document.querySelector('video').playbackRate = newSpeed;
document.getElementById("playbackSpeed").innerHTML = document.querySelector('video').playbackRate;
}


function slowerPlaybackSpeed() {
var newSpeed = document.querySelector('video').playbackRate - 0.05;
newSpeed = Math.round((newSpeed + Number.EPSILON) * 100) / 100
document.querySelector('video').playbackRate = newSpeed;
document.getElementById("playbackSpeed").innerHTML = document.querySelector('video').playbackRate;
}

function normalPlaybackSpeed() {
document.querySelector('video').playbackRate = 1.0;
document.getElementById("playbackSpeed").innerHTML = document.querySelector('video').playbackRate;
}



//Keypress for Volume Controls and more
//They also work when video is in fullscreen! :)
document.onkeydown = keydown; //connect with browsers key callback

function keydown(evt){
if (!evt) evt = event;

//if (evt.ctrlKey && evt.altKey && evt.keyCode==115){ //CTRL+ALT+F4
// alert("CTRL+ALT+F4");
//}
if (evt.keyCode == 82){ //key r
resetAllVolumeToDefault();
}
else if (evt.keyCode == 72){ //key h
setAllVolumeToOne();
}
else if (evt.keyCode == 83){ //key s
setAllVolumeToZero();
}
else if (evt.keyCode == 65){ //key a
slowerPlaybackSpeed();
}
else if (evt.keyCode == 68){ //key d
fasterPlaybackSpeed();
}
else if (evt.keyCode == 87){ //key w
normalPlaybackSpeed();
}


else if (evt.keyCode == 38){ //key arrow up
seek(30);
}
else if (evt.keyCode == 40){ //key arrow down
seek(-30);
}
else if (evt.keyCode == 39){ //key arrow right
seek(5);
}
else if (evt.keyCode == 37){ //key arrow left
seek(-5);
}


else if (evt.shiftKey && evt.keyCode >= 49 && evt.keyCode <= 57){ //Shift + Key 1-9. no zero.
halfFader(evt.keyCode-49); //-48 from keycodes to number as int, -1 because internally we are zero based
}
else if (evt.keyCode >= 49 && evt.keyCode <= 57){ //key 1-9. no zero.
toggleFader(evt.keyCode-49); //-48 from keycodes to number as int, -1 because internally we are zero based
}

else if (evt.keyCode == 32){ //key space
if (document.activeElement == document.querySelector('video')) {
//Active video has its own space key for play toggle. We want it outside as well.
return;
} else {
playPause();
document.querySelector('video').focus(); //Side effect: Video player gets into focus. Maybe we don'T want that?
}
}

}

Loading…
Cancel
Save