using System; using System.Linq; using System.Threading.Tasks; using CSCore.SoundOut; using NWaves.Audio; using ReactiveUI; using RedBookPlayer.Models.Discs; namespace RedBookPlayer.Models.Hardware { public class SoundOutput : ReactiveObject { #region Public Fields /// /// Indicate if the output is ready to be used /// public bool Initialized { get => _initialized; private set => this.RaiseAndSetIfChanged(ref _initialized, value); } /// /// Indicates the current player state /// public PlayerState PlayerState { get => _playerState; private set => this.RaiseAndSetIfChanged(ref _playerState, value); } /// /// Indicates the repeat mode /// public RepeatMode RepeatMode { get => _repeatMode; private set => this.RaiseAndSetIfChanged(ref _repeatMode, value); } /// /// Indicates if de-emphasis should be applied /// public bool ApplyDeEmphasis { get => _applyDeEmphasis; private set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value); } /// /// Current playback volume /// public int Volume { get => _volume; private set { int tempVolume = value; if(value > 100) tempVolume = 100; else if(value < 0) tempVolume = 0; this.RaiseAndSetIfChanged(ref _volume, tempVolume); } } private bool _initialized; private PlayerState _playerState; private RepeatMode _repeatMode; private bool _applyDeEmphasis; private int _volume; #endregion #region Private State Variables /// /// OpticalDisc from the parent player for easy access /// /// /// TODO: Can we remove the need for a local reference to OpticalDisc? /// private OpticalDiscBase _opticalDisc; /// /// Data provider for sound output /// private PlayerSource _source; /// /// Sound output instance /// private ALSoundOut _soundOut; /// /// Filtering stage for audio output /// private FilterStage _filterStage; /// /// Current position in the sector /// private int _currentSectorReadPosition = 0; /// /// Lock object for reading track data /// private readonly object _readingImage = new object(); #endregion /// /// Constructor /// /// Default volume between 0 and 100 to use when starting playback public SoundOutput(int defaultVolume = 100) { Volume = defaultVolume; _filterStage = new FilterStage(); } /// /// Initialize the output with a given image /// /// OpticalDisc to load from /// RepeatMode for sound output /// True if playback should begin immediately, false otherwise public void Init(OpticalDiscBase opticalDisc, RepeatMode repeatMode, bool autoPlay) { // If we have an unusable disc, just return if(opticalDisc == null || !opticalDisc.Initialized) return; // Save a reference to the disc _opticalDisc = opticalDisc; // Enable de-emphasis for CDs, if necessary if(opticalDisc is CompactDisc compactDisc) ApplyDeEmphasis = compactDisc.TrackHasEmphasis; // Setup de-emphasis filters _filterStage.SetupFilters(); // Setup the audio output SetupAudio(); // Setup the repeat mode RepeatMode = repeatMode; // Initialize playback, if necessary if(autoPlay) _soundOut.Play(); // Mark the output as ready Initialized = true; PlayerState = PlayerState.Stopped; // Begin loading data _source.Start(); } /// /// Reset the current internal state /// public void Reset() { _soundOut.Stop(); _opticalDisc = null; Initialized = false; PlayerState = PlayerState.NoDisc; } /// /// Fill the current byte buffer with playable data /// /// Buffer to load data into /// Offset in the buffer to load at /// Number of bytes to load /// Number of bytes read public int ProviderRead(byte[] buffer, int offset, int count) { // Set the current volume _soundOut.Volume = (float)Volume / 100; // If we have an unreadable track, just return if(_opticalDisc.BytesPerSector <= 0) { Array.Clear(buffer, offset, count); return count; } // Determine how many sectors we can read DetermineReadAmount(count, out ulong sectorsToRead, out ulong zeroSectorsAmount); // Get data to return byte[] audioDataSegment = ReadData(count, sectorsToRead, zeroSectorsAmount); if(audioDataSegment == null) { Array.Clear(buffer, offset, count); return count; } // Write out the audio data to the buffer Array.Copy(audioDataSegment, 0, buffer, offset, count); // Set the read position in the sector for easier access _currentSectorReadPosition += count; if(_currentSectorReadPosition >= _opticalDisc.BytesPerSector) { int currentTrack = _opticalDisc.CurrentTrackNumber; _opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector + (ulong)(_currentSectorReadPosition / _opticalDisc.BytesPerSector)); _currentSectorReadPosition %= _opticalDisc.BytesPerSector; if(RepeatMode == RepeatMode.None && _opticalDisc.CurrentTrackNumber < currentTrack) Stop(); else if(RepeatMode == RepeatMode.Single && _opticalDisc.CurrentTrackNumber != currentTrack) _opticalDisc.LoadTrack(currentTrack); } return count; } #region Playback /// /// Start audio playback /// public void Play() { if(_soundOut.PlaybackState != PlaybackState.Playing) _soundOut.Play(); PlayerState = PlayerState.Playing; } /// /// Pause audio playback /// public void Pause() { if(_soundOut.PlaybackState != PlaybackState.Paused) _soundOut.Pause(); PlayerState = PlayerState.Paused; } /// /// Stop audio playback /// public void Stop() { if(_soundOut.PlaybackState != PlaybackState.Stopped) _soundOut.Stop(); PlayerState = PlayerState.Stopped; } /// /// Eject the currently loaded disc /// public void Eject() => Reset(); #endregion #region Helpers /// /// Set de-emphasis status /// /// New de-emphasis status public void SetDeEmphasis(bool apply) => ApplyDeEmphasis = apply; /// /// Set repeat mode /// /// New repeat mode value public void SetRepeatMode(RepeatMode repeatMode) => RepeatMode = repeatMode; /// /// Set the value for the volume /// /// New volume value public void SetVolume(int volume) => Volume = volume; /// /// Determine the number of real and zero sectors to read /// /// Number of requested bytes to read /// Number of sectors to read /// Number of zeroed sectors to concatenate private void DetermineReadAmount(int count, out ulong sectorsToRead, out ulong zeroSectorsAmount) { // Attempt to read 10 more sectors than requested sectorsToRead = ((ulong)count / (ulong)_opticalDisc.BytesPerSector) + 10; zeroSectorsAmount = 0; // Avoid overreads by padding with 0-byte data at the end if(_opticalDisc.CurrentSector + sectorsToRead > _opticalDisc.TotalSectors) { ulong oldSectorsToRead = sectorsToRead; sectorsToRead = _opticalDisc.TotalSectors - _opticalDisc.CurrentSector; int tempZeroSectorCount = (int)(oldSectorsToRead - sectorsToRead); zeroSectorsAmount = (ulong)(tempZeroSectorCount < 0 ? 0 : tempZeroSectorCount); } } /// /// Read the requested amount of data from an input /// /// Number of bytes to load /// Number of sectors to read /// Number of zeroed sectors to concatenate /// The requested amount of data, if possible private byte[] ReadData(int count, ulong sectorsToRead, ulong zeroSectorsAmount) { // Create padding data for overreads byte[] zeroSectors = new byte[(int)zeroSectorsAmount * _opticalDisc.BytesPerSector]; byte[] audioData; // Attempt to read the required number of sectors var readSectorTask = Task.Run(() => { lock(_readingImage) { for(int i = 0; i < 4; i++) { try { return _opticalDisc.ReadSectors((uint)sectorsToRead).Concat(zeroSectors).ToArray(); } catch { } } return zeroSectors; } }); // Wait 100ms at longest for the read to occur if(readSectorTask.Wait(TimeSpan.FromMilliseconds(100))) audioData = readSectorTask.Result; else return null; // Load only the requested audio segment byte[] audioDataSegment = new byte[count]; int copyAmount = Math.Min(count, audioData.Length - _currentSectorReadPosition); if(Math.Max(0, copyAmount) == 0) return null; Array.Copy(audioData, _currentSectorReadPosition, audioDataSegment, 0, copyAmount); // Apply de-emphasis filtering, only if enabled if(ApplyDeEmphasis) _filterStage.ProcessAudioData(audioDataSegment); return audioDataSegment; } /// /// Sets or resets the audio playback objects /// private void SetupAudio() { if(_source == null) { _source = new PlayerSource(ProviderRead); _soundOut = new ALSoundOut(100); _soundOut.Initialize(_source); } else { _soundOut.Stop(); } } #endregion } }