Пишем простой генератор звука для азбуки Морзе
Давно я не писал что-то в свой блог. Надо исправлять данное недоразумение.
Так что давайте напишем генератор звука для азбуки Морзе.
Теория
Здесь должна быть теория по генерации звука, но мне как-то лень, поэтому читайте её по ссылкам приведённым в конце статьи.
Трансляция текста
Для начала нам нужно написать код для трансляции текста в кодовую азбуку Морзе, для этого воспользуемся возможностью предоставляемую нам словарём.
Для написания кода я буду использовать язык Rust и библиотеку lazy_static
.
#[macro_use]
extern crate lazy_static;
use std::collections::HashMap;
lazy_static! {
// спец. коды
static ref ERROR_CODE: &'static str = "········";
static ref END_TRANSMISSION: &'static str = "··-·-";
// таблица кодов
static ref MORSE_TABLE: Vec<(&'static str, &'static str)> = vec![
("А", "·-"), ("Б", "-···"), ("В", "·--"), ("Г", "--·"), ("Д", "-··"), ("Ё", "·"), ("Е", "·"),
("Ж", "···-"), ("З", "--··"), ("И", "··"), ("Й", "·---"), ("К", "-·-"), ("Л", "·-··"), ("М", "--"),
("Н", "-·"), ("О", "---"), ("П", "·--·"), ("Р", "·-·"), ("С", "···"), ("Т", "-"), ("У", "··-"),
("Ф", "··-·"), ("Х", "····"), ("Ц", "-·-·"), ("Ч", "---·"), ("Ш", "----"), ("Щ", "--·-"),
("Ъ", "--·--"), ("Ы", "-·--"), ("Ь", "-··-"), ("Э", "··-··"), ("Ю", "··--"), ("Я", "·-·-"),
("1", "·----"), ("2", "··---"), ("3", "···--"), ("4", "····-"), ("5", "·····"), ("6", "-····"),
("7", "--···"), ("8", "---··"), ("9", "----·"), ("0", "-----"), (".", "······"), (",", "·-·-·-"),
(":", "---···"), (";", "-·-·-·"), (")", "-·--·-"), ("(", "-·--·-"), ("'", "·----·"), ("-", "-····-"),
("\\", "-··-·"), ("/", "-··-·"), ("!", "··--··"), ("?", "--··--"), (" ", "-···-"), ("@", "·--·-·")
];
// генерируем словарь
static ref CODER: HashMap<String, String> = {
let mut hashmap = HashMap::new();
for &(symbol, morse) in MORSE_TABLE.iter() {
hashmap.insert(symbol.to_string(), morse.to_string());
}
hashmap
};
}
// функция кодирования
fn encode(input: &str) -> String {
let mut result = String::new();
// перегоняем текст в ЗАГЛАВНЫЙ формат
let input_str = input.to_uppercase();
for symbol in input_str.chars() {
match CODER.get(&format!("{}", symbol)) {
// кодируем символ
Some(code) => result.push_str(&format!("{} ", code)),
// игнорим остальные символы
None => ()
};
}
// добавляем индификатор конца трансляции
result.push_str(&END_TRANSMISSION);
result
}
Теперь у нас есть функционал отвечающий за преобразования текста в код Морзе.
Так давайте перейдём к генерации звука!
Генерация звука
Как и писал ранее, вся теория по генерации звука находится в конце. За справкой обращайтесь туда.
Я же покажу всё в виде кода. Для генерации звука будем использовать синус. Нам понадобится только три параметра:
- частота, Герцы
- продолжительность, секунды
- громкость, значение в интервале [0, 1].
Собственно код:
fn generate(freq: f32, duration: f32, volume: f32) -> Vec<u8> {
// частота дискретизации
let sample_rate = 44100;
// амплитуда определяется максимальным значением для i16 умноженая на громкость
let amplitude = std::i16::MAX as f32 * volume;
// количество генерируемых сэмплов
let total_samples: u32 = (sample_rate as f32 * duration).round() as u32;
// угловая частота / частоте дискретизации
let w = std::f32::consts::TAU * freq / sample_rate as f32;
// мы будем делать i16, а записывать по 2 блока u8 в формате little endian
let mut buffer: Vec<u8> = Vec::with_capacity(2 * total_samples as usize);
for k in 0..total_samples {
// f = A * sin(kw)
// A -- амплитуда сигнала
// k -- номер сэмпла
let sample = (amplitude * (k as f32 * w).sin()) as i16;
// проталкиваем в буффер
buffer.extend_from_slice(&sample.to_le_bytes());
}
buffer
}
Обратите внимание, что я использовал функцию определяющую порядок представления байт в итоговом файле. Она является не столь важным элементом, но мы же хорошие программисты и хотим быть точно уверены в правильном представлении звука.
Собираем звуковую дорожку
Теперь когда мы имеем транслятор и генератор остаётся только собрать всё в одну дорожку.
План у нас такой:
- сгенерировать звука для 'точки'
- сгенерировать звук для 'тире'
- сгенерировать звук для паузы
- собрать дорожку по строке с кодом Морзе
Для звука 'точки' будем использовать частоту в 300 Герц длительностью 0.15 и громкостью 1. Остальную информацию возьмём из вики:
За единицу времени принимается длительность одной точки. Длительность тире равна трём точкам. Пауза между элементами одного знака — одна точка, между знаками в слове — 3 точки, между словами — 7 точек
// весь предыдущий код
fn main() {
// длительность звучания 'точки'
let dot_len = 0.15;
// вектор для наших сэмплов
let mut result_audio: Vec<u8> = Vec::new();
// звук одной 'точки'
let dot = generate(300.0, dot_len, 1.0);
// и соответственно 'тире'
let dash = generate(300.0, 3.0 * dot_len, 1.0);
// собираем 44100 * 3 * dot_len нулей (u16 или по два для u8) для паузы между знаками
let e_pause = vec![0_u8; 2 * (44100.0 * 3.0 * dot_len).round() as usize];
// собираем 44100 * 7 * dot_len нулей для паузы между словами
let w_pause = vec![0_u8; 2 * (44100.0 * 7.0 * dot_len).round() as usize];
// собираем 44100 * dot_len нулей для паузы между элементами одного знака
let s_pause = vec![0_u8; 2 * (44100.0 * dot_len).round() as usize];
// кодируем наш текст
let morse_text = encode("привет мир!");
let mut last_char = '?';
for code in morse_text.chars() {
match code {
'·' => result_audio.extend(dot.clone()),
'-' => result_audio.extend(dash.clone()),
// пауза между словами
' ' => {
result_audio.extend(w_pause.clone());
continue
}
_ => {}
};
if last_char == code {
// пауза между элементами одного знака
result_audio.extend(s_pause.clone());
} else {
// пауза между символами
result_audio.extend(e_pause.clone());
}
last_char = code;
}
}
Запись в файл и воспроизведение
На данном мы уже имеем сгенерированную звуковую последовательность, но для того чтобы его прослушать нам необходимо либо отправить звук на устройство вывода, либо записать в файл.
Мы пойдем по пути наименьшего сопротивления и просто запишем данные в файл. А для того чтобы прослушать получившуюся мелодию используем консольную утилиту ffplay
.
Не будем слишком многословны и перейдём сразу к коду
// место lazy_static и HashMap
use std::fs::File;
use std::io::Write;
//
// место для generate и encode
//
fn main() {
let mut file = File::create("./generated.sound").expect("can't create file!");
//
// здесь находится генерация звука
//
file.write_all(result_audio.as_slice());
}
После компиляции и выполнения данного кода получим на выходе файл generated.sound
. Прослушать его можно следующей командой:
$ ffplay -f s16le -ar 44k -ac 1 generated.sound
На этом всё и до встречи через полгода :)
Полезные ссылки
[1] Азбука Морзе
[2] Процедурная генерация звука в реальном времени