デスクトップ音楽プレーヤーを作る #1

デスクトップ音楽プレーヤー作成記録

はじめに

今回はPCのデスクトップ上で手軽に扱える音楽プレーヤーの作成を行っていきます。
Windowsでは音楽の再生ソフトで「メディアプレーヤー」がインストールされています。
が、音楽ファイルを実行しないと(= 音楽ファイルの場所までいかなと)いけなかったり、使い勝手があんまり好みじゃなかったりするため自作していくことにしました。

このパートでは、まず音楽ファイルが再生できるところまでを実装していきます。

方針・構想

今回作りたいもの

今回作りたい音楽プレーヤーはこんな感じで考えています。



音楽プレーヤー自体はモニターの片隅に置けるくらいの大きさとします。また、PCの起動と同時に音楽プレーヤーも起動がかかるようにし、いつでも再生できるようにします。(これは実装するというよりもWindowsで設定する内容になると思います。)

音楽プレーヤーの表示内容としては、音の波形を表示できるようにして、視覚的にも楽しめるようにします。

UI部分はなるべく小さくし、最小限の機能のみを実装するようにします。

UI名 機能
フォルダパス指定 サブフォルダも含めて、このパス下に格納している音楽ファイルを再生できるようにする。
戻る/再生/停止/次へ 音楽を操作するボタン
カラー指定 波形表示やボタンの文字色などの色を自分好みに変更できるようにします。
再生モード 順番に再生 or ランダム再生

後は作ってく中で思いついたり、使ってく中で欲しい物を随時付け足していく形で実装していきます。

事前調査

上記の内容が作れるかどうかを調査していきます。
一番実装が難しいと思われるは音楽のは波形表示ですが、以下の記事ですでに実装されている方がいました。

C#で音声波形を表示する音楽プレーヤーを作る

ほとんどほしい内容が実装されていますね…。
ですが、サンプルプログラムはツールの起動で指定された音楽ファイル1曲しか再生されないようになっています。今回は複数の音楽ファイルを順番に再生していくようなをものを作りたいので、このあたりは自分で実装していく必要がありそうです。
波形の表示は高速フーリエ変換を使って行っているようです。計算自体はライブラリに任せていますが、その計算結果を使って表示の指定を行っているようです。かなり細かく座標など指定しています。

また、ツール自体はWPFで作成されており、NAudioという音楽ファイル操作ライブラリを使用して波形表示や音楽ファイルの操作を行っているようです。

とりあえず作れそうなことはわかりました。

環境構築

以上の内容から、今回の開発環境は以下となります。

  • WPF:Windows GUIツールを作成できる環境
  • Visual Studio 2022:WPFを扱える統合開発環境
  • NAudio v2.1.0:音楽ファイルを操作できるライブラリ
  • .NET Framework 4.8:C#を扱えるようにするためのフレームワーク(Windowsならデフォルトで入ってるはず)

Visual Studio 2022のインストール

まずはVisual Studioをインストールしていきます。これをインストールすればWPFも扱えるようになります。 WPFもここで一緒にインストールできます。

以下のサイトからVisualStuidioのインストーラを落としていきます。
https://visualstudio.microsoft.com/ja/downloads/

Visual Studioには無料版としてCommunityが用意されているのでこれを選びます。


インストーラを落としてきたら実行して、表示されている内容を進めていきます。

以下の画面まで来たら、「.NETデスクトップ開発」を選択して次に進みます。


これがWPFの環境です。他にもいろいろな開発環境がありますが、最低限のものだけ選択します。
他に必要な環境が出てきたら、インストーラを起動することで後からでも環境を追加することができます。

インストールができたら、プロジェクトを新規作成します。
(VisualStudioを初めて起動した際に、Microsoftアカウントでのサインインが求められます。)

[新しいプロジェクトの作成]を選択してプロジェクトの設定を行っていきます。
まずはWPFで開発できるように「WPFアプリ(.NET Framework)」を選択します。


プロジェクト名・作成場所・フレームワークのバージョンを決めて作成します。
.NET Frameworkのバージョンは4.8としました。


NAudioのインストール

NAudioのインストールもVisualStudio上からできます。

VisualStudioの画面右側の「ソリューションエクスプローラ内」の「参照」を右クリックして「NuGetパッケージの管理」をクリックします。

検索欄に「NAudio」と入力すると見つけることができます。
選択して「インストール」をクリックします。
(画像はインストール後のものなので「アンインストール」と表示されています。)


これでusingを使ってNAudioのライブラリを指定すれば利用することができるようになります。

サンプルプログラムを動かしてみる

まずは、すでに作成済みの波形表示のプログラムを動かしています。
サンプルプログラムとして以下のサイトのソースを動かしてみます。(再掲)

C#で音声波形を表示する音楽プレーヤーを作る

そのままでは動かなかったので一部修正します。
■グローバル変数outputDeviceの宣言(MainWindows.xaml.cs)
(before)

1
2
3
4
/// <summary>
/// 音楽プレーヤー
/// </summary>
private WaveOutEvent outputDevice;

(after)

1
2
3
4
/// <summary>
/// 音楽プレーヤー
/// </summary>
private WaveOut outputDevice;

■グローバル変数outputDeviceの初期化(MainWindows.xaml.cs)
(before)

1
2
// プレーヤーの生成
outputDevice = new WaveOutEvent();

(after)

1
2
// プレーヤーの生成
outputDevice = new WaveOut();

■再生する音楽ファイルの指定(MainWindows.xaml.cs)
(before)

1
2
// 再生するファイル名
fileName = "music\\sample.wav";

(after)

1
2
// 再生するファイル名
fileName = "(任意のファイルを指定)";

参考サイトが少し古いからでしょうか?NAudioに更新が入ったのか、上記のように修正しないと動きませんでした。


ツールの背景は半透過されてます。常駐するなら邪魔にならなそうです。
波形の表示の感じは真ん中の方があまり振れていませんが、これで良いのでしょうか…? 横軸は周波数?この辺の話に明るくないので、波形が合ってるのかわかりませんが、これをベースに実装していくことにします。

音楽再生部分の実装

まずは、音楽を再生できるようにします。

レイアウト

サンプルのものから、画面のレイアウトを以下のように変更します。


下の方にUIの領域を作りました。ボタンなどはデフォルトのデザインなの見た目がいまいちですが、この辺は後で整えていきます。まずは最低限のパス選択部・再生停止ボタンを実装します。

Gridコントロールで領域を分けて、GUIを配置していきます。(全体のソースは後述)

領域分けの例です。

MainWindow.xaml

1
2
3
4
<Grid.RowDefinitions>
    <RowDefinition Height="9*"></RowDefinition>
    <RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>

上記では波形表示領域とUI領域に分割する記述です。
画面を縦に9:1の比で分割するように設定しています。
横に分割する場合はGrid.ColumnDefinitionsで分割できます。

MainWindow.xaml

1
2
3
<Grid Grid.Row="0" Name="gridWaveArea">
            
</Grid>

分割の定義の下に上記のように記述すると、分割した1つの領域にコントロール(GUIパーツ)を配置することができます。この中にはGridコントロールも記述することができます。つまりGridコントロールの入れ子を作ることができます。これにより、細かく構造的にGUIパーツの配置を行うことができます。

MainWindow.xaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<Grid Grid.Column="1" Name="gridCtrlButtons">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"></ColumnDefinition>
        <ColumnDefinition Width="*"></ColumnDefinition>
        <ColumnDefinition Width="*"></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <Button Grid.Column="0" Margin="1" Name="buttonReturn" Click="click_buttonReturn">戻し</Button>
    <Button Grid.Column="1" Margin="1" Name="buttonStartPause" Click="click_buttonStartandPause">再生</Button>
    <Button Grid.Column="2" Margin="1" Name="buttonNext" Click="click_buttonNext">送り</Button>
</Grid>

戻し/再生/送りボタンの部分はこんな感じ。NameプロパティでC#のソース上でボタンを操作することができるようになります。(ボタンを操作するための変数の宣言というイメージ。)また、Clickプロパティはボタンをクリックしたときの動作関数です。ここには関数名を記述して、C#ソース上でこの関数を定義します。

機能実装

C#ソースの方を記述してボタンなどの機能を実装していきます。
サンプルプログラムで定義されているグローバル変数は予めコピーしてきたものとします。

パス選択部

「…」ボタンを押したらフォルダブラウザダイアログを表示して音楽ファイル格納先を選択できるようにします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void click_buttonBrowser(object sender, RoutedEventArgs e)
    {
    /* フォルダを選択させる */
    var folderBrowserDialog = new Forms.FolderBrowserDialog();

    folderBrowserDialog.Description = "MP3ファイルを格納しているフォルダーを選択してください。";

    if(folderBrowserDialog.ShowDialog() == Forms.DialogResult.OK)
    {
         basePath = folderBrowserDialog.SelectedPath;
        textboxBasePath.Text = basePath;
    }
    else
    {
        /* OK以外のときはリターンする */
         return;
     }

    /* 選択したパス内のMP3ファイルを取得する */
    fileNames = Directory.GetFiles(basePath, "*.mp3", SearchOption.AllDirectories);

    /* 音楽を再生する前準備 */
     setAudioRender();

 }

ダイアログの表示はFormsのFolderBrowserDialogクラスを使用します。

1
2
3
4
    /* フォルダを選択させる */
    var folderBrowserDialog = new Forms.FolderBrowserDialog();

    folderBrowserDialog.Description = "MP3ファイルを格納しているフォルダーを選択してください。";

このクラスは、FormsというWPFとは別の環境で使えるライブラリを利用します。
Formsを利用するには、ソリューションエクスプローラの「参照」を右クリックし、「参照の追加」を選択します。 出てきたダイアログ上の検索欄で「Forms」と検索して「System.Windows.Forms」にチェックします。



C#ソース上では以下のように記述して呼び出せるようにします。

1
using Forms = System.Windows.Forms;

フォルダを選んでOKしたときは選択したパスの格納と表示を行います。OKを押さなかったときは何もしません。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    if(folderBrowserDialog.ShowDialog() == Forms.DialogResult.OK)
    {
         basePath = folderBrowserDialog.SelectedPath;
        textboxBasePath.Text = basePath;
    }
    else
    {
        /* OK以外のときはリターンする */
         return;
     }

パス選択後はそのフォルダ内に存在するMP3ファイルのファイル名を配列に格納します。
(GetFilesメソッドの代3引数でサブフォルダの中のMP3ファイルも取得するようにしています。)
その後、音楽ファイルを再生するための前処理を行う関数をコールします。

1
2
3
4
5
/* 選択したパス内のMP3ファイルを取得する */
fileNames = Directory.GetFiles(basePath, "*.mp3", SearchOption.AllDirectories);

/* 音楽を再生する前準備 */
setAudioRender();

音楽再生の前処理

サンプルプログラムの音楽の再生に関わる部分だけ抜き出して関数化しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private void setAudioRender()
{
    // ファイル名の拡張子によって、異なるストリームを生成
    audioStream = new AudioFileReader(fileNames[fileNamesIndex]);

    // コンストラクタを呼んだ際に、Positionが最後尾に移動したため、0に戻す
    audioStream.Position = 0;

    // プレーヤーの生成
    outputDevice = new WaveOut();

    // 音楽ストリームの入力
    outputDevice.Init(audioStream);
}

本パートでは実装しませんが、1曲終わる毎に、次の曲再生のためにこの関数を呼び出すような形にします。

再生/一時停止ボタン

再生ボタンと一時停止のボタンは1つのボタンに割り付けてトグル動作できるようにします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void click_buttonStartandPause(object sender, RoutedEventArgs e)
{
    /* BasePath未入力 or MP3ファイルのないパスを選択した場合は何もしない */
    if (fileNames.Length == 0)
    { return; }

    if(isPlaying)
    {
        /* 再生中のとき => ポーズ状態へ以降 */
                
        pauseAudio();  //音楽を一時停止

        buttonStartPause.Content = "再生";
        isPlaying = false;
    }
    else
    {
        /* ポース中のとき => 再生状態へ以降 */
                
        startAudio();  //音楽を再生
                
        buttonStartPause.Content = "停止";
        isPlaying = true;
    }
}

MP3ファイルが格納されていないパスが選択されている場合に再生ボタンを押すとエラー終了を起こす可能性があるので例外処理を行っています。

1
2
3
/* BasePath未入力 or MP3ファイルのないパスを選択した場合は何もしない */
if (fileNames.Length == 0)
{ return; }

トグル動作の部分は以下です。
変数を使って音楽再生中かどうかを状態管理して、再生中のときは一時停止する処理&表示、一時停止中のときは再生する処理&表示を行います。状態管理の変数は、グローバル変数として宣言していますが、NAudioに状態を返してくれるプロパティがいるみたいです。もしかしたらそっちで状態管理するように変更するかも…?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
if(isPlaying)
{
    /* 再生中のとき => ポーズ状態へ以降 */
                
    pauseAudio();  //音楽を一時停止

    buttonStartPause.Content = "再生";
    isPlaying = false;
}
else
{
    /* ポース中のとき => 再生状態へ以降 */
                
    startAudio();  //音楽を再生
                
    buttonStartPause.Content = "停止";
    isPlaying = true;
}

音楽の停止処理と再生処理は以下のように関数化しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
private void startAudio()
{
    // 音楽の再生 (おそらく非同期処理)
    outputDevice.Play();
}

private void pauseAudio()
{
    //音楽の停止
    outputDevice.Stop();
}

今の時点では関数化する必要は全くありませんが、この後波形表示を実装する際に、音楽の停止と同時に表示も止めれるようにしたいので関数化しています。

成果物

動作

今回の実装でできあがったものはこんな感じ。
最初の方は動画に写っていませんが、フォルダブラウザダイアログが立ち上がってフォルダを選択できるようになっています。

ちなみに音源は自作で打ち込んだものを使用しています。
(UNISON SQUARE GARDENのスノウリバースです。)

ソース

■MainWindow.xaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<Window x:Class="DesktopMusicPlayer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DesktopMusicPlayer"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        AllowsTransparency="True" 
        Background="#80000024"
        WindowStyle="None">
    <Window.ContextMenu>
        <ContextMenu>
            <MenuItem Header="Exit" Click="Quit_Clicked"/>
        </ContextMenu>
    </Window.ContextMenu>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="9*"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>
        <Grid Grid.Row="0" Name="gridWaveArea">
            
        </Grid>
        <Grid Grid.Row="1" Name="gridUiArea">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="3*"></ColumnDefinition>
                <ColumnDefinition Width="*"></ColumnDefinition>
            </Grid.ColumnDefinitions>
            <Grid Grid.Column="0" Name="gridBasePathInput">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"></ColumnDefinition>
                    <ColumnDefinition Width="3*"></ColumnDefinition>
                    <ColumnDefinition Width="*"></ColumnDefinition>
                </Grid.ColumnDefinitions>
                <TextBlock Grid.Column="0" Foreground="white" HorizontalAlignment="Center" VerticalAlignment="Center">BasePath</TextBlock>
                <TextBox Grid.Column="1" Margin="1" IsReadOnly="True" Name="textboxBasePath"></TextBox>
                <Button Grid.Column="2" Margin="1" Name="buttonBrowser" Click="click_buttonBrowser">...</Button>
            </Grid>
            <Grid Grid.Column="1" Name="gridCtrlButtons">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"></ColumnDefinition>
                    <ColumnDefinition Width="*"></ColumnDefinition>
                    <ColumnDefinition Width="*"></ColumnDefinition>
                </Grid.ColumnDefinitions>
                <Button Grid.Column="0" Margin="1" Name="buttonReturn" Click="click_buttonReturn">戻し</Button>
                <Button Grid.Column="1" Margin="1" Name="buttonStartPause" Click="click_buttonStartandPause">再生</Button>
                <Button Grid.Column="2" Margin="1" Name="buttonNext" Click="click_buttonNext">送り</Button>
            </Grid>
        </Grid>
    </Grid>
</Window>

■Mainwindow.xaml.cs

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using NAudio.Wave;
using NAudio.Dsp;
using Microsoft.Win32;
using System.Runtime.CompilerServices;
using Forms = System.Windows.Forms;
using System.IO;
using System.Windows.Threading;

namespace DesktopMusicPlayer
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // ウィンドウをマウスのドラッグで移動できるようにする 
            this.MouseLeftButtonDown += (sender, e) => this.DragMove();
            isPlaying = false;
            fileNamesIndex= 0;
        }

        /* グローバル変数 */
        private string   basePath;                               //音楽ファイルを格納しているフォルダのパス
        private string[] fileNames;                              //音楽ファイルの名前
        private bool     isPlaying;                              //再生中かどうかのフラグ
        private int fileNamesIndex;                              //fileNamesのindex
        private WaveOut outputDevice;                            //音楽プレーヤー
        private AudioFileReader audioStream;                     //フーリエ変換前の音楽データ
        private readonly long reciprocal_of_FPS = 167000;        //60(fps)の逆数 (100ns)
        private float[,] result;                                 //フーリエ変換後の音楽データ
        private DispatcherTimer timer = null;                    //タイマー割り込みに使用するタイマー
        private Line[] bar;                                      //音声波形表示に使用するLine(バー)
        private Brush brush;                                     //音声波形表示のLine(バー)に使用するブラシ
        private int bytePerSec;                                  //1秒当たりのバイト数
        private int musicLength_s;                               //音楽の長さ (秒)
        private int playPosition_s;                              //再生位置 (秒)
        private int drawPosition;                                //音声波形表示位置
        private bool barDrawn = false;                           //描画済みのLine(バー)があるかを示すフラグ (生成済み = true, 未生成 = false)


        private void click_buttonReturn(object sender, RoutedEventArgs e)
        {
            /* BasePath未入力 or MP3ファイルのないパスを選択した場合は何もしない */
            if (fileNames.Length == 0)
            { return; }
        }

        private void click_buttonStartandPause(object sender, RoutedEventArgs e)
        {
            /* BasePath未入力 or MP3ファイルのないパスを選択した場合は何もしない */
            if (fileNames.Length == 0)
            { return; }

            if(isPlaying)
            {
                /* 再生中のとき => ポーズ状態へ以降 */
                
                pauseAudio();  //音楽を一時停止

                buttonStartPause.Content = "再生";
                isPlaying = false;
            }
            else
            {
                /* ポース中のとき => 再生状態へ以降 */
                
                startAudio();  //音楽を再生
                
                buttonStartPause.Content = "停止";
                isPlaying = true;
            }
        }

        private void click_buttonNext(object sender, RoutedEventArgs e)
        {
            /* BasePath未入力 or MP3ファイルのないパスを選択した場合は何もしない */
            if (fileNames.Length == 0)
            { return; }
        }

        private void click_buttonBrowser(object sender, RoutedEventArgs e)
        {
            /* フォルダを選択させる */
            var folderBrowserDialog = new Forms.FolderBrowserDialog();

            folderBrowserDialog.Description = "MP3ファイルを格納しているフォルダーを選択してください。";

            if(folderBrowserDialog.ShowDialog() == Forms.DialogResult.OK)
            {
                basePath = folderBrowserDialog.SelectedPath;
                textboxBasePath.Text = basePath;
            }
            else
            {
                /* OK以外のときはリターンする */
                return;
            }

            /* 選択したパス内のMP3ファイルを取得する */
            fileNames = Directory.GetFiles(basePath, "*.mp3", SearchOption.AllDirectories);

            /* 音楽を再生する前準備 */
            setAudioRender();

        }

        private void setAudioRender()
        {
            // ファイル名の拡張子によって、異なるストリームを生成
            audioStream = new AudioFileReader(fileNames[fileNamesIndex]);

            // コンストラクタを呼んだ際に、Positionが最後尾に移動したため、0に戻す
            audioStream.Position = 0;

            // プレーヤーの生成
            outputDevice = new WaveOut();

            // 音楽ストリームの入力
            outputDevice.Init(audioStream);

        }


        private void startAudio()
        {
            // 音楽の再生 (おそらく非同期処理)
            outputDevice.Play();
        }

        private void pauseAudio()
        {
            //音楽の停止
            outputDevice.Stop();
        }


        /// <summary>
        /// コンテキストメニューのExitが押されたときのイベントハンドラ
        /// </summary>
        /// <param name="sender">イベント送信元</param>
        /// <param name="e">イベント引数</param>
        private void Quit_Clicked(object sender, RoutedEventArgs e)
        {
            Close();
        }
    }
}

まとめ

今回は自作のデスクトップ音楽プレーヤーを作るということで、サンプルプログラムをもとに音楽を再生する部分を実装しました。次回は1曲終了後にループできるように実装します。

参考にさせていただいたサイト

Built with Hugo
テーマ StackJimmy によって設計されています。