App-MHFS

 view release on metacpan or  search on metacpan

share/public_html/static/music_inc/music_inc_module.js  view on Meta::CPAN

        }            
    }    
    
    return track;
}

function _PlayTrack(trackname) {
    let queuePos;
    if(AQ_ID() !== -1) {
        queuePos = AudioQueue[0].track;
    }
    else if(Tracks_QueueCurrent) {
        queuePos = Tracks_QueueCurrent;
    }
    
    let queueAfter; //falsey is tail
    if(queuePos) {
        queueAfter = queuePos.next;        
    }    

    AQ_stopAudioWithoutID(-1);
    Tracks_QueueCurrent = null;
    return _QueueTrack(trackname, queuePos, queueAfter);   
}

// BuildPTrack is expensive so _QueueTrack and _PlayTrack don't call it
function QueueTrack(trackname, after, before) {
    let res = _QueueTrack(trackname, after, before);
    BuildPTrack();
    return res;
}

function PlayTrack(trackname) {
    let res = _PlayTrack(trackname);
    BuildPTrack();
    return res;
}

function QueueTracks(tracks, after) {
    tracks.forEach(function(elm) {
        after = _QueueTrack(elm, after);
    });
    BuildPTrack();
    return after;
}

function PlayTracks(tracks) {
    let trackname = tracks.shift();
    if(!trackname) return;
    let after = _PlayTrack(trackname);
    if(!tracks.length) return;  
    QueueTracks(tracks, after);
}

// remove played items from the audio queue
function AQ_clean() {
    let toDelete = 0;
    for(let i = 0; i < AudioQueue.length; i++) {
        if(! AudioQueue[i].endTime) break;        

        // run and remove associated graphics timers
        let timerdel = 0;
        for(let j = 0; j < AudioQueue[i].timers.length; j++) {
            if(AudioQueue[i].timers[j].time <= MainAudioContext.currentTime) {
                console.log('aqid: ' + AudioQueue[i].aqid + ' running timer at ' + MainAudioContext.currentTime);
                AudioQueue[i].timers[j].func(AudioQueue[i].timers[j]);
                timerdel++;
            }
        }
        if(timerdel)AudioQueue[i].timers.splice(0, timerdel);
        
        // remove if it has passed
        if(AudioQueue[i].endTime <= MainAudioContext.currentTime) {
            console.log('aqid: ' + AudioQueue[i].aqid + ' segment elapsed, removing');
            toDelete++;
        }
    }
    if(toDelete) {
        // if the AQ is empty and there's a current track we fell behind
        if((toDelete === AudioQueue.length) && (Tracks_QueueCurrent)) {
            SetPlayText(Tracks_QueueCurrent.trackname + ' {LOADING}');
        }
        AudioQueue.splice(0, toDelete);
    }
}

//imprecise, in seconds
function AQ_unqueuedTime() { 
    let unqueuedtime = 0;
    for(let i = 0; i < AudioQueue.length; i++) {
        if(!AudioQueue[i].startTime) {
            unqueuedtime += (AudioQueue[i].duration / AudioQueue[i].track.sampleRate);
        }        
    }
    return unqueuedtime;
}

// returns the currently or about to be playing aqid
function AQ_ID() {
    AQ_clean();     
    for(let i = 0; i < AudioQueue.length; i++) {        
        return AudioQueue[i].aqid;                    
    }
    return -1;
}

function AQ_stopAudioWithoutID(aqid) {
    if(!AudioQueue.length) return;
    let dCount = 0;
    for(let i = AudioQueue.length-1; i >= 0; i--) {
        if(AudioQueue[i].aqid === aqid) {
            break;
        }
        dCount++;
        if(AudioQueue[i].source) {
            AudioQueue[i].source.disconnect();
            AudioQueue[i].source.stop();
        }
        console.log('aqid: ' + AudioQueue[i].aqid + ' AQ_stopAudioWithoutID delete, curr: ' + aqid);
    }
    if(dCount) {
        AudioQueue.splice(AudioQueue.length - dCount, dCount);
    }    
}

if(typeof abortablesleep === 'undefined') {
    //const sleep = m => new Promise(r => setTimeout(r, m));
    const abortablesleep = (ms, signal) => new Promise(function(resolve) {
        const onTimerDone = function() {
            resolve();
            signal.removeEventListener('abort', stoptimer);
        };
        let timer = setTimeout(function() {
            console.log('sleep done ' + ms);
            onTimerDone();
        }, ms);

        const stoptimer = function() {
            console.log('aborted sleep');            
            onTimerDone();
            clearTimeout(timer);            
        };
        signal.addEventListener('abort', stoptimer);
    });
    DeclareGlobalFunc('abortablesleep', abortablesleep);
}

let FAQ_MUTEX = new Mutex();
async function fillAudioQueue(time) {
    // starting a fresh queue, render the text
    if(Tracks_QueueCurrent) {
        let track = Tracks_QueueCurrent;
        let prevtext = track.prev ? track.prev.trackname : '';
        SetPrevText(prevtext);
        SetPlayText(track.trackname + ' {LOADING}');
        let nexttext =  track.next ? track.next.trackname : '';
        SetNextText(nexttext);
        SetCurtimeText(time || 0);
        if(!time) SetSeekbarValue(time || 0);
        SetEndtimeText(track.duration || 0);        
    }

    // Stop the previous FAQ before starting
    FACAbortController.abort();
    FACAbortController = new AbortController();
    let mysignal = FACAbortController.signal;
    let unlock = await FAQ_MUTEX.lock();    
    if(mysignal.aborted) {
        console.log('abort after mutex acquire');
        unlock();
        return;
    }
    let initializing = 1;
    
TRACKLOOP:while(1) {
        // advance the track
        AQID++;
        if(!initializing) {
            if(!document.getElementById("repeattrack").checked) {
                Tracks_QueueCurrent = Tracks_QueueCurrent.next;
            }
        }
        initializing = 0;        
        let track = Tracks_QueueCurrent;
        if(! track) {
            unlock();
            return;
        }
        
        // cleanup nwdrflac
        if(NWDRFLAC) {
            // we can reuse it if the urls match
            if(NWDRFLAC.url !== track.url)
            {
                await NWDRFLAC.close();
                NWDRFLAC = null;
                if(mysignal.aborted) {
                    console.log('abort after cleanup');
                    unlock();
                    return;
                }
            }
            else{

share/public_html/static/music_inc/music_inc_module.js  view on Meta::CPAN

        // queue the track
        let dectime = 0;
        if(time) {                         
            dectime = Math.floor(time * NWDRFLAC.sampleRate);            
            time = 0;
        }
        let isStart = true;      
        while(dectime < NWDRFLAC.totalPCMFrameCount) {
            let todec = Math.min(NWDRFLAC.sampleRate, NWDRFLAC.totalPCMFrameCount - dectime);
            
            // if plenty of audio is queued. Don't download yet
            let todecsecs = todec / NWDRFLAC.sampleRate;
            const  nextendtime = function(){
                return (AQ_unqueuedTime()+todecsecs);
            };
            console.log('nextendtime ' + nextendtime() + ' curdecsecs ' + todecsecs);
            while(nextendtime() > AQMaxDecodedTime) {
                let mssleep = (nextendtime()  - AQMaxDecodedTime) * 1000;
                await abortablesleep(mssleep, mysignal);
                if(mysignal.aborted) {
                    console.log('handling aborted sleep');
                    unlock();                     
                    return;
                }
            }
            
            // decode
            let buffer;
            for(let failedcount = 0;!buffer;) {
                try {
                    buffer = await NWDRFLAC.read_pcm_frames_to_AudioBuffer(dectime, todec, mysignal, MainAudioContext);
                    if(mysignal.aborted) {
                        console.log('aborted decodeaudiodata success');
                        unlock();                        
                        return;
                    }
                    if(buffer.duration !== (todec / NWDRFLAC.sampleRate)) {                       
                        buffer = null;
                        throw('network error? buffer wrong length');
                    }                        
                }
                catch(error) {
                    console.error(error);
                    if(mysignal.aborted) {
                        console.log('aborted read_pcm_frames decodeaudiodata catch');
                        unlock();                        
                        return;
                    }                   
                    failedcount++;
                    if(failedcount == 2) {
                        console.log('Encountered error twice, advancing to next track');
                         // assume it's corrupted. force free it
                        await NWDRFLAC.close();
                        NWDRFLAC = null;                      
                        continue TRACKLOOP;
                    }
                }
            }
         
            // Add to the audio queue
            let aqItem = { 'buffer' : buffer, 'duration' : todec, 'aqid' : AQID, 'skiptime' : (dectime / NWDRFLAC.sampleRate), 'track' : track, 'playbackinfo' : {}, 'timers' : []};
            // At start and end track update the GUI
            let isEnd = ((dectime+todec) === NWDRFLAC.totalPCMFrameCount);
            if(isStart || isEnd) {            
                aqItem.func = function(startTime, endTime) {
                    if(isStart) {
                        console.log('aqid: ' + aqItem.aqid + ' start timer at ' + startTime + ' currentTime ' + MainAudioContext.currentTime);
                        aqItem.timers.push({'time': startTime, 'func': function() {                             
                            seekbar.min = 0;
                            seekbar.max = track.duration;
                            SetEndtimeText(track.duration);
                            SetPlayText(track.trackname);
                            let prevtext = track.prev ? track.prev.trackname : '';
                            SetPrevText(prevtext);       
                            let nexttext =  track.next ? track.next.trackname : '';
                            SetNextText(nexttext);
                        }});
                        isStart = false;
                    }
                    if(isEnd) {
                        console.log('aqid: ' + aqItem.aqid + ' end timer at ' + endTime + ' currentTime ' + MainAudioContext.currentTime); 
                        aqItem.timers.push({'time': endTime, 'func': function(){
                            let curTime = 0;
                            SetEndtimeText(0);                    
                            SetCurtimeText(curTime);
                            SetSeekbarValue(curTime);
                            SetPrevText(track.trackname);
                            SetPlayText('');
                            SetNextText('');
                        }});
                    }
                }
            }
            AudioQueue.push(aqItem);        
            dectime += todec;
        }        
    }
    unlock();
}

var prevbtn    = document.getElementById("prevbtn");
var sktxt      = document.getElementById("seekfield");
var seekbar    = document.getElementById("seekbar");
var ppbtn      = document.getElementById("ppbtn");
var rptrackbtn = document.getElementById("repeattrack");
var curtimetxt = document.getElementById("curtime");
var endtimetxt = document.getElementById("endtime");
var nexttxt    = document.getElementById('next_text');
var prevtxt    = document.getElementById('prev_text');
var playtxt    = document.getElementById('play_text');
var dbarea     = document.getElementById('musicdb');

// BEGIN UI handlers

rptrackbtn.addEventListener('change', function(e) {
    let aqid = AQ_ID();
    if(aqid === -1) return;   // nothing is playing repeattrack should do nothing
    if(aqid === AQID) return; // current playing is still being queued do nothing 
    
    console.log('rptrack abort');
    AQ_stopAudioWithoutID(aqid); // stop the audio queue of next track(s)

    if(e.target.checked) {
        // repeat the currently playing track
        Tracks_QueueCurrent = AudioQueue[0].track;
    }
    else {
        // queue the next track
        Tracks_QueueCurrent = AudioQueue[0].track.next;
    }
    fillAudioQueue();
 });
 
 ppbtn.addEventListener('click', function (e) {
     if ((ppbtn.textContent == 'PAUSE')) {
         MainAudioContext.suspend();           
         ppbtn.textContent = 'PLAY';                        
     }
     else if ((ppbtn.textContent == 'PLAY')) {
         MainAudioContext.resume();
         ppbtn.textContent = 'PAUSE';
     }



( run in 3.358 seconds using v1.01-cache-2.11-cpan-75ffa21a3d4 )