2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【個人開発】MusicBeeプラグインの作り方

2
Last updated at Posted at 2025-12-19

プラグインの作成

 この記事を書くにあたって、私はMusicBeeのプラグインを作成しました。内容は、A-Bリピート機能の実装です。

もくじ

1 はじめに
2 この記事でわかること
3 開発環境
4 開発環境の準備

5 プラグイン開発の基本設定
6 プラグインの概要
7 実装開始

8 おわりに
9 あとがき
10 使用したAPIまとめ
11 注釈

1 はじめに

 この記事は、MusicBeeプラグインを自作しようと思っている人に向けて、開発過程を記録したものです。過程をすべて記入したらやたらめったら長くなったので、必要な箇所だけ見ることをおすすめします。

 需要があるかどうかわかりませんが、たった一人にでも需要があれば私は満足です。

 参考にしたチュートリアル: Tutorial: Creating A Simple Plugin

 注意: 最新のファイルはC++ですが、情報が少なかったりバグが多かったりでめんどくさかったのでこの記事ではC#を使用しています。チュートリアルもC#で構成されています。

2 この記事でわかること

  • MusicBeeAPIの使い方および環境構築方法

3 開発環境

  • バージョン等
    • VisualStudio 2022
    • MusicBeePortable 3.6
  • 言語
    • C#

4 開発環境の準備

  1. Visual Studioと最新バージョンのMusicBee、APIをダウンロードします。
     APIのリンク:https://getmusicbee.com/forum/index.php?topic=1972.0
     最新のC# sourceをダウンロードしてください。中にサンプルプログラムも含んであります。
     作業用のMusicBeeとして新しくポータブル版1をダウンロードしておきましょう。本環境でやるとバグなどでぶっ壊れる可能性があり危険です。
    https://getmusicbee.com/downloads/
    インストールするバージョンの画像スクリーンショット 2025-11-29 002519.png

5 プラグイン開発の基本設定

 はじめはチュートリアルに従って進めていきます。

5.1 TestCSharpDLL.csの書き換え

 Initialise内(以降初期化時)のnamedescriptionauthorを書き換えましょう。それぞれ、プラグイン名、説明、作者に対応します。

about.Name = "advanced A-B loop";
about.Description = "Provides advanced A-B loop functionality.";
about.Author = "bobshroom";

5.2 MusicBeeの追加

 ファイル > 追加 > 既存のプロジェクトからダウンロードしたポータブル版のMusicBee.exeを追加させましょう。
 追加したMusicBee.exeを右クリックし、スタートアッププロジェクトに設定を選択してください。

5.3 ビルド後の挙動設定

 ソリューションファイル(.sln)のプロパティを開き、ビルドイベント > ビルド後イベントのコマンドラインに以下のコマンドを貼り付けてください。:

copy /Y "$(TargetDir)\*.*" "musicBeeのpluginファイルのパス"

 チュートリアルのデフォルトでは C:\Program Files (x86)\MusicBee\Plugins\ になっているので、必要に応じて書き換えてください。

5.4 ビルドファイルの名称設定

 MusicBeeはmb_~.dllという名前のものをプラグインとして認識するようです。ビルドで出力されるdllファイルの名前を設定しましょう。

 ソリューションエクスプローラー(左にあるファイル一覧)のCSharpDllを右クリックし、プロパティを選択してください。
 以下の画面が出るので、アセンブリ名に好きな名前を入れてください。
(md_が必須です)
image.png

5.5 プロジェクトのリビルド

 ビルド > ソリューションのリビルドを行ってください。

5.6 デバッグして実行

 これでMusicBeeのプラグイン欄(左上の三本線 > 設定 > プラグイン)に作成したプラグインが入っているはずです。
プラグイン名と説明が設定したものと一致しているか確認してください。
 内容を書き換えるたびにリビルドが必要です。何かを忘れているのか、そういう仕様なのかはわかりません。

6 プラグインの概要

 ここからはチュートリアルから離れ、自作したプラグインの制作過程と説明を行っていきます。作成しながら記事を書いているので、内容に矛盾が生じる恐れがあります。

作業内容 (☆は余裕があれば)

  • タグAを開始位置、タグBを終了位置とし、A-Bリピート(地点Bを過ぎると自動的に地点Aに戻る)の実現
  • コンフィグからタグAとタグBの名称設定
  • ホットキーでおおよその再生位置と終了位置を設定可能にして手間を削減
  • ☆ 音量を滑らかに減衰させて違和感のないリピートの実現
  • ☆ 再生バーの上にリピート開始位置とリピート終了位置を描画し、ドラックで動かせるようにする
  • ☆ 左右ボタンで0.05秒ずつの微調整を可能に

実装したいこと

  • タグの設定、認識、読み取り、書き込み
  • ホットキー入力の読み取り
  • 再生位置の変更
  • タグが存在しない場合の初期値の設定
  • ☆ 音量設定
  • ☆ 一つの曲の二つの位置を同時再生

テスト楽曲

 YouTube Audio Libraryの「If I had a Chicken」と「The Entertainer」を使用。

7 実装開始

1 タグの設定

 タグの取得であったり保存であったりと、タグ周りを作っていきます。

1.1 プラグイン設定画面の作成

 プラグイン設定の画面が開かれている間は、Configure関数が呼ばれます。中身を書き換えていきましょう。

 初期設定では about.ConfigurationPanelHeight = 0; となっています。これを 100 に設定してみましょう。

about.ConfigurationPanelHeight = 100;

 設定パネルの高さが大きくなり、隠れていたpromptとテキストボックスが見えました。

高さを変えた結果の画像スクリーンショット 2025-11-29 003535.png

 設定の中身を作っていきましょう。

 画像内の"prompt:"という説明文とテキストボックスは、それぞれ LabelTextBox に対応しています。これを configPanel.Controls.AddRange(new Control[] { prompt, textBox }); でconfigPanelに要素を追加することで、描画しているようです。

 prompt.LocationtextBox.Bounds で座標の設定を行い、次のように書き換えました。

if (panelHandle != IntPtr.Zero)
{
    // これに追加した要素が表示されます
    Panel configPanel = (Panel)Panel.FromHandle(panelHandle);
    
    Label loopStartLabel = new Label();
    loopStartLabel.AutoSize = true; // 大きさを自動で設定
    loopStartLabel.Location = new Point(0, 0); // x, y座標を設定
    loopStartLabel.Text = "Loop start tag name:"; // ラベルのテキストを設定
    
    TextBox loopStartTextBox = new TextBox();
    loopStartTextBox.Bounds = new Rectangle(120, 0, 100, loopStartTextBox.Height); // x, y座標とテキストボックスの大きさを設定
    
    configPanel.Controls.AddRange(new Control[] { loopStartLabel, loopStartTextBox }); // configPanelに要素を追加

    // 同じ要領でもう一セット作成する
    Label loopEndLabel = new Label();
    loopEndLabel.AutoSize = true;
    loopEndLabel.Location = new Point(0, 30);
    loopEndLabel.Text = "Loop End tag name:";
    
    TextBox loopEndTextBox = new TextBox();
    loopEndTextBox.Bounds = new Rectangle(120, 30, 100, loopEndTextBox.Height);
    
    configPanel.Controls.AddRange(new Control[] { loopEndLabel, loopEndTextBox });
}

(実装結果)

スクリーンショット 2025-11-29 003535.png

1.2 設定内容の保存

 ファイルのパスは指定されているのでそれに従いましょう。今回はJSON形式で保存します。
 (JSONをそのまま利用したかったが拡張機能が必要で面倒だったため)

 保存と読み取りに使用するクラスをChatGPTに書いてもらいました。

JsonKeyValueStoreのクラス
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

public static class JsonKeyValueStore
{
    // 保存
    public static void SaveKeyValue(string filePath, Dictionary<string, string> data)
    {
        var sb = new StringBuilder();
        sb.AppendLine("{");

        int i = 0;
        foreach (var kv in data)
        {
            sb.Append("  \"")
              .Append(Escape(kv.Key))
              .Append("\": \"")
              .Append(Escape(kv.Value))
              .Append("\"");

            if (i < data.Count - 1)
                sb.Append(",");

            sb.AppendLine();
            i++;
        }

        sb.AppendLine("}");
        File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8);
    }

    // 読み込み
    public static Dictionary<string, string> LoadKeyValue(string filePath)
    {
        var result = new Dictionary<string, string>();

        if (!File.Exists(filePath))
            return result;

        foreach (var line in File.ReadAllLines(filePath))
        {
            string trimmed = line.Trim();

            if (trimmed.StartsWith("{") || trimmed.StartsWith("}"))
                continue;

            // "key": "value",
            int colon = trimmed.IndexOf(':');
            if (colon < 0) continue;

            string key = trimmed.Substring(0, colon).Trim();
            string value = trimmed.Substring(colon + 1).Trim();

            key = Unquote(key);
            value = Unquote(value.TrimEnd(','));

            result[key] = value;
        }

        return result;
    }

    // --- 補助関数 ---

    private static string Escape(string s)
    {
        return s.Replace("\\", "\\\\").Replace("\"", "\\\"");
    }

    private static string Unquote(string s)
    {
        if (s.StartsWith("\"") && s.EndsWith("\""))
            s = s.Substring(1, s.Length - 2);

        return s.Replace("\\\"", "\"").Replace("\\\\", "\\");
    }
}

 問題は、テキストボックスから受け取った値をいつ保存するかです。今回は、テキストボックスが更新されるたび仮変数に値を保存しておき、保存して終了が呼び出された際に仮変数の値を保存する方式で行きます。

// loopStartTextBoxが変更されたときに呼び出される
loopStartTextBox.TextChanged += (s, e) =>
{
    // loopStartTextBoxに記入されているテキストを取得する
    startTagTemp = loopStartTextBox.Text;
};

 TextChanged でテキストボックスが更新されたのを読み取り、startTagTemp に保存します。

 フィールド変数に文字列型で startTagendTag を作り、そこに保存しつつjsonにも保存するため、SaveSettings を書き換えます。SaveSettings はプラグインの設定画面で適用、また保存が押された際に自動的に呼ばれます。

public void SaveSettings()
{
    // 保存先はMusicBee側で指定してあるのでAPIで取得する
    string dataPath = mbApiInterface.Setting_GetPersistentStoragePath();
    string jsonFile = Path.Combine(dataPath, "advancedABLoopSettings.json");

    startTag = startTagTemp;
    endTag = endTagTemp;

    var settings = new Dictionary<string, string>
    {
        { "StartTag", startTag },
        { "EndTag", endTag }
    };

    JsonKeyValueStore.SaveKeyValue(jsonFile, settings);
}

1.3 保存内容の読み取り

 初期化時に保存された設定内容を読み込み、反映させます。

string dataPath = mbApiInterface.Setting_GetPersistentStoragePath();
string jsonFile = Path.Combine(dataPath, "advancedABLoopSettings.json");

var loaded = JsonKeyValueStore.LoadKeyValue(jsonFile);

startTag = loaded.ContainsKey("StartTag") ? loaded["StartTag"] : "Custom14";
endTag = loaded.ContainsKey("EndTag") ? loaded["EndTag"] : "Custom15";

1.4 レイアウト崩壊問題

 作り終わってから気がづいたのですが、画面サイズの環境次第ではレイアウトが崩壊するという問題が発生しました。

フルHDの場合

フルHDの設定画面(理想的)

フルHDより小さい場合

フルHDより小さい画面サイズの設定画面(レイアウトが崩壊)

 そこで、TableLayoutPanel を使用し、行と列を指定して自動的にレイアウトを組んでいきましょう。

var layout = new TableLayoutPanel();
layout.Dock = DockStyle.Fill;
layout.ColumnCount = 2;  // 列の数を指定
layout.RowCount = 1;     // 行の数を指定
layout.AutoSize = true;
layout.AutoSizeMode = AutoSizeMode.GrowAndShrink;

// configPanelに直接要素を追加しましたが、その行は削除してください。
// configPanel.Controls.AddRange(new Control[] { loopStartLabel, loopStartTextBox });

layout.Controls.Add(loopStartLabel, 0, 0);   // 0行0列に要素の追加
layout.Controls.Add(loopStartTextBox, 1, 0); // 1行0列に要素の追加

configPanel.Controls.Add(layout);            // layoutをconfigPanelに追加

 ほかの要素も同様に行列を指定し追加し、layout そのものを configPanel に追加しましょう。

 色々追加した結果(フルHDより小さい画面)

フルHDより小さい画面でも正常に表示される

2 タグの取得

 理想はタグ名から取得する予定でしたが、APIは内部データの元から設定されていたタグ名で取得することしかできないようです。(方法があるかはわからないが見つけられなかった)

 ということで、ユーザーにはタグの内部データ名を指定してもらいそこから取得することにします。

 本来は mbApiInterface.NowPlaying_GetFileTag(MetaDataType.Custom1); で取得するのですが、今回は文字列型から指定して取得したいので、TryParse を使用します。

string test = "Custom1";
if (Enum.TryParse<MetaDataType>(test, out MetaDataType result))
{
    string value = mbApiInterface.NowPlaying_GetFileTag(result);
    Debug.WriteLine("カスタム1: " + value);
}
else
{
    Debug.WriteLine("タグの取得に失敗しました");
}

実行結果 (画像挿入)
Custom1を
カスタムタグ設定画面

ユーザーに好きに決めてもらい(ただし設定ではCustom1と指定してください)
カスタムタグの設定を変更して

値を入力すると...
曲のタグを変更

正しく出力された!
デバッグ出力の様子

3 リピート機能の実装

 理想は、現在の再生位置がリピート終了地点以降ならばリピート開始地点に戻るというものです。段階に分けて実装していきます。

3.1 リピートすべきかどうかの判定

 以下のような関数を作成しました。

private bool IsOutLoopRegion(int currentPositionMs)
{
    if (loopStartMs < loopEndMs)
    {
        if (currentPositionMs > loopEndMs)
        {
            return true;
        }
    }
    return false;
}

 引数に現在の再生位置を入れ、リピートすべきなら true、そうでなければ false を返します。

 実際に使用する際の例: IsOutLoopRegion(mbApiInterface.Player_GetPosition());

3.2 リピート機能の実装

 ここの内容はほとんどMusicBeeAPI以外の関数などを使用します。C#に精通した人間なら飛ばしてもらっても構いません。

 リピート位置に来るまで処理を待機させる必要があります。今回は、「現在位置からリピート位置までの時間を計算し待機する。ユーザーが何かしら再生を操作したら再計算する」という方式で行きます。

 「一定時間待機」は、System.Timers.Timerで実現できます。

タイマーの初期設定
System.Timers.Timer checkTimer = new System.Timers.Timer();

{
    checkTimer.Interval = 1000;     // タイマーを1000ms(1秒)ごとに実行(後で上書きします)
    checkTimer.AutoReset = false;   // タイマーをリセットしない(繰り返さない)
    checkTimer.Elapsed += (sender, e) => checkLooping();    // タイマーにメソッドを登録
}
タイマーを設定するメソッド

 トラックが変更されたとき、プレイヤーが再生を再開させたとき、タグが変更されたときなどに呼び出します。タイマーがリセットされて、再設定されます。

private void SetCheckTimer()
{
    // 現在位置からリピート位置までの時間×0.3ms後に処理を実行
    // ユーザーが再生速度を3倍にしていても大丈夫なようにしています。
    // また、Intervalには0以上の値を設定する必要があるのでMax関数でエラーを回避します。
    checkTimer.Interval = Math.Max((int)(DelayMilliseconds() * 0.3), 1);
    // タイマーをスタートさせます。
    checkTimer.Start();
}
リピート区間外かどうかの判定
private bool IsOutLoopRegion(int currentPositionMs)
{
    // リピート開始位置よりリピート終了位置が後ろにあるか確認します。
    // ユーザーが間違った設定をしていても動作に支障をきたさないようにします。
    if (loopStartMs < loopEndMs)
    {
        // 再生位置がリピート終了位置より後ろならばtrue、それ以外ならfalseを返します。
        if (currentPositionMs > loopEndMs)
        {
            return true;
        }
    }
    return false;
}
リピート処理
private void checkLooping()
{
    if (isLoopEnabled)  // リピートが有効でなければタイマーをストップさせます
    {
        // Player_GetPositionで現在の再生位置を取得できます。
        // ただし、取得した位置と実際の再生位置は多少ずれます。
        // 設定でユーザーにoffsetMsを設定してもらい、誤差を軽減します。
        int currentPositionMs = mbApiInterface.Player_GetPosition();
        if (IsOutLoopRegion(currentPositionMs - offsetMs))
        {
            // Player_SetPositionで再生位置の変更ができます。
            mbApiInterface.Player_SetPosition(loopStartMs);
        }
        // リピートしてもしなくても、タイマーの再設定が必要です。
        SetCheckTimer();
        return;
    }
    StopCheckTimer();
}

4 ホットキーの設定

 手動で時間指定すると面倒です。登録したホットキーが押されるとその時間をタグに自動的に保存するようにしましょう。

4.1 ホットキーの追加

 初期化時に以下を記述してください。

mbApiInterface.MB_AddMenuItem("SaveStartTime", "Tools:Save start time", saveStartTime);

 第一引数が何を意味しているのか私には分かりませんでした。判明したら編集します。

 第二引数にホットキー設定画面で表示されるテキストを指定できます。ここの名前でホットキーの種類を保存しているようです。名前が変わるとホットキーも変わるので注意してください。

 第三引数にホットキーが押された際の処理を入れます。

private void saveStartTime(object sender, EventArgs args)
{ /* 略 */ }

(実装結果)
image.png

4.2 タグの書き込み

 ホットキーが押された際の処理を記述していきます。

private void saveStartTime(object sender, EventArgs args)
{
    // 現在の再生位置を取得する
    int currentPositionMs = mbApiInterface.Player_GetPosition();
    // ミリ秒で指定された時間を文字列に変更する
    string timeStr = MillisecondsToTimeString(currentPositionMs);
    mbApiInterface.Library_SetFileTag(mbApiInterface.NowPlaying_GetFileUrl(), (MetaDataType)Enum.Parse(typeof(MetaDataType), startTag), timeStr);
    mbApiInterface.Library_CommitTagsToFile(mbApiInterface.NowPlaying_GetFileUrl());
}

 MillisecondsToTimeString は自作した関数です。整数型のミリ秒を文字列型で hh:mm:ss:fff の形にフォーマットしてくれます。(61100 -> 1:1.100)

 Library_SetFileTag でタグの保存ができます。第一変数に保存先を指定します。NowPlaying_GetFileUrl で再生中の曲のディレクトリを取得できます。

 第二引数ではタグの種類を選択します。タグを取得した際と同じやり方で行きます。

 第三引数に保存したい値を入れます。

 Library_CommitTagsToFile で変更をコミットさせます。

 コミットさせても実際にタグがファイルに保存されて参照可能になるのはトラックを変更した際のようです。

5 リピートモードの設定

 新しいリピートモードを作成する予定でしたが、素人には難しそうなのでとりあえずホットキーを押したらモードが変更されるように変えたいと思います。

 これまでの応用なので難しい操作は必要ありません。

 新しい関数を用意し

private void toggleLooping(object sender, EventArgs args)
{
    isLoopEnabled = !isLoopEnabled;
    if (isLoopEnabled)
    {
        // リピートが有効化されたらタイマーを再始動させる
        SetCheckTimer();
    }
    else
    {
        StopCheckTimer();
    }
}

 メニューにアイテムを追加し

mbApiInterface.MB_AddMenuItem("ToggleLooping", "Tools:Toggle A-B Looping", toggleLooping);

 完成しました。ホットキーを押すたびにA-Bリピートのオンオフを切り替えることができました!

おわりに

 最初は完成するのか不安でしたが、一部機能を妥協しつつ無事完成できてよかったです。
 冗長な記事をここまで読んでくれた親切なあなたに特別にソースファイルのリンクを共有します。再配布禁止です。
 もし、より良いコードが書けたので再配布したい場合は、私に連絡をください。内容によっては配布を認めます。
https://github.com/bobshroom/advance-A-B-loop-plugin

あとがき

 MusicBeeの公式プラグイン共有サイトにてアップロードしました。良ければ見ていってください。
https://getmusicbee.com/addons/plugins/551/advanced-a-b-loop/

使用したAPIまとめ

// ホットキーに機能を追加する
mbApiInterface.MB_AddMenuItem(string 第一引数, string ホットキ, EventHandler イベントメソッド);

// 設定ファイルの保存場所を取得する
mbApiInterface.Setting_GetPersistentStoragePath();

// プレイヤーの状態を取得する 記事での説明はないがコード内に使用
mbApiInterface.Player_GetPlayState()

// 再生中の曲のタグ情報を取得
mbApiInterface.NowPlaying_GetFileTag(MetaDataType タグの種類);

// 再生中の曲の現在位置を取得
mbApiInterface.Player_GetPosition();

// 再生中の曲の現在位置を変更
mbApiInterface.Player_SetPosition(int 時間指定(ms));

// 曲の再生回数を増やしつつ、last.fmにscrobbleする コード内に使用
mbApiInterface.Player_UpdatePlayStatistics(string 曲のパス, PlayStatisticType カウントのタイプ, bool last.fmscrobbleしないか(falsescrobbleされるので注意));

// 曲のタグ情報を変更
mbApiInterface.Library_SetFileTag(string 曲のパス, MetaDataType, string タグの内容);

// 変更したタグ情報を確定
mbApiInterface.Library_CommitTagsToFile(string 曲のパス);

注釈

  1. PCにインストールせず、USBメモリなどに入れて持ち運び、どのPCでも設定やデータを保持したまま使えるようにしたソフトウェア。インストーラー版と違い機能が制限されるが、開発環境としては十分かつ、バグが起きてもメインの環境にダメージを与えない。

2
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?