システム全体のアラート音を鳴らす

Tauri には警告音の API が無いため、Rust から OS の関数を直接呼んで、ユーザーが設定したシステムの警告音を鳴らす。追加クレート不要の書き方と、Web Audio で代わりの音を作る方法も示す。

ダイアログ・通知 対象: Tauri 2.x 更新日: 読了目安: 約8分 dlg-014
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 2. バックエンドから実装する (Rust)
  4. 動作確認
  5. よくあるエラーと対処法
  6. OS ごとの違いと注意点
  7. 関連レシピ

入力エラーや長い処理の終了を音で知らせたいとき、OS の警告音を使えば、ユーザーがサウンド設定で選んだ音・音量・ミュートがそのまま反映されます。Tauri の API にも dialog / notification プラグインにも警告音だけを鳴らす関数は無いので、Rust のコマンドから OS の関数を直接呼びます。追加のクレートは要りません。WebView の Web Audio API で作る音は OS の警告音ではありませんが、OS に依存しない代わりの音として使えます。

前提条件

プラグインもクレートも追加しません。自分で書いたコマンドを呼ぶだけなので、capability の権限も core:default のままで構いません。Linux では、Tauri の事前準備で入る GTK の開発ファイルをそのまま使います。古い記事にある winapi や cocoa を Cargo.toml に足す必要もありません。

音で知らせる方法は、目的によって次のように選びます。

方法鳴る音OS の警告音の設定
Rust から OS の警告音(このレシピの 2 章)ユーザーが選んだ警告音反映される
Web Audio API(1 章)アプリが作った音反映されない
通知と一緒に鳴らす通知の音OS のネイティブ通知を送る
音を出さずに気付かせる音なしタスクバーでウィンドウを点滅させる

1. フロントエンドから実装する (TypeScript)

JS からは OS の警告音を直接鳴らせません。OS の音が必要なら 2 章のコマンドを呼びます。ここでは、どの OS でも同じように鳴るアプリ独自の短い音を Web Audio API で作ります。

let ctx: AudioContext | null = null;

// 短いビープ音を鳴らす。frequency は音の高さ (Hz)、duration は長さ (秒)、volume は 0〜1
export async function playBeep(frequency = 880, duration = 0.15, volume = 0.2): Promise<void> {
  const ac = (ctx ??= new AudioContext()); // 毎回作らず、1 つを使い回す
  if (ac.state === 'suspended') await ac.resume(); // ユーザーの操作前は止まっていることがある
  const osc = ac.createOscillator();
  const gain = ac.createGain();
  const t = ac.currentTime;
  osc.type = 'sine';
  osc.frequency.value = frequency;
  gain.gain.setValueAtTime(volume, t);
  gain.gain.exponentialRampToValueAtTime(0.0001, t + duration); // 急に切るとプツッと鳴るので絞る
  osc.connect(gain).connect(ac.destination);
  osc.start(t);
  osc.stop(t + duration);
}

// エラー用: 低めの音を 2 回
export async function playErrorBeep(): Promise<void> {
  await playBeep(330, 0.12);
  window.setTimeout(() => void playBeep(330, 0.12), 180);
}

AudioContext は作るたびに音声の出力を用意するので、1 つを使い回します。ユーザーがまだ画面を操作していない状態では、止まった状態(suspended)で作られることがあるため、鳴らす前に resume() します。音量はアプリの音量として扱われ、OS の「警告音」の設定とは関係しません。

2. バックエンドから実装する (Rust)

OS の関数は extern ブロックで宣言すれば、クレートを足さずに呼べます。使うのは各 OS が用意している警告音の関数で、どれも Tauri のアプリが最初から使っている OS のライブラリにあるので、追加の準備は要りません。

  • Windows: 警告の種類ごとに別の音を鳴らせます。音の再生は OS に任せてすぐ戻ります。
  • macOS / Linux: 鳴るのは 1 種類の警告音だけで、種類の指定は使いません。
  • macOS と Linux の画面まわりの関数はメインスレッドから呼ぶのが原則なので、run_on_main_thread() でメインスレッドへ回します。こうしておけば async コマンドや別スレッドからも鳴らせます。

unsafe extern という書き方は Rust 1.82 以降で使えます。Rust 2024 エディションでは unsafe の付かない extern ブロックがエラーになるので、どちらのエディションでも通るこの形にしています。

use serde::Deserialize;

/// フロントエンドから受け取る音の種類
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BeepKind {
    Default,
    Warning,
    Error,
    Info,
}

#[cfg(target_os = "windows")]
fn platform_beep(kind: BeepKind) {
    #[link(name = "user32")]
    unsafe extern "system" {
        fn MessageBeep(u_type: u32) -> i32;
    }
    let sound = match kind {
        BeepKind::Default => 0x00, // MB_OK: 一般の警告音
        BeepKind::Warning => 0x30, // MB_ICONWARNING: メッセージ (警告)
        BeepKind::Error => 0x10,   // MB_ICONERROR: システム エラー
        BeepKind::Info => 0x40,    // MB_ICONINFORMATION: メッセージ (情報)
    };
    unsafe {
        MessageBeep(sound); // 再生を OS に頼んですぐ戻る。失敗しても 0 が返るだけ
    }
}

#[cfg(target_os = "macos")]
fn platform_beep(_kind: BeepKind) {
    #[link(name = "AppKit", kind = "framework")]
    unsafe extern "C" {
        fn NSBeep();
    }
    unsafe { NSBeep() } // 「サウンド」設定で選んだ警告音
}

#[cfg(target_os = "linux")]
fn platform_beep(_kind: BeepKind) {
    use std::ffi::c_void;
    #[link(name = "gdk-3")]
    unsafe extern "C" {
        fn gdk_display_get_default() -> *mut c_void;
        fn gdk_display_beep(display: *mut c_void);
    }
    unsafe {
        let display = gdk_display_get_default();
        if !display.is_null() {
            gdk_display_beep(display); // どう鳴るかはデスクトップ環境の設定次第
        }
    }
}

#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
fn platform_beep(_kind: BeepKind) {} // iOS / Android などでは何もしない

/// どのスレッドから呼んでもよいように、メインスレッドへ回して鳴らす
pub fn beep(app: &tauri::AppHandle, kind: BeepKind) {
    let _ = app.run_on_main_thread(move || platform_beep(kind));
}

#[tauri::command]
fn system_beep(app: tauri::AppHandle, kind: Option<BeepKind>) {
    beep(&app, kind.unwrap_or(BeepKind::Default));
}

/// 時間のかかる処理が終わったら鳴らす(ウィンドウが裏にあっても気付ける)
#[tauri::command]
async fn run_long_task(app: tauri::AppHandle) -> Result<(), String> {
    tauri::async_runtime::spawn_blocking(|| std::thread::sleep(std::time::Duration::from_secs(3)))
        .await
        .map_err(|e| e.to_string())?;
    beep(&app, BeepKind::Info);
    Ok(())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![system_beep, run_long_task])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

フロントエンドからは種類を文字列で渡します。省略すると一般の警告音です。

import { invoke } from '@tauri-apps/api/core';

export type BeepKind = 'default' | 'warning' | 'error' | 'info';

// OS の警告音を鳴らす。鳴り終わるのを待たずに戻る
export async function systemBeep(kind: BeepKind = 'default'): Promise<void> {
  await invoke('system_beep', { kind });
}

await systemBeep('error');
await invoke('run_long_task'); // 約 3 秒後に情報の音が鳴る

動作確認

npm run tauri dev で起動し、systemBeep() をボタンから呼びます。Windows では 'default'・'warning'・'error'・'info' で、サウンド設定の「一般の警告音」「メッセージ (警告)」「システム エラー」「メッセージ (情報)」に割り当てた音がそれぞれ鳴ります。設定でどれかの音を変えると、アプリを作り直さなくても鳴る音が変わります。run_long_task を呼んでから別のウィンドウを前面にしても、約 3 秒後に音が鳴ります。playBeep() は OS に関係なく同じ音です。

よくあるエラーと対処法

  • 「Command system_beep not found」: invoke_handler の generate_handler! にコマンドを入れ忘れています。
  • extern ブロックに unsafe が必要という趣旨のコンパイルエラー: Rust 2024 エディションで、unsafe の付かない古い書き方をしています。上のように unsafe extern にします。逆に Rust 1.82 より古いと unsafe extern が書けないので、rustup update で更新します。
  • 何も鳴らない(Windows): コントロール パネルの「サウンド」で「サウンドなし」が選ばれているか、該当するイベントの音が「(なし)」になっています。音量ミキサーの「システム音」がミュートの場合も鳴りません。
  • 何も鳴らない(Linux): 警告音を鳴らさない、または画面の点滅で知らせる設定のデスクトップ環境があります。確実に音を出したいなら 1 章の Web Audio を併用します。古い記事にある println!("\x07") は端末に制御文字を出すだけで、デスクトップから起動したアプリでは鳴りません。
  • Web Audio の音が鳴らない: AudioContext が止まったままです。resume() を呼び、最初の 1 回はボタンのクリックなどの操作から鳴らします。

OS ごとの違いと注意点

  • Windows: このコードで種類ごとに別の音になるのは Windows だけです。関数は再生の終わりを待たずに戻ります。
  • macOS: 種類の指定にかかわらず、「サウンド」設定で選んだ警告音が鳴ります。
  • Linux: 鳴り方はデスクトップ環境とその設定によって違い、鳴らないこともあります。
  • iOS / Android: このコードでは何もしません。
  • 鳴らしすぎない: 入力のたびに鳴らすと煩わしく、音を切っているユーザーには伝わりません。音は補助として使い、何が起きたかは画面にも出します。エラーの内容を伝えるなら メッセージダイアログ・警告ダイアログを表示する と組み合わせます。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する