Pull to refresh

Воспроизведение MIDI звуков на языке JAVA

Reading time 7 min
Views 26K
Прежде, чем перейти к сути, я немного расскажу вам о компьютерном звуке.

Существует два основных формата воспроизведения звуков компьютером:
цифровой (WAV-формат) и синтезированный (MIDI).

Цифровой звук является основным стандартом компьютерного звука сегодня. Именно оцифрованный звук вы слышите, проигрывая композиции в mp3 формате или прослушивая компакт-диски, просматривая фильм или играя в комьютерные игры.

Оцифрованный звук представляет собой набор битов, который последовательно описывает значение уровня амплитуды звуковой волны в каждый момент времени звучания. При его воспроизведении звуковая карта лишь переводит «цифровой» звук в привычную нам «аналоговую» форму.

Но существует и другой вид компьютерного звука — синтезированный (MIDI)

Звуковая карта может служить музыкальным синтезатором, способном воспроизводить звучания до 128-ми различных музыкальных инструментов. Качество и принцип имитации инструментов зависит от вашей звуковой карты. Она может пытаться смоделировать звучание инструмента совокупностью нескольких FM (частотных) генераторов простых частот, для каждой из которых задана амплитуда, частота, фаза и куча других параметров. Но чаще она обращается к хранящимся в ней «банкам данных» образцов звучания инструментов.

В настоящий момент существует множество звуковых карт, которые могут работать с SoundFont. Это просто wav сэмплы, которые можно загрузить в звуковую карту и звучанием этих инструментов можно управлять в миди-секвенсоре. SoundFont обычно имеет расширение .sf2, их могут также называть патчами (patch) или программами (program). Несколько SoundFont объединяют в звуковые банки (SoundFont Banks), которые могут содержать до 128 инструментов и одного набора ударных.
В сети вы без труда сможете найти профессиональные библиотеки банков инструментов в формате SF2.

Чтобы синтезатор воспроизвел нужный звук, необходимо передать ему специальную команду. Совокупность таких команд описана стандартом MIDI.
MIDI это акроним от Musical Instruments Digital Interface, что в буквальном переводе — цифровой интерфейс музыкальных инструментов.

При помощи этих команд мы можем «сказать» синтезатору звуковой платы каким инструментом ту или иную ноту мы хотим чтобы он воспроизвел. Например Фа-диез на рояле или любом другом из 128-ми инструментов.

Допустим для каких-то целей вам понадобилось из java научиться воспроизводить ноты. И вы решили узнать, как заставить синтезатор вашей звуковой платы это делать. Окунувшись поисковиком в информационное море интернета, вы неожиданно поймете, что толковой информации по этому вопросу найти непросто.

Скажу, что существует достаточное количество библиотек для работы с midi. И все они обещают, что с их помощью процес программирования музыки будет проще. Я столкнулся с двумя такими: jfugue и jMusic. В Ютубе есть наглядные руководства о их применении. В каждой из подобных библиотек были выдуманы свои способы и правила. Чем более экзотическая библиотека, тем меньше информации и примеров. Так же придется довериться правильности и точности их работы.
Из многих соображений полагаю, что лучше начать изучение и научиться применять в начале стандартную библиотеку: javax/sound/midi

Тем, кто впервые сталкивается с этой темой, моя статья призвана помочь сделать первые шаги. Я научу вас самым базовым навыкам работы с midi звуком. А дальше вы уже сами сможете расширить их применение и, если потребуется, легко дополните информацию из описания библиотеки и примерами из интернета.

Итак, все что вам для начала нужно — научиться воспроизводить ноты.
Ваше желание включает некоторую дополнительную информацию — название ноты (номер) и длительность звучания. А так же инструмент, который вы хотите чтобы её сыграл.
Желание совершенно просто и ясно сформулировано: «Хочу, чтобы прозвучала такая-то нота, определенное время и выбранным инструментом.» Однако сходу найти соответствующую информацию, как это сделать, вам будет сложно. Вот по этому эта статья и написана — чтобы вы не тратили свое время а сразу получили то, что нужно. И начали применять.

Номера нот вы можете определить по данной таблице:


А номера инструментов можно посмотреть здесь

Перед непосредственным программированием воспроизведения потребуется некоторая подготовка. А именно, вам понадобится получить объект синтезатора (Synthesizer) и отрыть его. Затем получить доступ к его каналам.
Звучит, возможно, не совсем понятно, но на практике всё просто:
     Synthesizer synth = MidiSystem.getSynthesizer();
     synth.open();
     MidiChannel[] channels = synth.getChannels();

Канал можно представить как универсального музыканта, который способен играть на любом из 128-ми инструментов. Вы располагаете 16-тью такими каналами.
Вот в принципе и вся подготовка. Далее необходимо научиться давать команды музыкантам.
Допустим вы хотите, чтоб звучала нота Фа первой октавы. Для этого, посмотрев таблицу с номерами нот, выясняете, что её номер 65. Выбираем одного из 16-ти музыкантов. Допустим под номером 0 (channels[0]). Воспроизведение происходит по команде noteOn.
Она принимает 2 параметра: MIDI номер ноты (от 0 до 127) и громкость звучания (так же до 127)
Выглядеть это будет так:
     channels[0].noteOn(65, 80);

По этой команде музыкант под номером «ноль» начнет воспроизведение ноты Фа первой октавы. 80 — это громкость. Это можно представить, что музыкант нажал клавишу синтезатора и льется звук.
Думаю, вы догадались, что для прекращения звучания так же потребуется дать команду. Т.е. чтобы конкретно этот музыкант отпустил нажатую клавишу — выполняем команду noteOff.
Таким образом:
     channels[0].noteOff(65);

Между этими командами следует сделать паузу, от которой и будет зависеть длительность звучания.
Вот, собственно, и всё. И не нужны никакие сторонние библиотеки. Всё необходимое теперь вы сможете удобным способом написать под себя сами. Осталось открыть вам последний секрет — научить волшебным словам, которые заставят «музыкантов» взять другой муз. инструмент.
Напомню, что выбрать инструмент можно здесь. Любому из 16-ти каналов вы можете назначить один из 128-ми инструментов. По умолчанию почти все каналы используют фортепиано.
Так производится назначение «нулевому» каналу инструмент «скрипка»:
    channels[0].programChange(41);

Понадобится импортировать следующие стандартные пакеты:
    import javax.sound.midi.MidiChannel;
    import javax.sound.midi.MidiSystem;
    import javax.sound.midi.Synthesizer;


Итак, код воспроизводящий звук будет выглядеть следующим образом:
       try {
            Synthesizer synth = MidiSystem.getSynthesizer();
            synth.open();
            MidiChannel[] channels = synth.getChannels();
            channels[0].programChange(41);
            channels[0].noteOn(65, 80);
            Thread.sleep(1000); // in milliseconds
            channels[0].noteOff(65);
            synth.close();
       }  catch (Exception e) {
            e.printStackTrace();
       } 


Далее, как уже было сказано, возможности по программированию извлечения звука ограничены только вашей фантазией.

Для примитивной демонстрации создадим свой метод воспроизведения звуков, который нажатие, паузу и отпускание будет выполнять одной командой. Отведем для этих целей отдельный класс, структура и функционал которого не нуждаются в пояснении:
package music.player;

import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Synthesizer;

public class Player {

    private MidiChannel[] channels = null;
    private Synthesizer synth = null;

    public Player() {
        try {
            synth = MidiSystem.getSynthesizer();
            synth.open();
            channels = synth.getChannels();
            channels[0].programChange(41);
        } catch (MidiUnavailableException ex) {
            Logger.getLogger(Player.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public void close() {
        synth.close();
    }

    public void playSound(int channel, int duration, int volume, int... notes) {
        for (int note : notes) {
            channels[channel].noteOn(note, volume);
        }
        try {
            Thread.sleep(duration);
        } catch (InterruptedException ex) {
            Logger.getLogger(Player.class.getName()).log(Level.SEVERE, null, ex);
        }
        for (int note : notes) {
            channels[channel].noteOff(note);
        }
    }
}



В конструкторе класса создается и открывается синтезатор. Получаем массив каналов и первому (нулевому) назначаем инструмент «скрипка».
Метод playSound получает на вход номер канала, длительность звучания, громкость и последовательность нот, которые прозвучат одновременно. Реализация предельно проста — воспроизводятся все входящие ноты, удерживается интервал длительности и затем все выключаются.
Вот пример использования данного класса — воспроизводятся четыре аккорда:
    Player player = new Player();
       player.playSound(0, 1000, 80, 69, 72, 76);
       player.playSound(0, 1000, 80, 69, 74, 77);
       player.playSound(0, 1000, 80, 67, 71, 74);
       player.playSound(0, 1000, 80, 67, 72, 76);
    player.close();


Ну и в заключении, давайте для эксперимента запрограммируем какую-нибудь известную красивую мелодию.
Просто скопируйте себе код и, запустив, услышите «Jasper Forks — River Flows In You»:
      int notes[][] = {{470, 81}, {230, 80}, {470, 81}, {250, -1}, {230, 80}, {470, 81}, {230, 69}, {230, 76}, {470, 81}, {230, 69}, {470, 74}, {470, 73}, {470, 74}, {470, 76}, {470, 73}, {470, 71}, {970, -1}, {230, 69}, {230, 68}, {470, 69}, {730, -1}, {230, 64}, {230, 69}, {230, 71}, {470, 73}, {970, -1}, {230, 73}, {230, 74}, {470, 76}, {730, -1}, {230, 69}, {230, 74}, {230, 73}, {470, 71}, {1450, -1}, {470, 81}, {230, 80}, {470, 81}, {250, -1}, {230, 80}, {470, 81}, {230, 69}, {230, 76}, {470, 81}, {230, 69}, {470, 74}, {470, 73}, {470, 74}, {470, 76}, {470, 73}, {470, 71}, {970, -1}, {230, 69}, {230, 68}, {470, 69}, {730, -1}, {230, 64}, {230, 69}, {230, 71}, {470, 73}, {970, -1}, {230, 73}, {230, 74}, {470, 76}, {730, -1}, {230, 69}, {230, 74}, {230, 73}, {470, 71}, {250, -1}};
        Player player = new Player();
        for (int[] note : notes) {
            if (note[1] != -1) {
                player.playSound(0, note[0], 80, note[1]);
            } else {
                try {
                    Thread.sleep(note[0]);
                } catch (InterruptedException ex) {
                    Logger.getLogger(Music.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
        player.close();

В массиве пары чисел содержат величину длительности звучания и номер ноты. Паузу, для отличия, запишем «минус единицей». Канал и громкость оставим для всех нот одинаковые — 0 и 80 соответственно.

Что ж, полагаю у вас не возникло сложностей с пониманием изложенного материала. Буду рад, если эта статья сэкономит ваше время и силы, позволив с легкостью приступить к программированию воспроизведения MIDI звука на JAVA.
Tags:
Hubs:
+11
Comments 23
Comments Comments 23

Articles