Пишем простой генератор звука для азбуки Морзе

Давно я не писал что-то в свой блог. Надо исправлять данное недоразумение.

Так что давайте напишем генератор звука для азбуки Морзе.

Теория

Здесь должна быть теория по генерации звука, но мне как-то лень, поэтому читайте её по ссылкам приведённым в конце статьи.

Трансляция текста

Для начала нам нужно написать код для трансляции текста в кодовую азбуку Морзе, для этого воспользуемся возможностью предоставляемую нам словарём.

Для написания кода я буду использовать язык 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
}

Теперь у нас есть функционал отвечающий за преобразования текста в код Морзе.

Так давайте перейдём к генерации звука!

Генерация звука

Как и писал ранее, вся теория по генерации звука находится в конце. За справкой обращайтесь туда.

Я же покажу всё в виде кода. Для генерации звука будем использовать синус. Нам понадобится только три параметра:

Собственно код:

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] Процедурная генерация звука в реальном времени

[3] Программная генерация звуков

[4] Программный синтезатор

Назад