前書き
以前、@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ライクな波形を表示する音楽プレーヤ」をバージョンアップしてみます。
こんなものができます
(原作者の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;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
<省略>
}
}
コード全体
ソースコードの全体を以下に示します。
// オリジナル
// 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さんに、この場を借りてお礼を申し上げます。