Модуль музыки (music.rs)
В модуле музыки мы определяем константы для музыкальных нот и их значений частоты.
Каждая нота хранится как значение f64, чтобы его можно было сразу использовать при настройке частоты ШИМ. Также определено специальное значение REST, которое обозначает тишину между нотами.
#![allow(unused)]
fn main() {
// Частоты нот в герцах как f64
pub const NOTE_B0: f64 = 31.0;
pub const NOTE_C1: f64 = 33.0;
pub const NOTE_CS1: f64 = 35.0;
pub const NOTE_D1: f64 = 37.0;
pub const NOTE_DS1: f64 = 39.0;
pub const NOTE_E1: f64 = 41.0;
pub const NOTE_F1: f64 = 44.0;
pub const NOTE_FS1: f64 = 46.0;
pub const NOTE_G1: f64 = 49.0;
pub const NOTE_GS1: f64 = 52.0;
pub const NOTE_A1: f64 = 55.0;
pub const NOTE_AS1: f64 = 58.0;
pub const NOTE_B1: f64 = 62.0;
pub const NOTE_C2: f64 = 65.0;
pub const NOTE_CS2: f64 = 69.0;
pub const NOTE_D2: f64 = 73.0;
pub const NOTE_DS2: f64 = 78.0;
pub const NOTE_E2: f64 = 82.0;
pub const NOTE_F2: f64 = 87.0;
pub const NOTE_FS2: f64 = 93.0;
pub const NOTE_G2: f64 = 98.0;
pub const NOTE_GS2: f64 = 104.0;
pub const NOTE_A2: f64 = 110.0;
pub const NOTE_AS2: f64 = 117.0;
pub const NOTE_B2: f64 = 123.0;
pub const NOTE_C3: f64 = 131.0;
pub const NOTE_CS3: f64 = 139.0;
pub const NOTE_D3: f64 = 147.0;
pub const NOTE_DS3: f64 = 156.0;
pub const NOTE_E3: f64 = 165.0;
pub const NOTE_F3: f64 = 175.0;
pub const NOTE_FS3: f64 = 185.0;
pub const NOTE_G3: f64 = 196.0;
pub const NOTE_GS3: f64 = 208.0;
pub const NOTE_A3: f64 = 220.0;
pub const NOTE_AS3: f64 = 233.0;
pub const NOTE_B3: f64 = 247.0;
pub const NOTE_C4: f64 = 262.0;
pub const NOTE_CS4: f64 = 277.0;
pub const NOTE_D4: f64 = 294.0;
pub const NOTE_DS4: f64 = 311.0;
pub const NOTE_E4: f64 = 330.0;
pub const NOTE_F4: f64 = 349.0;
pub const NOTE_FS4: f64 = 370.0;
pub const NOTE_G4: f64 = 392.0;
pub const NOTE_GS4: f64 = 415.0;
pub const NOTE_A4: f64 = 440.0;
pub const NOTE_AS4: f64 = 466.0;
pub const NOTE_B4: f64 = 494.0;
pub const NOTE_C5: f64 = 523.0;
pub const NOTE_CS5: f64 = 554.0;
pub const NOTE_D5: f64 = 587.0;
pub const NOTE_DS5: f64 = 622.0;
pub const NOTE_E5: f64 = 659.0;
pub const NOTE_F5: f64 = 698.0;
pub const NOTE_FS5: f64 = 740.0;
pub const NOTE_G5: f64 = 784.0;
pub const NOTE_GS5: f64 = 831.0;
pub const NOTE_A5: f64 = 880.0;
pub const NOTE_AS5: f64 = 932.0;
pub const NOTE_B5: f64 = 988.0;
pub const NOTE_C6: f64 = 1047.0;
pub const NOTE_CS6: f64 = 1109.0;
pub const NOTE_D6: f64 = 1175.0;
pub const NOTE_DS6: f64 = 1245.0;
pub const NOTE_E6: f64 = 1319.0;
pub const NOTE_F6: f64 = 1397.0;
pub const NOTE_FS6: f64 = 1480.0;
pub const NOTE_G6: f64 = 1568.0;
pub const NOTE_GS6: f64 = 1661.0;
pub const NOTE_A6: f64 = 1760.0;
pub const NOTE_AS6: f64 = 1865.0;
pub const NOTE_B6: f64 = 1976.0;
pub const NOTE_C7: f64 = 2093.0;
pub const NOTE_CS7: f64 = 2217.0;
pub const NOTE_D7: f64 = 2349.0;
pub const NOTE_DS7: f64 = 2489.0;
pub const NOTE_E7: f64 = 2637.0;
pub const NOTE_F7: f64 = 2794.0;
pub const NOTE_FS7: f64 = 2960.0;
pub const NOTE_G7: f64 = 3136.0;
pub const NOTE_GS7: f64 = 3322.0;
pub const NOTE_A7: f64 = 3520.0;
pub const NOTE_AS7: f64 = 3729.0;
pub const NOTE_B7: f64 = 3951.0;
pub const NOTE_C8: f64 = 4186.0;
pub const NOTE_CS8: f64 = 4435.0;
pub const NOTE_D8: f64 = 4699.0;
pub const NOTE_DS8: f64 = 4978.0;
pub const REST: f64 = 0.0; // Тишина, для пауз
}
Структура песни
Мы определяем небольшую вспомогательную структуру, которая представляет песню и отвечает за тайминг нот.
#![allow(unused)]
fn main() {
pub struct Song {
whole_note: u64,
}
}
Поле whole_note хранит длительность целой ноты в миллисекундах. Все остальные длительности нот вычисляются из этого значения. Использование миллисекунд упрощает задержки при воспроизведении нот на зуммере.
Создание песни
При создании Song мы вычисляем длительность целой ноты по темпу.
#![allow(unused)]
fn main() {
impl Song {
pub fn new(tempo: u16) -> Self {
let whole_note = (60_000 * 4) / tempo as u64;
Self { whole_note }
}
}
}
Темп задаётся в ударах в минуту. В одной минуте 60 000 миллисекунд, а целая нота равна четырём долям. Деление на темп даёт время в миллисекундах, которое должна длиться одна целая нота.
Например, при 120 BPM одна доля длится 500 мс, поэтому целая нота длится 2000 мс.
Расчёт длительности ноты
Этот метод преобразует значение ноты во временную длительность.
#![allow(unused)]
fn main() {
pub fn calc_note_duration(&self, divider: i16) -> u64 {
if divider > 0 {
self.whole_note / divider as u64
} else {
let duration = self.whole_note / divider.unsigned_abs() as u64;
(duration as f64 * 1.5) as u64
}
}
}
Делитель показывает, как нота соотносится с целой. Значение 1 означает целую ноту, значение 2 — половинную, значение 4 — четвертную. Длительность вычисляется делением длительности целой ноты на это значение.
Отрицательные значения используются для обозначения нот с точкой. Нота с точкой звучит в полтора раза дольше обычной ноты того же вида. Когда делитель отрицательный, код сначала вычисляет обычную длительность по модулю значения, а затем умножает её на 1,5.
Такая логика с положительными и отрицательными значениями — собственный подход (основанный на примере для Arduino, на который я опирался), чтобы отличать ноты с точкой. Это не часть стандартной музыкальной нотации.