Jason. Cała informatyka w jednym miejscu!

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 interfejs MIDI w języku Java dawał 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 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 (nie w sensie programistycznym), aby odtwarzać dźwięki. Dla początkujących może sprawiać problem zrozumienie budowy zapisu nutowego, gdyż "komputerowy" sposób widzenia w porównaniu z ludzkim będą się mocno między sobą 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 / góra 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, które są 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ń.

PRZYKŁAD KODU ŹRÓDŁOWEGO

Niżej przedstawiam kodową demonstrację przedstawionych do tej pory wywodów. 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 NOTE_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(newTrackEvent(NOTE_CHANGE_INSTRUMENT, 1, 6, 0, 1));
			track.add(newTrackEvent(NOTE_ON, 1, 44, 100, 1));
			track.add(newTrackEvent(NOTE_OFF, 1, 44, 100, 16));
			sequencer.setSequence(sequence);
			sequencer.start();
		}
		catch (MidiUnavailableException mue)
		{
			mue.printStackTrace();
		}
		catch (InvalidMidiDataException imde)
		{
			imde.printStackTrace();
		}
	}

	private MidiEvent newTrackEvent(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 obiekt "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 (wybieramy jeden z nich)
    1. "pulses per quarter", czyli "PPQ"
    2. "timecode" interfejsu MIDI kryjący 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.

ZANIM ZACZNIESZ SIĘ DZIWIĆ, ŻE NIE DZIAŁA...

Przed jakimikolwiek wywołaniami metod, koniecznie "otwórzcie" sekwenser za pomocą metody "open". MIDI w języku Java tego wymaga i zaniechanie tego skutkować będzie zgłoszeniem wyjątku.

DODAWANIE ZDARZEŃ

Dopiero od teraz zaczyna się realny 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 z nich wymaga dwóch kolejnych obiektów, o których już wspomniałem. Są to:

  • ShortMessage
    • określanie rodzaju zdarzenia na podstawie trzech parametrów: stałej liczby całkowitej, jakiego kanału ma to dotyczyć oraz jakie dane w postaci dwóch kolejnych parametrów mają być brane pod uwagę
  • MidiEvent
    • pobiera powyższy obiekt; decyduje 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, zatem to "wyłącza" nutę.
  • "NOTE_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).

Parę słów o "data1" i "data2". W dokumentacji są nazywane "bajtami danych". Dopuszczalny zakres jest w przedziale <0; 127>, jeśli chodzi o wprowadzane wartości. 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 Javie.

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

Skoro część programowania została wyjaśniona, 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 książkach możecie też natrafić na termin "callback" (wywołanie zwrotne). 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 ono 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, a także tablicę liczb całkowitych oznaczających, jakie liczby wstawiane w parametr oznaczony jako "data1" 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"

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) -> 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 = 4; 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" mająca za zadanie inkrementowanie licznika uderzeń wraz z wypisywaniem go na ekranie terminala. Wprowadziłem również metodę "setTempoInBPM" wpływającą na prędkość odtwarzania melodii. Wartości zmiennej iteracyjnej pętli wyglądają bardzo nietypowo, a to dlatego że kontrolują one z jaką częstotliwością kolejne nuty mają być odtwarzane (dlatego wzrost licznika o cztery). Gdy podzielicie stówkę na cztery, to wyjdzie Wam 25 nut do odtworzenia.

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 ma 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 gdybyście nacisnęli klawisz na pianinie.


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