使用託管 Microsoft DirectX 庫和 C# 來即時合成音頻流

用 DirectSound 生成電子鼓

發佈日期: 11/12/2004 | 更新日期: 11/12/2004
Ianier Munoz
Chronotron
摘要:特約專欄作家 Ianier Munoz 使用託管 Microsoft DirectX 庫和 C# 來即時合成音頻流,從而生成了電子鼓。
*
本頁內容
鼓聲大作鼓聲大作
簡介簡介
DirectSound 概述DirectSound 概述
流式音頻播放器流式音頻播放器
使用 DirectSound 實現 IAudioPlayer使用 DirectSound 實現 IAudioPlayer
電子鼓引擎電子鼓引擎
將代碼組合在一起將代碼組合在一起
小結小結
編碼問題編碼問題
資源資源

鼓聲大作

(本介紹由 Duncan Mackenzie 提供。)
Ianier 有一份很棒的工作;他爲 DJ 們編寫代碼,以使他們能夠使用諸如 Microsoft® Windows Media® Player 之類的使用者軟件來進行專業數字信號處理 (DSP) 工作。這是一份美妙的工作,並且讓我們感到幸運的是,他正在鑽研託管代碼和託管 Microsoft® DirectX® 領域。在本文中,Ianier 已經生成了一個演示軟件(參見圖 1),該軟件將使您在幾分鐘之後通過您計算機上的小揚聲器發出您自己的低音打擊樂聲。這是一個託管電子鼓,正是它使您可以配置和播放多個聲道的取樣音樂。代碼無須進行任何實際配置就應該能夠工作,但您在打開並運行 winrythm 示例項目之前,您必須確保下載並安裝(然後重新啓動)DirectX SDK(可從這裏獲得)。
code4fun02032004_fig01
1. 電子鼓的主要窗體
(歐耶,歐耶...咚咚,咚咚,咚咚...

簡介

在 DirectX 9 SDK 發佈以前,Microsoft® .NET Framework 令人失望地沒有任何聲音。解決這一侷限性的唯一方法是通過 COM Interop 或 P-Invoke 訪問 Microsoft® Windows® API。
託管 Microsoft® DirectSound®(它是 DirectX 9 的一個組件)使您可以在 .NET 中播放聲音,而無須求助於 COM Interop 或 P-Invoke。在本文中,我將說明如何通過即時合成音頻樣本來實現簡單的電子鼓(參見圖 1),並且使用 DirectSound 播放得到的音頻流。
本文假設您熟悉 C# 和 .NET Framework。一些基本的音頻處理知識也可以幫助您更好地理解這裏描述的概念。
本文附帶的代碼是用 Microsoft® Visual Studio® .NET 2003 編譯的。它要求具有 DirectX 9 SDK(您可以從這裏下載它)。

DirectSound 概述

DirectSound 是 DirectX 的一個組件,它使應用程序可以用與硬件無關的方式訪問音頻資源。在 DirectSound 中,音頻播放單元是聲音緩衝區。聲音緩衝區屬於音頻設備,後者代表主機系統中的聲卡。當應用程序希望使用 DirectSound 播放聲音時,它會創建一個音頻設備對象,在該設備上創建一個緩衝區,用聲音數據填充該緩衝區,然後播放該緩衝區。有關不同的 DirectSound 對象之間的關係的詳細說明,請參閱 DirectX SDK 文檔資料。
根據聲音緩衝區的預期用途,可以將它們分爲靜態緩衝區或流式緩衝區。靜態緩衝區被用一些預定義的音頻數據初始化一次,然後根據需要播放任意多次。這種緩衝區通常用於射擊遊戲以及其他需要短暫效果的遊戲中。另一方面,流式緩衝區通常用於播放由於太大而無法放入內存的內容,或者那些無法事先確定其長度或內容的聲音,如電話應用程序中說話者的聲音。流式緩衝區是使用隨着緩衝區的播放而不斷用新數據刷新的小型緩衝區實現的。儘管託管 DirectSound 提供了有關靜態緩衝區的優秀文檔資料和示例,但它目前缺少有關流式緩衝區的示例。
然而,應該提到的是,託管 DirectX 確實包含一個用於播放音頻流的類,即 AudioVideoPlayback 命名空間中的 Audio 類。該類使您可以播放大多數種類的音頻文件,包括 WAV 和 MP3。但是,Audio 類不允許您以編程方式選擇輸出設備,並且它不爲您提供訪問音頻樣本的權限,以防您希望對其進行修改。

流式音頻播放器

我將流式音頻播放器定義爲一個從某個源中提取音頻數據並通過某個設備播放這些數據的組件。典型的流式音頻播放器組件通過聲卡播放傳入的音頻流,但它還可以通過網絡發送音頻流或者將其保存到文件中。
IAudioPlayer 接口包含我們的應用程序應該瞭解的有關該播放器的所有信息。該接口還將使您可以將您的聲音合成引擎從實際的播放器實現中分離出來,這在您希望將該示例移植到其他使用不同播放技術的 .NET 平臺時可能很有用。
/// <summary>
/// Delegate used to fill in a buffer
/// </summary>
public delegate void PullAudioCallback(IntPtr data, int count);

/// <summary>
/// Audio player interface
/// </summary>
public interface IAudioPlayer : IDisposable
{
    int SamplingRate { get; }
    int BitsPerSample { get; }
    int Channels { get; }

    int GetBufferedSize();
    void Play(PullAudioCallback onAudioData);
    void Stop();
}
SamplingRateBitsPerSampleChannels 屬性描述了該播放器理解的音頻格式。Play 方法開始播放由 PullAudioCallback 委託提供的音頻流,而 Stop 方法不出意外地停止音頻播放。
請注意,PullAudioCallback 要求將 count 字節的音頻數據複製到數據緩衝區(它是一個 IntPtr)。您可能會認爲我應該使用字節數組而不是 IntPtr,因爲在 IntPtr 中處理數據會強迫應用程序調用需要非託管代碼執行權限的函數。然而,託管 DirectSound 無論如何都需要這樣的權限,所以使用 IntPtr 沒有什麼嚴重後果,並且在處理不同的樣本格式和其他播放技術時可以避免額外的數據複製。
GetBufferedSize 返回自從上次調用 PullAudioCallback 委託以來已經被排到該播放器的隊列中的字節數。我們將使用該方法來計算相對於輸入流的當前播放位置。

使用 DirectSound 實現 IAudioPlayer

正如我在前面提到的那樣,DirectSound 中的流式緩衝區只是一個隨着緩衝區的播放而不斷用新數據刷新的小型緩衝區。StreamingPlayer 類使用流式緩衝區來實現 IAudioPlayer 接口。
讓我們觀察一下 StreamingPlayer 構造函數:
public StreamingPlayer(Control owner, 
        Device device, WaveFormat format)
{
    m_Device = device;
    if (m_Device == null)
    {
        m_Device = new Device();
        m_Device.SetCooperativeLevel( 
            owner, CooperativeLevel.Normal);
        m_OwnsDevice = true;
    }

    BufferDescription desc = new BufferDescription(format);
    desc.BufferBytes = format.AverageBytesPerSecond;
    desc.ControlVolume = true;
    desc.GlobalFocus = true;

    m_Buffer = new SecondaryBuffer(desc, m_Device);
    m_BufferBytes = m_Buffer.Caps.BufferBytes;

    m_Timer = new System.Timers.Timer( 
          BytesToMs(m_BufferBytes) / 6);
    m_Timer.Enabled = false;
    m_Timer.Elapsed += new System.Timers.ElapsedEventHandler(Timer_Elapsed);
}
StreamingPlayer 構造函數首先確保我們具有一個有效的 DirectSound 音頻設備以便使用,並且如果未指定這樣的設備,則它會創建一個新設備。爲了創建 Device 對象,我們必須指定一個 Microsoft® Windows 窗體控件,以便 DirectSound 用來跟蹤應用程序焦點;因此,使用了 owner 參數。然後,將創建並初始化一個 DirectSound SecondaryBuffer 實例,並且分配一個計時器。稍後,我將討論該計時器的作用。
IAudioPlayer.StartIAudioPlayer.Stop 的實現幾乎微不足道。Play 方法確保有一些音頻數據要播放;然後,它啓用計時器並開始播放緩衝區。與此對稱的是,Stop 方法禁用計數器並停止該緩衝區。
public void Play( 
     Chronotron.AudioPlayer.PullAudioCallback pullAudio)
{
    Stop();

    m_PullStream = new PullStream(pullAudio);

    m_Buffer.SetCurrentPosition(0);
    m_NextWrite = 0;
    Feed(m_BufferBytes);
    m_Timer.Enabled = true;
    m_Buffer.Play(0, BufferPlayFlags.Looping);
}

public void Stop()
{
    if (m_Timer != null)
        m_Timer.Enabled = false;
    if (m_Buffer != null)
        m_Buffer.Stop();
}
其思想是不斷地向該緩衝區供給來自委託的聲音數據。爲了達到這一目標,計時器定期檢查已經播放了多少音頻數據,並且根據需要向該緩衝區中添加更多的數據。
private void Timer_Elapsed( object sender, System.Timers.ElapsedEventArgs e){ Feed(GetPlayedSize());}
GetPlayedSize 函數使用緩衝區的 PlayPosition 屬性來計算播放遊標已經推進的字節數。請注意,因爲緩衝區循環播放,所以 GetPlayedSize 必須檢測播放遊標何時返繞,並相應地調整結果。
private int GetPlayedSize(){ int pos = m_Buffer.PlayPosition; return pos < m_NextWrite ? pos + m_BufferBytes - m_NextWrite : pos - m_NextWrite;}
填充該緩衝區的例程名爲 Feed,並且它顯示在下面的代碼中。該例程調用了 SecondaryBuffer.Write,該方法從流中提取音頻數據,並將其寫到緩衝區中。在我們所處的場合下,該流只是我們在 Play 方法中收到的 PullAudioCallback 委託的包裝。
private void Feed(int bytes)
{
    // limit latency to some milliseconds
    int tocopy = Math.Min(bytes, MsToBytes(MaxLatencyMs));

    if (tocopy > 0)
    {
        // restore buffer
        if (m_Buffer.Status.BufferLost)
            m_Buffer.Restore();

        // copy data to the buffer
        m_Buffer.Write(m_NextWrite, m_PullStream, 
                       tocopy, LockFlag.None);

        m_NextWrite += tocopy;
        if (m_NextWrite >= m_BufferBytes)
            m_NextWrite -= m_BufferBytes;
    }
}
請注意,我們將添加到該緩衝區的數據量強行限制在某個界限以下,以便減少播放延遲。可以將延遲定義爲傳入的音頻流中發生更改的時間與該更改被實際聽到的時間之間的差值。如果沒有這樣的延遲控制,則平均延遲將大約等於緩衝區總長度,這對於實時合成器而言是不可接受的。

電子鼓引擎

電子鼓是實時合成器的示例:一組表示每個可能的鼓音的樣本波形(用音樂行話說,也稱爲“音色”)被混合爲遵循某些節奏模式的輸出流,以模擬鼓手演奏。這就像聽起來那樣簡單,因此讓我們研究一下代碼!

核心

電子鼓的主要元素是在 PatchTrackMixer 類中實現的(參見圖 2)。所有這些類都在 Rhythm.cs 中實現。
code4fun02032004_fig02thumb
2. Rhythm.cs 的類關係圖
Patch 類存放特定樂器的波形。Patch 是用包含 WAV 格式音頻數據的 Stream 對象初始化的。這裏,我將不會說明有關讀取 WAV 文件的詳細信息,但您可以觀察一下 WaveStream 幫助器類以獲得總體印象。
出於簡單性的考慮,Patch 通過添加左聲道和右聲道(如果提供的文件是立體聲)將音頻數據轉換爲單聲道,並且將結果存儲在 32 位整數的數組中。實際的數據範圍是 -32768 到 +32767,以便我們可以混合多個音頻流,而無須注意溢出問題。
PatchReader 類使您可以從 Patch 中讀取音頻數據並將其混合到目標緩衝區中。將讀取器從實際的 Patch 數據中分離出來是必要的,因爲可以聽到單個 Patch 在不同的位置多次播放。特別是當相同的聲音在極短時間內多次出現時,會發生這種情況。
Track 類代表要使用單個樂器播放的一系列事件。曲目是使用一個 Patch、一些時間段(即可能的節拍位置)初始化的,同時還可以使用初始模式。該模式只是一個布爾值數組,其長度等於曲目中的時間段的個數。如果您將該數組的某個元素設置爲 true,則應該在該節拍位置播放所選的 PatchTrack.GetBeat 方法爲特定的節拍位置返回一個 PatchReader 實例,或者,如果不應該在當前節拍中播放任何內容,則返回 null。
Mixer 類在給定一組曲目的前提下生成實際的音頻流,因此它實現了一個與 PullAudioCallback 簽名相匹配的方法。混音器還跟蹤當前節拍位置和當前正在播放的 PatchReader 實例的列表。
最困難的工作是在 DoMix 方法內部完成的,您可以在下面的代碼中看到該方法。混音器計算有多少個樣本與節拍持續時間相對應,並且隨着輸出流的合成推進當前節拍位置。要生成一組樣本,混音器只須將正在當前節拍播放的音色加在一起。
private void DoMix(int samples)
{
    // grow mix buffer as necessary
    if (m_MixBuffer == null || m_MixBuffer.Length < samples)
        m_MixBuffer = new int[samples];

    // clear mix buffer
    Array.Clear(m_MixBuffer, 0, m_MixBuffer.Length);

    int pos = 0;
    while(pos < samples)
    {
        // load current patches
        if (m_TickLeft == 0)
        {
            DoTick();
            lock(m_BPMLock)
                m_TickLeft = m_TickPeriod;
        }

        int tomix = Math.Min(samples - pos, m_TickLeft);

        // mix current streams
        for (int i = m_Readers.Count - 1; i >= 0; i--)
        {
            PatchReader r = (PatchReader)m_Readers[i];
            if (!r.Mix(m_MixBuffer, pos, tomix))
                m_Readers.RemoveAt(i);
        }

        m_TickLeft -= tomix;
        pos += tomix;
    }
}
爲了計算與給定速度的時間段相對應的音頻樣本的數量,混音器使用以下公式:(SamplingRate * 60 / BPM) / Resolution,其中,SamplingRate 是播放器的取樣頻率(以 Hz 爲單位);Resolution 是每個節拍的時間段的數量;BPM 是速度(單位是節拍/分鐘)。BPM 屬性應用該公式以初始化 m_TickPeriod 成員變量。

將代碼組合在一起

既然我們具有了實現該電子鼓所需的所有元素,那麼使工作順利完成所需做的唯一一件事情是將它們連接在一起。下面是操作順序:
創建一個流式音頻播放器。
創建一個混音器。
根據 WAV 文件或資源創建一組鼓音色(聲音)。
將一組曲目添加到該混音器,以播放所需的音色。
爲要播放的每個曲目定義模式。
使用該混音器作爲數據源,啓動播放器。
正如您可以在下面的代碼中看到的那樣,RythmMachineApp 類恰好完成了這一工作。
public RythmMachineApp(Control control, IAudioPlayer player)
{
    int measuresPerBeat = 2;

    Type resType = control.GetType();
    Mixer = new Chronotron.Rythm.Mixer(
          player, measuresPerBeat);
    Mixer.Add(new Track("Bass drum", 
          new Patch(resType, "media.bass.wav"), TrackLength));
    Mixer.Add(new Track("Snare drum", 
          new Patch(resType, "media.snare.wav"), TrackLength));
    Mixer.Add(new Track("Closed hat", 
          new Patch(resType, "media.closed.wav"), TrackLength));
    Mixer.Add(new Track("Open hat", 
          new Patch(resType, "media.open.wav"), TrackLength));
    Mixer.Add(new Track("Toc", 
          new Patch(resType, "media.rim.wav"), TrackLength));
    // Init with any preset
    Mixer["Bass drum"].Init(new byte[] 
          { 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0 } );
    Mixer["Snare drum"].Init(new byte[] 
          { 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0 } );
    Mixer["Closed hat"].Init(new byte[] 
          { 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0 } );
    Mixer["Open hat"].Init(new byte[] 
          { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1 } );
    BuildUI(control);
    m_Timer = new Timer();
    m_Timer.Interval = 250;
    m_Timer.Tick += new EventHandler(m_Timer_Tick);
    m_Timer.Enabled = true;
}
這就是所有的操作。其餘代碼爲電子鼓實現了一個簡單的用戶界面,以便使用戶可以在計算機屏幕上創建有節奏的模式。

小結

本文向您說明了如何使用託管 DirectSound API 創建流式緩衝區,以及如何即時生成音頻流。我希望您在使用提供的示例代碼時能夠獲得一些樂趣。您還可以考慮進行一些改進,如對加載和保存模式的支持、用於更改速度的用戶界面控件、立體聲播放等等。我將把這些工作留待您來完成,因爲讓我擁有全部的樂趣是不公平的……
最後,我要感謝 Duncan 允許我將本文張貼到他的“Coding4Fun”專欄中。我希望您在使用這些代碼時能夠享受到像我在編寫它們時一樣多的樂趣。在以後的文章中,我將探討如何將該電子鼓移植到 Compact Framework,以使其能夠在 Pocket PC 上運行。

編碼問題

在這些“Coding4Fun”專欄文章的結尾,Duncan 通常會提出幾個編碼問題,如果您對它們感興趣,可以進行一番研究。在閱讀本文以後,我願意邀請您仿效我的示例並創建一些使用 DirectX 的代碼。請將您的作品張貼到 GotDotNet,並給 Duncan 發送電子郵件([email protected]),說明您所做的工作以及您感興趣的原因。您可以隨時向 Duncan 發送您的意見,但請只發送指向代碼示例的鏈接而不是示例本身。
您對嗜好者內容有何見解?您希望看到其他人來做特約專欄作家嗎?請通過 [email protected] 告知 Duncan。

資源

本文的核心是使用 DirectX 9 SDK(可以從這裏獲得)生成的,但您還應該參閱 MSDN Library 的 DirectX 部分(位於 http://msdn.Microsoft.com/nhp/default.asp?contentid=28000410)。如果您要尋找本主題的多媒體介紹,則 .NET 節目 (http://msdn.Microsoft.com/theshow/episode037/default.asp) 的一部分也重點討論了託管 DirectX。
Coding4Fun
code4fun02032004_fig03
Ianier Munoz 居住在法國梅斯市,並且是盧森堡的一家國際諮詢公司的高級顧問和分析師。他已經創作了一些流行的多媒體軟件,如 Chronotron、Adapt-X 和 American DJ 的 Pro-Mix。您可以通過 http://www.chronotron.com 與他聯繫。
發佈了35 篇原創文章 · 獲贊 0 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章