di Stefano Leone Monni
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):
XXX_FIGURA N.1_XXX (art_MIDI_2_fig1.gif)
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;
int durata = 3000;
// durata dell'accordo espressa
// in millisecondi
int velocity = 100; // volume dell'accordo: // (valore compreso fra 0 e 127)
public Midi2()
{
System.out.println("Esempio di utilizzo del Sintetizzatore
MIDI");
System.out.println();
// Apertura del Synthesizer. . .
try {
if (mySynthesizer == null) {
if ((mySynthesizer =
MidiSystem.getSynthesizer()) == 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 = 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();
}
}
N.B: Le parti di codice di non interesse (o relative ad un argomento già trattato nella puntata precedente) sono state inserite in nero; quelle che verranno commentate sono state evidenziate in blu. Questa procedura sarà seguita anche in seguito.
Commento dell’applicazione Midi2
Analizziamo ora il codice più nel dettaglio:
Le costanti intere:
final int DO = 60;
// 60: altezza del DO centrale
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:
mySynthesizer = MidiSystem.getSynthesizer() // Creazione del Synth di default di
// sistema
mySynthesizer.open();
// Apertura del nuovo Synth
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 = 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:
int numInstr=100;
// numero dello strumento (valore compreso fra 0 e 127)
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).
int velocity = 100;
// volume dell'accordo: (valore compreso fra 0 e 127).
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…) 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();
while(System.currentTimeMillis()-startTime<durata) {
}
// attesa di durata millisec.
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