spectrogram 을 완성했다.
스스로도 꽤나 무식하고 단순하게 구현하였다는 자각이 있으므로, 해당 부분의 태클은 삼가해 주기를...ㅠ
실 사용을 위해서는 상당히 많은 부분에서 손을 대야 할 것 같은데, 어차피 중단된(내 손에서 떠난) 프로젝트라서 뭐... 흐.
포기할 때는 꽤 아까웠는데 하면서 진짜 이거 하면서 느끼기를, 구현은 할 수 있는데(어쨌든 하루만에 만들었으니) '예쁘게' 만드는 데에 얼마나 걸릴지 짐작이 안 갔다.
포기하길 잘한 것 같다. 어휴.
아무튼간에, 굉장히 비효율적이나마 구현은 해냈다.
NAudio 예제를 보면서 작업하였는데, 여기서 코드를 가져다가 쓰는 바람에 불필요하게 코드가 복잡해졌다.
원본은 spectrogram이 아니라 spectrum analyzer 였기에 '매 순간' 그려지는 것이 필요했는데, 내 경우에는 처음에 모든 샘플을 읽을 필요가 있었다.
그래서 원본은 이벤트를 발생시키기 위한 클래스를 따로 두고 이벤트를 발생, 컨트롤에서 그걸 받아서 그리도록 되어있는데, 사실 내 경우에는 그냥 하나의 클래스에서 파일을 읽고 그려내면 될 것 같다.
그러나 이걸 고치기에는 어려우니까 손을 못 대겠다. 성능상의 문제도 따로 없고...
오히려 성능상의 문제라면 다른 쪽에 좀 있는데, 그건 후임자를 믿어보는 걸로.
말보다 코드가 나을 것이다.
유저 컨트롤 코드를 보자.
SpectrogramControl.xaml.cs 이다.
using NAudio.Dsp;
using NAudio.Wave;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SoundWingTest.CustomControl
{
/// <summary>
/// SpectrogramControl.xaml에 대한 상호 작용 논리
/// </summary>
public partial class SpectrogramControl : UserControl
{
AudioFileReader fileStream;
int ChannelCount;
const int BITMAP_WIDTH = 1200;
const int BITMAP_HEIGHT = 150;
private const int binsPerPoint = 2; // reduce the number of points we plot for a less jagged line?
byte[,,] pixelArray;
WriteableBitmap writeableBitmap;
public event EventHandler<FftEventArgs> FftCalculated;
int xPos = 0;
public SpectrogramControl()
{
InitializeComponent();
}
public SpectrogramControl(AudioFileReader fileStream, int ChannelCount)
{
InitializeComponent();
this.fileStream = fileStream;
this.ChannelCount = ChannelCount;
Debug.WriteLine("spectrogram control");
DrawSpectrogram();
}
//파일의 정보(샘플)을 읽어들인다.
private void loadFile()
{
long length = fileStream.Length;
long sampleCount = length / 4; //float은 4byte, float PCM
//fft length에 따라서 spectrogram 의 width가 변화한다.
int fftlen = 128;
int resultWidth = (int)(sampleCount / fftlen / fileStream.WaveFormat.Channels); //예상되는 fft block의 수, spectogram width.
while(resultWidth > BITMAP_WIDTH)//화면 길이를 넘지 않도록 fft length를 조정.
{
fftlen *= 2;
resultWidth = (int)(sampleCount / fftlen / fileStream.WaveFormat.Channels);
}
Debug.WriteLine("resultWidth ? : " + resultWidth + " fft len : ? " + fftlen);
//sampleAggregator 클래스를 이용하여 fft를 진행한다.
SampleAggregator sampleAggregator = new SampleAggregator(fileStream, ChannelCount, fftlen);
sampleAggregator.PerformFFT = true;
sampleAggregator.FftCalculated += (s, a) => FftCalculated?.Invoke(this, a);
this.FftCalculated += audioGraph_FftCalculated;
var samplesPerSecond = (fileStream.WaveFormat.SampleRate * fileStream.WaveFormat.Channels);
int blockAlign = fileStream.WaveFormat.BlockAlign;
if (samplesPerSecond % blockAlign != 0)
{
Debug.WriteLine("wrong align. sps : " + samplesPerSecond + " , align : " + blockAlign);
samplesPerSecond += samplesPerSecond % blockAlign;
}
var readBuffer = new float[samplesPerSecond];
int samplesRead;
do
{
samplesRead = sampleAggregator.Read(readBuffer, 0, samplesPerSecond);
} while (samplesRead > 0);
//원래 위치로 복귀
fileStream.Position = 0;
}
//적절한 y 좌표를 가져온다
private int getYPos(int len, int n)
{
double percent = (double)n / (len / 2);
int y = (int)(BITMAP_HEIGHT * percent);
if (y >= BITMAP_HEIGHT)
y = BITMAP_HEIGHT-1;
y = BITMAP_HEIGHT - (y + 1);
return y;
}
//FFT 이벤트. fft length만큼의 sample이 모여서 연산되었을 때 발생.
//여기서 bitmap 을 만든다. 한 번의 이벤트에서 세로줄 하나(1pixel)의 그림을 그리고 있음.
void audioGraph_FftCalculated(object sender, FftEventArgs e)
{
if (e.Channel != ChannelCount)
return;
Complex[] fftResults = e.Result;
for (int n = 0; n < fftResults.Length / 2; n += binsPerPoint)
{
// averaging out bins
double color = 0;
for (int bin = 0; bin < binsPerPoint; bin++)
{
color += GetColorLog(fftResults[n + bin]);
}
//RGB 색상값 계산
int colorsum = (int)color;
int blue, green, red;
blue = colorsum - 512;
blue = blue < 0 ? 0 : blue;
blue = blue >= 256 ? 255 : blue;
if (colorsum > 512)
{
green = 255;
}
else if (colorsum < 256)
{
green = 0;
}
else
{
green = (colorsum % 512) - 256;
green = green < 0 ? 0 : green;
green = green >= 256 ? 255 : green;
}
red = colorsum > 256 ? 255 : colorsum;
pixelArray[getYPos(fftResults.Length, n), xPos, 0] = (Byte)blue;//B
pixelArray[getYPos(fftResults.Length, n), xPos, 1] = (Byte)green;//G
pixelArray[getYPos(fftResults.Length, n), xPos, 2] = (Byte)red;//R
}
//다음 픽셀로
xPos++;
}
//spectrogram 그리기
private void DrawSpectrogram()
{
writeableBitmap = new WriteableBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, 96, 96, PixelFormats.Bgra32, null);
pixelArray = new byte[BITMAP_HEIGHT, BITMAP_WIDTH, 4];
//초기화, rgb는 0으로, a는 255로.
for (int y = 0; y < BITMAP_HEIGHT; y++)
{
for (int x = 0; x < BITMAP_WIDTH; x++)
{
for (int i = 0; i < 3; i++)
{
pixelArray[y, x, i] = 0;
}
pixelArray[y, x, 3] = 255;
}
}
//파일 읽어들이기
loadFile();
Debug.WriteLine("spectrogram sameple load fin");
//비트맵으로 변환하여 화면에 띄운다
byte[] byteArray = new byte[BITMAP_HEIGHT * BITMAP_WIDTH * 4];
int index = 0;
for (int row = 0; row < BITMAP_HEIGHT; row++)
{
for (int col = 0; col < BITMAP_WIDTH; col++)
{
for (int i = 0; i < 4; i++)
{
byteArray[index++] = pixelArray[row, col, i];
}
}
}
Int32Rect rectangle = new Int32Rect(0, 0, BITMAP_WIDTH, BITMAP_HEIGHT);
int stride = 4 * BITMAP_WIDTH;
writeableBitmap.WritePixels(rectangle, byteArray, stride, 0);
Image image = new Image();
image.Stretch = Stretch.None;
image.Margin = new Thickness(0);
this.mainCanvas.Children.Add(image);
image.Source = writeableBitmap;
}
//색상 값 계산. 진폭의 크기.
private double GetColorLog(Complex c)
{
// not entirely sure whether the multiplier should be 10 or 20 in this case.
// going with 10 from here http://stackoverflow.com/a/10636698/7532
double intensityDB = 10 * Math.Log10(Math.Sqrt(c.X * c.X + c.Y * c.Y));
double minDB = -90;
if (intensityDB < minDB) intensityDB = minDB;
double percent = intensityDB / minDB;
// we want 0dB to be at the top (i.e. yPos = 0)
// double yPos = percent * ActualHeight;
double yPos = percent * 384;
return yPos;
}
}
}
Colored by Color Scripter
이렇다.
여기저기 하드코딩이 박혀있고(widht, height 등) fft length 결정도 대강이라서 width가 딱 맞질 않고 적당히 1200 안으로만 떨어지게 짜다 보니, 601 같은 경우도 생기더라...ㅋㅋㅋㅋㅋ;;;
확실히 시각화 쪽에서는 scale이 일이다. 사실 waveform에서도 똑같은 문제가 있는데... 흐.
xaml 코드는 매우 단순하다.
<UserControl x:Class="SoundWingTest.CustomControl.SpectrogramControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SoundWingTest.CustomControl"
xmlns:soundwingtest="clr-namespace:SoundWingTest"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<StackPanel Orientation="Horizontal">
<Border BorderBrush="Black" BorderThickness="1">
<soundwingtest:AxisVerticalControl Width="50" DockPanel.Dock="Left"></soundwingtest:AxisVerticalControl>
</Border>
<Border BorderBrush="Black" BorderThickness="1">
<Canvas x:Name="mainCanvas" Height="150" Width="1200" Background="Cornsilk" >
</Canvas>
</Border>
</StackPanel>
</UserControl>
Colored by Color Scripter
중요한 건 하나도 없으니 무시해도 좋다.
중요한 부분인 SampleAggregator 클래스.
샘플을 모아서 fft 연산을 하고 이벤트를 발생시킨다.
using NAudio.Dsp;
using NAudio.Wave;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SoundWingTest
{
public class SampleAggregator : ISampleProvider
{
// volume
public event EventHandler<MaxSampleEventArgs> MaximumCalculated;
private float maxValue;
private float minValue;
public int NotificationCount { get; set; }
int count;
// FFT
public event EventHandler<FftEventArgs> FftCalculated;
public bool PerformFFT { get; set; }
private readonly Complex[] fftBuffer;
private readonly FftEventArgs fftArgs;
private int fftPos;
private readonly int fftLength;
private readonly int m;
private readonly ISampleProvider source;
private readonly int channels;
int Channel;
//원하는 채널 번호와 fft length를 받아, fft 결과물을 이벤트로 넘겨준다.
public SampleAggregator(ISampleProvider source, int channel, int fftLength = 1024)
{
Channel = channel;
channels = source.WaveFormat.Channels;
if (!IsPowerOfTwo(fftLength))
{
throw new ArgumentException("FFT Length must be a power of two");
}
m = (int)Math.Log(fftLength, 2.0);
this.fftLength = fftLength;
fftBuffer = new Complex[fftLength];
fftArgs = new FftEventArgs(fftBuffer, Channel);//channel select
this.source = source;
}
static bool IsPowerOfTwo(int x)
{
return (x & (x - 1)) == 0;
}
public void Reset()
{
count = 0;
maxValue = minValue = 0;
}
private void Add(float value)
{
if (PerformFFT && FftCalculated != null)
{
//Y는 허수부분. 오디오에 허수는 없으므로 Y는 0
fftBuffer[fftPos].X = (float)(value * FastFourierTransform.HammingWindow(fftPos, fftLength));
fftBuffer[fftPos].Y = 0;
fftPos++;
//fft length만큼의 sample이 모이면 fft 연산을 하고 이벤트를 발생시킨다.
if (fftPos >= fftBuffer.Length)
{
fftPos = 0;
FastFourierTransform.FFT(true, m, fftBuffer);
FftCalculated(this, fftArgs);
}
}
maxValue = Math.Max(maxValue, value);
minValue = Math.Min(minValue, value);
count++;
if (count >= NotificationCount && NotificationCount > 0)
{
MaximumCalculated?.Invoke(this, new MaxSampleEventArgs(minValue, maxValue));
Reset();
}
}
public WaveFormat WaveFormat => source.WaveFormat;
public int Read(float[] buffer, int offset, int count)
{
var samplesRead = source.Read(buffer, offset, count);
for (int n = 0; n < samplesRead; n += channels)
{
//원하는 채널의 데이터만 가져온다.
Add(buffer[n + offset+ Channel-1]);
}
return samplesRead;
}
}
public class MaxSampleEventArgs : EventArgs
{
[DebuggerStepThrough]
public MaxSampleEventArgs(float minValue, float maxValue)
{
MaxSample = maxValue;
MinSample = minValue;
}
public float MaxSample { get; private set; }
public float MinSample { get; private set; }
}
public class FftEventArgs : EventArgs
{
[DebuggerStepThrough]
public FftEventArgs(Complex[] result, int channel)
{
Result = result;
Channel = channel;
}
public Complex[] Result { get; private set; }
public int Channel { get; private set; }
}
}
Colored by Color Scripter
원본 예제는 재생 중에 이 기능을 쓰니까, 일종의 signal chain 에 편입시키기 위해서 이렇게 따로 클래스를 만들지만 내 경우에는 그럴 필요는 없는데... 수정하기에 시간이 좀 걸리지 싶었다.
'개발언어 > NAudio' 카테고리의 다른 글
[NAudio] 반복재생하기 (0) | 2021.07.08 |
---|---|
[NAudio] 재생 속도 조절 (0) | 2021.07.08 |
[NAudio] 6. Recording Audio (0) | 2021.07.08 |
[NAudio] 5. Working With Codecs (0) | 2021.07.08 |
[NAuidio] 4. Changing Wave Formats (0) | 2021.07.08 |