poniedziałek, 5 sierpnia 2013

Od głupiego pomysłu do jeszcze głupszego programu, czyli simpleMonoComposer

Czasem zdarza się tak, że wpadniesz na totalnie głupi pomysł. Jest on na tyle głupi, że aż nie możesz sobie odpuścić zmarnotrawienia czasu na zrealizowanie go. Nawet jak będzie to coś totalnie bezużytecznego, to samo stworzenie tego da satysfakcję, że zrobiło się coś. Tak właśnie powstał simpleMonoComposer.
simpleMonoComposer w całej swojej okazałości

W dalszej części notki opisuję co ten program w ogóle robi, krótką historię jego powstania i prawdopodobnie najciekawszą część, czyli różne sprawy techniczne. Projekt na bitbucket (program wykonywalny oraz kod źródłowy):
[klik]


Ale o co w ogóle chodzi?

simpleMonoComposer, jak chciałem zaznaczyć w tytule programu, to bardzo proste narzędzie do komponowania monofonicznej muzyki. Bez większej głębi, bez większych możliwości, bez sensownego interfejsu. Wybierz dźwięk, ustaw jego parametry techniczne i dodaj do niezwykle nieczytelnego notatnika. Do tego opcje odegrania naszego dzieła (cóż, nie zawsze idealnie działa :) ) jak i jego eksportu do dwóch formatów, o których ostatnie, co można powiedzieć, jest to, że komukolwiek się to przyda. Ale taka miała być idea, stąd najlepiej przejść do następnego rozdziału tej opowieści, czyli...

...ale jak takie coś mogło w ogóle powstać? Po co?

Historia powstania jest co najmniej dziwna. Na studiach, na architekturze systemów komputerowych, zostałem zmuszony do opanowania assemblera MIPSa (link do wiki, gdyż zdaję sobie sprawę, że część osób mogłaby w tej chwili właśnie tam zajrzeć). Poza tym, jak robić proste programy na laboratoria, szybko zainteresowałem się, jak robić tam wszelkie inne, dziwne rzeczy, które prawdopodobnie nigdy w życiu mi się nie przydadzą. Wtedy to, w dokumentacji dołączonej do symulatora MARS (całkiem przyjemnego assemblera MIPS) znalazłem na liście przerwań obsługę MIDI. Tak mnie to zafascynowało, że stwierdziłem, że napiszę program, w którym będę mógł sobie wygenerować kod assemblera odgrywający melodyjki. Głownie dlatego, że nie chciało mi się tego ręcznie programować :) . Tak oto powstał simpleMonoComposer. Dodatkowa funkcja eksportu do kodu Turbo Pascala to już tylko i wyłącznie efekt mojego sentymentu do tego języka, napisana praktycznie na kolanie.

Drobne problemy techniczne

Niestety, symulator MARS zawiódł moje oczekiwania. Odtwarzenie MIDI jest tam bardzo niepłynne i należy to traktować tylko jako wybryk fantazji jego programistów, a nic sensownego, wartego uwagi. Dzięki temu, dalsze pisanie tego jako zwykłego programu, nie miało już dla mnie sensu. Stąd interfejs jest taką toporną tymczasówką zrobioną na szybko w Eclipsowym Swing Designerze.

Najciekawsze, czyli zagadnienia związane z kodem

Jak nadmieniłem wcześniej, GUI nie było moim najważniejszym celem przy pisaniu tego, stąd jego kod to jeden wielki śmietnik zafundowany przez Swing Designera i moje momentami kombinowanie z doprowadzeniem go do ładu. Pomijając ten kod, zastosowałem tu parę rzeczy, które jak sądzę mogą mi się kiedyś przydać, a możliwe, że także innym.

Wczytanie całości pliku do Stringa bez użycia pętli

Bardzo przyjemny trick, który swego czasu znalazłem na sieci w tym miejscu. Na tyle sprytne, że myślę, iż warto to powtórzyć tutaj:

text = new Scanner(activeFile,"UTF-8").useDelimiter("\\A").next();
gdzie text to zmienna typu String, a activeFile typu File. Cały myk polega na tym, że ustawiamy, aby nasz ciąg był rozdzielany znakiem specjalnym oznaczającym początek wejścia. Jako że początek plik tekstowy ma tylko jeden, to dostajemy całość pliku w jednym Stringu, bez używania jakichkolwiek pętli.
Inne zapożyczone rozwiązania pomijam, gdyż nie są aż takie wyjątkowo przydatne. Zostały one oznaczone w kodzie źródłowym.

Obliczenie częstotliwości dźwięku z MIDI

Gdy postanowiłem dopisać eksport do kodu Turbo Pascala, potrzebne było mi obliczenie częstotliwości konkretnego dźwięku. Jest to zagadnienie bardziej matematyczne niż programistyczne, jednak mimo to stwierdziłem, że warto je opisać. Kod, który odpowiada u mnie za konwersję, wygląda następująco:
private int convertPitchToFrequency(int pitch){
  double tmp;
  tmp=((double)pitch-69.0)/12;
  tmp=Math.pow(2,tmp)*440;
  return (int)Math.round(tmp);
 }
Jakby to stwierdził mój wykładowca od algorytmów, można by to zmieścić w samym returnie. Jednak, czasem dla czytelności wolę sobie pozwolić na nieco więcej linijek.
Wzór, który ta metoda wykonuje, prezentuje się w zapisie matematycznym następująco:
n - numer dźwięku w MIDI
Jest to dopasowany pod format MIDI wzór, który możemy znaleźć chociażby na Wikipedii. Względem oryginalnego wzoru została zmieniona wartość 49 na 69. Bierze się to stąd, że format MIDI obsługuje więcej dźwięków niż fortepian. Wzór odnosi się do dźwięku A4, który posiada częstotliwość 440 Hz. Na standardowej klawiaturze jest to 49. klawisz, a w MIDI jest on pod numerem 69.
Jak łatwo się domyśleć, wzór ten zwraca nam także wartości ułamkowe, a w funkcji obsługi PC Speakera w Turbo Pascalu muszę podać liczbę całkowitą. Stąd zaokrąglenie. Dla wielu osób nie powinno być to zauważalne ;) .

Odtworzenie dźwięku MIDI

Jest to ostatnia, ciekawsza rzecz w kodzie, którą chciałem zaprezentować. Co prawda temat ten znajdziemy w Java Tutorials, jednak moja wersja jest nieco bardziej rozbudowana. Wygląda następująco:

Synthesizer synth = MidiSystem.getSynthesizer();
ShortMessage setInstrument = new ShortMessage();
ShortMessage playNote = new ShortMessage();
ShortMessage stopNote = new ShortMessage();
synth.open();
Receiver receiver = synth.getReceiver(); 
setInstrument.setMessage(ShortMessage.PROGRAM_CHANGE,0,tmp.getInstrument(),0);
playNote.setMessage(ShortMessage.NOTE_ON,0,tmp.getPitch(),tmp.getVolume());
stopNote.setMessage(ShortMessage.NOTE_OFF,0,tmp.getPitch());
receiver.send(setInstrument, -1);
receiver.send(playNote, -1);
try {
 Thread.sleep(tmp.getDuration());
} catch (InterruptedException e) {
 e.printStackTrace();
} finally {
 receiver.send(stopNote, -1);
}
synth.close();
Teraz krok po kroku. W czterech pierwszych linijkach tworzymy sobie przydatne nam zmienne. Pierwsza będzie przechowywać używany przez nas syntezator - w tym celu posłużymy się głównym systemowym. Trzy kolejne to obiekty klasy ShortMessage - wysyłamy za ich pomocą informacje do syntezatora. synth.open() rozpoczyna działanie syntezatora. Teraz tylko musimy jakoś przesyłać do niego, co ma wykonywać. W tym celu wyciągamy do zmiennej receiver "odbiornik" naszego syntezatora. Następne trzy linie ustawiają nam wiadomości do syntezatora (kolejno): ustawienie instrumentu, rozpoczęcie grania dźwięku, zakończenie grania dźwięku. W przypadku simpleMonoComposera posiadam obiekt tmp klasy Note, która zwraca mi wartości MIDI ustawionego instrumentu (getInstrument()), wysokości dźwięku (getPitch()), jego głośności (getVolume()) i długości (getDuration()). W miejscach, gdzie użyłem te metody, do własnych celów należy podawać samemu wartości typu int odpowiadające za jedną z tych rzeczy. Dla przykładu, zbudujmy trzy właśnie omawiane linijki dla następującego dźwięku:
(źródło: Wikipedia)
W MIDI posiada on wartość 61. Przedział głośności w MIDI to 0-127, jednak załóżmy, że chcemy zagrać standardową głośnością, czyli 100. Do tego, odegrajmy go fortepianem, który w MIDI jest pod numerem 0. (tutaj lista wszystkich instrumentów w MIDI)
Kod będzie wyglądał wtedy następująco:
setInstrument.setMessage(ShortMessage.PROGRAM_CHANGE,0,0,0);
playNote.setMessage(ShortMessage.NOTE_ON,0,61,100);
stopNote.setMessage(ShortMessage.NOTE_OFF,0,61);
Dwie kolejne linie w kodzie to wysłanie do syntezatora dwóch pierwszych wiadomości - ustawienia instrumentu i odegrania dźwięku. Jednak cały trick działa w dużej mierze dzięki kolejnemu fragmentowi kodu. W try...catch...finally... zawiera się kod odpowiadający za wygranie nuty w interesującej nas długości. Metodą Thread.sleep(długość) ustawiamy zatrzymanie wykonywania wątku (w przypadku simpleMonoComposera - całego programu) na określoną w milisekundach długość czasu. Część catch nas tutaj prawdę mówiąc nie interesuje - zostawiłem domyślną akcję drukowania wyjątku, generowaną przez Eclipse. To, co nas interesuje, to część finally. Wykonuje się ona wtedy, gdy kod wpisany w try przeszedł bez wyrzuczenia wyjątku. Wysyłamy wtedy do syntezatora wiadomość o zatrzymaniu dźwięku. Ostatnia linijka to już czysta formalność, czyli zatrzymanie syntezatora. Należy o niej pamiętać - inaczej w przypadku, gdy odgrywamy tylko jeden dźwięk, będzie on brzmiał mimo zatrzymania odtwarzania.
Niestety, metoda usypiania wątku jest nieperfekcyjna - czasem zdarza mi się, że nuta jest wygrywana dłużej, niż być powinna. Jednak to już wspólna wina mojego komputera i Javy :) .

Podsumowując...

Mimo, że symulator MARS zawiódł moje oczekiwania związane z przerabianiem go w maszynkę do odgrywania muzyki, to jednak samo pisanie tego programu poduczyło mnie wielu rzeczy. Prawdopodobnie dowiedziałbym się nawet więcej, gdybym ostatecznie zdecydował się na obsługę formatu MIDI - czy to do importu, czy chociaż eksportu. Ciekawe dla mnie było to, że program od początkowych założeń tak się zmienił - nie miałem w ogóle w planach np. odtwarzania skomponowanej sekwencji. Po skończeniu programu uważam, że niestety jest to jego najlepiej działająca część.

A dodatkowym słowem zakończenia, coś co mnie bardzo zastanawiało, gdy myślałem nad napisaniem eksportu do MIDI - czemu typ byte w Javie przechowuje wartości z zakresu [-128,127], a nie [0,255]? I kto w ogóle wpadł na to, że to będzie dobry pomysł?!

Brak komentarzy:

Prześlij komentarz