Merge pull request #1343 from jdpurcell/patch6

Waveform - show actual peak values
This commit is contained in:
Nikolaj Olsson 2015-10-05 22:09:14 +02:00
commit 738589f899
5 changed files with 560 additions and 461 deletions

View File

@ -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<WavePeak> 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<WavePeak> 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<Bitmap> 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<Bitmap> 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<Bitmap>();
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; }
/// <summary>
/// Lowest data value
/// </summary>
public int DataMinValue { get; private set; }
/// <summary>
/// Highest data value
/// </summary>
public int DataMaxValue { get; private set; }
/// <summary>
/// Number of peaks per second (should be divideable by SampleRate)
/// </summary>
public int PeaksPerSecond { get; private set; }
/// <summary>
/// List of all peak samples (channels are merged)
/// </summary>
public List<int> PeakSamples { get; private set; }
/// <summary>
/// List of all samples (channels are merged)
/// </summary>
public List<int> AllSamples { get; private set; }
private delegate void WriteSampleDataValue(byte[] buffer, int offset, int value);
/// <summary>
/// Constructor
/// </summary>
/// <param name="fileName">Wave file name</param>
public WavePeakGenerator(string fileName)
: this(File.OpenRead(fileName))
{
Initialize(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite));
}
/// <summary>
@ -232,214 +355,252 @@ namespace Nikse.SubtitleEdit.Core
/// <param name="stream">Stream of a wave file</param>
public WavePeakGenerator(Stream stream)
{
Initialize(stream);
_stream = stream;
_header = new WaveHeader(_stream);
}
/// <summary>
/// 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.
/// </summary>
public bool IsSupported
{
get
{
return Header.AudioFormat == WaveHeader.AudioFormatPcm && Header.Format == "WAVE" && Header.BytesPerSample < 4;
return _header.AudioFormat == WaveHeader.AudioFormatPcm && _header.Format == "WAVE";
}
}
/// <summary>
/// Generate peaks (samples with some interval) for an uncompressed wave file
/// Generates peaks and saves them to disk.
/// </summary>
/// <param name="delayInMilliseconds">Delay in milliseconds (normally zero)</param>
public void GeneratePeakSamples(int delayInMilliseconds)
/// <param name="peakFileName">Path of the output file</param>
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<WavePeak>();
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<int>();
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));
}
/// <summary>
/// Loads previously generated peaks from disk.
/// </summary>
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>();
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);
}
/// <summary>
/// Determine how to read sample values
/// </summary>
/// <returns>Sample data reader that matches bits per sample</returns>
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<byte[], int> 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<Bitmap> 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<Bitmap> bitmaps = new List<Bitmap>();
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<Bitmap>();
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("<SpectrogramInfo><SampleDuration/><NFFT/><ImageWidth/><SecondsPerImage/></SpectrogramInfo>");
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

View File

@ -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<Bitmap> _spectrogramBitmaps = new List<Bitmap>();
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<Bitmap>();
}
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<SelectionRange> _ranges = new List<SelectionRange>();
private int _lastPosition = int.MaxValue;
private SelectionRange _nextSelection;
public IsSelectedHelper(IEnumerable<Paragraph> paragraphs, Func<double, int> secondsToPosition)
public IsSelectedHelper(IEnumerable<Paragraph> 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<float, float> 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<Bitmap>();
_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<Bitmap> 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;

View File

@ -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<Bitmap> 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;
}

View File

@ -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));
}
}
}

View File

@ -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);