Perché Java Sound API?
È lecito porre la domanda in quanto già esiste Java Media Framework che si occupa di gestire formati multimediali. Diciamo che laddove sia necessaria la sola esecuzione di file multimediali è senz'altro sufficiente Java Media Framework, mentre dovunque siano richieste funzionalità più avanzate come elaborazione, registrazione e mixaggio di audio digitale la Java Sound API trova la sua ideale collocazione. Fra i formati audio supportati ricordiamo: MIDI, WAV, AU, AIFF. L'elaborazione audio in generale richiede un grosso lavoro a livello di processore, e la maggior parte dei sistemi di sviluppo audio sono fortemente basati su codice specifico per la piattaforma utilizzata. Java Sound utilizza invece uno strato di astrazione che isola la specificità architetturale dal lavoro dello sviluppatore, che diventa così realmente "platform-independent".
Per di più, Java Sound si pone come potenziale servizio a supporto di altre tecnologie Java. Ad esempio le Java 3D API, con l'introduzione del suono tridimensionale, o lo stesso Java Media Framework, per il quale Java Sound può mettere a disposizione il proprio timer, permettendo la sincronizzazione di fonti multimediali diverse, come suono e immagine, fino alla Java Speech API, per la quale è richiesta l'interazione con microfono e cuffie.
Architettura dell'API
In un sistema audio su computer possiamo distinguere dispositivi di due tipi: hardware e virtuali. I primi possono essere lettori di CD, schede audio, microfoni etc., mentre fra i secondi rientrano in generale dei moduli software, come ad esempio un programma mixer, un lettore MIDI, un sintetizzatore software etc. Più dispositivi virtuali possono utilizzare lo stesso dispositivo hardware, quindi ad esempio mixer e amplificatore software utilizzeranno per l'output la stessa scheda sonora. Ad un dispositivo hardware sono associate una o più porte che possono essere di input (microfono etc.) o di output (cuffie etc.). I dispositivi virtuali infine utilizzano dei canali multimediali per comunicare con il dispositivo hardware.
E fin qui potrebbe sembrare solo teoria molto astratta. In realtà ogni oggetto che abbiamo nominato ha un suo oggetto corrispondente nella Java Sound API; così un dispositivo hardware è un SystemDevice, un dispositivo virtuale è un ControlDevice, una porta è una Port e un canale multimediale è un MediaChannel.
Altri oggetti
L'oggetto base per la sincronizzazione è javax.media.Time. Questo oggetto supporta una precisione fino al nanosecondo. Esistono poi altre classi di Timer per i dati MIDI e per i campioni audio. È facile comprendere quanto l'accuratezza di questo oggetto sia importante, soprattutto nel caso in cui si presenti la necessità di intervenire su campioni di suono registrati, o nel caso si debbano sincronizzare sorgenti multimediali diverse.
Il trasporto dei dati viene effettuato mediante javax.media.Buffer. Un Buffer è un contenitore generico per diversi tipi di dati, siano essi suono, video o testo. Vengono gestiti due modelli di trasporto: "pull" e "push". In generale possiamo dire che il primo viene utilizzato per l'elaborazione del suono, il secondo per la cattura di campioni.
Per quanto riguarda la gestione degli eventi distinguiamo:
- TransportEvents, generati quando cambia lo stato di un oggetto implicato nel trasporto di dati.
- AudioControlEvents, riguardanti lo stato dei controlli audio-specifici come guadagno, volume etc.
- FormatChangeEvents, legati ad oggetti per i quali subentri una modifica del formato di dati trasportati.
Infine è indispensabile introdurre l'AudioManager, che è l'oggetto di ingresso nella Java Sound. Attraverso di esso si ottiene l'accesso ai vari SystemDevices e ControlDevices.
Al solito, il modo migliore per vedere in dettaglio le caratteristiche di questo tipo di prodotti è la pratica, quindi vediamo ora passo passo le fasi necessarie alla costruzione di un'applicazione per l'esecuzione di un brano MIDI o RMF. Al momento la Java Sound supporta i file MIDI di tipo 0 e 1. Ho preso come esempio l'applicazione MidiFileApp distribuita come esempio insieme alla Java Sound, trascurando tutta la gestione grafica e degli eventi, in modo da focalizzare l'attenzione sulla Java Sound API.
I package da importare sono tutti sotto javax.media, e nel nostro caso bastano i seguenti:
import javax.media.*; import javax.media.format.*; import javax.media.format.midi.*; import javax.media.transport.*; import javax.media.sound.*; import javax.media.sound.midi.*;
In più avremo bisogno di:
import java.io.*; import java.net.*;
per la gestione dei file e per l'oggetto URL.
Iniziamo la classe e dichiariamo il player della sequenza:
public class ProvaMidi {
SequencePlayer channel = null;
Per tutti gli errori che potranno essere generati lanciamo una MediaException, che verrà gestita nel main, che crea l'istanza della classe ProvaMidi. In questo modo semplifichiamo la gestione degli errori. Questo è possibile perché nel nostro caso un errore genera sempre lo stesso tipo di comportamento, cioè la visualizzazione di una stringa di errore e l'uscita dal programma.
public ProvaMidi(String file) throws MediaException {
Creiamo ora un FileInputStream da associare ad un MediaStream per la lettura del file MIDI.
MediaStream mediaStream;
InputStream is = null;
try {
URL url = new URL(file);
is = url.openStream();
} catch (Exception e) {
try {
is = new FileInputStream(file);
} catch (Exception e2) {
throw new MediaException("Impossibile accedere al file: " + file);
}
}
Costruiamo un MediaStream per il file MIDI. Notiamo che il MediaStream è creato nell'AudioManager, che è il punto di accesso alla Java Sound API. Facciamo tutti i controlli sul file, e cioè se esiste e se il suo formato è valido.
try {
mediaStream = AudioManager.newMediaStream(is);
} catch (Exception e) {
throw new MediaException("Controllare file " + file + " sia valido");
}
if (!(mediaStream.getFormat() instanceof MidiFileFormat)) {
throw new MediaException("Not a MIDI file: " +
mediaStream.getFormat().toString());
}
Creiamo ora sul dispositivo virtuale MIDI un "player" MIDI, che è poi l'esecutore del brano.
try {
MidiOutControlDevice controlDevice =
(MidiOutControlDevice) AudioManager.getControlDevice(Class.forName(
"javax.media.sound.midi.MidiOutControlDevice"), null);
channel = controlDevice.newSequencePlayer();
} catch (Exception e) {
throw new MediaException(
"MidiOutControlDevice non disponibile sul sistema");
}
Carichiamo la sequenza di dati MIDI nel "player" appena creato.
try {
channel.load(mediaStream);
} catch (Exception e) {
throw new MediaException(
"Controllare che il file " + file + " esista e sia valido");
}
Allochiamo le risorse per il MediaChannel e facciamo partire l'esecuzione del brano.
try {
channel.acquire();
channel.start();
} catch (ResourceUnavailableException e) {
throw new MediaException("Dispositivo audio non disponibile");
}
}
Il metodo main si occupa di istanziare la classe ProvaMidi e di intercettare gli errori generati. Il nome del brano MIDI o RMF deve essere passato come parametro dalla riga comando.
public static void main(String[] args) {
if (args.length == 0) {
System.err.println(
"\nÈ necessario specificare un file MIDI come parametro.\n");
} else {
try {
ProvaMidi app = new ProvaMidi(args[0]);
} catch (MediaException e) {
System.err.println(
"Impossibile eseguire l'applicazione: " + e.getMessage());
}
}
}
}
Per evitare di dover gestire degli eventi, cosa che avrebbe spostato la focalizzazione su altre tematiche, nell'applicazione non viene gestito lo stop del brano. La cosa è però molto semplice. Infatti, una volta creato il "player" MIDI, che abbiamo chiamato channel, esso è in grado di gestire tutti i metodi di controllo dell'esecuzione del brano. Così ad esempio:
channel.stop(); // Ferma l'esecuzione del brano. channel.setDB(float gain); // Stabilisce il guadagno in decibel. channel.setPan(float pan); // Stabilisce il panning (destra, sinistra, centro). channel.fade(float gain, Time time); // Porta il guadagno a "gain" nel tempo "time". channel.setMute(boolean mute); // Attiva/disattiva il "muto".
E così via. Come si vede la quantità di azioni intraprendibili è notevole, malgrado ci siamo soffermati solo sull'oggetto SequencePlayer. Questo può dare però un'idea di cosa sia possibile fare con questa potente API. Ribadiamo il fatto che la Java Sound API è da considerarsi una API a basso livello, e come tale va ad interagire spesso con parametri specialistici del suono, e quindi è giustificabile il suo utilizzo laddove queste finezze siano richieste.
Stato dei lavori
La versione testata è la 0.8 Early Access. I formati audio supportati sono: WAVE, AIFF/AIFC, AU, MIDI tipo 0 e 1, e RMF. Le piattaforme supportate sono: Solaris, Win-NT, Windows 95/98. È previsto il supporto per Solaris x86 nelle successive versioni. È richiesto il JDK 1.1 oppure Java 2.
Previste, ma non ancora implementate, numerose caratteristiche importanti: MIDI input da dispositivi esterni, esecuzione in loop di brani MIDI, gestione di Soundbank (banchi di suoni), effetti come il riverbero o il pitch. Sono presenti alcuni piccoli bug, ma se ne assicura l'eliminazione nella versione ufficiale, la 1.0.
Conclusioni
Un solo articolo non poteva essere minimamente esaustivo, data la vastità di oggetti implementati nell'API; ciò che abbiamo tentato di fare è stato di dare una panoramica chiara sul tipo di prodotto presentato. Sarebbe stato interessante anche vedere il trattamento dei file audio digitali e la cattura di audio, ma la struttura di base rimane quella mostrata, quindi non dovrebbe essere difficile sperimentare le altre caratteristiche della Java Sound.
L'impressione suscitata dall'uso dell'API è molto positiva. Innanzitutto l'idea di base, cioè quella di fornire uno strumento di sviluppo a basso livello per le esigenze musicali più avanzate non può che riscuotere il plauso degli addetti ai lavori, e poi lo sforzo fatto per presentare da subito un prodotto piuttosto stabile e potente si nota immediatamente.
Aspettiamo ardentemente le versioni successive con le implementazioni delle caratteristiche mancanti, per portare finalmente anche il sequencing sulla strada della "platform independence".
