Newsgroup: it.comp.lang.c
Titolo: Come riprodurre suono a diverse frequenze con libreria ALSA su Linux

Per un simulatore di volo, devo cambiare la frequenza del suono di sfondo
del motore.  Per esempio, ho campionato il suono del motore a 8000 Hz full
power, e per riprodurre il suono col motore al 50% RPM devo riprodurre
il suono a 4000 Hz.
 
Con la libreria ALSA su Linux (Salckware 14.2, 64 bits) uso la
funzione snd_pcm_set_params(), ma introduce un fastidioso click ad ogni
cambiamento, come prova il programmino qui sotto.
 
C'è un altro modo per cambiare la frequenza senza dover ricorrere a un
ricampionamento in software?
 
Per la cronaca, il simulatore di volo in questione è acm-5.0-ico disponibile
nella home page del mio sito.
 
Ciao,
 ___ 
/_|_\  Umberto Salsi
\/_\/  
www.icosaedro.it
 
 
 
 
/*
 * pitchchange.c - compile and execute with:
 * gcc pitchchange.c -o pitchchange.exe -lasound && ./pitchchange.exe
 */
 
#include <stdlib.h>
#include <assert.h>
#include <alsa/asoundlib.h>
 
static void setRate(snd_pcm_t *pcm, int rate)
{
	printf("now playing at %d Hz\n", rate);
	assert( snd_pcm_set_params(pcm, SND_PCM_FORMAT_U8,
			SND_PCM_ACCESS_RW_INTERLEAVED,
			1, /* channels */
			rate, /* Hz */
			1, /* allow alsa-lib sw resampler (what is it, though?) */
			100000 /* latency (us) - ??? */
		) == 0 );
}
 
int main(void)
{
	/* Generate square wave, 1 B/frame, 256 frames/period, 16 periods total: */
	unsigned char wave[4*1024];
	int amplitude = 5;
	int i;
	for (i = 0; i < sizeof(wave); i++)
		wave[i] = (i & 0x80) ? 128 + amplitude : 128 - amplitude;
 
	/* Open audio device: */
	snd_pcm_t *pcm;
	assert( snd_pcm_open(&pcm, "default", SND_PCM_STREAM_PLAYBACK, 0) == 0 );
 
	/* Play the square wave at increasing rates. */
	int rate = 6000;
	setRate(pcm, rate);
	while(1){
 
		/* Play the sample using current rate: */
		int avail_frames = sizeof(wave);
		while( avail_frames > 0 ){
			int wrote_frames = snd_pcm_writei(pcm,
				wave + sizeof(wave) - avail_frames, avail_frames);
			assert( wrote_frames >= 0 );
			if (wrote_frames > 0 )
				avail_frames -= wrote_frames;
		}
 
		/* Change the playback rate a bit: */
		rate += 100;
		if( rate > 8000 )
			rate = 6000;
		setRate(pcm, rate); // <-- comment out this line to get rid of the click
	}
}
 
// The end.
 


Ciao,
ma i dati per creare la forma d'onda non potesti generarli tu anziche' 
leggerli da un file dove, probabilmente v'e' registrato pure il 'click'?
Alla fine di sinusoide trattasi.
 
 
 

 Alla fine di sinusoide trattasi.
 
Non è una sinusoide, è il suono WAV registrato dall'interno della cabina
di un aereo che decolla col motore al 100% RPM, per esempio un Cessna 172.
Lo spezzone è lungo 1 secondo e io lo riproduco ciclicamente ma con una
frequenza che cambia con continuità seguendo l'andamento del motore.
Il suono è buono fintanto che gli RPM rimangono costanti, non c'è nessun
click nel campione registrato. Ma se accelero da 50% a 100% e cambio la
frequenza con "continuità" (diciamo, ogni 0.2 s), ecco che si sente un
click ad ogni cambio, esattamente come dimostra il programma di test.
 
L'alternativa è lasciar stare la configurazione del driver e fare un
ricampionamento in software nel mio programma. Cosa che ho già fatto,
ma è piuttosto oneroso per la CPU e vorrei evitare.  E siccome mi pare
di capire che il driver già esegue il ricampionamento per adattare la
frequenza a quella supportata dal chip audio, vorrei evitare un doppio
inutile lavoro alla CPU e un degrado ulteriore della qualità del suono.



Ciao,
ho dato un'occhiata in rete a qualche esempio di uso delle librerie che 
stai utilizzando in loop simili al tuo ed ho notato alcune differenze: 
innanzi tutto sembra che per il set del rate usino una funzione che 
riconosca la frequenza settabile piu' vicina possibile a quella 
richiesta perche' sembra non sia settabile un valore assolutamente 
arbitrario. Inoltre sembra che per cambiare la frequenza usino 
modificare il 'period' e cioe' il tempo che la libreria deve impiegare 
per leggere i dati che gli hai passato (per essere piu' chiari, se 
impiega il doppio del tempo a riprodurre lo stesso campionamento, la 
frequenza riultera' dimezzata). In altri casi, addirittura, usano il 
mixer par passare da un segnale all'altro. In ogni caso non ho trovato 
esempi dove venga modificato il rate. Spero che la mia ricerca ti possa 
in qualche modo aiutare.
 

 in qualche modo aiutare.
 
Windows: se l'hw lo supporta c'è waveOutSetPitch(), altrimenti si deve
ricorrere a qualche altra libreria di più basso livello o ricampionare. E
il mio PC non lo supporta.
 
Linux + ALSA: una volta settati i parametri e lanciata la riproduzione,
non c'è verso di cambiarli a basso livello con snd_pcm_hw_xxx() perché
dà sempre errore. Anche qui non rimane altro da fare che ricampionare.
 
Conclusione dei miei tentativi: l'unico modo portabile e sicuro
per cambiare con continuità la frequenza di riproduzione è
quello di ricampionare il buffer sorgente (src) nel buffer di
destinazione (dst) ricopiando il numero opportuno di frames;
ovviamente this->wav->nSamplesPerSec è la frequenza originale mentre
this->currentSamplesPerSec è la frequenza voluta; il "this" che appare
qui tradisce una implementazione simil-OOP fatta in C; per evitare
stalli in ogni situazione, viene sempre consumato almeno un frame da
src e viene sempre prodotto almeno un frame in dst:
 
 
static int audio_resample(audio_Type *this,
	char *src, int src_len,
	char *dst, int dst_len)
{
	int frame_len = this->wav->frameLength;
	// Destination must contain an integral number of frames:
	dst_len -= dst_len % frame_len;
	// Pitch factor maps frame offset of the src into dst:
	double k = (double) this->currentSamplesPerSec / this->wav->nSamplesPerSec;
	// For each dst sample, calculate the src frame and copy.
	int src_byte_index = 0;
	int dst_byte_index = 0;
	int dst_frame_index = 0;
	do {
		src_byte_index = frame_len * (int) (dst_frame_index * k + 0.5);
		if( src_byte_index >= src_len )
			break;
		memcpy(dst + dst_byte_index, src + src_byte_index, frame_len);
		dst_byte_index += frame_len;
		dst_frame_index++;
	} while( dst_byte_index < dst_len );
	if( src_byte_index < frame_len )
		src_byte_index = frame_len;
	else if( src_byte_index > src_len )
		src_byte_index = src_len;
	this->cursor += src_byte_index;
	return dst_byte_index;
