Last active
December 20, 2015 15:47
-
-
Save jimmcgowan/be8a52f2b03772811216 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Music Player | |
// This class will stream an MP3 file over HTTP using the Direct Show API | |
// | |
////////////////////////////// | |
// | |
// MusicPlayer.h | |
// | |
////////////////////////////// | |
#include <Windows.h> | |
#include <Dshow.h> // link with Strmiids.lib | |
#include <string> | |
// a struct to hold the details of a remote MP3 file | |
typedef struct _MP3Info { | |
std::wstring url; | |
long offsetInMS; | |
long fadeTimeInMS; | |
} MP3Info; | |
// a struct that holds a Direct Show playback graph and its various (COM) Interfaces | |
typedef struct _PlaybackGraph { | |
IGraphBuilder *graphBuilder; | |
IMediaSeeking *seekInterface; | |
IMediaControl *controlInterface; | |
IMediaEvent *eventInterface; | |
IBasicAudio *audioInterface; | |
} PlaybackGraph; | |
// The Music Player class | |
class MusicPlayer | |
{ | |
public: | |
MusicPlayer(); | |
~MusicPlayer(); | |
// The Basic Music Player API, volume is in the range 0.0 - 1.0 | |
void streamMP3(MP3Info newMP3Request); | |
void stop(); | |
float getVolume() const; | |
void setVolume(float newVolume); | |
protected: | |
// Creation and control of audio playback graphs is done on a dedicated thread, | |
// Friending this thread's main function allows it to query its corresponding Music Player instance | |
friend void audioControlThreadMain(LPVOID audioPlayerInstance); | |
PlaybackGraph getCurrentPlaybackgraph() const; | |
void setCurrentPlaybackGraph(PlaybackGraph newGraph); | |
MP3 mp3Request; | |
private: | |
PlaybackGraph mCurrentPlaybackGraph; | |
float mCurrentVolume; | |
CRITICAL_SECTION mp3StructAccess; | |
bool keepControlTheadAlive; | |
}; | |
////////////////////////////// | |
// | |
// MusicPlayer.cpp | |
// | |
////////////////////////////// | |
// Creation and control of DirectShow audio playback graphs is carried out on a dedicated thread to simplify | |
// events and timers, etc. These events are used to communicate with the audio control thread | |
HANDLE hControlThreadReadyEvent; | |
HANDLE hPlayNewMP3Event; | |
HANDLE hStopPlaybackEvent; | |
// Functions to create new playback graphs and release them, caller assumes ownership of graph members | |
HRESULT createNewPlaybackGraphForURL(LPCWSTR mp3URL, PlaybackGraph *out_pGraph); | |
void releasePlaybackGraph(PlaybackGraph playbackGraph); | |
PlaybackGraph nullPlaybackGraph(void); | |
// The Music Player Implementation | |
// Constructor and destructor | |
MusicPlayer::MusicPlayer(void *aDelegate, AudioPlayerFinishedPlaybackDelegateCall aCallback) | |
{ | |
// init ivars | |
mCurrentPlaybackGraph = nullPlaybackGraph(); | |
mVolume = 1.0; | |
// create the events that are used to communicate with the control thread | |
hControlThreadReadyEvent = CreateEvent(NULL, false, false, NULL); | |
hPlayNewMP3Event = CreateEvent(NULL, false, false, NULL); | |
hStopPlaybackEvent = CreateEvent(NULL, false, false, NULL); | |
// start the audio control thread | |
InitializeCriticalSection(&mp3StructAccess); | |
_keepControlTheadAlive = true; | |
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)&audioControlThreadMain, this, 0, NULL); | |
if (WaitForSingleObject(hControlThreadReadyEvent, 10000) != WAIT_OBJECT_0) | |
{ | |
printf("ERROR - MusicPlayer timed out waiting for audio control thread to setup"); | |
} | |
} | |
MusicPlayer::~MusicPlayer() | |
{ | |
setCurrentPlaybackGraph(nullPlaybackGraph()); | |
_keepControlTheadAlive = false; | |
DeleteCriticalSection(&mp3StructAccess); | |
} | |
// Music Player API | |
void MusicPlayer::streamMP3(MP3 newMP3Request) | |
{ | |
// set the MP3 struct | |
EnterCriticalSection(&mp3StructAccess); | |
mp3Request = newMP3Request; | |
LeaveCriticalSection(&mp3StructAccess); | |
// Signal the new playback setup event. This event will be handled in the audio control thread's event loop | |
SetEvent(hPlayNewMP3Event); | |
} | |
void MusicPlayer::stop() | |
{ | |
// Signal the playback stop event. This event will be handled in the audio control thread's event loop | |
SetEvent(hStopPlaybackEvent); | |
} | |
float MusicPlayer::getVolume() const | |
{ | |
return mCurrentVolume; | |
} | |
void MusicPlayer::setVolume(float newVolume) | |
{ | |
mCurrentVolume = constrainFloat(newVolume, 0.0, 1.0); | |
if(mCurrentPlaybackGraph.audioInterface != NULL) | |
{ | |
// DirectShow uses a volume range of -10000 to 0 | |
long volumeInDShowUnits = 10000 - ((long)mCurrentVolume * 10000); | |
mCurrentPlaybackGraph.audioInterface->put_Volume(volumeInDShowUnits); | |
} | |
} | |
// Graph builder function | |
HRESULT createNewPlaybackGraphForURL(LPCWSTR mp3URL, PlaybackGraph *out_pGraph) | |
{ | |
IGraphBuilder *graph; | |
HRESULT hr; | |
// Create the filter graph manager | |
hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void **)&graph); | |
if (FAILED(hr)) | |
{ | |
printf("ERROR - MusicPlayer - Could not create the Filter Graph Manager."); | |
return hr; | |
} | |
// Build the graph. | |
hr = graph->RenderFile(mp3URL, NULL); | |
if (FAILED(hr)) | |
{ | |
printf("ERROR - MusicPlayer - Could not build the Filter Graph."); | |
graph->Release(); | |
return hr; | |
} | |
// Get the seeking interface | |
IMediaSeeking *seekInterface = NULL; | |
if (FAILED(graph->QueryInterface(IID_IMediaSeeking, (void **)&seekInterface))) | |
{ | |
printf("ERROR - MusicPlayer - Could not access graph's seeking interface"); | |
graph->Release(); | |
return hr; | |
} | |
// Get the control interface | |
IMediaControl *controlInterface = NULL; | |
if (FAILED(graph->QueryInterface(IID_IMediaControl, (void **)&controlInterface))) | |
{ | |
printf("ERROR - MusicPlayer - Could not get graph's control interface"); | |
graph->Release(); | |
seekInterface->Release(); | |
return hr; | |
} | |
// get the graph's event interface | |
IMediaEvent *eventInterface = NULL; | |
if (FAILED(graph->QueryInterface(IID_IMediaEvent, (void **)&eventInterface))) | |
{ | |
printf("ERROR - MusicPlayer - Could not get graph's event interface"); | |
graph->Release(); | |
seekInterface->Release(); | |
controlInterface->Release(); | |
return hr; | |
} | |
// Get the audio interface | |
IBasicAudio *audioInterface; | |
if (FAILED(graph->QueryInterface(IID_IBasicAudio, (void **)&audioInterface))) | |
{ | |
printf("ERROR - MusicPlayer - Could not get graph's audio interface"); | |
graph->Release(); | |
seekInterface->Release(); | |
controlInterface->Release(); | |
eventInterface->Release(); | |
return hr; | |
} | |
out_pGraph->graphBuilder = graph; | |
out_pGraph->seekInterface = seekInterface; | |
out_pGraph->controlInterface = controlInterface; | |
out_pGraph->eventInterface = eventInterface; | |
out_pGraph->audioInterface = audioInterface; | |
return S_OK; | |
} | |
void releasePlaybackGraph(PlaybackGraph playbackGraph) | |
{ | |
if(playbackGraph.graphBuilder != NULL) | |
playbackGraph.graphBuilder->Release(); | |
if(playbackGraph.controlInterface != NULL) | |
playbackGraph.controlInterface->Release(); | |
if(playbackGraph.seekInterface != NULL) | |
playbackGraph.seekInterface->Release(); | |
if(playbackGraph.eventInterface != NULL) | |
playbackGraph.eventInterface->Release(); | |
if(playbackGraph.audioInterface != NULL) | |
playbackGraph.audioInterface->Release(); | |
} | |
PlaybackGraph nullPlaybackGraph(void) | |
{ | |
PlaybackGraph playbackGraph; | |
playbackGraph.graphBuilder = NULL; | |
playbackGraph.controlInterface = NULL; | |
playbackGraph.seekInterface = NULL; | |
playbackGraph.eventInterface = NULL; | |
playbackGraph.audioInterface = NULL; | |
return playbackGraph; | |
} | |
// Audio Control Thread Main Function | |
void audioControlThreadMain(LPVOID audioPlayerInstance) | |
{ | |
// Initialize the COM library. | |
if (FAILED(CoInitialize(NULL))) | |
{ | |
printf("MusicPlayer Could not initialize COM library"); | |
} | |
// cast a local var to point to the music player instance | |
MusicPlayer *audioPlayer = (MusicPlayer *)audioPlayerInstance; | |
// In addition to events from the Music Player instance, the event loop needs to observe and process for the playback graph's events, | |
// However there is no graph on the first pass through the event loop, so we stick in a dummy event the first time. | |
HANDLE hPlaybackGraphEvent = CreateEvent(NULL, false, false, NULL); | |
// signal that the control thread is ready | |
SetEvent(hControlThreadReadyEvent); | |
// thread event loop | |
while(audioPlayer->_keepControlTheadAlive) | |
{ | |
const HANDLE eventHandles[3] = {hPlayNewMP3Event, hStopPlaybackEvent, hPlaybackGraphEvent}; | |
DWORD eventSignalled = WaitForMultipleObjects(3, eventHandles, false, 10000); | |
switch (eventSignalled) { | |
case 0: // play New MP3 Event | |
{ | |
// grab the MP3 request from the audio player instance | |
EnterCriticalSection(&(audioPlayer->mp3StructAccess)); | |
std::wstring mp3URL = audioPlayer->mp3Request.url; | |
long fadeTimeInMS = audioPlayer->mp3Request.fadeTimeInMS; | |
long offsetInMS = audioPlayer->mp3Request.offsetInMS; | |
LeaveCriticalSection(&(audioPlayer->mp3StructAccess)); | |
// create a playback graph for the MP3 | |
PlaybackGraph newPlaybackGraph; | |
if(FAILED(createNewPlaybackGraphForURL(mp3URL.c_str(), &newPlaybackGraph))) | |
{ | |
printf("ERROR - MusicPlayer - Could not create new playback graph"); | |
break; | |
} | |
// get the MP3's duration | |
LONGLONG durationInUnits = -1; | |
LONGLONG durationInMS = -1; | |
if (FAILED(newPlaybackGraph.seekInterface->GetDuration(&durationInUnits))) | |
{ | |
printf("ERROR - MusicPlayer - Could not get MP3 duration"); | |
releasePlaybackGraph(newPlaybackGraph); | |
break; | |
} | |
else | |
{ | |
// The DirectShow framework has 10,000,000 "units" per second, so 1ms = 10,000 units | |
durationInMS = durationInUnits / 10000; | |
} | |
// Seek to the required start point in the file | |
// Seeking will cause a longer buffer time | |
if (offsetInMS < durationInMS) | |
{ | |
DWORD seekCapabilites = 0; | |
if (SUCCEEDED(newPlaybackGraph.seekInterface->GetCapabilities(&seekCapabilites))) | |
{ | |
if ((AM_SEEKING_CanSeekAbsolute & seekCapabilites) && (AM_SEEKING_CanSeekForwards & seekCapabilites)) | |
{ | |
// we have an offset in ms, the DirectShow framework has 10,000,000 "units" per second, so 1ms = 10,000 units | |
REFERENCE_TIME playPosition = offsetInMS * 10000; | |
if(SUCCEEDED(newPlaybackGraph.seekInterface->SetPositions(&playPosition, AM_SEEKING_AbsolutePositioning, NULL, AM_SEEKING_NoPositioning))) | |
{ | |
durationInMS -= offsetInMS; | |
} | |
else | |
{ | |
printf("MusicPlayer - Could not seek playback graph"); | |
} | |
} | |
else | |
{ | |
printf("MusicPlayer - Playback graph does not have seek capabilities"); | |
} | |
} | |
else | |
{ | |
printf("MusicPlayer - Could not get graph seek capabilities"); | |
} | |
} | |
// Take note if we should (cross)fade in the new MP3 | |
bool shouldCrossfade = false; | |
if (offsetInMS != 0) | |
shouldCrossfade = true; | |
if (audioPlayer->getCurrentPlaybackgraph().controlInterface != NULL) | |
{ | |
OAFilterState currentGraphState; | |
audioPlayer->getCurrentPlaybackgraph().controlInterface->GetState(100, ¤tGraphState); | |
if(currentGraphState == State_Running) | |
shouldCrossfade = true; | |
} | |
// set the initial volume of the new graph to zero (min) if we are crossfading, or to the current volume otherwise | |
long volumeInDShowUnits = (shouldCrossfade) ? -10000 : 10000 - ((long)audioPlayer->getVolume() * 10000); | |
newPlaybackGraph.audioInterface->put_Volume(volumeInDShowUnits); | |
// start the new graph running | |
if (FAILED(newPlaybackGraph.controlInterface->Run())) | |
{ | |
printf("ERROR - MusicPlayer - Could not start graph running"); | |
releasePlaybackGraph(newPlaybackGraph); | |
break; | |
} | |
// after starting to run, the graph will be in a paused state until it has buffered enough of the MP3 to start playing, then it will unpause | |
// so we wait for the unpaused event | |
HANDLE hNewPlaybackGraphEvent = NULL; | |
if (FAILED(newPlaybackGraph.eventInterface->GetEventHandle((OAEVENT*)&hNewPlaybackGraphEvent))) | |
{ | |
printf("ERROR - MusicPlayer - Could not get graph's event handle"); | |
newPlaybackGraph.controlInterface->Stop(); | |
releasePlaybackGraph(newPlaybackGraph); | |
break; | |
} | |
if (WaitForSingleObject(hNewPlaybackGraphEvent, 30000) != WAIT_OBJECT_0) | |
{ | |
printf("ERROR - MusicPlayer - Timed out buffering MP3 (>30 seconds)"); | |
newPlaybackGraph.controlInterface->Stop(); | |
releasePlaybackGraph(newPlaybackGraph); | |
break; | |
} | |
// Start fading of required. | |
// This implementation will block this thread until the fade is complete. An alternate approach would be to use | |
// timers or another thread with a callback if this blocking is a problem | |
if (shouldCrossfade) | |
{ | |
long fadeUpdateIntervalInMS = 10; | |
long incomingVolume = -10000, incomingTargetVolume = 10000 - ((long)audioPlayer->getVolume() * 10000); | |
long incomingVolumeIncrement = (incomingTargetVolume -incomingVolume) / (fadeTimeInMS / fadeUpdateIntervalInMS); | |
long outgoingVolume = -10000, outgoingTargetVolume = -10000, outgoingVolumeIncrement = 0; | |
bool haveOutgoingGraph = (audioPlayer->getCurrentPlaybackgraph().audioInterface != NULL); | |
if(haveOutgoingGraph) | |
{ | |
audioPlayer->getCurrentPlaybackgraph().audioInterface->get_Volume(&outgoingVolume); | |
outgoingVolumeIncrement = (outgoingTargetVolume -outgoingVolume) / (fadeTimeInMS / fadeUpdateIntervalInMS); | |
} | |
while (incomingVolume < incomingTargetVolume) | |
{ | |
incomingVolume += incomingVolumeIncrement; | |
newPlaybackGraph.audioInterface->put_Volume(incomingVolume); | |
if(haveOutgoingGraph) | |
{ | |
outgoingVolume += outgoingVolumeIncrement; | |
audioPlayer->getCurrentPlaybackgraph().audioInterface->put_Volume(outgoingVolume); | |
} | |
Sleep(fadeUpdateIntervalInMS); | |
} | |
} | |
// Set the new graph as the audio player instance's current graph, the player instance will stop and release any old graph. | |
audioPlayer->setCurrentPlaybackGraph(newPlaybackGraph); | |
// Set the graph event handle so that the event loop will trigger on the new graph's events | |
hPlaybackGraphEvent = hNewPlaybackGraphEvent; | |
break; | |
} | |
case 1: // stop playback event | |
{ | |
audioPlayer->getCurrentPlaybackgraph().controlInterface->Stop(); | |
} | |
case 2: // playback graph event | |
{ | |
// Get the event details from the graph | |
PlaybackGraph playbackGraph = audioPlayer->getCurrentPlaybackgraph(); | |
long evCode, param1, param2; | |
while (S_OK == playbackGraph.eventInterface->GetEvent(&evCode, ¶m1, ¶m2, 0)) | |
{ | |
// check for a condition that indicates playback has ended | |
if ((evCode == EC_COMPLETE) || | |
(evCode == EC_ERRORABORT) || | |
(evCode == EC_ERRORABORTEX) || | |
(evCode == EC_FILE_CLOSED) || | |
(evCode == EC_NEED_RESTART) || | |
(evCode == EC_SNDDEV_OUT_ERROR) || | |
(evCode == EC_STARVATION) || | |
(evCode == EC_USERABORT)) | |
{ | |
// release the event parameters here, as we don't use them and the processing of the notification might release the playback graph | |
playbackGraph.eventInterface->FreeEventParams(evCode, param1, param2); | |
// can post a notification that playback has ended, via a callback or some such method here | |
break; | |
} | |
else | |
{ | |
playbackGraph.eventInterface->FreeEventParams(evCode, param1, param2); | |
} | |
} | |
break; | |
} | |
default: | |
break; | |
} | |
} | |
// Uninitialize the COM library. | |
CoUninitialize(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment