Przejdziemy teraz do zupełnie innego tematu dotyczącego Javy i zaprezentuję Wam jak utworzyć podstawowy warsztat dla odtworzenia pojedynczej nuty granej za pomocą interfejsu MIDI. Aby MIDI w języku Java dawało oczekiwane rezultaty, wymaga poznania i zrozumienia kilku następnych klas które wspólnie przygotujemy i napiszemy. Zapraszam!

NAJPIERW TEORIA

MIDI to akronim od "Musical Instrument Digital Interface", czyli "cyfrowy interfejs instrumentów muzycznych". Nie wnikam w historię powstania, napiszę jedynie co nam to daje. Umożliwia w sposób komputerowy na dodawanie nut i konfigurowanie instrumentów muzycznych ustalając na przykład głośność w danym momencie lub kiedy dokładnie dana nuta ma być zagrana.

Java też obsługuje MIDI i można bez problemu dostać się do interfejsu i odtwarzać dźwięki. Dla początkujących może sprawiać problem zrozumienie budowy zapisu nutowego, gdyż sposoby widzenia w porównaniu z ludzkim będą się mocno różnić, przynajmniej patrząc przez pryzmat programowania tego w Javie. Dowiedzmy się najpierw z czym musimy mieć do czynienia!

WYJAŚNIENIE KLAS MIDI W JĘZYKU JAVA

Interfejs cyfrowy wymaga kilku obiektów o których w ogóle jeszcze nie pisałem na swojej stronie, dotychczas nie było takiej konieczności. Nie będę próbował zastępować literatury, wszystkie opiszę jednym / kilkoma zdaniami:

  • Sequencer
    • obiekt pełniący rolę "odtwarzacza"
    • dodajemy do niego obiekt sekwencji (opisany niżej)
  • Sequence
    • obiekt przechowujący nuty w postaci obiektów "MidiEvent" (opisane niżej)
  • Track
    • ścieżka dla pojedynczego kanału (MIDI obsługuje odtwarzanie na wiele ścieżek jednocześnie dzięki czemu można osobno dodać perkusję, melodię, bass itd.)
  • ShortMessage
    • kontener przechowujący polecenia dla obiektu "MidiEvent"
    • MIDI nie zostało zaprogramowane na automatycznie "uciszanie" danej nuty, zostało to podzielone na włączanie i wyłączanie więc trzeba dodawać osobno zdarzenia dla odtwarzania i zatrzymywania nuty
  • MidiEvent
    • pojedyncze zdarzenie uzyskujące dane pobierane z przekazywanego obiektu "ShortMessage"
    • ustalamy tutaj w którym "miejscu" dane zdarzenie ma nastąpić

Zwróćcie uwagę, że wielokrotnie podałem słowo "zdarzenie". Właśnie tak zachowuje się w języku Java MIDI, które będziemy programować. Zdarzenie może określać włączenie nuty, wyłączenie nuty, ale może też oznaczać zmianę instrumentu na inny. Nie określa się instrumentu na początku jak niektórym może się wydawać, tylko właśnie na podstawie zdarzeń. Niżej przedstawiam kodową demonstrację.

PRZYKŁAD KODU ŹRÓDŁOWEGO

Podane operacje wymagają obsłużenia wyjątków "MidiUnavailableException" oraz "InvalidMIDIDataException"! Najpierw wczytajcie się w kod, skompilujcie go, uruchomcie i wtedy przejdźcie do poniższych wyjaśnień:

KLASA "MAIN"

public class Main
{
	public static void main(String[] args)
	{
		new Launcher();
	}
}

KLASA "LAUNCHER"

import javax.sound.midi.*;

public class Launcher
{
	private static final int NOTE_OFF = 128;
	private static final int NOTE_ON = 144;
	private static final int CHANGE_INSTRUMENT = 192;

	public Launcher()
	{
		try
		{
			Sequencer sequencer = MidiSystem.getSequencer();
			Sequence sequence = new Sequence(Sequence.PPQ, 4);
			Track track = sequence.createTrack();

			sequencer.open();
			track.add(newEventToTrack(CHANGE_INSTRUMENT, 1, 6, 0, 1));
			track.add(newEventToTrack(NOTE_ON, 1, 44, 100, 1));
			track.add(newEventToTrack(NOTE_OFF, 1, 44, 100, 16));
			sequencer.setSequence(sequence);
			sequencer.start();
		}
		catch (MidiUnavailableException mue)
		{
			mue.printStackTrace();
		}
		catch (InvalidMidiDataException imde)
		{
			imde.printStackTrace();
		}
	}

	private MidiEvent newEventToTrack(int command, int channel, int data1, int data2, long tick) throws InvalidMidiDataException
	{
		ShortMessage sm = new ShortMessage(command, channel, data1, data2);
		MidiEvent me = new MidiEvent(sm, tick);

		return me;
	}
}

Pakiet "javax.sound.midi" otwiera Wam drogę do interfejsu MIDI. Nowy "Sequencer" pobierany jest ze statycznej metody "getSequencer", nie tworzymy go samodzielnie od zera. Linijkę niżej od razu tworzycie sobie "Sequence" tym razem w sposób tradycyjny. Przyjmuje on dwa parametry:

  1. rodzaj ustalania przedziału czasowego
    1. "pulses per quarter", czyli "PPQ"
    2. "timecode" interfejsu MIDI kryjące się pod skrótem "SMTPE"
  2. "rozdzielczość czasowa"
    • określa ona ile impulsów ma być w jednym takcie (to jest toporne określenie, ale naukowa definicja wymaga wiedzy z teorii muzyki), na przykład wartość 4 określa takt w taki sposób, jakby się mówiło w rytm "raz-dwa-trzy-cztery", a z kolei wartość 3 oznaczałaby mówienie w kółko "raz-dwa-trzy" (możecie znaleźć więcej informacji na ten temat również pod hasłem "time signature")

Trzecim obiektem z rzędu jest "Track". Jego również tworzycie przy użyciu metody zawartej w sekwencji, "createTrack". Nie przyjmuje parametrów. MIDI w języku Java wymaga szeregu obiektów, aby móc usłyszeć jedną nutkę. Przed jakimikolwiek wywołaniami metod, koniecznie "otwórzcie" sekwenser za pomocą metody "open". To jest wymaganie i zaniechanie tego skutkować będzie zgłoszeniem wyjątku.

Dopiero od teraz zaczyna się wpływ na melodię. Do ścieżki dodajemy zdarzenia korzystając z metody "add". Zalecam szczególnie napisać sobie osobną metodę dodającą zdarzenie, gdyż każde zdarzenie wymaga dwóch kolejnych obiektów o których już wspomniałem. Są to "ShortMessage" czyli określanie rodzaju zdarzenia na podstawie stałej wbudowanej liczby, jakiego kanału ma to dotyczyć oraz jakie dane w postaci dwóch kolejnych parametrów mają być brane pod uwagę oraz "MidiEvent" pobierający opisany przed chwilą obiekt oraz w którym "impulsie" ma to zostać wywołane.

Warto zapamiętać, że jest to podział określający co ("ShortMessage") i kiedy ("MidiEvent") ma być wykonane. Zapisałem stałe określające typ zdarzenia, abyście nie musieli zgadywać. "NOTE_ON" oznacza "odpalenie" nuty czyli moment, w którym nacisnęlibyście klawisz na pianinie. "NOTE_OFF" to puszczenie klawisza, a "CHANGE_INSTRUMENT" oznacza polecenie zmiany instrumentu na jakiś inny. Zauważcie, że podaje się go PRZED dodaniem zdarzenia "NOTE_ON" oraz w tym samym czasie (ostatni parametr).

Dopuszczalny zakres jest w przedziale <0; 127>, jeśli chodzi o parametry oznaczone jako "data1" oraz "data2". Inaczej będzie zgłoszenie wyjątku. Na samym końcu dorzucacie wywołanie metody "setSequence" i wprowadzacie sekwencję podtrzymującą Waszą zmienioną już ścieżkę. Po wszystkich wyżej wymienionych krokach dopiero teraz wywołujecie metodę "start" i odtwarzacie melodię MIDI w Java.

JAK DZIAŁA OBSŁUGA ZDARZEŃ W INTERFEJSIE MIDI W JĘZYKU JAVA?

A teraz druga część artykułu. Rzut światła na termin "obsługa zdarzeń" - chodzi mi o nic innego jak możliwość wywoływania podpiętej metody pod wpływem jakiegoś zdarzenia. W przypadku przycisków omówionych już miesiące temu, zdarzeniem jest najczęściej naciśnięcie przycisku. W przypadku muzycznego interfejsu, zdarzeniem będzie wprowadzenie osobnych obiektów "ShortMessage" oraz "MidiEvent" do ścieżki wraz z ustaleniem kiedy ma on nastąpić.

Z praktycznego punktu widzenia, MIDI obsługuje zdarzenia tak samo jak dodawaliśmy sobie nutki do ścieżki z tym, że należy zwrócić uwagę na dwa istotne czynniki:

  • pierwszy parametr konstruktora obiektu "ShortMessage" musi przyjmować stałą 176
  • tuż po utworzeniu sekwensera należy wywołać metodę "addControllerEventListener" wstawiając w miejsce parametrów klasę implementującą interfejs "ControllerEvent" lub wyrażenie lambda oraz tablicę liczb całkowitych oznaczających jakie liczby wstawiane w parametr oznaczony jako "data1" podczas tworzenia obiektów "ShortMessage" mają być brane pod uwagę

Nie bójcie się jeśli wydaje się to niezrozumiałe, wszystko zostało zawarte w przykładzie poniżej.

JESZCZE JEDEN PRZYKŁAD!

Oto ten sam kod źródłowy pobrany z pierwszej części rozbudowany o omawianą przez nas część obsługi zdarzeń. Jeśli chodzi o MIDI, to tutaj panuje hasło "ControllerEvent". Wówczas gdy to zobaczycie, bądźcie pewni, że chodzi tu o wywoływanie zdarzeń podczas grania melodii. Skompilujcie to sobie i obserwujcie wynik:

KLASA "MAIN"

public class Main
{
	public static void main(String[] args)
	{
		new Launcher();
	}
}

KLASA "LAUNCHER"

package pl.jasonxiii.midi;

import javax.sound.midi.*;

public class Launcher
{
	private static final int EVENT_NUMBER = 127;
	private static final int NOTE_OFF = 128;
	private static final int NOTE_ON = 144;
	private static final int NOTE_CONTROLLER_EVENT = 176;

	private Sequencer sequencer;
	private Sequence sequence;
	private Track track;
	private int count = 0;

	public Launcher()
	{
		try
		{
			createInstances();
			sequencer.open();
			sequencer.addControllerEventListener((e) ->
			{
				++count;

				System.out.println("Uderzenia: " + count);
			}, new int[]{EVENT_NUMBER});
			addEvents();
			sequencer.setSequence(sequence);
			sequencer.setTempoInBPM(220);
			sequencer.start();
		}
		catch (MidiUnavailableException mue)
		{
			mue.printStackTrace();
		}
		catch (InvalidMidiDataException imde)
		{
			imde.printStackTrace();
		}
	}

	private void createInstances() throws MidiUnavailableException, InvalidMidiDataException
	{
		sequencer = MidiSystem.getSequencer();
		sequence = new Sequence(Sequence.PPQ, 4);
		track = sequence.createTrack();
	}

	private void addEvents() throws InvalidMidiDataException
	{
		for (int t = 5; t < 100; t += 4)
		{
			int note = (int)((Math.random()*75) + 25);

			track.add(newEventToTrack(NOTE_ON, 1, note, 100, t));
			track.add(newEventToTrack(NOTE_CONTROLLER_EVENT, 1, EVENT_NUMBER, 0, t));
			track.add(newEventToTrack(NOTE_OFF, 1, note, 100, t + 2));
		}
	}

	private MidiEvent newEventToTrack(int command, int channel, int data1, int data2, long tick) throws InvalidMidiDataException
	{
		ShortMessage sm = new ShortMessage(command, channel, data1, data2);
		MidiEvent me = new MidiEvent(sm, tick);

		return me;
	}
}

Zmianie uległa w większości część bloku "try-catch", aczkolwiek wprowadziłem kilka dodatkowych rzeczy. Po pierwsze, kolejna zmienna stanowiąca licznik uderzeń na potrzeby eksperymentu, a także dwie stałe dla obsługi zdarzeń. Do tego jeszcze metoda dodająca zdarzenia w pętli "for" oraz wyrażenie lambda dla metody "addControllerEventListener". Wprowadziłem również metodę "setTempoInBPM" wpływającą na prędkość odtwarzania melodii.

W języku Java, MIDI wymaga kolejnych "bloków" dodawanych do ścieżki tak samo, jak przy tworzeniu melodii z tym, że teraz to traktuje jak sygnał kiedy dokładnie i pod jakimi warunkami wywołać metodę. W powyższym przykładzie zdarzenie jest dodawane w tym samym momencie kiedy następuje odtworzenie nuty, ale nie musi tak być. Możecie je dać w dowolnym innym miejscu. To akurat symuluje wywoływanie zdarzenia w tym samym momencie gdy występuje naciśnięcie klawisza.


To wszystko. Na tym kończę pisanie. Miło mi, że zaprezentowałem Wam takie możliwości jakie oferuje Java i MIDI. Jeśli zrozumieliście jak programować ciąg zdarzeń melodii, to bez wahania odpowiecie prawidłowo na pytanie ILE zdarzeń trzeba dodać dla zagrania jednej dodatkowej nuty.

PODOBNE ARTYKUŁY