changeset 482:6281b02dcb58 Dev_main

Added WAVE.js, conversion of some C scripts to JS for WAVE file manipulation. By default, will use the WAVE decoder first before fallback to browser decoders. All browsers now support Integer 8-/16-/24-/34-bit and IEEE Float 32 WAVE files.
author Nicholas Jillings <n.g.r.jillings@se14.qmul.ac.uk>
date Sat, 16 Jan 2016 14:51:02 +0000
parents d39c99d83891
children a275f9689af6
files WAVE.js core.js example_eval/radio_example.xml index.html
diffstat 4 files changed, 192 insertions(+), 19 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WAVE.js	Sat Jan 16 14:51:02 2016 +0000
@@ -0,0 +1,154 @@
+// Decode and perform WAVE file byte level manipulation
+
+find_subarray = function(arr,subarr) {
+    var arr_length = arr.length;
+    var subarr_length = subarr.length;
+    var last_check_index = arr_length - subarr_length;
+    
+    positionLoop:
+    for (var i=0; i <= last_check_index; i++)
+    {
+        for (var j=0; j< subarr_length; j++)
+        {
+            if (arr[i + j] !== subarr[j]) {
+                continue positionLoop;
+            }
+        }
+        return i;
+    }
+    return -1;
+};
+
+function WAVE()
+{
+    // The WAVE file object
+    this.status == 'WAVE_DECLARED'
+    
+    this.decoded_data = null;
+    
+    this.RIFF = String(); //ChunkID
+	this.size; //ChunkSize
+	this.FT_Header; //Format
+	this.fmt_marker; //Subchunk1ID
+	this.formatDataLength; //Subchunk1Size
+	this.type; //AudioFormat
+	this.num_channels; //NumChannels
+	this.sample_rate; //SampleRate
+	this.byte_rate; //ByteRate
+	this.block_align; //BlockAlign
+	this.bits_per_sample; //BitsPerSample
+	this.data_header; //Subchunk2ID
+	this.data_size; //Subchunk2Size
+    this.num_samples;
+    
+    this.open = function(IOArrayBuffer)
+    {
+        var IOView8 = new Uint8Array(IOArrayBuffer);
+        IOView8.subarray(0,4).forEach(function(i){
+            var char = String.fromCharCode(i);
+            this.RIFF = this.RIFF.concat(char);
+        },this);
+        if (this.RIFF != 'RIFF')
+        {
+            console.log('WAVE ERR - Not a RIFF file');
+            return 1;
+        }
+        this.size = 0;
+        IOView8.subarray(4,8).forEach(function(i,a){this.size += Number(i)<<(8*a);},this);
+        this.FT_Header = String();
+        IOView8.subarray(8,12).forEach(function(i){this.FT_Header = this.FT_Header.concat(String.fromCharCode(i));},this);
+        this.fmt_marker = String();
+        IOView8.subarray(12,16).forEach(function(i){this.fmt_marker = this.fmt_marker.concat(String.fromCharCode(i));},this);
+        this.formatDataLength = 0;
+        IOView8.subarray(16,20).forEach(function(i,a){this.formatDataLength += Number(i)<<(8*a);},this);
+        this.type = 0;
+        IOView8.subarray(20,22).forEach(function(i,a){this.type += Number(i)<<(8*a);},this);
+        this.num_channels = 0;
+        IOView8.subarray(22,24).forEach(function(i,a){this.num_channels += Number(i)<<(8*a);},this);
+        this.sample_rate = 0;
+        IOView8.subarray(24,28).forEach(function(i,a){this.sample_rate += Number(i)<<(8*a);},this);
+        this.byte_rate = 0;
+        IOView8.subarray(28,32).forEach(function(i,a){this.byte_rate += Number(i)<<(8*a);},this);
+        this.block_align = 0;
+        IOView8.subarray(32,34).forEach(function(i,a){this.block_align += Number(i)<<(8*a);},this);
+        this.bits_per_sample = 0;
+        IOView8.subarray(34,36).forEach(function(i,a){this.bits_per_sample += Number(i)<<(8*a);},this);
+        
+        // Find the data header first
+        var data_start = find_subarray(IOView8,[100, 97, 116, 97]);
+        
+        this.data_header = String();
+        IOView8.subarray(data_start,data_start+4).forEach(function(i){this.data_header = this.data_header.concat(String.fromCharCode(i));},this);
+        this.data_size = 0;
+        IOView8.subarray(data_start+4,data_start+8).forEach(function(i,a){this.data_size += Number(i)<<(8*a);},this);
+        
+        this.num_samples = this.data_size / this.block_align;
+        
+        this.decoded_data = [];
+        if (this.type != 1 && this.type != 3) {
+            console.log("Neither PCM nor IEEE float, cannot decode");
+            return 1;
+        }
+        for (var c=0; c<this.num_channels; c++)
+        {
+            this.decoded_data.push(new Float32Array(this.num_samples));
+        }
+        var sampleDataOffset = data_start+8;
+        
+        // Now need to decode the data from sampleDataOffset
+        // Data is always interleved
+        var data_view;
+        if (this.type == 3)
+        {
+            // Already in float
+            if (this.bits_per_sample == 32) {
+                data_view = new Float32Array(IOArrayBuffer.slice(sampleDataOffset,sampleDataOffset+this.data_size));
+            } else if (this.bits_per_sample == 64) {
+                data_view = new Float64Array(IOArrayBuffer.slice(sampleDataOffset,sampleDataOffset+this.data_size));
+            }
+        } else if (this.type == 1)
+        {
+            data_view = new Float32Array(this.num_samples);
+            integerConvert(new Uint8Array(IOArrayBuffer.slice(sampleDataOffset,sampleDataOffset+this.data_size)),data_view,this.bits_per_sample/8);
+        }
+        deInterlace(data_view,this.decoded_data);
+        return 0;
+    };
+}
+
+function deInterlace(src_array, dst_array)
+{
+    var number = src_array.length;
+    var channels = dst_array.length;
+    var channel_index = 0;
+    var dst_index = 0;
+    for (var n=0; n<number; n++)
+    {
+        dst_array[channel_index][dst_index] = src_array[n];
+        channel_index++;
+        if (channel_index >= channels) {
+            channel_index = 0;
+            dst_index++;
+        }
+    }
+}
+
+function integerConvert(srcView,dstView,srcBytes)
+{
+    //Convert integers of a Uint8Array of certain byte length into a Float32Array
+    var number = dstView.length;
+    var outBits = srcBytes*8;
+    var endShift = 32 - outBits;
+    if (srcView.length != dstView.length*srcBytes)
+    {
+        return -1;
+    }
+    for (var n=0; n<number; n++)
+    {
+        var intData;
+        var srcIndex = n*srcBytes;
+        srcView.subarray(srcIndex,srcIndex+srcBytes).forEach(function(i,a){intData += Number(i)<<(8*a);},this);
+        intData = (intData << (endShift));
+        dstView[n] = intData / 2147483648;
+    }
+}
\ No newline at end of file
--- a/core.js	Fri Jan 15 11:04:23 2016 +0000
+++ b/core.js	Sat Jan 16 14:51:02 2016 +0000
@@ -991,24 +991,42 @@
 			
 			// Create callback to decode the data asynchronously
 			this.xmlRequest.onloadend = function() {
-				audioContext.decodeAudioData(bufferObj.xmlRequest.response, function(decodedData) {
-					bufferObj.buffer = decodedData;
-					calculateLoudness(bufferObj,"I");
-					
-				}, function(){
-					// Should only be called if there was an error, but sometimes gets called continuously
-					// Check here if the error is genuine
-					if (bufferObj.buffer == undefined) {
-						// Genuine error
-						console.log('FATAL - Error loading buffer on '+audioObj.id);
-						if (request.status == 404)
-						{
-							console.log('FATAL - Fragment '+audioObj.id+' 404 error');
-							console.log('URL: '+audioObj.url);
-							errorSessionDump('Fragment '+audioObj.id+' 404 error');
-						}
-					}
-				});
+                // Use inbuilt WAVE decoder first
+                var waveObj = new WAVE();
+                if (waveObj.open(bufferObj.xmlRequest.response) == 0)
+                {
+                    bufferObj.buffer = audioContext.createBuffer(waveObj.num_channels,waveObj.num_samples,waveObj.sample_rate);
+                    for (var c=0; c<waveObj.num_channels; c++)
+                    {
+                        var buffer_ptr = bufferObj.buffer.getChannelData(c);
+                        for (var n=0; n<waveObj.num_samples; n++)
+                        {
+                            buffer_ptr[n] = waveObj.decoded_data[c][n];
+                        }
+                    }
+                    delete waveObj;
+                } else {
+                    audioContext.decodeAudioData(bufferObj.xmlRequest.response, function(decodedData) {
+                        bufferObj.buffer = decodedData;
+                    }, function(e){
+                        // Should only be called if there was an error, but sometimes gets called continuously
+                        // Check here if the error is genuine
+                        if (bufferObj.xmlRequest.response == undefined) {
+                            // Genuine error
+                            console.log('FATAL - Error loading buffer on '+audioObj.id);
+                            if (request.status == 404)
+                            {
+                                console.log('FATAL - Fragment '+audioObj.id+' 404 error');
+                                console.log('URL: '+audioObj.url);
+                                errorSessionDump('Fragment '+audioObj.id+' 404 error');
+                            }
+                        }
+                    });
+                }
+                if (bufferObj.buffer != undefined)
+                {
+                    calculateLoudness(bufferObj,"I");
+                }
 			};
 			this.progress = 0;
 			this.progressCallback = function(event){
--- a/example_eval/radio_example.xml	Fri Jan 15 11:04:23 2016 +0000
+++ b/example_eval/radio_example.xml	Sat Jan 16 14:51:02 2016 +0000
@@ -17,7 +17,7 @@
 			<interfaceoption type="show" name="page-count"/>
 		</interface>
 	</setup>
-	<page id='test-0' hostURL="example_eval/" randomiseOrder='true' repeatCount='0' loop='true' showElementComments='true' loudness="-12">
+	<page id='test-0' hostURL="example_eval/" randomiseOrder='true' repeatCount='0' loop='true' showElementComments='true' loudness="-23">
 		<interface>
 			<scales>
 				<scalelabel position="0">(1) Very Annoying</scalelabel>
--- a/index.html	Fri Jan 15 11:04:23 2016 +0000
+++ b/index.html	Sat Jan 16 14:51:02 2016 +0000
@@ -20,6 +20,7 @@
 		<script src='core.js'></script>
 		<script src='loudness.js'></script>
 		<script src='xmllint.js'></script>
+        <script src='WAVE.js'></script>
 		<script type="text/javascript">
 			// SEARCH QUERY: By using the GET Request option ?url=loca/path/to/project.xml in the URL bar, you can load a project quickly
 			if (window.location.search.length != 0)