MokaByte 97 - Giugno 2005
  MokaByte 97 - Giugno 2005  
di
Vincenzo Viola

 

 

 

Multimedialità su J2ME
II parte: gestione dell'audio

Questo articolo descrive la gestione dell'audio su piattaforma J2ME e le diverse e sempre più avanzate funzionalità che vengono offerte passando dal MIDP 1.0 all'ABB, e dalle MMAPI alle AMMS

Introduzione
In questo articolo vedremo come gestire l'audio sul nostro terminale mobile sia in termini di riproduzione, registrazione che di elaborazioni.
Nel MIDP 1.0 non è prevista una gestione standard dell'audio. E' la casa produttrice del dispositivo che fornisce un set di APIi proprietarie che permettono di utilizzare l'audio.
Ovviamente poiché i vari produttori forniscono delle diverse implementazioni, lo sviluppatore è costretto a riscrivere ogni volta le classi di gestione dell'audio per adattarle al dispositivo su cui fare il porting della midlet.
Il MIDP 2.0 include il cosiddetto Audio Building Block (ABB) che permette la riproduzione di toni, sequenze di toni e file WAV all'interno di applicazioni MIDP.
Le MMAPI costituiscono lo standard per la gestione dei contenuti multimediali e quindi in particolare per l'audio. Supportano i formati Wave, AU, MP3, MIDI e altri secondo le specifiche dei diversi dispositivi. Inoltre è possibile la registrazione e la sincronizzazione nella riproduzione simultanea di più pacchetti audio.
Infine le AMMS (Advanced Multimedia Supplements) permettono l'accesso alla radio e ad altri canali e sorgenti frequency-based, compreso l'RDS (Radio Data System); implementano particolari funzionalità di elaborazione audio come equalizzatore, effetti audio, riverbero artificiale, audio 3D; offrono la possibilità di direzionare l'uscita, ad esempio smistare l'audio a casse, microfono o cuffie.

 

Audio Building Block
Le API per ABB richiedono soltanto 6KB di spazio e supportano solamente la riproduzione di toni, sequenze di toni e WAV. Non è supportato il MIDI né la registrazione.
A livello implementativo è supportato il Player ma non il Datasource.
Le applicazioni realizzate con ABB sono compatibili con i dispositivi che usano le MMAPI in quanto ABB è un subset di MMAPI. Perciò quello che vedremo in seguito è ugualmente valido per le MMAPI.

Gli unici due controlli che l'ABB supporta sono:

  • VolumeControl: controlla il volume.
  • ToneControl: è un'interfaccia che abilita la riproduzione di sequenze di toni definite dall'utente.

Cominciamo dalla generazione di un tono. Utilizziamo un semplice metodo:

Manager.playTone(int note, int duration, int volume)

Il parametro "note" definisce il tono della nota in termini di frequenza. Il suo valore varia da 0 a 127 e può essere calcolato secondo la formula:

SEMITONE_CONST = 17.31234049066755 = 1/(ln(2^(1/12)))
note = ln(freq/8.176)*SEMITONE_CONST

Le note sono espresse in lettere (come in MIDI) e ad esempio la nota LA (A in MIDI) a 440Hz corrisponde al valore 69. Il volume va da un minimo di 0 ad un massimo di 100. La durata è espressa in millisecondi. Quindi per suonare un LA a 440Hz, per 1 sec al massimo del volume useremo:

try {
Manager.playTone(69, 1000, 100);
} catch (MediaException e) {
}

Per riprodurre una sequenza di toni abbiamo bisogno di un Player. Il Player viene costruito utilizzando il Manager e un particolare locator che specifica il tipo di riproduzione: Manager.TONE_DEVICE_LOCATOR. Una volta realizzato il Player occorre impostargli la sequenza di toni attraverso il suo ToneControl e poi avviare la riproduzione:

try {
Player p = Manager.createPlayer(Manager.TONE_DEVICE_LOCATOR);
p.realize();
ToneControl tc = (ToneControl)p.getControl("ToneControl");
tc.setSequence(mySequence);
p.start();
} catch (IOException ioe) {
} catch (MediaException me) {
}

L'interfaccia ToneControl specifica il formato di una sequenza di toni, la cui sintassi è descritta dalle notazioni ABNF (Augmented Backus-Naur Form):

sequence = version *1tempo_definition *1resolution_definition
*block_definition 1*sequence_event

version = VERSION version_number
VERSION = byte-value
version_number = 1 ; version # 1

tempo_definition = TEMPO tempo_modifier
TEMPO = byte-value
tempo_modifier = byte-value //multiplo di 4 per ottenere il tempo in bmp

resolution_definition = RESOLUTION resolution_unit
RESOLUTION = byte-value
resolution_unit = byte-value

block_definition = BLOCK_START block_number
1*sequence_event
BLOCK_END block_number
BLOCK_START = byte-value
BLOCK_END = byte-value
block_number = byte-value //block_number in BLOCK_END deve essere
//uguale a quello specificato in BLOCK_START

sequence_event = tone_event / block_event /
volume_event / repeat_event

tone_event = note duration
note = byte-value //nota che deve essere suonata
duration = byte-value //durata della nota

block_event = PLAY_BLOCK block_number
PLAY_BLOCK = byte-value
block_number = byte-value

volume_event = SET_VOLUME volume
SET_VOLUME = byte-value
volume = byte-value //nuovo volume

repeat_event = REPEAT multiplier tone_event
REPEAT = byte-value
multiplier = byte-value //numero di volte che il tono deve essere
//ripetuto

byte-value = -128 - 127

La seguente tabella mostra i range dei parametri:

Una sequenza di toni viene quindi specificata come una lista di coppie tono-durata e blocchi di sequenze definiti dall'utente. Il tutto viene inserito in un array di byte.
Vediamo un esempio di come costruire una sequenza di toni, composta da tre blocchi:

byte tempo = 30; // settiamo il tempo a 120 bpm
byte d = 8; // 8 note

byte C4 = ToneControl.C4;
byte D4 = (byte)(C4 + 2); // a whole step
byte E4 = (byte)(C4 + 4); // a major third
byte G4 = (byte)(C4 + 7); // a fifth
byte rest = ToneControl.SILENCE; // resto

byte[] mySequence = {
ToneControl.VERSION, 1, // versione 1
ToneControl.TEMPO, tempo, // settiamo il tempo
ToneControl.BLOCK_START, 0, // iniziamo a definire la prima sezione "A"
E4,d, D4,d, C4,d, E4,d, // contenuto della prima sezione
E4,d, E4,d, E4,d, rest,d,
ToneControl.BLOCK_END, 0, // fine della prima sezione
ToneControl.PLAY_BLOCK, 0, // riproduciamo la prima sezione
D4,d, D4,d, D4,d, rest,d, // riproduciamo la seconda sezione
E4,d, G4,d, G4,d, rest,d,
ToneControl.PLAY_BLOCK, 0, // ripetiamo la prima sezione
D4,d, D4,d, E4,d, D4,d, C4,d // riproduciamo la terza sezione
};

Vediamo infine come riprodurre un file WAV che si trova rispettivamente su un server http, in un MIDP RMS (Record Management System), nel file JAR:

...
try {
Player p = Manager.createPlayer ("http://server/test.wav");
p.start();
} catch(IOException ioe) {
} catch(MediaException e) {
}
...

...
RecordStore store;
int id;
// play back from a record store
try {
InputStream is = new ByteArrayInputStream
(store.getRecord(id));
Player player = Manager.createPlayer(is, "audio/X-wav");
p.start();
}
catch (IOException ioe) {
}
catch (MediaException me) {
}
...

...
try {
InputStream is =
getClass().getResourceAsStream("test.wav");
Player player = Manager.createPlayer(is, "audio/X-wav");
p.start();
}
catch(IOException ioe) {
}
catch(MediaException me) {
}
...

 

MMAPI
Abbiamo già visto come riprodurre un file WAV. Allo stesso modo si possono riprodurre file di formato diverso, ricordando di passare al metodo Manager.createPlayer(InputStream is, String contentType) il corretto content type espresso secondo la sintassi MIME:

  • Wave audio files: audio/x-wav
  • AU audio files: audio/basic
  • MP3 audio files: audio/mpeg
  • MIDI files: audio/midi
  • Sequenze di toni: audio/x-tone-seq

Nell'articolo http://www.mokabyte.it/2005/04/j2me_multimedia.htm abbiamo introdotto le MMAPI attraverso i concetti di Player, Manager, Datasource, Control. Approfondiamo ora il discorso introducendo delle caratteristiche delle MMAPI che saranno poi utilizzate anche nei successivi articoli, in cui parleremo di immagini e video.

Noi possiamo "ascoltare" gli eventi di un Player, registrandoli attraverso un PlayerListener. L'interfaccia PlayerListener dichiara un solo metodo playerUpdate ( ), che viene invocato ogni volta che il Player riceve un evento. Sarà l'utente ad implementare questo metodo per decidere come l'applicazione deve comportarsi quando si verifica un determinato evento.
Le stringhe che descrivono l'evento sono delle variabili statiche nell'interfaccia PlayerListener. La maggiorparte di loro sono autoesplicative:

BUFFERING_STARTED, BUFFERING_STOPPED, CLOSED, DEVICE_AVAILABLE, DEVICE_UNAVAILABLE, DURATION_UPDATED, END_OF_MEDIA, ERROR, RECORD_ERROR, RECORD_STARTED, RECORD_STOPPED, SIZE_CHANGED, STARTED, STOPPED, STOPPED_AT_TIME, e VOLUME_CHANGED.

Si noti come i cambiamenti di stato di un Player corrispondono a degli eventi quali CLOSED, STARTED e STOPPED.
Un Player potrebbe stopparsi per diversi motivi. L'evento END_OF_MEDIA si verifica quando è stato riprodotto l'intero flusso audio. L'evento STOPPED_AT_TIME si verifica quando il Player si ferma ad un determinato istante specificato nello StopTimeControl. L'evento STOPPED si verifica solo quando viene invocato il metodo stop ( ) del Player.
L'evento DEVICE_UNAVAILABLE si verifica quando arriva una chiamata. Quando la chiamata termina si verifica l'evento DEVICE_AVAILABLE.
La classe Player fornisce metodi per registrare e rimuovere oggetti PlayerListener:

void addPlayerListener (PlayerListener listener)
void removePlayerListener (PlayerListener listener)

Nell'interfaccia Player esistono dei metodi che servono a ottenere informazioni sul flusso audio (in generale sul flusso multimediale):

String getContentType ()
long getDuration ()
long getMediaTime ()
int getState ()
TimeBase getTimeBase ()

I seguenti metodi settano, rispettivamente, il numero di volte che il Player riproduce lo stesso flusso (loop), la posizione temporale del Player aperto sul flusso, una nuova base dei tempi (TimeBase) per sincronizzare il Player con un altro:

void setLoopCount (int count)
long setMediaTime (long now)
void setTimeBase (TimeBase master)

Rispetto all'ABB le MMAPI permettono la registrazione, supportano un numero maggiore di formati e forniscono oltre a ToneControl e VolumeControl i seguenti controlli:

  • MIDIControl: fornisce l'accesso ai dispositivi per la trasmissione e la renderizzazione di dati MIDI. Attraverso questo controllo è possibile ottenere o settare i volumi e assegnati programmi per ognuno dei 16 canali MIDI.
  • PitchControl: aumenta o diminuisce il pitch di riproduzione senza cambiarne la velocità.
    L'argomento del metodo setPitch ( int millisemitones ) è espresso in "milli-semitoni" ovvero mille volte il numero di semitoni di cui si vuole aumentare il pitch o diminuirlo se il valore è negativo.
  • RateControl: controlla la velocità di riproduzione. Viene utilizzato per settare il tempo come multiplo del tempo assoluto del Player. La velocità, definita in "milli-percentuale", è il rapporto tra il tempo del Player e l'attuale base dei tempi. Ad esempio, una velocità di 200000 (2 x 100% x 1000) indica che ogni secondo della base dei tempi (il tempo reale) il Player riproduce due secondi di flusso audio ovvero va più veloce. Se il valore della velocità è negativo il Player rallenta. La velocità di default è 100000. Per settare la velocità si usa il metodo setRate (int millirate). Utilizzando getMaxRate ( ) e getMinRate ( ) si ottengono la massima e la minima velocità supportate dal dispositivo.
  • TempoControl: controlla il "tempo" di una canzone MIDI. Il tempo di una canzone di solito è specificato in bpm (battute al minuto). Il metodo setTempo (int millibeats) prende come argomento "milli-battute", quindi un valore di 120000 significa 120 bpm.
  • RecordControl: registra quello che viene mostrato dal Player.
  • StopTimeControl: abilita l'applicazione a definire un tempo presettato di stop per il Player. L'argomento del metodo setStopTime (long time) è espresso in microsecondi.

Alcuni di questi controlli vengono utilizzati non solo per l'audio ma anche nella gestione dei flussi video.
Per quanto riguarda VolumeControl, già visto per l'ABB, il volume si cambia utilizzando il metodo setVolume (int volume) dove l'argomento va da 0 a 100. Se il volume cambia durante la riproduzione viene generato un evento VOLUME_CHANGED.

Un ultimo controllo è quello che viene utilizzato per registrare l'audio (in generale flussi multimediali): RecordControl. Vediamo in un esempio come fare.

try {
Player p = Manager.createPlayer("capture://audio ?rate=8000&bits=16 ");
p.realize();
RecordControl rc = (RecordControl)p.getControl("RecordControl");
ByteArrayOutputStream output = new ByteArrayOutputStream();
rc.setRecordStream(output);
rc.startRecord();
p.start();
Thread.currentThread().sleep(5000);
rc.commit();
p.close();
} catch (IOException ioe) {
} catch (MediaException me) {
} catch (InterruptedException ie) { }

Nella URI locator passata al createPlayer viene data l'informazione sul tipo di flusso ("capture://audio") e sulla codifica, in questo caso velocità di 8000 bit al secondo e campionamento (16 bit). Una volta ottenuto il RecordControl viene impostato il ByteArrayOutputStream sul quale i dati verranno salvati (rc.setRecordStream(output)). Viene quindi avviata la registrazione: rc.startRecord( ). Quando si vuole terminare la registrazione (in questo caso dopo 5 sec.) si invoca rc.commit( ).

Infine vediamo come con le MMAPI sia possibile ascoltare la radio se il terminale ne è dotato. La cosa è molto semplice, basta creare il Player utilizzando per il locator la seguente sintassi:: "capture://radio" [ "?" tuner_params ] dove:

tuner_params = tuner_param *( "&" tuner_param )
tuner_param = "f=" frequenza /
"mod=" modulazione /
"st=" modalità stereo /
"id=" program_id /
"preset=" preset
freq = megahertz /
kilohertz /
hertz
megahertz = pos_integer "M" /
pos_integer "." pos_integer "M"
kilohertz = pos_integer "k" /
pos_integer "." pos_integer "k"
hertz = pos_integer
modulation = "fm" / "am"
stereo_mode = "mono" / "stereo" / "auto"
program_id = alpanumerico /* identifica un canale FM tramite il program
service name (PS) inviato via RDS (Radio
Data System)
preset = pos_integer

Ad esempio:

capture://radio?f=91.9M&st=auto
(91.9 MHz con modalità stereo=auto)
capture://radio?f=558k&mod=am
(558 kHz in modulazione d'ampiezza)
capture://radio?id=yleq
(canale FM channel il cui PS è "yleq")


AMMS
Abbiamo già visto nell'articolo precedente come, riguardo l'audio, AMMS offre tre macroblocchi di funzionalità:

  • Tuner
  • Audio Effect
  • 3D Audio

Le funzionalità sono implementate sotto forma di controlli. Abbiamo già descritto i diversi controlli specifici di ogni macroblocco e quelli generali riguardanti l'audio. Vediamo quindi ora più nello specifico come realizzare qualche interessante applicativo audio. Per quanto riguarda la radio, se il terminale ne è dotato, l'accesso avviene come visto per le MMAPI.
Attraverso il TunerControl possiamo selezionare una stazione ad una determinata frequenza e modulazione, impostare una riproduzione di tipo stereo, salvare la stazione in un determinato slot e poi cambiare stazione:

Manager.createPlayer("capture://radio");
TunerControl tuner = (TunerControl)
player.getControl("javax.microedition.media.control.tuner.TunerControl");
int frequencyFound = tuner.seek(970000, TunerControl.MODULATION_FM, true);
tuner.setStereoMode(TunerControl.STEREO);
tuner.setPreset(1);
tuner.setPresetName(1, "Radio 1");
if(tuner.getNumberOfPresets()>=2) {
tuner.usePreset(2);
int secondFrequency = tuner.getFrequency(); // per mostrarla all'utente
String modulation = tuner.getModulation(); // per mostrarla all'utente
}

Se l'RDSControl è supportato possiamo accedere ai dati RDS su una frequenza FM selezionata. Vediamo come estrarre alcune informazioni per mostrarle all'utente e poi deselezioniamo il TA ovvero il cambiamento automatico di stazione che avviene nel caso di annunci sul traffico:

if((RDSControl rds = (RDSControl)
radio.getControl("javax.microedition.media.control.tuner.RDSControl"))
!= null) {
Date date = rds.getCT();
boolean ta = rds.getTA();
String ps = rds.getPS();
String pty = rds.getPTYString(true);
rds.setAutomaticTA(false);
}

Sulla riproduzione dell'audio esistono controlli in grado di modificare il suono, l'equalizzazione e il panning.
Di seguito vediamo come recuperare le impostazioni dell'equalizzatore e sceglierne una che preferiamo:

EqualizerControl equalizer = (EqualizerControl)
player.getControl("javax.microedition.media.control.audioeffect.EqualizerControl");
String[] presets = equalizer.getPresetNames();
equalizer.setPreset("rock");

Inoltre è possibile modificare i livelli dei bassi e degli alti. Ad esempio settiamo i bassi al valore "flat" o normale e gli alti al massimo:

equalizer.setBass(50);
equalizer.setTreble(100);

Per un controllo più preciso del suono possiamo far uso di un'equalizzazione multibanda, se il dispositivo la supporta. In questo caso dobbiamo prima sapere quante bande e che livelli di banda il dispositivo supporta:

int numberOfBands = equalizer.getNumberOfBands();
int minLevel = equalizer.getMinBandLevel();
int maxLevel = equalizer.getMaxBandLevel();

Possiamo anche ottenere le frequenze delle prime due bande:

int firstBandFrequency = equalizer.getCenterFreq(0);
int secondBandFrequency = equalizer.getCenterFreq(1);

Se vogliamo portare le frequenze vocali al massimo dobbiamo ricavare la banda corrispondente a 3kHz e impostargli il livello massimo:

int bandNumber = equalizer.getBand(3000000);
if(bandNumber!=-1) {
equalizer.setBandLevel(maxLevel, bandNumber);
}

Le funzionalità 3D audio sono abbastanza complicate sia dal punto di vista della comprensione che della programmazione.
Come già detto nel precedente articolo le API permettono allo sviluppatore di costruire una rete di Player che possono essere combinati e ai quali si possono applicare degli effetti audio
Nei seguenti esempi costruiremo tre Player p1, p2 e p3 con differenti suoni. p1 avrà una sorgente sonora 3D (SoundSource3D) localizzata nello spazio 3D circostante (LocationControl). I settaggi effettuati sono volti ad attenuare il suono mentre viene trasmesso, attraverso lo spazio, alla nostra posizione di ascolto, che definiremo successivamente:

SoundSource3D source = GlobalManager.createSoundSource3D();
source.addPlayer(p1);
LocationControl locationSource = (LocationControl)
source.getControl("javax.microedition.media.control.audio3d.LocationControl");
locationSource.setCartesian(0, 0, -10000); // 10 metri di fronte (asse z negativa)
DistanceAttenuationControl distanceSource = (DistanceAttenuationControl)
source.getControl("javax.microedition.media.control.audio3d.DistanceAttenuationControl");
distanceSource.setParameters(10, 50000, true, 1000);
Ora definiamo "quanto" suono subisce l'effetto riverbero (in questo caso -2dB):
if((ReverbSourceControl reverbSource = (ReverbSourceControl)
source.getControl("ReverbSourceControl")) != null) {
reverbSource.setLevel(-200);
}

Proviamo ora ad applicare degli effetti 2D (Audio Effect) ai Player p2 e p3, ad esempio un EffectModule ovvero un effetto che va a modificare il modulo del segnale audio, come il "flanger". Ricordiamo che il flanger è un effetto sonoro che sovrappone al segnale originale un segnale identico ma rallentato, ottenendo una modulazione costante.
Il controllo che regola il flanger è il ChorusControl:

EffectModule effect = GlobalManager.createEffectModule();
effect.addPlayer(p2);
effect.addPlayer(p3);
if((ChorusControl chorusEffect = (ChorusControl)
effect.getControl("javax.microedition.media.control.audioeffect.ChorusControl"))
!= null) {
String[] presets = chorusEffect.getPresets();
chorusEffect.setPreset("flanger");
chorusEffect.setModulationDepth(4000);
chorusEffect.setModulationRate(260);
}

Infine proviamo a spostare la postazione dell'ascoltatore nello spazio 3D usando GlobalManager's Spectator. Usiamo di nuovo il LocationControl e anche l'OrientationControl . L'orientazione è definita attraverso la rotazione intorno agli assi x-y-z, come in Fig.1:

 


Figura 1
- Orientazione dell'audio

Settiamo l'heading (rotazione intorno all'asse y) a 10°, il pitch (rotazione intorno all'asse x) a 5° e il roll (rotazione intorno all'asse z) a 5°:

Spectator spectator = GlobalManager.getSpectator();
LocationControl locationSpectator = (LocationControl)
spectator.getControl("javax.microedition.media.control.audio3d.LocationControl");
locationSpectator.setCartesian(0, 0, 0); // The origin (default)
OrientationControl orientationSpectator = (OrientationControl)
spectator.getControl("javax.microedition.media.control.audio3d.OrientationControl");
orientationSpectator.setOrientation(10, 0, 5);

 

Conclusioni
Ormai la maggiorparte dei terminali mobili Java supportano le MMAPI perciò è possibile realizzare applicazioni che coinvolgono l'audio abbastanza avanzate.
Le AMMS forniscono delle funzionalità che la maggior parte dei dispositivi ancora non supporta. E' uno dei pochi casi in cui il software non dispone ancora di un hardware adeguato, ma è anche un sintomo di come, rispetto al passato, si voglia mettere utenti e sviluppatori di sfruttare al meglio i propri terminali nel momento dell'acquisto, senza dover aspettare aggiornamenti di firmware e software che spesso arrivano quando il modello del proprio cellulare è già obsoleto!

 

Bibliografia
[1] http://jcp.org/en/jsr/detail?id=234
[2] http://java.sun.com/products/mmapi/index.jsp
[3] http://java.sun.com/products/midp/index.jsp


Vincenzo Viola
è nato a Formia (LT) il 03/11/1977, laureato in Ingegneria delle Telecomunicazioni presso l'Università Federico II di Napoli con una tesi sulla segmentazione automatica di video digitali in scene, con sviluppo software implementato su piattaforma Java - Oracle. Lavora come progettista software per una Mobile Company che offre la sua consulenza ai gestori di telefonia e alle major del settore ICT.