//****************************************************************************************************** // WavInputAdapter.cs - Gbtc // // Copyright © 2012, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 06/29/2011 - Stephen C. Wills // Generated original version of source code. // 07/01/2011 - Stephen C. Wills // Modified WaveData dependency to refer to GSF.Media and moved the library // into TimeSeriesFramework. // 12/13/2012 - Starlynn Danyelle Gilliam // Modified Header. // 05/21/2015 - J. Ritchie Carroll // Added ability to cache WAV file into memory for tests with disk I/O limitations // //****************************************************************************************************** using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Threading; using GSF; using GSF.Diagnostics; using GSF.IO; using GSF.Media; using GSF.TimeSeries; using GSF.TimeSeries.Adapters; namespace WavInputAdapter { /// /// Represents an input adapter that reads measurements from a WAV file. /// [Description("WAV: Reads measurements from a WAV file")] [EditorBrowsable(EditorBrowsableState.Advanced)] // Normally defined as an input device protocol public class WavInputAdapter : InputAdapterBase { #region [ Members ] // Constants private const double DefaultRecoveryDelay = 5.0D; private const long GapThreshold = Ticks.PerSecond; // Fields private WaveDataReader m_dataReader; private BlockAllocatedMemoryStream m_dataCache; private long m_dataIndex; private int m_channels; private int m_sampleRate; private TimeSpan m_audioLength; private long m_startTime; private bool m_disposed; #endregion #region [ Properties ] /// /// Gets or sets the name of the file from which to read measurements. /// [ConnectionStringParameter, Description("The name of the file from which to read measurements.")] public string WavFileName { get; set; } /// /// Gets or sets the amount of time, in seconds, needed to recover from a back log. /// [ConnectionStringParameter, Description("The amount of time, in seconds, needed to recover from a back log."), DefaultValue(DefaultRecoveryDelay)] public double RecoveryDelay { get; set; } /// /// Gets or sets flag that determines if WAV file should be cached in memory. /// /// /// Useful for situations where disk I/O can be a bottleneck. /// [ConnectionStringParameter, Description("Flag that determines if WAV file should be cached in memory. This can be useful for situations where disk I/O can be a bottleneck. Note that this will occupy RAM for entire file size - be cautious."), DefaultValue(false)] public bool MemoryCache { get; set; } /// /// Gets or sets the output measurements. /// /// /// The output measurements are like templates. New measurements generated /// by this adapter will be copies of these output measurements, in which /// only the values and timestamps will differ. /// [ConnectionStringParameter, DefaultValue(null), Description("Defines primary keys of output measurements the adapter expects; can be one of a filter expression, measurement key, point tag or Guid."), CustomConfigurationEditor("GSF.TimeSeries.UI.WPF.dll", "GSF.TimeSeries.UI.Editors.MeasurementEditor")] public override IMeasurement[] OutputMeasurements { get { return base.OutputMeasurements; } set { base.OutputMeasurements = value.OrderBy(measurement => measurement.Key.ToString()).ToArray(); } } /// /// Gets flag that determines if the data input connects asynchronously. /// /// /// This returns false because this adapter does not connect asynchronously. /// protected override bool UseAsyncConnect => false; /// /// Gets the flag indicating if this adapter supports temporal processing. /// public override bool SupportsTemporalProcessing => true; #endregion #region [ Methods ] /// /// Initializes the connection string parameters of this . /// public override void Initialize() { base.Initialize(); Dictionary settings = Settings; string setting; if (settings.TryGetValue("recoveryDelay", out setting)) RecoveryDelay = double.Parse(setting); else RecoveryDelay = DefaultRecoveryDelay; if (!settings.TryGetValue("wavFileName", out setting)) throw new ArgumentException("wavFileName is missing from settings - Example: wavFileName=Bohemian Rhapsody.wav"); WavFileName = setting; if (settings.TryGetValue("memoryCache", out setting)) MemoryCache = setting.ParseBoolean(); // Attempt to parse WAV file info during initialization, if this fails - no need to load adapter WaveFile fileInfo = WaveFile.Load(WavFileName, false); m_channels = fileInfo.Channels; m_sampleRate = fileInfo.SampleRate; m_audioLength = fileInfo.AudioLength; if (m_channels > OutputMeasurements.Length) throw new ArgumentException($"Not enough output measurements ({OutputMeasurements.Length}) defined for the number of channels in the WAV file ({m_channels})"); OnStatusMessage(MessageLevel.Info, $"Ready to play \"{Path.GetFileName(WavFileName)}\" with {m_channels} channels..."); } /// /// Attempts to open the file to start getting wave data. /// protected override void AttemptConnection() { m_dataReader = OpenWaveDataReader(); m_dataIndex = 0; m_startTime = DateTime.UtcNow.Ticks; new Thread(ProcessMeasurements) { IsBackground = true }.Start(); } /// /// Attempts to close the file and release resources held by the adapter. /// protected override void AttemptDisconnection() { if ((object)m_dataReader != null) { m_dataReader.Close(); m_dataReader.Dispose(); m_dataReader = null; } } /// /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected override void Dispose(bool disposing) { if (!m_disposed) { try { if (disposing) { if ((object)m_dataReader != null) { m_dataReader.Close(); m_dataReader.Dispose(); m_dataReader = null; } if ((object)m_dataCache != null) m_dataCache.Dispose(); } } finally { m_disposed = true; // Prevent duplicate dispose. base.Dispose(disposing); // Call base class Dispose(). } } } /// /// Gets a short one-line status of this . /// /// Maximum number of available characters for display. /// A short one-line summary of the current status of this . public override string GetShortStatus(int maxLength) { if (!Enabled || !IsConnected) return $"Streaming for \"{Path.GetFileName(WavFileName)}\" is paused...".CenterText(maxLength); TimeSpan time = Ticks.FromSeconds(m_dataIndex / (double)m_sampleRate); return $"Streaming \"{Path.GetFileName(WavFileName)}\" at time {time.ToString(@"m\:ss")} / {m_audioLength.ToString(@"m\:ss")} - {time.TotalSeconds / m_audioLength.TotalSeconds:0.00%}.".CenterText(maxLength); } private void ProcessMeasurements() { List measurements = new List((int)(Ticks.ToSeconds(GapThreshold) * m_sampleRate * m_channels * 1.1D)); LittleBinaryValue[] sample; while (Enabled) { try { SpinWait spinner = new SpinWait(); // Determine what time it is now long now = DateTime.UtcNow.Ticks; // Assign a timestamp to the next sample based on its location // in the file relative to the other samples in the file long timestamp = m_startTime + (m_dataIndex * Ticks.PerSecond / m_sampleRate); if (now - timestamp > GapThreshold) { // Reset the start time and delay next transmission in an attempt to catch up m_startTime = now - (m_dataIndex * Ticks.PerSecond / m_sampleRate) + Ticks.FromSeconds(RecoveryDelay); timestamp = now; OnStatusMessage(MessageLevel.Info, "Start time reset."); } // Keep generating measurements until // we catch up to the current time. while (timestamp < now) { sample = m_dataReader.GetNextSample(); // If the sample is null, we've reached the end of the file - close and reopen, // resetting the data index and start time if (sample == null) { m_dataReader.Close(); m_dataReader.Dispose(); m_dataReader = OpenWaveDataReader(); m_dataIndex = 0; m_startTime = timestamp; sample = m_dataReader.GetNextSample(); } // Create new measurements, one for each channel, and add them to the measurements list for (int i = 0; i < m_channels; i++) measurements.Add(Measurement.Clone(OutputMeasurements[i], sample[i].ConvertToType(TypeCode.Double), timestamp)); // Update the data index and recalculate the assigned timestamp for the next sample m_dataIndex++; timestamp = m_startTime + (m_dataIndex * Ticks.PerSecond / m_sampleRate); } OnNewMeasurements(measurements); measurements.Clear(); while (DateTime.UtcNow.Ticks - timestamp <= GapThreshold / 100) { // Ahead of schedule -- pause for a moment spinner.SpinOnce(); } } catch (Exception ex) { OnProcessException(MessageLevel.Warning, ex); } } } // Open wave reader - either from memory loaded WAV or directly from disk private WaveDataReader OpenWaveDataReader() { if (MemoryCache) { if ((object)m_dataCache == null) { m_dataCache = new BlockAllocatedMemoryStream(); using (FileStream stream = File.OpenRead(WavFileName)) stream.CopyTo(m_dataCache); } m_dataCache.Position = 0; return WaveDataReader.FromStream(m_dataCache); } return WaveDataReader.FromFile(WavFileName); } #endregion } }