W poprzednim artykule omówiłem podstawowe funkcje biblioteki NAudio. Przedstawiłem w jaki sposób odczytywać pliki audio i je odtwarzać. Dzisiaj zajmiemy się innymi przydatnymi funkcjami jakie udostępnia nam biblioteka.
Jedną z bardzo przydatnych funkcji jest konwersja formatów. Będzie to konwersja do formatu WAV. Do tego celu używamy klasy WaveFormatConversionStream. Klasa ta udostępnia nam jeden konstruktor.
public WaveFormatConversionStream(WaveFormat targetFormat, WaveStream sourceStream);
Pierwszy parametr opisuje format docelowego pliku WAV, a drugi stanowi strumień źródłowych danych.
Klasa WaveFormat zawiera konstruktor, który przyjmuje trzy parametry:
- częstotliwość próbkowania
- rozdzielczość bitowa
- ilość kanałów
public WaveFormat(int rate, int bits, int channels);
Gdy już mamy zdefiniowany obiekt klasy WaveFormatConversionStream, możemy przystąpić do samej konwersji. Do zapisu danych ze strumienia audio do pliku WAV, użyjemy statycznej metody CreateWaveFile klasy WaveFileWriter.
Funkcja konwertująca plik MP3 do WAV wygląda następująco:
private void ConvertMp3File(string inFileName, string outFileName, int rate, int bits, int channels)
{
using (Mp3FileReader reader = new Mp3FileReader(inFileName))
{
using (WaveFormatConversionStream wfcs = new WaveFormatConversionStream(new WaveFormat(rate, bits, channels), reader))
{
WaveFileWriter.CreateWaveFile(outFileName, wfcs);
}
}
}
Kolejną klasą wartą omówienia jest klasa WaveMixerStream32. Umożliwia ona sumowanie (miksowanie) wielu strumieni audio. Klasa ta posiada dwa konstruktory – jeden bezparametrowy, oraz drugi w którym przekazujemy kolekcję strumieni audio oraz wartość bool określającą czy strumień ma się zakończyć gdy dane ze wszystkich dodanych strumieni zostaną odczytane.
Strumienie do obiektu klasy WaveMixerStream32 dodajemy za pomocą metody AddInputStream. Należy pamiętać, że wszystkie strumienie muszą być tego samego formatu audio, czyli częstotliwość próbkowania, rozdzielczość bitowa i ilość kanałów muszą być w każdym strumieniu takie same. Zaglądając do źródeł biblioteki widzimy, że funkcja AddInputStream ustala format na podstawie pierwszego dodanego strumienia i nie pozwala na dodanie kolejnego strumienia o odmiennym formacie:
public void AddInputStream(WaveStream waveStream)
{
if (waveStream.WaveFormat.Encoding != WaveFormatEncoding.IeeeFloat)
throw new ArgumentException("Must be IEEE floating point", "waveStream");
if (waveStream.WaveFormat.BitsPerSample != 32)
throw new ArgumentException("Only 32 bit audio currently supported", "waveStream");
if (inputStreams.Count == 0)
{
// first one - set the format
int sampleRate = waveStream.WaveFormat.SampleRate;
int channels = waveStream.WaveFormat.Channels;
this.waveFormat = WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channels);
}
else
{
if (!waveStream.WaveFormat.Equals(waveFormat))
throw new ArgumentException("All incoming channels must have the same format", "waveStream");
}
lock (inputsLock)
{
this.inputStreams.Add(waveStream);
this.length = Math.Max(this.length, waveStream.Length);
// get to the right point in this input file
waveStream.Position = Position;
}
}
Poniżej przykład zastosowania klasy WaveMixerStream32 do zmiksowania dwóch plików audio (dowolnego formatu) do pliku wynikowego (WAV):
private void Mix2FilesTo1(string fileName1, string fileName2, string outputFile)
{
using (NAudio.Wave.WaveMixerStream32 mixStream = new NAudio.Wave.WaveMixerStream32())
{
NAudio.Wave.AudioFileReader afr1 = new AudioFileReader(fileName1);
NAudio.Wave.AudioFileReader afr2 = new AudioFileReader(fileName2);
mixStream.AddInputStream(afr1);
mixStream.AddInputStream(afr2);
WaveFileWriter.CreateWaveFile(outputFile, mixStream);
}
}
Należy mieć na uwadze, że wywołanie metody Dispose() na obiekcie klasy WaveMixerStream32 spowoduje wywołanie metody Dispose() na wszystkich obiektach reprezentujących strumienie wejściowe.
NAudio poprzez swoją modułową budowę umożliwia również łatwe implementowanie funkcjonalności typu DSP. Tworząc klasy dziedziczące po klasie WaveStream i umieszczając algorytmy przetwarzające w nadpisanej metodzie Read, możemy implementować funkcje DSP. Razem z biblioteką dostarczony jest doskonały przykład takiej funkcjonalności w postaci klasy SimpleCompressorStream – jest to prosty kompresor dynamiki. Zaglądając do źródeł, widzimy w jaki sposób realizowane jest przetwarzanie sygnału:
public override int Read(byte[] array, int offset, int count)
{
lock (lockObject)
{
if (Enabled)
{
if (sourceBuffer == null || sourceBuffer.Length < count)
sourceBuffer = new byte[count];
int sourceBytesRead = sourceStream.Read(sourceBuffer, 0, count);
int sampleCount = sourceBytesRead / (bytesPerSample * channels);
for (int sample = 0; sample < sampleCount; sample++)
{
int start = sample * bytesPerSample * channels;
double in1;
double in2;
ReadSamples(sourceBuffer, start, out in1, out in2);
simpleCompressor.Process(ref in1, ref in2);
WriteSamples(array, offset + start, in1, in2);
}
return count;
}
else
{
return sourceStream.Read(array, offset, count);
}
}
}
Witać tutaj, że przetwarzanie (czyli wywołanie simpleCompressor.Process… ) jest umieszczone w nadpisanej metodzie Read.
Wykorzystanie klasy SimpleCompressorStream jest analogiczne jak w poprzednich przypadkach. Poniżej funkcja odtwarzająca plik audio z zaaplikowanym kompresorem dynamiki. Oczywiście pamiętać należy o zwolnieniu zasobów po zakończeniu odtwarzania. Polecam eksperymenty z doborem parametrów Attack, Release, Ratio, Threshold oraz MakeUpGain w obiekcie klasy SimpleCompressorStream.
NAudio.Wave.IWavePlayer waveOutDevice = new NAudio.Wave.WaveOut();
NAudio.Wave.AudioFileReader audioFileReader;
NAudio.Wave.SimpleCompressorStream compressor;
private void PlayAudioFile(string fileName)
{
try
{
CloseWaveOut();
waveOutDevice = new NAudio.Wave.WaveOut();
audioFileReader = new NAudio.Wave.AudioFileReader(fileName);
compressor = new SimpleCompressorStream(audioFileReader);
waveOutDevice.Init(compressor);
waveOutDevice.Play();
}
catch (Exception ex)
{
MessageBox.Show(string.Format("Wystąpił błąd: {0}", ex.Message));
}
}
W niniejszym artykule starałem się przedstawić najczęściej używane funkcje biblioteki NAudio. Zachęcam czytelnika do zapoznania się z tą biblioteką, a także do analizy jej kodów źródłowych z których można się dowiedzieć wiele na temat przetwarzania danych audio.
Świetna seria o NAudio. Mam nadzieje, że będzie Pan kontynuował 🙂
Bardzo ciekawy artykuł. Spróbowałem skompliować klasę SimpleCompresorStream – wszystko działa ok ale nie wiem jak w czasie odtwarzania zmieniać właściwość Enabled (np. poprzez checkbox’a). Móglby Pan cos podpowiedzieć?