MokaByte 58 - Dicembre 2001 
Introduzione al MIDI in Java
III parte: come realizzare un file MIDI
di
 
Stefano
Leone
Monni
 

 

In questa terza puntata scopriremo come sia facile, tramite il package javax.sound.midi del jdk.1.3, comporre ex-novo un brano musicale da noi stessi ideato, per poi salvarlo sotto forma di file MIDI

Introduzione
In questo capitolo verrà spiegato il modo in cui realizzare da sé una sequenza MIDI per poi salvarla sotto forma di file MIDI.
A questo scopo è necessario avere a disposizione, oltre ad una Sequence (sulla quale andremo a scrivere tutte le informazioni relative alla nostra composizione musicale come le note, gli strumenti, etc) e ad un Sequencer (che riprodurrà musicalmente la Sequence stessa) i seguenti elementi:

  • Una o più tracce musicali (in Java rappresentate dalla classe Track) , che costituiscono dei "pezzi" di sequence e sono costituite, a loro volta, da eventi Midi.
  • Un insieme di eventi Midi (in Java rappresentati dalla classe MidiEvent(messaggio, istante di inizio)) generati attraverso la spedizione di messaggi (in Java rappresentati dalla classe MidiMessage e dalle sue sottoclassi) che specificano il tipo di evento Midi che si vuole generare (risulterà tutto più chiaro con il commento dell'esempio proposto più avanti).


Schematizzando, possiamo rappresentare l'intera rete di collegamenti come segue (fig.1):


L'esempio successivo riprenderà esattamente lo schema riportato in fig1 (le voci inserite nelle varie caselle di testo corrispondono alle omonime classi e interfacce utilizzate in Java).
E' importante notare come la mancanza di anche una sola delle classi e/o interfacce sopra menzionate renderebbe vano qualsiasi tentativo di riproduzione musicale di una sequenza Midi creata ex-novo.


Esempio n.3 (Come realizzare un file MIDI)

import javax.sound.midi.*;
import java.io.*;

/** Informazioni di base per la gestione della
* durata di una nota musicale:
* given 120 bpm:
* (120 bpm)/(60 seconds per minute)= 2 beats per second
* 2 / 1000 beats per millisecond
* (2 * resolution) ticks per second
* (2 * resolution)/1000 ticks per millisecond, or
* (resolution / 500) ticks per millisecond
* ticks = milliseconds * resolution / 500
*/


class Midi3 implements MetaEventListener
{
final int PROGRAM = 192;
final int NOTEON = 144;
final int NOTEOFF = 128;

final int DO = 60;
final int MI = 64;
final int SOL = 67;

Track track;
Sequence mySequence;
Sequencer mySequencer;

int accordoDo[] = { DO,MI,SOL } ;

int velocity = 100; // volume delle note
int res=1; // Resolution
int numChan = 1; // numero di canale Midi
int numInstr = 0; // numero dello strumento
// musicale ( 0:pianoforte)
int tempoBPM = 120; // tempo espresso in battiti
// al minuto (metronomo)

boolean end_of_track = false;

public Midi3()
{
try {
mySequence = new Sequence(Sequence.PPQ,res);
// Resolution = res
mySequencer = MidiSystem.getSequencer();
mySequencer.open();
mySequencer.addMetaEventListener(this);

// Creiamo una traccia MIDI inizialmente vuota:

track = mySequence.createTrack();

// Creiamo un evento MIDI fornendo i seguenti
// parametri:
// tipo di comando ,canale, strumento,
// istante d'inizio dell'evento):
createEvent(PROGRAM,numChan,numInstr,0);

for (int i = 0;i < accordoDo.length;i++)
{
createEvent(NOTEON,numChan,accordoDo[i],i*1000); createEvent(NOTEOFF,numChan,accordoDo[i],4000);
}

mySequencer.setSequence(mySequence);
mySequencer.start();
mySequencer.setTempoInBPM(tempoBPM); // 120 bpm

System.out.println("Esecuzione dell'accordo di DO Magg.");
System.out.println();
System.out.println("tempo del metronomo:" +
String.valueOf(tempoBPM) + " battiti al minuto\n");

// Attendiamo che il sequencer riproduca l'intera traccia:

while(!end_of_track()) {}

System.out.println("Esecuzione terminata.\n");

// Creiamo il nostro file MIDI:

System.out.println("Inserisci il nome del file da salvare:");
BufferedReader in =
new BufferedReader(new InputStreamReader(System.in));
String name = in.readLine();
if (!name.equals(""))
{ if (!name.endsWith(".mid")) name += ".mid";
saveMidiFile(new File(name));
}
System.out.println("Applicazione terminata");
closeDevice();
System.exit(0); }

catch (Exception ex) { ex.printStackTrace(); }}

// Il seguente metodo crea un nuovo evento MIDI:

private void createEvent(int type, int chan, int num,
long millis)
{
ShortMessage message = new ShortMessage();
try {
long tick = millis * mySequence.getResolution()/500;
message.setMessage(type, chan, num, velocity);
MidiEvent event = new MidiEvent( message, tick );
track.add(event);
} catch (Exception ex) { ex.printStackTrace();}
}

// Il seguente metodo crea un nuovo file MIDI:

public void saveMidiFile(File file) {
try {
int[] fileTypes = MidiSystem.getMidiFileTypes(mySequence);

if (fileTypes.length == 0)
System.out.println("Non posso salvare la sequenza!!!");
else
if (MidiSystem.write(mySequence, fileTypes[0], file) == -1) throw new IOException("Problemi nella scrittura del file");
else
System.out.println("File MIDI salvato correttamente.");
} catch (Exception ex) {ex.printStackTrace();}
}

// Il seguente metodo individua il metaEvent corrispondente
// alla fine di una traccia MIDI:

public void meta(MetaMessage message) {
if (message.getType() == 47) // 47 è la fine della traccia
{
System.out.println("MetaEvent: riscontrata la fine della
traccia.\n");
mySequencer.stop();
end_of_track = true; // è stata raggiunta la fine della
//traccia
}
}

public void closeDevice()
{
if (mySequencer != null) mySequencer.close();
mySequencer=null;
System.out.println("Sequencer chiuso.");
System.out.println();
}

public static void main(String [] args)

{ Midi3 app = new Midi3();
}
}

 

Commento dell'applicazione Midi3
Il codice riportato nell'esempio n.3 consiste in una applicazione Java che consente, - analogamente all'esempio visto nella scorsa puntata - la riproduzione musicale dell'accordo di Do Magg ,ma questa volta le note dell'accordo vengono caricate su una sequenza MIDI e quindi riprodotte musicalmente da un sequencer.
Al termine dell'esecuzione musicale della sequenza (ossia dell'accordo stesso) verrà data all'utente la possibilità di salvare la sequenza MIDI appena creata sotto forma di file MIDI, dopo aver specificato il nome da dare al file stesso. Infine, verranno effettuate tutte le operazioni di chiusura del dispositivo aperto in precedenza (nel nostro caso il Sequencer) e l'applicazione avrà termine.
Analizziamo ora il codice più nel dettaglio:

La costante intera

final int PROGRAM = 192;

definisce il numero di comando utilizzato per impostare un determinato strumento musicale su un determinato canale MIDI. Il valore PROGRAM verrà infatti passato come argomento alla procedura

createEvent(int type, int chan, int num, long millis)

tramite il comando

createEvent(PROGRAM,numChan,numInstr,0);

dove i vari argomenti indicano, rispettivamente:

  • PROGRAM: tipo (type) di comando da inviare all'evento che si deve costruire;
  • numChan: numero di canale MIDI (valore compreso fra 0 e 15);
  • numInstr: relativamente al comando PROGRAM, esso indica lo strumento
    musicale che si intende usare;
  • 0: rappresenta l'istante temporale - rispetto all'inizio della sequenza - in cui l'evento dovrà verificarsi. Questo valore verrà passato all'argomento 'long millis'.

Le costanti intere

final int NOTEON = 144;
final int NOTEOFF = 128;

definiscono i valori interi relativi ai comandi, rispettivamente, per la generazione di una nota ed il suo "spegnimento".

Il valore NOTEON (e analogamente NOTEOFF) verrà passato come argomento alla procedura

createEvent(int type, int chan, int num, long millis)

tramite il comando

createEvent(NOTEON,numChan,accordoDo[i],i*1000);

dove, ancora una volta, i vari argomenti staranno ad indicare:

  • NOTEON: tipo (type) di comando da inviare all'evento che si deve costruire;
  • numChan: numero del canale MIDI (valore compreso fra 0 e 15);
  • accordoDo[i]: relativamente al comando NOTEON, esso indica l'altezza tonale
    della nota musicale che si vuole generare;
  • i*1000: rappresenta l'istante di tempo - rispetto all'inizio della sequenza -
    in cui l'evento dovrà aver inizio. Questo valore verrà passato
    all'argomento 'long millis'.


Diamo ora un'occhiata al ciclo che chiama la procedura per la creazione dei vari eventi MIDI:

for (int i = 0;i < accordoDo.length;i++){
  createEvent(NOTEON,numChan,accordoDo[i],i*1000);
  createEvent(NOTEOFF,numChan,accordoDo[i],4000);
}

Esso impone di generare (alla distanza di un secondo l'una dall'altra) le tre note dell'accordo, a partire dall'inizio della sequenza MIDI (ossia il DO a 0 millisec, il MI a 1000 millisec, il SOL a 2000 millisec,) e quindi di spegnerle tutte al quarto secondo (4000 millisec.).
A questo punto sarà però necessario introdurre qualche fondamentale nozione sul tempo.

 

Gestione del tempo di un evento MIDI
Dalla teoria musicale, è noto che una intera composizione musicale vada eseguita con un precisa velocità di esecuzione, regolata dalla frequenza di oscillazione di un metronomo.
Ora, relativamente al nostro caso, imporre al metronomo, ad esempio, una scansione temporale di 120 battiti al minuto (dove questi ultimi vengono indicati coi termini "beats" o anche, relativamente al nostro esempio, "ticks"), significherà che verrà generato un battito (tick) ogni mezzo secondo.

Ebbene, il "tick" rappresenterà l'unità di misura temporale dell'intera sequenza musicale.
Relativamente al nostro caso, ciò significa che un qualsiasi evento MIDI potrà essere generato all'inizio della sequenza (tick=0), oppure in corrispondenza del primo tick (tick=1, ovvero dopo mezzo secondo), del secondo tick (tick=2, ovvero dopo un secondo) e così via, ma mai, ad esempio, in corrispondenza di ½ tick o ¾ tick.
I
l modo per impostare il tempo del metronomo desiderato sul Sequencer è il seguente:

mySequencer.setTempoInBPM(tempoBPM); // int tempoBPM = 120

Ragionando in termini di ticks (e non di millisecondi) avremmo potuto ottenere lo stesso risultato sonoro riportando le seguenti modifiche:

1- Al posto di

createEvent(NOTEON,numChan,accordoDo[i],i*1000);
// tempo espresso in millisecondi
createEvent(NOTEOFF,numChan,accordoDo[i],4000);
// sono passati 4000 ms

avremmo dovuto scrivere

createEvent(NOTEON,numChan,accordoDo[i],i*2);
// tempo espresso in ticks
createEvent(NOTEOFF,numChan,accordoDo[i],8);
// sono passati 8 ticks = 4000 ms

(poiché l'intervallo fra un tick ed il successivo è , nel nostro caso, pari a mezzo secondo)

2 - Avremmo poi dovuto eliminare la conversione dei millis in tick, ovvero l'istruzione (commentata più avanti):

long tick = millis * mySequence.getResolution() / 500;

3 - Cambiare l'intestazione della procedura

private void createEvent(int type, int chan, int num, long millis)

nella nuova

private void createEvent(int type, int chan, int num, long tick)

A questo punto potrebbe sorgere una domanda più che giustificata:
se si volesse inserire una nota, ad esempio dopo un tempo pari a 250 ms dall'inizio della sequenza, come si potrebbe fare?
Ciò equivarrebbe al tentativo di inserimento di una nota dopo un tempo pari a mezzo tick, il che non è possibile poiché un tick è sempre, per definizione, un valore intero.
In Java, questo problema è risolvibile in forma molto comoda e pressochè immediata attraverso il ricorso alla cosiddetta "risoluzione di sequenza", che permette di impostare una distanza temporale fra due ticks consecutivi arbitrariamente piccola.
L'assegnamento della risoluzione temporale di una determinata sequenza viene effettuata all'atto della creazione della sequenza stessa. Nel nostro esempio:

int res = 1; // Risoluzione: è sempre un valore intero maggiore di 0
mySequence = new Sequence(Sequence.PPQ,res);

dove il primo argomento, ovvero il campo PPQ della classe Sequence, specifica che la risoluzione res va espressa in ticks per quarto di nota (Per ulteriori modalità di risoluzione si consulti la voce [2]).
Maggiore sarà la nostra risoluzione res, più ravvicinati nel tempo saranno due ticks consecutivi, e conseguentemente in maggior misura sarà possibile ravvicinare due eventi MIDI consecutivi fra di loro.
Questo fatto si comprende esaminando l'istruzione di conversione:

long tick = millis * mySequence.getResolution() / 500;

dove il metodo getResolution() è il metodo che restituisce la risoluzione della sequenza ad esso relativa (nel nostro esempio sarà res).
In particolare, si può notare che, ponendo res=500, si ottenga proprio l'uguaglianza tick = millis, il che significa avere una risoluzione tanto alta da consentire la disposizione di due eventi MIDI consecutivi fino alla distanza di 1 ms l'uno dall'altro.

ATTENZIONE: Questo non significa che non sia possibile generare più di un evento MIDI nello stesso istante: la simultaneità fra due o più eventi (che è realizzabile associando a tali eventi lo stesso valore di tick) non ha niente a che vedere con la distanza minima consentita fra due ticks consecutivi.
L'istruzione di conversione è ricavata dalle osservazioni effettuate nei commenti introduttivi del codice dell'esempio, che si riporta qui sotto:


/* Informazioni di base per la gestione della durata di una nota musicale:

* given 120 bpm:
* (120 bpm) / (60 seconds per minute) = 2 beats per second
* 2 / 1000 beats per millisecond
* 2 * (resolution) ticks per second
* (2 * resolution)/1000 ticks per millisecond, or
* (resolution / 500) ticks per millisecond
* ticks = milliseconds * resolution / 500
*/

Le osservazioni da fare a tale proposito sono due:

1) Il valore della risoluzione, e quindi della distanza fra due ticks consecutivi non incide sulla velocità di esecuzione della sequenza, regolata esclusivamente, nel nostro esempio, dal valore di tempoBPM.

2) Tale conversione assicura che, fissato un tempo di metronomo pari a 120 beats per minuto il valore di 1 millis corrisponda effettivamente, nella realtà, ad 1 millisecondo.

Inoltre, dalle informazioni sopra fornite si può ricavare una interessante relazione fra i beats ed i ticks, che è la seguente:

tick = beat / resolution

dove in questo caso intendiamo, con la voce tick, la distanza temporale fra due ticks consecutivi. Infatti, fissata la durata di un beat (che è sempre pari alla metà del periodo di oscillazione del metronomo) , quella di un tick diminuirà all'aumentare della risoluzione, e viceversa.
Dal momento che, nell'esempio considerato, si è posto res =1 (cosicchè risultava l'equivalenza tick = beat) all'inizio di tale paragrafo si è preferito non precisare subito tale distinzione (sia pure importante) per non appesantire troppo la trattazione.
Chiariti questi punti chiave sul tempo, possiamo ritornare a commentare il codice dal punto in cui l'abbiamo interrotto.
Esaminiamo ora nel dettaglio la procedura che si occupa di generare un singolo evento MIDI:

private void createEvent(int type,int chan,int num,long millis) {

ShortMessage message = new ShortMessage();
try {
long tick = millis * mySequence.getResolution()/500;
message.setMessage(type, chan, num, velocity);
MidiEvent event = new MidiEvent( message, tick );
track.add(event);
} catch (Exception ex) { ex.printStackTrace(); }
}

In Java, un evento MIDI viene identificato con due attributi: il primo è un 'messaggio MIDI' (che conterrà tutte le informazioni necessarie per caratterizzare tale evento); il secondo è l'istante di tempo in cui tale evento dovrà essere generato all'interno della sequenza MIDI d'appartenenza.
Pertanto la prima cosa da fare è creare un nuovo messaggio (inizialmente vuoto):

ShortMessage message = new ShortMessage();

Ricordo che ShortMessage è una sottoclasse di MidiMessage.
Un messaggio di questo tipo può contenere al più due bytes di dati, oltre al suo byte di stato (ossia quello che specifica il tipo di comando da eseguire).
Nel nostro esempio risulta essere scritto:

message.setMessage(type, chan, num, velocity);

i cui argomenti sono descritti qui di seguito:

  • type : è il byte di stato, ovvero il tipo di comando che deve essere eseguito.
    Nel nostro esempio, il primo comando da impartire (ovvero il primo evento
    da generare) è PROGRAM, poiché esso provvederà a selezionare lo
    strumento musicale con cui sarà possibile ascoltare le note musicali;
    i comandi successivi saranno quelli relativi a NOTEON e quindi NOTEOFF.
  • chan: indica il canale Midi cui spedire il comando 'type'.
    num: indica il primo byte di dati: esso assumerà un significato diverso a seconda
    del tipo di comando type cui sarà associato (per PROGRAM il numero di
    strumento selezionato e per NOTEON e NOTEOFF l'altezza della nota
    d'interesse).
  • velocity: indica il volume della nota, relativamente ad un tipo di comando
    NOTEON o NOTEOFF. ( In generale rappresenta il secondo byte di
    informazione del messaggio stesso).

Nota: Con messaggi del tipo ShortMessage è possibile, in pratica, spedire qualsiasi tipo di messaggio, eccetto messaggi esclusivi del sistema (gestiti dalla classe SysexMessage) e quelli relativi ai cosiddetti meta-events (gestiti dalla classe MetaEvent, descritta in seguito).
Una volta realizzato il messaggio (message) desiderato, è necessario passarlo come argomento all'evento Midi che si vuole costruire:

MidiEvent event = new MidiEvent(message, tick);

dove il secondo argomento (tick) contiene le informazioni relative all'istante in cui l'evento stesso dovrà essere generato, come già ampiamente spiegato.
L'evento così creato dovrà essere inserito in una traccia Midi, la quale a sua volta dovrà appartenere ad una determinata sequenza.
Una volta creata una nuova sequenza con la già commentata istruzione

Sequence mySequence = new Sequence (Sequence.PPQ,res);

si può creare una nuova traccia (track) da associare alla sequenza appena creata:

Track track = mySequence.createTrack(); // traccia inizialmente vuota

C'è da notare, a questo proposito, che la dichiarazione della traccia avviene al di fuori della procedura CreateEvent, di modo che tutti gli eventi generati tramite una chiamata a quest'ultima vengano caricati in una stessa traccia (nel nostro esempio, track) .
A questo punto è possibile aggiungere alla nuova traccia l'evento precedentemente creato:

track.add(event); // aggiunge l'evento alla traccia

e la procedura CreateEvent può così portare a termine il proprio lavoro.
Una volta costruiti tutti gli eventi (uno per ogni chiamata alla procedura CreateEvent) la nostra sequenza è finalmente pronta per essere riprodotta dal sequencer.
Le istruzioni per eseguire tale operazione sono ormai note dal primo esempio:

mySequencer.open();
mySequencer.setSequence(mySequence);
mySequencer.start();

L'esecuzione musicale dovrà proseguire finchè non verrà raggiunta la fine della sequenza musicale (ovvero dell'unica traccia MIDI che la costituisce).
Per verificare ciò è possibile introdurre un particolare ascoltatore di eventi, ovvero un ascoltatore di MetaEvent. Così come gli ShortEvents sono costituiti da ShortMessages, i MetaEvents sono costituiti da MetaMessages. Come specificato dalla documentazione della Sun:

"Un MetaMessage è un MidiMessage che non è significativo per i sintetizzatori, ma che può essere immagazzinato in un file MIDI ed interpretato da un Sequencer. […]
Il formato Midi definisce vari tipi standard di meta-events, come il numero di sequenza […]", le specificazioni sui nomi delle tracce , sulle indicazioni del tempo, etc.

Per maggiori informazioni sui meta-events (e sul MIDI in generale!) si consiglia la consultazione del sito (http://www.midi.org).
Il verificarsi del raggiungimento della fine di una traccia MIDI è appunto un caso di meta-event.
Come per tutti i tipi di eventi in Java, anche questo tipo di eventi potrà essere individuato da un ascoltatore di eventi, che dovrà essere aggiunto al Sequencer
in uso, come segue:

mySequencer.addMetaEventListener(this);

Non ci si deve dimenticare che MetaEventListener è un'interfaccia che andrà implementata. Pertanto dovremo ricordarci all'inizio della classe, di specificare l'interfaccia da implementare

class Midi3 implements MetaEventListener

e ricordarci di implementare l'unico metodo che la costituisce, ovvero il metodo meta(MetaMessage meta)

come segue:

public void meta(MetaMessage message) {
if (message.getType() == 47) // 47 è la fine della traccia
{
System.out.println("MetaEvent: riscontrata la fine della
traccia.\n");
mySequencer.stop();
end_of_track = true; // è stata raggiunta la fine della
//traccia
}
}

In questo metodo, la condizione

if (message.getType() == 47)

verifica se il tipo di metaEvent riscontrato ha valore pari a 47, ovvero a quel particolare valore che corrisponde al raggiungimento della fine di una traccia (track). In caso affermativo il Sequencer viene stoppato e la variabile booleana

end_of_track = true;

in modo da consentire l'uscita dal ciclo while all'interno del metodo midi3()

while(!end_of_track) {}

Dopo l'uscita da tale ciclo si può procedere alla fase di salvataggio della sequenza sotto forma di file MIDI. Tale compito è svolto dalla procedura seguente:

public void saveMidiFile(File file) {
try {
int[] fileTypes = MidiSystem.getMidiFileTypes(mySequence);

if (fileTypes.length == 0)
System.out.println("Non posso salvare la sequenza!!!");
else
if (MidiSystem.write(mySequence, fileTypes[0], file) == -1) throw new IOException("Problemi nella scrittura del file");
else
System.out.println("File MIDI salvato correttamente.");
} catch (Exception ex) {ex.printStackTrace();}
}

Con la istruzione

int[] fileTypes = MidiSystem.getMidiFileTypes(mySequence);

stiamo in pratica richiedendo al nostro MIDISystem di fornirci l'insieme dei tipi di formato MIDI (formato MIDI tipo 0,1,2) disponibili per la sequenza mySequence, che è quella contenente la musica che vogliamo salvare (si veda la voce [1] per ulteriori informazioni sui vari formati MIDI).
Il metodo

public static int[] getMidiFileTypes(Sequence sequence)

restituisce un array di lunghezza nulla nel caso il nostro sistema non supporti, relativamente a quella sequenza, alcun tipo di formato MIDI.
Pertanto avremo le seguenti istruzioni

if (fileTypes.length == 0)
System.out.println("Non posso salvare la sequenza!!!");


Se invece il nostro sistema riconosce nella nostra sequenza un tipo di formato valido si procede al salvataggio su file della sequenza stessa, come segue:

else
if (MidiSystem.write(mySequence,fileTypes[0], file) == -1) throw new IOException("Problemi con la scrittura del file");

Il metodo

MidiSystem.write(mySequence, fileTypes[0], file)

scrive la sequenza 'mySequence' nel file 'file' utilizzando il primo formato MIDI disponibile fra quelli che ha trovato, ovvero 'fileTypes[0]'.
Tale metodo restituisce il numero di byte scritti nel file. Un valore di ritorno pari a -1 indicherebbe pertanto un errore nella scrittura del file MIDI, facendo scattare l'eccezione corrispondente.
Una volta salvato il nostro file MIDI, viene chiuso il sequencer tramite il metodo

closeDevice()

e l'applicazione ha termine.

Conclusioni
In questi primi tre appuntamenti sul MIDI con Java abbiamo affrontato diversi argomenti: come ascoltare un generico file MIDI, come utilizzare il sintetizzatore MIDI per generare suoni e musiche da noi stessi ideate, ed infine come realizzare con le nostre mani una piccola composizione musicale in modo da poterla salvare sotto forma di file MIDI. Ebbene, il nostro viaggio col MIDI non si ferma ancora qui. Infatti nel prossimo appuntamento, ormai esperti sull'argomento :-), saremo più che pronti per analizzare un software (implementato dal sottoscritto) completo per la creazione, riproduzione, modifica, introduzione di effetti in tempo reale e salvataggio di un file MIDI, il tutto, questa volta, con il comodo ausilio delle Swing. Non mancate all'appuntamento!!!


Bibliografia
[1] S.L.Monni - "Introduzione al MIDI in Java", Tesina di Ing. del Software
presso l'Università di Ing.Elettronica di Cagliari - gennaio 2001

[2] Documentazione fornita dalla Sun relativa al package javax.sound.midi
consultabile all'indirizzo:
http:// java.sun.com/j2se/1.3.0/docs/api/javax/sound/midi/package-summary.html


MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems; tutti i diritti riservati 
E' vietata la riproduzione anche parziale 
Per comunicazioni inviare una mail a info@mokabyte.it