Helilõigud, MIDI, saatehääl, kolmkõlad, kvanditud heli, stereo
Esimesest versioonist peale suudavad java programmid mängida au lihtsamas formaadis faile. Versioonist 1.2 on juurde liidetud ka muude (wav, midi) helifailitüüpide mängimise võimalus. Muusika mängimiseks tuleb luua AudioClip. Selle meetoditega play, loop ning stop saab panna klipi mängima ning mängimine katkestada. Loop tähendab, et klippi mängitakse pidevalt, s.t. pärast lõpetamist hakatakse mängimisega uuesti algusest peale.
import java.applet.*;
public class Muusika1 extends Applet{
public void start(){
AudioClip
lugu=getAudioClip(getCodeBase(), "spacemusic.au");
lugu.play();
}
}
Järgneva näite puhul hakatakse korduvalt mängima
lehele tulles ning lehelt lahkudes lõpetatakse mängimine.
import java.applet.*;
public class Muusika2 extends Applet{
AudioClip lugu;
public void init(){
lugu=getAudioClip(getCodeBase(), "spacemusic.au");
}
public void start(){
lugu.loop();
}
public void stop(){
lugu.stop();
}
}
Iseseisva programmi puhul saab klipi kätte klassi Applet
staatilise meetodi newAudioClip abil,
meetodi parameetriks on helifaili URL.
import java.applet.*;
import java.net.URL;
public class Muusika3a{
public static void main(String
argumendid[]) throws Exception{
AudioClip
lugu=Applet.newAudioClip(
new
URL("http://minitorn.tpu.ee/~jaagup/kool/java/"+
"abiinfo/tutorial/sound/example-1dot2/bottle-open.wav")
);
lugu.play();
Thread.sleep(5000);
}
}
Kui soovitakse mängida lugu samast masinast, siis
tuleb ka see kohalik aadress enne URLiks muuta.
import java.applet.*;
import java.net.URL;
public class Muusika3{
public static void main(String
argumendid[]) throws Exception{
AudioClip lugu=Applet.newAudioClip(
new
File("spacemusic.au").toURL()
);
lugu.play();
Thread.sleep(5000);
}
}
Lisaks varemvalminud lugude esitamisele saab ka ise meloodiaid kokku panna. MIDI abil saame määrata, millist nooti millal mängida. Salvestatud pillide helinäidete järgi luuakse selle tulemusena meie poolt küsitud hääl. Kui soovitakse samaaegselt kuulda mitme pilli meloodiat, tuleb need paigutada eri kanalitele. Iga kanal võib korraga teha ühe pilli häält ning harilikult on kanaleid kokku kuni 16.
Kanalist väljuvat heli saab kontrollida sinna saadetavate käskudega. Enamkasutatavad on noteOn ja noteOff. Esimene neist palub nooti mängima hakata, teine mängimise lõpetada. Meetod noteOn vajab kahte parameetrit: heli kõrgust ja valjust, noodi kõlamise lõpetamiseks piisab noodi numbrist. MIDI standardi järgi on igal pooltoonil oma number vahemikus 0-127. Esimese oktaavi C väärtuseks on näiteks 60, sealt saab siis vastavalt poole tooni kaupa üles ja allapoole arvutada. Valjust tähistab samuti number samast vahemikust. MIDI vahendid asuvad paketis javax.sound, mis kuulub standardvahendite hulka alates JDK versioonist 1.3. Operatsiooni muusikavahendite poole pöördumiseks saab kasutada klassi MidiSystem.
Järgnevas näites küsitakse selle klassi kaudu helitekitamise seade ehk süntesaator ning avatakse. Viimase käest küsitakse tema külge kuuluv kanalite massiiv ning sealt omakorda kanal nr. 0. Järgnevalt palun vastaval kanalil mängida A nooti (noot nr. 69) valjusega 65. Ootan sekundi ning siis lasen noodi kõlamise lõpetada. System.exit viimase käsuna on tarvilik, kuna MIDI vahendite tarvitamisega virtuaalmasina poolt loodud lõim ei oska nootide lõpuga oma tööd lõpetada ning programm jääks viimasele reale rippuma. Analoogiline olukord on ka graafikakomponentide juures, kus programmi töö lõpetamiseks tuleb kirjutada System.exit(0).
import javax.sound.midi.*;
public class Noot{
public static void main(String argumendid[]) throws Exception{
Synthesizer synthesizer=MidiSystem.getSynthesizer();
synthesizer.open();
MidiChannel kanal=synthesizer.getChannels()[0]; //kanal 0;
int korgus=69; //A
int valjus=65; //keskmine
kanal.noteOn(korgus, valjus);
Thread.sleep(1000);
kanal.noteOff(korgus);
System.exit(0);
}
}
Kromaatilist heliredelit võib mängida tsükli abil:
for(int i=40; i<120; i++){
kanal.noteOn(i, 60);
Thread.sleep(200);
kanal.noteOff(i);
}
Kui soovida, et samal kanalil mängiks korraga mitu nooti, tuleb lihtsalt ükshaaval määrata, millised helikõrgused peavad kõlama. Kõikide mängimise saab korraga lõpetada käsuga allNotesOff().
Kuigi MIDI puhul öeldakse helikõrgus numbriga ette, on ka siin võimalik toonil ujuda lasta. Seda saab käsuga setPitchBend, andes ette numbri, palju kõrgust muuta. Vaikimisi väärtuseks on 8192, sellisel juhul vastab noodi number tema helikõrgusele. Iga number sellest ülespoole viib helikõrgust kõrgemale, allapoole aga madalamaks. Vaikiva kokkuleppe järgi tähistab number 0 tooni võrra madalamat ning 16363 tooni jagu kõrgemat heli, kuid see kõikumise piirkond võib ka erineda. Järgnevas näites peaks tooni ülalt alla ujumine kuulda olema.
kanal.noteOn(60, 70);
kanal.noteOn(64, 70);
for(int korgus=16383; korgus>0; korgus-=500){
Thread.sleep(200);
kanal.setPitchBend(korgus);
}
kanal.allNotesOff();
Kanalil mängivat instrumenti saab muuta käsuga programChange, andes parameetritena ette uue pilli helipanga ning panga sees sellele pillile vastava programmijupi järjenumbri. Süntesaatorile kättesaadavad pillid saab küsida getDefaultSoundbank().getInstruments() abil. All näites paiknevad trükitakse tsüklis järgemööda välja pillide nimed ning mängitakse igal pillil noot.
Instrument[] pillid=synthesizer.getDefaultSoundbank().
getInstruments();
MidiChannel kanal=synthesizer.getChannels()[0];
for(int i=0; i<pillid.length; i++){
System.out.println(i+": "+pillid[i]);
kanal.programChange(pillid[i].getPatch().getBank(),
pillid[i].getPatch().getProgram());
kanal.noteOn(60, 50);
Thread.sleep(500);
kanal.noteOff(60);
}
osa väljundist:
12: Instrument
Marimba (bank 0 program 12)
13: Instrument
Xylophone (bank 0 program 13)
14: Instrument
Tubular Bell (bank 0 program 14)
15: Instrument
Dulcimer (bank 0 program 15)
16: Instrument
Hammond Organ (bank 0 program 16)
Kanalitele käske andes saab edukalt mängida programmi sees üksikuid noote ja luua kõlaefekte, kuid meloodiate ja viisijuppide esitamiseks on loodud täiendavad abivahendid: sekventser, sekventsid ja rajad. Rajal (track) on kirjas saadetavad teated koos ajatemplitega. Ühele rajale võib kanda näiteks ühe pillimehe mängitavad noodid. Sekvents on radade kogumik (nagu partituur). Sekventser on programmilõik, kes sekventsi kirjutatud käsklused õigel ajal süntesaatorile heli tekitamiseks edasi saadab.
Kanalile saadetava teate hoidmiseks on ShortMessage. Teate sisuks võib olla nii noodi mängimise algus, selle lõpp, kanalil oleva instrumendi vahetus kui muudki, näiteks klahvivajutuse tugevuse muutus. Rajale lisamiseks tuleb teatele ümber panna MidiEvent, mis lisab sinna aja – MIDI ajaühikutes mõõdetava vahemiku alates raja algushetkest. Esimeseks teateks kanalil peab olema PROGRAM_CHANGE, selle abil määratakse, millise pilliga hakatakse vastaval kanalil häält tegema. Siin näites määratakse kanalile 0 pill number 16, ehk praeguse helipanga järgi Hammondi orel.
Sekventsi puhul tuleb määrata, milliselt seal aega arvatakse: kas kaadrite või löökide järgi. Esimest võimalust kasutatakse video kõrvale heli loomiseks, teist nootide ja taktide järgi lugu seades, viimast tähistab Sequence.PPQ nagu siin näites. Käsk new Sequence(Sequence.PPQ, 4) tähistab, et luuakse nootidepõhine sekvents, mille iga löök (ehk veerandnoot 2/4, 3/3 ja 4/4 taktimõõdus) jagatakse neljaks tiksuks. Tiks on vähim ajaühik MIDI mõõdustikus, seega sellise jaotuse korral on võimalik lühimaks kestuseks panna kuueteistkümnendiknoodid. Kui tahta näiteks ka kolmekümnekahendikke kasutada, siis tuleks algselt veerandnoot mitte neljaks vaid kaheksaks tiksuks jagada.
Enne rajale sündmuste lisamist tuleb rada luua sekventsi käsuga createTrack(). Tulemuse kuulamiseks on vaja MidiSystem'i käest küsida sekventser, see avada. Siis määrata, et sekventseri poolt mängitavaks sekventsiks oleks (praegu meie üherajaline) sekvents ning käsuga start panna sekventser tööle.
import javax.sound.midi.*;
public class Rada1{
public static void main(String argumendid[]) throws Exception{
ShortMessage lahti = new ShortMessage();
ShortMessage kinni = new ShortMessage();
ShortMessage algus = new ShortMessage();
algus.setMessage(ShortMessage.PROGRAM_CHANGE, 0, 16, 0);
lahti.setMessage(ShortMessage.NOTE_ON, 0, 65, 93);
kinni.setMessage(ShortMessage.NOTE_OFF, 0, 65, 93);
Sequence sequence=new Sequence(Sequence.PPQ, 4);
Track track=sequence.createTrack();
track.add(new MidiEvent(algus, 0));
track.add(new
MidiEvent(lahti, 0));
track.add(new MidiEvent(kinni, 4));
track.add(new MidiEvent(lahti, 8));
track.add(new MidiEvent(kinni, 11));
track.add(new MidiEvent(lahti, 12));
track.add(new MidiEvent(kinni, 15));
track.add(new MidiEvent(lahti, 16));
track.add(new MidiEvent(kinni, 31));
Sequencer sequencer=MidiSystem.getSequencer();
sequencer.open();
sequencer.setSequence(sequence);
sequencer.start();
}
}
Loodud sekvetserile saab seada mitmeid parameetreid, samuti küsida andmeid mängitava loo kohta. Kui tahan määrata, et loo mängimise kiirus on 40 lööki minutis, siis tuleb kirjutada
sequencer.setTempoInBPM(40);
Kuna ennist olin määranud, et ühes löögis on 4 tiksu, siis eelmainitud reast järeldub, et igas minutis on 4*40 ehk 160 tiksu ning ühe tiksu pikkuseks on 60/160 sekundit. Analoogiliselt on võimalik määrata, millisest tiksust alates edasi mängitakse. Sekventseri käest saab küsida mängimise kiirust, temas sisalduva sekventsi ehk lõigu pikkust tiksudes, parasjagu mängitava tiksu numbrit ja muudki.
Kui soovin, et lõiku mitu korda järjest mängitaks, siis tuleb sekventserile panna külge kuular, mis lõputeate saabumisel paluks lõiku taas otsast mängima hakata. Piiritleja final on sekventseri ette toodud seetõttu, et teda saaks kohaliku muutujana sisemises klassis kasutada. Final ei luba muutujal vahetada isendit millele osutada (st., sellele muutujale ei saa anda uut väärtust). Muul juhul võiks juhtuda, et vahepeal pannakse sekventserile uus väärtus ning sisemise klassi sees lükatakse vale sekventser käima. Kui anonüümses sisemises klassis kasutada isendi (või klassi) tarvis kirjeldatud muutujat, siis seda probleemi ei teki.
Lõigu lõppu näitab kuularisse saabuv teade tüübiga nr. 47. Kui seepeale palun sekventseril uuesti otsast mängima hakata, siis tundub kasutajale, nagu lugu kordaks end järjepanu.
final Sequencer
sequencer=MidiSystem.getSequencer();
sequencer.open();
sequencer.setSequence(sequence);
sequencer.start();
sequencer.addMetaEventListener(new MetaEventListener(){
public void meta(MetaMessage m){
//kui lugu läbi, siis alustatakse uuesti
if(m.getType()==47)sequencer.start();
}
});
Kui soovida, et sekventsi kordamisel erinevalt mängitaks, siis võib sinna panna mitu rada ning igal korral määrata, milliseid radasid mängitakse, milliseid mitte. Sekventseri käsu setTrackMute abil saab määrata, kas rada on tumm või mitte. Kui soovitakse, et ainult üks rada mängiks ning teised oleksid vait, siis tuleb see rada panna soleerima käsu setTrackSolo abil.
... on lihtne, sest vastavad vahendid on küllaltki valmiskujul kättesaadavad. Kui arvutis on MIDI väljund (helikaardi kaudu) kõlaritesse või kõrvaklappidesse saadetud ning muusikafail ilusti kettal olemas, siis peaks all näha oleva nelja käsu abil olema võimalik etteantud nimega failist muusikat kuulata. Klassi MidiSystem käsk getSecuence võimaldab sekventsi lugeda failist (või mõnest muust voost). Kui avatud sekventser määrata vastavat sekventsi mängima, siis võimegi kuulata, mis faili salvestatud on.
import javax.sound.midi.*;
public class Midimangija1 {
public
static void main(String argumendid[]) throws Exception{
Sequencer sekventser=MidiSystem.getSequencer();
sekventser.open();
sekventser.setSequence(MidiSystem.getSequence(new
java.io.File("koduke.mid")));
sekventser.start();
}
}
MIDI failis paiknevate andmete kohta võib muudki teada saada: andmete pikkust mikrosekundites ning tiksudes, radade arvu ning teated ükshaaval radade kaupa. Rajalt saab käsuga get kätte järjekorranumbri järgi MidiEvent'i. Sealt edasi getMessage väljastab teate sisu ning getTick tiksu (ajahetke), millal vastav teade süntesaatorile saadetakse. Tuleb vaadata, millist tüüpi teatega on tegemist ning vastavalt käituda. Helidega seotud teated on tüübist ShortMessage. Iga teate juures on täisarvuna kirjas kanal, käsklus ning kaks teatebaiti. Nende baitide interpretatsioon sõltub sellest, millise käsuga on tegemist.
import javax.sound.midi.*;
import java.util.*;
public class Midimangija2 {
public
static void main(String argumendid[]) throws Exception{
Sequence sekvents=MidiSystem.getSequence(new
java.io.File("koduke.mid"));
System.out.println(sekvents.getDivisionType()+" pikkus:
"+sekvents.getMicrosecondLength()/1000000.0+
" sekundit, "+sekvents.getTickLength()+" tiksu");
Track[]
rajad=sekvents.getTracks();
System.out.println(rajad.length+" rada");
for(int
nr=0; nr<rajad.length; nr++){
System.out.println("Rada "+nr);
for(int t=0; t<rajad[nr].size(); t++){
MidiEvent me=rajad[nr].get(t);
long tiks=me.getTick();
MidiMessage teade=me.getMessage();
if(teade instanceof ShortMessage){
ShortMessage sm=(ShortMessage)teade;
System.out.println(tiks+" ShortMessage "+
"kanal: "+sm.getChannel()+
" teade: "+sm.getCommand()+
//näiteks noteOff
" bait1: "+sm.getData1()+
//kõrgus
" bait2: "+sm.getData2()
//valjus
);
}
if(teade instanceof MetaMessage){
MetaMessage mm=(MetaMessage)teade;
System.out.print(tiks+" MetaMessage"+
" tyyp: "+mm.getType());
byte[] b=mm.getData();
for(int i=0; i<b.length; i++)System.out.print(" "+b[i]);
System.out.println();
}
}
}
}
}
/*
Väljund:
0.0 pikkus: 15.5 sekundit, 2976 tiksu
2 rada
Rada 0
0 MetaMessage tyyp: 88 4 2 24 8
0 MetaMessage tyyp: 89 0 0
0 MetaMessage tyyp: 81 7 -95 32
2976 MetaMessage tyyp: 47
Rada 1
0 ShortMessage kanal: 0 teade: 192 bait1: 1 bait2:
0
0 ShortMessage kanal: 0 teade: 176 bait1: 7 bait2:
80
0 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 80
96 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 0
96 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 80
192 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 0
192 ShortMessage kanal: 0 teade: 144 bait1: 62
bait2: 80
288 ShortMessage kanal: 0 teade: 144 bait1: 62
bait2: 0
288 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 80
384 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 0
384 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 80
480 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 0
480 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 80
576 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 0
576 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 80
672 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 0
768 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 80
864 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 0
864 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 80
960 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 0
960 ShortMessage kanal: 0 teade: 144 bait1: 62
bait2: 80
1056 ShortMessage kanal: 0 teade: 144 bait1: 62
bait2: 0
1056 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 80
1152 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 0
1152 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 80
1248 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 0
1248 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 80
1344 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 0
1344 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 80
1440 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 0
1536 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 80
1632 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 0
1632 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 80
1728 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 0
1728 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 80
1824 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 0
1824 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 80
1920 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 0
1920 ShortMessage kanal: 0 teade: 144 bait1: 65
bait2: 80
2016 ShortMessage kanal: 0 teade: 144 bait1: 65
bait2: 0
2016 ShortMessage kanal: 0 teade: 144 bait1: 65
bait2: 80
2112 ShortMessage kanal: 0 teade: 144 bait1: 65
bait2: 0
2112 ShortMessage kanal: 0 teade: 144 bait1: 65
bait2: 80
2208 ShortMessage kanal: 0 teade: 144 bait1: 65
bait2: 0
2304 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 80
2400 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 0
2400 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 80
2496 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 0
2496 ShortMessage kanal: 0 teade: 144 bait1: 62
bait2: 80
2592 ShortMessage kanal: 0 teade: 144 bait1: 62
bait2: 0
2592 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 80
2688 ShortMessage kanal: 0 teade: 144 bait1: 64
bait2: 0
2688 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 80
2784 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 0
2784 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 80
2880 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 0
2880 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 80
2976 ShortMessage kanal: 0 teade: 144 bait1: 60
bait2: 0
2976 MetaMessage tyyp: 47
*/
Nagu näha, seda MIDI faili loonud programm on kasutanud noodi kõlamise alustamiseks ning lõpetamiseks sama käsku (NOTE_ON, 144), vaid mängimise lõpetamise puhul on noodile määratud valjus 0. Kanalile 0 pole muusika kõlamisega seotud noote pandud.
Sekventsi võib faili kirjutada ühe käsuga, andes ette MIDI formaadi (0 või 1) ning voo, kuhu andmed saata.
MidiSystem.write(sekvents, 1, new FileOutputStream("koduke2.mid"));
Nii võib rahumeeli lasta programmil loo välja mõtelda või kasutaja käest küsida ning siis kettale salvestada, et seda edaspidi kuulata või uuesti muuta. All näites on võetud ette lugu (koduke.mid), sellele lisatud rada ning algse viimase (siin ainukese) raja nootide järgi pandud uuele rajale samad noodid väikese tertsi (3 pooltooni) jagu allapoole. ShortMessage'd on ümber arvutatud, muud muutumatuna üle kantud.
import javax.sound.midi.*;
import java.io.*;
public class Midimangija3 {
public
static void main(String argumendid[]) throws Exception{
Sequence sekvents=MidiSystem.getSequence(new
File("koduke.mid"));
Track
algrada=sekvents.getTracks()[sekvents.getTracks().length-1]; //viimane rada
Track
uusrada=sekvents.createTrack();
int
nihe=-3;
for(int
nr=0; nr<algrada.size(); nr++){
MidiEvent me=algrada.get(nr);
if(me.getMessage() instanceof ShortMessage){
ShortMessage sm=(ShortMessage)me.getMessage();
ShortMessage sm2=new ShortMessage();
if(sm.getCommand()==ShortMessage.NOTE_ON ||
sm.getCommand()==ShortMessage.NOTE_OFF){
sm2.setMessage(
sm.getCommand(), sm.getChannel(), sm.getData1()+nihe, sm.getData2()
);
}
else {
sm2=(ShortMessage)sm.clone();
}
uusrada.add(new MidiEvent(sm2, me.getTick()));
}
else {
uusrada.add(me);
}
}
MidiSystem.write(sekvents, 1, new
FileOutputStream("koduke2.mid"));
}
}
Küllap olete näinud ja kuulnud süntesaatorit lugusid saatmas. Mõni koputab lihtsalt rütmi taha, teisele tuleb ette anda helistik ning selle peale mängitakse saateakordi. Leidub ka selliseid saateprogramme, mis vastavalt etteantud helistikule ise saatenoote juurde mõtlevad või lisaks sellele juba mängitava viisi pealt harmoonia ennustavad. Viimase võimaluse jätame siin välja, kuid juurde kuuluva pikema näite ja seletuse läbi töötanud lugeja peaks mõningase pusimise järel esimese kolme osaga küll hakkama saama.
Näites mängitakse saadet Juhansonide Unenäo laulule. Kasutaja saab määrata helistiku ning kuulata bassi, akorde ning taustaks mängitavat rahvalikku viiulipartiid, samuti määrata tempot. Koodi lühiduse ja kompaktsuse huvides pole viisi lisatud, kuid olles seletustest aru saanud, peaks see täiesti jõukohane olema.
Üheaegselt mängitavad rajad luuakse valmis ja pannakse kokku üheks sekventsiks. Rajad, mille heli parajasti kuulda ei soovita, pannakse vaikima. Kuna ühes helistikus mängimisel piirdub lugu kolme duuriga, siis arvutatakse nii bassi kui akordi tarvis välja rajad kõigi kolme duuri jaoks, kuid kõlada lastakse vaid siis, kui kasutaja vastavat instrumenti soovib kõlamas kuulda ning parajasti mängitav duur ja rada kokku langevad. Nii pääseb pidevast nootide arvutamisest ning uued rajad tuleb luua vaid helistiku vahetamisel. Vaid viiulipartii luuakse iga takti algul uuesti, et see tunduks värske ja kordumatuna.
Bassi partii on kõige lihtsam, sestap alustan selle loomise seletamisest. Takti jooksul on bassipartiis vaid üks noot, mängitava akordi esimene aste, mis kõlab kogu takti jooksul. Takt koosneb kolmest löögist, iga löök neljast tiksust (see määrati sekventsi loomisel). Nii peab bass alustama kõlamist tiksust 0 ning lõpetama selle viimase tiksu järel ehk kaheteistkümnenda tiksu alguses. Selle ühe noodi kõlamiseks on vaja rajale kolme teadet: tuleb määrata kanalil kõlama hakkav pill, kõlamise algus ja lõpp. Bass pannakse mängima etteantud põhinoodist 2 oktaavi ehk 24 pooltooni allpoolt.
void looBass(Track t, int toonika){
try{
Kõigepealt tehakse tühjad teated valmis
ShortMessage m[]=new ShortMessage[3];
for(int
i=0; i<m.length; i++){
m[i]=new ShortMessage();
}
seejärel antakse neile väärtused
m[0].setMessage(ShortMessage.PROGRAM_CHANGE, 1, 39, 0);
m[1].setMessage(ShortMessage.NOTE_ON, 1, toonika-24, 60);
m[2].setMessage(ShortMessage.NOTE_OFF, 1, toonika-24, 60);
ning lõpuks pannakse rajale, lisades juurde, mitmenda tiksu juures nad aktiivseks peavad muutuma.
t.add(new
MidiEvent(m[0], 0));
t.add(new MidiEvent(m[1], 0));
t.add(new MidiEvent(m[2], 12));
}catch(Exception e){e.printStackTrace();}
}
Kolmkõla loomine toimub tehniliselt sarnaselt bassiga võrrelduna. Kui bassi partii koosnes takti jooksul vaid ühest noodist, siis kolmkõla tarvis kõlavad takti jooksul kolm nooti. Esimesel löögil alustatakse põhinoodiga, teisel kolmanda ning kolmandal viienda astmega. Kolmanda löögi lõpus lõpetatakse kõikide nootide kõlamine. Endise kolme teate asemel on nüüd vaja seitset: instrumendi määramiseks ning kõigi kolme noodi kõlamise alustamiseks ja lõpetamiseks.
Et võiks korraga välja arvutada ühe helistiku juures vaja minevad kolmkõla- ning bassirajad, selleks sai loodud meetod, millele antakse ette toonika ning mis väljastab sekventsi kummagi esituse radadega esimese, neljanda ning viienda astme tarvis. Kolmkõlade juures võetakse nii neljas kui viies aste (vastavalt viis ja seitse pooltooni) toonikast üles, bassi puhul alla. Tegemist on täiesti sisetundega, milliselt poolt võtta, kuid siin tundus, et helilõigu keskele sobib sügav mahlakas bass paremini ning lõpus kõrgemas toonikas justkui saabub rahu ja tasakaal. Põhjalikumate saadete puhul tuleb leidmise algoritm tõenäoliselt keerulisemaks teha, et arvestataks ilusti kõlavaid absoluutkõrgusi ning miks mitte ka viisi kulgemist.
Sequence pohikolmkolad(int toonika) throws
InvalidMidiDataException{
Sequence
s1=new Sequence(Sequence.PPQ, 4);
looKolmkola(s1.createTrack(), toonika);
looKolmkola(s1.createTrack(), toonika+5); //kvart
looKolmkola(s1.createTrack(), toonika+7); //kvint
looBass(s1.createTrack(), toonika);
looBass(s1.createTrack(), toonika-7); //IV ehk kvint alla
looBass(s1.createTrack(), toonika-5);
return
s1;
}
Et parasjagu valitud helistiku põhikolmkõladeks saatehääled kätte saada, ka selleks on eraldi — kuigi lühike — alamprogramm.
Sequence
looSekvents() throws InvalidMidiDataException{
return
pohikolmkolad(helikorgused[helistik.getSelectedIndex()]);
}
Nime all helistik peitub valik ning helistik.getSelectedIndex() annab järjekorranumbri, mitmenda kasutaja oli loetelust valinud. Massiivis helistikud on tähtedega määratud, mitmendal kohal mingi helistik asetseb ning massiiv helikorgused näitab vastavate helistike toonikate asukohad MIDI skaalal. Sõnes jooksevHelistik hoitakse meeles viimati valitud helistiku nimi, et edaspidi oleks võrdlemisel teada, millal kasutaja on helistikku vahetanud, et oleks põhjust saatesekvents uuesti arvutada.
JComboBox
helistik=new JComboBox();
String[]
helistikud={"Bb", "F", "C", "G",
"D", "A", "E"};
int[]
helikorgused={58, 53, 60, 55, 50, 57, 52};
String
jooksevHelistik="";
Mängitav sekvents ning mängiv sektventser,
Sequence
sequence;
Sequencer
sequencer;
graafilised vahendid töö juhtimiseks,
JButton
nupp=new JButton("Mängi");
JCheckBox
ruut=new JCheckBox("Korda");
JCheckBox
bass=new JCheckBox("Bass");
JCheckBox
akord=new JCheckBox("Akord");
JCheckBox
taust=new JCheckBox("Taust");
JRadioButton[] raadionupud=new JRadioButton[3];
String[]
raadionupustring={"I", "IV", "V"};
JScrollBar tempo=new JScrollBar(JScrollBar.HORIZONTAL, 190, 5, 40, 320);
rada viiulipartii paigutamiseks.
Track
muutuvRada;
Diatoonilise duuri astmetele vastavate pooltoonide arv, et hiljem oleks võimalik määrata, mitmendale astmele liikuda ning see arvutile pooltoonides selgeks teha.
static final int[] noodivahed={-1, 0, 2, 4, 5, 7,
9, 11, 12};
//toonika
kohal 1 väärtus 0
Konkreetse loo duuride järjestus.
int[]
duurid={0, 1, 2, 0, 1, 2, 0, 0}; //
Juhansonide unenäo laul
// 0- toonika, 1-IV, 2-V
int
duurinr=0; //mängitava takti järjekorranumber
Taustaks mängib viiul lihtsa algoritmi järgi: alustatakse kõlava duuri esimeselt astmelt ning liigutakse iga noodiga sellelt ühe võrra kas üles või alla juhuslikult võetuna. Sellisena satub rõhuline noot kokku kõlava akordiga ning taustaks kõlab ühtlane meloodiline saagimine. Veidi parandades ning ebaloogilisi järske üleminekuid vältides annaks loomulikkust suurendada, kuid ka sellisel kujul ei riiva hullusti kõrva ning viiulimängu algusest alustades tuleks tükk harjutada, et selliselegi tulemusele jõuda.
Et algoritm arvestab diatooniliste astmetega, MIDI aga loeb kõik pooltoonid samaväärseteks siis on loodud ümberarvutamiseks eraldi alamprogramm, millele antakse ette algne aste ja soovitav nihe ning mis väljastab vahe pooltoonides, lubades ka ühe oktaavi piirest välja nihkuda.
static int
pooltoonid(int aste, int muutus){
int
loppaste=aste+muutus;
int okt=loppaste/7;
if(loppaste<0)okt--;
loppaste=loppaste-7*okt;
int
vahe=noodivahed[loppaste]-noodivahed[aste];
return
vahe+12*okt;
}
Meetodile enesele antakse ette rada, kuhu teated panna, toonikanoodinumber ning aste, millelt mängimist alustada.
void
looTaustaviiul(Track t, int toonika, int aste){
try{
Kõigepealt luuakse mäluruum arvutustulemuste paigutamiseks soovitud arvu leitud nootide tarvis,
int
nootidearv=6;
int[]
samm=new int[nootidearv];
int
i=0;
samm[i++]=0;
siis arvutatakse eelpool kirjeldatud algoritmi järgi väärtused nootidele, kusjuures iga järgmine asub eelmise kõrval
while(i<nootidearv){
if(Math.random()<0.5){
samm[i]=samm[i-1]-1;
} else
{
samm[i]=samm[i-1]+1;
}
i++;
}
ning lõpuks asendatakse leitud astmed MIDI teadetega. Ette nagu ikka pilli määramine, sedakorda kanalile number 2.
ShortMessage m=new ShortMessage();
m.setMessage(ShortMessage.PROGRAM_CHANGE, 2, 41, 0);
t.add(new MidiEvent(m, 0));
for(i=0;
i<nootidearv; i++){
m=new
ShortMessage();
Iga helikõrguse arvutamiseks liidetakse kokku helistiku põhikõrgus (toonika), pooltoonide arv kõlava duuri põhitoonini jõudmiseks (noodivahed[aste]) ning takti jooksul sellest nihkutud astmete arv pooltoonidena (pooltoonid(aste, samm[i])).
m.setMessage(ShortMessage.NOTE_ON, 2,
toonika+noodivahed[aste]+pooltoonid(aste, samm[i]), 60);
t.add(new MidiEvent(m, i*2));
m=new
ShortMessage();
m.setMessage(ShortMessage.NOTE_OFF, 2,
toonika+noodivahed[aste]+pooltoonid(aste,
samm[i]), 60);
t.add(new MidiEvent(m, (i+1)*2));
}
}catch(Exception e){e.printStackTrace();}
}
Hoolitsemaks, et mängitaks vaid nendel radadel, mille kuulamist kasutaja ootab, tuleb ülejäänud vaikima määrata. Sobivat duuri näitab raadionupp. Siin näites määrab selle valitu programm, kuid kergesti võib ka inimesel lasta määrata, mitmenda astme kolmkõla ta soovib saateks kuulata.
void paneSoolo(){
for(int
i=0; i<raadionupud.length; i++){
sequencer.setTrackMute(i,
!raadionupud[i].isSelected() || !akord.isSelected());
sequencer.setTrackMute(3+i, !raadionupud[i].isSelected() ||
!bass.isSelected());
}
Taustaviiul jäetakse kõlama vaid siis, kui vastav ruut on märgitud.
sequencer.setTrackMute(6, !taust.isSelected());
}
Iga uue takti mängimisel tuleb teha mõned ettevalmistused.
void
alusta(){
try{
Kui kasutaja on helistikku vahetanud, tuleb kogu sekvents uuesti arvutada.
if(!jooksevHelistik.equals(helistik.getSelectedItem())){
sequence=looSekvents();
jooksevHelistik=helistik.getSelectedItem().toString();
}
Kui tegemist pole loo algusega, siis tuleb kustutada eelmises taktis mängitud viiulipartii
if(muutuvRada!=null)sequence.deleteTrack(muutuvRada);
ning luua uus rada uue partii paigutamiseks.
muutuvRada=sequence.createTrack();
int
pohitoon=helikorgused[helistik.getSelectedIndex()];
int
aste=1;
if
(duurid[duurinr]==1)aste=4;
if
(duurid[duurinr]==2)aste=5;
ja arvutada sinna noodid.
looTaustaviiul(muutuvRada, pohitoon, aste);
sequencer.setSequence(sequence);
Mängitava duuri raadionupp märgistada,
raadionupud[duurid[duurinr]].setSelected(true);
leida järgmise korra jaoks järgneva takti number või suunata lugu algusse tagasi
duurinr=(duurinr+1)%duurid.length;
ning lõpuks väljundisse lugu mängima hakata.
mangi();
}catch(Exception e){e.printStackTrace(); System.out.println(e);}
}
Mängimise tarvis tuleb
void mangi(){
kõigepealt sekventser tööle panna
sequencer.start();
alles seejärel saab määrata, millised rajad vaikivad
paneSoolo();
ning kui kiiresti muusika liigub.
sequencer.setTempoInBPM(tempo.getValue());
}
Siia meetodisse saadetakse teade sekventsi lõppemise kohta. Kui kasutaja soovib jätkamist, hakatakse uuesti otsast peale, muul juhul lõpetatakse mängimine ning vabastatakse ressursid teiste programmide tarvis.
public
void meta(MetaMessage m){
if(m.getType()==47 && ruut.isSelected()){
alusta();
} else
{
sequencer.close();
}
}
Töölesaamise käivitusprogramm on ikka standardne. Tuleb vaid hoolitseda, et init-meetod käima pandaks. Rakendina käivitamisel jääb siinne staatiline meetod sootuks täitmata. Sõltuvalt masina jõudlusest ja konfiguratsioonist võib mõnikord kohalikult kettalt iseseisva programmi või rakendina siinse saateprogrammi käivitamine sujuvama tulemuse ande kui üle võrgu tööle panek, sest sel juhul kipub vähemalt aeglasemate masinate korral õiguste kontrolliks kuuldavalt aega kuluma.
public
static void main(String argumendid[]){
JFrame
f=new JFrame("Saade");
Noodirakend9a ap=new Noodirakend9a();
ap.init();
f.getContentPane().add(ap);
f.pack();
f.setVisible(true);
}
Siit lehekülgedelt aga peaks olema võimalik leida piisavalt ideid ja soovitusi süntesaatori programmeerimiseks, et mõningane maitse suhu saada ning muusikateadmisi ja keerulisematel juhtudel ka raamatuid appi võttes saaks midagi täiesti kasutatavat kokku panna.
import javax.sound.midi.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
public class Noodirakend9a extends JApplet
implements ActionListener, MetaEventListener{
Sequence
sequence;
Sequencer
sequencer;
JButton
nupp=new JButton("Mängi");
JCheckBox
ruut=new JCheckBox("Korda");
JCheckBox
bass=new JCheckBox("Bass");
JCheckBox
akord=new JCheckBox("Akord");
JCheckBox
taust=new JCheckBox("Taust");
JComboBox
helistik=new JComboBox();
JRadioButton[] raadionupud=new JRadioButton[3];
String[]
raadionupustring={"I", "IV", "V"};
JScrollBar tempo=new JScrollBar(JScrollBar.HORIZONTAL, 190, 5, 40, 320);
Track
muutuvRada;
String[]
helistikud={"Bb", "F", "C", "G",
"D", "A", "E"};
int[]
helikorgused={58, 53, 60, 55, 50, 57, 52};
String
jooksevHelistik="";
static
final int[] noodivahed={-1, 0, 2, 4, 5, 7, 9, 11, 12}; //toonika kohal 1 väärtus 0
int[]
duurid={0, 1, 2, 0, 1, 2, 0, 0}; //
Juhansonide unenäo laul
// 0- toonika, 1-IV, 2-V
int
duurinr=0; //takti järjekorranumber
void
looKolmkola(Track t, int toonika){
try{
ShortMessage m[]=new ShortMessage[7];
for(int
i=0; i<m.length; i++){
m[i]=new ShortMessage();
// Tühjad teated valmis
}
m[0].setMessage(ShortMessage.PROGRAM_CHANGE, 0, 25, 0); //pill nr 25 rajale 0
m[1].setMessage(ShortMessage.NOTE_ON, 0, toonika, 60);
m[2].setMessage(ShortMessage.NOTE_ON, 0, toonika+4, 60); //terts rajale
0 valjusega 60
m[3].setMessage(ShortMessage.NOTE_ON, 0, toonika+7, 60); //kvint
m[4].setMessage(ShortMessage.NOTE_OFF, 0, toonika, 60);
m[5].setMessage(ShortMessage.NOTE_OFF, 0, toonika+4, 60);
m[6].setMessage(ShortMessage.NOTE_OFF, 0, toonika+7, 60);
t.add(new MidiEvent(m[0], 0));
for(int
i=1; i<4; i++){
t.add(new MidiEvent(m[i], (i-1)*4));
// nelja tiksu (ühe löögi) tagant
} //
noodid sisse
for(int
i=4; i<7; i++){
t.add(new MidiEvent(m[i], 12));
// 12nda tiksu juures vaikus
}
}catch(Exception e){
e.printStackTrace();
}
}
void looBass(Track
t, int toonika){
try{
ShortMessage m[]=new ShortMessage[3];
for(int
i=0; i<m.length; i++){
m[i]=new ShortMessage();
}
m[0].setMessage(ShortMessage.PROGRAM_CHANGE, 1, 39, 0);
m[1].setMessage(ShortMessage.NOTE_ON, 1, toonika-24, 60);
m[2].setMessage(ShortMessage.NOTE_OFF, 1, toonika-24, 60);
t.add(new MidiEvent(m[0], 0));
t.add(new MidiEvent(m[1], 0));
t.add(new MidiEvent(m[2], 12));
}catch(Exception e){e.printStackTrace();}
}
static int
pooltoonid(int aste, int muutus){
int
loppaste=aste+muutus;
int
okt=loppaste/7;
if(loppaste<0)okt--;
loppaste=loppaste-7*okt;
int
vahe=noodivahed[loppaste]-noodivahed[aste];
return
vahe+12*okt;
}
void
looTaustaviiul(Track t, int toonika, int aste){
try{
int
nootidearv=6;
int[]
samm=new int[nootidearv];
int
i=0;
samm[i++]=0;
while(i<nootidearv){
if(Math.random()<0.5){
samm[i]=samm[i-1]-1;
} else
{
samm[i]=samm[i-1]+1;
}
i++;
}
ShortMessage
m=new ShortMessage();
m.setMessage(ShortMessage.PROGRAM_CHANGE, 2, 41, 0);
t.add(new MidiEvent(m, 0));
for(i=0;
i<nootidearv; i++){
m=new
ShortMessage();
m.setMessage(ShortMessage.NOTE_ON, 2, toonika+noodivahed[aste]+pooltoonid(aste,
samm[i]), 60);
t.add(new MidiEvent(m, i*2));
m=new
ShortMessage();
m.setMessage(ShortMessage.NOTE_OFF, 2,
toonika+noodivahed[aste]+pooltoonid(aste, samm[i]), 60);
t.add(new MidiEvent(m, (i+1)*2));
}
}catch(Exception e){e.printStackTrace();}
}
Sequence
pohikolmkolad(int toonika) throws InvalidMidiDataException{
Sequence
s1=new Sequence(Sequence.PPQ, 4);
looKolmkola(s1.createTrack(), toonika);
looKolmkola(s1.createTrack(), toonika+5); //kvart
looKolmkola(s1.createTrack(), toonika+7); //kvint
looBass(s1.createTrack(), toonika);
looBass(s1.createTrack(), toonika-7); //IV ehk kvint alla
looBass(s1.createTrack(), toonika-5);
return
s1;
}
Sequence
looSekvents() throws InvalidMidiDataException{
return
pohikolmkolad(helikorgused[helistik.getSelectedIndex()]);
}
void
paneSoolo(){
for(int
i=0; i<raadionupud.length; i++){
sequencer.setTrackMute(i, !raadionupud[i].isSelected() ||
!akord.isSelected());
sequencer.setTrackMute(3+i, !raadionupud[i].isSelected() ||
!bass.isSelected());
}
sequencer.setTrackMute(6, !taust.isSelected());
}
void
mangi(){
sequencer.start();
paneSoolo();
sequencer.setTempoInBPM(tempo.getValue());
}
void
alusta(){
try{
if(!jooksevHelistik.equals(helistik.getSelectedItem())){
sequence=looSekvents();
jooksevHelistik=helistik.getSelectedItem().toString();
}
if(muutuvRada!=null)sequence.deleteTrack(muutuvRada);
muutuvRada=sequence.createTrack();
int
pohitoon=helikorgused[helistik.getSelectedIndex()];
int
aste=1;
if
(duurid[duurinr]==1)aste=4;
if
(duurid[duurinr]==2)aste=5;
looTaustaviiul(muutuvRada, pohitoon, aste);
sequencer.setSequence(sequence);
raadionupud[duurid[duurinr]].setSelected(true);
duurinr=(duurinr+1)%duurid.length;
mangi();
}catch(Exception e){e.printStackTrace(); System.out.println(e);}
}
public
void init(){
Panel
nupupaneel=new Panel(new GridLayout(1, 4));
helistik=new JComboBox(helistikud);
helistik.setSelectedIndex(3);
nupupaneel.add(helistik);
ButtonGroup nupugrupp=new ButtonGroup();
for(int i=0; i<raadionupud.length; i++){
raadionupud[i]=new JRadioButton(raadionupustring[i]);
nupugrupp.add(raadionupud[i]);
nupupaneel.add(raadionupud[i]);
}
raadionupud[0].setSelected(true);
ruut.setSelected(true);
Panel
mangupaneel=new Panel(new GridLayout(1, 2));
mangupaneel.add(nupp);
mangupaneel.add(ruut);
JPanel tempopaneel=new JPanel(new BorderLayout());
tempopaneel.add(new JLabel("Tempo"), BorderLayout.WEST);
tempopaneel.add(tempo, BorderLayout.CENTER);
JPanel valikupaneel=new JPanel(new GridLayout(1, 3));
valikupaneel.add(bass);
valikupaneel.add(akord);
valikupaneel.add(taust);
akord.setSelected(true);
JPanel alumine=new JPanel(new GridLayout(3, 1));
alumine.add(tempopaneel);
alumine.add(mangupaneel);
alumine.add(valikupaneel);
getContentPane().add(alumine, BorderLayout.SOUTH);
getContentPane().add(nupupaneel, BorderLayout.NORTH);
nupp.addActionListener(this);
}
public
void actionPerformed(ActionEvent e){
try{
sequencer=MidiSystem.getSequencer();
sequencer.open();
sequencer.addMetaEventListener(this);
jooksevHelistik="";
alusta();
}catch(Exception ex){
ex.printStackTrace();
System.out.println(ex);
}
}
public
void meta(MetaMessage m){
if(m.getType()==47 && ruut.isSelected()){
alusta();
} else
{
sequencer.close();
}
}
public
static void main(String argumendid[]){
JFrame
f=new JFrame("Saade");
Noodirakend9a ap=new Noodirakend9a();
ap.init();
f.getContentPane().add(ap);
f.pack();
f.setVisible(true);
}
}
Järgnevalt näide,
kus ühendatud Java MIDI-vahendid failide lugemiseks, mängimiseks, muutmiseks ja
salvestamiseks ning Swingi võimalused kasutajaliidese loomisel. Näite on
koostanud TPÜ üliõpilane Kaur Männiko graafika ja muusika programmeerimise
kursuse kodutööna. Näide võimaldab avada ja luua MIDI sekventse. Vaadata radu
ning neid kopeerida nii sekventsi sees kui sekventside vahel. Raja sees saab
vaadata ja muuta kõiki teateid. Olles näite korralkult läbi mõelnud, peaks
olema API spetsifikatsiooni järgi võimalik luua enamikke MIDI-rakendusi, mille
jaoks ideid ja vajadusi on.
Graafilisest liidesest võtavad
suurema osa enese alla Swingi puu ning tabel. Esimene avatud
failide/sekventside ning nende sees olevate radade loeteluks. Tabelis on näha
märgistatud rajal asuvad teated toimumise järjekorras koos oma tähtsamate
andmetega.
Järgnevalt koodis
leiduvate tähelepanu nõudvate lõikude kirjeldus järjemööda.
Märgitud piirkonna mahamängimiseks
on loodud eraldi alamprogramm. Puu käest küsitakse kasutaja märgitud komponent
ning asutakse käituma vastavalt sellele, mida kasutaja märkinud oli.
Mängimiseks sobivad sekvents ning rada. Esimesel puhul paikneb puu viimase lehe
ehk node sees NodeObjectHolder, milles omakorda sekvents. Sekventsi saab
omaette tervikuna mängima panna.
public void play() {
try {
Object src =
tree.getLastSelectedPathComponent();
...
DefaultMutableTreeNode node =
(DefaultMutableTreeNode) src;
if ( node.getUserObject()
instanceof NodeObjectHolder) {
sekvents = (Sequence)
((NodeObjectHolder)node.getUserObject()).userObject;
} else if( node.getUserObject()
instanceof Track) {
Raja mängitamiseks
tuleb luua uus sekvents selle sekventsi parameetritega, milles mängitav rada on
ning siis rajas olevad teated uue sekventsi loodavale rajale üle kanda.
Track algrada =
(Track)node.getUserObject();
sekvents = (Sequence) ((NodeObjectHolder)
((DefaultMutableTreeNode)node.getParent()).getUserObject() ).userObject;
sekvents = new
Sequence(sekvents.getDivisionType(), sekvents.getResolution());
Track uusrada = sekvents.createTrack();
for (int nr = 0; nr < algrada.size();
nr++)
uusrada.add(algrada.get(nr));
Sekventsi
käivitamiseks piisab kahest käsust.
sekventser.setSequence(sekvents);
sekventser.start();
Klassil küljes
olev TreeSelectionListener näitab, et KMidi eksemplar peab oskama ümber käia
puu küljest tulevate teadetega. Kui puus valitakse uus koht, saabub selle kohta
teade valueChanged käsu kaudu. Edasi kontrollitakse, et juhul kui valituks
osutus rada ehk Track-tüüpi leht, siis luuakse uus TableModel ning selle kaudu
palutakse paremal pool asuval tabelil näidata just valitud raja sees olevaid
teateid.
Nii veerunimede kui raja jaoks
loodud muutujad on piiritlejaga final, et sisemise klassi loomisel võiks sealne
kood kindel olla, et muutuja külge seotud objekt ikka sinna püsima jääb ning
keegi seda ära vahetada ei saa. Abstraktsest tabelimudelist tabeli jaoks
tarviliku objekti kokkupanekul tuleb üle katta peotäis käsklusi, mis annavad
teavet tabeli sisalduse kohta või lubavad ka seda muuta. Niisuguse mudeli kaudu
ei pruugi sugugi alati kõik näidatavad väärtused kahemõõtmelises massiivis
asuma, vaid võidakse võtta kusagilt mujalt või sootuks käigu peal valmis
arvutada. Siin näites hoitakse andmeid raja sees ning tabel on vaid sealsete
väärtuste peegliks.
Tabeli mudel on loodud KMidi sees
sisemise anonüümse klassina. Paistab new AbstractTableModel(){} ning nende
loogeliste sulgude vahele on paigutatud kogu ülekaetavate käskude loetelu koos
sisuga. Nagu käskude nimedestki näha getColumnName annab veeru
järjekorranumbrile vastava pealkirja. getColumnCount ja getRowCount teatavad,
palju peaks sellele mudelile vastavas tabelise ridasid ja veerge olema.
Põhjalikumad käsklused on getValueAt ning setValueAt.
Nende abil saab
küsida või määrata tabeli konkreetses lahtris asuvat väärtust.
Sõltumata küsitavast veerust
küsitakse rajast kõigepealt reanumbrile vastav sündmus ning asutakse selle sisu
uurima. Tiksu väärtus leidub sõltumata sündmuse tüübist ning see küsitakse
eraldi muutujasse. Edasi toimetatakse vastavalt sündmuse tüübile. Üldjuhul
muusikalise teate ehk ShortMessage puhul leitakse kanali ja käskluse koodid
ning mõlemad andmebaidid. MetaMessage puhul antakse vaid tiks või tüübi kood
(nt. raja lõpp 47).
Andmete seadmisel setValueAt puhul
antakse ette uus väärtus ning rida ja veerg, kuhu soovitakse see väärtus
paigutada. Rea järgi otsitakse üles vastav MIDI tiks, veeru järgi parameeter
selle sees. Luuakse uus teade ning asendatakse muudetav väärtus, muud
kopeeritakse. Edasi pannakse uus teade vana asemele.
getColumnClass ning isCellEditable
toimivad nii nagu nimigi määrab. Esimene teatab, millise Java klassile
vastavate andmetega tegemist on. Teine vastab, kas vastava veeru andmeid
tabelis muuta lubatakse.
Edasi järgnevad mõned käsklused
radade haldamiseks puus. copyTrack paigutab märgistatud raja osuti ajutisse
muutujasse. getCurrentSequence leiab märgistatud kohale vastava sekventsi. Kui
märgistatud on sekvents, väljastatakse osuti sellele enesele. Kui aga rada,
siis leitakse sekvents, millesse vastav rada kuulub. newSequence loob uue
sekventsi, leiab selle nime ning palub sekventsi puusse paigutada. pasteTrack
loob märgistatud sekventsi sisse uue raja ning lisab sellesse eelnevalt meelde
jäetud rajal paiknevad teated. Uuele rajale kantakse üle vaid teadete osutid,
teadetest endist koopiaid ei tehta.
Alles siis, kui kasutaja soovib tabeli kaudu muuta teates paiknevaid
parameetreid, luuakse endise asemele uus teate eksemplar.
Sekventsi puuse lisamise eest
hoolitseb addSequenceToTree. Sekventsi (faili) nimi tehakse rasvaseks HTML-i
käskude abil, mida on Swingi komponentide juures lubatud kujunduseks kasutada.
Kõikide sekventsis asuvate radade tarbeks luuakse puus esitamise jaoks
tarvilikud DefaultMutableTreeNode tüüpi komponendid.
Järgnev kood on juba küllalt
tavapärane pea iga Swingi rakenduses puhul. actionPerformed sündmuste
haldamiseks. Faili lugemisel paigutatakse tekstifailis olevad andmed sekventsi,
salvestamisel aga talletatakse sekventsi andmed MIDI-faili.
Menüü juures on tarvis vähemasti
kolm osa. JMenuBar tähistab kogu menüüriba, JMenu ühte menüüpaneeli ning sinna
külge JMenuItem, mille kaudu juba inimene käsklusi anda saab. Ning edasi juba
head katsetamist ning jõudu näite põhjal omale tarviliku rakenduse koostamisel.
import
javax.swing.*;
import
java.awt.event.ActionListener;
import
java.awt.event.ActionEvent;
import
java.io.IOException;
import
java.io.File;
import
java.util.*;
import
java.awt.*;
import
java.awt.event.*;
import
java.io.*;
import
javax.sound.midi.*;
import
javax.swing.tree.*;
import
javax.swing.table.*;
import
javax.swing.event.*; //TreeSelectionListener, -event
/**
*
Midi-redaktor. Koostanud TPÜ Informaatika üliõpilane Kaur Männiko
*
graafika ja muusika programmeerimise kursuse kodutööna.
*/
public
class KMidi extends JPanel implements ActionListener, TreeSelectionListener {
Sequence sekvents;
Sequencer sekventser;
JButton btnPlay, btnStop, btnNew, btnCopy,
btnCut, btnPaste, btnDelete;
JScrollPane treeView;
final JTree tree;
final JTable table;
File file;
DefaultMutableTreeNode rootNode;
DefaultTreeModel treeModel;
public void play() {
try {
Object src =
tree.getLastSelectedPathComponent();
if (!(src instanceof
DefaultMutableTreeNode))
return;
DefaultMutableTreeNode node =
(DefaultMutableTreeNode) src;
if (node.getUserObject() == null)
return;
if ( node.getUserObject()
instanceof NodeObjectHolder) {
sekvents = (Sequence) ((NodeObjectHolder)node.getUserObject()).userObject;
} else if( node.getUserObject()
instanceof Track) {
Track algrada =
(Track)node.getUserObject();
sekvents = (Sequence)
((NodeObjectHolder) ((DefaultMutableTreeNode)node.getParent()).getUserObject()
).userObject;
sekvents = new
Sequence(sekvents.getDivisionType(), sekvents.getResolution());
Track uusrada =
sekvents.createTrack();
for (int nr = 0; nr <
algrada.size(); nr++)
uusrada.add(algrada.get(nr));
} else
return; //root
sekventser.setSequence(sekvents);
sekventser.start();
} catch (InvalidMidiDataException e)
{e.printStackTrace();};
}
public void stop() {
sekventser.stop();
}
public KMidi() {
super(new BorderLayout());
JToolBar toolbar = new JToolBar();
btnPlay = new
JButton("Play");
btnPlay.addActionListener(this);
toolbar.add(btnPlay);
btnStop = new
JButton("Stop");
btnStop.addActionListener(this);
toolbar.add(btnStop);
btnNew = new JButton("Uus");
btnNew.addActionListener(this);
toolbar.add(btnNew);
btnCopy = new
JButton("Kopeeri");
btnCopy.addActionListener(this);
toolbar.add(btnCopy);
btnCut = new
JButton("Lõika");
btnCut.addActionListener(this);
toolbar.add(btnCut);
btnPaste = new
JButton("Kleebi");
btnPaste.addActionListener(this);
toolbar.add(btnPaste);
btnDelete = new
JButton("Kustuta");
btnDelete.addActionListener(this);
toolbar.add(btnDelete);
toolbar.setOrientation(JToolBar.HORIZONTAL);
add(toolbar, BorderLayout.NORTH);
rootNode = new
DefaultMutableTreeNode("MidiSystem");
treeModel = new
DefaultTreeModel(rootNode);
tree = new JTree(treeModel);
tree.addTreeSelectionListener(this);
treeView = new JScrollPane(tree);
treeView.setPreferredSize(new
Dimension(300,200));
add(treeView, BorderLayout.WEST);
table = new JTable();
JScrollPane scrollpane = new
JScrollPane(table);
add(scrollpane, BorderLayout.CENTER);
try {
sekventser =
MidiSystem.getSequencer();
if (!sekventser.isOpen()) {
System.out.print("avatakse sekventser...");
sekventser.open();
System.out.println("
ok");
}
} catch (MidiUnavailableException e)
{e.printStackTrace();};
}
public void
valueChanged(TreeSelectionEvent e) {
Object src = e.getPath().getLastPathComponent();
if (!(src instanceof
DefaultMutableTreeNode))
return;
DefaultMutableTreeNode node =
(DefaultMutableTreeNode) src;
if (node.getUserObject() == null)
return;
if(!( node.getUserObject() instanceof
Track))
return;
final Track track = (Track)
node.getUserObject();
final String[] columnNames =
{"Tiks",
"Kanal",
"Teade",
"kõrgus",
"valjus",
"Meta
tüüp"};
TableModel dataModel = new
AbstractTableModel() {
public String getColumnName(int
column) { return columnNames[column];}
public int getColumnCount() {
return columnNames.length; }
public int getRowCount() { return
track.size();}
public Object getValueAt(int row,
int col) {
MidiEvent me = track.get(row);
Long tick = new
Long(me.getTick());
MidiMessage msg =
me.getMessage();
if (msg instanceof
ShortMessage){
ShortMessage sm =
(ShortMessage)msg;
switch (col) {
case 0: return tick;
case 1: return new
Integer(sm.getChannel());
case 2: return new
Integer(sm.getCommand());
case 3: return new
Integer(sm.getData1());
case 4: return new
Integer(sm.getData2());
default: return
"";
}
}
else if (msg instanceof
MetaMessage){
MetaMessage mm =
(MetaMessage)msg;
switch (col) {
case 0: return tick;
case 5: return new
Integer(mm.getType());
default: return
"";
}
}
else if (col == 0)
return tick;
else
return "";
}
public Class getColumnClass(int c)
{
//return getValueAt(0, c).getClass();
//return
Class.forName("Integer");
return (new
Long(0)).getClass();
}
public void setValueAt(Object
aValue, int row, int col) {
System.out.println("set
value "+row+","+ col);
MidiEvent me = track.get(row);
Long tick = new
Long(me.getTick());
MidiMessage msg =
me.getMessage();
if (col == 0) {
me.setTick(((Long)aValue).longValue());
return;
}
int val =
((Long)aValue).intValue();
if (msg instanceof
ShortMessage){
ShortMessage sm =
(ShortMessage)msg;
int command = sm.getCommand();
int channel =
sm.getChannel();
int data1 = sm.getData1();
int data2 = sm.getData2();
int status =
sm.getStatus();
System.out.println("enne muutmist:
"+command+","+channel+","+data1+","+ data2);
switch (col) {
//case 0:
me.setTick(val);
case 1: channel = val;
break;
case 2: command = val;
break;
case 3: data1 = val;
break;
case 4: data2 = val;
break;
}
System.out.println("muudetakse:
"+command+","+channel+","+data1+","+ data2);
try {
ShortMessage sm2 = new
ShortMessage();
sm2.setMessage(command, channel, data1, data2);
MidiEvent me2 = new
MidiEvent(sm2, me.getTick());
track.remove(me);
track.add(me2);
} catch
(InvalidMidiDataException ex) {
ex.printStackTrace();
}
}
}
public boolean isCellEditable(int
row, int col) {
if (col == 5)
return false;
else
return true;
}
};
table.setModel(dataModel);
}
Track tempTrack = null;
public void copyTrack() {
if
(tree.getLastSelectedPathComponent() == null) return;
DefaultMutableTreeNode node =
(DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
if( node.getUserObject() instanceof
Track) {
tempTrack =
(Track)node.getUserObject();
} else
return; //root
}
private Sequence getCurrentSequence() {
Sequence seq = null;
if
(tree.getLastSelectedPathComponent() == null) return null;
DefaultMutableTreeNode node =
(DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
if ( node.getUserObject() instanceof
NodeObjectHolder) {
seq = (Sequence)
((NodeObjectHolder)node.getUserObject()).userObject;
} else if( node.getUserObject()
instanceof Track) {
Track algrada =
(Track)node.getUserObject();
seq = (Sequence)
((NodeObjectHolder) ((DefaultMutableTreeNode)node.getParent()).getUserObject()
).userObject;
}
return seq;
}
private int jnr = 0;
public void newSequence() {
sekvents = getCurrentSequence();
if (sekvents == null) return;
try{
sekvents = new
Sequence(sekvents.getDivisionType(), sekvents.getResolution());
//addSeqenceToTree(sekvents,
"uus-"+(new Integer(jnr++)).toString()+".mid");
addSeqenceToTree(sekvents,
"uus-"+(jnr++)+".mid");
}catch (InvalidMidiDataException e)
{e.printStackTrace();}
}
public void cutTrack() {
copyTrack();
deleteTrack();
}
public void pasteTrack() {
if
(tree.getLastSelectedPathComponent() == null) return;
DefaultMutableTreeNode node =
(DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
if ( node.getUserObject() instanceof
Track)
node =
(DefaultMutableTreeNode)node.getParent();
if (!( node.getUserObject() instanceof
NodeObjectHolder))
return;
sekvents = (Sequence)
((NodeObjectHolder) node.getUserObject() ).userObject;
Track uusrada =
sekvents.createTrack();
for (int nr = 0; nr <
tempTrack.size(); nr++)
uusrada.add(tempTrack.get(nr)); //NB! ei tee uusi eksemplare
DefaultMutableTreeNode trackNode = new
DefaultMutableTreeNode(uusrada);
node.add(trackNode);
treeModel.insertNodeInto(trackNode,
node, 0);
}
public void deleteTrack() {
if
(tree.getLastSelectedPathComponent() == null) return;
DefaultMutableTreeNode node =
(DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
if( node.getUserObject() instanceof
Track) {
Sequence sekvents = (Sequence)
((NodeObjectHolder) ((DefaultMutableTreeNode)node.getParent()).getUserObject()
).userObject;
Track track =
(Track)node.getUserObject();
sekvents.deleteTrack(track);
treeModel.removeNodeFromParent(node);
} else
return; //root
}
public void addSeqenceToTree(Sequence seq,
String name) {
DefaultMutableTreeNode sequenceNode =
null;
DefaultMutableTreeNode trackNode =
null;
NodeObjectHolder noh = new NodeObjectHolder("<html><b>"
+ name + "</b></html>", seq);
sequenceNode = new
DefaultMutableTreeNode(noh);
Track[] rajad = seq.getTracks();
System.out.println(rajad.length+"
rada");
for (int nr = 0; nr < rajad.length;
nr++){
trackNode = new
DefaultMutableTreeNode(rajad[nr]);
sequenceNode.add(trackNode);
}
treeModel.insertNodeInto(sequenceNode,
rootNode, rootNode.getChildCount());
}
public void actionPerformed(ActionEvent e)
{
if (e.getSource() == btnPlay) {
play();
} else if (e.getSource() == btnStop) {
stop();
} else if (e.getSource() == btnCopy) {
copyTrack();
} else if (e.getSource() == btnNew) {
newSequence();
} else if (e.getSource() == btnCut) {
cutTrack();
} else if (e.getSource() == btnPaste)
{
pasteTrack();
} else if (e.getSource() == btnDelete)
{
deleteTrack();
} else if (e.getSource() == mnLoad) {
loadFile();
} else if (e.getSource() == mnSave) {
saveFile();
}
}
JFileChooser fc = new JFileChooser();
public void loadFile() {
int returnVal = fc.showOpenDialog(this);
if (returnVal ==
JFileChooser.APPROVE_OPTION) {
file = fc.getSelectedFile();
try {
System.out.println("Opening: " + file.getCanonicalPath());
sekvents =
MidiSystem.getSequence(file);
addSeqenceToTree(sekvents,
file.getName());
} catch (Exception ex) {
ex.printStackTrace();
}
} else {
System.out.println("Open
command cancelled by user.");
}
}
public void saveFile() {
sekvents = getCurrentSequence();
//millist faili salvestada
if (sekvents == null) return;
int returnVal =
fc.showSaveDialog(this);
if (returnVal ==
JFileChooser.APPROVE_OPTION) {
file = fc.getSelectedFile();
try {
MidiSystem.write(sekvents, 1,
new FileOutputStream(file.getCanonicalPath()));
System.out.println("Saved: " + file.getCanonicalPath());
} catch (IOException ex) {
ex.printStackTrace();
}
} else {
System.out.println("Save
command cancelled by user.");
}
}
JMenuItem mnUus, mnLoad, mnSave;
public JMenuBar createMenuBar() {
JMenuBar menuBar = new JMenuBar();
JMenu menu = null;
JMenuItem menuItem = null;
menu = new JMenu("Failid");
menuItem = new
JMenuItem("Lae..");
menuItem.addActionListener(this);
menu.add(menuItem);
mnLoad = menuItem;
menuItem = new
JMenuItem("Salvesta..");
menuItem.addActionListener(this);
menu.add(menuItem);
mnSave = menuItem;
menuBar.add(menu);
return menuBar;
}
public static void main(String[] args) {
//
JFrame.setDefaultLookAndFeelDecorated(true);
// Käsk kasutatav alates JDK
versioonist 1.4
JFrame frame = new
JFrame("KMidi");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
KMidi kodContentPane = new KMidi();
kodContentPane.setOpaque(true);
frame.setJMenuBar(kodContentPane.createMenuBar());
frame.setContentPane(kodContentPane);
frame.pack();
frame.setSize(600, 400);
frame.setVisible(true);
}
}
/**
*
Vahend, mille abil hoida objekti koos tema juurde kuuluva sildiga
*
Swingi puu tarvis esitataval kujul. Kasutatakse näiteks sekventsi
*
hoidmiseks.
*/
class
NodeObjectHolder {
public String label = "no name";
public Object userObject = null;
public NodeObjectHolder() {
}
public NodeObjectHolder(String label,
Object userObject) {
this.label = label;
this.userObject = userObject;
}
public String toString() {
return label;
}
}
Mandoliin
· Joonista ekraanile mandoliin.
· Kasutaja saab määrata joonistatavate krihvide arvu ning kaela laiust.
· Vajutades keele ja krihvi ristumiskohale, kõlab sellele vastav heli.
Kitarri
mudel
· Joonista
6-keelse kitarri kaela mudel
· Mängi
kitarriakord (E, H, G, D, A, E) (64, 59, 55, 50, 45, 40)
· Peenemal
keelel saab määrata, milline krihv on alla vajutatud. Hääl kõlab vastavalt vajutatule
· Krihve
saab valida ka teiste keelte puhul ning kuulata tulemust
· Lisaks
võib valida täisakorde.
Akordion
· Joonista klaviatuuri ja 36 bassiga akordion.
· Basside ridade ja veergude arvu saab kasutaja määrata. Samuti hiire abil lõõtsa lahti ja kokku vedada.
· Lisaks eelmisele kõlab bassinuppudele vajutades neile vastav heli.
Vilepill
· Joonista vilepill. Pillil on avatud tekstiväljas määratud hulk auke.
· Lisaks eelmisele teeb pillile vajutamisel viimane häält.
· Augule vajutamisel on augud lõpust kuni sinnani avatud. Kõlab sellisele sõrmede paigutusele vastav hääl.
MIDI fail
· Mängi
MIDI fail
· Väljasta
radade arv
· Salvesta
fail tooni jagu kõrgemalt.
· Kopeeri
faili muusika iseendale sappa
· Lisa
rada, kus viisi mängitakse nihkega (kaanon)