I am trying to write a c program (on linux), that plays a sine wave from the speakers, using the alsa kernel interface:
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <sound/asound.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <assert.h>
#include <errno.h>
#include <sys/mman.h>
#include <math.h>
#define SND_MASK_MAX 64 // idk why, alsalib used this value
#define MASK_OFS(i) ((i) >> 5)
#define MASK_BIT(i) (1U << ((i) & 31))
#ifndef ALSA_BUFFER_SIZE
#define ALSA_BUFFER_SIZE 4096
#endif
#ifndef ALSA_PERIOD_SIZE
#define ALSA_PERIOD_SIZE 1024
#endif
#define fail(err) {errno = EINVAL;return -1;}
#define success(operation) {operation;return 0;}
#define pexit(s) {perror(s);exit(1);}
#define complain(f) {f;exit(1);}
#define is_mask_empty(m) (m->bits[0] && m->bits[1])
#define is_interval_empty(i) (i->min > i->max || (i->min == i->max && (i->openmin || i->openmax)))
int set_param_mask(struct snd_pcm_hw_params* params, int var, int val) {
if (var-SNDRV_PCM_HW_PARAM_FIRST_MASK < 0) fail(EINVAL);
struct snd_mask* mask = &(params->masks[var-SNDRV_PCM_HW_PARAM_FIRST_MASK]);
if (is_mask_empty(mask)) fail(EFAULT);
unsigned int v;
if (val > SND_MASK_MAX) fail(EFAULT);
mask->bits[MASK_OFS(val)] &= MASK_BIT(val);
mask->bits[!MASK_OFS(val)] = 0;
if (!(mask->bits[MASK_OFS(val)])) fail(EFAULT);
return 0;
}
// dest - refined, src - set from
int snd_interval_refine(struct snd_interval* dest, const struct snd_interval *src)
{
int changed = 0;
if (dest->empty)
fail(ENOENT);
if (dest->min < src->min) {
dest->min = src->min;
dest->openmin = src->openmin;
changed = 1;
} else if (dest->min == src->min && !dest->openmin && src->openmin) {
dest->openmin = 1;
changed = 1;
}
if (dest->max > src->max) {
dest->max = src->max;
dest->openmax = src->openmax;
changed = 1;
} else if (dest->max == src->max && !dest->openmax && src->openmax) {
dest->openmax = 1;
changed = 1;
}
if (!dest->integer && src->integer) {
dest->integer = 1;
changed = 1;
}
if (dest->integer) {
if (dest->openmin) {
dest->min++;
dest->openmin = 0;
}
if (dest->openmax) {
dest->max--;
dest->openmax = 0;
}
} else if (!dest->openmin && !dest->openmax && dest->min == dest->max)
dest->integer = 1;
if (is_interval_empty(dest)) {
dest->empty = 1;
fail(EFAULT);
}
return changed;
}
int set_param_interval(struct snd_pcm_hw_params* params, int var, int val) {
if (var-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL < 0) fail(EINVAL);
struct snd_interval* interval = &(params->intervals[var-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL]);
struct snd_interval test;
test.empty = 0;
test.min = test.max = val;
test.openmin = test.openmax = 0;
test.integer = 1;
return snd_interval_refine(interval, &test);
}
int set_param(struct snd_pcm_hw_params* params, int var, int val) {
if (var <= SNDRV_PCM_HW_PARAM_LAST_MASK) return set_param_mask(params, var, val);
else if (var <= SNDRV_PCM_HW_PARAM_LAST_INTERVAL) return set_param_interval(params,var,val);
fail(EINVAL);
}
int get_param_int(struct snd_pcm_hw_params *params, snd_pcm_hw_param_t var, unsigned int *val)
{
struct snd_interval *i = &(params->intervals[var-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL]);
if (i->integer) success(*val = i->max);
fail(EINVAL);
}
int main() {
// open fd
int fd = open("/dev/snd/pcmC0D0p", O_RDWR | __O_CLOEXEC);
if (fd < 0) pexit("open /dev/snd/pcmC0D0p (O_RDWR | __O_CLOEXEC)");
// setup params
struct snd_pcm_hw_params* params = calloc(1,sizeof(struct snd_pcm_hw_params));
for (unsigned i = 0; i <= SNDRV_PCM_HW_PARAM_LAST_MASK-SNDRV_PCM_HW_PARAM_FIRST_MASK; i++)
memset(&(params->masks[i]), 0xff, SND_MASK_MAX * sizeof(__u32));
for (unsigned i = 0; i <= SNDRV_PCM_HW_PARAM_LAST_INTERVAL-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL; i++) {
(params->intervals)[i].min = 0;
(params->intervals)[i].openmin = 0;
(params->intervals)[i].max = 0xffffffffU;
(params->intervals)[i].openmax = 0;
(params->intervals)[i].integer = 0;
(params->intervals)[i].empty = 0;
}
params->rmask = ~0U;
params->cmask = 0;
params->info = ~0U;
// set specific params
if (ioctl(fd,SNDRV_PCM_IOCTL_HW_REFINE,params) < 0) pexit("ioctl SNDRV_PCM_IOCTL_HW_REFINE");
if (set_param(params,SNDRV_PCM_HW_PARAM_ACCESS,SNDRV_PCM_ACCESS_RW_INTERLEAVED) < 0 ) pexit("set param SNDRV_PCM_HW_PARAM_ACCESS");
if (set_param(params,SNDRV_PCM_HW_PARAM_FORMAT,SNDRV_PCM_FORMAT_S16_LE) < 0 ) pexit("set param SNDRV_PCM_HW_PARAM_FORMAT");
if (set_param(params,SNDRV_PCM_HW_PARAM_CHANNELS,2) < 0 ) pexit("set param SNDRV_PCM_HW_PARAM_CHANNELS");
if (set_param(params,SNDRV_PCM_HW_PARAM_RATE,48000) < 0 ) pexit("set param SNDRV_PCM_HW_PARAM_RATE");
if (ioctl(fd,SNDRV_PCM_IOCTL_HW_PARAMS,params) < 0) pexit("ioctl SNDRV_PCM_IOCTL_HW_PARAMS");
// get period size
unsigned int period_size;
if (get_param_int(params, SNDRV_PCM_HW_PARAM_PERIOD_SIZE,&period_size) < 0) pexit("set param SNDRV_PCM_HW_PARAM_PERIOD_SIZE");
// set sw_params
struct snd_pcm_sw_params* sparams = calloc(1,sizeof(struct snd_pcm_sw_params));
sparams->tstamp_mode = SNDRV_PCM_TSTAMP_ENABLE;
sparams->period_step = 1;
sparams->avail_min = period_size;
sparams->start_threshold = ALSA_BUFFER_SIZE - period_size;
sparams->stop_threshold = ALSA_BUFFER_SIZE;
sparams->xfer_align = period_size / 2;
if(ioctl(fd, SNDRV_PCM_IOCTL_SW_PARAMS, sparams) < 0) pexit("ioctl SNDRV_PCM_IOCTL_SW_PARAMS");
// create mmaps for sound playing
snd_pcm_uframes_t boundary = sparams->boundary;
struct snd_pcm_mmap_status* mmap_status = mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd, SNDRV_PCM_MMAP_OFFSET_STATUS);
if(!mmap_status || mmap_status == MAP_FAILED) pexit("mmap PROT_READ MAP_SHARED SNDRV_PCM_MMAP_OFFSET_STATUS");
struct snd_pcm_mmap_control* mmap_control = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, SNDRV_PCM_MMAP_OFFSET_CONTROL);
if(!mmap_control || mmap_control == MAP_FAILED) pexit("mmap PROT_READ|PROT_WRITE MAP_SHARED SNDRV_PCM_MMAP_OFFSET_CONTROL");
if(ioctl(fd, SNDRV_PCM_IOCTL_PREPARE) < 0) {
perror("ioctl SNDRV_PCM_IOCTL_PREPARE");
munmap(mmap_status, 4096); munmap(mmap_control, 4096);
exit(1);
}
unsigned int numframes=48000;
short* samples = calloc(numframes,sizeof(short));
// generate frames
for (int i = 0; i < numframes; i++) {
samples[i] = 30000 * sinf(2 * M_PI * 200 *((float)i / (numframes)));
}
unsigned char *data = (unsigned char*)samples;
numframes*=4; // (sizeof(int16_t)/sizeof(byte))
struct snd_xferi xfer = { 0 };
int ret, avail;
if (ioctl(fd,SNDRV_PCM_IOCTL_PREPARE) < 0) pexit("ioctl SNDRV_PCM_IOCTL_PREPARE");
// play the frames
int correct_count = 0;
do {
xfer.buf = data;
xfer.frames = numframes > period_size ? period_size : numframes;
xfer.result = 0;
if(!(ret = ioctl(fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &xfer))) {
avail = mmap_status->hw_ptr + ALSA_BUFFER_SIZE - mmap_control->appl_ptr;
if(avail < 0) avail += boundary; else
if((unsigned int)avail >= boundary) avail -= boundary;
numframes -= xfer.result;
data += xfer.result * 4; // 4==channels*(int16_t/byte)
++correct_count;
} else if (errno == EPIPE) {
if (ioctl(fd,SNDRV_PCM_IOCTL_PREPARE) < 0) pexit("ioctl SNDRV_PCM_IOCTL_PREPARE");
} else {
ioctl(fd, SNDRV_PCM_IOCTL_DRAIN);
close(fd);
fprintf(stderr,"ioctl SNDRV_PCM_IOCTL_WRITEI_FRAMES failed after successfully sending %li bytes in %d portions: ", ((size_t)data-(size_t)samples),correct_count);
pexit("");
}
} while(numframes > 0);
// clean up
free(sparams);
free(params);
munmap(mmap_status, 4096);
munmap(mmap_control, 4096);
ioctl(fd, SNDRV_PCM_IOCTL_DRAIN);
close(fd);
}
And after a few iterations of the while loop and emmiting ~0.1 seconds of (always the same) sound on my laptop, but not in the qemu vm, the program always exits with:
ioctl SNDRV_PCM_IOCTL_WRITEI_FRAMES failed after successfully sending 4096 bytes in 32 portions: Input/output error
... instead of writing the whole 48000 bytes in the buffer
(and for some reason 4096 is the same number as the ALSA_BUFFER_SIZE macro)
Does anyone know why this could happen (what I'm doing wrong) and how to fix it?
By the way I'm testing it on a laptop with ubuntu gnome x11 and a fedora quemu vm without pulseaudio or JACK or even asoundlib, and I have checked, that sound works in the vm.
writeand playing them withioctl(fd, SNDRV_PCM_IOCTL_START)and also litterly ignoring the "failiure" whenerrno == EIO || errno == EBADFD(this suggested doing that with the prior: github.com/ralph-irving/squeezelite/issues/35 and it still threw error on EBADFD so I made it not quit on it either). The full working code is here: gist.github.com/stas-badzi/73e1a46e2ea1d55b746694afb013ca5c/…