/// <summary> /// Attach a stream to the Mixer /// </summary> /// <param name="stream"></param> /// <returns></returns> public bool AttachStream(MusicStream stream) { Bass.BASS_ChannelLock(_mixer, true); // Set SynyPos at end of stream SetSyncPos(stream, 0.0); bool result = BassMix.BASS_Mixer_StreamAddChannel(_mixer, stream.BassStream, BASSFlag.BASS_MIXER_NORAMPIN | BASSFlag.BASS_MIXER_BUFFER | BASSFlag.BASS_MIXER_MATRIX | BASSFlag.BASS_MIXER_DOWNMIX | BASSFlag.BASS_STREAM_AUTOFREE); if (!result) { Log.Error("BASS: Error attaching stream to mixer. {0}", Bass.BASS_ErrorGetCode()); } Bass.BASS_ChannelLock(_mixer, false); if (result && _mixingMatrix != null) { Log.Debug("BASS: Setting mixing matrix..."); result = BassMix.BASS_Mixer_ChannelSetMatrix(stream.BassStream, _mixingMatrix); if (!result) { Log.Error("BASS: Error attaching Mixing Matrix. {0}", Bass.BASS_ErrorGetCode()); } } return(result); }
/// <summary> /// Sets a SyncPos on the mixer stream /// </summary> /// <param name="stream"></param> /// <param name="timePos"></param> public void SetSyncPos(MusicStream stream, double timePos) { double fadeOutSeconds = Config.CrossFadeIntervalMs / 1000.0; double totalStreamLen = Bass.BASS_ChannelBytes2Seconds(stream.BassStream, Bass.BASS_ChannelGetLength(stream.BassStream, BASSMode.BASS_POS_BYTES)); long mixerPos = Bass.BASS_ChannelGetPosition(_mixer, BASSMode.BASS_POS_BYTES | BASSMode.BASS_POS_DECODE); long syncPos = mixerPos + Bass.BASS_ChannelSeconds2Bytes(_mixer, totalStreamLen - timePos - fadeOutSeconds); if (_syncProc != 0) { Bass.BASS_ChannelRemoveSync(_mixer, _syncProc); } // We might have stored the pinned object already, because we are skipping // Only store object it, when it doesn't exist if (!_pinnedObjects.ContainsKey(stream.BassStream)) { Log.Debug("BASS: Updating Dictionary for GCHandle for stream {0}", stream.BassStream); // Add the pinned object to the global dictionary, so that we can free it later in the Sync End Proc _pinnedObjects.Add(stream.BassStream, GCHandle.Alloc(stream)); } _syncProc = Bass.BASS_ChannelSetSync(_mixer, BASSSync.BASS_SYNC_ONETIME | BASSSync.BASS_SYNC_POS | BASSSync.BASS_SYNC_MIXTIME, syncPos, _playbackEndProcDelegate, new IntPtr(stream.BassStream)); }
/// <summary> /// Sets a SyncPos on the mixer stream /// </summary> /// <param name="stream"></param> /// <param name="timePos"></param> public void SetSyncPos(MusicStream stream, double timePos) { double fadeOutSeconds = Config.CrossFadeIntervalMs / 1000.0; double totalStreamLen = Bass.BASS_ChannelBytes2Seconds(stream.BassStream, Bass.BASS_ChannelGetLength(stream.BassStream, BASSMode.BASS_POS_BYTES)); long mixerPos = Bass.BASS_ChannelGetPosition(_mixer, BASSMode.BASS_POS_BYTES | BASSMode.BASS_POS_DECODE); long syncPos = mixerPos + Bass.BASS_ChannelSeconds2Bytes(_mixer, totalStreamLen - timePos - fadeOutSeconds); if (_syncProc != 0) { Bass.BASS_ChannelRemoveSync(_mixer, _syncProc); } GCHandle pFilePath = GCHandle.Alloc(stream); _syncProc = Bass.BASS_ChannelSetSync(_mixer, BASSSync.BASS_SYNC_ONETIME | BASSSync.BASS_SYNC_POS | BASSSync.BASS_SYNC_MIXTIME, syncPos, _playbackEndProcDelegate, GCHandle.ToIntPtr(pFilePath)); }
/// <summary> /// Called to start playback of next files internally, without makinbg use of g_player. /// This gives us the capability to achieve gapless playback. /// </summary> /// <param name="filePath"></param> private bool PlayInternal(string filePath) { if (filePath == string.Empty) { return false; } // Cue support if (HandleCueFile(ref filePath, true)) { return true; } _filePath = filePath; MusicStream stream = new MusicStream(filePath); if (stream.BassStream == 0) { return false; } _streams.Add(stream); if (stream.Filetype.FileMainType == FileMainType.CDTrack) { _isCDDAFile = true; } else { _isCDDAFile = false; } if (_mixer == null) { _mixer = new MixerStream(this); _mixer.MusicStreamMessage += OnMusicStreamMessage; if (!_mixer.CreateMixer(stream)) { Log.Error("BASS: Could not create Mixer. Aborting playback."); return false; } } else { if (NewMixerNeeded(stream)) { Log.Debug("BASS: New stream has different number of channels or sample rate. Need a new mixer."); // Free Mixer _mixer.Dispose(); _mixer = null; _mixer = new MixerStream(this); _mixer.MusicStreamMessage += OnMusicStreamMessage; if (!_mixer.CreateMixer(stream)) { Log.Error("BASS: Could not create Mixer. Aborting playback."); return false; } } } // Enable events, for various Playback Actions to be handled stream.MusicStreamMessage += new MusicStream.MusicStreamMessageHandler(OnMusicStreamMessage); SetCueTrackEndPosition(stream, false); // Plug in the stream into the Mixer if (!_mixer.AttachStream(stream)) { return false; } return true; }
/// <summary> /// Sets the End Position for the CUE Track /// </summary> /// <param name="stream"></param> private void SetCueTrackEndPosition(MusicStream stream, bool endOnly) { if (_currentCueSheet != null) { stream.SetCueTrackEndPos(_cueTrackStartPos, _cueTrackEndPos, endOnly); } }
/// <summary> /// Create a mixer using the stream attributes /// </summary> /// <param name="stream"></param> /// <returns></returns> public bool CreateMixer(MusicStream stream) { Log.Debug("BASS: ---------------------------------------------"); Log.Debug("BASS: Creating BASS mixer stream"); bool result = false; BASSFlag mixerFlags = BASSFlag.BASS_MIXER_NONSTOP | BASSFlag.BASS_SAMPLE_FLOAT | BASSFlag.BASS_MIXER_NORAMPIN; if (Config.MusicPlayer == AudioPlayer.Asio || Config.MusicPlayer == AudioPlayer.WasApi) { mixerFlags |= BASSFlag.BASS_STREAM_DECODE; } int outputChannels = _bassPlayer.DeviceChannels; _mixingMatrix = null; // See, if we need Upmixing if (outputChannels > stream.ChannelInfo.chans) { Log.Debug("BASS: Found more output channels ({0}) than input channels ({1}). Check for upmixing.", outputChannels, stream.ChannelInfo.chans); _mixingMatrix = CreateMixingMatrix(stream.ChannelInfo.chans); if (_mixingMatrix != null) { outputChannels = Math.Min(_mixingMatrix.GetLength(0), outputChannels); _upmixing = true; } else { outputChannels = stream.ChannelInfo.chans; } } else if (outputChannels < stream.ChannelInfo.chans) { // Downmix to Stereo Log.Debug("BASS: Found more input channels ({0}) than output channels ({1}). Downmix.", stream.ChannelInfo.chans, outputChannels); outputChannels = Math.Min(outputChannels, 2); } Log.Debug("BASS: Creating {0} channel mixer with sample rate of {1}", outputChannels, stream.ChannelInfo.freq); _mixer = BassMix.BASS_Mixer_StreamCreate(stream.ChannelInfo.freq, outputChannels, mixerFlags); if (_mixer == 0) { Log.Error("BASS: Unable to create Mixer. Reason: {0}.", Enum.GetName(typeof(BASSError), Bass.BASS_ErrorGetCode())); return(false); } switch (Config.MusicPlayer) { case AudioPlayer.Bass: case AudioPlayer.DShow: if (!Bass.BASS_ChannelPlay(_mixer, false)) { Log.Error("BASS: Unable to start Mixer. Reason: {0}.", Enum.GetName(typeof(BASSError), Bass.BASS_ErrorGetCode())); return(false); } result = true; break; case AudioPlayer.Asio: Log.Info("BASS: Initialising ASIO device"); if (BassAsio.BASS_ASIO_IsStarted() && !BassAsio.BASS_ASIO_Stop()) { Log.Error("BASS: Error stopping Asio Device: {0}", BassAsio.BASS_ASIO_ErrorGetCode()); } // Disable and Unjoin all the channels if (!BassAsio.BASS_ASIO_ChannelReset(false, -1, BASSASIOReset.BASS_ASIO_RESET_ENABLE)) { Log.Error("BASS: Error disabling Asio Channels: {0}", BassAsio.BASS_ASIO_ErrorGetCode()); } if (!BassAsio.BASS_ASIO_ChannelReset(false, -1, BASSASIOReset.BASS_ASIO_RESET_JOIN)) { Log.Error("BASS: Error unjoining Asio Channels: {0}", BassAsio.BASS_ASIO_ErrorGetCode()); } _asioProc = new ASIOPROC(AsioCallback); BassAsio.BASS_ASIO_ChannelSetVolume(false, -1, (float)Config.StreamVolume / 100f); // enable 1st output channel...(0=first) Log.Debug("BASS: Joining Asio Channel #{0}", "0"); BassAsio.BASS_ASIO_ChannelEnable(false, 0, _asioProc, new IntPtr(_mixer)); // and join the next channels to it int numChannels = Math.Max(stream.ChannelInfo.chans, outputChannels); for (int i = 1; i < numChannels; i++) { Log.Debug("BASS: Joining Asio Channel #{0}", i); BassAsio.BASS_ASIO_ChannelJoin(false, i, 0); } // since we joined the channels, the next commands will apply to all channles joined // so setting the values to the first channels changes them all automatically // set the source format (float, as the decoding channel is) if (!BassAsio.BASS_ASIO_ChannelSetFormat(false, 0, BASSASIOFormat.BASS_ASIO_FORMAT_FLOAT)) { Log.Error("BASS: Error setting Asio Sample Format: {0}", BassAsio.BASS_ASIO_ErrorGetCode()); } // set the source rate Log.Debug("BASS: Set sample rate to {0}", stream.ChannelInfo.freq); if (!BassAsio.BASS_ASIO_ChannelSetRate(false, 0, (double)stream.ChannelInfo.freq)) { Log.Error("BASS: Error setting Asio Channel Samplerate: {0}", BassAsio.BASS_ASIO_ErrorGetCode()); } // try to set the device rate too (saves resampling) if (!BassAsio.BASS_ASIO_SetRate((double)stream.ChannelInfo.freq)) { Log.Error("BASS: Error setting Asio Samplerate: {0}", BassAsio.BASS_ASIO_ErrorGetCode()); } // and start playing it...start output using default buffer/latency if (!BassAsio.BASS_ASIO_Start(0)) { Log.Error("BASS: Error starting Asio playback: {0}", BassAsio.BASS_ASIO_ErrorGetCode()); } Log.Info("BASS: Finished initialising ASIO device"); result = true; break; case AudioPlayer.WasApi: Log.Info("BASS: Initialising WASAPI device"); try { BassWasapi.BASS_WASAPI_Free(); Log.Debug("BASS: Freed WASAPI device"); } catch (Exception ex) { Log.Error("BASS: Exception freeing WASAPI. {0} {1}", ex.Message, ex.StackTrace); } BASSWASAPIInit initFlags = BASSWASAPIInit.BASS_WASAPI_AUTOFORMAT; _wasapiProc = new WASAPIPROC(WasApiCallback); bool wasApiExclusiveSupported = true; // Check if we have an uneven number of channels var chkChannels = outputChannels % 2; if (chkChannels == 1) { Log.Warn("BASS: Found uneven number of channels {0}. increase output channels.", outputChannels); outputChannels++; // increase the number of output channels wasApiExclusiveSupported = false; // And indicate that we need a new mixer } // Handle the special cases of 3.0, 4.0 and 5.0 files being played on a 5.1 or 6.1 device if (outputChannels == 3) // a 3.0 file { Log.Info("BASS: Found a 3 channel file. Set upmixing with LFE, LR, RR set to silent"); _mixingMatrix = CreateThreeDotZeroUpMixMatrix(); outputChannels = _bassPlayer.DeviceChannels; // WASAPI device should be initialised with all channels active wasApiExclusiveSupported = false; // And indicate that we need a new mixer } else if (outputChannels == 4) // a 4.0 file { Log.Info("BASS: Found a 4 channel file. Set upmixing with Center and LFE set to silent"); _mixingMatrix = CreateFourDotZeroUpMixMatrix(); outputChannels = _bassPlayer.DeviceChannels; // WASAPI device should be initialised with all channels active wasApiExclusiveSupported = false; // And indicate that we need a new mixer } else if (outputChannels == 5) // a 5.0 file { Log.Info("BASS: Found a 5 channel file. Set upmixing with LFE set to silent"); _mixingMatrix = CreateFiveDotZeroUpMixMatrix(); outputChannels = _bassPlayer.DeviceChannels; // WASAPI device should be initialised with all channels active wasApiExclusiveSupported = false; // And indicate that we need a new mixer } // If Exclusive mode is used, check, if that would be supported, otherwise init in shared mode if (Config.WasApiExclusiveMode) { initFlags |= BASSWASAPIInit.BASS_WASAPI_EXCLUSIVE; _wasapiShared = false; _wasapiMixedChans = 0; _wasapiMixedFreq = 0; BASSWASAPIFormat wasapiFormat = BassWasapi.BASS_WASAPI_CheckFormat(_bassPlayer.DeviceNumber, stream.ChannelInfo.freq, outputChannels, BASSWASAPIInit.BASS_WASAPI_EXCLUSIVE); if (wasapiFormat == BASSWASAPIFormat.BASS_WASAPI_FORMAT_UNKNOWN) { Log.Warn("BASS: WASAPI exclusive mode not directly supported. Let BASS WASAPI choose better mode."); wasApiExclusiveSupported = false; } } else { Log.Debug("BASS: Init WASAPI shared mode with Event driven system enabled."); initFlags |= BASSWASAPIInit.BASS_WASAPI_SHARED | BASSWASAPIInit.BASS_WASAPI_EVENT; // In case of WASAPI Shared mode we need to setup the mixer to use the same sample rate as set in // the Windows Mixer, otherwise we wioll have increased playback speed BASS_WASAPI_DEVICEINFO devInfo = BassWasapi.BASS_WASAPI_GetDeviceInfo(_bassPlayer.DeviceNumber); Log.Debug("BASS: Creating {0} channel mixer for frequency {1}", devInfo.mixchans, devInfo.mixfreq); _mixer = BassMix.BASS_Mixer_StreamCreate(devInfo.mixfreq, devInfo.mixchans, mixerFlags); if (_mixer == 0) { Log.Error("BASS: Unable to create Mixer. Reason: {0}.", Enum.GetName(typeof(BASSError), Bass.BASS_ErrorGetCode())); return(false); } _wasapiShared = true; } Log.Debug("BASS: Try to init WASAPI with a Frequency of {0} and {1} channels", stream.ChannelInfo.freq, outputChannels); if (BassWasapi.BASS_WASAPI_Init(_bassPlayer.DeviceNumber, stream.ChannelInfo.freq, outputChannels, initFlags | BASSWASAPIInit.BASS_WASAPI_BUFFER, Convert.ToSingle(Config.BufferingMs / 1000.0), 0f, _wasapiProc, IntPtr.Zero)) { BASS_WASAPI_INFO wasapiInfo = BassWasapi.BASS_WASAPI_GetInfo(); Log.Debug("BASS: ---------------------------------------------"); Log.Debug("BASS: Buffer Length: {0}", wasapiInfo.buflen); Log.Debug("BASS: Channels: {0}", wasapiInfo.chans); Log.Debug("BASS: Frequency: {0}", wasapiInfo.freq); Log.Debug("BASS: Format: {0}", wasapiInfo.format.ToString()); Log.Debug("BASS: InitFlags: {0}", wasapiInfo.initflags.ToString()); Log.Debug("BASS: Exclusive: {0}", wasapiInfo.IsExclusive.ToString()); Log.Debug("BASS: ---------------------------------------------"); Log.Info("BASS: WASAPI Device successfully initialised"); // Now we need to check, if WASAPI decided to switch to a different mode if (Config.WasApiExclusiveMode && !wasApiExclusiveSupported) { // Recreate Mixer with new value Log.Debug("BASS: Creating new {0} channel mixer for frequency {1}", wasapiInfo.chans, wasapiInfo.freq); _mixer = BassMix.BASS_Mixer_StreamCreate(wasapiInfo.freq, wasapiInfo.chans, mixerFlags); if (_mixer == 0) { Log.Error("BASS: Unable to create Mixer. Reason: {0}.", Enum.GetName(typeof(BASSError), Bass.BASS_ErrorGetCode())); return(false); } } BassWasapi.BASS_WASAPI_SetVolume(BASSWASAPIVolume.BASS_WASAPI_CURVE_DB, (float)Config.StreamVolume / 100f); BassWasapi.BASS_WASAPI_Start(); result = true; } else { Log.Error("BASS: Couldn't init WASAPI device. Error: {0}", Enum.GetName(typeof(BASSError), Bass.BASS_ErrorGetCode())); } break; } if (result) { Log.Debug("BASS: Successfully created BASS Mixer stream"); } return(result); }
/// <summary> /// End of Playback for a stream has been signaled /// Send event to Bass player to start playback of next song /// </summary> /// <param name="handle"></param> /// <param name="stream"></param> /// <param name="data"></param> /// <param name="userData"></param> private void PlaybackEndProc(int handle, int stream, int data, IntPtr userData) { try { MusicStream musicstream; // Get the GC handle of the pinned object try { musicstream = (MusicStream)_pinnedObjects[userData.ToInt32()].Target; } catch (KeyNotFoundException ex) { Log.Error("BASS: GCHandle of Musicstream not found in Dictionary {0} {1}", userData.ToInt32(), ex.Message); return; } Log.Debug("BASS: End of Song {0}", musicstream.FilePath); // We need to find out, if the nextsongs sample rate and / or number of channels are different to the one just ended // If this is the case we need a new mixer and the OnMusicStreamMessage needs to be invoked in a thread to avoid crashes. // In order to have gapless playback, it needs to be invoked in sync. MusicStream nextStream = null; Playlists.PlayListItem nextSong = Playlists.PlayListPlayer.SingletonPlayer.GetNextItem(); MusicStream._fileType = Utils.GetFileType(musicstream.FilePath); if (nextSong != null && MusicStream._fileType.FileMainType != FileMainType.WebStream) { nextStream = new MusicStream(nextSong.FileName, true); } else if (MusicStream._fileType.FileMainType == FileMainType.WebStream) { if (MusicStreamMessage != null) { MusicStreamMessage(musicstream, MusicStream.StreamAction.InternetStreamChanged); return; } } bool newMixerNeeded = false; if (nextStream != null && nextStream.BassStream != 0) { if (_bassPlayer.NewMixerNeeded(nextStream)) { newMixerNeeded = true; } nextStream.Dispose(); } if (newMixerNeeded) { if (Config.MusicPlayer == AudioPlayer.WasApi && BassWasapi.BASS_WASAPI_IsStarted()) { BassWasapi.BASS_WASAPI_Stop(true); } // Unplug the Source channel from the mixer Log.Debug("BASS: Unplugging source channel from Mixer."); BassMix.BASS_Mixer_ChannelRemove(musicstream.BassStream); // invoke a thread because we need a new mixer Log.Debug("BASS: Next song needs a new mixer."); new Thread(() => { if (MusicStreamMessage != null) { MusicStreamMessage(musicstream, MusicStream.StreamAction.Crossfading); } }) { Name = "BASS" }.Start(); } else { if (MusicStreamMessage != null) { MusicStreamMessage(musicstream, MusicStream.StreamAction.Crossfading); } } } catch (AccessViolationException ex) { Log.Error("BASS: Caught AccessViolationException in Playback End Proc {0}", ex.Message); } }
/// <summary> /// This Message is sent from a MusicStream /// </summary> /// <param name="sender"></param> /// <param name="action"></param> private void OnMusicStreamMessage(object sender, MusicStream.StreamAction action) { if (sender == null) { return; } MusicStream musicStream = (MusicStream)sender; // we want gapless playback for external controller (aka UPnP Controller) // therefore we use the same mechanism if (g_Player.ExternalController && action == MusicStream.StreamAction.InternetStreamChanged) action = MusicStream.StreamAction.Crossfading; switch (action) { case MusicStream.StreamAction.Ended: break; case MusicStream.StreamAction.Crossfading: string nextSong = Playlists.PlayListPlayer.SingletonPlayer.GetNextSong(); if (nextSong != string.Empty) { g_Player.OnChanged(nextSong); PlayInternal(nextSong); g_Player.currentMedia = g_Player.MediaType.Music; g_Player.currentFilePlaying = nextSong; g_Player.OnStarted(); NotifyPlaying = true; } else { Log.Debug("BASS: Reached end of playlist."); g_Player.OnStopped(); Stop(); } break; case MusicStream.StreamAction.InternetStreamChanged: _tagInfo = musicStream.StreamTags; if (InternetStreamSongChanged != null) { InternetStreamSongChanged(this); } break; case MusicStream.StreamAction.Freed: { _mixer.FreeGcHandle(musicStream.BassStream); musicStream.Dispose(); } break; case MusicStream.StreamAction.Disposed: // Remove the stream from the active streams and free it lock (_streams) { if (_streams.Contains(musicStream)) { _streams.Remove(musicStream); } } break; } }
/// <summary> /// End of Playback for a stream has been signaled /// Send event to Bass player to start playback of next song /// </summary> /// <param name="handle"></param> /// <param name="stream"></param> /// <param name="data"></param> /// <param name="userData"></param> private void PlaybackEndProc(int handle, int stream, int data, IntPtr userData) { try { GCHandle gch = GCHandle.FromIntPtr(userData); MusicStream musicstream = (MusicStream)gch.Target; Log.Debug("BASS: End of Song {0}", musicstream.FilePath); // We need to find out, if the nextsongs sample rate and / or number of channels are different to the one just ended // If this is the case we need a new mixer and the OnMusicStreamMessage needs to be invoked in a thread to avoid crashes. // In order to have gapless playback, it needs to be invoked in sync. MusicStream nextStream = null; Playlists.PlayListItem nextSong = Playlists.PlayListPlayer.SingletonPlayer.GetNextItem(); MusicStream._fileType = Utils.GetFileType(musicstream.FilePath); if (nextSong != null && MusicStream._fileType.FileMainType != FileMainType.WebStream) { nextStream = new MusicStream(nextSong.FileName, true); } else if (MusicStream._fileType.FileMainType == FileMainType.WebStream) { if (MusicStreamMessage != null) { MusicStreamMessage(musicstream, MusicStream.StreamAction.InternetStreamChanged); return; } } bool newMixerNeeded = false; if (nextStream != null && nextStream.BassStream != 0) { if (_bassPlayer.NewMixerNeeded(nextStream)) { newMixerNeeded = true; } nextStream.Dispose(); } if (newMixerNeeded) { if (Config.MusicPlayer == AudioPlayer.WasApi && BassWasapi.BASS_WASAPI_IsStarted()) { BassWasapi.BASS_WASAPI_Stop(true); } // Unplug the Source channel from the mixer Log.Debug("BASS: Unplugging source channel from Mixer."); BassMix.BASS_Mixer_ChannelRemove(musicstream.BassStream); // invoke a thread because we need a new mixer Log.Debug("BASS: Next song needs a new mixer."); new Thread(() => { if (MusicStreamMessage != null) { MusicStreamMessage(musicstream, MusicStream.StreamAction.Crossfading); } }) { Name = "BASS" }.Start(); } else { if (MusicStreamMessage != null) { MusicStreamMessage(musicstream, MusicStream.StreamAction.Crossfading); } } } catch (AccessViolationException) { Log.Error("BASS: Caught AccessViolationException in Playback End Proc"); } }
/// <summary> /// Attach a stream to the Mixer /// </summary> /// <param name="stream"></param> /// <returns></returns> public bool AttachStream(MusicStream stream) { Bass.BASS_ChannelLock(_mixer, true); // Set SynyPos at end of stream SetSyncPos(stream, 0.0); bool result = BassMix.BASS_Mixer_StreamAddChannel(_mixer, stream.BassStream, BASSFlag.BASS_MIXER_NORAMPIN | BASSFlag.BASS_MIXER_BUFFER | BASSFlag.BASS_MIXER_MATRIX | BASSFlag.BASS_MIXER_DOWNMIX | BASSFlag.BASS_STREAM_AUTOFREE); if (!result) { Log.Error("BASS: Error attaching stream to mixer. {0}", Bass.BASS_ErrorGetCode()); } Bass.BASS_ChannelLock(_mixer, false); if (result && _mixingMatrix != null) { Log.Debug("BASS: Setting mixing matrix..."); result = BassMix.BASS_Mixer_ChannelSetMatrix(stream.BassStream, _mixingMatrix); if (!result) { Log.Error("BASS: Error attaching Mixing Matrix. {0}", Bass.BASS_ErrorGetCode()); } } return result; }
/// <summary> /// This Message is sent from a MusicStream /// </summary> /// <param name="sender"></param> /// <param name="action"></param> void OnMusicStreamMessage(object sender, MusicStream.StreamAction action) { if (sender == null) { return; } MusicStream musicStream = (MusicStream)sender; switch (action) { case MusicStream.StreamAction.Ended: if (TrackPlaybackCompleted != null) { TrackPlaybackCompleted(this, musicStream.FilePath); } BassMix.BASS_Mixer_ChannelRemove(musicStream.BassStream); musicStream.Dispose(); // Check, if PlaylistPlayer has to offer more files) if (Playlists.PlayListPlayer.SingletonPlayer.GetNext() == string.Empty) { MusicStream currentStream = GetCurrentStream(); if (currentStream == null || !currentStream.IsPlaying) { Log.Debug("BASS: Reached end of playlist."); Stop(); } } break; case MusicStream.StreamAction.InternetStreamChanged: if (InternetStreamSongChanged != null) { InternetStreamSongChanged(this); } break; case MusicStream.StreamAction.Disposed: // Remove the stream from the active streams and free it lock (_streams) { if (_streams.Contains(musicStream)) { _streams.Remove(musicStream); } } break; } }
/// <summary> /// Starts Playback of the given file /// </summary> /// <param name="filePath"></param> /// <returns></returns> public override bool Play(string filePath) { if (!_initialized) { return false; } MusicStream currentStream = GetCurrentStream(); bool result = true; Speed = 1; // Set playback Speed to normal speed try { if (currentStream != null && filePath.ToLowerInvariant().CompareTo(currentStream.FilePath.ToLowerInvariant()) == 0) { // Selected file is equal to current stream if (_state == PlayState.Paused) { // Resume paused stream currentStream.ResumePlayback(); result = Bass.BASS_Start(); if (Config.MusicPlayer == AudioPlayer.Asio) { result = BassAsio.BASS_ASIO_ChannelReset(false, 0, BASSASIOReset.BASS_ASIO_RESET_PAUSE); // Continue playback of Paused stream } else if (Config.MusicPlayer == AudioPlayer.WasApi) { BassWasapi.BASS_WASAPI_Start(); } if (result) { _state = PlayState.Playing; if (PlaybackStateChanged != null) { PlaybackStateChanged(this, PlayState.Paused, _state); } } return result; } } else { // Cue support if (HandleCueFile(ref filePath)) { return true; } } // If we're not Crossfading, we want to stop the current stream at this time if (currentStream != null && currentStream.IsPlaying) { if (!currentStream.IsCrossFading) { currentStream.FadeOutStop(); } } _state = PlayState.Init; if (filePath == string.Empty) { return result; } _filePath = filePath; MusicStream stream = new MusicStream(filePath); if (stream.BassStream == 0) { return false; } _streams.Add(stream); if (stream.Filetype.FileMainType == FileMainType.CDTrack) { _isCDDAFile = true; } else { _isCDDAFile = false; } bool playbackStarted = false; if (_mixer == null) { _mixer = new MixerStream(this); if (!_mixer.CreateMixer(stream)) { Log.Error("BASS: Could not create Mixer. Aborting playback."); return false; } // Start a StreamCopy for Visualisation purposes if (HasViz) { _streamcopy.ChannelHandle = _mixer.BassStream; _streamcopy.Start(); } } else { if (!_mixer.UpMixing) { BASS_CHANNELINFO chinfo = Bass.BASS_ChannelGetInfo(_mixer.BassStream); if (!_mixer.WasApiShared && (chinfo.freq != stream.ChannelInfo.freq || chinfo.chans != stream.ChannelInfo.chans)) { if (stream.ChannelInfo.freq != _mixer.WasApiMixedFreq || stream.ChannelInfo.chans != _mixer.WasApiMixedChans) { Log.Debug("BASS: New stream has different number of channels or sample rate. Need a new mixer."); // The new stream has a different frequency or number of channels // We need a new mixer _mixer.Dispose(); _mixer = null; _mixer = new MixerStream(this); if (!_mixer.CreateMixer(stream)) { Log.Error("BASS: Could not create Mixer. Aborting playback."); return false; } if (HasViz) { _streamcopy.ChannelHandle = _mixer.BassStream; } } } } } // Enable events, for various Playback Actions to be handled stream.MusicStreamMessage += new MusicStream.MusicStreamMessageHandler(OnMusicStreamMessage); SetCueTrackEndPosition(stream); // Plugin the stream into the Mixer result = _mixer.AttachStream(stream); if (Config.MusicPlayer == AudioPlayer.Asio && !BassAsio.BASS_ASIO_IsStarted()) { BassAsio.BASS_ASIO_Stop(); playbackStarted = BassAsio.BASS_ASIO_Start(0); } else if (Config.MusicPlayer == AudioPlayer.WasApi && !BassWasapi.BASS_WASAPI_IsStarted()) { playbackStarted = BassWasapi.BASS_WASAPI_Start(); } else { if (Bass.BASS_ChannelIsActive(_mixer.BassStream) == BASSActive.BASS_ACTIVE_PLAYING) { playbackStarted = true; } else { playbackStarted = Bass.BASS_ChannelPlay(_mixer.BassStream, false); } } if (stream.BassStream != 0 && playbackStarted) { Log.Info("BASS: playback started"); // Slide in the Stream over the Cross fade Interval stream.SlideIn(); GUIMessage msg = new GUIMessage(GUIMessage.MessageType.GUI_MSG_PLAYBACK_STARTED, 0, 0, 0, 0, 0, null); msg.Label = _filePath; GUIWindowManager.SendThreadMessage(msg); NotifyPlaying = true; NeedUpdate = true; _IsFullScreen = GUIGraphicsContext.IsFullScreenVideo; _VideoPositionX = GUIGraphicsContext.VideoWindow.Left; _VideoPositionY = GUIGraphicsContext.VideoWindow.Top; _VideoWidth = GUIGraphicsContext.VideoWindow.Width; _VideoHeight = GUIGraphicsContext.VideoWindow.Height; // Re-Add the Viswindow to the Mainform Control (It got removed on a manual Stop) SetVisualizationWindow(); SetVideoWindow(); PlayState oldState = _state; _state = PlayState.Playing; if (oldState != _state && PlaybackStateChanged != null) { PlaybackStateChanged(this, oldState, _state); } if (PlaybackStart != null) { PlaybackStart(this, stream.TotalStreamSeconds); } } else { Log.Error("BASS: Unable to play {0}. Reason: {1}.", filePath, Enum.GetName(typeof(BASSError), Bass.BASS_ErrorGetCode())); stream.Dispose(); result = false; } } catch (Exception ex) { result = false; Log.Error("BASS: Play caused an exception: {0}.", ex); } return result; }
/// <summary> /// Checks, if a new Mixer would be needed, because of changes in Sample Rate or number of channels /// </summary> /// <param name="stream"></param> /// <returns></returns> public bool NewMixerNeeded(MusicStream stream) { if (!_mixer.UpMixing) { BASS_CHANNELINFO chinfo = Bass.BASS_ChannelGetInfo(_mixer.BassStream); if (!_mixer.WasApiShared && (chinfo.freq != stream.ChannelInfo.freq || (chinfo.chans != stream.ChannelInfo.chans && stream.ChannelInfo.chans != 1))) { if (stream.ChannelInfo.freq != _mixer.WasApiMixedFreq || stream.ChannelInfo.chans != _mixer.WasApiMixedChans) { return true; } } } return false; }
/// <summary> /// Create a mixer using the stream attributes /// </summary> /// <param name="stream"></param> /// <returns></returns> public bool CreateMixer(MusicStream stream) { Log.Debug("BASS: ---------------------------------------------"); Log.Debug("BASS: Creating BASS mixer stream"); bool result = false; BASSFlag mixerFlags = BASSFlag.BASS_MIXER_NONSTOP | BASSFlag.BASS_SAMPLE_FLOAT | BASSFlag.BASS_MIXER_NORAMPIN; if (Config.MusicPlayer == AudioPlayer.Asio || Config.MusicPlayer == AudioPlayer.WasApi) { mixerFlags |= BASSFlag.BASS_STREAM_DECODE; } int outputChannels = _bassPlayer.DeviceChannels; _mixingMatrix = null; // See, if we need Upmixing if (outputChannels > stream.ChannelInfo.chans) { Log.Debug("BASS: Found more output channels ({0}) than input channels ({1}). Check for upmixing.", outputChannels, stream.ChannelInfo.chans); _mixingMatrix = CreateMixingMatrix(stream.ChannelInfo.chans); if (_mixingMatrix != null) { outputChannels = Math.Min(_mixingMatrix.GetLength(0), outputChannels); _upmixing = true; } else { outputChannels = stream.ChannelInfo.chans; } } else if (outputChannels < stream.ChannelInfo.chans) { // Downmix to Stereo Log.Debug("BASS: Found more input channels ({0}) than output channels ({1}). Downmix.", stream.ChannelInfo.chans, outputChannels); outputChannels = Math.Min(outputChannels, 2); } Log.Debug("BASS: Creating {0} channel mixer with sample rate of {1}", outputChannels, stream.ChannelInfo.freq); _mixer = BassMix.BASS_Mixer_StreamCreate(stream.ChannelInfo.freq, outputChannels, mixerFlags); if (_mixer == 0) { Log.Error("BASS: Unable to create Mixer. Reason: {0}.", Enum.GetName(typeof(BASSError), Bass.BASS_ErrorGetCode())); return false; } switch (Config.MusicPlayer) { case AudioPlayer.Bass: case AudioPlayer.DShow: if (!Bass.BASS_ChannelPlay(_mixer, false)) { Log.Error("BASS: Unable to start Mixer. Reason: {0}.", Enum.GetName(typeof(BASSError), Bass.BASS_ErrorGetCode())); return false; } result = true; break; case AudioPlayer.Asio: Log.Info("BASS: Initialising ASIO device"); if (BassAsio.BASS_ASIO_IsStarted() && !BassAsio.BASS_ASIO_Stop()) { Log.Error("BASS: Error stopping Asio Device: {0}", BassAsio.BASS_ASIO_ErrorGetCode()); } // Disable and Unjoin all the channels if (!BassAsio.BASS_ASIO_ChannelReset(false, -1, BASSASIOReset.BASS_ASIO_RESET_ENABLE)) { Log.Error("BASS: Error disabling Asio Channels: {0}", BassAsio.BASS_ASIO_ErrorGetCode()); } if (!BassAsio.BASS_ASIO_ChannelReset(false, -1, BASSASIOReset.BASS_ASIO_RESET_JOIN)) { Log.Error("BASS: Error unjoining Asio Channels: {0}", BassAsio.BASS_ASIO_ErrorGetCode()); } _asioProc = new ASIOPROC(AsioCallback); BassAsio.BASS_ASIO_ChannelSetVolume(false, -1, (float)Config.StreamVolume / 100f); // enable 1st output channel...(0=first) Log.Debug("BASS: Joining Asio Channel #{0}", "0"); BassAsio.BASS_ASIO_ChannelEnable(false, 0, _asioProc, new IntPtr(_mixer)); // and join the next channels to it int numChannels = Math.Max(stream.ChannelInfo.chans, outputChannels); for (int i = 1; i < numChannels; i++) { Log.Debug("BASS: Joining Asio Channel #{0}", i); BassAsio.BASS_ASIO_ChannelJoin(false, i, 0); } // since we joined the channels, the next commands will apply to all channles joined // so setting the values to the first channels changes them all automatically // set the source format (float, as the decoding channel is) if (!BassAsio.BASS_ASIO_ChannelSetFormat(false, 0, BASSASIOFormat.BASS_ASIO_FORMAT_FLOAT)) { Log.Error("BASS: Error setting Asio Sample Format: {0}", BassAsio.BASS_ASIO_ErrorGetCode()); } // set the source rate Log.Debug("BASS: Set sample rate to {0}", stream.ChannelInfo.freq); if (!BassAsio.BASS_ASIO_ChannelSetRate(false, 0, (double)stream.ChannelInfo.freq)) { Log.Error("BASS: Error setting Asio Channel Samplerate: {0}", BassAsio.BASS_ASIO_ErrorGetCode()); } // try to set the device rate too (saves resampling) if (!BassAsio.BASS_ASIO_SetRate((double)stream.ChannelInfo.freq)) { Log.Error("BASS: Error setting Asio Samplerate: {0}", BassAsio.BASS_ASIO_ErrorGetCode()); } // and start playing it...start output using default buffer/latency if (!BassAsio.BASS_ASIO_Start(0)) { Log.Error("BASS: Error starting Asio playback: {0}", BassAsio.BASS_ASIO_ErrorGetCode()); } Log.Info("BASS: Finished initialising ASIO device"); result = true; break; case AudioPlayer.WasApi: Log.Info("BASS: Initialising WASAPI device"); try { BassWasapi.BASS_WASAPI_Free(); Log.Debug("BASS: Freed WASAPI device"); } catch (Exception ex) { Log.Error("BASS: Exception freeing WASAPI. {0} {1}", ex.Message, ex.StackTrace); } BASSWASAPIInit initFlags = BASSWASAPIInit.BASS_WASAPI_AUTOFORMAT; _wasapiProc = new WASAPIPROC(WasApiCallback); bool wasApiExclusiveSupported = true; // Check if we have an uneven number of channels var chkChannels = outputChannels % 2; if (chkChannels == 1) { Log.Warn("BASS: Found uneven number of channels {0}. increase output channels.", outputChannels); outputChannels++; // increase the number of output channels wasApiExclusiveSupported = false; // And indicate that we need a new mixer } // Handle the special cases of 3.0, 4.0 and 5.0 files being played on a 5.1 or 6.1 device if (outputChannels == 3) // a 3.0 file { Log.Info("BASS: Found a 3 channel file. Set upmixing with LFE, LR, RR set to silent"); _mixingMatrix = CreateThreeDotZeroUpMixMatrix(); outputChannels = _bassPlayer.DeviceChannels; // WASAPI device should be initialised with all channels active wasApiExclusiveSupported = false; // And indicate that we need a new mixer } else if (outputChannels == 4) // a 4.0 file { Log.Info("BASS: Found a 4 channel file. Set upmixing with Center and LFE set to silent"); _mixingMatrix = CreateFourDotZeroUpMixMatrix(); outputChannels = _bassPlayer.DeviceChannels; // WASAPI device should be initialised with all channels active wasApiExclusiveSupported = false; // And indicate that we need a new mixer } else if (outputChannels == 5) // a 5.0 file { Log.Info("BASS: Found a 5 channel file. Set upmixing with LFE set to silent"); _mixingMatrix = CreateFiveDotZeroUpMixMatrix(); outputChannels = _bassPlayer.DeviceChannels; // WASAPI device should be initialised with all channels active wasApiExclusiveSupported = false; // And indicate that we need a new mixer } // If Exclusive mode is used, check, if that would be supported, otherwise init in shared mode if (Config.WasApiExclusiveMode) { initFlags |= BASSWASAPIInit.BASS_WASAPI_EXCLUSIVE; _wasapiShared = false; _wasapiMixedChans = 0; _wasapiMixedFreq = 0; BASSWASAPIFormat wasapiFormat = BassWasapi.BASS_WASAPI_CheckFormat(_bassPlayer.DeviceNumber, stream.ChannelInfo.freq, outputChannels, BASSWASAPIInit.BASS_WASAPI_EXCLUSIVE); if (wasapiFormat == BASSWASAPIFormat.BASS_WASAPI_FORMAT_UNKNOWN) { Log.Warn("BASS: WASAPI exclusive mode not directly supported. Let BASS WASAPI choose better mode."); wasApiExclusiveSupported = false; } } else { Log.Debug("BASS: Init WASAPI shared mode with Event driven system enabled."); initFlags |= BASSWASAPIInit.BASS_WASAPI_SHARED | BASSWASAPIInit.BASS_WASAPI_EVENT; // In case of WASAPI Shared mode we need to setup the mixer to use the same sample rate as set in // the Windows Mixer, otherwise we wioll have increased playback speed BASS_WASAPI_DEVICEINFO devInfo = BassWasapi.BASS_WASAPI_GetDeviceInfo(_bassPlayer.DeviceNumber); Log.Debug("BASS: Creating {0} channel mixer for frequency {1}", devInfo.mixchans, devInfo.mixfreq); _mixer = BassMix.BASS_Mixer_StreamCreate(devInfo.mixfreq, devInfo.mixchans, mixerFlags); if (_mixer == 0) { Log.Error("BASS: Unable to create Mixer. Reason: {0}.", Enum.GetName(typeof(BASSError), Bass.BASS_ErrorGetCode())); return false; } _wasapiShared = true; } Log.Debug("BASS: Try to init WASAPI with a Frequency of {0} and {1} channels", stream.ChannelInfo.freq, outputChannels); if (BassWasapi.BASS_WASAPI_Init(_bassPlayer.DeviceNumber, stream.ChannelInfo.freq, outputChannels, initFlags | BASSWASAPIInit.BASS_WASAPI_BUFFER, Convert.ToSingle(Config.BufferingMs / 1000.0), 0f, _wasapiProc, IntPtr.Zero)) { BASS_WASAPI_INFO wasapiInfo = BassWasapi.BASS_WASAPI_GetInfo(); Log.Debug("BASS: ---------------------------------------------"); Log.Debug("BASS: Buffer Length: {0}", wasapiInfo.buflen); Log.Debug("BASS: Channels: {0}", wasapiInfo.chans); Log.Debug("BASS: Frequency: {0}", wasapiInfo.freq); Log.Debug("BASS: Format: {0}", wasapiInfo.format.ToString()); Log.Debug("BASS: InitFlags: {0}", wasapiInfo.initflags.ToString()); Log.Debug("BASS: Exclusive: {0}", wasapiInfo.IsExclusive.ToString()); Log.Debug("BASS: ---------------------------------------------"); Log.Info("BASS: WASAPI Device successfully initialised"); // Now we need to check, if WASAPI decided to switch to a different mode if (Config.WasApiExclusiveMode && !wasApiExclusiveSupported) { // Recreate Mixer with new value Log.Debug("BASS: Creating new {0} channel mixer for frequency {1}", wasapiInfo.chans, wasapiInfo.freq); _mixer = BassMix.BASS_Mixer_StreamCreate(wasapiInfo.freq, wasapiInfo.chans, mixerFlags); if (_mixer == 0) { Log.Error("BASS: Unable to create Mixer. Reason: {0}.", Enum.GetName(typeof(BASSError), Bass.BASS_ErrorGetCode())); return false; } } BassWasapi.BASS_WASAPI_SetVolume(BASSWASAPIVolume.BASS_WASAPI_CURVE_DB, (float)Config.StreamVolume / 100f); BassWasapi.BASS_WASAPI_Start(); result = true; } else { Log.Error("BASS: Couldn't init WASAPI device. Error: {0}", Enum.GetName(typeof(BASSError), Bass.BASS_ErrorGetCode())); } break; } if (result) { Log.Debug("BASS: Successfully created BASS Mixer stream"); } return result; }
/// <summary> /// This Message is sent from a MusicStream /// </summary> /// <param name="sender"></param> /// <param name="action"></param> private void OnMusicStreamMessage(object sender, MusicStream.StreamAction action) { if (sender == null) { return; } MusicStream musicStream = (MusicStream)sender; switch (action) { case MusicStream.StreamAction.Ended: break; case MusicStream.StreamAction.Crossfading: string nextSong = Playlists.PlayListPlayer.SingletonPlayer.GetNextSong(); if (nextSong != string.Empty) { g_Player.OnChanged(nextSong); PlayInternal(nextSong); g_Player.currentMedia = g_Player.MediaType.Music; g_Player.currentFilePlaying = nextSong; g_Player.OnStarted(); NotifyPlaying = true; } else { Log.Debug("BASS: Reached end of playlist."); g_Player.OnStopped(); Stop(); } break; case MusicStream.StreamAction.InternetStreamChanged: _tagInfo = musicStream.StreamTags; if (InternetStreamSongChanged != null) { InternetStreamSongChanged(this); } break; case MusicStream.StreamAction.Freed: { musicStream.Dispose(); } break; case MusicStream.StreamAction.Disposed: // Remove the stream from the active streams and free it lock (_streams) { if (_streams.Contains(musicStream)) { _streams.Remove(musicStream); } } break; } }