首先,使用以下命令创建一个Rust新项目:
cargo new midi-rs
然后在Cargo.toml文件中加入依赖项:
[dependencies]
eframe = "0.27.2"
itertools = "0.12.1"
phf = { version = "0.11", features = ["macros"] }
rustysynth = "1.3.1"
tinyaudio = "0.1.3"
- eframe:EGUI框架——编写可以编译为web或本机的GUI应用程序
- itertools:扩展的迭代器适配器、函数和宏。
- phf:使用完美的散列函数在编译时生成高效的查找表。
- rustysynth:用纯Rust编写的MIDI 音色库合成器
- tinyaudio:是一个跨平台,易于使用,底层的音频输出库。
这个应用程序将打开一个接收键盘事件的egui窗口,这些事件被发送到rustysynth库作为midi音符并通过tinyaudio库输出声音。
首先,在src/main.rs文件中引入这些库:
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::*;
接下来,定义静态变量和常量:
const OUTPUT_PARAMS: OutputDeviceParameters = OutputDeviceParameters {
channels_count: 2,
sample_rate: 44100,
channel_sample_count: 441, // 样本的最大长度
};
#[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,
},
};
OUTPUT_PARAMS是tinyaudio的参数。MidiNote持有MIDI音符的音符数和速度,用rustysynth播放它。它们被保存在一个静态映射中,使用一个由按键值索引的phf_map!宏。
让我们定义SynthApp结构体,它是一个egui应用程序。它有合成器对象和方法来执行音符的开/关,在eframe::App的update方法中处理键盘事件。
struct SynthApp {
synthesizer: Arc>,
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));
});
}
}
在互联网上有很多不错的音色库,我们使用TimGM6mb.sf2,
可以在以下地址下载:
https://github.com/craffel/pretty-midi/blob/main/pretty_midi/TimGM6mb.sf2
将下载好的文件放入到项目的根目录下。
最后,我们编写main函数,合成器保存在Arc
fn main() -> Result<(), eframe::Error> {
// 加载音色库
let mut sf2 = File::open("TimGM6mb.sf2").unwrap();
let sound_font = Arc::new(SoundFont::new(&mut sf2).unwrap());
// 创建MIDI文件序列器
let settings = SynthesizerSettings::new(OUTPUT_PARAMS.sample_rate as i32);
let synthesizer = Arc::new(Mutex::new(
Synthesizer::new(&sound_font, &settings).unwrap(),
));
// 运行输出设备
let synth_c = synthesizer.clone();
let mut left: Vec = vec![0_f32; OUTPUT_PARAMS.channel_sample_count];
let mut right: Vec = 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键,就会播放音符。
为了进一步探索,你可以通过添加一些UI和乐器来尝试egui和rustysynth的各种功能。