using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Text; namespace Spectrogram { // Spectrogram File Format reader/writer public class SFF { public readonly byte VersionMajor = 1; public readonly byte VersionMinor = 1; public string FilePath { get; private set; } // time information public int SampleRate { get; private set; } public int StepSize { get; private set; } public int Width { get; private set; } // frequency information public int FftSize { get; private set; } public int FftFirstIndex { get; private set; } public int Height { get; private set; } public int OffsetHz { get; private set; } public int MelBinCount { get; private set; } public bool Decibels { get; private set; } public bool IsMel { get { return MelBinCount > 0; } } // image details public List Ffts { get; private set; } public int ImageHeight { get { return (Ffts is null) ? 0 : Ffts[0].Length; } } public int ImageWidth { get { return (Ffts is null) ? 0 : Ffts.Count; } } public double[] times { get; private set; } public double[] freqs { get; private set; } public double[] mels { get; private set; } [Obsolete("use ImageWidth", error: false)] public int FftWidth { get { return ImageWidth; } } [Obsolete("use ImageHeight", error: false)] public int FftHeight { get { return ImageHeight; } } public SFF() { } public override string ToString() { return $"SFF {ImageWidth}x{ImageHeight}"; } public SFF(string loadFilePath) { Load(loadFilePath); CalculateTimes(); CalculateFrequencies(); } public SFF(Spectrogram spec, int melBinCount = 0) { SampleRate = spec.SampleRate; StepSize = spec.StepSize; Width = spec.Width; FftSize = spec.FftSize; FftFirstIndex = spec.NextColumnIndex; Height = spec.Height; OffsetHz = spec.OffsetHz; MelBinCount = melBinCount; Ffts = (melBinCount > 0) ? spec.GetMelFFTs(melBinCount) : spec.GetFFTs(); CalculateTimes(); CalculateFrequencies(); } public Bitmap GetBitmap(Colormap cmap = null, double intensity = 1, bool dB = false) { cmap = cmap ?? Colormap.Viridis; return Image.GetBitmap(Ffts, cmap, intensity, dB); } public void Load(string filePath) { FilePath = Path.GetFullPath(filePath); byte[] bytes = File.ReadAllBytes(filePath); // ensure the first 4 bytes match what we expect int magicNumber = BitConverter.ToInt32(bytes, 0); if (magicNumber != 1179014099) throw new InvalidDataException("not a valid SFF file"); // read file version byte versionMajor = bytes[40]; byte versionMinor = bytes[41]; // read time information SampleRate = BitConverter.ToInt32(bytes, 42); StepSize = BitConverter.ToInt32(bytes, 46); Width = BitConverter.ToInt32(bytes, 50); // read frequency information FftSize = BitConverter.ToInt32(bytes, 54); FftFirstIndex = BitConverter.ToInt32(bytes, 58); Height = BitConverter.ToInt32(bytes, 62); OffsetHz = BitConverter.ToInt32(bytes, 66); MelBinCount = BitConverter.ToInt32(bytes, 84); // data format byte valuesPerPoint = bytes[70]; bool isComplex = valuesPerPoint == 2; if (isComplex) throw new NotImplementedException("complex data is not yet supported"); byte bytesPerValue = bytes[71]; Decibels = bytes[72] == 1; // recording start time - no longer stored in the SFF file //DateTime dt = new DateTime(bytes[74] + 2000, bytes[75], bytes[76], bytes[77], bytes[78], bytes[79]); // data storage int firstDataByte = (int)BitConverter.ToUInt32(bytes, 80); // FFT dimensions MelBinCount = BitConverter.ToInt32(bytes, 84); int FftHeight = BitConverter.ToInt32(bytes, 88); int FftWidth = BitConverter.ToInt32(bytes, 92); // create the FFT by reading data from file Ffts = new List(); int bytesPerPoint = bytesPerValue * valuesPerPoint; int bytesPerColumn = FftHeight * bytesPerPoint; for (int x = 0; x < FftWidth; x++) { Ffts.Add(new double[FftHeight]); int columnOffset = bytesPerColumn * x; for (int y = 0; y < FftHeight; y++) { int rowOffset = y * bytesPerPoint; int valueOffset = firstDataByte + columnOffset + rowOffset; double value = BitConverter.ToDouble(bytes, valueOffset); Ffts[x][y] = value; } } } public void Save(string filePath) { FilePath = Path.GetFullPath(filePath); byte[] header = new byte[256]; // file type designator header[0] = 211; // intentionally non-ASCII header[1] = (byte)'S'; header[2] = (byte)'F'; header[3] = (byte)'F'; header[4] = (byte)'\r'; header[5] = (byte)'\n'; header[6] = (byte)' '; header[7] = (byte)'\n'; int magicNumber = BitConverter.ToInt32(header, 0); if (magicNumber != 1179014099) throw new InvalidDataException("magic number for SFF files is 1179014099"); // plain text helpful for people who open this file in a text editor string fileInfo = $"Spectrogram File Format {VersionMajor}.{VersionMinor}\r\n"; byte[] fileInfoBytes = Encoding.UTF8.GetBytes(fileInfo); if (fileInfoBytes.Length > 32) throw new InvalidDataException("file info cannot exceed 32 bytes"); Array.Copy(fileInfoBytes, 0, header, 8, fileInfoBytes.Length); // version header[40] = VersionMajor; header[41] = VersionMinor; // time information Array.Copy(BitConverter.GetBytes(SampleRate), 0, header, 42, 4); Array.Copy(BitConverter.GetBytes(StepSize), 0, header, 46, 4); Array.Copy(BitConverter.GetBytes(Width), 0, header, 50, 4); // frequency information Array.Copy(BitConverter.GetBytes(FftSize), 0, header, 54, 4); Array.Copy(BitConverter.GetBytes(FftFirstIndex), 0, header, 58, 4); Array.Copy(BitConverter.GetBytes(Height), 0, header, 62, 4); Array.Copy(BitConverter.GetBytes(OffsetHz), 0, header, 66, 4); // data encoding details byte valuesPerPoint = 1; // 1 for magnitude or power data, 2 for complex data byte bytesPerValue = 8; // a double is 8 bytes byte decibelUnits = 0; // 1 if units are in dB byte dataExtraByte = 0; // unused header[70] = valuesPerPoint; header[71] = bytesPerValue; header[72] = decibelUnits; header[73] = dataExtraByte; // source file date and time // dont store this because it makes SFF files different every time they are generated //header[74] = (byte)(DateTime.UtcNow.Year - 2000); //header[75] = (byte)DateTime.UtcNow.Month; //header[76] = (byte)DateTime.UtcNow.Day; //header[77] = (byte)DateTime.UtcNow.Hour; //header[78] = (byte)DateTime.UtcNow.Minute; //header[79] = (byte)DateTime.UtcNow.Second; // ADD NEW VALUES HERE (after byte 80) Array.Copy(BitConverter.GetBytes(MelBinCount), 0, header, 84, 4); Array.Copy(BitConverter.GetBytes(ImageHeight), 0, header, 88, 4); Array.Copy(BitConverter.GetBytes(ImageWidth), 0, header, 92, 4); // binary data location (keep this at byte 80) int firstDataByte = header.Length; Array.Copy(BitConverter.GetBytes(firstDataByte), 0, header, 80, 4); // create bytes to write to file int dataPointCount = ImageHeight * ImageWidth; int bytesPerPoint = bytesPerValue * valuesPerPoint; byte[] fileBytes = new byte[header.Length + dataPointCount * bytesPerPoint]; Array.Copy(header, 0, fileBytes, 0, header.Length); // copy data into byte area int bytesPerColumn = ImageHeight * bytesPerPoint; for (int x = 0; x < ImageWidth; x++) { int columnOffset = bytesPerColumn * x; for (int y = 0; y < ImageHeight; y++) { int rowOffset = y * bytesPerPoint; int valueOffset = firstDataByte + columnOffset + rowOffset; double value = Ffts[x][y]; Array.Copy(BitConverter.GetBytes(value), 0, fileBytes, valueOffset, 8); } } // write file to disk File.WriteAllBytes(filePath, fileBytes); } public (double timeSec, double freqHz, double magRms) GetPixelInfo(int x, int y) { double timeSec = (double)x * StepSize / SampleRate; double maxFreq = SampleRate / 2; double maxMel = FftSharp.Transform.MelFromFreq(maxFreq); double frac = (ImageHeight - y) / (double)ImageHeight; double freq = IsMel ? FftSharp.Transform.MelToFreq(frac * maxMel) : frac * maxFreq; double mag = double.NaN; try { mag = Ffts[x][ImageHeight - y - 1]; } catch { } return (timeSec, freq, mag); } private void CalculateTimes() { times = new double[ImageWidth]; double stepSec = (double)StepSize / SampleRate; for (int i = 0; i < ImageWidth; i++) times[i] = i * stepSec; } private void CalculateFrequencies() { freqs = new double[ImageHeight]; mels = new double[ImageHeight]; double maxFreq = SampleRate / 2; double maxMel = FftSharp.Transform.MelFromFreq(maxFreq); for (int y = 0; y < ImageHeight; y++) { double frac = (ImageHeight - y) / (double)ImageHeight; if (IsMel) { mels[y] = frac * maxMel; freqs[y] = FftSharp.Transform.MelToFreq(mels[y]); } else { freqs[y] = frac * maxFreq; mels[y] = FftSharp.Transform.MelFromFreq(freqs[y]); } } } } }