mirror of
https://github.com/alsa-project/alsa-utils
synced 2024-11-09 17:05:41 +01:00
ad5a1c0c88
Signed-off-by: Jaroslav Kysela <perex@perex.cz>
456 lines
12 KiB
C
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),
|
|
};
|