Muziek terwijl u werkt!
Als ik zit te programmeren, heb ik altijd muziek aan staan en ik luister muziek via de computer. Waarschijnlijk doen jullie het niet anders. Om je mp3-tjes af te spelen zijn er vele spelers beschikbaar en allemaal hebben ze eigenschappen en mogelijkheden die handig zijn, maar er is er niet een die precies doet wat ik wil. Dus gaan we gaan de ideale mp3-speler zelf maken.
Eigenschappen
Voor we gaan bouwen, definiëren we de basiseisen waaraan onze speler moet voldoen. Omdat ik voornamelijk met Visual Studio werk, wil ik dat de speler als een Tool Window in Visual Studio beschikbaar is. Dan hoef ik niet van applicatie te wisselen, als ik een ander nummer wil horen. Natuurlijk zijn de standaard knoppen, i.e. Play, Stop, Pause, Previous en Next, benodigd en er moet informatie zichtbaar zijn over het nummer dat speelt.
“Ik wil dat de speler als een Tool Window in Visual Studio beschikbaar is”
Tool Window
De eerste eigenschap, de speler als Tool Window in Visual Studio, is eenvoudig te realiseren. Via www.nuFaqtz.com kun je de installer downloaden voor nuFaqtz Dynamic Window - de light versie is gratis -, wat het creëren van een Tool Window in Visual Studio vereenvoudigt.
Als je nuFaqtz Dynamic Window geïnstalleerd hebt, start je Visual Studio en kies voor ‘File/New/Project/Visual C# Projects’. Daar vind je de ‘ToolWindow Control’ wizard. Als naam voor het project kiezen we ‘nuFrameWork.nuPlayer’, en we laten de locatie op default staan. Klik op ‘OK’ om de wizard te starten.
Allereerst verschijnt een algemene introductie en als je op ‘Next’ klikt kunnen de eerste opties voor het Tool Window worden gezet. Alle opties zijn later eenvoudig in code aan te passen, dus als je snel klaar wilt zijn, klik je meteen op ‘Finish’ waarbij alle default settings worden gebruikt.

Fig. 1: Tool Window Wizard – step 1
Geef als eerste optie de naam van het project op. We gebruiken het GUI-achtervoegsel, omdat we in dit project de ‘look & feel’ definiëren. De logica van de mp3-afhandeling stoppen we in een ander project. De ‘Name for your ToolWindow’ bepaalt de naam van de control en de naam die zichtbaar is in de header van het uiteindelijke Tool Window.

Fig. 2: Tool Window Wizard – step 2
Op de volgende wizard pagina worden de ‘Behavior settings’ gezet voor het Tool Window. De eerste optie zetten we op false, want multiple instances van een mp3 speler is een beetje onzin.
Op de volgende wizard pagina kunnen we wat algemene informatie kwijt over het Tool Window, i.e. Company name, Namespace, Version en eventueel gedetailleerde informatie (de informatie die in de AssemblyInfo.cs komt te staan).
Op de laatste wizard pagina kunnen we aangeven of het Tool Window de IDE van Visual Studio wil benaderen (om bijvoorbeeld code te manipuleren of projecten te genereren). Onze speler heeft dat niet nodig, dus laten we deze optie op de default (false) staan.
De laatste optie die we kunnen zetten is of het gegeneerde project gebuild moet worden. Default staat die optie aangevinkt en als we op ‘Finish’ klikken, wordt de solution gegenereerd (en dus ook gebuild) en als het goed is eindigen we met een build error free solution en Visual Studio laat in de designer een nieuw, leeg Tool Window zien. Nu kunnen we dus de speler daadwerkelijk gaan bouwen.
Speler
Voordat we daadwerkelijk mp3-files kunnen afspelen, moeten we ze eerst ‘localiseren’. Omdat de locatie van de mp3-bestanden meestal niet wijzigt, verbergen we het zoeken van de files achter een button in een apart form. Creëer een form, ‘GetMp3dialog’ en plaats een viertal buttons en een listview op het form. Omdat we ook willen weten wat er gebeurt, voegen we nog een ‘Processbar’ toe. Deze ‘Processbar’ – eigenlijk een dubbele progressbar - kun je vinden in de nuFrameWork.Controls.dll, welke geïnstalleerd is samen met het Dynamic Window. Ach, je weet wel hoe dat gaat …voeg toe aan de Toolbox en sleep het control op het form. Omdat het GUI natuurlijk wel moet blijven reageren op onze acties, maken we gebruik van een tweetal threads om de Progressbar te updaten.

Fig. 3: Instellen van de mp3-folders
De ‘Add’ button maakt het mogelijk folders via een standaard FolderbrowserDialog te selecteren en toe te voegen. De ‘Remove’ button geeft ons de mogelijkehied geselecteerde folders uit de ListView te verwijderen. De ‘Search’ button doet het echte werk. Deze gaat recursief door de geselecteerde folders en voegt alle gevonden mp3-bestanden toe aan een arraylist. (Zie de nuPlayer solution voor de implementatie van de recursieve functie(s) en het gebruik van delegates om de Processbar te updaten).
Het form bevat drie public ArrayLists (Mp3Folders en Mp3Files en SelectedFolders), zodat we deze kunnen benaderen vanuit het Tool Window. De ‘Done’ button sluit de Dialog en geeft altijd DialogResult.OK. We bepalen in nuPlayer.GUI wel met welke arraylist we willen werken.
Omdat het GUI natuurlijk wel moet blijven reageren op onze acties, maken we gebruik van een tweetal threads om de Progressbar te updaten
Als de dialog wordt gesloten, moeten we de mp3-bestanden nog zichtbaar maken in het Tool Window. ArrayLists en Listviews werken goed samen, dus voor onze playlist voegen we een ListView toe aan de speler welke we vullen met de data uit de arraylist.
if(dlgGetMP3.Mp3Files.Count > 0)
{
this.PlayList.Items.Clear();
for (int j=0; j < dlgGetMP3.Mp3Files.Count; j++)
{
string filename =
Path.GetFileNameWithoutExtension(
dlgGetMP3.Mp3Files[j].ToString());
string[] subitems = {filename,
dlgGetMP3.Mp3Files[j].ToString()};
ListViewItem listViewItem =
new ListViewItem(subitems);
PlayList.Items.Add(listViewItem);
}
}
We voegen de fysieke locatie van het mp3-bestand ook toe aan de listview (in een hidden column), zodat we altijd een referentie hebben naar het mp3-bestand.
Afspelen
Omdat we nu met de logica van de player aan de slag gaan,voegen we een nieuw project toe aan de solution: nuFrameWork.nuPlayer. Creëer een project reference vanuit het GUI project naar het nuFrameWork.nuPlayer project en rename de default gecreëerde class PlayerLogic.

Fig. 4: Toevoegen van de Activemovie control type library
Om mp3-bestanden af te spelen gaan we gebruik maken van een COM-component die je kunt vinden in Quartz.dll – een component die bij Windows XP geleverd wordt als onderdeel van DirectShow. Voeg een reference aan het project toe via de COM-tab in de ‘Add reference’ dialog. Zoek dan naar ‘Activemovie control type library’ (Quartz.dll).
Visual Studio maakt er automatisch een interop assembly van.
De dll bevat één class en een aantal interfaces. Het belangrijkste interface is FilGraphManager. FilGraphManagerClass is een concrete implementatie van het interface waarbij we gebruik gaan maken van de interfaces IMediaControl, IMediaPosition en IBasicAudio die benodigd zijn voor het afspelen van audio-bestanden. De overige interfaces zijn bedoeld voor het afspelen van video-bestanden.
Zoek dan naar ‘Activemovie control type library’
IMediaControl:
- void RenderFile(string filename):
Opent het gespecificeerde bestand
- void Run():
Start het afspelen vanaf de huidige positie
- void Stop(): Stopt het afspelen
- void Pause(): Pauzeert het afspelen
- void GetState(int msTimeout, out int status):
Geeft de huidige status van de media control:
msTimeout specificeert hoeveel milliseconden gewacht
moet worden voordat de control klaar is met een
state wijziging;
status geeft de huidige status van de control:
0 stop, 1 pause, en 2 play
IMediaPosition:
- double Duration { get; }:
Lengte van het bestand in seconden
- double CurrentPosition { get; set; }:
Huidige positie in het bestand in seconden
IBasicAudio:
- int Volume { get; set; }:
Getter/Setter voor volume van de control
Waarde ligt tussen -10000 (stil) en 0 (maximaal)
1 Unit correspondeert met 0,01 decibel
- long Balance { get; set; }:
Getter/Setter voor stereobeeld van de control
Waarde ligt tussen -10000 (links) en 10000 (rechts)
Standaard waarde is 0
We implementeren de drie methods benodigd voor afhandeling van mp3’s, i.e. Play, Stop en Pause. De Next en Previous methods hebben betrekking op de selectie van een ander bestand uit de playlist wat alleen beschikbaar is in de GUI. Deze methods implementeren we in het GUI class.
///
/// Handle play
///
/// The file to play
public void Play(string filename)
{
try
{
// Check if a file is allready playing or paused
if(PlayerStatus == "Playing" ||
PlayerStatus == "Paused")
{
// Stop the current song
this.Stop();
}
// Set the audio manager
filgraphmanager =
new QuartzTypeLib.FilgraphManager();
// Create a new mediacontrol
mediacontrol = filgraphmanager as IMediaControl;
// Load the mp3 file
mediacontrol.RenderFile(filename);
// Create the mediaposition for this file
mediaposition = mediacontrol as IMediaPosition;
// Create the audiocontrol for this file
audiocontrol = mediacontrol as IBasicAudio;
// Set the Volume
audiocontrol.Volume = PlayerVolume;
}
catch(Exception ex)
{
// Render errors....
throw new Exception(string.Format(
"An error occured while processing {0}." +
Environment.NewLine + Environment.NewLine +
"Message:{1}", filename, ex.Message));
}
// Everything went fine...Play it
mediacontrol.Run();
// Set the status
PlayerStatus = "Playing";
}
///
/// Handle stop
///
public void Stop()
{
if(mediacontrol != null)
{
mediacontrol.Stop();
PlayerStatus = "Stopped";
// Kill all the media objects to facilitate
// playing a new mp3 file
this.filgraphmanager = null;
this.mediacontrol = null;
this.audiocontrol = null;
this.mediaposition = null;
}
}
///
/// Handle pause
///
public void Pause()
{
if(PlayerStatus == "Playing")
{
mediacontrol.Pause();
PlayerStatus = "Paused";
}
else if(PlayerStatus == "Paused")
{
mediacontrol.Run();
PlayerStatus = "Playing";
}
}
Omdat we via het GUI het volume willen instellen (gewoonlijk tussen 0 en 100), moeten we het volume voor de audiocontrol interface uitrekenen (de berekening is gebaseerd op het feit dat de decibel schaal die IBasicAudio gebruikt logaritmisch verloopt):
///
/// Getter / Setter for the audio volume
///
public int Volume
{
get
{
return _Volume;
}
set
{
// Only change the volume when value is acceptable
if (value >= 0 && value <= 100 && _Volume != value)
{
_Volume = value;
PlayerVolume = Convert.ToInt32(
-0.0004 * Math.Pow(_Volume, 4)
+0.1107 * Math.Pow(_Volume, 3)
-11.334 * _Volume * _Volume
+525.95 * _Volume
-10000);
}
}
}
We willen in onze player ook allerlei basisinformatie zien met betrekking tot de mp3. Daartoe moeten we een aantal public properties implementeren in de PlayerLogic class: de lengte van het nummer, de status en hoe lang het nummer al bezig is.
///
/// Getter for the Status of the player
///
public string Status
{
get { return PlayerStatus; }
}
///
/// Getter/setter for elapsed time of the mp3 playing
///
public double ElapsedTime
{
get
{
if(mediaposition != null)
{
return mediaposition.CurrentPosition;
}
else
{
return -1;
}
}
set
{
if(mediaposition != null)
{
mediaposition.CurrentPosition = value;
}
}
}
///
/// Getter for the total length of the file(in seconds)
///
public double Duration
{
get
{
if(mediaposition != null)
{
return mediaposition.Duration;
}
else
{
return -1;
}
}
}
Terug naar het GUI
Om in het GUI gebruik te kunnen maken van de PlayerLogic, creëren we een private member ‘playerlogic’ welke we eenmalig instantiëren in de constructor van het GUI.
Aan de constructor voegen we ook nog een Timer event toe:
// Create the playerlogic instance
playerlogic = new PlayerLogic();
// Create the timer
PlayTimer = new System.Timers.Timer(PlayTimerInterval);
// Set the Eventhandler
PlayTimer.Elapsed +=
new ElapsedEventHandler(PlayTimerEvent);
De bijbehorende eventhandler houdt de voortgang van de speler in de gaten. Als het huidige nummer is afgelopen, dan moet het volgende nummer in de lijst geselecteerd en afgespeeld worden. In de nuPlayer.GUI kunnen we nu de ‘Stop’, ‘Play’, ‘Next’, ‘Previous’ en ‘Pause’ buttons creëren. Creëer een aparte private method die het daadwerkelijk afspelen van een mp3 afhandelt. In de events (button- en muis-klik) stoppen we de logica om een nummer te selecteren.
///
/// Handle the play event for any method that starts
/// a (new) song.
///
private void PlaySong()
{
// Can we play a song?
if(CurrentSong != string.Empty)
{
playerlogic.Play(CurrentSong);
// Start the timer
PlayTimer.Start();
// Set the statusbar data for the duration
this.statusPlay.Panels[2].Text =
this.SecondsToTime(
Convert.ToInt32(playerlogic.Duration));
}
}
///
/// Play the mp3's
///
/// The source that raised
/// the event
/// The data for the event
private void btnPlay_Click(
object sender, System.EventArgs e)
{
// Check if we have items in the playlist
if(PlayList.Items.Count > 0)
{
// Check if an item is selected
if(PlayList.SelectedItems.Count > 0)
{
// Play first (index=0) song in selected list
CurrentSong =
PlayList.SelectedItems[0].SubItems[1].Text;
CurrentSongIndex =
PlayList.SelectedItems[0].Index;
}
else
{
// Just start playing first song in the list
CurrentSong = PlayList.Items[0].SubItems[1].Text;
CurrentSongIndex = 0;
}
}
else
{
// Get out of here...nothing to play
return;
}
// Play it..
this.PlaySong();
}
Het dubbel-klikken van een item in de playlist is redelijkerwijs te verwachten gedrag van de gebruiker, dus we implementeren ook een DoubleClick event:
///
/// The user doubleclicked on the listview to activate
/// a new song...
///
/// The source that raised
/// the event
/// The data for the event
private void PlayList_DoubleClick(
object sender, System.EventArgs e)
{
// We can use the first index of the selected items as
// double click on the listview also clears
// any selection
string songtoplay =
PlayList.SelectedItems[0].SubItems[1].Text;
// Only play the next song if its different
if(CurrentSong != songtoplay)
{
CurrentSong = songtoplay;
CurrentSongIndex = PlayList.SelectedItems[0].Index;
// Play it..
this.PlaySong();
}
}
///
/// Play the previous mp3 from the playlist
///
/// The source that raised
/// the event
/// The data for the event
private void btnPrevious_Click(
object sender, System.EventArgs e)
{
// Only play the previous song if we have one
if(CurrentSongIndex > 0)
{
CurrentSongIndex -= 1;
CurrentSong =
PlayList.Items[CurrentSongIndex].SubItems[1].Text;
// Play it..
this.PlaySong();
}
}
///
/// Play the next mp3 from the playlist
///
/// The source that raised
/// the event
/// The data for the event
private void btnNext_Click(
object sender, System.EventArgs e)
{
// Only play the next song if we have one
if(CurrentSongIndex < PlayList.Items.Count - 1)
{
CurrentSongIndex += 1;
CurrentSong =
PlayList.Items[CurrentSongIndex].SubItems[1].Text;
// Play it..
this.PlaySong();
}
}
Omdat we net als in de GetMp3Dialog de mogelijkheid willen hebben om mp3’s uit de playlist te verwijderen, creëren we een ‘Delete’ button. De code kopiëren we uit de code van de dialog:
///
/// Remove Items from the PlayList
///
/// The source that raised
/// the event
/// The data for the event
private void btnDelete_Click(
object sender, System.EventArgs e)
{
// Make sure a row is selected
if (this.PlayList.SelectedItems.Count > 0)
{
// Loop through the items in the listview
foreach(ListViewItem item in
this.PlayList.SelectedItems)
{
item.Remove();
}
}
}
Om de basisinformatie van de song weer te geven gebruiken we een statusbar die verdeeld is in drie panels. De eerste vullen we met de status, de tweede met de elapsed time en de derde met de totale tijd van het nummer. In de eventhandler van de timer realiseren we het updaten van de dynamische gegevens; de lengte van een bestand is een vast gegeven, die informatie updaten we bij het starten van een nieuw nummer.
///
/// Handles the timer event
///
/// The source that raised the event
/// The data for the event
private void PlayTimerEvent(object sender, ElapsedEventArgs e)
{
// Check the elapsedtime against the duration to know when to play
// the next song
if(playerlogic.ElapsedTime >= playerlogic.Duration )
{
// Stop the timer to prevent multiple next songs.
// The timer is started again when the new song starts playing
PlayTimer.Stop();
// Play the next song
this.btnNext_Click(null, null);
}
// Set the statusbar data
statusPlay.Panels[0].Text = playerlogic.Status;
statusPlay.Panels[1].Text = _
this.SecondsToTime(Convert.ToInt32(playerlogic.ElapsedTime));
}
Omdat zowel de ‘Duration’ als de ‘ElapsedTime’ doubles zijn (seconden), moeten die geconverteerd worden naar een DateTime om die via een string-format te kunnen weergeven zoals wij dat willen:
///
/// Returns a nice string representation for the amount of seconds
///
/// The integer value in seconds
/// A string in the format hh:mm:ss
private string SecondsToTime(int sec)
{
// Less than one second...
if(sec < 1)
{
return "00:00:00";
}
// Calculate seconds, minutes, hours...
int seconds=sec-Convert.ToInt32(sec/60)*60;
int minutes=Convert.ToInt32((sec-Convert.ToInt32(sec/3600)*3600)/60);
int hours=Convert.ToInt32(sec/3600);
// More than a day...
if(hours > 24)
{
return "24:59:59";
}
else
{
DateTime time = new DateTime(2005, 9, 29, hours, minutes, seconds);
return time.ToString("T", DateTimeFormatInfo.InvariantInfo);
}
}
Conclusie
Al met al hebben we nu een eenvoudige mp3-speler gerealiseerd die in staat is je mp3-tjes te selecteren en af te spelen binnen Visual Studio. Daarmee is het natuurlijk nog lang niet de ideale mp3-speler: in een mp3-bestand is nog allerlei informatie opgeslagen (ID3-tags) die we zichtbaar kunnen maken, in de GetMp3Dialog kunnen we het beheer van playlists nog inbouwen, etc. Een interessante functionaliteit die nogal wat extra aandacht zal vergen is bijvoorbeeld een shuffle functie.

Fig. 5: En spelen maar …
De meeste mp3-spelers bepalen hun playlists op basis van de ID3-tags. Een belangrijk nadeel daarvan is de enorme aanslag op de performance: om die informatie te verkrijgen moet elk bestand apart worden geopend. Ik ben ervan uit gegaan dat jullie, net als ik, de bestanden een logische naam geven. Het voordeel daarvan is dat je selecties kunt maken op basis van het file-systeem. Dat is vele malen sneller, zoals je in de afbeelding van de GetMp3Dialog kunt zien: ruim 42000 mp3’s in een krappe 15 minuten is lang niet slecht.
Jullie ‘geluk’ is dat ik dit artikel natuurlijk eerder heb geschreven dan dat het verschijnt. De solution die je kunt downloaden op www.nufaqtz.com zal veel meer functionaliteit bevatten dan ik in dit artikel kwijt kan. Daar vind je ook alle informatie over Dynamic Window en het nuFrameWork, die je natuurlijk ook kan gebruiken voor andere Tool Windows dan de hier besproken mp3-speler.
Happy programming!