MP3タグ(ID3)の編集が面倒くさいと思ったことはありませんか?
そこでTagLibというフリーのライブラリを使って、自分の好きなようにMP3タグ編集できるツールを作りました。
この記事では、ツールの紹介、Taglibの使い方をプログラミング初心者に向けてコード付きで解説しています。
プログラミングの勉強としてではなく、単にツールとしてご利用になりたい方は自由に使っていただいて結構ですが、あくまでも自己責任でお願いします。
実際の利用に際しては、必ずオリジナルのMP3をバックアップしておくことをお忘れなく。
実行ファイルのダウンロードと使い方
実際の実行ファイルは下記からダウンロードできます。
ZIPファイルで圧縮していますので、任意のフォルダに解凍後、Mp3TagEditor.exe を実行して下さい。
実際に操作しながら読み進めて頂くと、より理解しやすいかと思います。
尚、動作確認で用いるMP3ファイルは、ファイル名やタグが書き変わっても良いものをご用意ください。
画面レイアウト
次回の記事に掲載されているソースコードを読むときの前提知識となりますので、一通りの使い方は理解しておいて下さい。
まず画面レイアウトは次の通りです。

基本操作
MP3のあるフォルダ、又はMP3ファイルを一覧部分に直接ドラッグ&ドロップすることで、ファイル名(拡張子は省略)とMP3タグが表示されます。
フォルダの下にサブフォルダが有る場合、そのサブフォルダも含めた全てのMP3ファイルが表示されます。
基本操作としては
- 一覧部分に表示されている内容に対して、「絞り込み条件」で絞り込みを行う
- 対象項目に対してフォーマットに記載したルールで文字列を書き込むか、文字列の置換を行う
- タグ更新ボタンをクリックして編集内容をMP3ファイルに適用する
の3ステップになります。
絞り込み検索
絞り込み検索は「簡易」と「詳細」の2つのモードがあり、起動時は「簡易」にチェックが入っています。
「簡易」にチェックが入っている場合、項目名(列名)とキーワードを入力することで、その項目にキーワードが含まれている行だけ絞り込みを行います。
画面の例では ”アーティスト いきものがかり” と入力されていますので、アーテイスト列に ”いきものがかり” というキーワードが含まれている行が絞り込まれます。
複数項目を指定したい場合、”アーティスト いきものがかり タイトル よ " とすると、アーティストに ”いきものがかり” が含まれていて、さらに タイトルに ”よ” の文字が含まれている行が絞り込まれます。
「簡易」のチェックが外れている場合、DataGridView に指定できる絞り込み条件の記述方法が利用できます。
例えば、アーティストが ”いきものがかり” で、タイトルが ”心” 以外から始まる行を絞り込みたい場合、 ”アーティスト = 'いきものがかり' and タイトル not like '心%' ” という記述が出来ます。
詳しくは、こちら をご参照ください。
フォーマットを使った文字列のセット
対象項目のコンボボックスで選んだ項目に対して、指定したフォーマットの文字列をセットします。
フォーマットには、固定文字列の他に任意のタグ名や連番を指定できます。
画面の例では "{連番}.{アルバム名}_{アーティスト}" と記載されていますが、仮にアルバム名が ”いきものばかり ~メンバーズBESTセレクション” だとすると、次のようになります。
01.いきものばかり ~メンバーズBESTセレクション~_いきものがかり
また、連番部分のフォーマットは、連番書式で指定することが可能です。
画面の例では "0#" になっていますので、2桁で先頭を0で埋める指定になっています。
もし3桁で0埋めをしない場合、 "###" という指定ができます。
単純置換と正規表現による置換
対象項目に対して「置換」をしたい場合、置換前の文字列と置換後の文字列を指定します。
「置換」には「正規表現」と「単純」の2モードがあり、初期値は「正規表現」モードになっています。
正規表現はすこし分かり難いので、こちら のサイトを参考にして頂ければと思います。
画面の例では "^.*?[-. ]" となっていますが、これは「先頭から数えて最初にハイフンかピリオドか半角スペースのいずれかが見つかるまで」という指定になります。
置換後の入力欄は未入力になっていますので、「上記の指定に一致する部分を消去する」という動作になります。
これは何をしているかというと、例えばファイル名などの先頭に連番が振られている場合、連番部分だけ抹消するという動作になります。
「正規表現」のチェックを外すと「単純」モードになりますが、こちらは単に置換前文字列を置換後文字列に置換するというものです。
ソースコード(プロジェクト)一式のダウンロードと解説
プロジェクト一式(TagLib含む)は下記からダウンロードが可能です。
プロジェクトを任意のフォルダに解答の上、Mp3TagEditor.sln をダブルクリックするとVisual Studio 2019 が立ち上がり、ソースコードが表示されると思います。
もしTagLibの参照でエラーになるようなら、一旦TagLibをアンインストールの上、再度NuGetからインストールすることで解決します。
ここからは、ダウンロードして頂いたソースコードに対するカスタマイズや、皆さんのソースコードへの流用のヒントになるような観点で、ポイントを解説していきます。
コンストラクタとフォームロード処理
コンストラクタにはドロップダウンをMainFormの表示位置を設定するコードを書いています。
//対象項目コンボボックスのドロップダウンを入力不可に設定
uxTargetColumn.DropDownStyle = ComboBoxStyle.DropDownList;
//スクリーンの中央にWindowを表示
this.StartPosition = FormStartPosition.CenterScreen;
これらはVisual Studio のプロパティでも設定することが出来ますので、好みに合わせてもらえればOKです。

フォームロード(MainForm_Load)も
//フィルターの簡易モードをONにする
uxIsEasy.Checked = true;
//正規表現を用いた置換を有効にする
uxUseRegular.Checked = true;
//変更前の初期値として、先頭から'-','.',' 'が見つかるまでが対象となる正規表現を設定
uxBeforeString.Text = "^.*?[-. ]";
という記述がありますが、これらはコンストラクタに記述しても良いですし、 Visual Studio のプロパティを設定しても構いません。
フォームロードはコンストラクタの後に呼び出されるイベントで、画面が表示されるタイミングで呼び出されますので、
this.StartPosition = FormStartPosition.CenterScreen;
だけはフォームロードの前(つまりコンストラクタが実行される時)に設定しておく必要があります。
フォームロードでは、DataGridViewにドラッグ&ドロップの受付と行番号の表示に対応させるため、次の2行のコードを記述しています。
//DataGridViewにドラッグ&ドロップ機能を付加
SetDragDrop(uxMp3List);
//DataGridViewに行番号表示機能を付加
SetLineNumber(uxMp3List);
これらは他のプログラムで再利用しやすいように、メソッド化しています。
SetDragDropメソッド
任意のコントロールにドラッグ&ドロップの機能を付加するメソッドで、DataGridViewを指定しています(17行目と20行目)。
DataGridViewに対してDragEnterとDragDropのイベントハンドラを作成する書き方も多いですが、再利用を考えてメソッドの中でイベントハンドラを追加するようにしました。
private void SetDragDrop(Control control)
{
//ドラッグ&ドロップを受け付けられるようにする
control.AllowDrop = true;
//ドラッグが開始された時のイベント処理(マウスカーソルをドラッグ中のアイコンに変更)
control.DragEnter += (s, e) =>
{
//ファイルがドラッグされたとき、カーソルをドラッグ中のアイコンに変更し、そうでない場合は何もしない。
e.Effect = (e.Data.GetDataPresent(DataFormats.FileDrop)) ? DragDropEffects.Copy : e.Effect = DragDropEffects.None;
};
//ドラッグ&ドロップが完了した時の処理(ファイル名を取得し、ファイルの中身をTextプロパティに代入)
control.DragDrop += (s, e) =>
{
//ドロップされたすべてのファイル名を取得する
string[] filenames = (string[])e.Data.GetData(DataFormats.FileDrop, false);
//タグの一覧を表示
ShowData(GetMp3Tag(filenames));
};
}
SetLineNumberメソッド
DataGridViewに行番号を付加する部分です。
これはGoogle検索で見つかったソースコードをそのまま掲載しています。
少し改造すると行番号の色を変えたり、別のセルに別の文字列を表示することも出来そうですが、単に行番号を表示したいというだけなら、このまま利用できます。
private void SetLineNumber(DataGridView dgb)
{
dgb.CellPainting += (sender, e) =>
{
//列ヘッダーかどうか調べる
if (e.ColumnIndex < 0 && e.RowIndex >= 0)
{
//セルを描画する
e.Paint(e.ClipBounds, DataGridViewPaintParts.All);
//行番号を描画する範囲を決定する
//e.AdvancedBorderStyleやe.CellStyle.Paddingは無視しています
Rectangle indexRect = e.CellBounds;
indexRect.Inflate(-2, -2);
//行番号を描画する
TextRenderer.DrawText(e.Graphics,
(e.RowIndex + 1).ToString(),
e.CellStyle.Font,
indexRect,
e.CellStyle.ForeColor,
TextFormatFlags.Right | TextFormatFlags.VerticalCenter);
//描画が完了したことを知らせる
e.Handled = true;
}
};
}
ShowDataメソッド
これはDataGridViewの体裁を整えつつ、DataTableの内容を表示するメソッドです。
引数に表示したDataTableを渡す仕様になっていて、8行目にDataGridViewのDataSourceプロパティに引数を代入しています。
これは8行目である必要はなく、メソッドの先頭に記述しても構いません。
ただ、列幅や列のReadOnly属性、列の入れ替え許可などの設定は、列が存在しないと出来ません。
逆に言うと、DataSourceにDataTableを代入した時点で新たな列が作られるため、その列に対して設定する必要があるので、ここだけは順番を気にする必要があります。
private void ShowData(DataTable dt)
{
uxMp3List.Font = new Font("Meiryo UI", 9f); //フォントの指定
uxMp3List.AllowUserToAddRows = false; //ユーザー操作による行追加を禁止
uxMp3List.AllowUserToDeleteRows = false; //ユーザー操作による行削除を禁止
uxMp3List.BackgroundColor = Color.White; //DataGridViewの背景色を設定
uxMp3List.AlternatingRowsDefaultCellStyle.BackColor = Color.FromArgb(230, 230, 255); //DataGridViewの奇数行の背景色を設定
uxMp3List.DataSource = dt; // DataTablをDataGridViewに表示
uxMp3List.AllowUserToOrderColumns = true; //ユーザー操作による列の入れ替えを許可
//DataGridViewに設定したい列幅の配列を作り、これを使ってDataGridViewの列幅を設定
int[] width = new int[] {200,200,200,200,200,200,100,100,100,200,200,120,120,120,300};
Enumerable.Range(0, width.Length).Select(i => uxMp3List.Columns[i].Width = width[i]).ToList();
//DataGridViewの列に設定したいReadOnly属性の配列を作り、これを使って列のReadOnly属性を設定、
bool[] enable = new bool[] { false, false, false, false, false, false, false, false, false, false, false, true, true, true, true };
Enumerable.Range(0, enable.Length).Select(i => uxMp3List.Columns[i].ReadOnly = enable[i]).ToList();
}
また、列幅やReadOnlyは設定したい値を配列に持たせて、Enumerable.Range を使ってuxMp3List.Columns[i] に代入していますが、これは好みの問題です。
以下の様に全ての列に対して、インデックス番号や列名を使って値を代入する方法もあります。
uxMp3List.Columns[0].Width =200;
uxMp3List.Columns[1].Width =200;
uxMp3List.Columns[2].Width =200;
uxMp3List.Columns["ファイル名"].Width =200;
uxMp3List.Columns["タイトル"].Width =200;
uxMp3List.Columns["アルバムアーティスト"].Width =200;
分かりやすさを優先するなら列名を使って個々の列を設定する方法が良いと思いますが、ソースコードの行数が長くなるので今回は採用しませんでした。
CreateDataTableメソッド
これはuxMp3List に表示するDataTableを作るためだけのメソッドです。
ShowDataメソッドで列幅を設定した時のように、列名を配列にしてEnumerable.Rangeで列を追加していく方法も出来ますが、別のソースコードに(列名を書き換えて)流用する際、Columnsの第2引数に変数の型を指定したい場合もあるため、今回はこのようにしました。
private DataTable CreateDataTable()
{
DataTable dt = new DataTable();
dt.Columns.Add("ファイル名");
dt.Columns.Add("タイトル");
dt.Columns.Add("アーティスト");
dt.Columns.Add("アルバムアーティスト");
dt.Columns.Add("アルバム名");
dt.Columns.Add("作曲家");
dt.Columns.Add("年");
dt.Columns.Add("トラック番号");
dt.Columns.Add("ディスク番号 ");
dt.Columns.Add("ジャンル");
dt.Columns.Add("コメント");
dt.Columns.Add("時間");
dt.Columns.Add("ビットレート(Kbps)");
dt.Columns.Add("タグ");
dt.Columns.Add("ファイルパス");
return dt;
}
DataTableを作る部分をわざわざメソッドにしている理由は、フォームロードとドラッグ&ドロップの2か所で呼び出す必要があるからです。
プログラム起動時、uxMp3List に何も表示されないと寂しいので、せめて列名だけでも表示させたいと考えた時、フォームロードにも同じことを記述する必要が生じます。
それを避けるためメソッド化しました。
GetMp3Tagメソッド
SetDragDropメソッドの中で呼ばれていますが、ドラッグ&ドロップで受け取ったファイル名のMP3タグを読み出し、DataTableに格納して返すメソッドです。
最初にCreateDataTableメソッドでDataTableを作成しています。
次に、引数として渡されたファイル名の配列からファイル名を取り出し、 filelist に追加しています。
何故 filelist に追加しているかと言うと、渡されたファイル名が実はフォルダだった場合、そのフォルダ以下のファイルを全て取り出したかったからです。
その為、渡されたファイル名がフォルダか否かをチェックし、フォルダだったら配下のファイルを全て取り出して filelist に追加しています。
//ドロップされたファイル名にフォルダが含まれていれば、その配下のファイルを追加する
List<string> filelist = new List<string>();
foreach (string filename in fileNames)
{
//ファイル名がディレクトリ名で処理を分岐
if (Directory.Exists(filename))
{
//フォルダ名なら配下のファイルを全て取得し、filelistに追加
filelist.AddRange(Directory.GetFiles(filename, "*.mp3", SearchOption.AllDirectories).ToArray());
}
else
{
//ファイル名なら、そのまま filelist に追加
filelist.Add(filename);
}
}
こうやってドラッグ&ドロップで渡されたファイルを全て取り出し、ループ処理にて1ファイルずつMP3タグを抽出、DataTableに追加しています。
foreach (string filename in filelist)
{
//TagLibクラスのインスタンスを生成
TagLib.File mp3 = TagLib.File.Create(filename);
//DataRowを作成し
DataRow dr = dt.NewRow();
//MP3タグの値をセット
dr["ファイル名"] = Path.GetFileNameWithoutExtension(filename);
dr["タイトル"] = UTF2Jis(mp3.Tag.Title);
dr["アーティスト"] = string.Join(";", mp3.Tag.Artists.Select(i=> UTF2Jis(i)));
dr["アルバムアーティスト"] = string.Join(";", mp3.Tag.AlbumArtists.Select(i => UTF2Jis(i)));
dr["アルバム名"] = UTF2Jis(mp3.Tag.Album);
dr["作曲家"] = string.Join(";", mp3.Tag.Composers.Select(i => UTF2Jis(i)));
dr["年"] = mp3.Tag.Year;
dr["トラック番号"] = mp3.Tag.Track;
dr["ディスク番号 "] = mp3.Tag.Disc;
dr["ジャンル"] = string.Join(";", mp3.Tag.Genres.Select(i => UTF2Jis(i)));
dr["時間"] = string.Format("{0:D2}:{1:D2}:{2:D2}", mp3.Properties.Duration.Hours, mp3.Properties.Duration.Minutes, mp3.Properties.Duration.Seconds);
dr["ビットレート(Kbps)"] = mp3.Properties.AudioBitrate;
dr["タグ"] = mp3.Tag.TagTypes;
dr["コメント"] = UTF2Jis(mp3.Tag.Comment);
dr["ファイルパス"] = filename;
//DataTableにDataRowを追加
dt.Rows.Add(dr);
}
メソッドの最後(リターンの直前)にAcceptChangesメソッドを読んでいます。
dt.AcceptChanges();
DataTableの各行(DataRow)はステータス(RowStat)を持っていて、その行が追加(Added)されたのか、変更(Modified)されたが分かるようになっています。
後にMP3ファイルに編集内容を書き戻す時、タグを編集したものだけを特定する際にRowStateが使えそうですが、そこには少しだけ工夫が必要です。
直前のforeach ループでMP3タグをDataTableに追加した訳ですから、今の RowState は追加状態(Added)になっています。
この状態でDataRowに変更を加えても、RowStat は Addedが優先されてModifiedにはならないのです。
このRowStateをリセットするメソッドが AcceptChanges であり、このメソッドを呼ぶことでDataTableに登録されている全てのDataRowのRowStateが リセット(Unchanged)されます。
こうすることで、以降このDataTableに対して変更を加えると、RowState が Modified になってくれます。
SetMp3Tag
引数で渡されたDataTableの内容(タグ情報)をMP3ファイルに書き込むメソッドです。
ポイントとしては、 foreach でループしながら DataRow を取り出し、RowStateが変更状態(Modified)である場合のみ、MP3ファイルにタグを書き戻すようにしている点です。
foreach (DataRow dr in dt.Rows)
{
//値が変更された行だけタグを書き換えたいので、変更されたかどうかをDataRowのRowStatusでチェック
if (dr.RowState == DataRowState.Modified)
{
var filename = dr["ファイルパス"].ToString();
TagLib.File mp3 = TagLib.File.Create(filename);
mp3.Tag.Title = dr["タイトル"].ToString();
mp3.Tag.Artists = dr["アーティスト"].ToString().Split(';').Select(i => i.Trim()).ToArray();
mp3.Tag.AlbumArtists = dr["アルバムアーティスト"].ToString().Split(';').Select(i => i.Trim()).ToArray();
mp3.Tag.Album = dr["アルバム名"].ToString();
mp3.Tag.Composers = dr["作曲家"].ToString().Split(';').Select(i => i.Trim()).ToArray();
mp3.Tag.Year = uint.TryParse(dr["年"].ToString(), out uint year) ? year : (uint)DateTime.Now.Year;
mp3.Tag.Track = uint.TryParse(dr["トラック番号"].ToString(), out uint track) ? track : 0;
mp3.Tag.Disc = uint.TryParse(dr["ディスク番号 "].ToString(), out uint disc) ? disc : 0;
mp3.Tag.Genres = dr["ジャンル"].ToString().Split(';').Select(i => i.Trim()).ToArray();
mp3.Tag.Comment = dr["コメント"].ToString();
//実際のmp3ファイルのタグを更新
mp3.Save();
実際のMP3ファイルにタグを書き戻すには、Save メソッドを呼ぶだけで完了します。
タグを書き戻した次の処理は、ファイル名の変更処理です。
1列目のタイトル列と、最終列のファイルパスの内容を比較して、違っていたらタイトル列の内容でファイル名を変更しています。
//ファイル名列に表示されているファイル名とファイルパス列のファイル名が異なるかチェック
string new_name = dr["ファイル名"].ToString();
if (new_name != Path.GetFileNameWithoutExtension(filename))
{
//ファイルパス列のフルパスからファイル名部分を置き換えた新しいファイルパスを作成
new_name = Path.Combine(Path.GetDirectoryName(filename), new_name)+ Path.GetExtension(filename);
//ファイル名の変更(Moveメソッドを利用)
File.Move(filename, new_name);
}
ファイル名の変更はMoveメソッドを使います。
Moveメソッドはファイルの移動を行うメソッドですが、フォルダを同じにしておけば、ファイル名の変更になります。
変更後のファイル名を作るため、現在のフルパスからディレクトリ(フォルダ)と拡張子を取り出しているので、少々ややこしい記述になっています。
UTF2Jisメソッド
文字コードがShift-JISか否かを判定し、Shift-JISでなければ Shift-JISに変換するメソッドです。
MP3タグのタイトル、アーティスト名、アルバム名、コメントなどは日本語が登録できるのですが、MP3タグにもバージョンがあって、SHIFT-JIS で入っていたりUTF16で入っていたりと統一されていません。
これに対応するため、SHIFT-JISか否かを判定し、SHIFT-JIS でなければ SHIFT-JIS に文字コードを変換するようにしています。
具体的には、文字コードを一旦バイトの配列に変換して、再び SHIFT-JIS に変換し直した結果と、元々の文字列を比較しています。
もし元々の文字列がSHIFT-JISなら元に戻るはずなので、この方法で元に戻らなければ少なくともSHIFT-JISではなかったと判断できます。
//Shift-JISのエンコーディングを取得
var enc = System.Text.Encoding.GetEncoding("shift-jis");
//取得したエンコーディングを使って引数ををバイト列に変換後、再びShift-JISの文字列に変換
string cnv_str = enc.GetString(enc.GetBytes(str));
//上記の変換結果と引数を比較し、異なればShift-JISではないと判断
if (str != cnv_str)
{
//引数をShift-JISに変換
var l_bytes = System.Text.Encoding.Unicode.GetBytes(str);
//Unicodeにバイト列に変換すると1バイト毎に0x00 が付加されるため、0x00を取り除いてShift-JISに変換
return System.Text.Encoding.GetEncoding("shift-jis").GetString(l_bytes.Where(i => i > 0).ToArray<byte>());
}
もしSHIFT-JISでないと判断した場合、UNICODE から 一旦バイト配列に変換後、SHIFT-JIS に変換し直すという手順を踏むわけですが、バイト配列に変換すると1バイトごとに0x00 が付加されてしまいます。
このままだとSHIFT-JIS に正しく変換できないので、苦肉の策で 0x00を取り除く処理を入れています。
System.Text.Encoding.GetEncoding("shift-jis").GetString(l_bytes.Where(i => i > 0).ToArray());
約1000ファイルの様々なMP3ファイルで試したところ、有名どころのフリーのMP3タグと同じ結果が得られたので、多分これで問題無いと判断しました。
フィルター実行ボタンイベントハンドラ
絞り込み処理は、簡易モード(uxIsEasy)にチェックが入っている場合、入力された文字から絞り込み条件を作る処理を行い、チェックが入っていなければ、入力された文字をそのまま絞り込み条件として使っています。
DataTableには DefaulView クラスがプロパティとして公開されており、RowFilterプロパティに条件を入れることで、DataTableの中身を絞り込んでくれます。
DataGridVIew(uxMp3List)のDataSource には DataTable が入っていますが、DataSourceはオブジェクト型なので、型変換を行ってDefaultViewのプロパティにアクセスしています。
//絞り込み条件の初期値として uxFilter.Textをそのまま代入
string filter = uxFilter.Text;
//簡易モードにチェックが入っている場合、uxFilter.Textの内容を解析
if (uxIsEasy.Checked)
{
//簡易モードの場合、入力された文字列を半角又は全角スペースで分割
string[] items = uxFilter.Text.Split(new char[] { ' ', ' ' }, StringSplitOptions.RemoveEmptyEntries);
//列名と条件の組み合わせを順に取り出しListに追加
List<string> list = new List<string>();
for (int i = 0; i < items.Length / 2; i++)
{
list.Add(items[i * 2] + " like '%" + items[i * 2 + 1] + "%'");
}
//取り出した列と条件の組み合わせに対して、間に and を挟んで結合
filter = string.Join(" and ", list.ToArray());
}
//DataGridViewが表示中のDataTableに対して、絞り込み条件を設定
((DataTable)uxMp3List.DataSource).DefaultView.RowFilter = filter;
簡易モードの時は、半角又は全角で分割し、「列名1」、「値1」、「列名2」、「値2」・・・という前提で2個ずつ取り出し、曖昧検索になるような条件式を作り出しています。
置換ボタン
ここは大きく2つの処理に分かれています。
1つは、フォーマット(uxFormat)の中身を展開しながら指定された列の全ての行に値をセットしていく処理、もう1つは入力されたフォーマットをドロップダウンリストに登録する処理です。
まず、指定された列の全ての行に値をセットしている処理を見てみましょう。
ここでは、DataGridView から 行(DataGridViewRow)を順番に取り出し、{列名} の文字列を、その列の値で置き換えているだけです。
ちなみに、DataGirdViewRowではなく、DataSourceからDataTableを取り出して、DataRowに対して変更を加えても良いのですが、DataGridViewのヘッダをクリックしてソートが行われると、DataGridViewとDataTableの行の並びが一致しなくなるので、ここではあえてDataGirdViewRowを使っています。
replace はこのメソッドの中で使うローカル関数です。
//DataGridViewに表示されている行をループ処理で取り出す
foreach (DataGridViewRow dgr in uxMp3List.Rows)
{
//フォーマットの中身に{列名}の記述があれば、列の値に置き換える
string str = replace(uxMp3List.Columns, dgr, uxFormat.Text);
//{連番}の記述があれば、連番に置き換える
str = str.Replace("{連番}", string.Format("{0:" + uxNumberFormat.Text + "}", no++));
//フォーマット加工した結果を対象項目の列に代入
dgr.Cells[uxTargetColumn.Text].Value = str;
}
では、ローカル関数の中身を見ていきましょう。
第1引数はDataGridViewの列を保持しているコレクションクラス、第2引数は DataGridViewRow、第3引数は文字列ですが、ここは uxFormat.Text の値がセットされています。
そして、全ての列に対して以下の3ステップの処理を行うというのが、このローカル関数の役割になります。
- DataGridViewColumnCollection から列名を取り出して前後を "{"、 "}" で挟み、置換前の文字列を作る
- 次に、DataGridViewRow から 列名でセルを特定し、そのセルの値から置換後の文字列を作る
- 第3引数で渡された文字列(uxFormat.Textの値)に対して、Replaceメソッドを使って文字列置換を行う
///{列名}の記述を列の値に置き換えるローカル関数
string replace(DataGridViewColumnCollection p_col,DataGridViewRow p_dgvr,string p_str)
{
for(int i = 0;i < p_col.Count;i ++)
{
// "{列名}"の文字列を作成
string l_sour = "{" + p_col[i].Name + "}";
// "{列名}"の文字列を列の値に置換
p_str = p_str.Replace(l_sour, p_dgvr.Cells[i].Value.ToString());
}
return p_str;
}
次はドロップダウンリストへの登録処理を見ていきましょう。
uxFormat.Text の値がドロップダウンリストに含まれているかを確認し、含まれていれば一旦削除してから、ドロップダウンの先頭に挿入しています。
こうすることにより、最新のフォーマット(最後に入力された uxFormat.Textの内容)が常にドロップダウンの先頭に来るようにしています。
//過去に使ったフォーマットならなら、ドロップダウンから一旦削除
string format = uxFormat.Text;
if (uxFormat.Items.Contains(format))
{
//ドロップダウンから削除
uxFormat.Items.Remove(format);
//RemoveするとText部がクリアされるので復旧
uxFormat.Text = format;
}
//ドロップダウンの先頭に登録
uxFormat.Items.Insert(0, format);
文字列置換の実行イベントハンドラ
正規表現チェックボックス(uxUseRegular.Checked)にチェックが入っていると正規表現を使った文字列置換を行い、チェックが無ければ単純な文字列置換を行っています。
格安MP3プレーヤーの中には、曲の再生順がファイル名のソート順というケースがあります。
この場合、ファイル名の先頭に連番を付ける事が多いのですが、付けるのは簡単ですが、抹消するのは少々厄介です。
というのは、連番と曲名の間がピリオドだったりアンダーバーだったり、半角スペースだったりする可能性がありますし、桁も1桁~3桁混在していたり、先頭に0が付加されていたりと様々なパターンが考えられます。
これらのパターンに対応するには、正規表現を使うのが一番手っ取り早いのです。
ただ、正規表現はとっつき難く、複雑な条件を書くのは難易度が高いのが難点です。
でも、簡単な条件であれば少し勉強すれば使える様になりますし、使えると何かと便利なので、ここで少しだけ使ってみることにしました。
ちなみに、ここでも DataTable ではなく、DataGridViewに対して文字列置換を行っています。
//正規表現クラスのインスタンスを生成
Regex reg = new Regex(uxBeforeString.Text);
//表示中の行に対するループ処理
foreach (DataGridViewRow dgr in uxMp3List.Rows)
{
//置換対象となる文字列を取得
string target = dgr.Cells[uxTargetColumn.Text].Value.ToString();
//正規表現チェックボックスのチェック状態を確認
if (uxUseRegular.Checked)
{
//正規表現にチェックが入っていると、正規表現として文字列置換を実施
dgr.Cells[uxTargetColumn.Text].Value = new Regex(uxBeforeString.Text).Replace(target, uxAfterString.Text);
}
else
{
//正規表現にチェックが入っていない場合、単純な文字列置換を実行
dgr.Cells[uxTargetColumn.Text].Value = target.Replace(uxBeforeString.Text, uxAfterString.Text);
}
}
フォームロード処理のところで、以下の様に初期値を設定しています。
最初の '^' は、先頭を表しています。
次に ピリオド '.' がありますが、これは任意の1文字を表し、アスタリスク ’*’ は直前の文字が0個以上繰り返し、はてな '?' は直前の文字が0か1回あるという意味になります。
”[ -. ]” は、ハイフンかピリオドか半角スペースのいずれかという意味になります。
つまり、「先頭から数えて、任意の文字が1個以上連続で続いていて、ハイフンかピリオドか半角スペースのいずれかが1つ登場するまで」という意味になります。
uxBeforeString.Text = "^.*?[-. ]";
もし '?' を抜いて "^.*[-. ]" と書くと、「任意の文字が1個以上連続で続いていてハイフンかピリオドか半角スペースが終わるまで」という意味になります。
ちょっとややこしいですが、”.*?" というパターンは良く使いますので、頭の片隅にでも入れておいていただければ、次に役立つと思います。
今回公開したソースコードを自分様にカスタマイズしたり、あるいは皆さんのソースコードに流用する時のヒントになるような観点で、ポイントとなる部分を解説していきましょう。
今回公開したソースコードを自分様にカスタマイズしたり、あるいは皆さんのソースコードに流用する時のヒントになるような観点で、ポイントとなる部分を解説していきましょう。
TagLibの使い方
MP3のタグを編集する用途では、「TagLib」が広く使われていて安心感がありますが、日本語の場合は文字化けすることがあります。
この文字化け対策についても対応策を後ほど紹介します。
インストール方法
TagLib は Visual StudioのNuGetから簡単に自分のプロジェクトに取り込むことが出来ます。
具体的には、Visual Studio のパッケージ管理機能 NuGetとは? の記事に記載していますが、以下の様に画面をクリックしていくことで、インストール(自分のプロジェクトへの取り込み)が行えます。


タグの読み出し
まず、静的クラスである Taglib.FileクラスのCreate メソッドを呼んで、TagLib.Fileクラスのインスタンスを生成します。
TagLib.File mp3 = TagLib.File.Create(filename);
以降は、インスタンスのプロパティを通して、MP3タグにアクセスできます。

主なタグは次の様に記述することで取得できます。
TagLib.File mp3 = TagLib.File.Create(@"d:\Boyfriend (Moonlight Version).mp3");
Debug.WriteLine(mp3.Tag.Title); //タイトル
Debug.WriteLine(string.Join(";", mp3.Tag.Artists)); //アーティスト
Debug.WriteLine(string.Join(";", mp3.Tag.AlbumArtists)); //アルバムアーティスト
Debug.WriteLine(mp3.Tag.Album); // アルバム
Debug.WriteLine(string.Join(";", mp3.Tag.Composers)); //作曲家
Debug.WriteLine(mp3.Tag.Year); // 年
Debug.WriteLine(mp3.Tag.Track); //トラック番号
Debug.WriteLine(mp3.Tag.Disc); //ディスク番号
Debug.WriteLine(string.Join(";", mp3.Tag.Genres)); //ジャンル
Debug.WriteLine(string.Format("{0:D2}:{1:D2}:{2:D2}",
mp3.Properties.Duration.Hours, mp3.Properties.Duration.Minutes, mp3.Properties.Duration.Seconds)); // 再生時間
Debug.WriteLine(mp3.Properties.AudioBitrate); //ビットレート
Debug.WriteLine(mp3.Tag.TagTypes); //タグのタイプ
Debug.WriteLine(mp3.Tag.Comment); //コメント
以下は主なタグ名とTagLibのプロパティの対応表です。
実際にはこれ以外にもありますが、ほとんど馴染みがないものばかりです。
| MP3タグの名前 | 型 | 対応するプロパティ名 |
|---|---|---|
| タイトル | string | Title |
| アーティスト | string[] | Artists |
| アルバムアーティスト | string[] | AlbumArtists |
| アルバム名 | string | Tag.Album |
| 作曲家 | string[] | Composers |
| 年 | uint | Year |
| トラック番号 | uint | Track |
| ディスク番号 | uint | Disc |
| ジャンル | string[] | Genres |
| 長さ(再生時間) | TimeSman | Properties.Duration |
| ビットレート(Kbps) | int | Properties.AudioBitrate |
| タグ | string | TagTypes |
| コメント | string | Comment |
ちなみに、TagLibの長さ(再生時間)やビットレートについては、正しく取得できない場合がありました。
たとえば、5分の曲が30分で表示されたり、128kbps が 32kbps で表示されるようなパターンです。
エクスプローラーからファイルを右クリックしてプロパティを表示してみると、そちらは正しく表示されていました。
有名どころのMP3編集ソフトとTagLibの取得結果は同じだったので、そもそもMP3タグの値がおかしいのだろうと思います。
MP3に埋め込まれた画像の取得
MP3には画像を埋め込むことが出来ますが、その画像は Tag.Pictureプロパティにバイト配列(byte[])で格納されています。
PictureBoxコントロールを用いて画面に表示するには Image 型に変換する必要がありますが、それはFromStream メソッドを使います。
var bin = (byte[])(mp3.Tag.Pictures[0].Data.Data);
Image img = Image.FromStream(new MemoryStream(bin))
尚、画像は複数格納できるように配列型になっているため、Length プロパティを使って画像が埋め込まれているかを判定できます。
以上のことを踏まえ、画像が埋め込まれている時のみ pictureBox1 に画像を表示するサンプルは次の様になります。
if (mp3.Tag.Pictures.Length >= 1)
{
var bin = (byte[])(mp3.Tag.Pictures[0].Data.Data);
pictureBox1.Image = Image.FromStream(new MemoryStream(bin)).GetThumbnailImage(200, 200, null, IntPtr.Zero);
}
埋め込まれている画像は必ずしも小さな画像とは限らないため、サンプルでは縦横200ドットのサムネイルを作成し、それをpictureBox1.Image に渡しています。
サムネイルを作成せず、pictureBox1のSizeModeプロパティを使って適切なサイズ時自動縮小して表示する方法もあります。
if (mp3.Tag.Pictures.Length >= 1)
{
var bin = (byte[])(mp3.Tag.Pictures[0].Data.Data);
uxImage.SizeMode = PictureBoxSizeMode.Zoom;
uxImage.Image = Image.FromStream(new MemoryStream(bin));
}
タグの書き込み
タグの書き込みは、読み込みと同様 Createメソッドで Fileクラスを作成し、Tagクラスのプロパティに値を設定していき、最後に Saveメソッドを呼ぶだけです。
TagLib.File mp3 = TagLib.File.Create(@"d:\Boyfriend (Moonlight Version).mp3");
mp3.Tag.Title = "Boyfriend";
mp3.Tag.Artists = "Flower";
mp3.Tag.AlbumArtists = new string[]{"Flower"};
mp3.Tag.Album = "THIS IS Flower THIS IS BEST";
mp3.Tag.Composers = "";
mp3.Tag.Year = 2016;
mp3.Tag.Track = 6;
mp3.Tag.Disc = 1;
mp3.Tag.Genres = new string[]{"JPop"};
mp3.Tag.Comment = "Flowerベストアルバム";
//mp3ファイルにタグを書き込む
mp3.Save();
MP3ファイルへの画像の埋め込み
Pictureクラスのコンストラクタにimageデータを渡すことで、TagLibで取り扱えるデータに変換できます。
それを TagクラスのPictures プロパティに配列形式で渡した後、Saveメソッドを呼ぶだけで埋め込むことが出来ます。
TagLib.IPicture picture = new TagLib.Picture(image);
mp3.Tag.Pictures = new TagLib.IPicture[] { picture };
mp3.Save();
文字化け対応
さて、TagLibで一番問題になるのは漢字の文字化けです。
文字化けする場合としない場合があるのですが、これはMP3タグのバージョンによって文字コードが異なることと、その規則を無視してシフトJISを書き込んでしまうMP3エンコーダーが横行していることが原因のようです。
理由はともかく、私が試したところによるとTagLibで正しく日本語が表示される場合と文字化けする場合があったので、それを解決する手段を思考錯誤しました。
最終的にたどり着いたのが次のソースコードになります。
有名どころのMP3タグ編集ソフトで表示される結果と、この方法で変換した結果を約1000ファイルに渡って比較したところ、両者は一致したので、おそらくこれが正解なんだろうと思ってます。
考え方としては、元の文字列をシフトJISと仮定して一旦バイト列に変換後、再びシフト列に変換してあげると、もしシフトJISであったなら元の文字列に戻るはずです。
この考え方を使ってシフトJISか否かを判断し、シフトJISでなければシフトJISに変換してあげるという事をやっています。
private string UTF2Jis(string str)
{
//指定された文字列が null なら、空の文字列を返す
if (str == null)
{
return "";
}
//Shift-JISのエンコーディングを取得
var enc = System.Text.Encoding.GetEncoding("shift-jis");
//取得したエンコーディングを使って引数ををバイト列に変換後、再びShift-JISの文字列に変換
string cnv_str = enc.GetString(enc.GetBytes(str));
//上記の変換結果と引数を比較し、異なればShift-JISではないと判断
if (str != cnv_str)
{
//引数をShift-JISに変換
var l_bytes = System.Text.Encoding.Unicode.GetBytes(str);
//Unicodeにバイト列に変換すると1バイト毎に0x00 が付加されるため、0x00を取り除いてShift-JISに変換
return System.Text.Encoding.GetEncoding("shift-jis").GetString(l_bytes.Where(i => i > 0).ToArray<byte>());
}
return str;
}
シフトJISに変換する際、一旦バイト列に変換する必要があるのですが、その時1バイトごとに0x00が付加されていますので、これを取り除いています。
私はMP3タグについて詳しく無いので、何故こうなるか理由は分かりませんので、もし分かる方がいれば是非教えて頂きたいと思います。
参考:Shell32.dllを使ったタグの読み出し
参考情報として、Shell32.dll を使ったタグの読み出しについても軽く紹介しておきます。
Google検索して見つけた内容なので、原文はこちらの記事をご覧下さい。
方法は、Shell32.dll をプロジェクト参照してから、数行プログラムを書くことで、プロパティが取得できます。
参照設定の方法
Visual Studio のソリューションエクスプローラーから、「参照」→「参照の追加」→「参照マネージャ」→「COM」と選択していきます。
次に、「Microsoft Shell Controls And Automation 」を探してチェックを入れ、「OK」ボタンをクリックすると完了です。

「参照」の一覧に Sell32 が表示されたと思いますので、これをクリックし、「相互運用型の埋め込み」をFalseに設定します。
初期値はTrueなのですが、Trueのままだとエラーになります。

タグ読み出しのソースコード
参照した Shell32 を使ってタグを読み出す方法は次の通りです。
GetDetailsOfメソッドの第2引数にタグ番号を代入すると、そのタグ番号に登録されている値が取得できます。
//あらかじめ Shell32 のネームスペースを using しておく
using Shell32;
//事前準備
ShellClass shell = new ShellClass();
Folder folder = shell.NameSpace(フォルダ名);
FolderItem item = folder.ParseName(ファイル名);
//タグの読み出し
folder.GetDetailsOf(item, タグ番号);
実験的に、0~300までタグ番号を変えて値を取得するプログラムを作って、どんな値が返ってくるのか調べてみました。
0010(タグ番号10)は私の端末名とユーザー名が表示されていたので、あえてボカしました。

private void button1_Click(object sender, EventArgs e)
{
string dir = Path.GetDirectoryName(textBox2.Text);
string file = Path.GetFileName(textBox2.Text);
ShellClass shell = new ShellClass();
Folder folder = shell.NameSpace(dir);
FolderItem item = folder.ParseName(file);
textBox1.Text = "";
for (int i = 0; i < 300; i++)
{
var val = folder.GetDetailsOf(item, i);
if (val != "")
{
textBox1.AppendText(string.Format("{0:0##}\t{1}", i, val) + Environment.NewLine);
}
}
}
原文にもタグ番号と内容の一覧が記載されていますが、WindowsXP時代のもののようで、Windows10の場合は次の様になりました。
| タグ番号 | 内容 |
|---|---|
| 0 | ファイル名 |
| 1 | ファイル容量 |
| 2 | MP3 形式サウンド |
| 3 | 更新日時 |
| 4 | 作成日時 |
| 5 | アクセス日時 |
| 9 | ファイルの種類(オーディオ、ビデオ等) |
| 10 | 所有者 |
| 11 | 音楽 |
| 13 | アーティスト |
| 14 | アルバム名 |
| 15 | 年 |
| 16 | ジャンル |
| 19 | 評価なし |
| 20 | アーティスト |
| 21 | タイトル |
| 24 | コメント |
| 26 | トラック番号 |
| 27 | 長さ(再生時間) |
| 28 | ビットレート |
| 194 | フルパス |
| 196 | ファイル形式(MP3 形式サウンド、MP4ファイルなど) |
| 215 | サブタイトル |
| 237 | アルバムアーティスト |
| 243 | 作曲家 |
| 248 | 雰囲気 |
| 249 | ディスク番号 |
まとめ
今回はC#、WindowsForm、TagLibを使って、MP3タグを編集するプログラムを作成したので、EXEファイルのダウンロード方法、プログラムの使い方、ソースコード解説、TagLibの使い方について解説しました。
今回作成したプログラムは、MP3ファイルが格納されているフォルダを本ツールにドラッグ&ドロップし、編集対象項目を選び、フォーマットや置換の機能を用いてタグを設定したり、ファイル名を変更するという単純なものですが、再利用を考慮して極力関数化していますので、プログラム作り方について参考になるかと思います。
今回採用したTagLibは文字化けという問題はあるものの、本記事で紹介した対策を使えば回避可能なため、十分重宝すると思います。
もしMP3タグを編集する機会が有れば、是非この記事を参考にして頂ければと思います。
コメント