/// <summary>Loads the specified <see cref="MDBPlayListItem"/>.</summary>
        /// <param name="mdb">The <see cref="MusicDataBase"/> instance.</param>
        /// <param name="item">The <see cref="MDBPlayListItem"/>.</param>
        /// <returns></returns>
        public static MDBFileSelection Load(MusicDataBase mdb, MDBPlayListItem item)
        {
            var result = new MDBFileSelection();

            result.AudioFile    = mdb.AudioFiles.TryGetStruct(item.AudioFileID);
            result.File         = mdb.Files.TryGetStruct(result.AudioFile.FileID);
            result.NowPlaying   = MDBNowPlaying.Create(mdb, item.StreamID, item.OwnerID, item.SubsetID, DateTime.MinValue, result.AudioFile);
            result.PlayListItem = item;
            return(result);
        }
        private void PlayFile(MDBFileSelection selection)
        {
            var silenceCompression = mdb.Config.ReadBool("Player", "SilenceCompression", false);

            IAudioDecoder decoder = new Mpg123(false);

            if (!decoder.IsAvailable)
            {
                decoder = new MP3AudioDecoder();
            }
            this.LogInfo("Prepare playing {0} using audio decoder <cyan>{1}<default> silence compression {2}", selection.AudioFile, decoder, silenceCompression);

            string fileName = selection.File.GetFullPath(mdb);

            this.LogDebug("Open file {0}", fileName);
            var mp3FileStream = ResistantFileStream.OpenSequentialRead(fileName);

            decoder.BeginDecode(mp3FileStream);
            currentNowPlaying = MDBNowPlaying.Create(mdb, selection.PlayListItem.StreamID, selection.PlayListItem.OwnerID, selection.PlayListItem.SubsetID, DateTime.MinValue, selection.AudioFile);

            IAudioData audioData = decoder.Decode();

            if (device == null)
            {
                device = SelectDevice() ?? throw new Exception("Could not find any available audio device implementation!");
            }
            var audioOut = device.CreateAudioOut(audioData);

            this.LogInfo("Start buffering {0} <cyan>{1}", selection.AudioFile, audioData);

            TimeSpan inSilenceTime    = TimeSpan.Zero;
            TimeSpan fileReadPosition = TimeSpan.Zero;

            skip = false;
            bool started   = false;
            long underflow = 0;

            while (!exit && !skip)
            {
                //buffer until we got at least one second, or ten during playback
                var sleepTime = started ? audioOut.TimeBuffered - TenSeconds : audioOut.TimeBuffered - OneSecond;
                //buffer filled ?
                if (sleepTime > TimeSpan.Zero)
                {
                    if (CurrentStreamSettings.StreamType != MDBStreamType.JukeBob)
                    {
                        skip = true;
                    }
                    if (audioOut.Volume != Math.Max(0, CurrentStreamSettings.Volume))
                    {
                        audioOut.Volume = CurrentStreamSettings.Volume;
                    }

                    //already started ?
                    if (started)
                    {
                        //yes, check for a gap/buffer underrun ?
                        if (audioOut.BufferUnderflowCount != underflow)
                        {
                            //we got a gap, fix starttime
                            underflow = audioOut.BufferUnderflowCount;
                            this.LogWarning("Player GAP {0}, Buffer was empty!", underflow);
                            currentNowPlaying.StartDateTime = DateTime.UtcNow - fileReadPosition + audioOut.TimeBuffered;
                            ThreadPool.QueueUserWorkItem(delegate { mdb.NowPlaying.Replace(currentNowPlaying); });
                        }
                        else
                        {
                            Thread.Sleep(Math.Min(1000, (int)sleepTime.TotalMilliseconds));
                        }
                    }
                    else
                    {
                        //do start if we are allowed to (check playing previous title)
                        sleepTime = nextStart - DateTime.UtcNow;
                        if (sleepTime > TimeSpan.Zero)
                        {
                            this.LogVerbose("Sleep {0}", sleepTime.FormatTime());
                            Thread.Sleep(sleepTime);
                        }

                        currentNowPlaying.StartDateTime = DateTime.UtcNow;
                        audioOut.Start();
                        started = true;
                        this.LogInfo("Start playing {0}", selection.AudioFile);
                        //write to now playing
                        ThreadPool.QueueUserWorkItem(delegate
                        {
                            mdb.NowPlaying.Replace(currentNowPlaying);
                            mdb.PlayListItems.TryDelete(nameof(MDBPlayListItem.ID), selection.PlayListItem.ID);
                        });
                    }
                }

                audioData = decoder.Decode();
                //end of file ?
                if (audioData == null)
                {
                    break;
                }
                //add packet duration to file position
                fileReadPosition += audioData.Duration;
                //skip silence
                if (silenceCompression)
                {
                    if (audioData.Peak < 0.001f)
                    {
                        if (inSilenceTime > OneSecond)
                        {
                            continue;
                        }
                        inSilenceTime += audioData.Duration;
                    }
                    else
                    {
                        if (inSilenceTime > OneSecond)
                        {
                            //we skipped some silence, fix starttime
                            this.LogDebug("Silence compression {0}", inSilenceTime.FormatTime());
                            currentNowPlaying.StartDateTime = DateTime.UtcNow - fileReadPosition + audioOut.TimeBuffered;
                            ThreadPool.QueueUserWorkItem(delegate { mdb.NowPlaying.Replace(currentNowPlaying); });
                        }
                        inSilenceTime = TimeSpan.Zero;
                    }
                }

                if (!audioOut.Configuration.Equals(audioData))
                {
                    this.LogWarning("Frankenstein Stream in file <red>{0}", fileName);
                    break;
                }
                audioOut.Write(audioData);
            }
            nextStart = DateTime.UtcNow;
            if (skip)
            {
                //skipped, start in 2s
                nextStart += TimeSpan.FromSeconds(1);
            }
            else
            {
                //start after current title
                nextStart += audioOut.TimeBuffered - TimeSpan.FromSeconds(1);
            }

            this.LogInfo("Finish playing {0}", selection.AudioFile);
            if (exit)
            {
                CloseAudioOut(audioOut);
            }
            else
            {
                CloseAudioOutAsync(audioOut);
            }
        }
        /// <summary>Selects the next file.</summary>
        /// <param name="streamID">The stream identifier.</param>
        /// <returns>Returns true on success, false otherwise</returns>
        public MDBFileSelection SelectNextFile(long streamID)
        {
            var       config = mdb.GetStreamSettings(streamID);
            MDBSubset subset = mdb.Subsets.TryGetStruct(config.SubsetID);

            if (subset.ID == 0)
            {
                subset.Name = "Undefined";
            }
            int seqNumber = mdb.Subsets.SequenceNumber ^ mdb.SubsetFilters.SequenceNumber ^ mdb.AudioFiles.SequenceNumber;

            if (audioFileIDs == null || seqNumber != sequenceNumber)
            {
                audioFileIDs = mdb.GetSubsetAudioFileIDs(subset.ID, config.MinimumLength, config.MaximumLength);
                if (subset.TitleCount != audioFileIDs.Count)
                {
                    subset.TitleCount = audioFileIDs.Count;
                    if (subset.ID > 0)
                    {
                        mdb.Subsets.Update(subset);
                    }
                }
                sequenceNumber = seqNumber;
                this.LogInfo("Reloaded subset {0} at player", subset);
            }

            if (subset.ID > 0 && subset.TitleCount != audioFileIDs.Count)
            {
                subset.TitleCount = audioFileIDs.Count;
                try
                {
                    mdb.Subsets.Update(subset);
                    this.LogInfo("Subset {0} title count updated!", subset);
                }
                catch { }
            }

            if (audioFileIDs.Count == 0)
            {
                this.LogDebug("No subset defined or subset result empty for stream <red>{0}<default> selecting random titles.", streamID);
                audioFileIDs = mdb.AudioFiles.IDs;
            }
            if (audioFileIDs.Count == 0)
            {
                this.LogDebug("No audio files found!");
                Selection = null;
                return(null);
            }

            var listSearch = Search.FieldEquals(nameof(MDBPlayListItem.StreamID), streamID);

            while (true)
            {
                Func <long> getCount = () => mdb.PlayListItems.Count(listSearch);

                //fill playlist
                for (int n = 0; getCount() < config.MinimumTitleCount; n++)
                {
                    //max 4 tries per slot
                    if (n > config.MinimumTitleCount * 4)
                    {
                        break;
                    }
                    int  i      = (int)((rnd.Next() * (long)rnd.Next()) % audioFileIDs.Count);
                    long nextID = audioFileIDs[i];
                    //audiofile valid ?
                    MDBAudioFile audioFile;
                    if (!mdb.AudioFiles.TryGetStruct(nextID, out audioFile))
                    {
                        continue;
                    }

                    //file does not exist
                    if (!File.Exists(mdb.Files.TryGetStruct(audioFile.FileID).GetFullPath(mdb)))
                    {
                        mdb.AudioFiles.TryDelete(audioFile.FileID);
                        mdb.Files.TryDelete(audioFile.FileID);
                        this.LogError("AudioFile <red>{0}<default> removed (inaccessible).", audioFile);
                        continue;
                    }

                    //yes, playlist contains id already ?
                    if (mdb.PlayListItems.Exist(listSearch & Search.FieldEquals(nameof(MDBPlayListItem.AudioFileID), nextID)))
                    {
                        continue;
                    }
                    //no add
                    mdb.PlayListItems.Insert(new MDBPlayListItem()
                    {
                        AudioFileID = audioFile.FileID,
                        StreamID    = streamID,
                        SubsetID    = subset.ID,
                        Added       = DateTime.UtcNow.AddTicks(DefaultRNG.Int8),
                    });
                    this.LogInfo("Added audio file {0} from subset {1} to {2} playlist.", audioFile, subset, streamID);
                }

                mdb.Save();

                //get current entry
                try
                {
                    var items = mdb.PlayListItems.GetStructs(
                        listSearch & Search.FieldGreater(nameof(MDBPlayListItem.OwnerID), 0),
                        ResultOption.SortAscending(nameof(MDBPlayListItem.Added)) + ResultOption.Limit(1));
                    if (items.Count == 0)
                    {
                        items = mdb.PlayListItems.GetStructs(
                            listSearch & Search.FieldEquals(nameof(MDBPlayListItem.OwnerID), 0),
                            ResultOption.SortAscending(nameof(MDBPlayListItem.Added)) + ResultOption.Limit(1));
                    }
                    var item = items.FirstOrDefault();
                    if (item.ID == 0)
                    {
                        continue;
                    }

                    try
                    {
                        var result = Selection = MDBFileSelection.Load(mdb, item);
                        return(result);
                    }
                    finally
                    {
                        //always remove playlistitem (even on errors)
                        mdb.PlayListItems.TryDelete(item.ID);
                    }
                }
                catch (Exception ex)
                {
                    this.LogError(ex, "Cannot start stream {0}!", streamID, ex.Message);
                    Selection = null;
                    return(null);
                }
            }
        }