How to pipe Howler.masterGain output to mediaRecorder - web-audio-api

I'm trying to record composed Web-Audio-API output from Howler (a bunch of sounds and audio sprites combined programmatically).
So far I've tried connecting the Howler.masterGain context to a createMediaStreamDestination Node but the MediaRecorder.ondataavailable event only fires once and without data.
Here's an example with a wired up version here: https://jsfiddle.net/chadananda/kvzrsx5t/86/
const audioURL = 'https://cdn.glitch.com/02dcea11-9bd2-4462-ac38-eeb6a5ad9530%2F331_full_beautiful-minds_0171_preview.mp3?1522829295082'
// connect MediaStreamDestination to Howler.masterGain
Howler.mute(false) // to initialize Howler internals
let mediaDest = Howler.ctx.createMediaStreamDestination()
// first disconnect
Howler.masterGain.disconnect()
// then reconnect to our new destination?
Howler.masterGain.connect(mediaDest)
// set up media recorder to record output
let audioChunks = []
let mediaRecorder = new MediaRecorder(mediaDest.stream, {mimeType: 'audio/webm'})
mediaRecorder.onstart = (event) => { console.log('Started recording Howl output...') }
mediaRecorder.ondataavailable = (e) => { if (e.data.size) audioChunks.push(e.data) }
mediaRecorder.onstop = (event) => {
console.log('Completed Recording', audioChunks) // why is this returning empty?
// let buffer = new Blob(chunks)
// let audioPlayer = document.createElement("AUDIO")
// audioPlayer.src = window.URL.createObjectURL(buffer)
// audioPlayer.play()
}
// example of recording one sound looping for 3 seconds
let sound = new Howl({ html5: false, src: audioURL })
// start sound and recording
sound.play(); mediaRecorder.start()
// stop in a few seconds
setTimeout( ()=>{ mediaRecorder.stop(); sound.stop() }, 5000)

The recording seems to be blocked by the autoplay policy which prevents you from playing anything without a user interaction. If you trigger the recording from within a click handler it works. I created an updated version of your fiddle which does that.
https://jsfiddle.net/786zkLcu/
I only changed the last lines. The code now generates a dynamic button and attaches a click handler. This also means that the user has to click the button to start the recording.
const $button = document.createElement('button');
$button.innerHTML = 'click';
$button.onclick = () => {
sound.play();
mediaRecorder.start();
setTimeout(() => {
mediaRecorder.stop();
sound.stop();
}, 5000);
};
document.body.append($button);

The problem is that Howler.masterGain changes after you create your Howl instance:
const audioURL = 'https://cdn.glitch.com/02dcea11-9bd2-4462-ac38-eeb6a5ad9530%2F331_full_beautiful-minds_0171_preview.mp3?1522829295082';
const masterGain = Howler.masterGain;
const sound = new Howl({ html5: false, src: audioURL });
sound.play();
console.log( masterGain === Howler.masterGain ); // false
<script src="https://cdnjs.cloudflare.com/ajax/libs/howler/2.1.3/howler.min.js"></script>
So the one you connect to your MediaStreamDestination node is not the one where the audio data will be passed, and hence it records only silence.
I don't realy know this library, so I can't tell why it behaves like that, or if there is a way to make it behave differently, but a simple workaround is to initialize your stream and create your recorder only after you initialized your Howl instance:
onclick = e => { onclick = null; msg.remove();
const audioURL = 'https://cdn.glitch.com/02dcea11-9bd2-4462-ac38-eeb6a5ad9530%2F331_full_beautiful-minds_0171_preview.mp3?1522829295082'
Howler.mute(false) // to initialize Howler internals
// first initiate your Howl instance
const sound = new Howl( { html5: false, src: audioURL } );
sound.play();
// now intitiate your MediaStreamDestination
const mediaDest = Howler.ctx.createMediaStreamDestination();
Howler.masterGain.connect( mediaDest );
// set up media recorder to record output
const audioChunks = []
const mediaRecorder = new MediaRecorder( mediaDest.stream, { mimeType: 'audio/webm' } );
mediaRecorder.onstart = (event) =>
{ console.log('Started recording Howl output...') };
mediaRecorder.ondataavailable = (event) =>
{ audioChunks.push( event.data ) };
mediaRecorder.onstop = (event) =>
{ console.log('Completed Recording', new Blob( audioChunks ) ) };
mediaRecorder.start();
setTimeout( ()=>{ mediaRecorder.stop(); sound.stop() }, 5000);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/howler/2.1.3/howler.min.js"></script>
<pre id="msg">click anywhere to start</pre>

Related

OfflineAudioContext Latency Shift

I'm using OfflineAudioContext to download an input file with effects applied. The download works great and it's really fast, but the problem I'm running into is that when I apply gain, I'm using an analyser to signal when the gain should increase or decrease.
This works great when playing the audio using an AudioContext, but the offline version causes the timing of the gain to shift very noticeably. The increase is starting late and the decrease is starting late. It's like there's a latency shift overall.
Is there a way to combat this shift? I'm fine with the rendering process taking longer.
var chunks = [];
var fileInput = document.getElementById("input");
var process = document.getElementById("process");
//Load audio file listener
process.addEventListener(
"click",
function () {
// Web Audio
var audioCtx2 = new (AudioContext || webkitAudioContext)();
// Reset buttons and log
$("#log").empty();
$("#download_link").addClass("d-none");
$("#repeat_link").addClass("d-none");
// Check for file
if (fileInput.files[0] == undefined) {
if ($("#upload_err").hasClass("d-none")) {
$("#upload_err").removeClass("d-none");
}
return false;
}
var reader1 = new FileReader();
reader1.onload = function (ev) {
// console.log("Reader loaded.");
var tempBuffer = audioCtx2.createBufferSource();
// Decode audio
audioCtx2.decodeAudioData(ev.target.result).then(function (buffer) {
// console.log("Duration1 = " + buffer.duration);
var offlineAudioCtx = new OfflineAudioContext({
numberOfChannels: 2,
length: 44100 * buffer.duration,
sampleRate: 44100,
});
// console.log("test 1");
// Audio Buffer Source
var soundSource = offlineAudioCtx.createBufferSource();
var analyser2d = offlineAudioCtx.createAnalyser();
var dgate1 = offlineAudioCtx.createGain();
var dhpf = offlineAudioCtx.createBiquadFilter();
var dhum60 = offlineAudioCtx.createBiquadFilter();
var dcompressor = offlineAudioCtx.createDynamicsCompressor();
dhpf.type = "highpass";
dhpf.Q.value = 0.5;
dhum60.type = "notch";
dhum60.Q.value = 130;
dcompressor.knee.setValueAtTime(40, offlineAudioCtx.currentTime);
dcompressor.attack.setValueAtTime(0.1, offlineAudioCtx.currentTime);
dcompressor.release.setValueAtTime(0.2, offlineAudioCtx.currentTime);
var reader2 = new FileReader();
// console.log("Created Reader");
reader2.onload = function (ev) {
// console.log("Reading audio data to buffer...");
$("#log").append("<p>Buffering...</p>");
soundSource.buffer = buffer;
let context = offlineAudioCtx;
//Before Effects
analyser2d = context.createAnalyser();
analyser2d.fftSize = 2048;
analyser2d.smoothingTimeConstant = 0.85;
const sampleBuffer = new Float32Array(analyser2d.fftSize);
function loop() {
analyser2d.getFloatTimeDomainData(sampleBuffer);
let sumOfSquares = 0;
for (let i = 0; i < sampleBuffer.length; i++) {
sumOfSquares += sampleBuffer[i] ** 2;
}
const avgPowerDecibels = Math.round(
10 * Math.log10(sumOfSquares / sampleBuffer.length)
);
const gainset = avgPowerDecibels > -50 ? 1 : 0;
//real-time effects choices start
if (
document.getElementById("gate").getAttribute("data-active") ===
"true"
) {
dgate1.gain.setTargetAtTime(
gainset,
offlineAudioCtx.currentTime,
0.05
);
} else if (
document.getElementById("gate").getAttribute("data-active") ===
"false"
) {
dgate1.gain.setTargetAtTime(
1,
offlineAudioCtx.currentTime,
0.05
);
}
if (
document.getElementById("hpf").getAttribute("data-active") ===
"true"
) {
dhpf.frequency.value = 90;
} else if (
document.getElementById("hpf").getAttribute("data-active") ===
"false"
) {
dhpf.frequency.value = 0;
}
if (
document.getElementById("hum").getAttribute("data-active") ===
"true"
) {
dhum60.frequency.value = 60;
} else if (
document.getElementById("hum").getAttribute("data-active") ===
"false"
) {
dhum60.frequency.value = 0;
}
if (
document.getElementById("comp").getAttribute("data-active") ===
"true"
) {
dcompressor.threshold.setValueAtTime(
-30,
offlineAudioCtx.currentTime
);
dcompressor.ratio.setValueAtTime(
3.5,
offlineAudioCtx.currentTime
);
} else if (
document.getElementById("comp").getAttribute("data-active") ===
"false"
) {
dcompressor.threshold.setValueAtTime(
0,
offlineAudioCtx.currentTime
);
dcompressor.ratio.setValueAtTime(1, offlineAudioCtx.currentTime);
}
// Display value.
requestAnimationFrame(loop);
}
loop();
soundSource
.connect(analyser2d)
.connect(dhpf)
.connect(dhum60)
.connect(dgate1)
.connect(dcompressor);
dcompressor.connect(offlineAudioCtx.destination);
offlineAudioCtx
.startRendering()
.then(function (renderedBuffer) {
// console.log('Rendering completed successfully.');
$("#log").append("<p>Rendering new file...</p>");
//var song = offlineAudioCtx.createBufferSource();
console.log(
"OfflineAudioContext.length = " + offlineAudioCtx.length
);
split(renderedBuffer, offlineAudioCtx.length);
$("#log").append("<p>Finished!</p>");
})
.catch(function (err) {
// console.log('Rendering failed: ' + err);
$("#log").append("<p>Rendering failed.</p>");
});
soundSource.loop = false;
};
reader2.readAsArrayBuffer(fileInput.files[0]);
soundSource.start(0);
});
};
reader1.readAsArrayBuffer(fileInput.files[0]);
},
false
);
I've included what I believe is the relevant portion of the code. Let me know if more is needed. Thanks!
This is doesn't work because the OfflineAudioContext runs faster (sometimes hundreds of times faster) than realtime. Your loop that gets the analyser data is done every 16 ms or so. In a realtime system, this might be accurate enough, but the offline context could be running much, much faster than realtime so by the time you've grabbed the analyser data, much more time has passed. (You can probably see this by printing out the context currentTime in the loop. It will probably increment by more (much more) than 16 ms.
The best way to do this is to use context.suspend(t) to suspend the context at known times so you can grab the analyser data synchronously. Note that the time is rounded so it might not be exactly the time you want but perhaps close enough. Note also that AFAIK, Firefox has not implemented this, and neither has Safari (but will soon).
Here's a short snippet with how do use suspend. There's more than one way though. Untested, but I think the general idea is correct:
// Grab the data every 16 ms, roughly. `suspend` rounds the time up to
// the nearest multiple of 128 frames (or 128/context.sampleRate time).
for (t = 0; t < <length of buffer>; t += 0.016) {
context.suspend(t)
.then(() => {
// Use analyser to grab the data and compute the values.
// Use setValueAtTime and friends to adjust the gain and compressor
// appropriately. Use context.currentTime to know at what time
// the context was suspended, and schedule the modifications to be
// at least 128 samples in the future. (I think).
})
.then(() => context.resume());
}
An alternative is to create a realtime context and process it as you do now, and when the buffer is finished playing, close the context. Of course, you'll have to add something (ScriptProcessorNode, AudioWorkletNode, or MediaRecorder) to capture the rendered data.
If none of these work for you, then I'm not sure what the alternatives would be.

Protractor upload stuck when filesearch pops up

I'm trying to figure out what's wrong with the code below:
it('should upload a photo', function(){
var photo = './photos/et-test.jpeg',
exactPhoto = path.resolve(__dirname, photo);
var form = element(by.id('fileupload'));
var upload = element(by.css('input[type = "file"]'));
var addFiles = element(by.cssContainingText('.btn.btn-success.fileinput-button.mb-10','Add files...'));
var uploadBtn = element(by.css('.btn.btn-primary.start.mt-20'));
element(by.cssContainingText('.inline_link','Upload more album photos now')).click();
element(by.id('secondary_upload_link')).click();
browser.wait(EC.visibilityOf(addFiles), 5000);
addFiles.click();
upload.sendKeys(exactPhoto);
browser.wait(EC.visibilityOf(uploadBtn), 5000);
uploadBtn.click();
expect(element(by.css('.table')).getText()).toBe('Upload Finised');
});
I keep getting stuck on the filesearch popup and receive this error:
Message:
Failed: Wait timed out after 5006ms
Is there anything lacking or should've been done based on the flow of my code?
If I'm understanding your issue correctly you are seeing a popup window when uploading a file. Can you try the following capability in your conf (assuming you are using Chrome)
capabilities: {
browserName: 'chrome',
chromeOptions: {
prefs: {
download: {
'prompt_for_download': false,
'directory_upgrade': true,
'default_directory': 'src/test/javascript/e2e/downloads'(or where ever you prefer)
}
}
}
}
Yes, you don't need to click on it, just sendKeys() for this input element
Sometimes it needs to make this input visible. Just try like that (it works in my case for await/async protractor):
private addAttachment = element(by.css('input[type="file"]'))
await browser.executeScript("arguments[0].style.visibility = 'visible'; arguments[0].style.height = '1px'; arguments[0].style.width = '1px'; arguments[0].style.opacity = 1; arguments[0].style.display = 'inline'; arguments[0].style.overflow = 'visible'", this.addAttachment)
await browser.executeScript("arguments[0].focus();", await this.addAttachment.getWebElement())
await this.addAttachment.sendKeys(path)
Try this:
var photo = './photos/et-test.jpg',
exactPhoto = path.resolve(__dirname, photo);
var form = element(by.id('fileupload')); ///Users/leochardc/ET/photos/et-test.jpg
var upload = element(by.css('.fileupload_section input[type = "file"]'));
var addFiles = element(by.cssContainingText('.btn.btn-success.fileinput-button.mb-10','Add files...'));
var uploadBtn = element(by.css('.btn.btn-primary.start.mt-20'));
var uploadFinished = element(by.css('.fileupload_section .text-center p'));
browser.get(url);
element(by.cssContainingText('.inline_link','Upload more album photos now')).click();
element(by.id('secondary_upload_link')).click();
browser.wait(EC.visibilityOf(addFiles), 5000);
// addFiles.click();
upload.sendKeys(exactPhoto);
var previewPic = element(by.css('.fileupload_section.fileupload_files'));
browser.wait(EC.visibilityOf(previewPic), 5000);
browser.wait(EC.visibilityOf(uploadBtn), 5000);
uploadBtn.click();
browser.wait(EC.visibilityOf(uploadFinished), 30000);
expect(uploadFinished.getText()).toBe('Upload Finished');

How could I load video files from my library? Ionic 3

I observed that the Native File has not been supported by the Ionic View anymore see list here.
I am trying to get a video from my library by using Native Camera to access the videos. It can return me 3 different formats of path to my videos (DATA_URL, FILE_URI, and NATIVE_URI).reference to Native Camera here
I am currently using FILE_URI as recommended in this post. It returns something like "/storage/emulated/0/DCIM/Camera/VID_20180312_210545.mp4"
Please have a look at my code below. Aiming a better understanding, the current behavior is highlighted by comments with "//** comment ***" :
addVideoToOffer(){
this.platform.ready().then(() =>{
const options: CameraOptions = {
sourceType: this.camera.PictureSourceType.PHOTOLIBRARY,
destinationType: this.camera.DestinationType.FILE_URI,
mediaType: this.camera.MediaType.VIDEO,
}
this.camera.getPicture(options).then((data_uri) => {
this.readVideoFileasGeneral(data_uri);
});
});
}
readVideoFileasGeneral(data_uri) {
if(!data_uri.includes('file://')) {
data_uri = 'file://' + data_uri;
}
return this.file.resolveLocalFilesystemUrl(data_uri)
.then((entry: FileEntry) => {
//***it does not get in here***
this.presentQuickToastMessage(data_uri);
return new Promise((resolve)=>{//, reject) => {
entry.file((file) => {
let fileReader = new FileReader();
fileReader.onloadend = () => {
let blob = new Blob([fileReader.result], {type: file.type});
resolve({blob: blob, file: file});
};
fileReader.readAsArrayBuffer(file);
});
})
})
.catch((error) => {
this.presentQuickToastMessage(error);
//***it presents "plugin_not_installed" here***
});
}
I understand that I am having this message because Native File is not supported anymore (maybe reason of the plugin_not_installed message). However, I still have to do this task. So, if someone has any idea of what I could be using in order to have the selected videos in a blob, it would be great!
Thanks for reading until here,
Cheers,
Roger A L
makeFileIntoBlob(uri) {
// get the correct path for resolve device file system
let pathIndex = uri.indexOf('var');
let correctPath = uri.slice(+pathIndex);
this.file
.resolveLocalFilesystemUrl((this.platform.is('ios') ? 'file:///' : '') + correctPath)
.then(entry => (<FileEntry>entry).file(file => this.readFile(file)))
.catch(err => console.log('ERROR: ', err));
}
readFile(file) {
if(file) {
const reader = new FileReader();
reader.onloadend = () => {
const blob: any = new Blob([reader.result], { type: file.type });
blob.name = file.name;
console.log(blob);
return blob;
};
reader.readAsArrayBuffer(file);
}
}
You need to get rid of the /private/ and keep file:///, so that your path goes like file:///var/
I'm currently working on something similar.. I have the video recorded with media-capture and then I can display it within a normal video html tag.. if this is all you need then this code may help you...
this.mediaCapture.captureVideo({duration: 10, quality: 0}).then(
(data: MediaFile[]) => {
if (data.length > 0) {
let originname = data[0].fullPath.substr(data[0].fullPath.lastIndexOf('/') + 1);
let originpath = data[0].fullPath.substr(0, data[0].fullPath.lastIndexOf('/') + 1);
let alerta = this.alerts.create({
buttons: ['ok'],
message: this.file.externalDataDirectory
});
alerta.then(set => set.present());
this.file.copyFile(originpath, originname, this.file.externalDataDirectory, 'video.mp4')
.then(result =>{
let videopath = this.webview.convertFileSrc(result.nativeURL)
let video = (document.getElementById('myvideo') as HTMLVideoElement).src = videopath;
.... rest of the code
The problem raise when you try to use the native File plugin... converting files with any method (readAsDataURL, readAsArrayBuffer or readAsBinaryString) will never resolve, this is a known problem with the Ionic Native File plugin but is not taken care of...
What I did is to take the ionic native Filesystem and use it to read the file, this does read the file and get you with a base64 (pretty sure as I don't specify the encoding field) and then you can handle it the way you want...
const data = Filesystem.readFile({
path: result.nativeURL
})
.then(data =>{
...handle data as base64
...rest of the code

Chrome App FileReader

I'm trying to make use of the file system API in a Chrome App. I've tried all the sample code I can find and can't get a simple text file to read. I'm logging almost every step, and what seems to happen (or not happen) is everything stops the first time I reference a file reader object. It creates just fine, because I can log the .readyState, but after that I can't seem to even set an onload()event or execute a .readAsText().
Here's what I'm calling from a button:
function clickButton(){
chrome.fileSystem.chooseEntry({type: 'openFile', acceptsMultiple: false}, function(FileEntry){
if(chrome.runtime.lastError) {console.warn("Warning: " + chrome.runtime.lastError.message);}
else{
console.log(FileEntry);
var thing = new FileReader();
console.log(thing.readyState);
thing.onloadstart(function(){
console.log("Started loading " & FileEntry);
});
console.log("added onloadstart");
console.log(thing.readyState);
console.log(thing);
thing.readAsText(FileEntry);
console.log(thing.readyState);
console.log(thing.result);
}
});
document.getElementById("status").innerHTML = "I did something";
}
I did read somewhere that Chrome doesn't allow access to local files, but the chrome apps seem to be different. At least, the documentation seems to suggest that.
The only thing I end up with in my console is the FileEntry object.
https://developer.chrome.com/apps/app_storage#filesystem
I've used the example code right from the above link and still can't get it right. Anyone else have this issue or know what I'm doing wrong?
There is a difference between a FileEntry and a File. You need to call FileEntry's .file() method. So, replace
thing.readAsText(FileEntry);
with
FileEntry.file(function(File) {
thing.readAsText(File)
})
https://developer.mozilla.org/en-US/docs/Web/API/FileEntry#File
Try this code...
<!doctype html>
<html>
<script>
function handle_files(files) {
for (i = 0; i < files.length; i++) {
file = files[i]
console.log(file)
var reader = new FileReader()
ret = []
reader.onload = function(e) {
console.log(e.target.result)
}
reader.onerror = function(stuff) {
console.log("error", stuff)
console.log (stuff.getMessage())
}
reader.readAsText(file) //readAsdataURL
}
}
</script>
<body>
FileReader that works!
<input type="file" multiple onchange="handle_files(this.files)">
</body>
</html>
I've written a function to extract text from a file.
function getFileEntryText(fileEntry) {
return new Promise(function (resolve, reject) {
fileEntry.file(function (file) {
var fileReader = new FileReader();
fileReader.onload = function (text) {
resolve(fileReader.result);
};
fileReader.onerror = function () {
reject(fileReader.error);
};
fileReader.readAsText(file);
});
});
}
You can invoke this method like so:
getFileEntryText(fileEntry).then(function(text) {
// Process the file text here
}, function(error) {
// Handle the file error here
});
One thing I'm grappling with when working with the FileSystem is that every call is asynchronous. Having multiple levels of nested callbacks can make for code that's hard to read. I'm currently working around this by converting everything I can to a Promise.
for anyone who is interested, here's my final (working) code, complete with all the console.log()'s I needed to follow all those callbacks.
var chosenEntry = null;
function clickButton(){
console.log("Button clicked");
var accepts = [{
mimeTypes: ['text/*'],
extensions: ['js', 'css', 'txt', 'html', 'xml', 'tsv', 'csv', 'rtf']
}];
chrome.fileSystem.chooseEntry({type: 'openFile', accepts: accepts}, function(theEntry) {
if (!theEntry) {
output.textContent = 'No file selected.';
return;
}
// use local storage to retain access to this file
chrome.storage.local.set({'chosenFile': chrome.fileSystem.retainEntry(theEntry)});
console.log("local data set. calling loadFileEntry");
loadFileEntry(theEntry);
console.log("loadFileEntry called, returned to clickButton()");
});
}
function loadFileEntry(_chosenEntry) {
console.log("entered loadFileEntry()");
chosenEntry = _chosenEntry;
chosenEntry.file(function(file) {
readAsText(chosenEntry, function(result) {
console.log("running callback in readAsText");
document.getElementById('text').innerHTML = result;
console.log("I just tried to update textarea.innerHTML");
});
});
console.log("added function to chosenEntry.file()");
}
function readAsText(fileEntry, callback) {
console.log("readAsText called");
fileEntry.file(function(file) {
var reader = new FileReader();
console.log("Created reader as FileReader");
reader.onload = function(e) {
console.log("called reader.onload function");
callback(e.target.result);
};
console.log("calling reader.readAsText");
reader.readAsText(file);
});
}

Streaming through SoundManager2 for webradios is pending for a very long time

I'm developping a little javascript snippet that allow a user to listen a webradio in streaming using soundManager2. By giving an URL to this snippet, the stream is sending me music so it's cool, it works.
Yes, but...
Query to the URL can be very very long (above 2-3 min!). I would like to know if there's a workaround or option I can use to make this query faster.
Why, opening my m3u (which just contains the mp3 URL inside) with Windows Media Player, the loading spend only 5-10 sec max, while acces to the same URL in a browser or with soundManager2 is during 2-3 min, sometimes more?
Here is my jsFiddle trying with the OuïFM (French radio). The waiting time, for this radio, is about 115 seconds.
var url = 'http://ice39.infomaniak.ch:8000/ouifm3.mp3';
var time = 0;
var timing = setInterval(function() {
$('#Streaming').html((time / 10) + ' seconds.');
time++;
}, 100);
var start = new Date;
soundManager.onready(function() {
soundManager.createSound({
id:'Radio',
url:url,
autoLoad: true,
autoPlay: true,
multiShot: false,
onload: function() {
$('#Loading').css('display', 'none');
clearInterval(timing);
}
});
});
Your fiddle was broken because SoundManager2's javascript wasn't loading properly. After I fixed that (and some of your code), I was able to get the audio playing in under a second: http://jsfiddle.net/y8GDp/
var url = 'http://ice39.infomaniak.ch:8000/ouifm3.mp3';
var time = 0;
var timer = $('#Streaming');
var loading = $('#Loading');
var start = new Date;
var seconds = function(){
var diff = ((new Date).getTime() - start.getTime()) / 1000;
return diff + ' seconds.';
};
var timing = setInterval(function() {
timer.html(seconds());
}, 100);
soundManager.onready(function() {
soundManager.createSound({
id:'Radio',
url:url,
autoPlay: true,
onplay: function() {
clearInterval(timing);
loading.html('Finished loading');
timer.html(seconds());
}
});
});