/// <summary> /// Get a spectrogram of the signal specified at the input /// </summary> /// <param name="spectrogram">Signal</param> /// <param name="width">Width of the image</param> /// <param name="height">Height of the image</param> /// <param name="milliseconds">Time in ms</param> /// <param name="sampleRate">Sample rate in hz</param> /// <param name="colorPalette">Specify to color palette</param> /// <param name="doLogScale">log scale or not?</param> /// <param name="logFrequenciesIndex">log frequency index array</param> /// <param name="logFrequencies">log frequency array</param> /// <remarks> /// X axis - time /// Y axis - frequency /// Color - magnitude level of corresponding band value of the signal /// <returns>Spectral image of the signal</returns> public static Bitmap GetSpectrogramImage(float[][] spectrogram, int width, int height, double milliseconds, double sampleRate, ColorUtils.ColorPaletteType colorPalette, bool doLogScale, int[] logFrequenciesIndex, float[] logFrequencies) { #if SAFE if (width < 0) throw new ArgumentException("width should be bigger than 0"); if (height < 0) throw new ArgumentException("height should be bigger than 0"); #endif bool drawLabels = true; float minDb = -90.0f; // -80.0f also works good float maxDb = 10.0f; // with the current color palettes 10.0f works well // Basic constants int TOTAL_HEIGHT = height; // Height of graph int TOTAL_WIDTH = width; // Width of graph int TOP = 40; // Top of graph int LEFT = 60; // Left edge of graph int HEIGHT = height-2*TOP; // Height of graph int WIDTH = width-2*LEFT; // Width of graph string LABEL_X = "Time (ms)"; // Label for X axis string LABEL_Y = "Frequency (Hz)"; // Label for Y axis float MAX_FREQ = (float) sampleRate / 2; // Maximum frequency (Hz) on vertical axis. float MIN_FREQ = 27.5f; // Minimum frequency (Hz) on vertical axis. float FREQ_STEP = 1000; // Interval between ticks (dB) on vertical axis. // if the max frequency gets lower than ... lower the frequency step if (MAX_FREQ < 20000) { FREQ_STEP = (float) MathUtils.GetNicerNumber(MAX_FREQ / 20); } // Derived constants int BOTTOM = TOTAL_HEIGHT-TOP; // Bottom of graph float FREQTOPIXEL = (float) HEIGHT/(MAX_FREQ-MIN_FREQ); // Pixels/Hz float MIN_TIME = 0.0f; float MAX_TIME = (float) milliseconds; if (MAX_TIME == 0) MAX_TIME = 1000; // Interval between ticks (time) on horizontal axis. float TIME_STEP = (float) MathUtils.GetNicerNumber(MAX_TIME / 20); float TIMETOPIXEL = (float) WIDTH/(MAX_TIME-MIN_TIME); // Pixels/second // Colors // black, gray, white style Color lineColor = ColorTranslator.FromHtml("#BFBFBF"); Color middleLineColor = ColorTranslator.FromHtml("#BFBFBF"); Color labelColor = ColorTranslator.FromHtml("#FFFFFF"); Color tickColor = ColorTranslator.FromHtml("#BFBFBF"); Color fillOuterColor = ColorTranslator.FromHtml("#000000"); Color fillColor = ColorTranslator.FromHtml("#000000"); Bitmap fullImage = new Bitmap(TOTAL_WIDTH, TOTAL_HEIGHT); Graphics g = Graphics.FromImage(fullImage); Pen linePen = new Pen(lineColor, 0.5f); Pen middleLinePen = new Pen(middleLineColor, 0.5f); Pen labelPen = new Pen(labelColor, 1); Pen tickPen = new Pen(tickColor, 1); // Draw a rectangular box marking the boundaries of the graph Rectangle rectOuter = new Rectangle(0, 0, TOTAL_WIDTH, TOTAL_HEIGHT); Brush fillBrushOuter = new SolidBrush(fillOuterColor); g.FillRectangle(fillBrushOuter, rectOuter); // Create rectangle. Rectangle rect = new Rectangle(LEFT, TOP, WIDTH, HEIGHT); Brush fillBrush = new SolidBrush(fillColor); g.FillRectangle(fillBrush, rect); g.DrawRectangle(linePen, rect); // Label for horizontal axis Font drawLabelFont = new Font("Arial", 8); SolidBrush drawLabelBrush = new SolidBrush(labelPen.Color); if (drawLabels) { SizeF drawLabelTextSize = g.MeasureString(LABEL_X, drawLabelFont); g.DrawString(LABEL_X, drawLabelFont, drawLabelBrush, (TOTAL_WIDTH/2) - (drawLabelTextSize.Width/2), TOTAL_HEIGHT - drawLabelFont.GetHeight(g) - 5); } float y = 0; float yMiddle = 0; float x = 0; float xMiddle = 0; if (!doLogScale) { // LINEAR SCALE // Tick marks on the vertical axis for ( float freqTick = MIN_FREQ; freqTick <= MAX_FREQ; freqTick += FREQ_STEP ) { // draw horozontal main line y = BOTTOM - FREQTOPIXEL*(freqTick-MIN_FREQ); if (y < BOTTOM && y > TOP+1) { g.DrawLine(linePen, LEFT-2, y, LEFT+WIDTH+2, y); } // draw horozontal middle line (between the main lines) yMiddle = y-(FREQTOPIXEL*FREQ_STEP)/2; if (yMiddle > TOP && yMiddle < HEIGHT+TOP) { g.DrawLine(middleLinePen, LEFT, yMiddle, LEFT+WIDTH, yMiddle); } if ( freqTick != MAX_FREQ ) { // Numbers on the tick marks Font drawFont = new Font("Arial", 8); SolidBrush drawBrush = new SolidBrush(tickPen.Color); // left g.DrawString(MathUtils.FormatNumber((int) freqTick), drawFont, drawBrush, LEFT - 33, y - drawFont.GetHeight(g)/2); // right g.DrawString(MathUtils.FormatNumber((int) freqTick), drawFont, drawBrush, WIDTH + LEFT + 4, y - drawFont.GetHeight(g)/2); } } } else { // LOG SCALE for (int i = 0; i < logFrequencies.Length; i+=20) { float freqTick = logFrequencies[i]; y = BOTTOM - i; // draw horozontal main line if (y < BOTTOM && y > TOP+1) { g.DrawLine(linePen, LEFT-2, y, LEFT+WIDTH+2, y); } // Numbers on the tick marks Font drawFont = new Font("Arial", 8); SolidBrush drawBrush = new SolidBrush(tickPen.Color); // left g.DrawString(MathUtils.FormatNumber((int) freqTick), drawFont, drawBrush, LEFT - 33, y - drawFont.GetHeight(g)/2); // right g.DrawString(MathUtils.FormatNumber((int) freqTick), drawFont, drawBrush, WIDTH + LEFT + 4, y - drawFont.GetHeight(g)/2); } } if (drawLabels) { // Label for vertical axis StringFormat format = new StringFormat(); format.Alignment = StringAlignment.Center; g.TranslateTransform(g.VisibleClipBounds.Width, 0); g.RotateTransform(270); g.DrawString(LABEL_Y, drawLabelFont, drawLabelBrush, -(TOTAL_HEIGHT/2), -TOTAL_WIDTH + 5, format); g.ResetTransform(); } // Tick marks on the horizontal axis for ( float timeTick = MIN_TIME; timeTick <= MAX_TIME; timeTick += TIME_STEP ) { // draw vertical main line x = LEFT + TIMETOPIXEL*(timeTick-MIN_TIME); if (x > LEFT && x < WIDTH) { g.DrawLine(linePen, x, BOTTOM+2, x, TOP-2); } // draw vertical middle line (between the main lines) xMiddle = x + TIMETOPIXEL*TIME_STEP/2; if (xMiddle < WIDTH+LEFT) { g.DrawLine(middleLinePen, xMiddle, BOTTOM, xMiddle, TOP); } if ( timeTick != MIN_TIME && timeTick != MAX_TIME ) { // Numbers on the tick marks Font drawFont = new Font("Arial", 8); SolidBrush drawBrush = new SolidBrush(tickPen.Color); SizeF drawTimeTickTextSize = g.MeasureString("" + timeTick, drawFont); // top g.DrawString("" + timeTick, drawFont, drawBrush, x-(drawTimeTickTextSize.Width/2), TOP - 15); // bottom g.DrawString("" + timeTick, drawFont, drawBrush, x-(drawTimeTickTextSize.Width/2), BOTTOM + 2); } } // draw spectrogram Bitmap spectrogramImage = new Bitmap(WIDTH, HEIGHT); // calculate min and max double max = spectrogram.Max((b) => b.Max((v) => Math.Abs(v))); double min = spectrogram.Min((b) => b.Min((v) => Math.Abs(v))); int numberOfSamplesX = spectrogram.Length; // time int numberOfSamplesY = spectrogram[0].Length; // hz double deltaX = (double) (WIDTH - 1)/(numberOfSamplesX); // By how much the image will move to the left double deltaY = (double) (HEIGHT- 1)/(numberOfSamplesY); // By how much the image will move upward int prevX = 0; Color prevColor = Color.Black; for (int i = 0; i < numberOfSamplesX; i++) { double xCoord = i*deltaX; if ((int) xCoord == prevX) continue; for (int j = 0; j < numberOfSamplesY; j++) { float amplitude = spectrogram[i][j]; Color colorbw = Color.Black; if (amplitude > 0) { float dB = MathUtils.AmplitudeToDecibel(amplitude, minDb, maxDb); int colorval = (int) MathUtils.ConvertAndMainainRatio(dB, minDb, maxDb, 0, 255); // 255 is full brightness, and good for REW colors (for SOX 220 is good, and for PHOTOSOUNDER 245 seems good) colorbw = Color.FromArgb(colorval, colorval, colorval); //colorbw = ValueToBlackWhiteColor(amplitude, max*0.010); prevColor = colorbw; } else { colorbw = prevColor; } spectrogramImage.SetPixel((int) xCoord + 1, HEIGHT - (int) (deltaY*j) - 1, colorbw); } prevX = (int) xCoord; } if (colorPalette != ColorUtils.ColorPaletteType.BLACK_AND_WHITE) { spectrogramImage = ColorUtils.Colorize(spectrogramImage, 255, colorPalette); } // add the spectrogram to the full image g.DrawImage(spectrogramImage, LEFT, TOP); return fullImage; }
/// <summary> /// Utility method to return spectrogram image using audio data /// </summary> /// <param name="audioData"></param> /// <param name="width"></param> /// <param name="height"></param> /// <param name="sampleRate"></param> /// <param name="fftWindowsSize"></param> /// <param name="fftOverlap"></param> /// <param name="colorPalette"></param> /// <param name="doLogScale"></param> /// <returns>Spectrogram image</returns> public static Bitmap GetSpectrogramImage(float[] audioData, int width, int height, double sampleRate, int fftWindowsSize, int fftOverlap, ColorUtils.ColorPaletteType colorPalette, bool doLogScale) { float[][] spectrogram; double minFrequency = 27.5; double maxFrequency = sampleRate / 2; int logBins = height - 2*40; // the margins used int[] logFrequenciesIndex = new int[1]; float[] logFrequencies = new float[1]; // find the time int numberOfSamples = audioData.Length; double seconds = numberOfSamples / sampleRate; if (!doLogScale) { spectrogram = CreateSpectrogramLomont(audioData, fftWindowsSize, fftOverlap); } else { // calculate the log frequency index table GetLogFrequenciesIndex(sampleRate, minFrequency, maxFrequency, logBins, fftWindowsSize, LogBase, out logFrequenciesIndex, out logFrequencies); spectrogram = CreateLogSpectrogramLomont(audioData, fftWindowsSize, fftOverlap, logBins, logFrequenciesIndex, logFrequencies); } return GetSpectrogramImage(spectrogram, width, height, seconds*1000, sampleRate, colorPalette, doLogScale, logFrequenciesIndex, logFrequencies); }