Skip to content

Instantly share code, notes, and snippets.

@jimmcgowan
Last active December 20, 2015 15:47
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jimmcgowan/be8a52f2b03772811216 to your computer and use it in GitHub Desktop.
Save jimmcgowan/be8a52f2b03772811216 to your computer and use it in GitHub Desktop.
//
// 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