Home
Accesso / Registrazione
 di 

ARM Cortex-M3: regolare il volume numericamente

Quando leggo i campioni audio da un file wave
https://ccrma.stanford.edu/courses/422/projects/WaveFormat/
ciascuno è formato da 2 byte in little-endian, dove il valore complessivo è un
signed 16-bit in complemento a 2.

Quando vado a comandare il DAC devo trasformarli in unsigned, quindi faccio qualcosa del genere:

dac = (((unsigned long) (g_pucAudioBuf[g_ulReadIdx + 1] ^ 0x80)) << 8) +
((unsigned long) (g_pucAudioBuf[g_ulReadIdx]));

ora, mi si chiede di regolare il volume numericamente, tramite un
parametro che può essere un float (da 0 a 1) oppure un intero (da 0 a 1024).

Secondo voi, quale è il metodo più performante dal punto di vista della
velocità di esecuzione per eseguire tale operazione?

Sto lavorando un ARM Cortex-M3.

L'idea ovvia è di comporre il valore a 16 bit con segno (quindi senza ^0x80) moltiplicare per il float, e infine arrivare all'intero senza segno.

Però mi chiedo:

- si riesce a usare l'intero (da 0 a 1024) per evitare moltiplicazioni float?

- si riesce a eseguire le operazioni sui singoli byte anziché su un intero a 16 bit?
Lo chiedo perché poi quando vado a scrivere nel DAC devo nuovamente inviare 1 byte alla volta....

 

 

Scrivi un commento all'articolo esprimendo la tua opinione sul tema, chiedendo eventuali spiegazioni e/o approfondimenti e contribuendo allo sviluppo dell'argomento proposto. Verranno accettati solo commenti a tema con l'argomento dell'articolo stesso. Commenti NON a tema dovranno essere necessariamente inseriti nel Forum creando un "nuovo argomento di discussione". Per commentare devi accedere al Blog
ritratto di elettronica_generale

Re: Regolare il volume numericamente

> In C, nelle espressioni, c'è una implicita conversione ad almeno a "int"
> di tutti i tipi coinvolti.
> Ho specificato "almeno" perché dipende dai tipi usati.

Ah ok, probabilmente però dipende dai compilatori. Ora non ricordo
esattamente quale, però di sicuro mi è capitato di dover esplicitamente
convertire a int (con un cast) altrimenti lo << 8 di un char restituiva
zero.

ritratto di elettronica_generale

Re: Regolare il volume numericamente

> Un'ultima domanda, poi ti lascio in pace :)
> Non mi è del tutto chiaro quando devo utilizzare i cast, quando posso e
> quando non devo... Nel senso, a pelle avrei detto che se sample è un
> int, buf un char[] e dac un uint avrei dovuto scrivere:

> sample = (int) buf[idx++];
> sample |= ((int) buf[idx++]) << 8;
> sample ^= 32768;
> sample *= volume;
> sample >>= 10;
> dac = (uint16_t) sample;

> Eppure ho fatto una prova anche senza alcun cast *apparentemente* non
> sorgono problemi. Anche qui trattasi di caso, oppure mi mancano ancora
> delle basi per comprendere bene la situazione?

In C, nelle espressioni, c'è una implicita conversione ad almeno a "int"
di tutti i tipi coinvolti.
Ho specificato "almeno" perché dipende dai tipi usati.

ritratto di elettronica_generale

Re: Regolare il volume numericamente

> Se invece la scalatura la fai dopo, l'ampiezza sarà sempre la stessa del
> caso precedente, ma il segnale avrà una componente continua sempre più
> vicina allo 0 al crescere dell'attenuazione.

> Il tuo segnale vedilo come una somma data dal segnale e dall'offset:
> (s + o)

> Se moltiplici (s + o) per il volume v otterrai sempre (sv + ov).

Ok, era banale ma non lo vedevo. Ora è chiaro. Grazie per la pazienza :)

> Nei due casi (signed ed unsigned) cambia solo l'offset, s è lo stesso, e
> anche dopo l'applicazione del volume sv sarà lo stesso in entrambi i
> casi, cambia solo la componente continua ov. E siccome suppongo che nel
> path audio ci saranno di sicuro dei condensatori, risulta indifferente
> fare la moltiplicazione prima o dopo la conversione.

Certamente, l'accoppiamento tra l'uscita dell'operazione di filtro e il
finale audio è in AC.

> Facciamo spesso delle guerre di religione tra colleghi: io uso sempre
> gli unsigned, qualcun altro i signed. Cerchiamo di convincerci che una
> prassi sia migliore dell'altra, ovviamente senza riuscirci ;-)

> Alla fin fine nel tuo caso non cambia molto tra un approccio e l'altro.
> A dire la verità forse nel tuo modo è pure più chiaro.

> Quindi vada per la versione più semplice e chiara:

> int sample = (buf[idx + 1] << 8) | buf[idx];
> sample *= volume;
> sample >>= 10;
> sample += 32768;
> dac = (uint16_t)sample;

L'ho provato, ma non funziona. Sicuramente però a questo punto non
dipende da questo codice ma da qualcos'altro nel programma che non
riesco a identificare.
La versione che genera l'audio corretto è la seguente:

sample = buf[idx++];
sample |= buf[idx++] << 8;
sample ^= 32768;
sample *= volume;
sample >>= 10;

L'assegnazione così è forse ancora più chiara... dopo tutto è un
little-endian quindi basta che leggo i byte in sequenza e il secondo lo
butto sul byte alto.

Se sommo 32768 ottengo cose strane: l'oscilloscopio mi mostra una specie
di onda quadra con sovrapposto il segnale audio, e dall'altoparlante
escono ovviamente solo strazianti gracchii :)
Invece mandando in XOR il bit più significativo dei byte letti (che se
non prendo altre cantonate è effettivamente il bit di segno *dei dati
grezzi*) tutto torna a posto.

Un'ultima domanda, poi ti lascio in pace :)
Non mi è del tutto chiaro quando devo utilizzare i cast, quando posso e
quando non devo... Nel senso, a pelle avrei detto che se sample è un
int, buf un char[] e dac un uint avrei dovuto scrivere:

sample = (int) buf[idx++];
sample |= ((int) buf[idx++]) << 8;
sample ^= 32768;
sample *= volume;
sample >>= 10;
dac = (uint16_t) sample;

Eppure ho fatto una prova anche senza alcun cast *apparentemente* non
sorgono problemi. Anche qui trattasi di caso, oppure mi mancano ancora
delle basi per comprendere bene la situazione?

ritratto di elettronica_generale

Re: Regolare il volume numericamente

Se guardi il mio primo post vedrai che sample l'ho dichiarato int; sul
cortex gli interi sono a 32bit, quindi non ci dovrebbero essere
problemi di overflow se il volume va tra 0 e 1024.

> E' questo che non capisco: se ho un offset (nel caso in esempio a metà
> scala) e moltiplico anche questo viene interessato e il risultato non è
> quello aspettato.

Se ci pensi, in realtà cambia solo dove è localizzato questo offset.

L'applicazione del "volume" può solo attenuare il segnale, non può
amplificarlo senza clipparlo. Quindi con volume 1024 avrai il segnale
originario, e via via che il volume scende, scenderà linearmente anche
l'ampiezza.

Se la scalatura la fai prima di "convertire" ad unsigned, il tuo
segnale sarà sempre centrato su 32768.

Se invece la scalatura la fai dopo, l'ampiezza sarà sempre la stessa
del caso precedente, ma il segnale avrà una componente continua sempre
più vicina allo 0 al crescere dell'attenuazione.

Il tuo segnale vedilo come una somma data dal segnale e dall'offset:
(s + o)

Se moltiplici (s + o) per il volume v otterrai sempre (sv + ov).

Nei due casi (signed ed unsigned) cambia solo l'offset, s è lo stesso,
e anche dopo l'applicazione del volume sv sarà lo stesso in entrambi i
casi, cambia solo la componente continua ov. E siccome suppongo che
nel path audio ci saranno di sicuro dei condensatori, risulta
indifferente fare la moltiplicazione prima o dopo la conversione.

A pensarci bene, credo che sia indifferente anche a livello di codice
generato (sempre una moltiplicazione + una divisione devi fare) quindi
direi che puoi scegliere indifferentemente quale approccio preferisci.

Io preferisco lavorare con gli unsigned perché, a differenza dei
signed, hanno un comportamento di overflow ben definito dallo standard
e li trovo più "naturali", ma è solo una questione di gusti.

Facciamo spesso delle guerre di religione tra colleghi: io uso sempre
gli unsigned, qualcun altro i signed. Cerchiamo di convincerci che una
prassi sia migliore dell'altra, ovviamente senza riuscirci ;-)

Alla fin fine nel tuo caso non cambia molto tra un approccio e
l'altro. A dire la verità forse nel tuo modo è pure più chiaro.

Quindi vada per la versione più semplice e chiara:

int sample = (buf[idx + 1] << 8) | buf[idx];
sample *= volume;
sample >>= 10;
sample += 32768;
dac = (uint16_t)sample;

ritratto di elettronica_generale

Re: Regolare il volume numericamente

Di questo ne ero cosciente, ma quando vado a scrivere il valore nel
registro della SPI prendo solo i 2 byte di interesse.

> - Quando fai dac.sample *= volume, stai molto probabilmente ottenendo un
> overflow, perché supponendo che sample abbia il bit più alto a 1,
> moltiplicarlo per qualsiasi numero maggiore di 1 manda il risultato in
> overflow.

Questo non mi torna con quanto vedo con il debugger... Nel senso che il
conto me lo fa corretto. Boh. Comunque concordo che è una porcata :)

> Rinnovo il consiglio, lasciala semplice!

La sto riscrivendo :)

ritratto di elettronica_generale

Re: Regolare il volume numericamente

Facciamo un esempio così mi capisco:

range di input -> -32768 a +32767
range di output -> 0 a 65535

ora ho il mio segnalino, di ampiezza 16 che gira attorno allo 0 (quindi
da -8 a +8).

Proviamo nei due casi:

1) moltiplicazione prima della conversione a unsigned:

posso moltiplicare al massimo per 4096

2) moltiplicazione dopo la conversione a unsigned:

non posso moltiplicare nemmeno per 2 perché andrei in overflow.

E' questo che non capisco: se ho un offset (nel caso in esempio a metà
scala) e moltiplico anche questo viene interessato e il risultato non è
quello aspettato.

Ovviamente so di avere torto perché hai ragione te :) Però ancora ci
picchio la testa e non capisco dove sbaglio.

> No, lo shift è con segno se la variabile è signed!

Ah ecco :) Avevo la convinzione che lo shift fosse una cosa "hardware" a
prescindere di come erano organizzati i bit.

ritratto di elettronica_generale

Re: Regolare il volume numericamente

Ti consiglio di non usare questo approccio, probabilmente funziona per
pura fortuna.

Infatti ci sono diversi problemi:
- Usi la union in un modo non definito: le union *non* sono fatte per
scrivere in un campo e rileggere dagli altri. Anche se molto spesso
funziona, il risultato di queste operazioni è indefinito, perché dipende
da come viene ottimizzato il codice.
- Se dac è allocato nello stack, siccome assegni solo 2 bytes su 4, il
contenuto di dac.sample è per metà casuale.
- Se invece dac è statico (e quindi la parte alta di sample è a 0),
quando fai dac.sample ^= 0x8000, stai semplicemente mettendo a 1 il bit
più alto di sample.
- Quando fai dac.sample *= volume, stai molto probabilmente ottenendo un
overflow, perché supponendo che sample abbia il bit più alto a 1,
moltiplicarlo per qualsiasi numero maggiore di 1 manda il risultato in
overflow.

Rinnovo il consiglio, lasciala semplice!

ritratto di elettronica_generale

Re: Regolare il volume numericamente

Ed è un problema?

Se sono campioni audio non sentirai la differenza, immagino ci saranno
comunque dei condensatori nel path che eliminaranno eventuali componenti
continue. E nota che questa componente sarà maggiore se la scalatura la
fai *prima* della conversione a unsigned.

> E non posso semplicemente invertire l'ordine delle operazioni perché lo
> shift mi sposterebbe il bit di segno.

No, lo shift è con segno se la variabile è signed!

In ogni caso, se questa cosa ti preoccupa, basta che sostituisci lo
shift con una divisione per 1024. Stai tranquillo, il compilatore la
ottimizzerà lo stesso con uno shift :-)

> Magari sbaglio ma credo che dovrei:

> 1) memorizzare il bit di segno
> 2) metterlo a zero nel dato iniziale
> 3) moltiplicare ed effettuare lo shift
> 4) rimettere il bit di segno
> 5) aggiungere 32768

Non serve, e inoltre mi sembra un po' macchinoso.

Come detto nella mia mail precedente, cerca sempre di mantenere le cose
il più semplici possibile.

Questo perché a differenza di quanto si pensa, i compilatori sono fatti
mediamente bene e sono in grado di ottimizzare efficacemente queste
semplici situazioni.

In genere ottimizzano meglio se scrivi codice "naturale", come se
trascrivessi semplicemente formule o algoritmi.

Quindi come consiglio generale direi di scrivere il codice come se non
sapessi quello che succede sotto.

Solo quando ti accorgi che hai un problema è il caso di indagare e
forzare il compilatore a fare una cosa specifica; negli altri casi si
rischia di fare peggio!

ritratto di elettronica_generale

Re: Regolare il volume numericamente

> Magari sbaglio ma credo che dovrei:

> 1) memorizzare il bit di segno
> 2) metterlo a zero nel dato iniziale
> 3) moltiplicare ed effettuare lo shift
> 4) rimettere il bit di segno
> 5) aggiungere 32768

> Intanto ci provo :)

Boh, ho risolto così ma non so perché funziona:

typedef union {
ULONG sample;
UCHAR bytes[4];

} SAMPLE;

SAMPLE dac;

dac.bytes[0] = buf[idx];
dac.bytes[1] = buf[idx + 1];
dac.sample ^= 0x8000;
dac.sample *= volume;
dac.sample >>= 10;

A parte la union che mi sono inventato (ok, non è molto portabile perché
dipende dall'endianness del processore) non capisco perché funziona il
resto.

Eppure fa il suo lavoro.

ritratto di elettronica_generale

Re: Regolare il volume numericamente

> Certo, se prendi il codice che ho scritto sopra basta modificarlo in:

> sample = (buf[idx + 1] << 8) | buf[idx];
> sample += 32768;

> /* volume è un uint16_t e va da 0 a 1024 */
> sample *= volume;
> sample >>= 10;

> dac = (uint16_t)sample;

C'è qualcosa che non mi quadra.
Se con le prime due righe trasformiamo già il valore tra 0 e 65535 dopo
non posso più "scalarlo" perché il mio zero sarà a metà scala.

E non posso semplicemente invertire l'ordine delle operazioni perché lo
shift mi sposterebbe il bit di segno.

Magari sbaglio ma credo che dovrei:

1) memorizzare il bit di segno
2) metterlo a zero nel dato iniziale
3) moltiplicare ed effettuare lo shift
4) rimettere il bit di segno
5) aggiungere 32768

Intanto ci provo :)

ritratto di elettronica_generale

Re: Regolare il volume numericamente

> Hai i campioni che vanno da -32768 a 32767 e li devi invece avere nel
> range 0 - 65535, giusto?

Yes sir.

> Se non ho capito male quello che vuoi fare, ti consiglio di scriverla
> nel modo più semplice possibile, qualcosa tipo:

> int sample;

> sample = buf[idx + 1] << 8 | buf[idx];
> sample += 32768;
> dac = (uint16_t)sample;

> Oltre a essere immediatamente comprensibile da un umano, il compilatore
> la ottimizza meglio. La tua versione sono 10 istruzioni, questa qui
> sopra solo 7 (compreso caricamento e store del risultato, compilati con
> Sourcery G++ Lite 2010q1-188 con -O1).

Ecco vedi? Anche qui ho taaaanto da imparare :(

> Certo, se prendi il codice che ho scritto sopra basta modificarlo in:

> sample = (buf[idx + 1] << 8) | buf[idx];
> sample += 32768;

> /* volume è un uint16_t e va da 0 a 1024 */
> sample *= volume;
> sample >>= 10;

> dac = (uint16_t)sample;

> Questo codice qui sopra sono solo 13 istruzioni, dovrebbe farcela alla
> grande.

Direi di si! Domani provo. Ovviamente sample dovrà essere a 32 bit per
evitare l'overflow.

> Penso di sì, ma credo che sia più lento. Il cortex m3 è in grado di fare
> moltiplicazioni su numeri a 32 bit, quindi se spezzi la moltiplicazione
> in 2 dovrà farne 2 invece di 1. Niente di che comunque, sarà qualche
> istruzione in più.

ritratto di elettronica_generale

Re: Regolare il volume numericamente

Hai i campioni che vanno da -32768 a 32767 e li devi invece avere nel
range 0 - 65535, giusto?

quindi

> faccio qualcosa del genere:

> dac = (((unsigned long) (g_pucAudioBuf[g_ulReadIdx + 1] ^ 0x80)) << 8) +
> ((unsigned long) (g_pucAudioBuf[g_ulReadIdx]));

Se non ho capito male quello che vuoi fare, ti consiglio di scriverla
nel modo più semplice possibile, qualcosa tipo:

int sample;

sample = buf[idx + 1] << 8 | buf[idx];
sample += 32768;
dac = (uint16_t)sample;

Oltre a essere immediatamente comprensibile da un umano, il compilatore
la ottimizza meglio. La tua versione sono 10 istruzioni, questa qui
sopra solo 7 (compreso caricamento e store del risultato, compilati con
Sourcery G++ Lite 2010q1-188 con -O1).

> ora, mi si chiede di regolare il volume numericamente, tramite un
> parametro che può essere un float (da 0 a 1) oppure un intero (da 0 a
> 1024).

> Secondo voi, quale è il metodo più performante dal punto di vista della
> velocità di esecuzione per eseguire tale operazione? Sto lavorando un
> ARM Cortex-M3.

Sicuramente conviene usare gli interi, anche se penso che possa farcela
pure con i float.

> L'idea ovvia è di comporre il valore a 16 bit con segno (quindi senza ^
> 0x80) moltiplicare per il float, e infine arrivare all'intero senza segno.

> Però mi chiedo:

> - si riesce a usare l'intero (da 0 a 1024) per evitare moltiplicazioni
> float?

Certo, se prendi il codice che ho scritto sopra basta modificarlo in:

sample = (buf[idx + 1] << 8) | buf[idx];
sample += 32768;

/* volume è un uint16_t e va da 0 a 1024 */
sample *= volume;
sample >>= 10;

dac = (uint16_t)sample;

Questo codice qui sopra sono solo 13 istruzioni, dovrebbe farcela alla
grande.

> - si riesce a eseguire le operazioni sui singoli byte anziché su un
> intero a 16 bit? Lo chiedo perché poi quando vado a scrivere nel DAC
> devo nuovamente inviare 1 byte alla volta....

Penso di sì, ma credo che sia più lento. Il cortex m3 è in grado di fare
moltiplicazioni su numeri a 32 bit, quindi se spezzi la moltiplicazione
in 2 dovrà farne 2 invece di 1. Niente di che comunque, sarà qualche
istruzione in più.

 

 

Login   
 Twitter Facebook LinkedIn Youtube Google RSS

Chi è online

Ci sono attualmente 18 utenti e 69 visitatori collegati.

Ultimi Commenti