見出し画像

Rustでピアノ🎹を作ってみよう

Rustの勉強に、main.rsのみでキーボードを使ってピアノの音が出るアプリを作ってみましょう。シンプルでありながらRustのエッセンスを感じられると思います。
注: この例はMacでしか確認していません。一応クロスプラットフォームのはずですが。

まず、cargoでアプリケーションを初期化します。

$ mkdir rs-midi
$ cd rs-midi
$ cargo init .

作られたCargo.tomlを開いて、中身の[dependencies]を以下のようにします。(バージョンは適宜最新化してください。)

[dependencies]
eframe = "0.27.2"
itertools = "0.12.1"
phf = { version = "0.11", features = ["macros"] }
rustysynth = "1.3.1"
tinyaudio = "0.1.3"

では、src/main.rsを書いていきます。
ここでは、キー操作を受け取るためのウインドウをeguiで開き、rustysynthでMIDIノートをtinyaudioに出力します。

まず、先頭に依存モジュールを記述します。

use eframe::egui;
use itertools::Itertools;
use phf::{phf_map, Map};
use rustysynth::{SoundFont, Synthesizer, SynthesizerSettings};
use std::{
    fs::File,
    sync::{Arc, Mutex},
};
use tinyaudio::prelude::*;

続いて定数を書いていきます。
OUTPUT_PARAMSはtinyaudioのパラメータです。MidiNoteはrustysynthで鳴らす音をMIDIのノートナンバーとベロシティで持ち、それをキーボードのキーと対応付けた静的なマップにするためphfのマクロを使っています。

const OUTPUT_PARAMS: OutputDeviceParameters = OutputDeviceParameters {
    channels_count: 2,
    sample_rate: 44100,
    channel_sample_count: 441, // サンプルのmaxの長さ
};

#[derive(Debug)]
pub struct MidiNote {
    pub note: i32,
    pub velocity: i32,
}

pub static NOTE_KEY_MAP: Map<&'static str, MidiNote> = phf_map! {
    "A" => MidiNote {
        note: 60,
        velocity: 100,
    },
    "S" => MidiNote {
        note: 62,
        velocity: 100,
    },
    "D" => MidiNote {
        note: 64,
        velocity: 100,
    },
    "F" => MidiNote {
        note: 65,
        velocity: 100,
    },
    "G" => MidiNote {
        note: 67,
        velocity: 100,
    },
};

では、eguiのアプリケーションであるSynthAppを作成します。
メンバにsynthersizerとnote on/offのメソッドを持ち、eframe::Appのupdateでキーイベントを処理します。

struct SynthApp {
    synthesizer: Arc<Mutex<Synthesizer>>,
    midi_channel: i32,
}

impl SynthApp {
    fn note_on(&mut self, key: &str) {
        let note = match NOTE_KEY_MAP.get(key) {
            Some(note) => note,
            None => return,
        };
        self.synthesizer
            .lock()
            .unwrap()
            .note_on(self.midi_channel, note.note, note.velocity)
    }

    fn note_off(&mut self, key: &str) {
        let note = match NOTE_KEY_MAP.get(key) {
            Some(note) => note,
            None => return,
        };
        self.synthesizer
            .lock()
            .unwrap()
            .note_off(self.midi_channel, note.note);
    }
}

impl eframe::App for SynthApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        ctx.input(|i| {
            for key_str in NOTE_KEY_MAP.keys() {
                if let Some(key) = egui::Key::from_name(key_str) {
                    if i.key_pressed(key) {
                        self.note_on(key_str);
                    } else if i.key_released(key) {
                        self.note_off(key_str);
                    }
                }
            }
        });

        egui::CentralPanel::default().show(ctx, |ui| {
            ui.heading("My egui Application");
            ui.label(format!("Midi channel {}", self.midi_channel));
        });
    }
}

最後に、mainを書きます。
ここで、synthesizerはArc<Mutex<..>>で囲んでいます。これはrun_output_deviceとSynthAppの両方からアクセスするためです。
なお、サウンドフォントのファイル名は適宜変更してください。TimGM6mb.sf2を使われる人が多いようですねぇ。。

fn main() -> Result<(), eframe::Error> {
    // Load the SoundFont.
    let mut sf2 = File::open("your_soundfont.sf2").unwrap();
    let sound_font = Arc::new(SoundFont::new(&mut sf2).unwrap());

    // Create the MIDI file sequencer.
    let settings = SynthesizerSettings::new(OUTPUT_PARAMS.sample_rate as i32);
    let synthesizer = Arc::new(Mutex::new(
        Synthesizer::new(&sound_font, &settings).unwrap(),
    ));

    // Run output device.
    let synth_c = synthesizer.clone();
    let mut left: Vec<f32> = vec![0_f32; OUTPUT_PARAMS.channel_sample_count];
    let mut right: Vec<f32> = vec![0_f32; OUTPUT_PARAMS.channel_sample_count];
    let _device = run_output_device(OUTPUT_PARAMS, move |data| {
        synth_c
            .lock()
            .unwrap()
            .render(&mut left[..], &mut right[..]);
        for (i, value) in left.iter().interleave(right.iter()).enumerate() {
            data[i] = *value;
        }
    })
    .unwrap();

    // eframe
    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default().with_inner_size([640.0, 480.0]),
        ..Default::default()
    };
    eframe::run_native(
        "My egui App",
        options,
        Box::new(|_cc| {
            Box::new(SynthApp {
                synthesizer,
                midi_channel: 0,
            })
        }),
    )
}

ここまでできたら、cargo runで実行してみましょう。
ウインドウが開いたらキーボードのASDFGでドレミファソの音が出ると思います。

どうだったでしょうか?
小粒ながらもRustでつまづきがちなmutやref、マクロ、struct、クロージャー、iter、Arc<Mutex<..>>といったところが押さえられていると思います。
rustysynthはMIDI音源として扱えますし、eguiでいろんなUIを追加することもできるので是非チャレンジしてみてください。

それでは良いRustライフを!