alsa-project / alsa-lib

The Advanced Linux Sound Architecture (ALSA) - library
GNU Lesser General Public License v2.1
368 stars 177 forks source link

Help to solve pop when sound loops #417

Open charlesmulder opened 1 week ago

charlesmulder commented 1 week ago

Hi.

Apologies for asking for help here. I don't know where else to go.

I'm working on a wavetable project for college.

I've managed to create a wavetable containing a sine wave. The pitch can be altered via a frequency variable (f).

There is an audible popping sound when the sound loops. I have noticed that increasing the buffer size, postpones the pop.

I've been reading up on buffer > period > frames, but I don't know how to go about removing the pop.

Would really appreciate some guidance.

Thanks C

#include <stdio.h>
#include <alsa/asoundlib.h>
#include <math.h>

unsigned char sinuc( float );

#define TWOPI 2*M_PI
#define ALSA_INFO 1

static char *device = "default";            /* playback device */
unsigned char buffer[24*1024];            

int main(void) {

        // ALSA playback related
        int err; 
        snd_pcm_t *playback_handle; // pcm
        snd_pcm_hw_params_t *hw_params;
        unsigned int val, val2; 
        int dir;
        snd_pcm_uframes_t period_size;
        snd_pcm_format_t format;

        // Wavetable related
        unsigned int N = 1024;
        float f = 440; // A4 note
        //float f = 261.626; // C4 note
        unsigned int fs = 44100;

        // playback
        if ((err = snd_pcm_open(&playback_handle, device, SND_PCM_STREAM_PLAYBACK, 0)) < 0) {                                                                          
                printf("Playback open error: %s\n", snd_strerror(err));                                                                                           
                exit(EXIT_FAILURE);                                                                                                                               
        }

        // ALSA configuration 

        /* Allocate a hardware parameters object. */
        snd_pcm_hw_params_alloca(&hw_params);

        /* Fill it in with default values. */
        snd_pcm_hw_params_any(playback_handle, hw_params);

        /* Set the desired hardware parameters. */

        /* Interleaved mode */
        snd_pcm_hw_params_set_access(playback_handle, hw_params,
                        SND_PCM_ACCESS_RW_INTERLEAVED);

        /* Unsigned 8-bit little-endian format */
        snd_pcm_hw_params_set_format(playback_handle, hw_params,
                        SND_PCM_FORMAT_U8);

        /* Two channels (stereo) */
        snd_pcm_hw_params_set_channels(playback_handle, hw_params, 1);

        /* 44100 bits/second sampling rate (CD quality) */
        snd_pcm_hw_params_set_rate_near(playback_handle,
                        hw_params, &fs, &dir);

         /* Set period size to 32 frames. */
        period_size = 32;
        snd_pcm_hw_params_set_period_size_near(playback_handle, hw_params, &period_size, &dir);

        /* Write the parameters to the driver */
        err = snd_pcm_hw_params(playback_handle, hw_params);
        if (err < 0) {
                fprintf(stderr,
                                "unable to set hw parameters: %s\n",
                                snd_strerror(err));
                exit(1);
        }

        // Wavetable init
        unsigned char wavetable[N]; // wavetable buffer
        float angle_inc = TWOPI/(float)N; // sine wave angle increment
        float index_inc = N*f/(float)fs; // wavetable index increment

        // Populate wavetable with a sine wave
        for( int n = 0; n < N; n++ ) {
                wavetable[n] = sinuc( angle_inc * n ); // 0 - 255 range
        }

        // ALSA Sample Buffer
        // period = 940 frames
        // buffer = 15052 frames
        float n = 0;
        for (int i = 0; i < sizeof(buffer)/sizeof(char); i++) {
                buffer[i] = wavetable[(int)n];
                //printf("%d\n",buffer[i]);
                n = n+index_inc;
                if( (int)n >= N ) {
                        n = 0;
                }
        }

        if ((err = snd_pcm_prepare (playback_handle)) < 0) {
                fprintf (stderr, "cannot prepare audio interface for use (%s)\n",
                                snd_strerror (err));
                exit (1);
        }

        //for (int i = 0; i < 16; i++) {
        while(1) {
                period_size = snd_pcm_writei(playback_handle, buffer, sizeof(buffer));
                if (period_size < 0)
                        period_size = snd_pcm_recover(playback_handle, period_size, 0);
                if (period_size < 0) {
                        printf("snd_pcm_writei failed: %s\n", snd_strerror(period_size));
                        break;
                }
                if (period_size > 0 && period_size < (long)sizeof(buffer))
                        printf("Short write (expected %li, wrote %li)\n", (long)sizeof(buffer), period_size);
        }

        // pass the remaining samples, otherwise they're dropped in close 
        err = snd_pcm_drain(playback_handle);
        if (err < 0)
                printf("snd_pcm_drain failed: %s\n", snd_strerror(err));

        //snd_pcm_hw_params_free(hw_params);
        snd_pcm_close(playback_handle);

        return 0;
}

/**
 * Sine unsigned char.
 * Scales sine output to a char
 * Original range -1 to 1.
 * New range 0 - 255.
 */
unsigned char sinuc( float angle ) {
        return (sinf( angle ) * 255 + 255) / 2;
}
geraldog commented 6 days ago

Generally speaking, pops when looping occur because the loop transition does not match the zero-crossing area of the wave.

geraldog commented 6 days ago

But in your case, I tested your code, and itself warns about Short Writes to the buffer.

Declaring the buffer as:

unsigned char buffer[32];

instead of:

unsigned char buffer[24*1024];

... to match your requested period size results in a continuous tone for me.

charlesmulder commented 5 days ago

Ah Geraldo. Thank you so, so much. I'll do some more reading on frames, period and buffer to try and make sense of it. Will post my understanding here. Would really appreciate if you could check it over. Thanks again.

charlesmulder commented 5 days ago

Ok. Tested with buffer size of 32 and the pop is indeed gone, but the tone is wrong.

pops when looping occur because the loop transition does not match the zero-crossing area of the wave

This was my thinking also.

I'll do some calcs and post them here.

charlesmulder commented 5 days ago

I have been unable to set period size and buffer size using the default device. Switched to using more specific device identifier like hw:1,0. Started getting some useful errors.

Looks like the soundcard doesn't support unsigned 8 bit format or single channel.

So, I guess I need to adapt the code to suit the soundcard or maybe try using the plughw device? Not sure, will experiment a bit.

What is strange to me is that the command says it's playing Unsigned 8 bit rate, Mono, but the HW Params doesn't list the format or channels as a valid option?


$ aplay -v -D hw:1,0 /dev/zero --dump-hw-params

Playing raw data '/dev/zero' : Unsigned 8 bit, Rate 8000 Hz, Mono
HW Params of device "hw:1,0":
--------------------
ACCESS:  MMAP_INTERLEAVED RW_INTERLEAVED
FORMAT:  S16_LE S32_LE
SUBFORMAT:  STD MSBITS_MAX
SAMPLE_BITS: [16 32]
FRAME_BITS: [32 128]
CHANNELS: [2 4]
RATE: [44100 192000]
PERIOD_TIME: (41 11888617)
PERIOD_SIZE: [8 524288]
PERIOD_BYTES: [128 2097152]
PERIODS: [2 32]
BUFFER_TIME: (83 23777234)
BUFFER_SIZE: [16 1048576]
BUFFER_BYTES: [128 4194304]
TICK_TIME: ALL
--------------------
aplay: set_params:1387: Sample format non available
Available formats:
- S16_LE
- S32_LE
- ```     
geraldog commented 4 days ago

Here's an adapted version of your code, Charles:

#include <stdio.h>
#include <alsa/asoundlib.h>
#include <math.h>
#include <limits.h>

int sinuc( float );

#define TWOPI 2*M_PI
#define ALSA_INFO 1

static char *device = "hw:2";            /* playback device */
int buffer[44100];

int main(void) {

        // ALSA playback related
        int err; 
        snd_pcm_t *playback_handle; // pcm
        snd_pcm_hw_params_t *hw_params;
        unsigned int val, val2; 
        int dir;
        snd_pcm_uframes_t period_size;
        snd_pcm_format_t format;

        // Wavetable related
        unsigned int N = 2004;
        float f = 440; // A4 note
        //float f = 261.626; // C4 note
        unsigned int fs = 44100;

        // playback
        if ((err = snd_pcm_open(&playback_handle, device, SND_PCM_STREAM_PLAYBACK, 0)) < 0) {                                                                          
                printf("Playback open error: %s\n", snd_strerror(err));                                                                                           
                exit(EXIT_FAILURE);                                                                                                                               
        }

        // ALSA configuration 

        /* Allocate a hardware parameters object. */
        snd_pcm_hw_params_alloca(&hw_params);

        /* Fill it in with default values. */
        snd_pcm_hw_params_any(playback_handle, hw_params);

        /* Set the desired hardware parameters. */

        /* Interleaved mode */
        snd_pcm_hw_params_set_access(playback_handle, hw_params,
                        SND_PCM_ACCESS_RW_INTERLEAVED);

        /* Unsigned 8-bit little-endian format */
        snd_pcm_hw_params_set_format(playback_handle, hw_params,
                        SND_PCM_FORMAT_S32_LE);

        /* Two channels (stereo) */
        snd_pcm_hw_params_set_channels(playback_handle, hw_params, 1);

        /* 44100 bits/second sampling rate (CD quality) */
        snd_pcm_hw_params_set_rate_near(playback_handle,
                        hw_params, &fs, &dir);

        /* Write the parameters to the driver */
        err = snd_pcm_hw_params(playback_handle, hw_params);
        if (err < 0) {
                fprintf(stderr,
                                "unable to set hw parameters: %s\n",
                                snd_strerror(err));
                exit(1);
        }

        // Wavetable init
        int wavetable[N]; // wavetable buffer
        float angle_inc = TWOPI/(float)N; // sine wave angle increment
        float index_inc = N*f/(float)fs; // wavetable index increment

        // Populate wavetable with a sine wave
        for( int n = 0; n < N; n++ ) {
                wavetable[n] = sinuc( angle_inc * n ); // 0 - 255 range
        }

        // ALSA Sample Buffer
        // period = 940 frames
        // buffer = 15052 frames
        float n = 0.0;
        for (int i = 0; i < sizeof(buffer)/sizeof(int); i++) {
                buffer[i] = wavetable[(int)n];
                       n = n+index_inc;
                if ( (int)n == N ) {
                       n = 0;
                }

                else if ( (int) n > N) {
                       n = 0.005;
                }
        }

        int idx_zerocross = 0;
        int zerocross_diff = 0;
        for (int counter_b = (sizeof(buffer) / sizeof(int)) - 1; counter_b > 1; counter_b--) {
              if (buffer[counter_b] == 0 && (counter_b) % 22 == 0) {
                  idx_zerocross = counter_b;
                  zerocross_diff = (sizeof(buffer) / sizeof(int)) + 1 - idx_zerocross;
                  printf("IDX ZEROCROSS: %i\n", counter_b);
                  printf("DIFF: %i\n", zerocross_diff);
                  break;
              }
        }
         /* Set period size to 32 frames. */
        snd_pcm_uframes_t period_frames = (idx_zerocross) / 22;
        snd_pcm_hw_params_set_period_size(playback_handle, hw_params, period_frames, dir);

        if ((err = snd_pcm_prepare (playback_handle)) < 0) {
                fprintf (stderr, "cannot prepare audio interface for use (%s)\n",
                                snd_strerror (err));
                exit (1);
        }

        int iterate_wt = 0;
        while(1) {
                int ret = snd_pcm_wait(playback_handle, 5);
                if (ret == 0)
                    continue;
                if ((iterate_wt*period_frames) + period_frames <= idx_zerocross) {
                       period_size = snd_pcm_writei(playback_handle, buffer+(((iterate_wt*period_frames)) * sizeof(int)), period_frames);
                }
                if ((iterate_wt*period_frames) + period_frames == idx_zerocross) {
                    iterate_wt = 0;
                    continue;
                }
                iterate_wt++;
                if ((iterate_wt * period_frames) + period_frames >= idx_zerocross) {
                        iterate_wt = 0;
                        continue;
                } 
                if (period_size < 0)
                        period_size = snd_pcm_recover(playback_handle, period_size, 0);
                if (period_size < 0) {
                        printf("snd_pcm_writei failed: %s\n", snd_strerror(period_size));
                        break;
                }
                if (period_size > 0 && period_size < period_frames) {
                        printf("Short write (expected %li, wrote %li) iterate_wt = %i\n", period_frames, period_size, iterate_wt);
                }
        }

        // pass the remaining samples, otherwise they're dropped in close 
        err = snd_pcm_drain(playback_handle);
        if (err < 0)
                printf("snd_pcm_drain failed: %s\n", snd_strerror(err));

        //snd_pcm_hw_params_free(hw_params);
        snd_pcm_close(playback_handle);

        return 0;
}

/**
 * Sine unsigned char.
 * Scales sine output to a char
 * Original range -1 to 1.
 * New range 0 - 255.
 */
int sinuc( float angle ) {
        return sinf( angle ) * INT_MAX;
}
charlesmulder commented 3 days ago

Thank a lot Geraldo. I'll take a look. Been sick.

I managed to get it to work. Will post my version here after work.

Will be interesting to compare.

I'll be in touch.