alsa-utils/axfer/xfer-libasound-timer-mmap.c
Jaroslav Kysela ad5a1c0c88 axfer: fix the verbose compilation warnings for latest gcc
Signed-off-by: Jaroslav Kysela <perex@perex.cz>
2023-08-30 12:35:57 +02:00

456 lines
12 KiB
C

// SPDX-License-Identifier: GPL-2.0
//
// xfer-libasound-irq-mmap.c - Timer-based scheduling model for mmap operation.
//
// Copyright (c) 2018 Takashi Sakamoto <o-takashi@sakamocchi.jp>
//
// Licensed under the terms of the GNU General Public License, version 2.
#include "xfer-libasound.h"
#include "misc.h"
struct map_layout {
snd_pcm_status_t *status;
bool need_forward_or_rewind;
char **vector;
unsigned int frames_per_second;
unsigned int samples_per_frame;
unsigned int frames_per_buffer;
};
static int timer_mmap_pre_process(struct libasound_state *state)
{
struct map_layout *layout = state->private_data;
snd_pcm_access_t access;
snd_pcm_uframes_t frame_offset;
snd_pcm_uframes_t avail = 0;
snd_pcm_uframes_t frames_per_buffer;
int i;
int err;
// This parameter, 'period event', is a software feature in alsa-lib.
// This switch a handler in 'hw' PCM plugin from irq-based one to
// timer-based one. This handler has two file descriptors for
// ALSA PCM character device and ALSA timer device. The latter is used
// to catch suspend/resume events as wakeup event.
err = snd_pcm_sw_params_set_period_event(state->handle,
state->sw_params, 1);
if (err < 0)
return err;
err = snd_pcm_status_malloc(&layout->status);
if (err < 0)
return err;
err = snd_pcm_hw_params_get_access(state->hw_params, &access);
if (err < 0)
return err;
err = snd_pcm_hw_params_get_channels(state->hw_params,
&layout->samples_per_frame);
if (err < 0)
return err;
err = snd_pcm_hw_params_get_rate(state->hw_params,
&layout->frames_per_second, NULL);
if (err < 0)
return err;
err = snd_pcm_hw_params_get_buffer_size(state->hw_params,
&frames_per_buffer);
if (err < 0)
return err;
layout->frames_per_buffer = (unsigned int)frames_per_buffer;
if (access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) {
layout->vector = calloc(layout->samples_per_frame,
sizeof(*layout->vector));
if (layout->vector == NULL)
return err;
}
if (state->verbose) {
const snd_pcm_channel_area_t *areas;
err = snd_pcm_mmap_begin(state->handle, &areas, &frame_offset,
&avail);
if (err < 0)
return err;
logging(state, "attributes for mapped page frame:\n");
for (i = 0; i < (int)layout->samples_per_frame; ++i) {
const snd_pcm_channel_area_t *area = areas + i;
logging(state, " sample number: %d\n", i);
logging(state, " address: %p\n", area->addr);
logging(state, " bits for offset: %u\n", area->first);
logging(state, " bits/frame: %u\n", area->step);
}
}
return 0;
}
static void *get_buffer(struct libasound_state *state,
const snd_pcm_channel_area_t *areas,
snd_pcm_uframes_t frame_offset)
{
struct map_layout *layout = state->private_data;
void *frame_buf;
if (layout->vector == NULL) {
char *buf;
buf = areas[0].addr;
buf += snd_pcm_frames_to_bytes(state->handle, frame_offset);
frame_buf = buf;
} else {
int i;
for (i = 0; i < (int)layout->samples_per_frame; ++i) {
layout->vector[i] = areas[i].addr;
layout->vector[i] += snd_pcm_samples_to_bytes(
state->handle, frame_offset);
}
frame_buf = layout->vector;
}
return frame_buf;
}
static int timer_mmap_process_frames(struct libasound_state *state,
unsigned int *frame_count,
struct mapper_context *mapper,
struct container_context *cntrs)
{
struct map_layout *layout = state->private_data;
snd_pcm_uframes_t planned_count;
snd_pcm_sframes_t avail;
snd_pcm_uframes_t avail_count;
const snd_pcm_channel_area_t *areas;
snd_pcm_uframes_t frame_offset;
void *frame_buf;
snd_pcm_sframes_t consumed_count;
int err;
// Retrieve avail space on PCM buffer between kernel/user spaces.
// On cache incoherent architectures, still care of data
// synchronization.
avail = snd_pcm_avail_update(state->handle);
if (avail < 0)
return (int)avail;
// Retrieve pointers of the buffer and left space up to the boundary.
avail_count = (snd_pcm_uframes_t)avail;
err = snd_pcm_mmap_begin(state->handle, &areas, &frame_offset,
&avail_count);
if (err < 0)
return err;
// MEMO: Use the amount of data frames as you like.
planned_count = layout->frames_per_buffer * random() / RAND_MAX;
if (frame_offset + planned_count > layout->frames_per_buffer)
planned_count = layout->frames_per_buffer - frame_offset;
// Trim up to expected frame count.
if (*frame_count < planned_count)
planned_count = *frame_count;
// Yield this CPU till planned amount of frames become available.
if (avail_count < planned_count) {
unsigned short revents;
int timeout_msec;
// TODO; precise granularity of timeout; e.g. ppoll(2).
// Furthermore, wrap up according to granularity of reported
// value for hw_ptr.
timeout_msec = ((planned_count - avail_count) * 1000 +
layout->frames_per_second - 1) /
layout->frames_per_second;
// TODO: However, experimentally, the above is not enough to
// keep planned amount of frames when waking up. I don't know
// exactly the mechanism yet.
err = xfer_libasound_wait_event(state, timeout_msec,
&revents);
// MEMO: timeout is expected since the above call is just to measure time elapse.
if (err < 0 && err != -ETIMEDOUT)
return err;
if (revents & POLLERR) {
// TODO: error reporting.
return -EIO;
}
if (!(revents & (POLLIN | POLLOUT)))
return -EAGAIN;
// MEMO: Need to perform hwsync explicitly because hwptr is not
// synchronized to actual position of data frame transmission
// on hardware because IRQ handlers are not used in this
// scheduling strategy.
avail = snd_pcm_avail(state->handle);
if (avail < 0)
return (int)avail;
if (avail < (snd_pcm_sframes_t)planned_count) {
logging(state,
"Wake up but not enough space: %lu %lu %u\n",
planned_count, avail, timeout_msec);
planned_count = avail;
}
}
// Let's process data frames.
*frame_count = planned_count;
frame_buf = get_buffer(state, areas, frame_offset);
err = mapper_context_process_frames(mapper, frame_buf, frame_count,
cntrs);
if (err < 0)
return err;
consumed_count = snd_pcm_mmap_commit(state->handle, frame_offset,
*frame_count);
if (consumed_count != *frame_count) {
logging(state,
"A bug of 'hw' PCM plugin or driver for this PCM "
"node.\n");
}
*frame_count = consumed_count;
return 0;
}
static int forward_appl_ptr(struct libasound_state *state)
{
struct map_layout *layout = state->private_data;
snd_pcm_uframes_t forwardable_count;
snd_pcm_sframes_t forward_count;
forward_count = snd_pcm_forwardable(state->handle);
if (forward_count < 0)
return (int)forward_count;
forwardable_count = forward_count;
// No need to add safe-gurard because hwptr goes ahead.
forward_count = snd_pcm_forward(state->handle, forwardable_count);
if (forward_count < 0)
return (int)forward_count;
if (state->verbose) {
logging(state,
" forwarded: %lu/%u\n",
forward_count, layout->frames_per_buffer);
}
return 0;
}
static int timer_mmap_r_process_frames(struct libasound_state *state,
unsigned *frame_count,
struct mapper_context *mapper,
struct container_context *cntrs)
{
struct map_layout *layout = state->private_data;
snd_pcm_state_t s;
int err;
// SNDRV_PCM_HW_PARAMS_NO_PERIOD_WAKEUP suppresses any IRQ to notify
// period elapse for data transmission, therefore no need to care of
// concurrent access by IRQ context and process context, unlike
// IRQ-based operations.
// Here, this is just to query current status to hardware, for later
// processing.
err = snd_pcm_status(state->handle, layout->status);
if (err < 0)
goto error;
s = snd_pcm_status_get_state(layout->status);
// TODO: if reporting something, do here with the status data.
if (s == SND_PCM_STATE_RUNNING) {
// Reduce delay between sampling on hardware and handling by
// this program.
if (layout->need_forward_or_rewind) {
err = forward_appl_ptr(state);
if (err < 0)
goto error;
layout->need_forward_or_rewind = false;
}
err = timer_mmap_process_frames(state, frame_count, mapper,
cntrs);
if (err < 0)
goto error;
} else {
if (s == SND_PCM_STATE_PREPARED) {
// For capture direction, need to start stream
// explicitly.
err = snd_pcm_start(state->handle);
if (err < 0)
goto error;
layout->need_forward_or_rewind = true;
// Not yet.
*frame_count = 0;
} else {
err = -EPIPE;
goto error;
}
}
return 0;
error:
*frame_count = 0;
return err;
}
static int rewind_appl_ptr(struct libasound_state *state)
{
struct map_layout *layout = state->private_data;
snd_pcm_uframes_t rewindable_count;
snd_pcm_sframes_t rewind_count;
rewind_count = snd_pcm_rewindable(state->handle);
if (rewind_count < 0)
return (int)rewind_count;
rewindable_count = rewind_count;
// If appl_ptr were rewound just to position of hw_ptr, at next time,
// hw_ptr could catch up appl_ptr. This is overrun. We need a space
// between these two pointers to prevent this XRUN.
// This space is largely affected by time to process data frames later.
//
// TODO: a generous way to estimate a good value.
if (rewindable_count < 32)
return 0;
rewindable_count -= 32;
rewind_count = snd_pcm_rewind(state->handle, rewindable_count);
if (rewind_count < 0)
return (int)rewind_count;
if (state->verbose) {
logging(state,
" rewound: %lu/%u\n",
rewind_count, layout->frames_per_buffer);
}
return 0;
}
static int fill_buffer_with_zero_samples(struct libasound_state *state)
{
struct map_layout *layout = state->private_data;
const snd_pcm_channel_area_t *areas;
snd_pcm_uframes_t frame_offset;
snd_pcm_uframes_t avail_count;
snd_pcm_format_t sample_format;
snd_pcm_uframes_t consumed_count;
int err;
err = snd_pcm_hw_params_get_buffer_size(state->hw_params,
&avail_count);
if (err < 0)
return err;
err = snd_pcm_mmap_begin(state->handle, &areas, &frame_offset,
&avail_count);
if (err < 0)
return err;
err = snd_pcm_hw_params_get_format(state->hw_params, &sample_format);
if (err < 0)
return err;
err = snd_pcm_areas_silence(areas, frame_offset,
layout->samples_per_frame, avail_count,
sample_format);
if (err < 0)
return err;
consumed_count = snd_pcm_mmap_commit(state->handle, frame_offset,
avail_count);
if (consumed_count != avail_count)
logging(state, "A bug of access plugin for this PCM node.\n");
return 0;
}
static int timer_mmap_w_process_frames(struct libasound_state *state,
unsigned *frame_count,
struct mapper_context *mapper,
struct container_context *cntrs)
{
struct map_layout *layout = state->private_data;
snd_pcm_state_t s;
int err;
// Read my comment in 'timer_mmap_w_process_frames()'.
err = snd_pcm_status(state->handle, layout->status);
if (err < 0)
goto error;
s = snd_pcm_status_get_state(layout->status);
// TODO: if reporting something, do here with the status data.
if (s == SND_PCM_STATE_RUNNING) {
// Reduce delay between queueing by this program and presenting
// on hardware.
if (layout->need_forward_or_rewind) {
err = rewind_appl_ptr(state);
if (err < 0)
goto error;
layout->need_forward_or_rewind = false;
}
err = timer_mmap_process_frames(state, frame_count, mapper,
cntrs);
if (err < 0)
goto error;
} else {
// Need to start playback stream explicitly
if (s == SND_PCM_STATE_PREPARED) {
err = fill_buffer_with_zero_samples(state);
if (err < 0)
goto error;
err = snd_pcm_start(state->handle);
if (err < 0)
goto error;
layout->need_forward_or_rewind = true;
// Not yet.
*frame_count = 0;
} else {
err = -EPIPE;
goto error;
}
}
return 0;
error:
*frame_count = 0;
return err;
}
static void timer_mmap_post_process(struct libasound_state *state)
{
struct map_layout *layout = state->private_data;
if (layout->status)
snd_pcm_status_free(layout->status);
layout->status = NULL;
if (layout->vector)
free(layout->vector);
layout->vector = NULL;
}
const struct xfer_libasound_ops xfer_libasound_timer_mmap_w_ops = {
.pre_process = timer_mmap_pre_process,
.process_frames = timer_mmap_w_process_frames,
.post_process = timer_mmap_post_process,
.private_size = sizeof(struct map_layout),
};
const struct xfer_libasound_ops xfer_libasound_timer_mmap_r_ops = {
.pre_process = timer_mmap_pre_process,
.process_frames = timer_mmap_r_process_frames,
.post_process = timer_mmap_post_process,
.private_size = sizeof(struct map_layout),
};