// ----------------------------------------------------------------------------
//
//  Copyright (C) 2012 Fons Adriaensen <fons@linuxaudio.org>
//    
//  This program is free software; you can redistribute it and/or modify
//  it under the terms of the GNU 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 General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------


#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <math.h>
#include <jack/jack.h>
#include "alsathread.h"


Alsathread::Alsathread (Alsa_pcmi *alsadev, int mode) :
    _alsadev (alsadev ),
    _mode (mode),
    _state (INIT),
    _fsize (alsadev->fsize ()),
    _audioq (0),
    _commq (0),
    _alsaq (0)
{
    // Compute DLL filter coefficients.
    _dt = (double) _fsize / _alsadev->fsamp ();
    _w1 = 2 * M_PI * 0.1 * _dt;
    _w2 = _w1 * _w1;
    _w1 *= 1.6;
    // Time wraps around after 2^28 usecs.
    _tq = ldexp (1e-6, 28);
}


Alsathread::~Alsathread (void)
{
    _alsadev->pcm_stop ();
}


int Alsathread::start (Lfq_audio *audioq, Lfq_int32 *commq, Lfq_adata *alsaq, int rtprio)
{
    // Start the ALSA thread.
    _audioq = audioq;
    _commq = commq;
    _alsaq = alsaq;
    _state = WAIT;
    if (thr_start (SCHED_FIFO, rtprio, 0x10000)) return 1;
    return 0;
}


void Alsathread::send (int k, double t)
{
    Adata *D;

    // Send (state, frame count, timestamp) to Jack thread.
    if (_alsaq->wr_avail ())
    {
        D = _alsaq->wr_datap (); 
        D->_state = _state;
        D->_nsamp = k;
        D->_timer = t;
        _alsaq->wr_commit ();
    }
}


// The following two functions transfer data between the audio queue
// and the ALSA device. Note that we do *not* check the queue's fill
// state, and it may overrun or underrun. It actually will in the first
// few iterations and in error conditions. This is entirely intentional.
// The queue keeps correct read and write counters even in that case,
// and the  main control loop and error recovery depend on it working
// and being used in this way. 

int Alsathread::capture (void)
{
    int    c, n, k;
    float  *p;

    // Start reading from ALSA device.
    _alsadev->capt_init (_fsize);
    if (_state == PROC)
    {
	// Input frames from the ALSA device to the audio queue.
	// The outer loop takes care of wraparound.
	for (n = _fsize; n; n -= k)
	{
	    p = _audioq->wr_datap ();  // Audio queue write pointer.
	    k = _audioq->wr_linav ();  // Number of frames that can be
	    if (k > n) k = n;          // written without wraparound.
	    for (c = 0; c < _audioq->nchan (); c++)
	    {
		// Copy and interleave one channel.
		_alsadev->capt_chan (c, p + c, k, _audioq->nchan ());
	    }
	    _audioq->wr_commit (k);    // Update audio queue state.
	}
    }
    // Finish reading from ALSA device.
    _alsadev->capt_done (_fsize);
    return _fsize;
}


int Alsathread::playback (void)
{
    int    c, n, k;
    float  *p;

    // Start writing to ALSA device.
    _alsadev->play_init (_fsize);
    c = 0;
    if (_state == PROC)
    {
	// Output frames from the audio queue to the ALSA device.
	// The outer loop takes care of wraparound. 
	for (n = _fsize; n; n -= k)
	{
	    p = _audioq->rd_datap ();  // Audio queue read pointer.
	    k = _audioq->rd_linav ();  // Number of frames that can
	    if (k > n) k = n;          // be read without wraparound.
	    for (c = 0; c < _audioq->nchan (); c++)
	    {
		// De-interleave and copy one channel.
		_alsadev->play_chan (c, p + c, k, _audioq->nchan ());
	    }
	    _audioq->rd_commit (k);    // Update audio queue state.
	}
    }
    // Clear all or remaining channels.
    while (c < _alsadev->nplay ()) _alsadev->clear_chan (c++, _fsize);
    // Finish writing to ALSA device.
    _alsadev->play_done (_fsize);
    return _fsize;
}


void Alsathread::thr_main (void)
{
    int     na, nu;
    double  tw, er;

    _alsadev->pcm_start ();
    while (_state != TERM)
    {
        // Wait for next cycle, then take timestamp.
	na = _alsadev->pcm_wait ();  
        tw = 1e-6 * (int)(jack_get_time () & 0x0FFFFFFF);
	// Check for errors - requires restart.
	if (_alsadev->state () && (na == 0))
	{
            _state = WAIT;
	    send (0, 0);
	    continue;
	}

        // Check for commands from the Jack thread.
        if (_commq->rd_avail ())
	{
	    _state = _commq->rd_int32 ();
	    if (_state == PROC) _first = true;
	    if (_state == TERM) send (0, 0);
	}

        // We could have more than one period.
        nu = 0;
        while (na >= _fsize)
       	{
	    // Transfer frames.
	    if (_mode == PLAY) nu += playback ();
	    else               nu += capture ();
            // Update loop condition.
            na -= _fsize;
	    // Run the DLL if in PROC state.
	    if (_state == PROC)
	    {
   	        if (_first)
	        {
		    // Init DLL in first iteration.
		    _first = false;
                    _t0 = tw;
	            _t1 = tw + _dt;
		    // Required for initial delay calculation.
		    if (_mode == PLAY) nu -= _fsize;
		    else               nu += _fsize;
	        } 
	        else 
	        {
		    // Update the DLL.
                    er = tw - _t1;
		    // Check for time wraparound.
	            if (er < -200)
	            {
	                _t1 -= _tq;
	                er = tw - _t1;
		    }
		    // If we have more than one period, use
                    // the time error only for the last one.
	            if (na >= _fsize) er = 0;
	            _t0 = _t1;
	            _t1 += _w1 * er + _dt;
	            _dt += _w2 * er;
		}
	    }
	}

	// Send number of frames used and timestamp to Jack thread.
	if (_state == PROC) send (nu, _t1);
    }
    _alsadev->pcm_stop ();
}
