audiocogs / aurora.js

JavaScript audio decoding framework
http://audiocogs.org/codecs
1.26k stars 185 forks source link

Can't re-commence playback after playback ends (using flac.js) #104

Open wsot opened 9 years ago

wsot commented 9 years ago

I'm using aurora.js and flac.js to decode FLAC client-side (using Chromium Version 38.0.2125.122 (290379)). I'm finding that once a Player has finished playing, then you can't re-use it for playback of the same file.

I've been trying to implement an ABX tool in Javascript so generally I want to perform many repetitions of the same samples.

Super-simplified code: x = AV.Player.fromURL('http://myserver/tracks/mysample.flac'); x.preload(); x.play(); // plays happily until completion x.seek(0); //try to return to start x.play(); // does nothing

This problem is compounded by tracks not playing if they are retrieved from cache. To test, open dev tools in Chrome, and ensure that on the 'network' tab the 'Disable cache' option is disabled. Then do the commands: x = AV.Player.fromURL('http://myserver/tracks/mysample.flac'); x.preload(); x.play(); // plays happily, then finishes. Let's use the same asset from cache y = AV.Player.fromURL('http://myserver/tracks/mysample.flac'); y.preload(); y.play(); // 'network' tab in dev tools says the file was loaded from cache; trying to play simply doesn't work.

Thus, it seems that in order to create an ABX tool using aurora.js/flac.js, the user would have to download the each sample once per repetition of the track (e.g. 20 times per sample).

master255 commented 9 years ago

@wsot you need to use x.remove(); function after playback end.

wsot commented 9 years ago

There is no x.remove() function, at least not in the latest provided release prebuilts.

alexwhb commented 9 years ago

I'm experiencing the same issue. I've come up with a temporary fix in aurora.js after each this.xhr.open(): I added this this.xhr.setRequestHeader("Cache-Control","no-cache"); which fixes the problem, because the browser no longer tries to access the audio through cache.

wsot commented 9 years ago

@alexwhb I partially resolved the problem of needing to re-download the file each time by modifying the aurora js lib as follows:

--- aurora.js   2014-11-27 11:09:05.000000000 +1100
+++ aurora.mod.js   2014-11-27 11:33:27.000000000 +1100
@@ -3788,7 +3788,8 @@ HTTPSource = (function(_super) {

   HTTPSource.prototype.loop = function() {
     var endPos;
-    if (this.inflight || !this.length) {
+    if (this.inflight) {
+    //if (this.inflight || !this.length) {
       return this.emit('error', 'Something is wrong in HTTPSource.loop');
     }
     this.inflight = true;
@@ -3805,16 +3806,24 @@ HTTPSource = (function(_super) {
             buf[i] = txt.charCodeAt(i) & 0xff;
           }
         }
+
         buffer = new AVBuffer(buf);
         _this.offset += buffer.length;
         _this.emit('data', buffer);
-        if (_this.offset >= _this.length) {
-          _this.emit('end');
+        if ((!_this.length && _this.xhr.readyState==4) ||

This is actually intended to solve two problems, but the problems are basically the same.

  1. Aurora.js doesn't seem to work (i.e. doesn't download the file) if the HTTP Response doesn't have a content length (which means no loading files from Dropbox, etc).
  2. When a file is loaded from cache, it appears to have no 'Content-Length', which means it doesn't get detected as loaded, and so on.

Although this worked for me for a while, I believe problems have arisen with Chrome in some cases since I did it. Also, I only used the code on something I didn't really care about so be wary with that, too.

wsot commented 9 years ago

Note for anyone else having the problem of not being able to re-play tracks: I solved that problem by not using the Aurora player classes, just the Asset ones, and writing code for the Web Audio API myself. I downloaded and decoded files using the Asset, then dumped the results in WebAudio Buffer. The problem with this approach is that each channel needs to be stored in a separate flac file (since there doesn't seem to be any way to pull out one channel at a time from an asset).

Example code: Note: the example code is slapped together from a somewhat larger codebase and hasn't been tested in the provided form. Hopefully it will give you enough of a jump to work out what is going on. (Note: I should point out that I'm decoding files in advance, not as I go, and so there is a decoding-time delay)

window.AudioContext = window.AudioContext || window.webkitAudioContext;
var context = new AudioContext();

var buffer, // this buffer will be a web audio buffer, and hold the ready-to-play data: that way, we can re-use it without needing to re-decode etc
    buffers = {'AL':null, 'AR':null}, // these are buffers used to store the decoded FLAC channels prior to merging theminto a single web audio buffer 
    currentlyPlaying = false,
    virtualStartTime = 0, //used for seeking
    sample = {source: null, gainNode: null, buffer: null};

// get the channels from the decoded single channel, and if all the channels are loaded create a web audio buffer from them (which we can actually play)
function handleDecodeComplete(arr, label) {
    var sampleLength;
    buffers[label] = arr;
    if (buffers['AL'] !== null && buffers['AR'] !== null) {
        sampleLength = Math.max(buffers['AL'].length,buffers['AR'].length);
        buffer = context.createBuffer(2, sampleLength, 44100),
        buffer.getChannelData(0).set(buffers['AL']);
        buffer.getChannelData(1).set(buffers['AR']);
    }
}

//Requires that decoding be complete; initialise the playback Web Audio bits
// this is done for every seek, every new playback, etc. The cost of creating the nods is very small.
function initPlayer() {
    sample.gainNode = context.createGain();
    sample.gainNode.connect(context.destination);
    sample.gainNode.gain.value = 0;

    sample.source = context.createBufferSource();
    sample.source.connect(sample.gainNode);
    sample.source.buffer = buffer;
    sample.gainNode.connect(context.destination);
    sample.source.onended = handleEnd;
}

//starts playing from the beginning, or a specific offset; the '0.10' is to minimise the risk that there will be processing delays prior to playback which are *unknown*. (i.e. I'm allowing 100ms for
// webaudio to get it together and then start playing. More important for me because I was synchronising multiple tracks. 
function playFrom(offset) {
    if (currentlyPlaying) {
        sample.source.stop();
    }

    initPlayer();

    virtualStartTime = context.currentTime + 0.10;
    if (typeof offset == 'undefined') { offset = 0 };
    sample.source.start(0.10, offset);
}

// seek forward or backward by offset seconds
// The way webaudio works is that there you don't seek a track; you create a new player and use that. Once you've created the source node, gain node, etc you use it once then ditch it.
function handleSeek(offset) {
    if (!(currentlyPlaying)) return;

    var currentPosition = context.currentTime - virtualStartTime;
    sample.source.stop();
    initPlayer();

    var newTime = currentPosition + offset;
    if (newTime < 0) { newTime = 0; }
    if (newTime < sample.buffer.duration) {
        virtualStartTime = context.currentTime - newTime;
        playFrom(newTime);
    }
}

function handleEnd() {
    currentlyPlaying = false;
}

// One file for each channel. Joy.
var assetAL = AV.Asset.fromURL(samplePath + filename + '.1.flac'),
    assetAR = AV.Asset.fromURL(samplePath + filename + '.2.flac');

assetAL.decodeToBuffer(function(arr) { handleDecodeComplete(arr, 'AL'); });
assetAR.decodeToBuffer(function(arr) { handleDecodeComplete(arr, 'AR'); });
moechofe commented 9 years ago

@wsot your patch aurora.mod.js seems to be not complete.

wsot commented 9 years ago

(Months later) Sorry, you're right:

<     if (this.inflight || !this.length) {
---
>     if (this.inflight) {
>     //if (this.inflight || !this.length) {
3807a3809
>
3811,3812c3813,3819
<         if (_this.offset >= _this.length) {
<           _this.emit('end');
---
>         if ((!_this.length && _this.xhr.readyState==4) ||
>             (_this.offset >= _this.length)) {
>                   if (!_this.length) {
>                       _this.length = _this.offset;
>                       _this.emit('progress', 100);
>                   }
>                   _this.emit('end');
3814a3822
>
3816c3824
<           return _this.loop();
---
>             return _this.loop();
3817a3826
>

I should point out that this doesn't necessarily work 100%. I haven't used aurora.js in a while so YMMV.