using System; using System.Linq; using System.Threading.Tasks; using CSCore.SoundOut; using NWaves.Audio; using NWaves.Filters.BiQuad; using ReactiveUI; using RedBookPlayer.Common.Discs; namespace RedBookPlayer.Common.Hardware { public class SoundOutput : ReactiveObject { #region Public Fields /// /// Indicate if the output is ready to be used /// public bool Initialized { get; private set; } = false; /// /// Indicate if the output is playing /// public bool Playing { get => _playing; private set => this.RaiseAndSetIfChanged(ref _playing, 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 _playing; 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 OpticalDisc _opticalDisc; /// /// Data provider for sound output /// private PlayerSource _source; /// /// Sound output instance /// private ALSoundOut _soundOut; /// /// Left channel de-emphasis filter /// private BiQuadFilter _deEmphasisFilterLeft; /// /// Right channel de-emphasis filter /// private BiQuadFilter _deEmphasisFilterRight; /// /// Current position in the sector /// private int _currentSectorReadPosition = 0; /// /// Lock object for reading track data /// private readonly object _readingImage = new object(); #endregion /// /// Initialize the output with a given image /// /// OpticalDisc to load from /// True if playback should begin immediately, false otherwise /// Default volume between 0 and 100 to use when starting playback public void Init(OpticalDisc opticalDisc, bool autoPlay = false, int defaultVolume = 100) { // If we have an unusable disc, just return if(opticalDisc == null || !opticalDisc.Initialized) return; // Save a reference to the disc _opticalDisc = opticalDisc; // Set the initial playback volume Volume = defaultVolume; // Enable de-emphasis for CDs, if necessary if(opticalDisc is CompactDisc compactDisc) ApplyDeEmphasis = compactDisc.TrackHasEmphasis; // Setup de-emphasis filters SetupFilters(); // Setup the audio output SetupAudio(); // Initialize playback, if necessary if(autoPlay) _soundOut.Play(); // Mark the output as ready Initialized = true; // Begin loading data _source.Start(); } /// /// 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 ulong sectorsToRead; ulong zeroSectorsAmount; do { // Attempt to read 2 more sectors than requested sectorsToRead = ((ulong)count / (ulong)_opticalDisc.BytesPerSector) + 2; 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); } // TODO: Figure out when this value could be negative if(sectorsToRead <= 0) { _opticalDisc.LoadFirstTrack(); _currentSectorReadPosition = 0; } } while(sectorsToRead <= 0); // 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(ArgumentOutOfRangeException) { _opticalDisc.LoadFirstTrack(); } } return zeroSectors; } }); // Wait 100ms at longest for the read to occur if(readSectorTask.Wait(TimeSpan.FromMilliseconds(100))) { audioData = readSectorTask.Result; } else { Array.Clear(buffer, offset, count); return count; } // 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) { Array.Clear(buffer, offset, count); return count; } Array.Copy(audioData, _currentSectorReadPosition, audioDataSegment, 0, copyAmount); // Apply de-emphasis filtering, only if enabled if(ApplyDeEmphasis) { float[][] floatAudioData = new float[2][]; floatAudioData[0] = new float[audioDataSegment.Length / 4]; floatAudioData[1] = new float[audioDataSegment.Length / 4]; ByteConverter.ToFloats16Bit(audioDataSegment, floatAudioData); for(int i = 0; i < floatAudioData[0].Length; i++) { floatAudioData[0][i] = _deEmphasisFilterLeft.Process(floatAudioData[0][i]); floatAudioData[1][i] = _deEmphasisFilterRight.Process(floatAudioData[1][i]); } ByteConverter.FromFloats16Bit(floatAudioData, audioDataSegment); } // 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) { _opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector + (ulong)(_currentSectorReadPosition / _opticalDisc.BytesPerSector)); _currentSectorReadPosition %= _opticalDisc.BytesPerSector; } return count; } #region Playback /// /// Start audio playback /// public void Play() { if (_soundOut.PlaybackState != PlaybackState.Playing) _soundOut.Play(); Playing = _soundOut.PlaybackState == PlaybackState.Playing; } /// /// Pause audio playback /// public void Pause() { if(_soundOut.PlaybackState != PlaybackState.Paused) _soundOut.Pause(); Playing = _soundOut.PlaybackState == PlaybackState.Playing; } /// /// Stop audio playback /// public void Stop() { if(_soundOut.PlaybackState != PlaybackState.Stopped) _soundOut.Stop(); Playing = _soundOut.PlaybackState == PlaybackState.Playing; } #endregion #region Helpers /// /// Set de-emphasis status /// /// public void SetDeEmphasis(bool apply) => ApplyDeEmphasis = apply; /// /// Set the value for the volume /// /// New volume value public void SetVolume(int volume) => Volume = volume; /// /// Sets or resets the de-emphasis filters /// private void SetupFilters() { if(_deEmphasisFilterLeft == null) { _deEmphasisFilterLeft = new DeEmphasisFilter(); _deEmphasisFilterRight = new DeEmphasisFilter(); } else { _deEmphasisFilterLeft.Reset(); _deEmphasisFilterRight.Reset(); } } /// /// 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 } }