Microsoft-3D-Movie-Maker/kauai/SRC/MIDIDEV.CPP
2022-05-03 16:31:19 -07:00

909 lines
21 KiB
C++

/* Copyright (c) Microsoft Corporation.
Licensed under the MIT License. */
/***************************************************************************
Author: ShonK
Project: Kauai
Copyright (c) Microsoft Corporation
The midi player device.
***************************************************************************/
#include "frame.h"
ASSERTNAME
RTCLASS(MIDP)
const long kdtsMinSlip = kdtsSecond / 30;
const long klwInfinite = klwMax;
/***************************************************************************
Midi output object.
***************************************************************************/
enum
{
fmidoNil = 0x0,
fmidoFirst = 0x1,
fmidoFastFwd = 0x2,
};
typedef class MIDO *PMIDO;
#define MIDO_PAR BASE
#define kclsMIDO 'MIDO'
class MIDO : public MIDO_PAR
{
RTCLASS_DEC
protected:
typedef HMIDIOUT HMO;
MUTX _mutx; // restricts access to member variables
HMO _hmo; // the output device
// system volume level - to be saved and restored. The volume we set
// is always relative to this
ulong _luVolSys;
long _vlmBase; // our current volume relative to _luVolSys.
long _vlm; // our current volume relative to _vlmBase
long _sii; // the sound that owns the _hmo
long _spr; // the priority of sound that owns the _hmo
bool _fRestart: 1; // whether the device needs reset
bool _fSetVol: 1; // whether the volume needs set
void _GetSysVol(void);
void _SetSysVol(ulong luVol);
void _SetSysVlm(void);
void _Reset(void);
public:
MIDO(void);
~MIDO(void);
void Suspend(bool fSuspend);
void SetVlm(long vlm);
long VlmCur(void);
bool FPlay(long sii, long spr, MIDEV *pmidev, long vlm, ulong grfmido);
void Transition(long siiOld, long siiNew, long sprNew);
void Close(long sii);
};
static MIDO _mido;
RTCLASS(MIDO)
/***************************************************************************
Constructor for the low level midi output device.
***************************************************************************/
MIDO::MIDO(void)
{
_hmo = hNil;
_sii = siiNil;
_luVolSys = (ulong)(-1);
_vlmBase = _vlm = kvlmFull;
_fSetVol = fFalse;
AssertThis(0);
}
/***************************************************************************
Destructor for the low level midi output device.
***************************************************************************/
MIDO::~MIDO(void)
{
AssertThis(0);
_mutx.Enter();
Suspend(fTrue);
_mutx.Leave();
}
/***************************************************************************
Get the system volume level.
***************************************************************************/
void MIDO::_GetSysVol(void)
{
Assert(hNil != _hmo, "calling _VlmGetSys with nil _hmo");
if (0 != midiOutGetVolume((uint)_hmo, &_luVolSys))
{
// failed - assume full volume
_luVolSys = (ulong)(-1);
}
}
/***************************************************************************
Set the system volume level.
***************************************************************************/
void MIDO::_SetSysVol(ulong luVol)
{
Assert(hNil != _hmo, "calling _SetSysVol with nil _hmo");
midiOutSetVolume((uint)_hmo, luVol);
}
/***************************************************************************
Set the system volume level from the current values of _vlm, _vlmBase
and _luVolSys. We set the system volume to the result of scaling
_luVolSys by _vlm and _vlmBase.
***************************************************************************/
void MIDO::_SetSysVlm(void)
{
ulong luVol;
luVol = LuVolScale(_luVolSys, _vlmBase);
luVol = LuVolScale(luVol, _vlm);
_SetSysVol(luVol);
}
/***************************************************************************
Reset the midi device. Assumes that the mutx is already ours.
***************************************************************************/
void MIDO::_Reset(void)
{
AssertThis(0);
if (hNil != _hmo)
{
// Reset channel pressure and pitch wheel on all channels
MIDEV midev;
long iv;
midiOutReset(_hmo);
for (iv = 0; iv < 16; iv++)
{
midev.lwSend = 0;
midev.rgbSend[0] = (byte)(0xD0 | iv);
midiOutShortMsg(_hmo, midev.lwSend);
midev.rgbSend[0] = (byte)(0xE0 | iv);
midev.rgbSend[2] = 0x40;
midiOutShortMsg(_hmo, midev.lwSend);
}
}
}
/***************************************************************************
Release or grab the midi output device depending on fSuspend.
***************************************************************************/
void MIDO::Suspend(bool fSuspend)
{
AssertThis(0);
_mutx.Enter();
if (FPure(fSuspend) != (hNil == _hmo))
{
if (fSuspend)
{
// kill all notes
_Reset();
// restore the volume level and free the device
_SetSysVol(_luVolSys);
midiOutClose(_hmo);
_hmo = hNil;
}
else
{
if (MMSYSERR_NOERROR == midiOutOpen(&_hmo, MIDI_MAPPER,
0, 0, CALLBACK_NULL))
{
_GetSysVol();
}
else
{
_hmo = hNil;
PushErc(ercSndMidiDeviceBusy);
}
}
}
_fSetVol = _fRestart = fTrue;
_mutx.Leave();
}
/***************************************************************************
Set the master volume for the device.
***************************************************************************/
void MIDO::SetVlm(long vlm)
{
AssertThis(0);
if (vlm != _vlmBase)
{
_vlmBase = vlm;
_fSetVol = fTrue;
}
}
/***************************************************************************
Return the current master volume.
***************************************************************************/
long MIDO::VlmCur(void)
{
AssertThis(0);
return _vlmBase;
}
/***************************************************************************
Play the given midi event. Returns false iff the midi stream should be
started over from the beginning in fast forward mode.
***************************************************************************/
bool MIDO::FPlay(long sii, long spr, MIDEV *pmidev, long vlm, ulong grfmido)
{
AssertThis(0);
AssertVarMem(pmidev);
// assume we don't have to restart
bool fRet = fTrue;
_mutx.Enter();
// see if this sound has higher priority than the current one
if (_sii == sii)
Assert(_spr == spr, 0);
else if (siiNil == _sii || spr >= _spr && (sii > _sii || spr > _spr))
{
// this sound is higher priority so play it.
_sii = sii;
_spr = spr;
_fRestart = fTrue;
}
// if this sound isn't the current one or the output we're deactivated
// just pretend we played the event
if (_sii != sii || hNil == _hmo)
goto LDone;
// If we need to restart, reset the device. If this is the first event
// in the stream, go ahead and play it - otherwise, return false to tell
// the client to restart.
if (_fRestart)
{
_Reset();
_fRestart = fFalse;
if (!(grfmido & fmidoFirst))
{
fRet = fFalse;
goto LDone;
}
}
// do fast forward filtering
if (grfmido & fmidoFastFwd)
{
// don't play notes or do other stuff that doesn't affect
// the (long term) device state.
switch (pmidev->rgbSend[0] & 0xF0)
{
default:
goto LDone;
case 0xB0: // control change
case 0xC0: // program change
case 0xD0: // channel pressure
case 0xF0: // special stuff
break;
}
}
// make sure the volume is set correctly
if (_fSetVol || _vlm != vlm)
{
_vlm = vlm;
_fSetVol = fFalse;
_SetSysVlm();
}
// finally, we can play the event
midiOutShortMsg(_hmo, pmidev->lwSend);
LDone:
_mutx.Leave();
return fRet;
}
/***************************************************************************
siiOld is being replaced by siiNew.
***************************************************************************/
void MIDO::Transition(long siiOld, long siiNew, long sprNew)
{
AssertThis(0);
_mutx.Enter();
if (_sii == siiOld && siiNil != siiOld)
{
_sii = siiNew;
_spr = sprNew;
_Reset();
}
_mutx.Leave();
}
/***************************************************************************
sii is going away.
***************************************************************************/
void MIDO::Close(long sii)
{
AssertThis(0);
_mutx.Enter();
if (_sii == sii && siiNil != sii)
{
_sii = siiNil;
if (hNil != _hmo)
_Reset();
}
_mutx.Leave();
}
/***************************************************************************
Midi player queue.
***************************************************************************/
typedef class MPQUE *PMPQUE;
#define MPQUE_PAR SNQUE
#define kclsMPQUE 'mpqu'
class MPQUE : public MPQUE_PAR
{
RTCLASS_DEC
ASSERT
MARKMEM
protected:
HN _hevtQueue; // the queue event object - to signal new input
bool _fChanged; // also signals new input - for extra protection
HN _hth; // the thread handle
MUTX _mutx; // mutex to restrict access to member variables
MSTP _mstp; // midi stream parser
long _dtsSlip; // amount of time we've slipped by
long _sii; // id and priority of sound we're currently serving
long _spr;
long _vlm; // volume to play back at
MIDEV _midev; // current midi event
ulong _tsStart; // time current sound was started
ulong _grfmido; // options for midi output device
bool _fMidevValid: 1; // is _midev valid?
bool _fDone: 1; // should the thread terminate?
MPQUE(void);
virtual void _Enter(void);
virtual void _Leave(void);
virtual bool _FInit(void);
virtual PBACO _PbacoFetch(PRCA prca, CTG ctg, CNO cno);
virtual void _Queue(long isndinMin);
virtual void _PauseQueue(long isndinMin);
virtual void _ResumeQueue(long isndinMin);
static ulong __stdcall _ThreadProc(void *pv);
ulong _LuThread(void);
void _DoEvent(bool fRestart, long *pdtsWait);
bool _FGetEvt(void);
bool _FStartQueue(void);
void _PlayEvt(void);
public:
static PMPQUE PmpqueNew(void);
~MPQUE(void);
};
RTCLASS(MPQUE)
/***************************************************************************
MT: Constructor for a midi player queue.
***************************************************************************/
MPQUE::MPQUE(void)
{
}
/***************************************************************************
MP: Destructor for a midi player queue.
***************************************************************************/
MPQUE::~MPQUE(void)
{
AssertThis(0);
if (hNil != _hth)
{
// tell the thread to end and wait for it to finish
_fDone = fTrue;
SetEvent(_hevtQueue);
WaitForSingleObject(_hth, INFINITE);
}
_mutx.Enter();
if (hNil != _hevtQueue)
CloseHandle(_hevtQueue);
// clear the midi stream parser
_mstp.Init(pvNil);
_mutx.Leave();
}
#ifdef DEBUG
/***************************************************************************
Assert the validity of a MPQUE.
***************************************************************************/
void MPQUE::AssertValid(ulong grf)
{
_mutx.Enter();
MPQUE_PAR::AssertValid(0);
AssertPo(&_mstp, 0);
_mutx.Leave();
}
/***************************************************************************
Mark memory for the MPQUE.
***************************************************************************/
void MPQUE::MarkMem(void)
{
AssertValid(0);
MPQUE_PAR::MarkMem();
_mutx.Enter();
MarkMemObj(&_mstp);
_mutx.Leave();
}
#endif //DEBUG
/***************************************************************************
MT: Static method to create a new midi player queue.
***************************************************************************/
PMPQUE MPQUE::PmpqueNew(void)
{
PMPQUE pmpque;
if (pvNil == (pmpque = NewObj MPQUE))
return pvNil;
if (!pmpque->_FInit())
ReleasePpo(&pmpque);
AssertNilOrPo(pmpque, 0);
return pmpque;
}
/***************************************************************************
MT: Initialize the midi queue.
***************************************************************************/
bool MPQUE::_FInit(void)
{
AssertBaseThis(0);
ulong luThread;
if (!MPQUE_PAR::_FInit())
return fFalse;
// create an auto-reset event to signal that the midi stream at
// the head of the queue has changed.
_hevtQueue = CreateEvent(pvNil, fFalse, fFalse, pvNil);
if (hNil == _hevtQueue)
return fFalse;
// create the thread in a suspended state
_hth = CreateThread(pvNil, 1024, MPQUE::_ThreadProc, this,
CREATE_SUSPENDED, &luThread);
if (hNil == _hth)
return fFalse;
SetThreadPriority(_hth, THREAD_PRIORITY_TIME_CRITICAL);
// set other members
_sii = siiNil;
// start the thread
ResumeThread(_hth);
AssertThis(0);
return fTrue;
}
/***************************************************************************
Enter the critical section protecting member variables.
***************************************************************************/
void MPQUE::_Enter(void)
{
_mutx.Enter();
}
/***************************************************************************
Leave the critical section protecting member variables.
***************************************************************************/
void MPQUE::_Leave(void)
{
_mutx.Leave();
}
/***************************************************************************
MT: Fetch the given sound chunk as a midi stream.
***************************************************************************/
PBACO MPQUE::_PbacoFetch(PRCA prca, CTG ctg, CNO cno)
{
AssertThis(0);
AssertPo(prca, 0);
return prca->PbacoFetch(ctg, cno, &MIDS::FReadMids);
}
/***************************************************************************
The element at the head of the queue changed, notify the thread.
***************************************************************************/
void MPQUE::_Queue(long isndinMin)
{
AssertThis(0);
_mutx.Enter();
if (_isndinCur == isndinMin)
{
// signal the thread that data changed
SetEvent(_hevtQueue);
_fChanged = fTrue;
}
_mutx.Leave();
}
/***************************************************************************
Pause the sound at the head of the queue.
***************************************************************************/
void MPQUE::_PauseQueue(long isndinMin)
{
AssertThis(0);
SNDIN sndin;
_mutx.Enter();
if (_isndinCur == isndinMin && _pglsndin->IvMac() > _isndinCur)
{
_pglsndin->Get(_isndinCur, &sndin);
sndin.dtsStart = TsCurrentSystem() - _tsStart;
_pglsndin->Put(_isndinCur, &sndin);
_Queue(_isndinCur);
}
_mutx.Leave();
}
/***************************************************************************
Resume the sound at the head of the queue.
***************************************************************************/
void MPQUE::_ResumeQueue(long isndinMin)
{
AssertThis(0);
_Queue(isndinMin);
}
/***************************************************************************
AT: Static method. Thread function for the midi thread object.
***************************************************************************/
ulong __stdcall MPQUE::_ThreadProc(void *pv)
{
PMPQUE pmpque = (PMPQUE)pv;
AssertPo(pmpque, 0);
return pmpque->_LuThread();
}
/***************************************************************************
AT: The midi playback thread.
***************************************************************************/
ulong MPQUE::_LuThread(void)
{
AssertThis(0);
bool fRestart;
long dtsWait = klwInfinite;
for (;;)
{
// wait until our time has expired or there is new data
fRestart = dtsWait > 0 &&
WAIT_TIMEOUT != WaitForSingleObject(_hevtQueue,
dtsWait == klwInfinite ? INFINITE : dtsWait);
// check to see if this thread should end
if (_fDone)
return 0;
_mutx.Enter();
if (_fChanged && !fRestart)
dtsWait = klwInfinite;
else
{
_fChanged = fFalse;
_DoEvent(fRestart, &dtsWait);
}
_mutx.Leave();
}
}
/***************************************************************************
Called when it's time to send the next midi event or when the queue
has changed. Assumes the mutx is already checked out.
***************************************************************************/
void MPQUE::_DoEvent(bool fRestart, long *pdtsWait)
{
if (fRestart && !_FStartQueue())
*pdtsWait = klwInfinite;
else if (!_FGetEvt())
{
// we're done playing this tune, so start the next one
_isndinCur++;
*pdtsWait = _FStartQueue() ? 0 : klwInfinite;
}
else
{
// we have a valid midi event
*pdtsWait = (long)(_midev.ts - TsCurrentSystem());
if (*pdtsWait <= 0)
{
// go ahead and send it
if (*pdtsWait < -kdtsMinSlip && !(_grfmido & fmidoFastFwd))
{
_dtsSlip -= *pdtsWait;
*pdtsWait = 0;
}
_PlayEvt();
}
else
_grfmido &= ~fmidoFastFwd;
}
}
/***************************************************************************
AT: Start playing the sound at the head of the queue. Return non-zero
iff the queue wasn't empty. Note that the sound is left in the queue.
***************************************************************************/
bool MPQUE::_FStartQueue(void)
{
SNDIN sndin;
_mutx.Enter();
// set up the midi stream parser (_mstp).
for ( ; _isndinCur < _pglsndin->IvMac(); _isndinCur++)
{
_pglsndin->Get(_isndinCur, &sndin);
AssertPo(sndin.pbaco, 0);
if (0 <= sndin.cactPause)
break;
}
if (_isndinCur < _pglsndin->IvMac() && 0 == sndin.cactPause)
{
// transition to the new tune
_mido.Transition(_sii, sndin.sii, sndin.spr);
_sii = sndin.sii;
_spr = sndin.spr;
_vlm = sndin.vlm;
_tsStart = TsCurrentSystem() - sndin.dtsStart;
_mstp.Init((PMIDS)sndin.pbaco, _tsStart);
_dtsSlip = TsCurrentSystem() - sndin.dtsStart - _tsStart;
_grfmido = fmidoNil;
_fMidevValid = fFalse;
if (sndin.dtsStart > 0)
_grfmido |= fmidoFastFwd;
if (sndin.pbaco != pvNil)
_grfmido |= fmidoFirst;
}
else
{
// close the old tune
_mido.Close(_sii);
_sii = siiNil;
sndin.pbaco = pvNil;
_mstp.Init(pvNil, 0);
_fMidevValid = fFalse;
}
_mutx.Leave();
return sndin.pbaco != pvNil;
}
/***************************************************************************
AT: Get the next event. Assumes we already have the mutex.
***************************************************************************/
bool MPQUE::_FGetEvt(void)
{
AssertThis(0);
ulong ts;
SNDIN sndin;
if (_fMidevValid)
return fTrue;
ts = kluMax;
while (_mstp.FGetEvent(&_midev))
{
_midev.ts += _dtsSlip;
// skip empty events
if (_midev.cb > 0)
{
_fMidevValid = fTrue;
return fTrue;
}
ts = _midev.ts;
}
// see if we should repeat the current midi stream
_pglsndin->Get(_isndinCur, &sndin);
if (--sndin.cactPlay == 0)
return fFalse;
_pglsndin->Put(_isndinCur, &sndin);
_tsStart = TsCurrentSystem();
if (ts != kluMax && ts > _tsStart)
_tsStart = ts;
_mstp.Init((PMIDS)sndin.pbaco, _tsStart);
_dtsSlip = TsCurrentSystem() - _tsStart;
if (!_mstp.FGetEvent(&_midev))
{
// there's nothing in this midi stream
return fFalse;
}
_midev.ts += _dtsSlip;
_fMidevValid = fTrue;
return fTrue;
}
/***************************************************************************
AT: Play the current event. Assumes we have the member mutex (_mutx).
***************************************************************************/
void MPQUE::_PlayEvt(void)
{
AssertThis(0);
Assert(_fMidevValid, 0);
if (!_mido.FPlay(_sii, _spr, &_midev, _vlm, _grfmido))
{
// restart the stream in fast forward mode
SNDIN sndin;
_pglsndin->Get(_isndinCur, &sndin);
_mstp.Init((PMIDS)sndin.pbaco, _tsStart);
_grfmido |= fmidoFastFwd;
}
_fMidevValid = fFalse;
_grfmido &= ~fmidoFirst;
}
/***************************************************************************
Constructor for the midi player device.
***************************************************************************/
MIDP::MIDP(void)
{
}
/***************************************************************************
Destructor for the midi player device.
***************************************************************************/
MIDP::~MIDP(void)
{
_Suspend(fTrue);
}
/***************************************************************************
Static method to create the midiplayer device.
***************************************************************************/
PMIDP MIDP::PmidpNew(void)
{
PMIDP pmidp;
if (pvNil == (pmidp = NewObj MIDP))
return pvNil;
if (!pmidp->_FInit())
ReleasePpo(&pmidp);
pmidp->_Suspend(!pmidp->_fActive || pmidp->_cactSuspend > 0);
AssertNilOrPo(pmidp, 0);
return pmidp;
}
/***************************************************************************
Allocate a new midi queue.
***************************************************************************/
PSNQUE MIDP::_PsnqueNew(void)
{
AssertThis(0);
return MPQUE::PmpqueNew();
}
/***************************************************************************
Get or release the HMIDIOUT depending on fSuspend.
***************************************************************************/
void MIDP::_Suspend(bool fSuspend)
{
_mido.Suspend(fSuspend);
}
/***************************************************************************
Set the volume.
***************************************************************************/
void MIDP::SetVlm(long vlm)
{
AssertThis(0);
_mido.SetVlm(vlm);
}
/***************************************************************************
Get the volume.
***************************************************************************/
long MIDP::VlmCur(void)
{
AssertThis(0);
return _mido.VlmCur();
}