「TAURI」で「ピアノ音楽」アプリを作ろう

2024年1月16日(火)
大西 武 (オオニシ タケシ)
第8回の今回は、「TAURI」でピアノのUIを演奏するとピアノのサウンドが鳴るデスクトップアプリを開発していきます。

ドの音を再生するだけのサンプル

まずはRustで1音出すだけの実装をしてみます。ピアノの鍵盤は「HTML5+JavaScript+CSS」を使ってWebページをデザインします(後述)。本当は、見た目だけ先に作り、後から音を出すようにプログラミングしても良かったのですが、今回は1音鳴らせるようになればWebページの鍵盤に合わせてそれぞれ音が鳴らせるようになれば良いので、このような順番の解説にしました。

今回のTAURIでは、図4のような「State(ステート)」という機能を使います。この機能は「TAURIコマンド」「TAURIセットアップハンドラ」「Windowイベントハンドラ」からState変数を共有できます。つまりグローバル変数のように、3つの機能からState変数にアクセスできます。なぜこのようなやり方をするかと言うと、Rustはグローバル変数を使わないように推奨されているからだと思われます。

図4:「TAURIステート」について

Sound構造体のコーディング

次のように「src」→「sound.rs」ファイルを作成し、コーディングしてサウンドを扱う部分を「Sound」構造体にまとめます。と言っても、今回は「コンストラクタ」の「new」メソッドと、サウンドを再生する「play」メソッドしかありませんが。

図4で見たようにTAURIでステート機能を使うのですが、State変数は「イミュータブル(変更不可)」です。そこで「Mutex型」という機能でラップすれば、「ミュータブル(変更可能)」にデータを死守してくれます。

なお、サウンドの読み込みと再生についての詳細は、kiraクレートを使った第5回の内容が参考になると思います。ただし、今回はMutex型で2つのプロパティをラップしています。

・「src-tauri」→「src」→「sound.rs」サンプルコード
//Mutexのクレート
use std::sync::Mutex;
//サウンドクレート
use kira::{
  manager::{
    AudioManager, AudioManagerSettings,
    backend::cpal::CpalBackend,
  },
  sound::static_sound::{StaticSoundData, StaticSoundSettings},
};
//tauriクレート
use tauri::App;
//サウンド構造体
pub struct Sound {
  sound_manager: Mutex<AudioManager>,
  sound_data: Mutex<Vec<StaticSoundData>>,
}
//サウンド構造体のメソッド
impl Sound {
  //コンストラクタ
  pub fn new(
    app:&mut App,
    path:Vec<String>
  ) -> Self {
    let snd_manager = AudioManager::<CpalBackend>::new(
      AudioManagerSettings::default()
    ).expect("Error");
    let mut data = Vec::new();
    for p in path {
      let resource_path = app.path_resolver()
      .resolve_resource(p)
      .expect("failed to resolve resource");
      let d = StaticSoundData::from_file(
        resource_path,
        StaticSoundSettings::default()
      ).unwrap();
      data.push(d);
    }
    Sound {
      sound_manager: Mutex::new(snd_manager),
      sound_data: Mutex::new(data),
    }
  }
  //サウンド再生
  pub fn play(
    &self,
    index:usize
  ) {
    let mut snd_manager = self.sound_manager.lock().unwrap();
    let snd_data = self.sound_data.lock().unwrap();
    snd_manager.play(snd_data[index].clone()).expect("Error play");
  }
}

【サンプルコードの解説】
Mutex型を扱うクレートとサウンドクレート、TAURIクレートを読み込みます。
Mutex型でラップしたオーディオマネージャー変数とサウンドデータ配列で構成されたSound構造体を宣言します。
「impl Sound」でSound構造体のメソッドを定義します。
コンストラクタの「new」メソッドでサウンドファイルパス配列からサウンドを読み込みます。その際「tauri.conf.json」のリソースで指定した"sounds"フォルダ内のoggファイルのパスを「path_resolver」メソッドの「resolve_resource」メソッドでリソースパスを取得します。
「play」メソッドでサウンドを再生します。その際Mutex型でラップしたオーディオマネージャーとサウンドデータを「lock」メソッドで「ロック」してからそれぞれ使えるようになります。

メインファイルのコーディング

次のように「src」→「main.rs」ファイルをコーディングしたら、ターミナルで「$ cargo-tauri dev」コマンドを実行してください。最初に「ド」の音が1度だけ再生されます。図5のように、まだピアノの鍵盤もなくデフォルトのWebページなのでピアノは弾けません。

ここでステート機能を使います。ステート機能を使わないと、バックエンド内の大部分からアクセスできるState変数が保持できません。

図5:デフォルトのWebページ

・「src-tauri」→「src」→「main.rs」サンプルコード
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
//TAURIクレート
use tauri::{State,Manager};
//サウンドモジュール
mod sound;
//サウンド再生コマンド
#[tauri::command]
fn playpiano(
  note: usize,
  snd: State<'_, sound::Sound>,
) {
  snd.play(note);
}
//メイン関数
fn main() {
  //サウンドデータ
  let files = vec![
    String::from("sounds/C3.ogg"), //白鍵
    String::from("sounds/D3.ogg"),
    String::from("sounds/E3.ogg"),
    String::from("sounds/F3.ogg"),
    String::from("sounds/G3.ogg"),
    String::from("sounds/A3.ogg"),
    String::from("sounds/B3.ogg"),
    String::from("sounds/C4.ogg"),
    String::from("sounds/D4.ogg"),
    String::from("sounds/E4.ogg"),
    String::from("sounds/F4.ogg"),
    String::from("sounds/G4.ogg"),
    String::from("sounds/A4.ogg"),
    String::from("sounds/B4.ogg"),
    String::from("sounds/C5.ogg"),
    String::from("sounds/D5.ogg"),
    String::from("sounds/E5.ogg"),
    String::from("sounds/F5.ogg"),
    String::from("sounds/G5.ogg"),
    String::from("sounds/A5.ogg"),
    String::from("sounds/B5.ogg"),
    String::from("sounds/Df3.ogg"), //黒鍵
    String::from("sounds/Ef3.ogg"),
    String::from("sounds/Gf3.ogg"),
    String::from("sounds/Af3.ogg"),
    String::from("sounds/Bf3.ogg"),
    String::from("sounds/Df4.ogg"),
    String::from("sounds/Ef4.ogg"),
    String::from("sounds/Gf4.ogg"),
    String::from("sounds/Af4.ogg"),
    String::from("sounds/Bf4.ogg"),
    String::from("sounds/Df5.ogg"),
    String::from("sounds/Ef5.ogg"),
    String::from("sounds/Gf5.ogg"),
    String::from("sounds/Af5.ogg"),
    String::from("sounds/Bf5.ogg"),
  ];
  //デスクトップアプリのセットアップ
  tauri::Builder::default()
  .setup(|app| {
    let snd = sound::Sound::new(app,files);
    snd.play(0);
    app.manage(snd);
    Ok(())
  })
  .invoke_handler(tauri::generate_handler![playpiano])
  .run(tauri::generate_context!())
  .expect("error while running tauri application");
}

【サンプルコードの解説】
TAURIクレートのState構造体とManager構造体を読み込みます。
「sound.rs」ファイルを使えるようにするために「mod sound;」でsound.rsファイルをモジュールとして読み込みます。
「playpiano」関数はTAURIコマンド関数で、フロントエンドの同名のメッセージから呼ばれて実行できます。「note」引数はピアノ音のインデックス番号です。「snd」引数はsoundモジュールのSound構造体のState変数で、TAURIのバックエンドの大部分で使いまわせるState変数です。
「main」関数で「files」配列に白鍵7×3オクターブ、続いて黒鍵5×3オクターブの相対パスを指定します。相対パスはリソースのsoundsフォルダ内のoggサウンドファイルを指しています。
TAURIビルダー(「Builder」構造体)のデフォルト(「default」メソッド)のセットアップ(「setup」メソッド)でsound.rsファイルのSound構造体のインスタンスを生成し「snd」変数に代入します。サウンドの0インデックスを再生(playメソッド)します。TAURIマネージャー(「manage」メソッド)でsnd変数をState変数に登録します。「invoke_handler」メソッドで「playpiano」をTAURIコマンド関数にセットしてTAURIを走らせます(「run」メソッド)。

著者
大西 武 (オオニシ タケシ)
1975年香川県生まれ。大阪大学経済学部経営学科中退。プログラミング入門書など30冊以上を商業出版。Microsoftで大賞やNTTドコモでグランプリなど20回以上全国区のコンテストに入賞。オリジナルの間違い探し「3Dクイズ」が全国放送のTVで約10回出題。

連載バックナンバー

開発言語技術解説
第14回

「TAURI」で気象庁の「CSVデータ」を解析する

2024/5/1
第14回の今回は気象庁のWebサイトから指定した地域の1年間の気象データをダウンロードして「TAURI」で解析していきます。
開発言語技術解説
第13回

「TAURI」で「簡易RSSリーダー」を開発してみよう

2024/4/16
第13回の今回は「TAURI」で「RSSフィード」を読み込んでWebページに一覧表示し、リンクのページを開くための新規ウィンドウを作成するところまでを解説します。
開発言語技術解説
第12回

「TAURI」でExcelのデータを読み書きしてWebページに表示してみよう

2024/4/2
第12回の今回は「TAURI」で「Rust」の「umya-spreadsheet」クレートを使って「Excel」の「xlsx」ファイルを読み書きし、Webページに表示するところまでを解説します。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています