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
|