W poprzednim artykule przedstawiłem niskopoziomowe podejście do odtwarzania dźwięków w Windows, używając natywnego interfejsu WINAPI. Tym razem postaram się zaprezentować inne podejście do tego tematu. Użyję platformy .NET oraz biblioteki NAudio. Biblioteka autorstwa Marka Heatha jest chyba najpopularniejszym i najbardziej rozbudowanym narzędziem do pracy z dźwiękiem w środowisku .NET. Projekt NAudio jest prowadzony na zasadzie open source, mamy więc dostęp do źródeł tej biblioteki – można je odnaleźć pod adresem https://github.com/naudio/NAudio
Jak już wspomniałem możliwości tej biblioteki są bardzo duże – według dokumentacji umożliwia ona:
Odtwarzanie audio poprzez następujące interfejsy:
- WaveOut
- DirectSound
- ASIO
- WASAPI (od Windows Vista)
Odczytywanie plików w następujących formatach:
- WAV
- AIFF
- WMA
- SoundFont (SF2)
Dekodowanie skompresowanych danych:
- MP3
- G.711 mu-law i a-law
- ADPCM
- G.722
- Speex (NSpeex)
- WMA, AAC, MP4
Konwersja nieskompresowanego audio:
- Zmiana ilości kanałów (mono – stereo, stereo – mono)
- Zmiana rozdzielczości bitowej
- Zmiana częstotliwości próbkowania (resampling)
Kodowanie danych audio (w zależności od kodeków zainstalowanych w systemie):
- Tworzenie plików MP3s (od Windows 8)
- Tworzenie plików AAC/MP4 audio (od Windows 7)
- Tworzenie plików WMA
- Tworzenie plików WAV (G.711, ADPCM, G.722, i inne…)
Działania na strumieniach audio:
- Analiza poziomów audio
- Przetwarzanie FFT (Fast Fourier Transformation)
- Proste efekty (delay, loop, fade in, fade out)
- Proste EQ
Nagrywanie z użyciem interfejsów:
- WaveIn
- WASAPI
- ASIO
Pełna obsługa MIDI:
- Odczyt i zapis plików MIDI
- Odbieranie zdarzeń MIDI
- Wysyłanie zdarzeń MIDI
Na potrzeby tego artykułu zaimplementujemy bardzo prosty odtwarzacz plików MP3. Będzie on wymagał systemu Windows Vista lub nowszego, z uwagi na wykorzystane funkcje (Core Audio Interface, które zostało wprowadzone w Windows Vista).
Rozpoczynamy od utworzenia nowego projektu (Windows Forms) i dodania referencji do omawianej biblioteki NAudio. Następnie musimy utworzyć globalny obiekt który będzie reprezentował nasze urządzenie odtwarzające. Wykorzystamy do tego celu interfejs IWavePlayer znajdujący się w przestrzeni nazw NAudio.Wave. Klasa WaveOut implementuje interfejs IWavePlayer, a zaglądając do jej źródeł widzimy, że opakowuje ona funkcje z rodziny waveOut* które poznaliśmy w poprzednim artykule.
NAudio.Wave.IWavePlayer waveOutDevice = new NAudio.Wave.WaveOut();
Kolejnym krokiem będzie utworzenie globalnego obiektu, który będzie odczytywał dane z naszego pliku MP3.
NAudio.Wave.AudioFileReader audioFileReader;
Skorzystamy z klasy AudioFileReader, która w konstruktorze przyjmuje nazwę pliku audio. Klasa AudioFileReader, dziedziczy po klasie WaveStream, a ta z kolei wywodzi się od klasy Stream. Używając klasy AudioFileReader uzyskujemy strumień już zdekodowanych danych audio. Już tutaj widać siłę biblioteki NAudio. Nie musimy się martwić o obsługę danego formatu – klasa AudioFileReader sama wybierze klasę odpowiednią do obsługi danego formatu. Widać to, gdy zajrzymy do jej źródeł – poniżej wycinek klasy AudioFileReader odpowiadający za dobór odpowiedniej klasy do odczytywania konkretnego formatu. :
private void CreateReaderStream(string fileName)
{
if (fileName.EndsWith(".wav", StringComparison.OrdinalIgnoreCase))
{
readerStream = new WaveFileReader(fileName);
if (readerStream.WaveFormat.Encoding != WaveFormatEncoding.Pcm && readerStream.WaveFormat.Encoding != WaveFormatEncoding.IeeeFloat)
{
readerStream = WaveFormatConversionStream.CreatePcmStream(readerStream);
readerStream = new BlockAlignReductionStream(readerStream);
}
}
else if (fileName.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase))
{
readerStream = new Mp3FileReader(fileName);
}
else if (fileName.EndsWith(".aiff"))
{
readerStream = new AiffFileReader(fileName);
}
else
{
readerStream = new MediaFoundationReader(fileName);
}
}
Skoro mamy już gotowy strumień danych audio, wystarczy przekazać go do obiektu reprezentującego urządzenie odtwarzające. W tym celu używamy metody Init klasy WaveOut i w parametrze tej metody przekazujemy strumień danych audio, czyli obiekt klasy AudioFileReader.
Następnie po wywołaniu metody Play() na obiekcie klasy WaveOut, możemy cieszyć się z efektów naszej pracy – w głośnikach słychać odtwarzany plik (oczywiście przy założeniu, że nie wystąpiły żadne błędy).
Wszystkie te operacje umieścimy w metodzie PlayAudioFile, która wygląda następująco:
private void PlayAudioFile(string fileName)
{
try
{
CloseWaveOut(); //Na wszelki wypadek czyścimy zasoby po poprzednim odtwarzaniu
waveOutDevice = new NAudio.Wave.WaveOut();
audioFileReader = new NAudio.Wave.AudioFileReader(fileName);
waveOutDevice.Init(audioFileReader);
waveOutDevice.Play();
}
catch (Exception ex)
{
MessageBox.Show(string.Format("Wystąpił błąd: {0}", ex.Message));
}
}
Należy również przygotować metodę, która pozamyka otwarte zasoby. Funkcję tą pełni metoda CloseWaveOut() przedstawiona poniżej:
private void CloseWaveOut()
{
if (waveOutDevice != null)
{
waveOutDevice.Stop();
}
if (audioFileReader != null)
{
audioFileReader.Dispose();
audioFileReader = null;
}
if (waveOutDevice != null)
{
waveOutDevice.Dispose();
waveOutDevice = null;
}
}
Klasa WaveOut udostępnia również inne metody i właściwości, np. Stop(), Pause(), Volume itp.. które służą do sterowania odtwarzaniem. Ich nazwy mówią bardzo wiele i myślę, że nikt nie będzie miał problemu z ich użyciem.
Klasa AudioFileReader umożliwia natomiast poruszanie się po odtwarzanym pliku dokładnie tak samo jak w każdym obiekcie klasy Stream. Na przykład chcąc przeskoczyć do dowolnego miejsca w strumieniu danych (co może być przydatne w budowie odtwarzacza), wywołujemy metodę Seek(). Do odczytania pozycji czasowej w strumieniu audio, służy właściwość CurrentTime. Właściwość ta zwraca strukturę TimeSpan, nie ma więc potrzeby wyliczania czasu na podstawie ilości próbek, częstotliwości próbkowania itp…
Powyższe funkcje wystarczą do zaimplementowania bardzo prostego odtwarzacza, jednak chciałbym przedstawić jeszcze jeden ciekawy element omawianej biblioteki, a mianowicie dostęp do Core Audio Api wprowadzonego w Windows Vista. Dokładny opis tego elementu systemu znajdziemy na stronie MSDN: https://msdn.microsoft.com/en-us/library/windows/desktop/dd370784(v=vs.85).aspx
Dzięki dostępowi do Core Audio Api mamy możliwość np. zaimplementowania wskaźnika wysterowania w bardzo prosty sposób.
W tym celu tworzymy globalny obiekt klasy MMDevice reprezentujący kartę dźwiękową:
NAudio.CoreAudioApi.MMDevice defaultDevice;
Następnie przypisujemy do niego domyślne urządzenie dźwiękowe korzystając z klasy MMDeviceEnumerator:
NAudio.CoreAudioApi.MMDeviceEnumerator devEnum = new NAudio.CoreAudioApi.MMDeviceEnumerator();
defaultDevice = devEnum.GetDefaultAudioEndpoint( NAudio.CoreAudioApi.DataFlow.Render, NAudio.CoreAudioApi.Role.Multimedia);
Dzięki obiektowi defaultDevice mamy dostęp do sterowania parametrami miksera systemowego, możemy również odczytywać parametry i sterować „sesjami” audio egzystującymi w naszym systemie.
Dane sesji naszego procesu możemy odczytać z obiektu defaultDevice.AudioSessionManager.AudioSessionControl. Aby dostać się do aktualnych wartości poziomu audio w danym kanale odczytujemy wartości PeakValues właściwości AudioMeterInformation:
defaultDevice.AudioSessionManager.AudioSessionControl.AudioMeterInformation.PeakValues[channelNo] //channelNo - numer kanału (w przypadku stereo mamy dwa kanały - 0 i 1)
Wskaźnik wysterowania najprościej można zaimplementować używając kontrolki ProgressBar (w WinForms). Poniższa metoda UpdateMeters aktualizuje wartości kontrolek w zależności od poziomu audio. Metodę tą można wywoływać np. co 10 ms używając timera z WinForms.
private void UpdateMeters()
{
if (defaultDevice.AudioSessionManager.AudioSessionControl.State == NAudio.CoreAudioApi.Interfaces.AudioSessionState.AudioSessionStateActive)
{
progressBar1.Value = (int)( defaultDevice.AudioSessionManager.AudioSessionControl.AudioMeterInformation.PeakValues[0] * progressBar1.Maximum);
progressBar2.Value = (int)( defaultDevice.AudioSessionManager.AudioSessionControl.AudioMeterInformation.PeakValues[1] * progressBar2.Maximum);
}
}
Jak widać na powyższym przykładzie, biblioteka NAudio dostarcza nam wielu narzędzi do pracy z dźwiękiem i to w bardzo przyjaznej formie. Zaprezentowane tutaj funkcje to tylko wierzchołek góry lodowej jeśli chodzi o jej możliwości. W kolejnej części artykułu zajmiemy się kolejnymi funkcjonalnościami NAudio.
Czy NAudio da się dostosować do asp mvc? Chodzi o to, że jeden przycisk na widoku uruchamia wav (output.Play()) a następny powinien zastopować go. Nie mogę przekazać z widoku do kontrolera „output” jako NAudio.Wave.DirectSoundOut, stąd output.Stop() nie działa (output to null). Pytanie jak użyć (czy w ogóle można?) Stop() i Pause() w innym kontrolerze?