2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「C#でSoundCloudライクな波形を表示する音楽プレーヤ」を ~~勝手に~~ バージョンアップしてみる

Last updated at Posted at 2024-06-27

前書き

以前、@siy1121さんがNAudioライブラリを使って「C#でSoundCloudライクな波形を表示する音楽プレーヤを作ってみる」と言う記事を書かれました。私はこの記事を参考に「語学学習用アプリの開発」を作りました。
しかし時間の経過とともにNAudio(ver2.x)がマルチプラットフォーム対応になり、特に音声波形をレンダリングするNAudio.WaveFormRendererの仕様が変更になました。またNAudio(ver1.9)は、.NetFramewark4.0で動いていたようで、これが既にサポート外になっており、win11環境では簡単に開発できなくなりました(特別なイントール方法があるようですが、面倒臭そう)。
私の作った語学学習用アプリの開発は、既に3,4年前に作ったもので、その間Androidをやったりjavaをやったり、pythonをやったりするうちに、C#ほぼ忘れてしまいました。そこでC#やNAudioを思い出すために、「C#でSoundCloudライクな波形を表示する音楽プレーヤ」をバージョンアップしてみます。

こんなものができます

image.png
原作者のUIの方がかっこいいです)

C#で音声ファイルを扱う

次のライブラリを使います

NAudio
NAudio.WaveFormRenderer

準備

  • C#プロジェクト
    Windowsフォームアプリケーション(.NET Framework 4.7.2)
  • NAudioとNAudio.WaveFormRendererのインストール
    ともにNuGetからインストールできます。
    私の使っているバージョンは
    NAudio(Ver.2.2.1)
    NAudio.WaveFormRenderer(Ver.2.0.0)

UI部分

UI部分の作り方は、原作を参照してください。丁寧に記述されています。

ソースコード

改変箇所

改変した箇所は次のとおりです。
WaveFormRendererのインスタンスのパラメータにWaveStreamを代入する箇所です(もともとはファイルパスを入れていた)。

        private void openOToolStripMenuItem_Click(object sender, EventArgs e)
        {
            <省略>
                ///////////////////////////////////////////////// ここから /////////////////////////////////////////////////////////////////
                using (var audioFile_rend1 = new AudioFileReader(filePath))
                {
                    pictureBox1.BackgroundImage = renderer.Render(audioFile_rend1, averagePeakProvider, soundCloudDarkBlocks);
                }

                using(var audioFile_rend2 = new AudioFileReader(filePath))
                {
                    pictureBox2.BackgroundImage = renderer.Render(audioFile_rend2, averagePeakProvider, soundCloudOrangeBlocks);
                    pictureBox2.Width = 0;
                }
                ///////////////////////////////////////////////// ここまで変更 /////////////////////////////////////////////////////////////////
                ///////////////////////////////////////////////// 元々は       /////////////////////////////////////////////////////////////////
                // pictureBox1.BackgroundImage = renderer.Render(filePath, averagePeakProvider, soundCloudDarkBlocks);
                // pictureBox2.BackgroundImage = renderer.Render(filePath, averagePeakProvider, soundCloudOrangeBlocks);
                // pictureBox2.Width = 0;
                ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
               <省略>
            }
        }

コード全体

ソースコードの全体を以下に示します。

Form1.cs
// オリジナル
// https://qiita.com/siy1121/items/dd06a5e700dcf9543af7
// 作成 @siy1121さん
//
// NAudio.WaveFormRendererが、バージョン2では、より良いクロスプラットフォームサポートのために
// オーディオファイルのファイル名からWaveStreamを使用するように変更されたため、
// その対応するためを変更を行った。
//

using System;
using System.Drawing;
using System.Windows.Forms;
using NAudio.WaveFormRenderer;
using NAudio.Wave;
using System.IO;

namespace WaveFormPlayer
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            //イベントの設定 共通にしたいので手動で
            pictureBox1.MouseDown += PictureBox_MouseDown;
            pictureBox2.MouseDown += PictureBox_MouseDown;
            pictureBox1.MouseMove += PictureBox_MouseMove;
            pictureBox2.MouseMove += PictureBox_MouseMove;
            pictureBox1.MouseUp += PictureBox_MouseUp;
            pictureBox2.MouseUp += PictureBox_MouseUp;
        }

        WaveOutEvent outputDevice;
        AudioFileReader audioFile;
        string filePath;//音声ファイルのパス
        int bytePerSec;//一秒あたりのバイト数
        int length;//曲の長さ(秒)
        int position;//再生位置(秒)
        bool mouseDownFlag;//ドラッグ時に使うフラグ MouseDown中にtrue

        private void openOToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                filePath = openFileDialog1.FileName;

                //黒い波形の設定
                var soundCloudDarkBlocks = new SoundCloudBlockWaveFormSettings(Color.FromArgb(52, 52, 52), Color.FromArgb(55, 55, 55), Color.FromArgb(154, 154, 154),
                    Color.FromArgb(204, 204, 204));//バーの色の設定

                soundCloudDarkBlocks.Width = pictureBox1.Width;//生成する画像の幅
                soundCloudDarkBlocks.TopHeight = pictureBox1.Height / 4 * 3;//上に伸びるバーの高さ
                soundCloudDarkBlocks.BottomHeight = pictureBox1.Height / 4;//下に伸びるバーの長さ
                soundCloudDarkBlocks.BackgroundColor = Color.Transparent;//生成される画像の背景色 今回は透明
                soundCloudDarkBlocks.PixelsPerPeak = 2;//バーの幅
                //soundCloudDarkBlocks.SpacerPixels = 1; バーの間に挟まる細いバーの幅

                //オレンジの波形の設定
                var soundCloudOrangeBlocks = new SoundCloudBlockWaveFormSettings(Color.FromArgb(255, 76, 0), Color.FromArgb(255, 52, 2), Color.FromArgb(255, 171, 141),
                     Color.FromArgb(255, 213, 199));

                soundCloudOrangeBlocks.Width = pictureBox1.Width;
                soundCloudOrangeBlocks.TopHeight = pictureBox1.Height / 4 * 3;
                soundCloudOrangeBlocks.BottomHeight = pictureBox1.Height / 4;
                soundCloudOrangeBlocks.BackgroundColor = Color.Transparent;
                soundCloudOrangeBlocks.PixelsPerPeak = 2;

                var renderer = new WaveFormRenderer(); //波形レンダラの生成
                var averagePeakProvider = new AveragePeakProvider(3); //波形レンダラ内部で使用されるもの

                //レンダリングした画像をPictureBoxに設定

                ///////////////////////////////////////////////// ここから /////////////////////////////////////////////////////////////////
                using (var audioFile_rend1 = new AudioFileReader(filePath))
                {
                    pictureBox1.BackgroundImage = renderer.Render(audioFile_rend1, averagePeakProvider, soundCloudDarkBlocks);
                }

                using(var audioFile_rend2 = new AudioFileReader(filePath))
                {
                    pictureBox2.BackgroundImage = renderer.Render(audioFile_rend2, averagePeakProvider, soundCloudOrangeBlocks);
                    pictureBox2.Width = 0;
                }
                ///////////////////////////////////////////////// ここまで変更 /////////////////////////////////////////////////////////////////
                ///////////////////////////////////////////////// 元々は       /////////////////////////////////////////////////////////////////
                // pictureBox1.BackgroundImage = renderer.Render(filePath, averagePeakProvider, soundCloudDarkBlocks);
                // pictureBox2.BackgroundImage = renderer.Render(filePath, averagePeakProvider, soundCloudOrangeBlocks);
                // pictureBox2.Width = 0;
                ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

                label1.Text = Path.GetFileName(filePath);

                outputDevice = new WaveOutEvent();
                audioFile = new AudioFileReader(filePath);
                outputDevice.Init(audioFile);
                playButton.BackgroundImage = Properties.Resources.play;
                
            }
        }

        private void playButton_Click(object sender, EventArgs e)
        {
            switch (outputDevice.PlaybackState)
            {
                case PlaybackState.Stopped://ファイルが読み込まれてまだ一度も再生されていない場合
                    //必要な値を求める
                    bytePerSec = audioFile.WaveFormat.BitsPerSample / 8 * audioFile.WaveFormat.SampleRate * audioFile.WaveFormat.Channels;
                    length = (int)audioFile.Length / bytePerSec;

                    label3.Text = new TimeSpan(0, 0, length).ToString();
                    timer1.Enabled = true;

                    outputDevice.Play();
                    playButton.BackgroundImage = Properties.Resources.pause;
                    //Thread.Sleep(200);
                    break;

                case PlaybackState.Paused://一時停止時の場合
                    outputDevice.Play();
                    playButton.BackgroundImage = Properties.Resources.pause;
                    break;

                case PlaybackState.Playing://再生中の場合
                    outputDevice.Pause();
                    playButton.BackgroundImage = Properties.Resources.play;
                    break;
            }
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            //再生位置(秒)を計算して表示
            position = (int)audioFile.Position / bytePerSec;
            label2.Text = new TimeSpan(0, 0, position).ToString();

            if (!mouseDownFlag)//ドラッグ時に幅を変更するとチカチカするのを防止
                //再生位置からオレンジ波形をすすめる
                pictureBox2.Width = (int)(((double)position / length) * pictureBox1.Width);
        }

        private void PictureBox_MouseDown(object sender, MouseEventArgs e)
        {
            if (audioFile == null) return;
            mouseDownFlag = true;//ドラッグ時のフラグをtrueに
        }

        private void PictureBox_MouseMove(object sender, MouseEventArgs e)
        {
            if (mouseDownFlag) pictureBox2.Width = e.X;//ドラッグ中にオレンジの波形の幅を変更
        }

        private void PictureBox_MouseUp(object sender, MouseEventArgs e)
        {
            if (!mouseDownFlag) return;
            mouseDownFlag = false;

            //ドラッグが終了した場所から曲の再生位置を計算して設定
            audioFile.Position = (int)(((double)e.X / pictureBox1.Width) * audioFile.Length);
        }

    }
}

コーディング上の注意

波形のレンダリングや音声ファイルの再生の際、WaveStreamのインスタンスaudioFileを以下のように共通にすると、音声の再生(outputDevice.play())や、二つ目のレンダリングができなくなりました。(ひとつ目のレンダリングはでき、pictureBox1のイメージは生成され表示された。気持ち的には共通にしたいが、なぜできないのだろう?深堀出来た方、教えてください。)

                //レンダリングした画像をPictureBoxに設定
                audioFile = new AudioFileReader(filePath))   // これを共通にすると動かない
                pictureBox1.BackgroundImage = renderer.Render(audioFile, averagePeakProvider, soundCloudDarkBlocks);
                pictureBox2.BackgroundImage = renderer.Render(audioFile, averagePeakProvider, soundCloudOrangeBlocks);
                pictureBox2.Width = 0;

                outputDevice = new WaveOutEvent();
                // audioFile = new AudioFileReader(filePath);
                outputDevice.Init(audioFile);
                playButton.BackgroundImage = Properties.Resources.play;

このようなわけで、WaveStreamをaudioFile、audioFile_rend1、audioFile_rend2に分けました。

その他ソースコードの動作説明は、原作に丁寧に書かれていますので、参照してください。

処理の流れ
レンダリングの設定
レンダリングする
再生ボタンを押したときの処理
PlaybackState.Stopped (初回再生時)
Timerでの処理
ドラッグしてシークする処理
完成品
描画の内部処理を紐解いてみる

謝辞

NAudioの使い方を教えてくれた本音楽プレーヤとその原作者@siy1121さんに、この場を借りてお礼を申し上げます。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?