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.
|