Yet Another (WA)SAPI New Technology (YASAPI/NT) Output Plugin for Winamp (out_yasapi-nt) 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.
out_yasapi-nt 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 out_yasapi-nt. If not, see <http://www.gnu.org/licenses/>.
Last generated 201007-0848.
Nanos gigantum humeris insidentes: This project is dedicated to my European heritage. It is strictly to be understood as a statement against the "sweet" liberal lie of "multiculturalism" which is going to destroy Europe as we know it, in particular against the Merkel regime selling out Europe for nothing as we watch. #TeamWhite
PLEASE NOTE THAT THIS PROJECT IS AN EXPERIMENTAL RATHER THEN AN INDUSTRIAL STRENGTH EFFORT. THIS PROJECT IS NOT FOR YOU. IT IS FOR ME IN ORDER TO LEARN SOMETHING. IF THER'S SOMETHING ALONG THE WAY I CAN DO FOR YOU THAT'S GREAT!
The UML-diagrams are created by means of the impressive plantuml tool.
Note: This document in some details might be ahead of time
Note: | You may toggle the size of an image by clicking on it. |
The aim of this document is to give a rough overview on the ideas the YASAPI/NT plug-in is based on. This document to some extend describes YASAPI/NT v2.2.0-β5. Later we decided to work out the concept of a state more sharply and moved on.
The aim of class Cache is to cache several objects and to provide an interface to the configuration dialog.
The aim of class Engine is to implement a WASAPI playback engine.
At the core of the plug-in is class Engine. The main idea is to implement class Engine in such a way that at each point in time it is in a definite state.
State Transition to State Action Taken by Winamp's / the Active Input Plug-in's Threads
(mostly triggered by User Intervention)YASAPI/NT's Render Thread
(at constant Time Intervals called a Playback Cycle)start ⇥ end When exit and no track has ever been played: no operation. ↓ eEngineBase When the first track is played: acquire several non-WASAPI objects from Windows persisting during the plug-in's lifetime (including starting the render thread.) eEngineBase ⇥ end When exit: release the non-WASAPI objects. ↓ eEngineInitialized When next track, i.e. this plug-in's Open method is called: from an initially cached (by means of class Cache) instance of IMMDevice, aquire and initialize an instance of IAudioClient along with an instance of YASAPI/NT's buffer, i.e. class Ring. eEngineInitialized ↑ eEngineBase When stop or exit: release the instance of IAudioClient along with the instance of YASAPI/NT's buffer, i.e. class Ring. ↺ eEngineInitialized This plug-in's Write method is called and the minimum amount of audio frames is not already written to YASAPI/NT's buffer. ↓ eEngineStarted This plug-in's Write method is called and the minimum amount of audio frames is written to YASAPI/NT's buffer: dispatch the request for entering state eEngineStarted to the render thread. The minimum amount of audio frames is written to YASAPI/NT's buffer: from the instance of IAudioClient, acquire an instance of IAudioRenderClient. eEngineStarted ↑ eEngineInitialized When stop or exit: release the instance of IAudioRenderClient. ↺ eEngineStarted This plug-in's Write method (writing to YASAPI/NT's buffer) is periodically called by the active input plug-in in a time interval which is Note: These two are the main reasons for letting YASAPI/NT have its own buffer, i.e. for decoupling calls to this plug-in's Write method from really writing to WASAPI's buffer.
- vastly fluctuating (at least compared to the high accuracy of the render thread's playback cycle), and
- by no means correlated to the the render thread's playback cycle.
While decoding of the active input plug-in hasn't come to an end: with each playback cycle play, i.e.
- read a certain amount of audio frames from YASAPI/NT's buffer, and
- render that amount of audio frames by means of IAudioRenderClient's instance (in case no sufficient audio frames are available insert silence and count the event as dropout)
↓ eEngineDraining This plug-in's IsPlaying method is called for the first time telling YASAP/NT that
- decoding of the current track has come to an end,
- no further calls to the Write method will follow, i.e.
- the active input plugin-in is waiting for YASAPI/NT to be drained (for that means the active input plug-in periodically re-issues the call if needed.)
When decoding of the active input plug-in has come to an end (signaled due to the active input plug-in by calling this ouput plug-in's IsPlaying method for the first time). eEngineDraining ↺ eEngineDraining While YASAPI/NT's buffer is not empty: with each playback cycle play, i.e.
- read a certain amount of audio frames from YASAPI/NT's buffer, and
- render that amount of audio frames by means of IAudioRenderClient's instance
↑ eEngineStarted When in gapless mode, the active input plug-in is able to issue the next Open() call before YASAP/NT's buffer is drained, and the input format hasn't changed.
Note: This is a perfect example of contradicting constraints:The only way to resolve this is to put the burdon upon you: you need to configure this plug-in according to you're own preferences.
- when in gapless mode you might want to have YASAPI/NT's buffer as large as possible in order to make sure that it's not already drained before the active input plug-in is able to issue the next Open() call, and
- for low latency playback you need YASAPI/NT's buffer as small as possible.
↓ eEngineEot When YASAPI/NT's buffer is empty.
Note: In case of double bufferd playback (i.e. exclusive pulled playback) an artificial (i.e. non WASAPI supported) extra cycle needs to be added (cf. the usage of a Waitable Timer Object by Microsoft's text-book example for rendering Exclusive-Mode Streams.) That's the reason why state eEngineEot is really needed because we cannnot in each case immediately go to state eEngineBase.eEngineEot ↟ eEngineBase Unconditionally.
State Transition to State Action Taken by Winamp's / the Active Input Plug-in's Threads
(the Clients/Producers)YASAPI/NT's Render Thread
(the Server/Consumer)start ↡ eEngineThreadRunning When destruction, i.e. when coming from state eEngineInitialized: no operation. ↓ eEngineCriticalSection When creation: acquire Engine::cs by means of InitializeCriticalSection. eEngineCriticalSection ⇥ end When destruction: release Engine::cs by means of DeleteCriticalSection. ↓ eEngineTimerEvent When creation: acquire Engine::timer.hEvent by means of CreateEvent. eEngineTimerEvent ↑ eEngineCriticalSection When destruction: release Engine::timer.hEvent by means of CloseHandle. ↓ eEngineTimer When creation: Acquire Engine:timer.hTimer by means of CreateWaitableTimer. eEngineTimer ↑ eEngineTimerEvent When destruction: release Engine::timer.hTimer by means of CloseHandle. ↓ eEngineProducer When creation: acquire Engine::thread.hProducer by means of CreateEvent.
Note: It'swhere the render thread (i.e. the server/consumer thread) in a forever loop waits on by means of WaitForMultipleObjects for some request it might serve. It will wake up when the clients/producers call SetEvent on Engine::thread.hProducer in order to dispatch some request.
- Engine::thread.hProducer,
- Engine::timer.hTimerEvent, and
- Engine::timer.hTimer
eEngineProducer ↑ eEngineTimer When destruction: release Engine::thread.hProducer by means of CloseHandle. ↓ eEngineConsumer When creation: acquire Engine::thread.hConsumer by means of CreateEvent.
Note: It's Engine::thread.hConsumer where the client/producer threads wait on by means of WaitForSingleObject until they're signaled by the the render thread (i.e. the server/consumer thread). They will wake up when the the render thread (i.e. the server/consumer thread) call SetEvent on Engine::thread.hConsumer in order to hand-shake having received some request.eEngineConsumer ↑ eEngineProducer When destruction: release Engine::thread.hConsumer by means of CloseHandle. ↓ eEngineEot When creation: acquire Engine::thread.hEot by means of CreateEvent. eEngineEot ↑ eEngineConsumer When destruction: release Engine::thread.hEot by means of CloseHandle. ↓ eEngineApi When creation: acquire Engine::thread.hApi by means of CreateEvent. eEngineApi ↑ eEngineEot When destruction: release Engine::thread.hApi by means of CloseHandle. ↓ eEngineThread When creation: acquire Engine::thread.hThread by means of CreateThread (i.e. start the render thread.) eEngineThread ↑ eEngineApi When destruction:
- wait on Engine::thread.hThread by means of WaitForSingleObject until the thread has vanished, and
- release Engine::thread.hThread by means of CloseHandle.
↓ eEngineThreadRunning When creation: wait on Engine:thread.hConsumer by means of WaitForSingleObject that the render thread (i.e. the server/consumer thread) signals that it has entered the eEngineThreadRunning state and hence is ready to serve requests from clients/producers. Before entering a forever loop waiting for client's/producer's requests to serve enter the eEngineThreadRunning state and signal Engine::thread.hConsumer of that event by means of SetEvent. eEngineThreadRunning ↑ eEngineThread When destruction:
- signal the render thread (i.e. the server/consumer thread) a eRequestExit by means of SetEvent on Engine::thread.hProducer, and
- wait on Engine::thread.hConsumer by means of WaitForSingleObject that the render thread (i.e. the server/consumer thread) signals that it is going to quit, i.e. that it has made the transition to state eEngineThread.
In it's forever loop the the render thread (i.e. the server/consumer thread) most of the time spends with WaitForMultipleObjects on When woke-up on Engine::thread.hProducer and signaled with a eRequestExit:
- Engine::thread.hProducer,
- Engine::timer.hTimerEvent, and
- Engine::timer.hTimer.
- break out of the forever loop,
- go to state eEngineThread, and
- signal the client/producer thread of the state transition by means of SetEvent on Engine::hConsumer.
⇥ end When creation: no operation.
State Transition to State Action Taken by Winamp's / the Active Input Plug-in's Threads
(the Clients/Producers)start ↡ eEngineRing When stop or exit: no operation. ↓ eEngineClose When next track: no operation. eEngineClose ⇥ end When stop or exit: no operation. ↓ eEngineAudioClientInititial When next track: from an initially cached (by means of class Cache) instance of IMMDevice, aquire and initialize an instance of IAudioClient. eEngineAudioClientInititial ↑ eEngineClose When stop or exit: no operation (because the instance of IAudioClient is already released.) ↓ eEngineRing When next track and for the current gapless sequence the active input plug-in calls this output plug-in's Write method for the first time: initialize the instance of YASAPI/NT's buffer (i.e. class Ring) considering parmeter len (i.e. ceil YASAPI/NT' buffer size to a multiple of parameter len in order to avoid the following deadlock: the active input plug-in tries to Write a block of audio samples of size len needed to reach the minimum amount and this output plug-in avoids doing it because it doesn't fit even if there's still room left - too less. Note: prior and most likely as well hijacked versions of this plug-in where vulnerable for this kind of deadlock you might have noticed when playback doesn't start without seeing an obvious reason.) eEngineRing ↑ eEngineAudioClientInititial When stop or exit: destroy the instance of YASAPI/NT's buffer, i.e. class Ring. ⇥ end When next track: no operation.
State Transition to State Action Taken by Winamp's / the Active Input Plug-in's Thread
(mostly triggered by User Intervention)YASAPI/NT's Render Thread start ↡ eEngineTimerStarted When destruction. ↓ eEnginePauseDisconnected When within the current gapless session the active input plug-in calls for the first time Write with the consequence that the minimum number of audio frames written to YASAPI/NT's buffer is reached: dispatch the request to go to state eEnginePauseDisconnected to the the render thread (i.e. the server/consumer thread):
- SetEvent on Engine::thread.hProducer in order to wake-up the the render thread (i.e. the server/consumer thread), and
- WaitForSingleObject on Engine::thread.hConsumer in order to wait for the the render thread (i.e. the server/consumer thread) to hand-shake.
In it's forever loop the render thread (i.e. the server/consumer thread) most of the time spends with WaitForMultipleObjects on When woke-up on Engine::thread.hProducer and signaled with a eRequestStart:
- Engine::thread.hProducer,
- Engine::timer.hTimer, and
- Engine::timer.hTimerEvent.
- go to state eEnginePauseDisconnected, and
- signal the client/producer thread of the state transition by means of SetEvent on Engine::thread.hConsumer.
eEnginePauseDisconnected ⇥ end When destruction. ↓ eEngineAudioClient When creation. eEngineAudioClient ↑ eEnginePauseDisconnected When destruction. ↓ eEngineVolume When creation. eEngineVolume ↑ eEngineAudioClient When destruction. ↓ eEngineAudioClock When creation. eEngineAudioClock ↑ eEngineVolume When destruction. ↓ eEngineAudioRenderClient When creation. eEngineAudioRenderClient ↑ eEngineAudioClock When destruction. ↓ eEnginePauseConnected When creation. eEnginePauseConnected ↑ eEngineAudioRenderClient When destruction. ↓ eEngineAudioClientStarted When creation. eEngineAudioClientStarted ↑ eEnginePauseConnected When destruction. ↓ eEngineTimerStarted When creation. eEngineTimerStarted ↑ eEnginePauseDisconnected When destruction. ⇥ end When creation.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: void EngineRequestState(Engine *pEngine, EEngine eState) { pEngine->thread.eStateRequest=eState; SetEvent(pEngine->thread.hProducer); while (pEngine->eState!=eState) { ResetEvent(pEngine->thread.hConsumer); // { LeaveCriticalSection(&pEngine->cs); // { WaitForSingleObject(pEngine->thread.hConsumer,INFINITE); EnterCriticalSection(&pEngine->cs); // } } }
Lines Remark 3 Place the state the render/server thread should enter as a request into commonly used memory. 4 Signal the render/server thread of that request. 6-10 In a loop wait until the render/server thread signals that the requested state is entered.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: DWORD WINAPI EngineThreadProc(LPVOID lpParameter) { Engine *pEngine=lpParameter; Thread thread; DWORD dwWait; _ThreadCreate(&thread,pEngine); // { for (;;) { dwWait=_ThreadWait(&thread); switch (dwWait) { case WAIT_OBJECT_0+eThreadProducer: switch (pEngine->thread.eStateRequest) { case eEngineThread: goto exit; case eEngineThreadRunning: // The request is issued when not playing. _ThreadRequestKill(&thread,eEngineThreadRunning); continue; case eEnginePauseDisconnected: case eEnginePauseConnected: // The request is deferred. continue; case eEngineStarted: // The request is executed immediately. if (_ThreadRequestStart(&thread)<0) goto exit; continue; default: continue; } continue; case WAIT_OBJECT_0+eThreadTimerEvent: case WAIT_OBJECT_0+eThreadTimer: if (_ThreadRender(&thread)<0) goto exit; continue; default: continue; } } exit: _ThreadDestroy(&thread); // } return 0ul; }
Lines Remark to be continued ...
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 20: 31: 32: 33: int EngineCreateThread(Engine *pEngine) { int code=-1; EnterCriticalSection(&pEngine->cs); // { pEngine->thread.eStateRequest=eEngineNull; pEngine->thread.hThread=CreateThread( NULL, // _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, 0ul, // _In_ SIZE_T dwStackSize, EngineThreadProc, // _In_ LPTHREAD_START_ROUTINE lpStartAddress, pEngine, // _In_opt_ LPVOID lpParameter, 0ul, // _In_ DWORD dwCreationFlags, NULL // _Out_opt_ LPDWORD lpThreadId ); if (!pEngine->thread.hThread) goto exit; ++pEngine->eState; while (pEngine->eState<eEngineThreadRunning) { ResetEvent(pEngine->thread.hConsumer); // { LeaveCriticalSection(&pEngine->cs); // { WaitForSingleObject(pEngine->thread.hConsumer,INFINITE); EnterCriticalSection(&pEngine->cs); // } } code=0; exit: LeaveCriticalSection(&pEngine->cs); // } return code; }
Lines Remark to be continued ...
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: void EngineKill(Engine *pEngine, const EEngine eState) { // Left out ... switch (pEngine->eState) { // Left out ... case eEngineThreadRunning: if (eState==pEngine->eState) break; EnterCriticalSection(&pEngine->cs); // { EngineRequestState(pEngine,pEngine->eState-1); LeaveCriticalSection(&pEngine->cs); // } // Intentional fall-through. case eEngineThread: if (eState==pEngine->eState) break; --pEngine->eState; WaitForSingleObject(pEngine->thread.hThread,INFINITE); CloseHandle(pEngine->thread.hThread); pEngine->thread.hThread=NULL; // Intentional fall-through. // Left out ... default: break; } }
Lines Remark to be continued ...
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: typedef enum _EThread EThread; typedef struct _Thread Thread; enum _EThread { eThreadProducer, eThreadTimerEvent, eThreadTimer, eThreadCount, }; struct _Thread { Engine *pEngine; HANDLE aHandles[eThreadCount]; HANDLE hTask; DWORD dwTaskIndex; };
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: static void _ThreadCreate(Thread *pThread, Engine *pEngine) { pThread->pEngine=pEngine; pThread->aHandles[eThreadProducer]=pEngine->thread.hProducer; pThread->aHandles[eThreadTimerEvent]=pEngine->timer.hEvent; pThread->aHandles[eThreadTimer]=pEngine->timer.hTimer; pThread->hTask=NULL; pThread->dwTaskIndex=0ul; EnterCriticalSection(&pEngine->cs); ++pEngine->eState; SetEvent(pEngine->thread.hConsumer); }
Lines Remark to be continued ...
1: 2: 3: 4: 5: 6: 7: 8: 9: static void _ThreadDestroy(Thread *pThread) { Engine *pEngine=pThread->pEngine; --pEngine->eState; SetEvent(pEngine->thread.hConsumer); LeaveCriticalSection(&pEngine->cs); ExitThread(0ul); }
Lines Remark to be continued ...
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: static DWORD _ThreadWait(Thread *pThread) { Engine *pEngine=pThread->pEngine; DWORD dwWait; ResetEvent(pEngine->thread.hProducer); // { LeaveCriticalSection(&pEngine->cs); // { dwWait=WaitForMultipleObjects( eThreadCount, // _In_ DWORD nCount, pThread->aHandles, // _In_ const HANDLE *lpHandles, FALSE, // _In_ BOOL bWaitAll, INFINITE // _In_ DWORD dwMilliseconds ); EnterCriticalSection(&pEngine->cs); // } return dwWait; }
Lines Remark to be continued ...
1: 2: 3: 4: 5: 6: 7: static void _ThreadHandshake(Thread *pThread) { Engine *pEngine=pThread->pEngine; pEngine->thread.eStateRequest=eEngineNull; SetEvent(pEngine->thread.hConsumer); }
Lines Remark to be continued ...
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: static int _ThreadRequestStart(Thread *pThread) { int code=-1; Engine *pEngine=pThread->pEngine; if (eEngineStarted<=pEngine->eState) goto estart; if (pEngine->options.device.bProAudio&&!pThread->hTask) { pThread->hTask=AvSetMmThreadCharacteristicsW(L"Pro Audio", &pThread->dwTaskIndex); } EngineRender(pEngine); _ThreadHandshake(pThread); code=0; estart: return code; }
Lines Remark to be continued ...
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: static void _ThreadRequestKill(Thread *pThread, const EEngine eState) { Engine *pEngine=pThread->pEngine; EngineKill(pEngine,eState); if (pEngine->eState<eEngineAudioClient&&pThread->hTask) { AvRevertMmThreadCharacteristics(pThread->hTask); pThread->hTask=NULL; pThread->dwTaskIndex=0ul; } _ThreadHandshake(pThread); }
Lines Remark to be continued ...
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: static int _ThreadRender(Thread *pThread) { int code=-1; Engine *pEngine=pThread->pEngine; Ring *pRing=&pEngine->ring; EEngine eStateRequest=pEngine->thread.eStateRequest; switch (eStateRequest) { case eEngineNull: break; case eEngineBase: case eEnginePauseConnected: case eEnginePauseDisconnected: _ThreadRequestKill(pThread,eStateRequest); goto success; default: // Emit some error message (left out) ... break; } switch (pEngine->eState) { case eEnginePauseConnected: case eEnginePauseDisconnected: break; case eEngineDrainingDoubleBuffered: SetEvent(pEngine->thread.hApi); _ThreadRequestKill(pThread,eEngineBase); break; case eEngineDraining: if (0u==RingTargetSizeWritten(pRing)) { union { struct { UINT32 uFrames; IAudioRenderClient *pAudioRenderClient; BYTE *pData; HRESULT hr; }; struct { LARGE_INTEGER DueTime; int bCode; }; } u; if (!pEngine->param.bDoubleBuffered) { SetEvent(pEngine->thread.hApi); _ThreadRequestKill(pThread,eEngineBase); break; } // In case of double buffering we need to wait for another // playcack cycle. ++pEngine->eState; DWRITELN(EngineState(pEngine)); DASSERT_ENGINE_STATE(pEngine,eEngineDrainingDoubleBuffered,exit); switch (pEngine->options.device.eCycle) { case eCycleSilence: u.uFrames=pEngine->param.uNumFramesDevice; u.pAudioRenderClient=pEngine->pAudioRenderClient; u.hr=u.pAudioRenderClient->lpVtbl->GetBuffer(u.pAudioRenderClient, u.uFrames, // [in] UINT32 NumFramesRequested, &u.pData // [out] BYTE **ppData ); if (FAILED(u.hr)) { // Emit some error message (left out) ... goto exit; } u.hr=u.pAudioRenderClient->lpVtbl->ReleaseBuffer(u.pAudioRenderClient, u.uFrames, // [in] UINT32 NumFramesWritten, AUDCLNT_BUFFERFLAGS_SILENT // [in] DWORD dwFlags ); if (FAILED(u.hr)) { // Emit some error message (left out) ... // nowhere to go ... } break; default: u.DueTime=pEngine->param.DueTime; u.bCode=SetWaitableTimer( pEngine->timer.hTimer, // _In_ HANDLE hTimer, &u.DueTime, // _In_ const LARGE_INTEGER *pDueTime, 0l, // _In_ LONG lPeriod, NULL, // _In_opt_ PTIMERAPCROUTINE pfnCompletionRoutine, NULL, // _In_opt_ LPVOID lpArgToCompletionRoutine, FALSE // _In_ BOOL fResume ); if (!u.bCode) { // Emit some error message (left out) ... goto exit; } break; } break; } // Intentional fall-through. default: if (eEngineStarted<=pEngine->eState&&EngineRender(pEngine)<0) { // Emit some error message (left out) ... goto exit; } break; } success: code=0; exit: return code; }
Lines Remark to be continued ...