/*
     _____________________________________________________________
    /                                                             \
    | Program:  SOUND CHANNEL MANAGEMENT DLL
    \_____________________________________________________________/

    Windows 3.1 dynamic link library that supplies FOUR CHANNEL SOUND
    by mixing 4 virtual source channels onto 1 (mono) or 2 (stereo)
    target channels.

    This is Public Domain.

    CURRENT LIMITATIONS:
    --------------------

        samples with a size < TBSIZE+16 could NOT be played
        with parameter 'howoften' > 1 in call to ChanMan::play().

        if this is tried, ChanMan::play will ignore the call.

        this limitiation is due to blockmix() which currently
        can't handle copying multiple copies of the same sample
        into the same track block.


    Developers: ID: Name:
    =========== --- -----
                JTH Juergen Thumm

  _____________________________________/VERSION HISTORY\____________________
 /Date_ Version Who What____________________________________________________\
 yymmdd ------- --- ---------------------------------------------------------
 941023 1.00    JTH added this version history header.
                    endlessly played sounds have now no priority, even
                    if they're longer than a newly played sound.
 941127             'stereo' parm added to OpenChanMan.
                    blockmix now also supports mono mixing.
 941128 1.10        Bug identified: Samples with size < TBSIZE
                    drive blockmix() into crash when repeatcnt is set.
                    Fix: in ChanMan::play: check if (howoften > 1 &&
                    size < TBSIZE). If so, playing isn't started.

                    Bug: remixcur(): ISRC sometimes became < 0 with
                         active samples. Cause unknown, done quickfix.
 941129 1.11        ChanMan::open no longer issues an perr() if wave
                    device opening fails, so now one can just TRY if
                    it opens, e.g. in stereo mode.
                    ChanManVersion() added.
 950307 1.11.1      volatile qualifiers deactivated.
                    waveOutFunc tuned.
                    LocalInit call added.
 950308 1.12        waveOutFunc: reset flag check reactivated.
 950311 1.13        22 kHz sound support.

 950505 1.14        Windows crashed when playing sound with virtual memory.
                    This was the case when out-swapped pages were read
                    in the wave interrupt.
                    So, there is now done a pre-scan loop in ChanMan::play
                    which enforces in-swapping all pages before
                    the sound is played in interrupt.
                    Interestingly enough Win-OS/2 didn't care about these
                    problems - there it worked all the time.
                    Changed also sequence of slot initialization
                    in ChanMan::play.
                    Logfile creation now disabled by default, but could
                    be allowed with a trigger file.
 950508 1.15        Security fix: WaveOutFunc: static flag 'inthemix' avoids
                    several threads to jam in the mix functions if there's
                    heavy memory swapping.
*/

#define  CHANMAN_VERSION    0x0115UL

#include "expdef.h"

#include <windows.h>
#include <mmsystem.h>
#include <dos.h>
#include <limits.h>

#ifdef  TRACK_PC
#undef  TRACK_PC        // NOT in the DLL, it is somewhat critical
#endif
#define NO_MEMTRACK     // no memory surveillance too
#define EXPLIB_NOROOT   // expect no RootBase instance

#include "explib.h"

#include "sndchan.h"

#define local static

#define VOLATILE

//  ---------------- Sound Channel Block parameters -------------------------

#   define  TBLOCKS_POW 3       // set this to select number of track blocks
#   define  TBLOCKS (1<<TBLOCKS_POW)    // track is divided in so many blocks
#   define  TBIMASK (TBLOCKS-1) // index mask
#   define  TBSIZE  (1L<<13)    // MUST be a power of 2 and <= 32768

#   define  DEFAULT_SPEED   32000   // default playback sample rate

//  -------------------------------------------------------------------------

//  ulong ginf[10]={0xeeee,0xeeee,0,0,0,0,0,0,0,0};

//  -------------- DLL stuff ----------------

local HINSTANCE LibInstance = 0;

// Every DLL has an entry point LibMain and an exit point WEP.
sysint FAR PASCAL LibMain( HINSTANCE hInstance, WORD wDataSegment,
                           WORD wHeapSize, LPSTR lpszCmdLine )
{
    // The startup code for the DLL initializes the local heap (if there is one)
    // with a call to LocalInit which locks the data segment.

    if(wDataSegment && wHeapSize)
        LocalInit(wDataSegment, 0, wHeapSize - 1);

    // if ( wHeapSize != 0 ) UnlockData( 0 );

    LibInstance = hInstance;

    return 1;   // Indicate that the DLL was initialized successfully.
}

// Turn off warning: Parameter '' is never used
#pragma argsused

sysint FAR PASCAL WEP (sysint bSystemExit )
{
    return 1;
}

//  ----------- RootBase replacement -----------
//  this is only precautionary; maybe RootBase would work
//  in a DLL, I didn't try.

#undef  perr
#undef  pmsg
#undef  dmsg

#define dmsg perr

void logmsg(char *str, ...)
{
 FILE FAR *lf;
 static char cnol=0;

 va_list arg_ptr;

  va_start(arg_ptr, str);

  if(!cnol)
  {
    if(lf = fopen("C:\\allow.log","r"))
    {
      fclose(lf);

      if((lf = fopen("C:\\snd.log", "a"))!=NULL)
      {
        vfprintf(lf, str, arg_ptr);
        fclose(lf);
      }
      else
        cnol = 1;
    }
    else
      cnol = 1;
  }

  va_end(arg_ptr);
}

short perr(const char *str, ...)
{
 static char buf[320];
 va_list arg_ptr;

  va_start(arg_ptr, str);

    vsprintf(buf, str, arg_ptr);

    logmsg("ERROR:%s\n", buf);

  va_end(arg_ptr);

  return 0;
}

//  --------- hardware track/channel declarations ---------

struct  TrackBlock  {

    uchar * adr;    // memory adress of this block's start

#ifdef  WIN16
    WAVEHDR whdr;   // wave header for a block
#endif

    TrackBlock()    {adr=0;}

    };

struct  TrackTable  {

    TrackBlock  tblock[TBLOCKS];    // track block controls

    VOLATILE ushort iplay;  // index to currently played block

    ushort ipoffs;  // iplay represents currently played block + ipoffs

    };

//  ------------- Channel Manager declarations ------------

class   ChanMan {

    FARPROC proc;

    TrackTable  *ttab;  // track block table

    bool    idead;

    uchar   stereo;     // using 2 parallel hardware tracks

 public:

    ChanMan(WaveInfo FAR * parms, uchar stereo);
    ~ChanMan();

    bool    dead()  {return idead;}

    void        IRQServe();

    rcode       play(WaveInfo FAR * winfo, ulong howoften = 1, ulong opts=0);
    void        stop(WaveInfo FAR * winfo = 0); // if 0, stops all

    void        remixcur(ulong ifrom, ushort iblock);
    void        remixnext(ushort iblock);

    short       volume(short newvol = -1);  // get/set volume in %

    };

local   ChanMan *InstChanMan = 0;   // local instance

//  ----------- WIN16 hardware sound channel ------------
//  this code will call InstChanMan->IQRServe directly.

class   Channel {

#ifdef  WIN16
    // system dependent controls

    HWAVEOUT    hdev;   // wave device handle

    PCMWAVEFORMAT   playparm;   // wave playing parms for whole track

#endif

    WaveInfo    winfo;  // describes channel's playback parms

    VOLATILE bool   ireset; // set after waveOutReset call, important for IRQ handler

    VOLATILE bool   iinit;  // while set, Channel is in init phase

    bool    idead;  // set if constructor failed

 public:

    TrackTable  tt; // the hardware track memory

    Channel();
    ~Channel();

    inline  bool    dead()  {return idead;}     // check after construction
    inline  bool    reset() {return ireset;}    // waveOutReset() was called
    inline  bool    init()  {return iinit;}

    ulong   iplaypos(); // returns index of sample currently played

    short   open(short stereo, WaveInfo FAR * parms = 0);

    void    close();

    ulong   playindex();

    void    clearblock(short iblock);   // clear a block of the track

    short   volume(short newvol = -1);  // get/set volume in %

    };

Channel::Channel()
{
    idead   = 1;

    // alloc all track blocks

    for(short i=0; i<TBLOCKS; i++)
    {
        ifn(tt.tblock[i].adr = (uchar*)farmalloc((ulong)TBSIZE + 16))
        {   perr("can't alloc sound track mem"); return;    }

        memset(tt.tblock[i].adr, 0x80, min(TBSIZE, 65535UL));
    }

    idead   = 0;
}

Channel::~Channel()
{
    for(short i=0; i<TBLOCKS; i++)

        if(tt.tblock[i].adr)

            farfree(tt.tblock[i].adr);
}

ulong   Channel::playindex()
{
 MMTIME pinfo;  // pos'n info

    pinfo.wType = TIME_BYTES;   // return pos'n in bytes

    ulong usr = waveOutGetPosition(hdev, (LPMMTIME)&pinfo, sizeof(MMTIME));

    if(usr) {   perr("getpos'n failed"); return 0;  }

    if(pinfo.wType != TIME_BYTES)
        {   perr("byte position request not supported"); return 0;  }

    return  (pinfo.u.cb & (TBSIZE-1)) + (ulong)tt.iplay * TBSIZE;
}

local Channel lchan;    // local channel instance

//  ---------------------------------------------------------------

void CALLBACK _export WaveOutFunc(
            HWAVEOUT    hWaveOut,   // device from which interrupt came
            UINT        wMsg,       // WOM_OPEN/CLOSE/DONE
            DWORD       dwInstance, // user-supplied instance data
            DWORD       dwParam1,   // depends on message
            DWORD       dwParam2    // depends on message
            )
{
    if(wMsg == WOM_OPEN)    return; // open:  do nothing
    if(wMsg == WOM_CLOSE)   return; // close: do nothing
    if(wMsg != WOM_DONE)    return; // usually impossible

    if(lchan.reset())   return;

    //  re-write it immediately for looping

    waveOutWrite(hWaveOut, (LPWAVEHDR)dwParam1, sizeof(WAVEHDR));

    static uchar inthemix = 0;

    if(!inthemix)
    {
        inthemix = 1;   // lock mixing

        //  set index to currently played block:
        //  this is this block's index (stored in dwUser) plus one
        //  plus an optional offset which may be needed with higher
        //  sample rates

        if(dwParam1)    // if wave header given
          lchan.tt.iplay =
            ((ushort)((WAVEHDR*)dwParam1)->dwUser + 1 + lchan.tt.ipoffs) & TBIMASK;

        if(InstChanMan && !InstChanMan->dead())
            InstChanMan->IRQServe();

        inthemix = 0;   // unlock mixing
    }
}

short   Channel::open(short stereo, WaveInfo FAR * parms)
{
 ushort usr;    // ushort result

    tt.iplay    = 0;    // re-set Channel's playback block index
    tt.ipoffs   = 1;    // by default, iplay == played block + 1

    ireset      = 0;    // WaveOutReset was not yet done

    ushort idev = WAVE_MAPPER;  // device id

    playparm.wf.wFormatTag      = WAVE_FORMAT_PCM;

    if(parms)
        winfo   = *parms;
    else
    {
        winfo.speed = DEFAULT_SPEED;    // default SamplesPerSec
    }

    if(winfo.speed < 20000U)
        tt.ipoffs   = 0;    // must use this to avoid delays with 11 kHz

    if(stereo)
    {
     playparm.wf.nChannels      = 2;        // 1 = mono, 2 = stereo
     playparm.wf.nSamplesPerSec = winfo.speed;
     playparm.wf.nAvgBytesPerSec= ((ulong)winfo.speed) << 1;
     playparm.wf.nBlockAlign    = 2;        // 2 byte per sample
     playparm.wBitsPerSample    = 8;        // bits per sample
    }
    else
    {
     playparm.wf.nChannels      = 1;        // 1 = mono, 2 = stereo
     playparm.wf.nSamplesPerSec = winfo.speed;
     playparm.wf.nAvgBytesPerSec= winfo.speed;
     playparm.wf.nBlockAlign    = 1;        // 1 byte per sample
     playparm.wBitsPerSample    = 8;        // bits per sample
    }

    usr = waveOutOpen(

            (LPHWAVEOUT)&hdev,
            idev,
            (LPWAVEFORMAT)&playparm,

            (DWORD)WaveOutFunc,         // callback function for played blocks
            (DWORD)LibInstance,         // user instance data
            (DWORD)CALLBACK_FUNCTION    // callback func supplied

            );

    if(usr) {   return 1;   }   // just do this, nothing else.

    //  prepare all track blocks for playing

    for(ulong i=0; i<TBLOCKS; i++)
    {
        tt.tblock[i].whdr.lpData            = tt.tblock[i].adr;
        tt.tblock[i].whdr.dwBufferLength    = TBSIZE;
        tt.tblock[i].whdr.dwBytesRecorded   = 0;    // used when recording
        tt.tblock[i].whdr.dwUser            = i;    // additional user data
        tt.tblock[i].whdr.dwFlags           = 0;
        tt.tblock[i].whdr.dwLoops           = 0;    // or no. of loops
        tt.tblock[i].whdr.lpNext    = 0;    // reserved
        tt.tblock[i].whdr.reserved  = 0;    // reserved

        /*
        if(i == 0)
            tt.tblock[i].whdr.dwFlags   =   WHDR_BEGINLOOP;

        if(i == TBLOCKS - 1)
            tt.tblock[i].whdr.dwFlags   =   WHDR_ENDLOOP;
        */

        usr = waveOutPrepareHeader(hdev, (LPWAVEHDR)&tt.tblock[i].whdr, sizeof(WAVEHDR));
        if(usr) {   perr("can't prepare wave header"); return 1;    }

        clearblock(i);
    }

    // start loop playing of track buffer

    waveOutPause(hdev);

    lchan.iinit = 1;

    for(i=0; i<TBLOCKS; i++)
    {
        usr = waveOutWrite(hdev, (LPWAVEHDR)&tt.tblock[i].whdr, sizeof(WAVEHDR));

        if(usr)
        {
            perr("can't play wave block %d", i);

            lchan.ireset    = 1;

            waveOutReset(hdev); // stop all playing

            for(i=0; i<TBLOCKS; i++)

                waveOutUnprepareHeader(hdev, (LPWAVEHDR)&tt.tblock[i].whdr, sizeof(WAVEHDR));

            return 1;
        }
    }

    lchan.iinit = 0;

    waveOutRestart(hdev);

    return  0;  // track buffer now playing in a loop
}

void    Channel::close()
{
    //  stop all playing

    lchan.ireset    = 1;

    waveOutReset(hdev);

    //  unprepare all block headers

    for(short i=0; i<TBLOCKS; i++)

        waveOutUnprepareHeader(hdev, (LPWAVEHDR)&tt.tblock[i].whdr, sizeof(WAVEHDR));

    //  close wave device

    if(waveOutClose(hdev))
    {   perr("can't close wave device");    return; }
}

void    Channel::clearblock(short iblock)
{
    if(idead || !tt.tblock[iblock].adr)
    {   perr("internal error at 1809941246"); return;   }

    memset(tt.tblock[iblock].adr, 0x80, min(TBSIZE, 65535UL));
}

short   Channel::volume(short newvol)
{
    if(idead)   return -1;

    ulong   winvol;

    if(newvol != -1)
    {
        // set volume

        winvol  = (ulong)newvol * 0xFFFFUL / 100UL;

        if(!waveOutSetVolume(hdev, (winvol << 16) | winvol))
            return  newvol;
        else
            return  -1; // volume setting failed
    }

    // get volume

    if(!waveOutGetVolume(hdev, (LPDWORD)&winvol))
        return  (short)((winvol & 0xFFFFUL) * 100UL / 0xFFFFUL);
    else
        return  -1; // volume getting failed
}

//  ----------------------------------------------------

local TrackTable * opentrack(short stereo, WaveInfo FAR * parms)
{
    if(lchan.dead())
    {   perr("channel's dead"); return 0;   }

    if(lchan.open(stereo, parms))

        return  0;  // don't perr(), it might've been just a try

    return  &lchan.tt;
}

local void closetrack()
{
    lchan.close();
}

local ulong trackindex()
{
    return  lchan.playindex();
}

//  ------------- CHANNEL MANAGER code --------------

/*
        switch(ginf[0]++)   //***
        {
            case 0: ginf[1]=(ulong)s0a; break;
            case 1: ginf[2]=(ulong)s0a; break;
            case 2: ginf[3]=(ulong)s0a; break;
            case 3: ginf[4]=(ulong)s0a; break;
            case 4: ginf[5]=(ulong)s0a; break;
            case 5: ginf[6]=(ulong)s0a; break;
            case 6: ginf[7]=(ulong)s0a; break;
            case 7: ginf[8]=(ulong)s0a; break;
            case 8: ginf[9]=(ulong)s0a; break;
        }
*/

#   define  TMP_SSIZE 4096  // temporar buffer size for stereo
#   define  TMP_MSIZE 8192  // temporar buffer size for mono
#   define  TMP_ASIZE 8192  // alloc size of a temporar buffer

#if (TMP_ASIZE != TBSIZE || TMP_SSIZE * 2 != TBSIZE)
#   error   "readjust TBSIZE or TMP_xSIZE first"
#endif

#if (TBSIZE < (1L<<14))
static uchar mbuf[4][TMP_ASIZE];    // use this buffer for faster mixing
#endif

#ifdef  WIN16

    // source across segment boundary copy:
    //  'src'   may cross segment bound, max size is 64k
    //  'dest'  should NOT cross a segment bound!

void FAR * _srcxsegcpy(void FAR *dest, void FAR *src, ushort size)
{
 typedef uchar HUGE * HPTR;

    // would (src + size) cross a segment boundary?

    ulong offs = (ulong)((ushort)src);  // get source offset

    ulong lowsize,hisize;

    if(offs + (ulong)size >= (1UL<<16))
    {
        // YES. Copy first lower then upper part of block

        lowsize = 65536UL - offs;
        hisize  = (ulong)size - lowsize;

        if(lowsize) _fmemcpy(dest, src, (ushort)lowsize);

        if(hisize)  _fmemcpy((HPTR)dest + lowsize, (HPTR)src + lowsize,
                             (ushort)hisize);
    }
    else
        _fmemcpy(dest, src, size);

    return  dest;
}
#endif

#ifdef  WIN16
#define MEMCPY _srcxsegcpy
#else
#define MEMCPY memcpy
#endif

#define ADRPTR  uchar HUGE *

//  WARNING !!! Do NOT supply wrap-around samples with sizes
//              lower than TBSIZE! This cannot be handled
//              at the moment and would lead to a crash.

local void blockmix(

                uchar   FAR *ta, // target adress
                ulong   tsize,   // target block size
                ushort  stereo,  // 0 = mono, 1 = stereo

      // source adress / remainsize / restart adress for endless play
      ADRPTR s0a, long s0r, ADRPTR wrap0,
      ADRPTR s1a, long s1r, ADRPTR wrap1,
      ADRPTR s2a, long s2r, ADRPTR wrap2,
      ADRPTR s3a, long s3r, ADRPTR wrap3

      // where no endless play is needed, wrapx must be set to 0

      // number of valid source bytes after wrapx
      // is expected to be >= blocksize !!!

                )
{
#if (TBSIZE < (1L<<14))

    // SMALL track block compile: use fast memcpy preparation
    // for fastest-possible mixing in the mix kernal loop

 ushort i;

    // prepare

    memset(mbuf, 0x80, sizeof(mbuf));   // silence

    ushort  nsamp = (ushort)(tsize >> stereo);

    if(s0r) MEMCPY((void FAR *)mbuf[0], s0a, min(nsamp, s0r));
    if(wrap0 && (s0r < nsamp))
        MEMCPY(&mbuf[0][s0r], wrap0, nsamp - s0r);

    if(s2r) MEMCPY((void FAR *)mbuf[1], s2a, min(nsamp, s2r));
    if(wrap2 && (s2r < nsamp))
        MEMCPY(&mbuf[1][s2r], wrap2, nsamp - s2r);

    if(s1r) MEMCPY((void FAR *)mbuf[2], s1a, min(nsamp, s1r));
    if(wrap1 && (s1r < nsamp))
        MEMCPY(&mbuf[2][s1r], wrap1, nsamp - s1r);

    if(s3r) MEMCPY((void FAR *)mbuf[3], s3a, min(nsamp, s3r));
    if(wrap3 && (s3r < nsamp))
        MEMCPY(&mbuf[3][s3r], wrap3, nsamp - s3r);

    // mix mbuf[] -> ta

    if(!nsamp)  return;

    if(stereo)
    {

    asm {

        pushf
        push    SI
        push    ES
        push    DI
        push    CX
        push    AX
        push    BX

        lea     SI,mbuf     // load source index base
        les     DI,ta       // load 32 bit ptr value at targadr -> ES:DI

        mov     CX,nsamp    // load loop counter

        }

mixstereo:

   asm  {

        xor     BX,BX       // set BX to 0
        xor     AX,AX       // set AX to 0

        // mix src0 + src2 to left channel byte of target

        mov     AL,[SI]     // get byte from srcchan 0
        mov     BL,[SI+TMP_ASIZE]   // get byte from srcchan 2
        add     AX,BX       // mix
        shr     AX,1        // them

        mov     [ES:DI],AL  // write left target byte
        inc     DI          // inc target index

        // mix src1 + src3 to right channel byte of target

        mov     AL,[SI+TMP_ASIZE*2] // get byte from srcchan 1
        mov     BL,[SI+TMP_ASIZE*3] // get byte from srcchan 3
        add     AX,BX       // mix
        shr     AX,1        // them

        mov     [ES:DI],AL  // write right target byte
        inc     DI          // inc target index

        // increment source index and loop°

        inc     SI

        loop    mixstereo

        pop     BX
        pop     AX
        pop     CX
        pop     DI
        pop     ES
        pop     SI
        popf

        }

   }
   else // if mono
   {

    asm {

        pushf
        push    SI
        push    ES
        push    DI
        push    CX
        push    AX
        push    BX

        lea     SI,mbuf     // load source index base
        les     DI,ta       // load 32 bit ptr value at targadr -> ES:DI

        mov     CX,nsamp    // load loop counter

        }

mixmono:

   asm  {

        xor     BX,BX       // set BX to 0
        xor     AX,AX       // set AX to 0

        // mix all 4 sources onto single destination

        mov     AL,[SI]

        mov     BL,[SI+TMP_ASIZE]
        add     AX,BX

        mov     BL,[SI+TMP_ASIZE*2]
        add     AX,BX

        mov     BL,[SI+TMP_ASIZE*3]
        add     AX,BX

        shr     AX,2        // divide by 4

        mov     [ES:DI],AL  // write target byte
        inc     DI          // inc target index

        // increment source index and loop

        inc     SI

        loop    mixmono

        pop     BX
        pop     AX
        pop     CX
        pop     DI
        pop     ES
        pop     SI
        popf

        }

   }    // endif stereo

/*
    uchar near *x0  = mbuf[0];
    uchar near *x1  = mbuf[1];
    uchar near *x2  = mbuf[2];
    uchar near *x3  = mbuf[3];

    if(stereo)
    {
        for(i = 0; i < nsamp; i++)
        {
            *ta++   = (uchar)(((ushort)*x1++ + (ushort)*x3++) >> 1);
            *ta++   = (uchar)(((ushort)*x0++ + (ushort)*x2++) >> 1);
        }
    }
    else
    {
        for(i = 0; i < nsamp; i++)
        {
            *ta++   = (uchar)( ((ushort)*x0++ + (ushort)*x1++
                              + (ushort)*x2++ + (ushort)*x3++) >> 2);
        }
    }
*/

#else

    // LARGE track block compile: no mbuffer possible, this
    // DLL's single data segment isn't enough for this.
    // So use a plain C loop which does anything.

    ushort  i,v0,v1,v2,v3;

    ushort  maxsamp = (ushort)(tsize >> (uchar)stereo);

    uchar   checkwrap   = 0;

    if(wrap0 || wrap1 || wrap2 || wrap3)    checkwrap   = 1;

    if(stereo)
    {
        //  mix source tracks onto stereo channels:

        for(i = 0; i < maxsamp; i++)
        {
            if(checkwrap)
            {
                //  any endless play wrap-arounds?

                if(!s0r && wrap0)   {   s0a = wrap0;    s0r = tsize;    }
                if(!s1r && wrap1)   {   s1a = wrap1;    s1r = tsize;    }
                if(!s2r && wrap2)   {   s2a = wrap2;    s2r = tsize;    }
                if(!s3r && wrap3)   {   s3a = wrap3;    s3r = tsize;    }
            }

            //  get next sample byte(s), if there are:

            if(s0r) {   v0 = *s0a++;    s0r--;  } else v0 = 0x80;
            if(s1r) {   v1 = *s1a++;    s1r--;  } else v1 = 0x80;
            if(s2r) {   v2 = *s2a++;    s2r--;  } else v2 = 0x80;
            if(s3r) {   v3 = *s3a++;    s3r--;  } else v3 = 0x80;

            //  mix source bytes to target byte(s)

            //  right channel: source tracks 1 and 3

            *ta++   = (uchar)((v1 + v3) >> 1);

            //  left channel: source tracks 0 and 2

            *ta++   = (uchar)((v0 + v2) >> 1);
        }
    }
    else
    {
        //  mix source tracks onto mono channel:

        for(i = 0; i < maxsamp; i++)
        {
            if(checkwrap)
            {
                //  any endless play wrap-arounds?

                if(!s0r && wrap0)   {   s0a = wrap0;    s0r = tsize;    }
                if(!s1r && wrap1)   {   s1a = wrap1;    s1r = tsize;    }
                if(!s2r && wrap2)   {   s2a = wrap2;    s2r = tsize;    }
                if(!s3r && wrap3)   {   s3a = wrap3;    s3r = tsize;    }
            }

            //  get next sample byte(s), if there are:

            if(s0r) {   v0 = *s0a++;    s0r--;  } else v0 = 0x80;
            if(s1r) {   v1 = *s1a++;    s1r--;  } else v1 = 0x80;
            if(s2r) {   v2 = *s2a++;    s2r--;  } else v2 = 0x80;
            if(s3r) {   v3 = *s3a++;    s3r--;  } else v3 = 0x80;

            //  mix source bytes to target byte(s)

            *ta++   = (uchar)((v0 + v1 + v2 + v3) >> 2);
        }
    }

#endif
}

struct  WaveSlot    {

    uchar   HUGE * sdata;
    ulong   ssize;
    ulong   irelstart;
    ulong   iplaycnt;
    ulong   seqnum;
    ulong   repeatcnt;  // repeat playing this often (0 = play once)
    bool    initial;    // if set, first block not yet passed

    WaveSlot()
        {   ssize=0; sdata=0;   }

    };

local WaveSlot slot[4];

#   define  FOR_SLOTS   for(i=0; i<4; i++)
#   define  SLOT        slot[i]

ChanMan::ChanMan(WaveInfo FAR * parms, uchar xstereo)
{
    idead   = 1;

    stereo  = xstereo;

    ushort  i;

    FOR_SLOTS   SLOT.ssize = 0; // all slots quiet

    ttab = (TrackTable *)opentrack(stereo, parms);

    if(ttab)    idead = 0;
}

ChanMan::~ChanMan()
{
    if(ttab)    closetrack();
}

rcode ChanMan::play(WaveInfo FAR * winfo, ulong howoften, ulong opts)
{
    ushort  i, istart=0, istep=1;

    //  check: sounds with size < TBSIZE are NOT supported
    //         with replay, due to blockmix() limitations.

    if(howoften > 1 && winfo->ssize < TBSIZE + 16)  // 16 == tolerance

        return  FAILED;

    //  find target slot

    if(opts & SND_PLAYLEFT)     istep = 2;  // just check 0 and 2

    if(opts & SND_PLAYRIGHT)
        {   istart  = 1;    istep = 2;  }   // just check 1 and 3

    ulong   minseq  = ULONG_MAX;
    ushort  itarget = istart;

    for(i = istart; i < 4; i += istep)
    {
        if(SLOT.seqnum < minseq)
            {   minseq = SLOT.seqnum; itarget = i;  }

        if(SLOT.ssize == 0)
            {   itarget = i; break; }
    }

    i   = itarget;

    //  if the target slot isn't empty, who has priority?
    //  give larger sounds priority, if not played endless.

    if(SLOT.ssize)
    {
        //  if the new sound is an endless playing one,
        //  it has no priority at all:

        if(howoften > 1)
            //  do NOT replace 'normal' sound by endless
            return  OK;

        //  if the currently playing sound is larger,
        //  and it's NOT played more often than once,
        if(SLOT.ssize > winfo->ssize && !SLOT.repeatcnt)
            //  do NOT replace him by newer.
            return  OK;
    }

    static ulong seqsrc = 13;

    //  prefetch scan: enforce memory swap-in of sound to be played
    uchar bnil1,bnil2;
    uchar _huge *pscan  = winfo->sdata;
    long lremain        = winfo->ssize;
    long lstep          = 1024UL;   // should be <= memory page size
    while(lremain > 0)
    {
        bnil1    = *pscan;  // enforce page swap-in
        pscan   +=  lstep;  // step to next area
        lremain -=  lstep;  // count down remaining size
    }
    //  explicite last-byte scan:
    if(winfo->ssize)
    {
        pscan   =  winfo->sdata + (ulong)winfo->ssize - 1;
        bnil1   = *pscan;
    }
    //  avoid 'not used' warning(s) by this:
    bnil2   = bnil1;
    bnil1   = bnil2;

    //  what now comes is a bit critical, in extreme cases
    //  the sequence of these instructions might count.
    SLOT.iplaycnt   = 0;
    SLOT.sdata      = winfo->sdata;
    SLOT.ssize      = winfo->ssize;
    SLOT.seqnum     = seqsrc++;
    SLOT.repeatcnt  = howoften - 1;
    SLOT.irelstart  = trackindex() & (TBSIZE-1);
    //  irelstart is the relative index in the startblock.
    SLOT.initial    = 1;
    //  first block not yet passed by IRQ server

    //  remix current & next slot

    //  disable();  // time critical, no interrupts

    ushort ipnow = ttab->iplay;

    if(TBSIZE < (1L<<14))
    {
        //  small target blocks: mix current & next

        remixcur(SLOT.irelstart, ipnow);
        remixnext((ipnow + 1) & TBIMASK);
    }
    else
    {
        //  mix just 'current' in NEXT block to save CPU time

        remixcur(SLOT.irelstart, (ipnow + 1) & TBIMASK);

        //  ... remixnext will just mix from 'then current src'.
    }

    //  enable();

    return  OK;
}

void ChanMan::stop(WaveInfo FAR * wi)
{
    ushort  i;

    FOR_SLOTS
    {
        if(!wi || wi->sdata == SLOT.sdata)

            SLOT.ssize   = 0;
    }
}

void ChanMan::IRQServe()
{
    ushort  i;

    FOR_SLOTS
    {
        if(SLOT.ssize)
        {
            SLOT.iplaycnt   += (TBSIZE - SLOT.irelstart) >> stereo;
            SLOT.irelstart   = 0;
            SLOT.initial     = 0;

            if(SLOT.iplaycnt >= SLOT.ssize)
            {
                if(SLOT.repeatcnt)
                {
                    // repeat this slot once more

                    SLOT.repeatcnt--;

                    SLOT.iplaycnt   -= SLOT.ssize;

                    if(SLOT.iplaycnt >= SLOT.ssize)
                    {
                        // this slot's wave would fit multiple
                        // times into a single playing block.
                        // this is NOT supported.

                        SLOT.ssize  = 0;    // stop playing
                    }
                }
                else
                    SLOT.ssize  = 0;    // make slot quiet
            }
        }
    }

    remixnext((ttab->iplay + 1) & TBIMASK); // remix next (not current) block
}

#   define  ISRC    isrc[i]
#   define  XREM    xrem[i]
#   define  ASRC    asrc[i]
#   define  AWRAP   awrap[i]

void ChanMan::remixcur(ulong ifrom, ushort iblock)
{
    ushort  i;

    ulong   isrc[4];    // source indexes in slots
    long    xrem[4];    // remaining transfer size
    ADRPTR  asrc[4];    // source adresses in slots
    ADRPTR  awrap[4];   // wrap-around adresses in slots

    memset(awrap, 0, sizeof(awrap));    // all NULL by default

    FOR_SLOTS
    {
        if(SLOT.ssize)
        {
            if(SLOT.initial)
            {
                if(SLOT.irelstart > ifrom)
                    ISRC    = 0;    // quickfix
                else
                    ISRC    = (ifrom - SLOT.irelstart) >> stereo;
            }
            else
            {
                ISRC    = SLOT.iplaycnt + (ifrom >> stereo);
            }

            XREM    = (long)SLOT.ssize - (long)ISRC;

            if(XREM <= 0)
            {
                XREM    = 0;
                ASRC    = 0;
            }
            else
            {
                // really take this slot into the mix:
                ASRC    = (ADRPTR)(SLOT.sdata + ISRC);

                if(SLOT.repeatcnt)
                    // if blockmix reaches end of sound,
                    // it must start again at start of sound:
                    AWRAP   = (ADRPTR)SLOT.sdata;
            }
        }
        else
        {
            ASRC    = 0;
            XREM    = 0;
        }
    }

    blockmix(   ttab->tblock[iblock].adr + ifrom,
                TBSIZE - ifrom,
                stereo,

                asrc[0], xrem[0], awrap[0],
                asrc[1], xrem[1], awrap[1],
                asrc[2], xrem[2], awrap[2],
                asrc[3], xrem[3], awrap[3]
            );
}

void ChanMan::remixnext(ushort iblock)
{
    ushort  i;

    ulong   isrc[4];    // source indexes in slots
    long    xrem[4];    // remaining transfer size
    ADRPTR  asrc[4];    // source adresses in slots
    ADRPTR  awrap[4];   // wrap-around adresses in slots

    memset(awrap, 0, sizeof(awrap));    // all NULL by default

    FOR_SLOTS
    {
        if(SLOT.ssize)
        {
            if(SLOT.initial)
                ISRC    = (TBSIZE - SLOT.irelstart) >> stereo;
            else
            {
                if(TBSIZE < (1L<<14))
                    //  small track blocks: really mix from next src block
                    ISRC    = SLOT.iplaycnt + ((ulong)TBSIZE >> stereo);
                else
                    //  large track blocks: current src wasn't yet mixed
                    ISRC    = SLOT.iplaycnt;
            }

            if(ISRC >= SLOT.ssize && SLOT.repeatcnt)
                // perform a wrap-around
                ISRC    -= SLOT.ssize;
                // if sample is so small ISRC is still >= ssize,
                // XREM will go negative or zero.

            XREM    = (long)SLOT.ssize - (long)ISRC;

            if(XREM <= 0)
            {
                XREM    = 0;
                ASRC    = 0;
            }
            else
            {
                // really take this slot into the mix:
                ASRC    = (ADRPTR)(SLOT.sdata + ISRC);

                if(SLOT.repeatcnt)
                    // if blockmix reaches end of sound,
                    // it must start again at start of sound:
                    AWRAP   = (ADRPTR)SLOT.sdata;
            }
        }
        else
        {
            ISRC    = 0;
            XREM    = 0;
            ASRC    = 0;
        }
    }

    blockmix(   ttab->tblock[iblock].adr,
                TBSIZE,
                stereo,

                asrc[0], xrem[0], awrap[0],
                asrc[1], xrem[1], awrap[1],
                asrc[2], xrem[2], awrap[2],
                asrc[3], xrem[3], awrap[3]
            );
}

short   ChanMan::volume(short newvol)
{
    if(idead || lchan.dead())   return -1;

    return  lchan.volume(newvol);
}

//  --------- ChanMan interface to 'outer world' ---------

ulong _far _export ChanManVersion(void)
{
    return  CHANMAN_VERSION;
}

rcode _far _export OpenChanMan(WaveInfo FAR * parms, uchar stereo)
{
    ifn(InstChanMan)

        InstChanMan = new ChanMan(parms, stereo);

    if(!InstChanMan || InstChanMan->dead())
    {
        CloseChanMan();

        return FAILED;  // issue no perr, it might've been a try
    }

    return OK;
}

void _far _export CloseChanMan()
{
    if(InstChanMan)

        delete  InstChanMan;

    InstChanMan = 0;
}

rcode _far _export ChanManPlay(WaveInfo FAR * wi, ulong howoften, ulong opts)
{
    if(InstChanMan)
        return InstChanMan->play(wi, howoften, opts);
    else
        return NOTAVAIL;
}

void _far _export ChanManStop(WaveInfo FAR * wi)
{
    if(InstChanMan) InstChanMan->stop(wi);
}

/*  // not yet supported
short _far _export ChanManVolume(short newvol)
{
    if(InstChanMan) return InstChanMan->volume(newvol);

    return  -1;
}
*/

ulong _far _export GlobInfo(void)
{
    return  0;
}
