Click here to Skip to main content
15,886,799 members
Articles / Mobile Apps / Windows Mobile

Audio Book Player

Rate me:
Please Sign up or sign in to vote.
4.86/5 (36 votes)
11 Jun 2009CPOL6 min read 197.6K   3.5K   84  
Audio player designed specifically for listening to audio books
using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Configuration;
using Microsoft.Win32;
using System.Collections;
using AxWMPLib;
using System.IO;
using System.Threading;

public class cPlayer : cBasePlayer
{
    // tag name constants used
    private String[] sTagNames = {"album",
                                  "title",
                                  "author",
                                  "comment",
                                  "filename"};
    private String[] sPlayerStatus = {
                                "UNDEFINED",
                                "STOPPED",
                                "PAUSED",
                                "PLAYING",
                                "SCAN-FORWARD",
                                "SCAN-REVERSE",
                                "BUFFERING",
                                "WAITING",
                                "MEDIA-ENDED",
                                "TRANSITIONING",
                                "READY",
                                "RECONNECTING"};
    // the event handler place holder
    public event PlayItemChangedEventHandler PlayItemChanged = null;
    public event StatusChangedEventHandler StatusChanged = null;
    // windows media player object
    private AxWindowsMediaPlayer WMP = null;
    // internal playlist holder
    private ArrayList pPlaylistFiles = null;
    // files duration population objects
    private ArrayList pDurations = null;
    // total duration thread
    private Thread tMP3 = null;
    // player status
    private ePlayerStatus pStatus = ePlayerStatus.STOPPED;
    // flag indicating playlist is beeing loaded to the player
    // at this time intermediate status change events are suppressed
    // and any attempt to manipulate media (play/pause/stop/next/previous)
    // is ignored
    private bool LoadingPlaylist = false;

    public cPlayer() 
    {
        // windows media player object
        WMP = new AxWindowsMediaPlayer();
        // internal playlist holder
        pPlaylistFiles = new ArrayList();
        pDurations = new ArrayList();
        // initial player settings
        WMP.uiMode = "invisible";
        WMP.enableContextMenu = false;
        WMP.settings.autoStart = false;
        // windows media player volume control affects the media player
        // only. we prefer to control volume via the wave out master volume
        // we set the player internal volume to max.
        WMP.settings.volume = 100;
        // create a dummy playlist - this will be managed and manipulated
        // when user loads/plays playlists
        WMP.newPlaylist("dummy_name", "dummy_file.mp3");
        // add internal event handlers
        WMP.PlayStateChange += new _WMPOCXEvents_PlayStateChangeEventHandler(wmp_PlayStateChange);
        WMP.NewStream += new EventHandler(WMP_NewStream);
        WMP.CurrentItemChange += new _WMPOCXEvents_CurrentItemChangeEventHandler(WMP_CurrentItemChange);
    }
    // pseudo destructor
    public override void Dispose()
    {
        try { WMP.Dispose(); }
        catch { }
    }
    // get the file name of the indexed
    // media file path
    public override String PlaylistFile(int i)
    {
        return ((i >= pPlaylistFiles.Count) ||(i < 0)) ?
            "" : (String)pPlaylistFiles[i];
    }
    // the player item changed event is passed on with the index
    // of the media in the playlist
    void WMP_NewStream(object sender, EventArgs e)
    {
        SignalNewMediaEvent();
    }
    void WMP_CurrentItemChange(object sender, _WMPOCXEvents_CurrentItemChangeEvent e)
    {
        SignalNewMediaEvent();
    }
    public override int FileDuration(int i)
    {
        if((i >= pPlaylistFiles.Count) ||
            (i < 0) ||
            (Duration == -1) || 
            (pDurations.Count - pPlaylistFiles.Count != 1))
            return -1;
        return (int)pDurations[i + 1] - (int)pDurations[i];
    }
    public override int TotalDuration
    {
        get 
        {
            return (Duration == -1)? 0:
                (pDurations.Count - pPlaylistFiles.Count == 1) ?
                (int)pDurations[pPlaylistFiles.Count]: 
                0;
        }
    }
    public override String TotalDurationString
    { 
        get { return TrimTimeString(TimeToString(TotalDuration)); } 
    }
    public override int TotalDurationSoFar
    {
        get
        {
            if ((Location == -1) ||
                (pDurations.Count - pPlaylistFiles.Count != 1))
                return 0;
            return (int)pDurations[PlayingIndex] + Location;
        }
    }
    public override String TotalDurationSoFarString
    {
        get { return TrimTimeString(TimeToString(TotalDurationSoFar)); }
    }
    public override int TotalDurationRemain
    {
        get
        {
            if ((Location == -1) ||
                (pDurations.Count - pPlaylistFiles.Count != 1))
                return 0;
            return TotalDuration - TotalDurationSoFar;
       }
    }
    public override String TotalDurationRemainString
    {
        get { return TrimTimeString(TimeToString(TotalDurationRemain)); }
    }
    private void SignalNewMediaEvent()
    {
        if ((PlayItemChanged != null) &&
            (!LoadingPlaylist))
        {
            PlayItemChangedEventArgs ea = new PlayItemChangedEventArgs(PlayingIndex);
            PlayItemChanged(this, ea);
        }
    }
    // the player state chnanged is passed on with the new state 
    // (alphabetized and capitalized)
    private void wmp_PlayStateChange(object sender, _WMPOCXEvents_PlayStateChangeEvent e)
    {
        pStatus = (ePlayerStatus)e.newState;
        if ((StatusChanged != null) &&
            (!LoadingPlaylist))
        {
            StatusChangedEventArgs ea = new StatusChangedEventArgs(pStatus, sPlayerStatus[(int)pStatus]);
            StatusChanged(this, ea);
        }
    }
    // playlist count is a get only property
    public override int PlaylistCount
    {
        get { return pPlaylistFiles.Count; }
    }
    // playing item path is a get only property derived from the 
    // sourceurl property of the media being played
    public override String PlayingPath
    {
        get
        {
            try
            {
                return WMP.currentMedia.sourceURL;
            }
            catch { return ""; }
        }
    }
    // plying index is a get property derived at by using indexof
    // of the file paths
    public override int PlayingIndex
    {
        get 
        {
            try
            {
                return pPlaylistFiles.IndexOf(PlayingPath);
            }
            catch { return -1; }
        }
        set
        {
            if ((value >= pPlaylistFiles.Count) ||
                (value < 0))
                return;
            try
            {
                WMP.Ctlcontrols.playItem(WMP.currentPlaylist.get_Item(value));
                WaitForStatusWithTimeout(ePlayerStatus.PLAYING, 2); 
                // pause again
                WMP.Ctlcontrols.pause();
            }
            catch { }
        }
    }
    // playing name is a get only property. file name is extracted from
    // the playingpath property
    public override String PlayingName
    {
        get
        {
            return new FileInfo(PlayingPath).Name;
        }
    }
    // delete a playlist entry
    public override int RemoveAt(int ix)
    {
        if ((ix < 0) || (ix > (pPlaylistFiles.Count - 1))) return ix;
        TerminateDurationThread();
        pDurations.Clear();
        pPlaylistFiles.RemoveAt(ix);
        int nix = (ix > (pPlaylistFiles.Count - 1)) ? ix - 1 : ix;
        try { Playlist((String[])pPlaylistFiles.ToArray(typeof(String)), nix, 0, Shuffle); }
        catch { }
        return nix;
    }
    // status is a get only property. state returned is the last reported
    // in the statechanged event
    public override ePlayerStatus Status
    {
        get { return pStatus; }
    }
    public override String StatusString
    {
        get { return sPlayerStatus[(int)pStatus]; }
    }
    // set/reset the shuffle attribute in the player.
    public override bool Shuffle
    {
        get
        {
            try { return WMP.settings.getMode("shuffle"); }
            catch { return false; }
        }
        set 
        { 
            try { WMP.settings.setMode("shuffle", value); }  
            catch{}
        }
    }
    // duration is a get only property (returns the currently playing
    // media duration
    public override int Duration
    {
        get
        {
            try { return (int)WMP.currentMedia.duration; }
            catch { return -1; }
        }
    }
    // same as duration but converted to a string
    public override String DurationString
    {
        get
        {
            return (Duration == -1) ? "No Media" :
            TrimTimeString(TimeToString(Duration));
        }
    }
    // location is a get/set property of the currently playing media
    // timeline
    public override int Location
    {
        get
        {
            try { return (int)WMP.Ctlcontrols.currentPosition; }
            catch { return -1; }
        }
        set
        {
            try { WMP.Ctlcontrols.currentPosition = value; }
            catch { }
        }
    }
    // same as duration but as string. obviously only a get property
    public override String LocationString
    {
        get
        {
            return (Location == -1) ? "No Media" :
            TrimTimeString(TimeToString(Location));
        }
    }
    // get ID3 media tag.
    // some tags can be accessed using getiteminfo (using the standard
    // tag names) others (which have no standard names) have indexers and
    // can be accessed using getiteminfobyatom
    public override String GetMediaTAG(eTagNames tag)
    {
        try
        {
            switch (tag)
            {
                case eTagNames.ALBUM:
                case eTagNames.TITLE:
                case eTagNames.AUTHOR:
                    return WMP.currentMedia.getItemInfo(sTagNames[(int)tag]);
                case eTagNames.COMMENT:
                    return WMP.currentMedia.getItemInfoByAtom(24);
                case eTagNames.FILE_NAME:
                    return WMP.currentMedia.getItemInfoByAtom(2);
                default:
                    return "";
            }
        }
        catch { return ""; }
    }
    // as described above - player actions are suppressed while loading a 
    // a playlist
    // call player 'play' function
    public override bool Play()
    {
        if (LoadingPlaylist) return false;
        try { WMP.Ctlcontrols.play(); }
        catch { return false; }
        return true;
    }
    // call 'pause'
    public override bool Pause()
    {
        if (LoadingPlaylist) return false;
        try { WMP.Ctlcontrols.pause(); }
        catch { return false; }
        return true;
    }
    // call 'next'
    public override bool Next()
    {
        if (LoadingPlaylist) return false;
        try { WMP.Ctlcontrols.next(); }
        catch { return false; }
        return true;
    }
    // call 'previous'
    public override bool Previous()
    {
        if (LoadingPlaylist) return false;
        try { WMP.Ctlcontrols.previous(); }
        catch { return false; }
        return true;
    }
    // call 'stop'
    public override bool Stop()
    {
        if (LoadingPlaylist) return false;
        try { WMP.Ctlcontrols.stop(); }
        catch { return false; }
        return true;
    }
    // stop playing and clear playlist
    public override bool StopAndClear()
    {
        // clear internal playlist
        TerminateDurationThread();
        pPlaylistFiles.Clear();
        pDurations.Clear();
        try
        { 
            // try to stop
            if (pStatus == ePlayerStatus.PLAYING)
            {
                WMP.Ctlcontrols.stop();
                WaitForStatusWithTimeout(ePlayerStatus.STOPPED, 2);
            }
        }
        catch {}
        // clear the media's playlist
        try {WMP.currentPlaylist.clear();}
        catch { return false; }
        return true;
    }
    // loading a playlist (and also setting various properties) is a 
    // multi step procedure. when each step is done the player status
    // changes to reflect it.
    // on ocations the procedure fails (beats me), moreover, the length of time 
    // it takes to complete each step varies. 
    // we use a function called 'WaitForStatusWithTimeout' to wait  for
    // the completion of each step. i use 2 seconds as the max wait time 
    // before an exception is raised. if we fail - we stop and clear the
    // playlist so we can just start over.
    public override bool Playlist(String[] files, int Entry, int Location, 
        bool Shuffle)
    {
        // flag indicating player actions should be ignored as well as
        // intermedia status change events
        LoadingPlaylist = true;
        try
        {
            TerminateDurationThread();
            // we might activate a previously loaded playlist or load a new
            if (files != null)
            {
                // add files to internal play list
                pPlaylistFiles.Clear();
                pPlaylistFiles.AddRange(files);
                // clear any previously loaded playlists
                WMP.currentPlaylist.clear();
                foreach (String file in files)
                {
                    // for each file in the playlist - 
                    // create a media object and load into player
                    WMP.currentPlaylist.appendItem((WMPLib.IWMPMedia)WMP.newMedia(file));
                }
                // we force garbage collection here to dispose of the media
                // objects.
                // if we let garbage collection to work automatically it 
                // would kick in when the player is playing and since it might
                // have a large number of objects to dispose, the device (and
                // the playing) freezes for a few seconds.
                GC.Collect();
            }
            // start playing - this would initialize various properties
            // in the player that we need access to.
            // (lame !!) - but i couldn't find any other way to do it
            if (pPlaylistFiles.Count > 0)
            {
                WMP.Ctlcontrols.play();
                // wait for it to achieve play state and immediately pause
                WaitForStatusWithTimeout(ePlayerStatus.PLAYING, 2);
                WMP.Ctlcontrols.pause();
                WaitForStatusWithTimeout(ePlayerStatus.PAUSED, 2);
                // load shuffle attribute
                WMP.settings.setMode("shuffle", Shuffle);
                WaitForStatusWithTimeout(ePlayerStatus.PAUSED, 2);
                // set the playing item desired
                WMP.Ctlcontrols.playItem(WMP.currentPlaylist.get_Item(Entry));
                // wait for player to go there
                WaitForStatusWithTimeout(ePlayerStatus.PLAYING, 2);
                // pause again
                WMP.Ctlcontrols.pause();
                WaitForStatusWithTimeout(ePlayerStatus.PAUSED, 2);
                // change timeline to desired location
                WMP.Ctlcontrols.currentPosition = Location;
            }
            wmp_PlayStateChange(this, new _WMPOCXEvents_PlayStateChangeEvent((int)ePlayerStatus.PAUSED));

            tMP3 = new Thread(new ThreadStart(GetTotalsDurationsThreadFunction));
            tMP3.IsBackground = true;
            tMP3.Priority = ThreadPriority.BelowNormal;
            tMP3.Start();
        }
        catch (Exception e)
        {
            StopAndClear();
            MessageBox.Show(e.Message);
            return false;
        }
        finally
        {
            LoadingPlaylist = false;
        }
        return true;
    }
    public override bool WaitForPlayerStatus(ePlayerStatus s)
    {
        try
        {
            WaitForStatusWithTimeout(s, 2);
        }
        catch { return false; }
        return true;
    }
    // as described above this function waits a maximum alloted time 
    // for a player action to take place
    private void WaitForStatusWithTimeout(ePlayerStatus value, int seconds)
    {
        int start = (Environment.TickCount & Int32.MaxValue);
        while (pStatus != value)
        {
            if (((Environment.TickCount & Int32.MaxValue) - start) >= seconds * 1000)
                throw new Exception("Timed Out"); 
            Application.DoEvents();
        }
    }
    // function to convert time to string
    public override String TimeToString(int time)
    {
        if(time < 0) return "00:00:00";
        int s = time % 60;
        int tmp = (time - s) / 60;
        int m = tmp % 60;
        int h = (tmp - m) / 60;
        String str = ((h < 10) ? "0" + h.ToString() + ":" : h.ToString() + ":") +
                     ((m < 10) ? "0" + m.ToString() + ":" : m.ToString() + ":") +
                     ((s < 10) ? "0" + s.ToString() : s.ToString());
        return str;
    }

    public override String TrimTimeString(String str)
    {
        return (str[0] == '0') ?
            ((str.Substring(0, 3) == "00:") ? str.Substring(3) : str.Substring(1)) : str;
    }

    public override void GetTotalsDurationsThreadFunction()
    {
        try
        {
            pDurations.Clear();
            int total = 0;
            for (int n = 0; n < pPlaylistFiles.Count; n++)
            {
                pDurations.Add(total);
                total += new cMP3Info((String)pPlaylistFiles[n]).Duration;
            }
            pDurations.Add(total);
        }
        catch 
        {
            pDurations.Clear();
        }
    }

    private void TerminateDurationThread()
    {
        if (tMP3 != null) 
            tMP3.Abort();
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Israel Israel
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions