Browse Source

Add first few files

master
Nils 3 months ago
parent
commit
d6bf85ce33
8 changed files with 1304 additions and 0 deletions
  1. +109
    -0
      template.html
  2. +433
    -0
      videomixer.js
  3. +21
    -0
      wip/wavyjs/LICENSE
  4. +45
    -0
      wip/wavyjs/README.md
  5. +147
    -0
      wip/wavyjs/wavy_example.js
  6. +41
    -0
      wip/wavyjs/wavyjs.html
  7. +507
    -0
      wip/wavyjs/wavyjs.js
  8. +1
    -0
      wip/wavyjs/wavyjs.min.js

+ 109
- 0
template.html View File

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

This template for the multichannel video audio mixer, or the html page generated from it,
is part of a free software project.
While generator, javascript etc. are release under AGPLv3+ this document is
distributed under https://creativecommons.org/publicdomain/zero/1.0/ "Public Domain"
-->


<!-- https://www.blindtextgenerator.de/snippets -->
<!DOCTYPE html>

<html lang="en">

<meta charset="utf-8">

<head>
<link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC"> <!-- Deactivate favicon -->

<link rel="stylesheet" type="text/css" href="css/sanitize/sanitize.css" />
<link rel="stylesheet" type="text/css" href="css/sanitize/forms.css" />
<link rel="stylesheet" type="text/css" href="css/sanitize/typography.css" />

<link rel="stylesheet" type="text/css" href="css/design.css"/>

<meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>

<!-- Filled in by the generator -->

<header>
<h1>$$$TITLE$$$</h1>
<div class="subtitle"><small><b>$$$HUMAN$$$</b> - $$$DATE$$$, $$$LICENSE$$$</small></div>
</header>


<script>
var tracknames = [
$$$TRACKNAMES$$$
];

<!-- Initial / Default volume. Same order as tracks.-->
var volumeMap = [
$$$VOLUMEMAP$$$
];

var videoAudioSampleRate = $$$SAMPLERATE$$$ ;
</script>

<p>
<video controls>
<source id="videofilename" src="$$$VIDEOFILE$$$" type="video/mp4">
</video>
</p>

<p>
<div id="mixerstrips" align="center">
<!-- filled in by javascript -->
</div>
</p>

<!-- Buttons for all tracks volume control -->
<p align="center">
<button onclick="setAllVolumeToZero()">[S] Silence All</button> <!-- All to zero. Not Mute like a DAW. -->
<button onclick="resetAllVolumeToDefault()">[R] Reset to Default</button> <!-- All to initial volumeMap (see above) -->
<button onclick="setAllVolumeToOne()">[H] Hear All</button> <!-- All to original video volume -->
<br> Use a <b>[number]</b> key to toggle tracks directly.
<br> <b>[Shift]</b> + <b>[number]</b> to set track to half volume.
</p>

<!-- Work in Progress. Does not yet work. Maybe in a later version
<hr>
<p align="center">
<button onclick="renderOfflineAndDownload()">Download Your Mix</button>
</p>
-->

<hr>

<p align="center">
<button onclick="playPause()">[Space] Play/Pause</button> <!-- Even if video is not in focus. -->
<button onclick="seek(5)">[➡] +5 sec</button> <!-- Even if video is not in focus. -->
<button onclick="seek(-5)">[⬅] -5 sec</button> <!-- Even if video is not in focus. -->
<button onclick="seek(30)">[⬆] +30 sec</button> <!-- Even if video is not in focus. -->
<button onclick="seek(-30)">[⬇] -30 sec</button> <!-- Even if video is not in focus. -->

</p>
<p align="center">
<button onclick="fasterPlaybackSpeed()">[D] Faster</button>
<button onclick="slowerPlaybackSpeed()">[A] Slower</button>
<button onclick="normalPlaybackSpeed()">[W] Normal Speed</button>
<br>Speed: &times;<span id="playbackSpeed">1.0</span>
</p>

<hr>

<p>
$$$DESCRIPTION$$$
</p>

</body>
</html>

<!-- load video player file last. Uses data from this file in <script> above -->
<!-- <script type="text/javascript" src="js/wavyjs/wavyjs.js"></script> -->
<script type="text/javascript" src="js/videomixer.js"></script>

+ 433
- 0
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?
}
}

}

+ 21
- 0
wip/wavyjs/LICENSE View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Chris Schalick
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 45
- 0
wip/wavyjs/README.md View File

@@ -0,0 +1,45 @@
# wavyjs
Zero dependency javascript RIFF Wav file manipulation routines. It's small (1914 bytes minified, gzipped).

# Usage
These routines do creation and manipulation of Wav file structures in browser memory. The intended uses are for generating sounds from algorithms and / or manipulating existing sound files loaded from files or URLs. Modified results can be saved as Wav files or played as desired.

The project has an example HTML and script file that shows intended use with a Wav file loader and a tremelo generator. The loaded sound is amplitude modulated by a sin wave with a user selected frequency on the first audio channel (ie, left in a stereo file). The original and "tremulated" sounds can be played and the modulated sound saved to a file.

You can create Wav structures from scratch by calling the make() method with desired waveform parameters. The wave properties are attached to the returned object along with allocated space for waveform data. The same structure is returned when reading a Wav file from disk with the allocated space filled in from audio in the file.

There are methods to set and get individual samples called set_sample() and get_sample(). These take index and channel number arguments so you can random access read and write sample data. Those routines also set pointers used for auto-increment routines used to traverse the waveforms called push_sample() and pop_sample(). Pop doesn't actually remove the sample, it returns sample data and advances the read pointer. Same for push - it doesn't allocate space, it overwrites sample data and advances the write pointer.The push and pop methods operate on single samples so the caller has to deal with unique channels (ie, if you push the left channel data in a stereo file the write pointer advances to the right channel not the next left channel sample). Typical expected sequencing is to call set_sample(0, 0, 0) then iterate through sample data with push_sample(new_values).

The example reads a Wav file and traverses it to find the peak amplitude which is reported with sampling information. You can enter a tremelo frequency (try 0.3) and click Make Tremelo, then Play Tremelo to here how it sounds and save it to a Wav file with the Save button. The demo has some useful information written to the browser console, like how long conversion takes. This isn't a useful app, just a way to try the Wavyjs library.

Have fun!

There is a live demo here: https://rawgit.com/northeastnerd/wavyjs/master/wavyjs.html
and the library is used for audio back end in applications below for drum loops, frequency analysis and retro computer tape interfaces:

https://www.webthinglabs.com/bopkit.html

https://www.webthinglabs.com/audio_analyzer.html

https://www.webthinglabs.com/retro_wav.html

# Status
## Version 0.3
32 bit floating point sample support was added, project is stable and in use for a while.

## Version 0.2
There were some problems with consistency in internal function calling parameters that were causing the mix function to generate clicks and distortion, which are now fixed.

Preliminary 24 bit sample support was added.

## Version 0.1
Performance enhancements done, it is 5x-10x faster now, and is usable for song length material. It processes Wav data at roughly 6 mB / Sec. Traversing a 4 minute CD quality track takes about 10 seconds on the machine described below (not speedy). Read-modify-write on the same track takes 20 seconds (done in the demo). On a desktop quad core Q8400 cpu with 8 gB of memory running Chrome it takes about 7 seconds for the r-m-w (I guess it's time for a new laptop :). Firefox takes 9 seconds on the same conversion and plays fine, the Edge browser takes 45 seconds for the same conversion and is persnickity about playing the result.

The interface changed to include push and pop sample routines that auto-increment write and read pointers. They operate on single samples so the caller has to deal with unique channels (ie, if you push the left channel data in a stereo file the write pointer advances to the right channel not the next left channel sample). You can set the pointers by calling set_sample() or get_sample() and providing an index.

Still no tests, and expect there are some unsupported formats as a result.

## Version 0.0
The library works in all browsers, the demo is working for all the Wav files I tried with Chrome v.50, Firefox 46 and Edge v25.10586.0.0. No formal testing and I believe there are functional gaps (all permutations of Wav formats).

Performance with large files (40 MB) is an issue - traversing a 40.9 MB file from begin to end takes roughly 66 seconds on a dual core N2840 running Chrome 50 and traversing, manipulating and writing a modified version of the same file in memory takes 110 seconds. Current performance is ~700 KB/s. For small Wav files (samples) it's reasonable, for larger files (songs) this may not be an ideal solution.

+ 147
- 0
wip/wavyjs/wavy_example.js View File

@@ -0,0 +1,147 @@
// License information: The MIT License
//
// Copyright (c) 2016 Chris Schalick All Rights Reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
// Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
function err(e){
var msg = document.getElementById("msg");
msg.innerHTML += e + "<br>";
}
var html5_snd = function(){};
html5_snd.prototype.audio_ctx = null;
html5_snd.prototype.init = function(err_cb){
try{
window.AudioContext = window.AudioContext||window.webkitAudioContext;
html5_snd.prototype.audio_ctx = new AudioContext();
} catch(e){
err_cb(e);
}
};
html5_snd.prototype.play = function(snd){
var ctx = this.audio_ctx;
var me = this;
this.audio_ctx.decodeAudioData(snd).then(
function(bfr){
if(me.is_playing)
me.stop();
me.is_playing = true;
me.src = ctx.createBufferSource();
me.src.buffer = bfr;
me.src.connect(ctx.destination);
me.src.loop = false;
me.src.start();
},
function(e){out.innerHTML += "decoding failed: " + e + "<br>";}
);
};
html5_snd.prototype.stop = function(){
if(typeof this.src != "undefined"){
this.src.stop();
}
this.is_playing = false;
};
var sound = new html5_snd;
sound.init(err);
var user = new wavyjs;
var tremelo = new wavyjs;
document.getElementById("user").onchange = function(){
var f = document.getElementById("user");
var start = new Date();
user.load_file(f, function(){console.log("wav file loaded");show_params();});
elapsed_time(start);
};
function do_tremelo(){
tremelo = new wavyjs;
tremelo.make(user.channels, user.rate, user.bits, user.samples, user.type);
var p1 = document.getElementById("period1").value * user.rate;
var len = tremelo.samples;
var amp = user.get_sample(0, 0);
var ch;
for(var ch = 0; ch < user.channels; ch++){
for(var x = 0; x < len; x++){
amp = user.get_sample(x, ch);
amp = amp * Math.cos(x / p1 * 2 * 3.14159);
if(user.type == 0x1)
amp = parseInt(amp);
tremelo.set_sample(x, ch, amp);
}
}
}
function process(){
out.innerHTML += "creating tremelo...<br>";
var start = new Date();
function do_tr(){
do_tremelo();
out.innerHTML += "tremelo ready<br>";
elapsed_time(start);
};
setTimeout(do_tr, 10);
};
function play(name){
if(name == "user"){
out.innerHTML += "playing sound...<br>";
sound.play(user.audio());
} if(name == "tremelo"){
out.innerHTML += "playing sound<br>";
sound.play(tremelo.audio());
out.innerHTML += "playing sound...<br>";
}
};
function stop(){
sound.stop();
out.innerHTML += "sound stopped<br>";
};
function save(name){
console.log("saving wav file");
tremelo.save(tremelo.raw, name);
console.log("saved");
};
function show_params(){
out.innerHTML = "File: " + document.getElementById("user").name + "<br>";
out.innerHTML += "Audio Channels: " + user.channels + "<br>";
out.innerHTML += "Bits / Sample: " + user.bits + "<br>";
out.innerHTML += "Samples: " + user.samples + "<br>";
var max = user.get_sample(0, 0);
var data;
for(var x = 0; x < user.samples; x++){
data = user.pop_sample();
if(data > max)
max = data;
}
var lim = Math.pow(2, (user.bits - 1));
out.innerHTML += "Peak: " + max + " of " + lim + "<br>";
}
function elapsed_time(start){
var end = new Date();
out.innerHTML += "elapsed time: " + (end - start) / 1000.0 + " seconds<br>";
}

+ 41
- 0
wip/wavyjs/wavyjs.html View File

@@ -0,0 +1,41 @@
<html>
<!-- License information: The MIT License -->
<!-- -->
<!-- Copyright (c) Chris Schalick 2016 All Rights Reserved. -->
<!-- -->
<!-- Permission is hereby granted, free of charge, to any person obtaining a copy -->
<!-- of this software and associated documentation files (the "Software"), to deal -->
<!-- in the Software without restriction, including without limitation the rights -->
<!-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -->
<!-- copies of the Software, and to permit persons to whom the Software is furnished -->
<!-- to do so, subject to the following conditions: -->
<!-- -->
<!-- The above copyright notice and this permission notice shall be included in all -->
<!-- copies or substantial portions of the Software. -->
<!-- -->
<!-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -->
<!-- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -->
<!-- PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -->
<!-- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -->
<!-- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -->
<!-- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -->
<!-- -->
<head>
</head>
<body>
Load Wav File:<br>
<input type=file id="user"></input><button onclick=play("user")>Play</button><button onclick=stop()>Stop</button><br>
Envelope Parameters:<br>
Tremelo Period (Seconds):<input type=text id="period1"></input><br>
<button onclick=process()>Make Tremelo</button><br>
<button onclick=play("tremelo")>Play Tremelo</button><br>
<button onclick=save("tremelo.wav")>Save Tremelo</button><br>
<script src="https://rawgit.com/northeastnerd/wavyjs/master/wavyjs.js"></script>
<script src="https://rawgit.com/northeastnerd/wavyjs/master/wavy_example.js"></script>
<!-- script src="scripts/wavyjs.js"></script-->
<!-- script src="scripts/wavy_example.js"></script-->
<div id="out"></div>
<div id="dump"></div>
</body>
</html>

+ 507
- 0
wip/wavyjs/wavyjs.js View File

@@ -0,0 +1,507 @@
/*
License information: The MIT License
Copyright (c) 2016 Chris Schalick All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
function wavyjs(){
"use strict";
this.me = wavyjs.count++;
this.filename = "";
this.rptr = 0;
this.wptr = 0;
this.fmt = 0;
this.data = 0; // location of "data" marker
this.rate = 0;
this.channels = 0;
this.bits = 0;
this.bytes = 0;
this.inc = 0;
this.type = 0;
this.samples = 0;
this.raw = null;
this.sound = null;
this.max = [];
this.max_idx = [];
this.avg = [];
this.abslim = 0;
};
// instance counter
wavyjs.count = 0;
// little endian 24 bit access (ls byte first)
DataView.prototype.getInt24 = function(idx){
"use strict";
var val = this.getUint8(idx) + (this.getUint8(idx + 1) << 8) + (this.getUint8(idx + 2) << 16);
if(val & 0x800000)
val = val - 0x1000000;
return val;
}
// little endian 24 bit storage (ls byte first)
DataView.prototype.setInt24 = function(idx, val){
"use strict";
var pval = val;
if(val < 0)
pval = val + 0x1000000;
this.setUint8(idx, pval & 0xff);
this.setUint8(idx + 1, (pval & 0xff00) >> 8);
this.setUint8(idx + 2, (pval & 0xff0000) >> 16);
}
// This method allocates space for waveform data in the
// object and sets up the wav file headers. The headers
// describe the size and content of the file in the 1st
// 44 bytes. Wav files are split into "chunks" that have a
// name, size and data. This only uses one RIFF, fmt and
// data chunk. The fmt and data chunk are contained in
// children of the RIFF chunk. All numeric and data values
// are stored little endian (LSB first) because this is
// a Windows format.
wavyjs.prototype.make = function(channels, smprate, bits, samples, data_enc = 0x1){
this.channels = channels;
this.rate = smprate;
this.bits = bits;
this.abslim = Math.pow(2, (bits - 1));
this.inc = bits / 8;
this.type = data_enc;
this.bytes = this.inc * channels;
this.samples = Math.round(samples);
var total = 44 + this.samples * channels * bits / 8;
var byterate = smprate * bits / 8 * channels;
this.raw = new ArrayBuffer(total);
this.sound = new DataView(this.raw);
this.fmt = 12;
this.data = 36;
this.rptr = 44;
this.wptr = 44;
// content endian-ness
this.sound.setInt32(0, 0x52494646, false); // "RIFF" big |
this.sound.setInt32(4, total - 8, true); // file size - 8 (bytes) little |
this.sound.setInt32(8, 0x57415645, false); // "WAVE" big L____ up to here must match exact
this.sound.setInt32(12, 0x666d7420, false); // "fmt " big <---- might not be fmt chunk, skip unknown
this.sound.setInt16(16, 16, true); // header size little LIST chunk in ex, len = 180(xb4)
this.sound.setInt16(20, this.type, true); // format tag 1 = PCM little <---- len of fmt chunk in ex is 18, not 16
// format tag 3 = IEEE floating point
this.sound.setInt16(22, channels, true); // channels 1 = mono little <---- force invsible INFO? chunk w/"wavyjs"
this.sound.setInt32(24, smprate, true); // sample rate little <---- sort out info/list headers: title in plyr
this.sound.setInt32(28, byterate, true); // bytes/sec little
this.sound.setInt16(32, this.bytes, true); // bytes/sample little
this.sound.setInt16(34, bits, true); // bits/sample little
this.sound.setInt32(36, 0x64617461, false); // "data" big
this.sound.setInt32(40, total - 44, true); // data length little
var s, c;
for(s = 0; s < this.samples; s++)
for(c = 0; c < channels; c++)
this.set_sample(s, c, 0);
this.rptr = 44;
this.wptr = 44;
};
// waveform setters / getters
wavyjs.prototype.set_sample = function(idx, chan, data){
"use strict";
var safe_idx = Math.round(idx);
var safe_chan = (chan > (this.channels - 1)) ? 0 : chan;
var offset = (this.channels * this.inc) * safe_idx + this.data + 8 + safe_chan * this.inc;
var safe_data = (this.type == 0x3) ? data : Math.round(data);
this.wptr = offset;
if(this.raw == null)
return;
if((safe_idx > this.samples) ||
(offset >= this.raw.byteLength) ||
(offset == undefined))
return;
if(this.bits == 8){
this.sound.setUint8(offset, safe_data + 128);
} else if(this.bits == 16){
this.sound.setInt16(offset, safe_data, true);
} else if(this.bits == 24){
this.sound.setInt24(offset, safe_data, true);
} else if((this.bits == 32) && (this.type == 0x1)){
this.sound.setInt32(offset, safe_data, true);
} else if((this.bits == 32) && (this.type == 0x3))
this.sound.setFloat32(offset, safe_data, true);
};
wavyjs.prototype.push_sample = function(data){
"use strict";
if(this.raw == null)
return;
if((this.wptr >= this.raw.byteLength) ||
(this.wptr == undefined))
return;
if(this.bits == 8)
this.sound.setUint8(this.wptr, data + 128);
else if(this.bits == 16)
this.sound.setInt16(this.wptr, data, true);
else if(this.bits == 24)
this.sound.setInt24(this.wptr, data, true);
else if((this.bits == 32) && (this.type == 0x1))
this.sound.setInt32(this.wptr, data, true);
else if((this.bits == 32) && (this.type == 0x3))
this.sound.setFloat32(this.wptr, data, true);
this.wptr += this.inc;
};
wavyjs.prototype.get_sample = function(idx, chan){
"use strict";
var safe_idx = Math.round(idx);
var safe_chan = (chan > (this.channels - 1)) ? 0 : chan;
// var offset = (this.channels * this.bits / 8) * safe_idx + this.data + 8 + safe_chan * this.bits / 8;
var offset = (this.channels * this.inc) * safe_idx + this.data + 8 + safe_chan * this.inc;
this.rptr = offset;
if(this.raw == null)
return NaN;
if((safe_idx > this.samples) ||
(offset > (this.raw.byteLength - this.inc)))
// (offset > (this.raw.byteLength - (this.bits / 8))))
return 0;
var data;
if(this.bits == 8)
data = this.sound.getUint8(offset) - 128;
else if(this.bits == 16)
data = this.sound.getInt16(offset, true);
else if(this.bits == 24)
data = this.sound.getInt24(offset, true);
else if((this.bits == 32) && (this.type == 0x1))
data = this.sound.getInt32(offset, true);
else if((this.bits == 32) && (this.type == 0x3))
data = this.sound.getFloat32(offset, true);
return data;
};
wavyjs.prototype.pop_sample = function(){
"use strict";
if(this.raw == null)
return NaN;
if((this.rptr >= this.raw.byteLength) ||
(this.rptr == undefined))
return NaN;
var data;
if(this.bits == 8)
data = this.sound.getUint8(this.rptr) - 128;
else if(this.bits == 16)
data = this.sound.getInt16(this.rptr, true);
else if(this.bits == 24)
data = this.sound.getInt24(this.rptr, true);
else if((this.bits == 32) && (this.type == 0x1))
data = this.sound.getInt32(this.rptr, true);
else if((this.bits == 32) && (this.type == 0x3))
data = this.sound.getFloat32(this.rptr, true);
this.rptr += this.inc;
return data;
};
wavyjs.prototype.audio = function(){
"use strict";
var len = this.raw == null ? 0 : this.raw.byteLength;
var cp = new ArrayBuffer(len);
var dst = new Uint8Array(cp);
var src = new Uint8Array(this.raw);
var x;
for(x = 0; x < cp.byteLength; x++)
dst[x] = src[x];
return cp;
};
// file i/o routines
wavyjs.prototype.save = function(data, name) {
"use strict";
var a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
var blob = new Blob([data], {type: "octet/stream"});
var url = window.URL.createObjectURL(blob);
a.href = url;
a.download = name;
a.click();
// window.URL.revokeObjectURL(url);
};
wavyjs.prototype.bfr_to_b64 = function(bfr){
"use strict";
var i, bin = '';
var bytes = new Uint8Array(bfr);
var len = bytes.byteLength;
for(i = 0; i < len; i++)
bin += String.fromCharCode(bytes[i]);
return window.btoa(bin);
}
wavyjs.prototype.b64_to_bfr = function(b64){
"use strict";
var i, bin_str = window.atob(b64);
var len = bin_str.length;
var bytes = new Uint8Array(len);
for(i = 0; i < len; i++)
bytes[i] = bin_str.charCodeAt(i);
return bytes.buffer;
}
wavyjs.prototype.to_json = function(){
"use strict";
var wav, txt;
wav = this;
wav.raw = this.bfr_to_b64(this.raw);
txt = JSON.stringify(wav);
return txt;
};
wavyjs.prototype.from_json = function(data){
"use strict";
var a, p, wav = new wavyjs;
for(p in data){
if(data.hasOwnProperty(p))
wav[p] = data[p];
}
wav.raw = wav.b64_to_bfr(data.raw);
wav.parse_header();
return wav;
};
wavyjs.prototype.load_url = function(url, ok_callb, err_callb) {
"use strict";
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onload = function() {
if(typeof request.response.byteLength == "undefined")
return;
context.decodeAudioData(request.response).then(
function(buffer){ok_callb(buffer);},
function(e){err_callb(e)});
}
request.send();
}
wavyjs.prototype.load_file = function(selected, ok_callb, err_callb) {
"use strict";
if(window.File && window.FileReader && window.FileList && window.Blob){
var files = selected.files;
for(var x = 0; x < files.length; x++){
var size = files[x].size;
var reader = new FileReader;
var wavptr = this;
var fname = files[x].name;
reader.onload = (function(afile){
return function(e){
wavptr.raw = e.target.result;
wavptr.sound = new DataView(wavptr.raw);
wavptr.fmt = 12;
wavptr.data = 36;
wavptr.filename = fname;
for(var y = 0; y < 1024; y++){
var sig = wavptr.sound.getInt32(y, false);
if(sig == 0x666d7420)
wavptr.fmt = y;
if(sig == 0x64617461)
wavptr.data = y;
}
wavptr.parse_header();
ok_callb();
};
})(files[x]);
reader.readAsArrayBuffer(files[x]);
}
} else {
err_callb("File reader not supported by your browser.");
}
}
wavyjs.prototype.parse_header = function(){
"use strict";
var ch, smp, raw, flt, raw_view, dec;
if(this.raw == null)
return 0;
this.sound = new DataView(this.raw);
this.type = this.sound.getInt16(this.fmt + 8, true);
this.bytes = this.sound.getInt16(this.fmt + 20, true);
this.channels = this.sound.getInt16(this.fmt + 10, true);
this.rate = this.sound.getInt32(this.fmt + 12, true);
this.bits = this.sound.getInt16(this.fmt + 22, true);
if(this.type == 0x3)
this.abslim = 1.0;
else
this.abslim = Math.pow(2, (this.bits - 1));
this.inc = this.bits / 8; // this.bytes;
var len = this.sound.getInt32(this.data + 4, true);
this.samples = len / this.channels / this.inc; // (this.bits / 8);
this.rptr = this.data + 8;
this.wptr = this.data + 8;
// check if the imported data is floating point, if so convert it
if(this.type == 0x3)
console.log("NOTE: WAV file data is floating point.");
}
wavyjs.prototype.get_stats = function(){
"use strict";
var ch, idx, smp;
for(ch = 0; ch < this.channels; ch++){
this.max[ch] = 0;
this.max_idx[ch] = 0;
this.avg[ch] = 0;
}
for(idx = 0; idx < this.samples; idx++){
for(ch = 0; ch < this.channels; ch++){
smp = this.get_sample(idx, ch);
this.avg[ch] += smp;
if(Math.abs(smp) > this.max[ch]){
this.max[ch] = Math.abs(smp);
this.max_idx[ch] = idx;
}
}
}
idx = 0;
for(ch = 0; ch < this.channels; ch++)
idx += this.max_idx[ch];
this.max_idx = idx / this.channels;
}
wavyjs.prototype.resample = function(rate, bits){
"use strict";
var s, c, s1, s2, sx, dx, si, di, dn, rs, delta, scale;
// if there's nothing to do just return
if((rate == this.rate) && (bits == this.bits))
return this;
// figure out time increment of new and old formats
si = 1 / this.rate;
di = 1 / rate;
dn = Math.round(this.samples * rate / this.rate);
// figure out scale factor for old to new format
scale = Math.pow(2, bits - 1) / Math.pow(2, this.bits - 1);
// create a new object to hold the resampled data
rs = new wavyjs;
rs.make(this.channels, rate, bits, dn);
// loop through re-sampling to new array
for(c = 0; c < this.channels; c++){
for(dx = 0; dx < dn; dx++){
sx = Math.trunc(dx * di / si);
s1 = this.get_sample(sx, c);
s2 = this.get_sample(sx + 1, c);
delta = dx * di - sx * si;
s = s1 + (s2 - s1) * delta / si;
s = s * scale;
if(this.type == 0x3)
s = this.float_to_int(s, bits);
rs.set_sample(dx, c, s);
}
}
// return re-sampled data
return rs;
}
wavyjs.prototype.float_to_int = function(val, bits){
"use strict";
var max = Math.pow(2, (bits - 1));
var ival = val * max;
return ival;
}
wavyjs.prototype.int_to_float = function(val, bits){
"use strict";
var max = Math.pow(2, (bits - 1));
var fval = val / max;
return fval;
}
// wavyjs.prototype.reformat = function(sound1, sound2){} returns sound1 formatted in sound2 format
// for channel expansion from m to n, m/n amplitude on all n chans
// re-sample, scale bits as needed
// wavyjs.prototype.pan = function(start, end){} params are 0-1, 0 = left, 1 = right
// wavyjs.prototype.fade = function(start, end){} fade from start * amp linear to end * amp
// wavyjs.prototype.tremelo = function(min, max, rate){} sin(rate) * (max - min) + min * amp
// wavyjs.prototype.reverb = function(?){}
wavyjs.prototype.mix = function(src, dst_offset, src_offset, vol = 1.0){
var safe_s_off = Math.round(src_offset);
var safe_d_off = Math.round(dst_offset);
var ch, id, is, sd, ss;
for(ch = 0; ch < this.channels; ch++){
for(is = 0; is < src.samples; is++){
id = safe_d_off + (is - safe_s_off);
if((id >= 0) && (id < this.samples)){
sd = this.get_sample(id, ch);
ss = src.get_sample(is, ch);
sd = sd + ss * vol;
this.set_sample(id, ch, sd);
}
}
}
}
// this returns an object with a clip of a portion of the sound in
// the wav file at the given indices
wavyjs.prototype.copy_clip = function(start, end){
"use strict";
var safe_start = Math.trunc(start);
var safe_end = Math.trunc(end);
var bytes = this.bytes * (safe_end - safe_start);
var smps = new ArrayBuffer(bytes);
var dst = new Uint8Array(smps);
var src = new Uint8Array(this.raw);
var s, b;
for(s = safe_start; s < safe_end; s++){
for(b = 0; b < this.bytes; b++){
dst[(s - safe_start) * this.bytes + b] = src[44 + s * this.bytes + b];
}
}
var clip = {start: safe_start, end: safe_end, type: this.type, data: smps};
return clip;
};
// this takes an object in the format of copy_clip and pastes it into
// the wav file at the given indices
wavyjs.prototype.paste_clip = function(clip){
"use strict";
var dst = new Uint8Array(this.raw);
var src = new Uint8Array(clip.data);
var bytes = this.bytes * (clip.end - clip.start);
var s, b;
for(s = clip.start; s < clip.end; s++){
for(b = 0; b < this.bytes; b++){
dst[44 + s * this.bytes + b] = src[(s - clip.start) * this.bytes + b];
}
}
};

+ 1
- 0
wip/wavyjs/wavyjs.min.js
File diff suppressed because it is too large
View File


Loading…
Cancel
Save