diff --git a/libse/WaveToVisualizer.cs b/libse/WaveToVisualizer.cs index 292312d8d..a634e1047 100644 --- a/libse/WaveToVisualizer.cs +++ b/libse/WaveToVisualizer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Drawing; using System.Globalization; using System.IO; +using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml; @@ -141,12 +142,13 @@ namespace Nikse.SubtitleEdit.Core } } - internal static void WriteHeader(Stream toStream, int sampleRate, int numberOfChannels, int bitsPerSample, int dataSize) + internal static void WriteHeader(Stream toStream, int sampleRate, int numberOfChannels, int bitsPerSample, int sampleCount) { const int headerSize = 44; int bytesPerSample = (bitsPerSample + 7) / 8; int blockAlign = numberOfChannels * bytesPerSample; int byteRate = sampleRate * blockAlign; + int dataSize = sampleCount * bytesPerSample * numberOfChannels; byte[] header = new byte[headerSize]; WriteStringToByteArray(header, 0, "RIFF"); WriteInt32ToByteArray(header, 4, headerSize + dataSize - 8); // size of RIFF chunk's data @@ -183,47 +185,168 @@ namespace Nikse.SubtitleEdit.Core } } + public struct WavePeak + { + public readonly short Max; + public readonly short Min; + + public WavePeak(short max, short min) + { + Max = max; + Min = min; + } + + public int Abs + { + get { return Math.Max(Math.Abs((int)Max), Math.Abs((int)Min)); } + } + } + + public class WavePeakData + { + public WavePeakData(int sampleRate, IList peaks) + { + SampleRate = sampleRate; + LengthInSeconds = (double)peaks.Count / sampleRate; + Peaks = peaks; + CalculateHighestPeak(); + } + + public int SampleRate { get; private set; } + + public double LengthInSeconds { get; private set; } + + public IList Peaks { get; private set; } + + public int HighestPeak { get; private set; } + + private void CalculateHighestPeak() + { + HighestPeak = 0; + foreach (var peak in Peaks) + { + int abs = peak.Abs; + if (abs > HighestPeak) + HighestPeak = abs; + } + } + + public static WavePeakData FromDisk(string peakFileName) + { + using (var peakGenerator = new WavePeakGenerator(peakFileName)) + { + return peakGenerator.LoadPeaks(); + } + } + } + + public class SpectrogramData : IDisposable + { + private string _loadFromDirectory; + + public SpectrogramData(int fftSize, int imageWidth, double sampleDuration, IList images) + { + FftSize = fftSize; + ImageWidth = imageWidth; + SampleDuration = sampleDuration; + Images = images; + } + + private SpectrogramData(string loadFromDirectory) + { + _loadFromDirectory = loadFromDirectory; + Images = new Bitmap[0]; + } + + public int FftSize { get; private set; } + + public int ImageWidth { get; private set; } + + public double SampleDuration { get; private set; } + + public IList Images { get; private set; } + + public bool IsLoaded + { + get { return _loadFromDirectory == null; } + } + + public void Load() + { + if (_loadFromDirectory == null) + return; + + string directory = _loadFromDirectory; + _loadFromDirectory = null; + + try + { + string xmlInfoFileName = Path.Combine(directory, "Info.xml"); + if (!File.Exists(xmlInfoFileName)) + return; + var doc = new XmlDocument(); + var culture = CultureInfo.InvariantCulture; + doc.Load(xmlInfoFileName); + FftSize = Convert.ToInt32(doc.DocumentElement.SelectSingleNode("NFFT").InnerText, culture); + ImageWidth = Convert.ToInt32(doc.DocumentElement.SelectSingleNode("ImageWidth").InnerText, culture); + SampleDuration = Convert.ToDouble(doc.DocumentElement.SelectSingleNode("SampleDuration").InnerText, culture); + + var images = new List(); + var fileNames = Enumerable.Range(0, int.MaxValue) + .Select(n => Path.Combine(directory, n + ".gif")) + .TakeWhile(p => File.Exists(p)); + foreach (string fileName in fileNames) + { + // important that this does not lock file (do NOT use Image.FromFile(fileName) or alike!!!) + using (var ms = new MemoryStream(File.ReadAllBytes(fileName))) + { + images.Add((Bitmap)Image.FromStream(ms)); + } + } + Images = images; + } + catch + { + } + } + + public void Dispose() + { + foreach (var image in Images) + { + try + { + image.Dispose(); + } + catch + { + } + } + Images = new Bitmap[0]; + } + + public static SpectrogramData FromDisk(string spectrogramDirectory) + { + return new SpectrogramData(spectrogramDirectory); + } + } + public class WavePeakGenerator : IDisposable { private Stream _stream; - private byte[] _data; + private WaveHeader _header; - private delegate int ReadSampleDataValueDelegate(ref int index); + private delegate int ReadSampleDataValue(byte[] data, ref int index); - public WaveHeader Header { get; private set; } - - /// - /// Lowest data value - /// - public int DataMinValue { get; private set; } - - /// - /// Highest data value - /// - public int DataMaxValue { get; private set; } - - /// - /// Number of peaks per second (should be divideable by SampleRate) - /// - public int PeaksPerSecond { get; private set; } - - /// - /// List of all peak samples (channels are merged) - /// - public List PeakSamples { get; private set; } - - /// - /// List of all samples (channels are merged) - /// - public List AllSamples { get; private set; } + private delegate void WriteSampleDataValue(byte[] buffer, int offset, int value); /// /// Constructor /// /// Wave file name public WavePeakGenerator(string fileName) + : this(File.OpenRead(fileName)) { - Initialize(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); } /// @@ -232,214 +355,252 @@ namespace Nikse.SubtitleEdit.Core /// Stream of a wave file public WavePeakGenerator(Stream stream) { - Initialize(stream); + _stream = stream; + _header = new WaveHeader(_stream); } /// - /// Returns true if the current wave file can be processed. Compressed wave files or 32-bit files are not supported + /// Returns true if the current wave file can be processed. Compressed wave files are not supported. /// public bool IsSupported { get { - return Header.AudioFormat == WaveHeader.AudioFormatPcm && Header.Format == "WAVE" && Header.BytesPerSample < 4; + return _header.AudioFormat == WaveHeader.AudioFormatPcm && _header.Format == "WAVE"; } } /// - /// Generate peaks (samples with some interval) for an uncompressed wave file + /// Generates peaks and saves them to disk. /// /// Delay in milliseconds (normally zero) - public void GeneratePeakSamples(int delayInMilliseconds) + /// Path of the output file + public WavePeakData GeneratePeaks(int delayInMilliseconds, string peakFileName) { - if (Header.BytesPerSample == 4) + int peaksPerSecond = Math.Min(Configuration.Settings.VideoControls.WaveformMinimumSampleRate, _header.SampleRate); + + // ensure that peaks per second is a factor of the sample rate + while (_header.SampleRate % peaksPerSecond != 0) + peaksPerSecond++; + + int delaySampleCount = (int)(_header.SampleRate * (delayInMilliseconds / TimeCode.BaseUnit)); + + // ignore negative delays for now (pretty sure it can't happen in mkv and some places pass in -1 by mistake) + delaySampleCount = Math.Max(delaySampleCount, 0); + + var peaks = new List(); + var readSampleDataValue = GetSampleDataReader(); + float sampleAndChannelScale = (float)GetSampleAndChannelScale(); + long fileSampleCount = _header.LengthInSamples; + long fileSampleOffset = -delaySampleCount; + int chunkSampleCount = _header.SampleRate / peaksPerSecond; + byte[] data = new byte[chunkSampleCount * _header.BlockAlign]; + float[] chunkSamples = new float[chunkSampleCount]; + + _stream.Seek(_header.DataStartPosition, SeekOrigin.Begin); + + // for negative delays, skip samples at the beginning + if (fileSampleOffset > 0) { - // Can't handle 32-bit samples due to the way the channel averaging is done - throw new Exception("32-bit samples are unsupported."); + _stream.Seek(fileSampleOffset * _header.BlockAlign, SeekOrigin.Current); } - PeaksPerSecond = Math.Min(Configuration.Settings.VideoControls.WaveformMinimumSampleRate, Header.SampleRate); - - // Ensure that peaks per second is a factor of the sample rate - while (Header.SampleRate % PeaksPerSecond != 0) - PeaksPerSecond++; - - ReadSampleDataValueDelegate readSampleDataValue = GetSampleDataReader(); - DataMinValue = int.MaxValue; - DataMaxValue = int.MinValue; - PeakSamples = new List(); - - if (delayInMilliseconds > 0) + while (fileSampleOffset < fileSampleCount) { - for (int i = 0; i < PeaksPerSecond * delayInMilliseconds / 1000; i++) - PeakSamples.Add(0); - } - - int bytesInterval = (int)Header.BytesPerSecond / PeaksPerSecond; - _data = new byte[Header.BytesPerSecond]; - _stream.Position = Header.DataStartPosition; - int bytesRead = _stream.Read(_data, 0, _data.Length); - while (bytesRead > 0) - { - for (int i = 0; i < bytesRead; i += bytesInterval) + // calculate how many samples to skip at the beginning (for positive delays) + int startSkipSampleCount = 0; + if (fileSampleOffset < 0) { - int index = i; - int value = 0; - for (int channelNumber = 0; channelNumber < Header.NumberOfChannels; channelNumber++) - { - value += readSampleDataValue.Invoke(ref index); - } - value /= Header.NumberOfChannels; - if (value < DataMinValue) - DataMinValue = value; - if (value > DataMaxValue) - DataMaxValue = value; - PeakSamples.Add(value); + startSkipSampleCount = (int)Math.Min(-fileSampleOffset, chunkSampleCount); + fileSampleOffset += startSkipSampleCount; } - bytesRead = _stream.Read(_data, 0, _data.Length); + + // calculate how many samples to read from the file + long fileSamplesRemaining = fileSampleCount - Math.Max(fileSampleOffset, 0); + int fileReadSampleCount = (int)Math.Min(fileSamplesRemaining, chunkSampleCount - startSkipSampleCount); + + // read samples from the file + if (fileReadSampleCount > 0) + { + int fileReadByteCount = fileReadSampleCount * _header.BlockAlign; + _stream.Read(data, 0, fileReadByteCount); + fileSampleOffset += fileReadSampleCount; + + int chunkSampleOffset = 0; + int dataByteOffset = 0; + while (dataByteOffset < fileReadByteCount) + { + float value = 0F; + for (int iChannel = 0; iChannel < _header.NumberOfChannels; iChannel++) + { + value += readSampleDataValue(data, ref dataByteOffset); + } + chunkSamples[chunkSampleOffset] = value * sampleAndChannelScale; + chunkSampleOffset += 1; + } + } + + // calculate peaks + peaks.Add(CalculatePeak(chunkSamples, fileReadSampleCount)); } + + // save results to file + using (var stream = File.Create(peakFileName)) + { + WaveHeader.WriteHeader(stream, peaksPerSecond, 2, 16, peaks.Count); + byte[] buffer = new byte[4]; + foreach (var peak in peaks) + { + WriteValue16Bit(buffer, 0, peak.Max); + WriteValue16Bit(buffer, 2, peak.Min); + stream.Write(buffer, 0, 4); + } + } + + return new WavePeakData(peaksPerSecond, peaks); } - public void GenerateAllSamples() + private static WavePeak CalculatePeak(float[] chunk, int count) { - if (Header.BytesPerSample == 4) - { - // Can't handle 32-bit samples due to the way the channel averaging is done - throw new Exception("32-bit samples are unsupported."); - } + if (count == 0) + return new WavePeak(); - // determine how to read sample values - ReadSampleDataValueDelegate readSampleDataValue = GetSampleDataReader(); + float max = chunk[0]; + float min = chunk[0]; + for (int i = 1; i < count; i++) + { + float value = chunk[i]; + if (value > max) + max = value; + if (value < min) + min = value; + } + return new WavePeak((short)(short.MaxValue * max), (short)(short.MaxValue * min)); + } + + /// + /// Loads previously generated peaks from disk. + /// + internal WavePeakData LoadPeaks() + { + if (_header.BitsPerSample != 16) + throw new Exception("Peaks file must be 16 bits per sample."); + + if (_header.NumberOfChannels != 1 && _header.NumberOfChannels != 2) + throw new Exception("Peaks file must have 1 or 2 channels."); // load data - _data = new byte[Header.DataChunkSize]; - _stream.Position = Header.DataStartPosition; - _stream.Read(_data, 0, _data.Length); + byte[] data = new byte[_header.DataChunkSize]; + _stream.Position = _header.DataStartPosition; + _stream.Read(data, 0, data.Length); - // read sample values - DataMinValue = int.MaxValue; - DataMaxValue = int.MinValue; - AllSamples = new List(); - int index = 0; - while (index < Header.DataChunkSize) + // read peak values + WavePeak[] peaks = new WavePeak[_header.LengthInSamples]; + int peakIndex = 0; + if (_header.NumberOfChannels == 2) { - int value = 0; - for (int channelNumber = 0; channelNumber < Header.NumberOfChannels; channelNumber++) + // max value in left channel, min value in right channel + int byteIndex = 0; + while (byteIndex < data.Length) { - value += readSampleDataValue.Invoke(ref index); + short max = (short)ReadValue16Bit(data, ref byteIndex); + short min = (short)ReadValue16Bit(data, ref byteIndex); + peaks[peakIndex++] = new WavePeak(max, min); } - value /= Header.NumberOfChannels; - if (value < DataMinValue) - DataMinValue = value; - if (value > DataMaxValue) - DataMaxValue = value; - AllSamples.Add(value); } - } - - public void WritePeakSamples(string fileName) - { - using (var fs = new FileStream(fileName, FileMode.Create, FileAccess.Write)) + else if (_header.NumberOfChannels == 1) { - WritePeakSamples(fs); + // single sample value (for backwards compatibility) + int byteIndex = 0; + while (byteIndex < data.Length) + { + short value = (short)ReadValue16Bit(data, ref byteIndex); + if (value == short.MinValue) + value = -short.MaxValue; + value = Math.Abs(value); + peaks[peakIndex++] = new WavePeak(value, (short)-value); + } } + + return new WavePeakData(_header.SampleRate, peaks); } - public void WritePeakSamples(Stream stream) + private static int ReadValue8Bit(byte[] data, ref int index) { - WaveHeader.WriteHeader(stream, PeaksPerSecond, 1, Header.BytesPerSample * 8, PeakSamples.Count * Header.BytesPerSample); - WritePeakData(stream); - stream.Flush(); - } - - private void WritePeakData(Stream stream) - { - var writeSample = GetSampleDataWriter(); - byte[] buffer = new byte[4]; - int bytesPerSample = Header.BytesPerSample; - foreach (var value in PeakSamples) - { - writeSample(buffer, value); - stream.Write(buffer, 0, bytesPerSample); - } - } - - private void Initialize(Stream stream) - { - _stream = stream; - Header = new WaveHeader(_stream); - } - - private int ReadValue8Bit(ref int index) - { - int result = sbyte.MinValue + _data[index]; + int result = sbyte.MinValue + data[index]; index += 1; return result; } - private int ReadValue16Bit(ref int index) + private static int ReadValue16Bit(byte[] data, ref int index) { int result = (short) - ((_data[index ] ) | - (_data[index + 1] << 8)); + ((data[index ] ) | + (data[index + 1] << 8)); index += 2; return result; } - private int ReadValue24Bit(ref int index) + private static int ReadValue24Bit(byte[] data, ref int index) { int result = - ((_data[index ] << 8) | - (_data[index + 1] << 16) | - (_data[index + 2] << 24)) >> 8; + ((data[index ] << 8) | + (data[index + 1] << 16) | + (data[index + 2] << 24)) >> 8; index += 3; return result; } - private int ReadValue32Bit(ref int index) + private static int ReadValue32Bit(byte[] data, ref int index) { int result = - (_data[index ] ) | - (_data[index + 1] << 8) | - (_data[index + 2] << 16) | - (_data[index + 3] << 24); + (data[index ] ) | + (data[index + 1] << 8) | + (data[index + 2] << 16) | + (data[index + 3] << 24); index += 4; return result; } - private void WriteValue8Bit(byte[] buffer, int value) + private static void WriteValue8Bit(byte[] buffer, int offset, int value) { - buffer[0] = (byte)(value - sbyte.MinValue); + buffer[offset] = (byte)(value - sbyte.MinValue); } - private void WriteValue16Bit(byte[] buffer, int value) + private static void WriteValue16Bit(byte[] buffer, int offset, int value) { - buffer[0] = (byte)value; - buffer[1] = (byte)(value >> 8); + buffer[offset ] = (byte)value; + buffer[offset + 1] = (byte)(value >> 8); } - private void WriteValue24Bit(byte[] buffer, int value) + private static void WriteValue24Bit(byte[] buffer, int offset, int value) { - buffer[0] = (byte)value; - buffer[1] = (byte)(value >> 8); - buffer[2] = (byte)(value >> 16); + buffer[offset ] = (byte)value; + buffer[offset + 1] = (byte)(value >> 8); + buffer[offset + 2] = (byte)(value >> 16); } - private void WriteValue32Bit(byte[] buffer, int value) + private static void WriteValue32Bit(byte[] buffer, int offset, int value) { - buffer[0] = (byte)value; - buffer[1] = (byte)(value >> 8); - buffer[2] = (byte)(value >> 16); - buffer[3] = (byte)(value >> 24); + buffer[offset ] = (byte)value; + buffer[offset + 1] = (byte)(value >> 8); + buffer[offset + 2] = (byte)(value >> 16); + buffer[offset + 3] = (byte)(value >> 24); } - /// - /// Determine how to read sample values - /// - /// Sample data reader that matches bits per sample - private ReadSampleDataValueDelegate GetSampleDataReader() + private double GetSampleScale() { - switch (Header.BytesPerSample) + return (1.0 / Math.Pow(2.0, _header.BytesPerSample * 8 - 1)); + } + + private double GetSampleAndChannelScale() + { + return GetSampleScale() / _header.NumberOfChannels; + } + + private ReadSampleDataValue GetSampleDataReader() + { + switch (_header.BytesPerSample) { case 1: return ReadValue8Bit; @@ -450,13 +611,13 @@ namespace Nikse.SubtitleEdit.Core case 4: return ReadValue32Bit; default: - throw new InvalidDataException("Cannot read bits per sample of " + Header.BitsPerSample); + throw new InvalidDataException("Cannot read bits per sample of " + _header.BitsPerSample); } } - private Action GetSampleDataWriter() + private WriteSampleDataValue GetSampleDataWriter() { - switch (Header.BytesPerSample) + switch (_header.BytesPerSample) { case 1: return WriteValue8Bit; @@ -467,7 +628,7 @@ namespace Nikse.SubtitleEdit.Core case 4: return WriteValue32Bit; default: - throw new InvalidDataException("Cannot write bits per sample of " + Header.BitsPerSample); + throw new InvalidDataException("Cannot write bits per sample of " + _header.BitsPerSample); } } @@ -484,42 +645,36 @@ namespace Nikse.SubtitleEdit.Core //////////////////////////////////////// SPECTRUM /////////////////////////////////////////////////////////// - public List GenerateFourierData(int nfft, string spectrogramDirectory, int delayInMilliseconds) + public SpectrogramData GenerateSpectrogram(int delayInMilliseconds, string spectrogramDirectory) { - if (Header.BytesPerSample == 4) - { - // Can't handle 32-bit samples due to the way the channel averaging is done - throw new Exception("32-bit samples are unsupported."); - } + const int fftSize = 256; // image height = fft size / 2 + const int imageWidth = 1024; - const int bitmapWidth = 1024; + int delaySampleCount = (int)(_header.SampleRate * (delayInMilliseconds / TimeCode.BaseUnit)); - List bitmaps = new List(); - SpectrogramDrawer drawer = new SpectrogramDrawer(nfft); - Task saveImageTask = null; - ReadSampleDataValueDelegate readSampleDataValue = GetSampleDataReader(); - double sampleScale = 1.0 / (Math.Pow(2.0, Header.BytesPerSample * 8 - 1) * Header.NumberOfChannels); - - int delaySampleCount = (int)(Header.SampleRate * (delayInMilliseconds / TimeCode.BaseUnit)); - - // other code (e.g. generating peaks) doesn't handle negative delays, so we'll do the same for now + // ignore negative delays for now (pretty sure it can't happen in mkv and some places pass in -1 by mistake) delaySampleCount = Math.Max(delaySampleCount, 0); - long fileSampleCount = Header.LengthInSamples; + var images = new List(); + var drawer = new SpectrogramDrawer(fftSize); + var readSampleDataValue = GetSampleDataReader(); + Task saveImageTask = null; + double sampleAndChannelScale = GetSampleAndChannelScale(); + long fileSampleCount = _header.LengthInSamples; long fileSampleOffset = -delaySampleCount; - int chunkSampleCount = nfft * bitmapWidth; + int chunkSampleCount = fftSize * imageWidth; int chunkCount = (int)Math.Ceiling((double)(fileSampleCount + delaySampleCount) / chunkSampleCount); + byte[] data = new byte[chunkSampleCount * _header.BlockAlign]; double[] chunkSamples = new double[chunkSampleCount]; Directory.CreateDirectory(spectrogramDirectory); - _data = new byte[chunkSampleCount * Header.BlockAlign]; - _stream.Seek(Header.DataStartPosition, SeekOrigin.Begin); + _stream.Seek(_header.DataStartPosition, SeekOrigin.Begin); // for negative delays, skip samples at the beginning if (fileSampleOffset > 0) { - _stream.Seek(fileSampleOffset * Header.BlockAlign, SeekOrigin.Current); + _stream.Seek(fileSampleOffset * _header.BlockAlign, SeekOrigin.Current); } for (int iChunk = 0; iChunk < chunkCount; iChunk++) @@ -551,19 +706,19 @@ namespace Nikse.SubtitleEdit.Core // read samples from the file if (fileReadSampleCount > 0) { - int fileReadByteCount = fileReadSampleCount * Header.BlockAlign; - _stream.Read(_data, 0, fileReadByteCount); + int fileReadByteCount = fileReadSampleCount * _header.BlockAlign; + _stream.Read(data, 0, fileReadByteCount); fileSampleOffset += fileReadSampleCount; int dataByteOffset = 0; while (dataByteOffset < fileReadByteCount) { - int value = 0; - for (int iChannel = 0; iChannel < Header.NumberOfChannels; iChannel++) + double value = 0D; + for (int iChannel = 0; iChannel < _header.NumberOfChannels; iChannel++) { - value += readSampleDataValue(ref dataByteOffset); + value += readSampleDataValue(data, ref dataByteOffset); } - chunkSamples[chunkSampleOffset] = value * sampleScale; + chunkSamples[chunkSampleOffset] = value * sampleAndChannelScale; chunkSampleOffset += 1; } } @@ -577,7 +732,7 @@ namespace Nikse.SubtitleEdit.Core // generate spectrogram for this chunk Bitmap bmp = drawer.Draw(chunkSamples); - bitmaps.Add(bmp); + images.Add(bmp); // wait for previous image to finish saving if (saveImageTask != null) @@ -597,14 +752,15 @@ namespace Nikse.SubtitleEdit.Core var doc = new XmlDocument(); var culture = CultureInfo.InvariantCulture; + double sampleDuration = (double)fftSize / _header.SampleRate; doc.LoadXml(""); - doc.DocumentElement.SelectSingleNode("SampleDuration").InnerText = ((double)nfft / Header.SampleRate).ToString(culture); - doc.DocumentElement.SelectSingleNode("NFFT").InnerText = nfft.ToString(culture); - doc.DocumentElement.SelectSingleNode("ImageWidth").InnerText = bitmapWidth.ToString(culture); - doc.DocumentElement.SelectSingleNode("SecondsPerImage").InnerText = ((double)chunkSampleCount / Header.SampleRate).ToString(culture); // currently unused; for backwards compatibility + doc.DocumentElement.SelectSingleNode("SampleDuration").InnerText = sampleDuration.ToString(culture); + doc.DocumentElement.SelectSingleNode("NFFT").InnerText = fftSize.ToString(culture); + doc.DocumentElement.SelectSingleNode("ImageWidth").InnerText = imageWidth.ToString(culture); + doc.DocumentElement.SelectSingleNode("SecondsPerImage").InnerText = ((double)chunkSampleCount / _header.SampleRate).ToString(culture); // currently unused; for backwards compatibility doc.Save(Path.Combine(spectrogramDirectory, "Info.xml")); - return bitmaps; + return new SpectrogramData(fftSize, imageWidth, sampleDuration, images); } private class SpectrogramDrawer diff --git a/src/Controls/AudioVisualizer.cs b/src/Controls/AudioVisualizer.cs index 4501aeac1..da16d998a 100644 --- a/src/Controls/AudioVisualizer.cs +++ b/src/Controls/AudioVisualizer.cs @@ -4,9 +4,8 @@ using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Globalization; -using System.IO; +using System.Threading.Tasks; using System.Windows.Forms; -using System.Xml; namespace Nikse.SubtitleEdit.Controls { @@ -78,17 +77,13 @@ namespace Nikse.SubtitleEdit.Controls private Paragraph _nextParagraph; private bool _firstMove = true; private double _currentVideoPositionSeconds = -1; - private WavePeakGenerator _wavePeaks; + private WavePeakData _wavePeaks; private Subtitle _subtitle; private bool _noClear; private double _gapAtStart = -1; - private List _spectrogramBitmaps = new List(); - private string _spectrogramDirectory; + private SpectrogramData _spectrogram; private const int SpectrogramDisplayHeight = 128; - private double _sampleDuration; - private double _imageWidth; - private int _nfft; public delegate void ParagraphEventHandler(object sender, ParagraphEventArgs e); public event ParagraphEventHandler OnNewSelectionRightClicked; @@ -107,7 +102,6 @@ namespace Nikse.SubtitleEdit.Controls private double _wholeParagraphMinMilliseconds; private double _wholeParagraphMaxMilliseconds = double.MaxValue; - private System.ComponentModel.BackgroundWorker _spectrogramBackgroundWorker; public Keys InsertAtVideoPositionShortcut = Keys.None; public bool MouseWheelScrollUpIsForward = true; @@ -136,8 +130,8 @@ namespace Nikse.SubtitleEdit.Controls } public const double VerticalZoomMinimum = 1.0; - public const double VerticalZoomMaximum = 40.0; - private double _verticalZoomFactor = 2.0; // 1.0=no zoom + public const double VerticalZoomMaximum = 20.0; + private double _verticalZoomFactor = 1.0; // 1.0=no zoom public double VerticalZoomFactor { get @@ -186,7 +180,7 @@ namespace Nikse.SubtitleEdit.Controls { get { - return _spectrogramBitmaps != null && _spectrogramBitmaps.Count > 0; + return _spectrogram != null && _spectrogram.Images.Count > 0; } } @@ -208,7 +202,6 @@ namespace Nikse.SubtitleEdit.Controls } public bool AllowOverlap { get; set; } - private bool _tempShowSpectrogram; private bool _showWaveform; public bool ShowWaveform @@ -238,9 +231,9 @@ namespace Nikse.SubtitleEdit.Controls { if (_wavePeaks != null) { - double endPositionSeconds = value + ((double)Width / _wavePeaks.Header.SampleRate) / _zoomFactor; - if (endPositionSeconds > _wavePeaks.Header.LengthInSeconds) - value -= endPositionSeconds - _wavePeaks.Header.LengthInSeconds; + double endPositionSeconds = value + ((double)Width / _wavePeaks.SampleRate) / _zoomFactor; + if (endPositionSeconds > _wavePeaks.LengthInSeconds) + value -= endPositionSeconds - _wavePeaks.LengthInSeconds; } if (value < 0) value = 0; @@ -275,11 +268,11 @@ namespace Nikse.SubtitleEdit.Controls { get { - return XPositionToSeconds(Width); + return RelativeXPositionToSeconds(Width); } } - public WavePeakGenerator WavePeaks + public WavePeakData WavePeaks { get { @@ -304,23 +297,12 @@ namespace Nikse.SubtitleEdit.Controls } } - public void ResetSpectrogram() + public SpectrogramData Spectrogram { - if (_spectrogramBitmaps != null) + set { - for (int i = 0; i < _spectrogramBitmaps.Count; i++) - { - try - { - Bitmap bmp = _spectrogramBitmaps[i]; - bmp.Dispose(); - } - catch - { - } - } + InitializeSpectrogram(value); } - _spectrogramBitmaps = new List(); } public void ClearSelection() @@ -404,23 +386,20 @@ namespace Nikse.SubtitleEdit.Controls Invalidate(); } - private static int CalculateHeight(double value, int imageHeight, int maxHeight) - { - double percentage = value / maxHeight; - var result = (int)Math.Round((percentage / 2.0 + 0.5) * imageHeight); - return imageHeight - result; - } - private class IsSelectedHelper { private readonly List _ranges = new List(); private int _lastPosition = int.MaxValue; private SelectionRange _nextSelection; - public IsSelectedHelper(IEnumerable paragraphs, Func secondsToPosition) + public IsSelectedHelper(IEnumerable paragraphs, int sampleRate) { foreach (Paragraph p in paragraphs) - _ranges.Add(new SelectionRange(secondsToPosition(p.StartTime.TotalSeconds), secondsToPosition(p.EndTime.TotalSeconds))); + { + int start = (int)Math.Round(p.StartTime.TotalSeconds * sampleRate); + int end = (int)Math.Round(p.EndTime.TotalSeconds * sampleRate); + _ranges.Add(new SelectionRange(start, end)); + } } public bool IsSelected(int position) @@ -459,7 +438,7 @@ namespace Nikse.SubtitleEdit.Controls internal void WaveformPaint(object sender, PaintEventArgs e) { Graphics graphics = e.Graphics; - if (_wavePeaks != null && _wavePeaks.AllSamples != null) + if (_wavePeaks != null) { bool showSpectrogram = IsSpectrogramAvailable && ShowSpectrogram; bool showSpectrogramOnly = showSpectrogram && !ShowWaveform; @@ -477,7 +456,7 @@ namespace Nikse.SubtitleEdit.Controls // spectrogram if (showSpectrogram) { - DrawSpectrogramBitmap(StartPositionSeconds, graphics); + DrawSpectrogram(graphics); } // waveform @@ -486,23 +465,35 @@ namespace Nikse.SubtitleEdit.Controls using (var penNormal = new Pen(Color)) using (var penSelected = new Pen(SelectedColor)) // selected paragraph { - var pen = penNormal; - var isSelectedHelper = new IsSelectedHelper(_allSelectedParagraphs, SecondsToXPositionNoZoom); - int maxHeight = (int)(Math.Max(Math.Abs(_wavePeaks.DataMinValue), Math.Abs(_wavePeaks.DataMaxValue)) / VerticalZoomFactor); - int start = SecondsToXPositionNoZoom(StartPositionSeconds); - float xPrev = 0; - int yPrev = Height / 2; - float x = 0; - int y; - for (int i = 0; i < _wavePeaks.AllSamples.Count - start && x < Width; i++) + var isSelectedHelper = new IsSelectedHelper(_allSelectedParagraphs, _wavePeaks.SampleRate); + int baseHeight = (int)(_wavePeaks.HighestPeak / VerticalZoomFactor); + int halfWaveformHeight = waveformHeight / 2; + Func calculateY = (value) => { - int n = start + i; - x = (float)(_zoomFactor * i); - y = CalculateHeight(_wavePeaks.AllSamples[n], waveformHeight, maxHeight); - graphics.DrawLine(pen, xPrev, yPrev, x, y); - xPrev = x; - yPrev = y; - pen = isSelectedHelper.IsSelected(n) ? penSelected : penNormal; + float offset = (value / baseHeight) * halfWaveformHeight; + if (offset > halfWaveformHeight) + offset = halfWaveformHeight; + if (offset < -halfWaveformHeight) + offset = -halfWaveformHeight; + return halfWaveformHeight - offset; + }; + for (int x = 0; x < Width; x++) + { + float pos = (float)RelativeXPositionToSeconds(x) * _wavePeaks.SampleRate; + int pos0 = (int)pos; + int pos1 = pos0 + 1; + if (pos1 >= _wavePeaks.Peaks.Count) + break; + float pos1Weight = pos - pos0; + float pos0Weight = 1F - pos1Weight; + var peak0 = _wavePeaks.Peaks[pos0]; + var peak1 = _wavePeaks.Peaks[pos1]; + float max = peak0.Max * pos0Weight + peak1.Max * pos1Weight; + float min = peak0.Min * pos0Weight + peak1.Min * pos1Weight; + float yMax = calculateY(max); + float yMin = Math.Max(calculateY(min), yMax + 0.1F); + var pen = isSelectedHelper.IsSelected(pos0) ? penSelected : penNormal; + graphics.DrawLine(pen, x, yMax, x, yMin); } } } @@ -561,7 +552,7 @@ namespace Nikse.SubtitleEdit.Controls if (currentRegionWidth > 40) { using (var brush = new SolidBrush(Color.Turquoise)) - graphics.DrawString(string.Format("{0:0.###} {1}", ((double)currentRegionWidth / _wavePeaks.Header.SampleRate / _zoomFactor), Configuration.Settings.Language.Waveform.Seconds), Font, brush, new PointF(currentRegionLeft + 3, Height - 32)); + graphics.DrawString(string.Format("{0:0.###} {1}", ((double)currentRegionWidth / _wavePeaks.SampleRate / _zoomFactor), Configuration.Settings.Language.Waveform.Seconds), Font, brush, new PointF(currentRegionLeft + 3, Height - 32)); } } } @@ -617,8 +608,8 @@ namespace Nikse.SubtitleEdit.Controls else { double interval = ZoomFactor >= 0.4 ? - 0.1 * _wavePeaks.Header.SampleRate * _zoomFactor : // a pixel is 0.1 second - 1.0 * _wavePeaks.Header.SampleRate * _zoomFactor; // a pixel is 1.0 second + 0.1 * _wavePeaks.SampleRate * _zoomFactor : // a pixel is 0.1 second + 1.0 * _wavePeaks.SampleRate * _zoomFactor; // a pixel is 1.0 second using (var pen = new Pen(new SolidBrush(GridColor))) { for (double i = SecondsToXPosition(StartPositionSeconds) % ((int)Math.Round(interval)); i < Width; i += interval) @@ -638,14 +629,14 @@ namespace Nikse.SubtitleEdit.Controls private void DrawTimeLine(Graphics graphics, int imageHeight) { double seconds = Math.Ceiling(StartPositionSeconds) - StartPositionSeconds; - float position = SecondsToXPosition(seconds); + int position = SecondsToXPosition(seconds); using (var pen = new Pen(TextColor)) using (var textBrush = new SolidBrush(TextColor)) using (var textFont = new Font(Font.FontFamily, 7)) { while (position < Width) { - var n = _zoomFactor * _wavePeaks.Header.SampleRate; + var n = _zoomFactor * _wavePeaks.SampleRate; if (n > 38 || (int)Math.Round(StartPositionSeconds + seconds) % 5 == 0) { graphics.DrawLine(pen, position, imageHeight, position, imageHeight - 10); @@ -707,7 +698,7 @@ namespace Nikse.SubtitleEdit.Controls }; const int padding = 3; - double n = _zoomFactor * _wavePeaks.Header.SampleRate; + double n = _zoomFactor * _wavePeaks.SampleRate; // paragraph text if (n > 80) @@ -733,19 +724,24 @@ namespace Nikse.SubtitleEdit.Controls } } - private double XPositionToSeconds(double x) + private double RelativeXPositionToSeconds(int x) { - return StartPositionSeconds + (x / _wavePeaks.Header.SampleRate) / _zoomFactor; + return StartPositionSeconds + ((double)x / _wavePeaks.SampleRate) / _zoomFactor; } private int SecondsToXPosition(double seconds) { - return (int)Math.Round(seconds * _wavePeaks.Header.SampleRate * _zoomFactor); + return (int)Math.Round(seconds * _wavePeaks.SampleRate * _zoomFactor); } - private int SecondsToXPositionNoZoom(double seconds) + private int SecondsToSampleIndex(double seconds) { - return (int)Math.Round(seconds * _wavePeaks.Header.SampleRate); + return (int)Math.Round(seconds * _wavePeaks.SampleRate); + } + + private double SampleIndexToSeconds(int index) + { + return (double)index / _wavePeaks.SampleRate; } private void WaveformMouseDown(object sender, MouseEventArgs e) @@ -762,7 +758,7 @@ namespace Nikse.SubtitleEdit.Controls _buttonDownTimeTicks = DateTime.Now.Ticks; Cursor = Cursors.VSplit; - double seconds = XPositionToSeconds(e.X); + double seconds = RelativeXPositionToSeconds(e.X); var milliseconds = (int)(seconds * TimeCode.BaseUnit); if (SetParagrapBorderHit(milliseconds, NewSelectionParagraph)) @@ -830,7 +826,7 @@ namespace Nikse.SubtitleEdit.Controls _mouseDownParagraph = p; oldMouseDownParagraph = new Paragraph(_mouseDownParagraph); _mouseDownParagraphType = MouseDownParagraphType.Whole; - _moveWholeStartDifferenceMilliseconds = (XPositionToSeconds(e.X) * TimeCode.BaseUnit) - p.StartTime.TotalMilliseconds; + _moveWholeStartDifferenceMilliseconds = (RelativeXPositionToSeconds(e.X) * TimeCode.BaseUnit) - p.StartTime.TotalMilliseconds; Cursor = Cursors.Hand; SetMinAndMax(); } @@ -872,13 +868,11 @@ namespace Nikse.SubtitleEdit.Controls { if (e.Button == MouseButtons.Right) { - double seconds = XPositionToSeconds(e.X); + double seconds = RelativeXPositionToSeconds(e.X); var milliseconds = (int)(seconds * TimeCode.BaseUnit); - double currentRegionLeft = Math.Min(_mouseMoveStartX, _mouseMoveEndX); - double currentRegionRight = Math.Max(_mouseMoveStartX, _mouseMoveEndX); - currentRegionLeft = XPositionToSeconds(currentRegionLeft); - currentRegionRight = XPositionToSeconds(currentRegionRight); + double currentRegionLeft = RelativeXPositionToSeconds(Math.Min(_mouseMoveStartX, _mouseMoveEndX)); + double currentRegionRight = RelativeXPositionToSeconds(Math.Max(_mouseMoveStartX, _mouseMoveEndX)); if (OnNewSelectionRightClicked != null && seconds > currentRegionLeft && seconds < currentRegionRight) { @@ -1095,7 +1089,7 @@ namespace Nikse.SubtitleEdit.Controls if (_mouseDownParagraph == null) { _mouseMoveEndX = 0; - _mouseMoveStartX += (int)(_wavePeaks.Header.SampleRate * 0.1); + _mouseMoveStartX += (int)(_wavePeaks.SampleRate * 0.1); OnPositionSelected.Invoke(this, new ParagraphEventArgs(StartPositionSeconds, null)); } } @@ -1103,7 +1097,7 @@ namespace Nikse.SubtitleEdit.Controls Invalidate(); return; } - if (e.X > Width && StartPositionSeconds + 0.1 < _wavePeaks.Header.LengthInSeconds && _mouseDown) + if (e.X > Width && StartPositionSeconds + 0.1 < _wavePeaks.LengthInSeconds && _mouseDown) { //if (e.X > _mouseMoveLastX) // not much room for moving mouse cursor, so just scroll right { @@ -1111,7 +1105,7 @@ namespace Nikse.SubtitleEdit.Controls if (_mouseDownParagraph == null) { _mouseMoveEndX = Width; - _mouseMoveStartX -= (int)(_wavePeaks.Header.SampleRate * 0.1); + _mouseMoveStartX -= (int)(_wavePeaks.SampleRate * 0.1); OnPositionSelected.Invoke(this, new ParagraphEventArgs(StartPositionSeconds, null)); } } @@ -1126,7 +1120,7 @@ namespace Nikse.SubtitleEdit.Controls if (e.Button == MouseButtons.None) { - double seconds = XPositionToSeconds(e.X); + double seconds = RelativeXPositionToSeconds(e.X); var milliseconds = (int)(seconds * TimeCode.BaseUnit); if (IsParagrapBorderHit(milliseconds, NewSelectionParagraph)) @@ -1150,7 +1144,7 @@ namespace Nikse.SubtitleEdit.Controls { if (_mouseDownParagraph != null) { - double seconds = XPositionToSeconds(e.X); + double seconds = RelativeXPositionToSeconds(e.X); var milliseconds = (int)(seconds * TimeCode.BaseUnit); var subtitleIndex = _subtitle.GetIndex(_mouseDownParagraph); _prevParagraph = _subtitle.GetParagraphOrDefault(subtitleIndex - 1); @@ -1292,8 +1286,8 @@ namespace Nikse.SubtitleEdit.Controls int start = Math.Min(_mouseMoveStartX, _mouseMoveEndX); int end = Math.Max(_mouseMoveStartX, _mouseMoveEndX); - var startTotalSeconds = XPositionToSeconds(start); - var endTotalSeconds = XPositionToSeconds(end); + var startTotalSeconds = RelativeXPositionToSeconds(start); + var endTotalSeconds = RelativeXPositionToSeconds(end); if (PreventOverlap && endTotalSeconds * TimeCode.BaseUnit >= _wholeParagraphMaxMilliseconds) { @@ -1375,7 +1369,7 @@ namespace Nikse.SubtitleEdit.Controls private void WaveformMouseEnter(object sender, EventArgs e) { - if (_wavePeaks == null || _wavePeaks.Header == null) + if (_wavePeaks == null) return; if (_noClear) @@ -1413,7 +1407,7 @@ namespace Nikse.SubtitleEdit.Controls { if (OnPause != null) OnPause.Invoke(sender, null); - double seconds = XPositionToSeconds(e.X); + double seconds = RelativeXPositionToSeconds(e.X); var milliseconds = (int)(seconds * TimeCode.BaseUnit); Paragraph p = GetParagraphAtMilliseconds(milliseconds); @@ -1455,7 +1449,7 @@ namespace Nikse.SubtitleEdit.Controls { if (ModifierKeys == Keys.Shift && _selectedParagraph != null) { - double seconds = XPositionToSeconds(e.X); + double seconds = RelativeXPositionToSeconds(e.X); var milliseconds = (int)(seconds * TimeCode.BaseUnit); if (_mouseDownParagraphType == MouseDownParagraphType.None || _mouseDownParagraphType == MouseDownParagraphType.Whole) { @@ -1472,7 +1466,7 @@ namespace Nikse.SubtitleEdit.Controls } if (ModifierKeys == Keys.Control && _selectedParagraph != null) { - double seconds = XPositionToSeconds(e.X); + double seconds = RelativeXPositionToSeconds(e.X); var milliseconds = (int)(seconds * TimeCode.BaseUnit); if (_mouseDownParagraphType == MouseDownParagraphType.None || _mouseDownParagraphType == MouseDownParagraphType.Whole) { @@ -1489,7 +1483,7 @@ namespace Nikse.SubtitleEdit.Controls } if (ModifierKeys == (Keys.Control | Keys.Shift) && _selectedParagraph != null) { - double seconds = XPositionToSeconds(e.X); + double seconds = RelativeXPositionToSeconds(e.X); if (_mouseDownParagraphType == MouseDownParagraphType.None || _mouseDownParagraphType == MouseDownParagraphType.Whole) { _oldParagraph = new Paragraph(_selectedParagraph); @@ -1501,7 +1495,7 @@ namespace Nikse.SubtitleEdit.Controls } if (ModifierKeys == Keys.Alt && _selectedParagraph != null) { - double seconds = XPositionToSeconds(e.X); + double seconds = RelativeXPositionToSeconds(e.X); var milliseconds = (int)(seconds * TimeCode.BaseUnit); if (_mouseDownParagraphType == MouseDownParagraphType.None || _mouseDownParagraphType == MouseDownParagraphType.Whole) { @@ -1517,7 +1511,7 @@ namespace Nikse.SubtitleEdit.Controls } if (_mouseDownParagraphType == MouseDownParagraphType.None || _mouseDownParagraphType == MouseDownParagraphType.Whole) - OnSingleClick.Invoke(this, new ParagraphEventArgs(XPositionToSeconds(e.X), null)); + OnSingleClick.Invoke(this, new ParagraphEventArgs(RelativeXPositionToSeconds(e.X), null)); } } } @@ -1535,6 +1529,12 @@ namespace Nikse.SubtitleEdit.Controls { ZoomOut(); } + else if (e.Modifiers == Keys.Control && e.KeyCode == Keys.D0) + { + ZoomFactor = 1.0; + if (OnZoomedChanged != null) + OnZoomedChanged.Invoke(this, null); + } else if (e.Modifiers == Keys.None && e.KeyCode == Keys.Z) { if (StartPositionSeconds > 0.1) @@ -1547,7 +1547,7 @@ namespace Nikse.SubtitleEdit.Controls } else if (e.Modifiers == Keys.None && e.KeyCode == Keys.X) { - if (StartPositionSeconds + 0.1 < _wavePeaks.Header.LengthInSeconds) + if (StartPositionSeconds + 0.1 < _wavePeaks.LengthInSeconds) { StartPositionSeconds += 0.1; OnPositionSelected.Invoke(this, new ParagraphEventArgs(StartPositionSeconds, null)); @@ -1571,21 +1571,22 @@ namespace Nikse.SubtitleEdit.Controls } } - public double FindDataBelowThreshold(int threshold, double durationInSeconds) + public double FindDataBelowThreshold(int thresholdPercent, double durationInSeconds) { - int begin = SecondsToXPosition(_currentVideoPositionSeconds + 1); - int length = SecondsToXPosition(durationInSeconds); + int begin = SecondsToSampleIndex(_currentVideoPositionSeconds + 1); + int length = SecondsToSampleIndex(durationInSeconds); + int threshold = (int)(thresholdPercent / 100.0 * _wavePeaks.HighestPeak); int hitCount = 0; - for (int i = begin; i < _wavePeaks.AllSamples.Count; i++) + for (int i = begin; i < _wavePeaks.Peaks.Count; i++) { - if (i > 0 && i < _wavePeaks.AllSamples.Count && Math.Abs(_wavePeaks.AllSamples[i]) <= threshold) + if (i > 0 && i < _wavePeaks.Peaks.Count && _wavePeaks.Peaks[i].Abs <= threshold) hitCount++; else hitCount = 0; if (hitCount > length) { - double seconds = ((i - (length / 2)) / (double)_wavePeaks.Header.SampleRate) / _zoomFactor; + double seconds = SampleIndexToSeconds(i - (length / 2)); if (seconds >= 0) { StartPositionSeconds = seconds; @@ -1600,21 +1601,22 @@ namespace Nikse.SubtitleEdit.Controls return -1; } - public double FindDataBelowThresholdBack(int threshold, double durationInSeconds) + public double FindDataBelowThresholdBack(int thresholdPercent, double durationInSeconds) { - int begin = SecondsToXPosition(_currentVideoPositionSeconds - 1); - int length = SecondsToXPosition(durationInSeconds); + int begin = SecondsToSampleIndex(_currentVideoPositionSeconds - 1); + int length = SecondsToSampleIndex(durationInSeconds); + int threshold = (int)(thresholdPercent / 100.0 * _wavePeaks.HighestPeak); int hitCount = 0; for (int i = begin; i > 0; i--) { - if (i > 0 && i < _wavePeaks.AllSamples.Count && Math.Abs(_wavePeaks.AllSamples[i]) <= threshold) + if (i > 0 && i < _wavePeaks.Peaks.Count && _wavePeaks.Peaks[i].Abs <= threshold) hitCount++; else hitCount = 0; if (hitCount > length) { - double seconds = (i + (length / 2)) / (double)_wavePeaks.Header.SampleRate / _zoomFactor; + double seconds = SampleIndexToSeconds(i + (length / 2)); if (seconds >= 0) { StartPositionSeconds = seconds; @@ -1701,126 +1703,78 @@ namespace Nikse.SubtitleEdit.Controls ///////////////////////////////////////////////// - public void InitializeSpectrogram(string spectrogramDirectory) + private void InitializeSpectrogram(SpectrogramData spectrogram) { - _spectrogramBitmaps = new List(); - _tempShowSpectrogram = ShowSpectrogram; - ShowSpectrogram = false; - if (Directory.Exists(spectrogramDirectory)) + if (_spectrogram != null) { - _spectrogramDirectory = spectrogramDirectory; - _spectrogramBackgroundWorker = new System.ComponentModel.BackgroundWorker(); - _spectrogramBackgroundWorker.DoWork += LoadSpectrogramBitmapsAsync; - _spectrogramBackgroundWorker.RunWorkerCompleted += LoadSpectrogramBitmapsCompleted; - _spectrogramBackgroundWorker.RunWorkerAsync(); + _spectrogram.Dispose(); + _spectrogram = null; + Invalidate(); } - } - private void LoadSpectrogramBitmapsCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e) - { - LoadSpectrogramInfo(_spectrogramDirectory); - ShowSpectrogram = _tempShowSpectrogram; - if (_spectrogramBackgroundWorker != null) - _spectrogramBackgroundWorker.Dispose(); - } + if (spectrogram == null) + return; - private void LoadSpectrogramBitmapsAsync(object sender, System.ComponentModel.DoWorkEventArgs e) - { - try + if (spectrogram.IsLoaded) { - for (var count = 0; ; count++) + InitializeSpectrogramInternal(spectrogram); + } + else + { + Task.Factory.StartNew(() => { - var fileName = Path.Combine(_spectrogramDirectory, count + ".gif"); - - // important that this does not lock file (do NOT use Image.FromFile(fileName) or alike!!!) - using (var ms = new MemoryStream(File.ReadAllBytes(fileName))) + spectrogram.Load(); + BeginInvoke((Action)(() => { - _spectrogramBitmaps.Add((Bitmap)Image.FromStream(ms)); - } - } - } - catch (FileNotFoundException) - { - // no more files + InitializeSpectrogramInternal(spectrogram); + })); + }); } } - public void InitializeSpectrogram(List spectrogramBitmaps, string spectrogramDirectory) + private void InitializeSpectrogramInternal(SpectrogramData spectrogram) { - _spectrogramBitmaps = spectrogramBitmaps; - LoadSpectrogramInfo(spectrogramDirectory); + if (_spectrogram != null) + return; + + _spectrogram = spectrogram; + Invalidate(); } - private void LoadSpectrogramInfo(string spectrogramDirectory) + private void DrawSpectrogram(Graphics graphics) { - try + int width = (int)Math.Round((EndPositionSeconds - StartPositionSeconds) / _spectrogram.SampleDuration); + using (var bmpCombined = new Bitmap(width, _spectrogram.FftSize / 2)) + using (var gfxCombined = Graphics.FromImage(bmpCombined)) { - var doc = new XmlDocument(); - string xmlInfoFileName = Path.Combine(spectrogramDirectory, "Info.xml"); - if (File.Exists(xmlInfoFileName)) + int left = (int)Math.Round(StartPositionSeconds / _spectrogram.SampleDuration); + int offset = 0; + int imageIndex = left / _spectrogram.ImageWidth; + while (offset < width && imageIndex < _spectrogram.Images.Count) { - doc.Load(xmlInfoFileName); - _sampleDuration = Convert.ToDouble(doc.DocumentElement.SelectSingleNode("SampleDuration").InnerText, CultureInfo.InvariantCulture); - _nfft = Convert.ToInt32(doc.DocumentElement.SelectSingleNode("NFFT").InnerText, CultureInfo.InvariantCulture); - _imageWidth = Convert.ToInt32(doc.DocumentElement.SelectSingleNode("ImageWidth").InnerText, CultureInfo.InvariantCulture); - ShowSpectrogram = true; + int x = (left + offset) % _spectrogram.ImageWidth; + int w = Math.Min(_spectrogram.ImageWidth - x, width - offset); + gfxCombined.DrawImage(_spectrogram.Images[imageIndex], offset, 0, new Rectangle(x, 0, w, bmpCombined.Height), GraphicsUnit.Pixel); + offset += w; + imageIndex++; } - else - { - ShowSpectrogram = false; - } - } - catch - { - ShowSpectrogram = false; - } - } - - private void DrawSpectrogramBitmap(double seconds, Graphics graphics) - { - double duration = EndPositionSeconds - StartPositionSeconds; - var width = (int)(duration / _sampleDuration); - - using (var bmpDestination = new Bitmap(width, _nfft / 2)) //calculate width - { - using (var gfx = Graphics.FromImage(bmpDestination)) - { - double startRow = seconds / (_sampleDuration * _imageWidth); - var bitmapIndex = (int)startRow; - var subtractValue = (int)Math.Round((startRow - bitmapIndex) * _imageWidth); - - int i = 0; - while (i * _imageWidth < width && i + bitmapIndex < _spectrogramBitmaps.Count) - { - var bmp = _spectrogramBitmaps[i + bitmapIndex]; - gfx.DrawImageUnscaled(bmp, new Point(bmp.Width * i - subtractValue, 0)); - i++; - } - if (i + bitmapIndex < _spectrogramBitmaps.Count && subtractValue > 0) - { - var bmp = _spectrogramBitmaps[i + bitmapIndex]; - gfx.DrawImageUnscaled(bmp, new Point(bmp.Width * i - subtractValue, 0)); - } - } - if (ShowWaveform) - graphics.DrawImage(bmpDestination, new Rectangle(0, Height - SpectrogramDisplayHeight, Width, SpectrogramDisplayHeight)); - else - graphics.DrawImage(bmpDestination, new Rectangle(0, 0, Width, Height)); + int displayHeight = ShowWaveform ? SpectrogramDisplayHeight : Height; + graphics.DrawImage(bmpCombined, new Rectangle(0, Height - displayHeight, Width, displayHeight)); } } private double GetAverageVolumeForNextMilliseconds(int sampleIndex, int milliseconds) { - int length = SecondsToXPosition(milliseconds / TimeCode.BaseUnit); + int length = SecondsToSampleIndex(milliseconds / TimeCode.BaseUnit); if (length < 9) length = 9; double v = 0; int count = 0; for (int i = sampleIndex; i < sampleIndex + length; i++) { - if (i > 0 && i < _wavePeaks.AllSamples.Count) + if (i > 0 && i < _wavePeaks.Peaks.Count) { - v += Math.Abs(_wavePeaks.AllSamples[i]); + v += _wavePeaks.Peaks[i].Abs; count++; } } @@ -1829,31 +1783,31 @@ namespace Nikse.SubtitleEdit.Controls return v / count; } - internal void GenerateTimeCodes(double startFromSeconds, int minimumVolumePercent, int maximumVolumePercent, int defaultMilliseconds) + internal void GenerateTimeCodes(Subtitle subtitle, double startFromSeconds, int blockSizeMilliseconds, int minimumVolumePercent, int maximumVolumePercent, int defaultMilliseconds) { - int begin = SecondsToXPosition(startFromSeconds); + int begin = SecondsToSampleIndex(startFromSeconds); double average = 0; - for (int k = begin; k < _wavePeaks.AllSamples.Count; k++) - average += Math.Abs(_wavePeaks.AllSamples[k]); - average = average / (_wavePeaks.AllSamples.Count - begin); + for (int k = begin; k < _wavePeaks.Peaks.Count; k++) + average += _wavePeaks.Peaks[k].Abs; + average /= _wavePeaks.Peaks.Count - begin; - var maxThreshold = (int)(_wavePeaks.DataMaxValue * (maximumVolumePercent / 100.0)); + var maxThreshold = (int)(_wavePeaks.HighestPeak * (maximumVolumePercent / 100.0)); var silenceThreshold = (int)(average * (minimumVolumePercent / 100.0)); - int length50Ms = SecondsToXPosition(0.050); + int length50Ms = SecondsToSampleIndex(0.050); double secondsPerParagraph = defaultMilliseconds / TimeCode.BaseUnit; - int minBetween = SecondsToXPosition(Configuration.Settings.General.MinimumMillisecondsBetweenLines / TimeCode.BaseUnit); + int minBetween = SecondsToSampleIndex(Configuration.Settings.General.MinimumMillisecondsBetweenLines / TimeCode.BaseUnit); bool subtitleOn = false; int i = begin; - while (i < _wavePeaks.AllSamples.Count) + while (i < _wavePeaks.Peaks.Count) { if (subtitleOn) { - var currentLengthInSeconds = XPositionToSeconds(i - begin) - StartPositionSeconds; + var currentLengthInSeconds = SampleIndexToSeconds(i - begin); if (currentLengthInSeconds > 1.0) { - subtitleOn = EndParagraphDueToLowVolume(silenceThreshold, begin, true, i); + subtitleOn = EndParagraphDueToLowVolume(subtitle, blockSizeMilliseconds, silenceThreshold, begin, true, i); if (!subtitleOn) { begin = i + minBetween; @@ -1864,7 +1818,7 @@ namespace Nikse.SubtitleEdit.Controls { for (int j = 0; j < 20; j++) { - subtitleOn = EndParagraphDueToLowVolume(silenceThreshold, begin, true, i + (j * length50Ms)); + subtitleOn = EndParagraphDueToLowVolume(subtitle, blockSizeMilliseconds, silenceThreshold, begin, true, i + (j * length50Ms)); if (!subtitleOn) { i += (j * length50Ms); @@ -1876,8 +1830,8 @@ namespace Nikse.SubtitleEdit.Controls if (subtitleOn) // force break { - var p = new Paragraph(string.Empty, (XPositionToSeconds(begin) - StartPositionSeconds) * TimeCode.BaseUnit, (XPositionToSeconds(i) - StartPositionSeconds) * TimeCode.BaseUnit); - _subtitle.Paragraphs.Add(p); + var p = new Paragraph(string.Empty, SampleIndexToSeconds(begin) * TimeCode.BaseUnit, SampleIndexToSeconds(i) * TimeCode.BaseUnit); + subtitle.Paragraphs.Add(p); begin = i + minBetween; i = begin; } @@ -1885,7 +1839,7 @@ namespace Nikse.SubtitleEdit.Controls } else { - double avgVol = GetAverageVolumeForNextMilliseconds(i, 100); + double avgVol = GetAverageVolumeForNextMilliseconds(i, blockSizeMilliseconds); if (avgVol > silenceThreshold) { if (avgVol < maxThreshold) @@ -1897,15 +1851,17 @@ namespace Nikse.SubtitleEdit.Controls } i++; } + + subtitle.Renumber(); } - private bool EndParagraphDueToLowVolume(double silenceThreshold, int begin, bool subtitleOn, int i) + private bool EndParagraphDueToLowVolume(Subtitle subtitle, int blockSizeMilliseconds, double silenceThreshold, int begin, bool subtitleOn, int i) { - double avgVol = GetAverageVolumeForNextMilliseconds(i, 100); + double avgVol = GetAverageVolumeForNextMilliseconds(i, blockSizeMilliseconds); if (avgVol < silenceThreshold) { - var p = new Paragraph(string.Empty, (XPositionToSeconds(begin) - StartPositionSeconds) * TimeCode.BaseUnit, (XPositionToSeconds(i) - StartPositionSeconds) * TimeCode.BaseUnit); - _subtitle.Paragraphs.Add(p); + var p = new Paragraph(string.Empty, SampleIndexToSeconds(begin) * TimeCode.BaseUnit, SampleIndexToSeconds(i) * TimeCode.BaseUnit); + subtitle.Paragraphs.Add(p); subtitleOn = false; } return subtitleOn; diff --git a/src/Forms/AddWaveForm.cs b/src/Forms/AddWaveForm.cs index e9e932775..426408561 100644 --- a/src/Forms/AddWaveForm.cs +++ b/src/Forms/AddWaveForm.cs @@ -15,9 +15,11 @@ namespace Nikse.SubtitleEdit.Forms { public string SourceVideoFileName { get; private set; } private bool _cancel; + private string _peakWaveFileName; private string _wavFileName; private string _spectrogramDirectory; - public List SpectrogramBitmaps { get; private set; } + public WavePeakData Peaks { get; private set; } + public SpectrogramData Spectrogram { get; private set; } private string _encodeParamters; private const string RetryEncodeParameters = "acodec=s16l"; private int _audioTrackNumber = -1; @@ -31,10 +33,9 @@ namespace Nikse.SubtitleEdit.Forms labelInfo.Text = string.Empty; } - public WavePeakGenerator WavePeak { get; private set; } - - public void Initialize(string videoFile, string spectrogramDirectory, int audioTrackNumber) + public void Initialize(string videoFile, string peakWaveFileName, string spectrogramDirectory, int audioTrackNumber) { + _peakWaveFileName = peakWaveFileName; _audioTrackNumber = audioTrackNumber; if (_audioTrackNumber < 0) _audioTrackNumber = 0; @@ -236,16 +237,14 @@ namespace Nikse.SubtitleEdit.Forms using (var waveFile = new WavePeakGenerator(targetFile)) { - waveFile.GeneratePeakSamples(delayInMilliseconds); + Peaks = waveFile.GeneratePeaks(delayInMilliseconds, _peakWaveFileName); if (Configuration.Settings.VideoControls.GenerateSpectrogram) { labelProgress.Text = Configuration.Settings.Language.AddWaveform.GeneratingSpectrogram; Refresh(); - SpectrogramBitmaps = waveFile.GenerateFourierData(256, _spectrogramDirectory, delayInMilliseconds); // image height = nfft / 2 + Spectrogram = waveFile.GenerateSpectrogram(delayInMilliseconds, _spectrogramDirectory); } - - WavePeak = waveFile; } labelPleaseWait.Visible = false; @@ -379,8 +378,9 @@ namespace Nikse.SubtitleEdit.Forms _cancel = true; } - internal void InitializeViaWaveFile(string fileName, string spectrogramFolder) + internal void InitializeViaWaveFile(string fileName, string peakWaveFileName, string spectrogramFolder) { + _peakWaveFileName = peakWaveFileName; _wavFileName = fileName; _spectrogramDirectory = spectrogramFolder; } diff --git a/src/Forms/AddWaveformBatch.cs b/src/Forms/AddWaveformBatch.cs index 728ddb750..8bef37716 100644 --- a/src/Forms/AddWaveformBatch.cs +++ b/src/Forms/AddWaveformBatch.cs @@ -329,12 +329,11 @@ namespace Nikse.SubtitleEdit.Forms { using (var waveFile = new WavePeakGenerator(targetFile)) { - waveFile.GeneratePeakSamples(delayInMilliseconds); - waveFile.WritePeakSamples(Main.GetPeakWaveFileName(videoFileName)); + waveFile.GeneratePeaks(delayInMilliseconds, Main.GetPeakWaveFileName(videoFileName)); if (Configuration.Settings.VideoControls.GenerateSpectrogram) { - waveFile.GenerateFourierData(256, Main.GetSpectrogramFolder(videoFileName), delayInMilliseconds); // image height = nfft / 2 + waveFile.GenerateSpectrogram(delayInMilliseconds, Main.GetSpectrogramFolder(videoFileName)); } } } diff --git a/src/Forms/Main.cs b/src/Forms/Main.cs index 56485e123..da32ab963 100644 --- a/src/Forms/Main.cs +++ b/src/Forms/Main.cs @@ -2552,8 +2552,7 @@ namespace Nikse.SubtitleEdit.Forms _videoAudioTrackNumber = -1; labelVideoInfo.Text = _languageGeneral.NoVideoLoaded; audioVisualizer.WavePeaks = null; - audioVisualizer.ResetSpectrogram(); - audioVisualizer.Invalidate(); + audioVisualizer.Spectrogram = null; } if (Configuration.Settings.General.ShowVideoPlayer || Configuration.Settings.General.ShowAudioVisualizer) @@ -2643,8 +2642,7 @@ namespace Nikse.SubtitleEdit.Forms _videoAudioTrackNumber = -1; labelVideoInfo.Text = _languageGeneral.NoVideoLoaded; audioVisualizer.WavePeaks = null; - audioVisualizer.ResetSpectrogram(); - audioVisualizer.Invalidate(); + audioVisualizer.Spectrogram = null; Configuration.Settings.RecentFiles.Add(fileName, FirstVisibleIndex, FirstSelectedIndex, _videoFileName, _subtitleAlternateFileName); Configuration.Settings.Save(); @@ -3301,8 +3299,7 @@ namespace Nikse.SubtitleEdit.Forms _videoAudioTrackNumber = -1; labelVideoInfo.Text = _languageGeneral.NoVideoLoaded; audioVisualizer.WavePeaks = null; - audioVisualizer.ResetSpectrogram(); - audioVisualizer.Invalidate(); + audioVisualizer.Spectrogram = null; _sourceViewChange = false; @@ -12679,12 +12676,9 @@ namespace Nikse.SubtitleEdit.Forms string spectrogramFolder = GetSpectrogramFolder(fileName); if (File.Exists(peakWaveFileName)) { - audioVisualizer.WavePeaks = new WavePeakGenerator(peakWaveFileName); - audioVisualizer.ResetSpectrogram(); - audioVisualizer.InitializeSpectrogram(spectrogramFolder); + audioVisualizer.WavePeaks = WavePeakData.FromDisk(peakWaveFileName); + audioVisualizer.Spectrogram = SpectrogramData.FromDisk(spectrogramFolder); toolStripComboBoxWaveform_SelectedIndexChanged(null, null); - audioVisualizer.WavePeaks.GenerateAllSamples(); - audioVisualizer.WavePeaks.Close(); SetWaveformPosition(0, 0, 0); timerWaveform.Start(); } @@ -13426,8 +13420,7 @@ namespace Nikse.SubtitleEdit.Forms if (audioVisualizer.WavePeaks != null) { audioVisualizer.WavePeaks = null; - audioVisualizer.ResetSpectrogram(); - audioVisualizer.Invalidate(); + audioVisualizer.Spectrogram = null; } openFileDialog1.InitialDirectory = Path.GetDirectoryName(openFileDialog1.FileName); if (!panelVideoPlayer.Visible) @@ -14819,21 +14812,16 @@ namespace Nikse.SubtitleEdit.Forms if (IsFileValidForVisualizer(_videoFileName)) { - addWaveform.InitializeViaWaveFile(_videoFileName, spectrogramFolder); + addWaveform.InitializeViaWaveFile(_videoFileName, peakWaveFileName, spectrogramFolder); } else { - addWaveform.Initialize(_videoFileName, spectrogramFolder, _videoAudioTrackNumber); + addWaveform.Initialize(_videoFileName, peakWaveFileName, spectrogramFolder, _videoAudioTrackNumber); } if (addWaveform.ShowDialog() == DialogResult.OK) { - addWaveform.WavePeak.WritePeakSamples(peakWaveFileName); - var audioPeakWave = new WavePeakGenerator(peakWaveFileName); - audioPeakWave.GenerateAllSamples(); - audioPeakWave.Close(); - audioVisualizer.WavePeaks = audioPeakWave; - if (addWaveform.SpectrogramBitmaps != null) - audioVisualizer.InitializeSpectrogram(addWaveform.SpectrogramBitmaps, spectrogramFolder); + audioVisualizer.WavePeaks = addWaveform.Peaks; + audioVisualizer.Spectrogram = addWaveform.Spectrogram; timerWaveform.Start(); } } @@ -15204,19 +15192,20 @@ namespace Nikse.SubtitleEdit.Forms } if (_videoFileName == null) + { OpenVideo(fileName); + return; + } using (var addWaveform = new AddWaveform()) { + string peakWaveFileName = GetPeakWaveFileName(_videoFileName); string spectrogramFolder = GetSpectrogramFolder(_videoFileName); - addWaveform.InitializeViaWaveFile(fileName, spectrogramFolder); + addWaveform.InitializeViaWaveFile(fileName, peakWaveFileName, spectrogramFolder); if (addWaveform.ShowDialog() == DialogResult.OK) { - string peakWaveFileName = GetPeakWaveFileName(_videoFileName); - addWaveform.WavePeak.WritePeakSamples(peakWaveFileName); - var audioPeakWave = new WavePeakGenerator(peakWaveFileName); - audioPeakWave.GenerateAllSamples(); - audioVisualizer.WavePeaks = audioPeakWave; + audioVisualizer.WavePeaks = addWaveform.Peaks; + audioVisualizer.Spectrogram = addWaveform.Spectrogram; timerWaveform.Start(); } } @@ -16501,8 +16490,7 @@ namespace Nikse.SubtitleEdit.Forms _videoAudioTrackNumber = -1; labelVideoInfo.Text = _languageGeneral.NoVideoLoaded; audioVisualizer.WavePeaks = null; - audioVisualizer.ResetSpectrogram(); - audioVisualizer.Invalidate(); + audioVisualizer.Spectrogram = null; } private void ToolStripMenuItemVideoDropDownOpening(object sender, EventArgs e) @@ -16540,7 +16528,7 @@ namespace Nikse.SubtitleEdit.Forms } } - if (mediaPlayer.VideoPlayer != null && audioVisualizer != null && audioVisualizer.WavePeaks != null && audioVisualizer.WavePeaks.AllSamples.Count > 0) + if (mediaPlayer.VideoPlayer != null && audioVisualizer != null && audioVisualizer.WavePeaks != null && audioVisualizer.WavePeaks.Peaks.Count > 0) { toolStripMenuItemImportSceneChanges.Visible = true; toolStripMenuItemRemoveSceneChanges.Visible = audioVisualizer.SceneChanges.Count > 0; @@ -18451,9 +18439,9 @@ namespace Nikse.SubtitleEdit.Forms { MakeHistoryForUndoOnlyIfNotResent(string.Format(_language.BeforeGuessingTimeCodes)); - double startFrom = 0; + double startFromSeconds = 0; if (form.StartFromVideoPosition) - startFrom = mediaPlayer.CurrentPosition; + startFromSeconds = mediaPlayer.CurrentPosition; if (form.DeleteAll) { @@ -18463,11 +18451,11 @@ namespace Nikse.SubtitleEdit.Forms { for (int i = _subtitle.Paragraphs.Count - 1; i > 0; i--) { - if (_subtitle.Paragraphs[i].EndTime.TotalSeconds + 1 > startFrom) + if (_subtitle.Paragraphs[i].EndTime.TotalSeconds + 1 > startFromSeconds) _subtitle.Paragraphs.RemoveAt(i); } } - audioVisualizer.GenerateTimeCodes(form.BlockSize, form.VolumeMinimum, form.VolumeMaximum, form.DefaultMilliseconds); + audioVisualizer.GenerateTimeCodes(_subtitle, startFromSeconds, form.BlockSize, form.VolumeMinimum, form.VolumeMaximum, form.DefaultMilliseconds); if (IsFramesRelevant && CurrentFrameRate > 0) _subtitle.CalculateFrameNumbersFromTimeCodesNoCheck(CurrentFrameRate); SubtitleListview1.Fill(_subtitle, _subtitleAlternate);