入力エラーや長い処理の終了を音で知らせたいとき、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: このコードでは何もしません。
- 鳴らしすぎない: 入力のたびに鳴らすと煩わしく、音を切っているユーザーには伝わりません。音は補助として使い、何が起きたかは画面にも出します。エラーの内容を伝えるなら メッセージダイアログ・警告ダイアログを表示する と組み合わせます。
