MokaByte 57 - 9mbre 2001 
Introduzione al MIDI in Java
II parte: come generare suoni e musiche
col sintetizzatore MIDI 
di
Stefano
Leone Monni
In questa seconda puntata proseguiremo con lo studio del package javax.sound.midi fornito dal Jdk 1.3 della Sun. In particolare, ci soffermeremo sulle modalità di utilizzo del sintetizzatore MIDI e spiegheremo come sia possibile, tramite un esempio concreto, realizzare una semplice applicazione Java che generi un brano musicale da noi stessi composto

Introduzione
In questa puntata verranno fornite tutte le conoscenze di base necessarie per un corretto utilizzo del sintetizzatore e delle sue principali funzionalità, tramite le classi ed interfacce fornite da Java a tale proposito.

Con un sintetizzatore dovrà essere possibile effettuare le seguenti operazioni di base: 
 

  • generare una o più note musicali (simultaneamente o singolarmente) ad una certa altezza;
  • selezionare un canale MIDI su cui le note verranno indirizzate e quindi ascoltate;
  • selezionare lo strumento musicale desiderato e, all’occorrenza, modificarlo;
  • regolare il volume delle singole note (ovvero la cosiddetta “velocity” delle note);
  • introdurre effetti sonori nella dinamica della nota;
  • far cessare il suono di una o più note.


Ovviamente, tutte queste operazioni sono consentite anche attraverso la creazione di una sequenza musicale, la quale potrà essere poi salvata in un file Midi e/o riascoltata all’occorrenza tramite un Sequencer. Tale  approccio verrà descritto dettagliatamente nella prossima puntata.
Per ora invece ci soffermeremo esclusivamente sullo studio della generazione di suoni da parte del sintetizzatore.
Per fare ciò avremo bisogno dei seguenti elementi fondamentali:
 

  • Uno o più strumenti musicali (in Java rappresentati dalla classe Instrument), reperibili da un “soundbank” (in Java rappresentato dall’interfaccia SoundBank), che a sua volta può essere quello di default del sintetizzatore (se esiste) oppure un banco prelevato dall’esterno.
  • Un sintetizzatore MIDI (che in Java è rappresentato dall’interfaccia Synthesizer) su cui vanno caricati gli strumenti e grazie al quale le note musicali possono essere generate (in seguito alla spedizione di opportuni messaggi ad un determinato canale MIDI).
  • Caricamento dei canali MIDI (in Java rappresentati dall’interfaccia MidiChannel) controllati dal sintetizzatore, che gestiscono le proprietà fondamentali di una nota (altezza, timbro, effetti sonori, controllers, stato di una o più note, tramite metodi come noteOn(int noteNumber, int velocity) .


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







Il codice dell’esempio
L’esempio che verrà proposto consiste in una applicazione Java che consente la riproduzione dell’accordo di Do Magg. da parte di uno strumento musicale precedentemente caricato sul sintetizzatore. 
Al termine dell’esecuzione musicale (che consiste, in questo caso, nella riproduzione dell’accordo stesso) verranno effettuate tutte le operazioni di chiusura del dispositivo aperto in precedenza (nel nostro caso il Synthesizer) e l’applicazione avrà termine.

Il codice dell’esempio riprenderà esattamente lo schema riportato in fig.1 (le voci inserite nelle varie caselle di testo corrispondono alle omonime classi e/o interfacce utilizzate in Java). 
Ma vediamo subito il codice nel suo complesso, per poi riprenderlo ed analizzarlo passo passo: 
 

Esempio n.2 (Come generare i suoni dal sintetizzatore)

import javax.sound.midi.*;

class Midi2 { 
    final int DO =  60;    // 60: altezza del DO centrale (DO3)
    final int MI =  64;
    final int SOL = 67;

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

    Instrument instruments[]; // array di strumenti musicali
    int numInstr=100;         // numero dello strumento 
    // (valore compreso fra 0 e 127)

    Synthesizer mySynthesizer; // il nostro sintetizzatore MIDI
    Soundbank sb;   // il nostro banco di strumenti
    MidiChannel channel;

   //durata dell'accordo espressa in millisecondi
    int durata = 3000; 

   // volume dell'accordo:
   // (valore compreso fra 0 e 127)
   int velocity = 100; 

  public Midi2(){ 
    System.out.println("Esempio di utilizzo del
      Sintetizzatore  MIDI");
    System.out.println();

    // Apertura del Synthesizer. . . 
    try {
      if (mySynthesizer == null) {
        mySynthesizer = MidiSystem.getSynthesizer();
        if (( mySynthesizer == null) {
          System.out.println("getSynthesizer() fallito!");
          return;
        } 
      } 
      mySynthesizer.open();
    }
    catch (Exception ex) { 
      ex.printStackTrace(); 
      return;
    }
 

  // Verifichiamo se esiste un banco MIDI di default 
  // per il nostro sistema:

  sb = mySynthesizer.getDefaultSoundbank();
  System.out.println ("Il banco di default del Synth " +
  (mySynthesizer.isSoundbankSupported(sb) ? " e' supportato." 
  :" non e' supportato."));
  System.out.println();

  // Se non esiste un banco di default lo si carica dall'esterno:
  if  (!mySynthesizer.isSoundbankSupported(sb)){
    try {
      FileInputStream is;
      is = new FileInputStream(new File("soundbank.gm"));
      sb = MidiSystem.getSoundbank(is); 
    }
    catch (Exception ex) { 
      ex.printStackTrace(); return;
    }

  // Se è stato possibile caricare un banco si utilizzano gli 
 //  strumenti musicali messi a disposizione dal banco stesso:

  if (sb != null) {
    System.out.println("Banco caricato: " + sb.getName());
    System.out.println();
    // Carichiamo ora gli strumenti musicali 
    //sul sintetizzatore:
    instruments = sb.getInstruments();
    mySynthesizer.loadAllInstruments(sb);

    //  Per caricare solo lo strumento ‘numInstr’  sul
    // synth si può sostituire l’ultima istruzione 
    // con la seguente:
    //  mySynthesizer.loadInstrument(instruments[numInstr]);

  }
  else{
    // Se non è stato possibile caricare un banco, 
    // si utilizzano gli strumenti musicali messi 
    // direttamente a disposizione dal Synth:
    System.out.println("Impossibile caricare un SoundBank...");
    System.out.println();
    instruments = mySynthesizer.getAvailableInstruments();
    mySynthesizer.loadInstrument(instruments[numInstr]); 
  }

  // Impostiamo i canali MIDI:
  MidiChannel[] channels = mySynthesizer.getChannels();
  channel = channels[0];

  // Selezioniamo lo strumento ‘numInstr’ dal corrente banco 
  // degli strumenti e generiamo l’accordo musicale:

  int prog = instruments[numInstr].getPatch().getProgram(); 
  channel.programChange(prog);

  for (int i = 0;i < accordoDo.length;i++){ 
    channel.noteOn(accordoDo[i],velocity);
  }
  System.out.println("Esecuzione dell'accordo di DO Magg.");
  System.out.println();
  System.out.println("Strumento:"+ 
                     ((instruments!=null) ?
                     instruments[numInstr].getName():
                     "non disponibile"));

   System.out.println();
   System.out.println("Volume:"+ String.valueOf(velocity)+"/127");
   System.out.println("Durata:"+ String.valueOf(durata)+
                      " millisecondi");
   System.out.println();

   // attesa di ‘durata’ millisecondi:

   long startTime = System.currentTimeMillis();
   while(System.currentTimeMillis()-startTime<durata) {} 

   // azzeriamo il volume di tutte le note e rimuoviamo gli 
   // strumenti musicali precedentemente caricati sul 
   //sintetizzatore:

   channel.allNotesOff();
   mySynthesizer.unloadAllInstruments(sb);
   System.out.println("Esecuzione terminata. Note azzerate.");
    // Chiusura del synthesizer. . .

   if (mySynthesizer.isOpen()) mySynthesizer.close();
      mySynthesizer = null;
   sb = null;
 }

  public static void main(String [] args){
     Midi2 app = new Midi2();
  }
}
 

Commento dell’applicazione Midi2
Analizziamo ora il codice più nel dettaglio:
Le costanti intere:

// 60: altezza del DO centrale
final int  DO  =  60; 
final int   MI  =  64;
final int  SOL = 67;

definiscono le altezze delle note che dovranno essere suonate.
Le nozioni relative all’altezza delle note che occorre conoscere sono le seguenti:

  • I valori consentiti per l’altezza delle note sono compresi nell’intervallo [0,127] ;
  • Più  grande è il valore numerico scelto in tale intervallo, più alto è il tono della nota;
  • La distanza fra due interi consecutivi nell’intervallo consentito corrisponde alla distanza tonale di un semitono.


Così, ad esempio, se volessimo far eseguire un DO# nell’ottava centrale dovremmo selezionare il valore 61 (ovvero il valore del Do centrale 60 +1= 61).
Il codice prosegue con la  dichiarazione:

Instrument instruments[];

che dichiara un array di strumenti. Gli strumenti  potranno essere prelevati da un SoundBank, che può essere quello di default del sintetizzatore (se esiste) oppure un banco prelevato dall’esterno.
Ricordo che, per consentire la generazione dei suoni da parte del sintetizzatore, è necessario che quest’ultimo sia  provvisto di strumenti musicali e canali Midi ai quali spedire tutte le informazioni relative alla modalità di esecuzione delle singole note ( come il volume, gli effetti sonori, o comandi precisi come noteOn() o noteOff(),  la selezione di un nuovo strumento etc.)
Per fare ciò, la prima cosa da fare è creare ed aprire un nuovo sintetizzatore, inserendo gli opportuni controlli:

try { 
  if (mySynthesizer == null) {
    if ((mySynthesizer = MidiSystem.getSynthesizer()) == null) {
       System.out.println("getSynthesizer() failed!");
       return;
    }
  } 
  mySynthesizer.open(); 
}
catch (Exception ex) {
  ex.printStackTrace();
  return;

Analogamente alle operazioni di creazione ed apertura del Sequencer (analizzate nella puntata precedente) per generare ed aprire il  Synthesizer sono necessarie le seguenti istruzioni:

// Creazione del Synth di default di  sistema
mySynthesizer = MidiSystem.getSynthesizer() 
// Apertura del nuovo Synth
mySynthesizer.open(); 

A questo punto occorre caricare gli strumenti musicali sul sintetizzatore. Per far ciò andiamo a creare il nostro SoundBank iniziando a verificare se, per caso, il nostro Synthesizer è in grado di fornircene uno di default:

Soundbank sb = mySynthesizer.getDefaultSoundbank();
System.out.println ("Il banco di default del Synth " +
     (mySynthesizer.isSoundbankSupported(sb) ? " e' supportato." : 
      " non e' supportato."));

Il metodo:

mySynthesizer.isSoundbankSupported(sb)

restituisce il valore booleano true solo nel caso in cui il banco sb sia supportato dal nostro sintetizzatore.
Nel caso il banco non sia supportato, tale  metodo restituisce il valore false ed allora è possibile procedere al caricamento di un banco dall’esterno, come segue:

if  (!mySynthesizer.isSoundbankSupported(sb)){
  try
    FileInputStream is:
    is = new FileInputStream(new File("soundbank.gm"));
    sb = MidiSystem.getSoundbank(is); 
  }
  catch (Exception ex) { 
    ex.printStackTrace(); 
    return;
  }

Qualora si sia  riusciti  - in un modo o nell’altro -  a caricare il nostro SoundBank, (ed in tal caso risulterà  sb!=null),  possiamo caricare tutti gli strumenti in esso contenuti sia,  ovviamente , sul nostro sintetizzatore, sia nell’array di strumenti precedentemente dichiarato, come segue:

if (sb != null) {
  instruments =  sb.getInstruments();
  mySynthesizer.loadAllInstruments(sb);
}

Ovviamente, nel caso non si debbano utilizzare  tutti gli strumenti presenti nel banco, non è necessario caricarli tutti sul sintetizzatore, ma è possibile caricare solo quelli di cui si ha realmente bisogno.  Ad esempio, relativamente al nostro caso, basterebbe caricare sul sintetizzatore lo strumento ‘numInstr’, come segue:

mySynthesizer.loadInstrument(instruments[numInstr]);

Nel caso invece non sia proprio possibile disporre di un SoundBank (ed in tal caso risulterà  sb = null), è comunque possibile ricorrere agli strumenti messi a disposizione direttamente dal sintetizzatore, tramite le istruzioni:

instruments = mySynthesizer.getAvailableInstruments();
mySynthesizer.loadInstrument(instruments[numInstr]);

Caricati gli strumenti musicali (o lo strumento singolo), carichiamo ora 
i canali Midi:

MidiChannel[] channels = mySynthesizer.getChannels();

e selezioniamo il primo canale cui verranno inviate, subito dopo, tutte le informazioni relative alle note da suonare:

channel = channels[0];

Con le istruzioni:

// numero dello strumento (valore compreso fra 0 e 127)
int numInstr=100; 
int prog = instruments[numInstr].getPatch().getProgram();
channel.programChange(prog); 

selezioniamo  lo strumento relativo al valore intero numInstr  dal banco degli strumenti corrente.
Ogni strumento musicale possiede un proprio oggetto di tipo Patch, il quale rappresenta la “locazione di memoria”, sul sintetizzatore MIDI, in cui quello specifico strumento è immagazzinato. Tale locazione viene a sua volta specificata da due valori interi: il primo è il banco di appartenenza dello strumento in questione (ottenibile tramite il metodo getBank()); il secondo è il cosiddetto program number (ottenibile tramite il metodo getProgram()), ovvero quel valore intero che identifica univocamente uno strumento all’interno del banco di appartenenza.
A questo punto non ci rimane che inviare i comandi appropriati per la generazione musicale delle note del nostro accordo.
Perché le note generate possano essere udite, è necessario regolare il volume ad un valore maggiore di 0 (e sempre minore di 128).

// volume dell'accordo: (valore compreso fra 0 e 127).
int velocity = 100; 

Più  grande è il valore numerico scelto in tale intervallo, più alto è il volume della nota.
Selezionando un valore pari a 0 o maggiore di 127 non sarà possibile udire il suono dell’accordo.
Il ciclo:

for (int i = 0;i < accordoDo.length;i++){ 
  channel.noteOn(accordoDo[i],velocity); 
}

contiene l’istruzione fondamentale

channel.noteOn(accordoDo[i],velocity); 

che consente al sintetizzatore di generare le note dell’accordo accordoDo[i] sul canale channel con un volume pari a velocity (un valore pari a zero sarebbe equivalente al comando channel.noteOff(accordoDo[i]), che azzera il volume della nota selezionata).
Relativamente al nostro esempio,  tutte e tre le note dell’accordo verranno suonate simultaneamente poiché, in pratica, il comando channel.noteOn(accordoDo[i],velocity)  all’interno del ciclo for   viene impartito nello stesso istante (o quasi…J)  per tutte e tre le note dell’accordo.
Analogamente, tutte le note cesseranno di suonare non appena verrà impartito il comando 

channel.allNotesOff();

Nel nostro esempio, questo comando verrà impartito dopo un tempo pari a

int durata = 3000;  // durata dell'accordo espressa in millisecondi.

tramite le istruzioni:

long startTime = System.currentTimeMillis();
// attesa di durata millisec.
 while(System.currentTimeMillis()-startTime<durata) {} 

che precedono, appunto, il comando channel.allNotesOff();.
Una volta terminata l’esecuzione musicale, sarà possibile rimuovere dal Synthesizer  tutti  gli strumenti caricati in precedenza e quindi chiudere il dispositivo, come segue:

mySynthesizer.unloadAllInstruments(sb);
if (mySynthesizer.isOpen())  mySynthesizer.close();
mySynthesizer = null;
 

Per concludere questo esempio è bene far notare, fra le informazioni visualizzate sullo schermo durante l’esecuzione dell’applicazione, la seguente:

System.out.println("Strumento:" + 
  ((instruments!=null) ? instruments[numInstr].getName():
  "non disponibile"));

che, nel caso si sia riusciti a caricare gli strumenti musicali, stampa sullo schermo il nome dello strumento correntemente in uso dal canale Midi selezionato; in caso contrario stampa un messaggio che avvisa l’utente del non caricamento dello strumento.
 
 
 

Conclusione del capitolo
L’uso del sintetizzatore è utile se si vogliono generare i suoni di un certo strumento con una certa rapidità e senza l’esigenza di salvare in una sequenza la successione di note generate. Infatti, il sintetizzatore si limita a generare dei suoni appena gli viene richiesto, ma non si può assumere il compito di tenere traccia della successione di note generate in  modo da poterle recuperare e riascoltare in un secondo momento.
Per assolvere a questo compito è necessario immagazzinare tutte le informazioni musicali in una sequenza (sequence) MIDI. In pratica, si tratta di costruire un file MIDI  “con le proprie mani”.
Nella prossima puntata vedremo, attraverso un semplice esempio di implementazione, come sia possibile realizzare una piccola composizione musicale in grado di essere poi salvata sotto forma di file MIDI. Scopriremo che l’impresa è tutt’altro che difficile!!!
 
 
 

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