Newsgroup: it.hobby.elettronica.digitale
Titolo: Regolare il volume numericamente

Ciao,
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....
Grazie!
Marco

 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ù.
-- 
  _|/ Francesco Sacchi - Develer S.r.l., R&D dept.
   |\ 
http://www.develer.com/
 - 
http://www.bertos.org/

 istruzione in più.
Ok, grazie mille Francesco
Marco

 dac = (uint16_t)sample;
Mumble... 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 :)
Marco

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

 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!



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!


 rischia di fare peggio!
Occhei!
Marco

 Rinnovo il consiglio, lasciala semplice!
La sto riscrivendo :)
Marco

 1) moltiplicazione prima della conversione a unsigned:
+8*4096=?
-ice-

 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;


 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?
Ciao e se capiterà l'occasione ti offrirò una birra :) Sono molto utili 
i tuoi interventi!
Marco

 i tuoi interventi!
Non potresti rendermi più felice, la birra è una delle cose che apprezzo 
di più (e me la faccio pure in casa :-) )
Ciao!
-- 
  _|/ Francesco Sacchi - Develer S.r.l., R&D dept.
   |\ 
http://www.develer.com/
 - 
http://www.bertos.org/

 Non potresti rendermi più felice, la birra è una delle cose che
 > apprezzo di più (e me la faccio pure in casa :-) )
Vedrò di ricordarmelo quando ci incontreremo! ;)
Marco

 e li trovo piů "naturali", ma č solo una questione di gusti.
Faccio un inutile commento dicendo che sono d'accordo coi tuoi gusti e
complimentandomi per la chiarezza e (dice giustamente Marco) pazienza con
la quale hai esplicitato il tutto, ma spero mi perdonerete.

 Ah ok, probabilmente però dipende dai compilatori.
In realtà no, già dallo standard ANSI (quindi di parla di C89/C90, quasi 
20 anni fa) è uno standard. O almeno tutti i compilatori che si 
dichiarano ANSI dovrebbero comportarsi così.
