Multimedialità su J2ME

II parte: la gestione dell‘audiodi

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 API 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_eventversion = VERSION version_numberVERSION = byte-valueversion_number = 1 ; version # 1tempo_definition = TEMPO tempo_modifierTEMPO = byte-valuetempo_modifier = byte-value //multiplo di 4 per ottenere il tempo in bmpresolution_definition = RESOLUTION resolution_unitRESOLUTION = byte-valueresolution_unit = byte-valueblock_definition = BLOCK_START block_number 1*sequence_event BLOCK_END block_numberBLOCK_START = byte-valueBLOCK_END = byte-valueblock_number = byte-value //block_number in BLOCK_END deve essere //uguale a quello specificato in BLOCK_STARTsequence_event = tone_event / block_event / volume_event / repeat_eventtone_event = note durationnote = byte-value //nota che deve essere suonataduration = byte-value //durata della notablock_event = PLAY_BLOCK block_numberPLAY_BLOCK = byte-valueblock_number = byte-value volume_event = SET_VOLUME volumeSET_VOLUME = byte-valuevolume = byte-value //nuovo volumerepeat_event = REPEAT multiplier tone_eventREPEAT = byte-valuemultiplier = byte-value //numero di volte che il tono deve essere //ripetutobyte-value = -128 - 127La tabella di figura 1 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; // restobyte[] mySequence = {ToneControl.VERSION, 1, // versione 1ToneControl.TEMPO, tempo, // settiamo il tempoToneControl.BLOCK_START, 0, // iniziamo a definire la prima sezione "A"E4,d, D4,d, C4,d, E4,d, // contenuto della prima sezioneE4,d, E4,d, E4,d, rest,d, ToneControl.BLOCK_END, 0, // fine della prima sezioneToneControl.PLAY_BLOCK, 0, // riproduciamo la prima sezioneD4,d, D4,d, D4,d, rest,d, // riproduciamo la seconda sezioneE4,d, G4,d, G4,d, rest,d,ToneControl.PLAY_BLOCK, 0, // ripetiamo la prima sezioneD4,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 storetry {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 pubblicato su Mokabyte ad Aprile 2005 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=" presetfreq = megahertz /kilohertz /hertzmegahertz = pos_integer "M" /pos_integer "." pos_integer "M"kilohertz = pos_integer "k" /pos_integer "." pos_integer "k"hertz = pos_integermodulation = "fm" / "am"stereo_mode = "mono" / "stereo" / "auto"program_id = alpanumerico /* identifica un canale FM tramite il program service name (PS) inviato via RDS (RadioData 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‘utenteString 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.2:

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

Condividi

Pubblicato nel numero
97 giugno 2005
Nato a Formia (LT) il 03/11/1977, laureato in Ingegneria delle Telecomunicazioni presso l‘Università Federico II di Napoli. Lavora a Roma, come progettista software per una Mobile Company che offre la sua consulenza ai gestori di telefonia e alle major del settore ICT.
Articoli nella stessa serie
Ti potrebbe interessare anche