.NET オーディオ/MIDIライブラリ「NAudio」 を使って、C#言語で音楽ファイルをリアルタイムに再生するソフトを作ります。なお、開発環境はVisual Studio 2013 ExpressでWindows 7 Proです。

NAudioライブラリのインストールおよび設定

NAudioライブラリは「naudio/NAudio」から「NAudio-Release.zip」をダウンロードし、解凍したライブラリを、Visual Studio 2013 Expressの「参照の追加」によりプロジェクトに追加します。

リアルタイム再生ソフトの作成

リアルタイム再生ソフトは、デモコード「NAudio/NAudioDemo/Mp3StreamingDemo/」を参照して作成します。デモソフトではメニュー形式で様々なソフトをロードして実行しますが、今回はMp3StreamingDemoを直接動作させます。

StreamingPanel.cs

using NAudio.Wave;

public partial class StreamingPanel : Form
{

    enum StreamingPlaybackState
    {
        Stopped,
        Playing,
        Buffering,
        Paused
    }

    public StreamingPanel()
    {
        InitializeComponent();
        volumeSlider1.VolumeChanged += OnVolumeSliderChanged;
        Disposed += MP3StreamingPanel_Disposing;
    }

    void OnVolumeSliderChanged(object sender, EventArgs e)
    {
        if (volumeProvider != null)
        {
            volumeProvider.Volume = volumeSlider1.Volume;
        }
    }

    private BufferedWaveProvider bufferedWaveProvider;
    private IWavePlayer waveOut;
    private volatile StreamingPlaybackState playbackState;
    private volatile bool fullyDownloaded;
    private HttpWebRequest webRequest;
    private VolumeWaveProvider16 volumeProvider;

    delegate void ShowErrorDelegate(string message);

    private void ShowError(string message)
    {
        if (InvokeRequired)
        {
            BeginInvoke(new ShowErrorDelegate(ShowError), message);
        }
        else
        {
            MessageBox.Show(message);
        }
    }

    private void StreamMp3(object state)
    {
        bufferedWaveProvider = new BufferedWaveProvider(new WaveFormat(44100, 16, 2));
        bufferedWaveProvider.BufferDuration =
            TimeSpan.FromSeconds(20); // allow us to get well ahead of ourselves

        IMp3FrameDecompressor decompressor = null;
        try
        {
            //外部入力のダミーとして適当な音声データを用意して使う
            string wavFilePath = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
                "sample.wav"
                );
            //mp3を使うならこう。
            string mp3FilePath = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
                "sample.mp3"
                );

            if (!(File.Exists(wavFilePath) || File.Exists(mp3FilePath)))
            {
                Console.WriteLine("Target sound files were not found. Wav file or MP3 file is needed for this program.");
                Console.WriteLine("expected wav file: {wavFilePath}");
                Console.WriteLine("expected mp3 file: {wavFilePath}");
                Console.WriteLine("(note: ONE file is enough, two files is not needed)");
                return;
            }

            //mp3しかない場合、先にwavへ変換を行う
            if (!File.Exists(wavFilePath))
            {
                using (var mp3reader = new Mp3FileReader(mp3FilePath))
                using (var pcmStream = WaveFormatConversionStream.CreatePcmStream(mp3reader))
                {
                    WaveFileWriter.CreateWaveFile(wavFilePath, pcmStream);
                }
            }

            byte[] data = File.ReadAllBytes(wavFilePath);

            //若干効率が悪いがヘッダのバイト数を確実に割り出して削る
            using (var r = new WaveFileReader(wavFilePath))
            {
                int headerLength = (int)(data.Length - r.Length);
                data = data.Skip(headerLength).ToArray();
            }

            //            int bufsize = 16000;
            int bufsize = 20000;
            for (int i = 0; i + bufsize < data.Length; i += bufsize)
            {
                bufferedWaveProvider.AddSamples(data, i, bufsize);
                while (bufferedWaveProvider.BufferedDuration.Seconds > 5)
                {
                    Thread.Sleep(100);
                }
            }
            // was doing this in a finally block, but for some reason
            // we are hanging on response stream .Dispose so never get there
            //                decompressor.Dispose();
        }
        finally
        {
            if (decompressor != null)
            {
                decompressor.Dispose();
            }
        }
    }

    private static IMp3FrameDecompressor CreateFrameDecompressor(Mp3Frame frame)
    {
        WaveFormat waveFormat = new Mp3WaveFormat(frame.SampleRate, frame.ChannelMode == ChannelMode.Mono ? 1 : 2,
            frame.FrameLength, frame.BitRate);
        return new AcmMp3FrameDecompressor(waveFormat);
    }

    private bool IsBufferNearlyFull
    {
        get
        {
            return bufferedWaveProvider != null && 
                   bufferedWaveProvider.BufferLength - bufferedWaveProvider.BufferedBytes 
                   < bufferedWaveProvider.WaveFormat.AverageBytesPerSecond / 4;
        }
    }

    private void buttonPlay_Click(object sender, EventArgs e)
    {
        if (playbackState == StreamingPlaybackState.Stopped)
        {
            playbackState = StreamingPlaybackState.Buffering;
            bufferedWaveProvider = null;
            ThreadPool.QueueUserWorkItem(StreamMp3, textBoxStreamingUrl.Text);
            timer1.Enabled = true;
        }
        else if (playbackState == StreamingPlaybackState.Paused)
        {
            playbackState = StreamingPlaybackState.Buffering;
        }
    }

    private void StopPlayback()
    {
        if (playbackState != StreamingPlaybackState.Stopped)
        {
            if (!fullyDownloaded)
            {
            }
            
            playbackState = StreamingPlaybackState.Stopped;
            if (waveOut != null)
            { 
                waveOut.Stop();
                waveOut.Dispose();
                waveOut = null;
            }
            timer1.Enabled = false;
            // n.b. streaming thread may not yet have exited
            Thread.Sleep(500);
            ShowBufferState(0);
        }
    }

    private void ShowBufferState(double totalSeconds)
    {
        labelBuffered.Text = String.Format("{0:0.0}s", totalSeconds);
        progressBarBuffer.Value = (int)(totalSeconds * 1000);
    }

    private void timer1_Tick(object sender, EventArgs e)
    {
        if (playbackState != StreamingPlaybackState.Stopped)
        {
            if (waveOut == null && bufferedWaveProvider != null)
            {
                Debug.WriteLine("Creating WaveOut Device");
                waveOut = CreateWaveOut(); 
                waveOut.PlaybackStopped += OnPlaybackStopped;
                volumeProvider = new VolumeWaveProvider16(bufferedWaveProvider);
                volumeProvider.Volume = volumeSlider1.Volume;
                waveOut.Init(volumeProvider);
                progressBarBuffer.Maximum = (int)bufferedWaveProvider.BufferDuration.TotalMilliseconds;
            }
            else if (bufferedWaveProvider != null)
            {
                var bufferedSeconds = bufferedWaveProvider.BufferedDuration.TotalSeconds;
                ShowBufferState(bufferedSeconds);
                // make it stutter less if we buffer up a decent amount before playing
                if (bufferedSeconds < 0.5 && playbackState == StreamingPlaybackState.Playing && !fullyDownloaded)
                {
                    Pause();
                }
                else if (bufferedSeconds > 4 && playbackState == StreamingPlaybackState.Buffering)
                {
                    Play();
                }
                else if (fullyDownloaded && bufferedSeconds == 0)
                {
                    Debug.WriteLine("Reached end of stream");
                    StopPlayback();
                }
            }

        }
    }

    private void Play()
    {
        waveOut.Play();
        Debug.WriteLine(String.Format("Started playing, waveOut.PlaybackState={0}", waveOut.PlaybackState));
        playbackState = StreamingPlaybackState.Playing;
    }

    private void Pause()
    {
        playbackState = StreamingPlaybackState.Buffering;
        waveOut.Pause();
        Debug.WriteLine(String.Format("Paused to buffer, waveOut.PlaybackState={0}", waveOut.PlaybackState));
    }

    private IWavePlayer CreateWaveOut()
    {
        return new WaveOut();
    }

    private void MP3StreamingPanel_Disposing(object sender, EventArgs e)
    {
        StopPlayback();
    }

    private void buttonPause_Click(object sender, EventArgs e)
    {
        if (playbackState == StreamingPlaybackState.Playing || playbackState == StreamingPlaybackState.Buffering)
        {
            waveOut.Pause();
            Debug.WriteLine(String.Format("User requested Pause, waveOut.PlaybackState={0}", waveOut.PlaybackState));
            playbackState = StreamingPlaybackState.Paused;
        }
    }

    private void buttonStop_Click(object sender, EventArgs e)
    {
        StopPlayback();
    }

    private void OnPlaybackStopped(object sender, StoppedEventArgs e)
    {
        Debug.WriteLine("Playback Stopped");
        if (e.Exception != null)
        {
            MessageBox.Show(String.Format("Playback Error {0}", e.Exception.Message));
        }
    }
}


リアルタイム再生ソフトの実行

リアルタイム再生ソフトを実行すると、次の画面が表示されます。

リアルタイム再生ソフトの実行