diff --git a/axfer/Makefile.am b/axfer/Makefile.am index ab1d0b4..497b9ff 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -53,4 +53,5 @@ axfer_SOURCES = \ waiter.c \ waiter-poll.c \ waiter-select.c \ - waiter-epoll.c + waiter-epoll.c \ + xfer-libasound-timer-mmap.c diff --git a/axfer/xfer-libasound-timer-mmap.c b/axfer/xfer-libasound-timer-mmap.c new file mode 100644 index 0000000..1c642fe --- /dev/null +++ b/axfer/xfer-libasound-timer-mmap.c @@ -0,0 +1,455 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer-libasound-irq-mmap.c - Timer-based scheduling model for mmap operation. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// 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 < 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 + snd_pcm_frames_to_bytes(state->handle, + frame_offset); + frame_buf = buf; + } else { + int i; + for (i = 0; i < 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); + if (err < 0) + 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 < 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), +}; diff --git a/axfer/xfer-libasound.c b/axfer/xfer-libasound.c index fa72839..f6d0515 100644 --- a/axfer/xfer-libasound.c +++ b/axfer/xfer-libasound.c @@ -9,11 +9,17 @@ #include "xfer-libasound.h" #include "misc.h" +static const char *const sched_model_labels [] = { + [SCHED_MODEL_IRQ] = "irq", + [SCHED_MODEL_TIMER] = "timer", +}; + enum no_short_opts { // 200 or later belong to non us-ascii character set. OPT_PERIOD_SIZE = 200, OPT_BUFFER_SIZE, OPT_WAITER_TYPE, + OPT_SCHED_MODEL, OPT_DISABLE_RESAMPLE, OPT_DISABLE_CHANNELS, OPT_DISABLE_FORMAT, @@ -35,6 +41,7 @@ static const struct option l_opts[] = { {"start-delay", 1, 0, 'R'}, {"stop-delay", 1, 0, 'T'}, {"waiter-type", 1, 0, OPT_WAITER_TYPE}, + {"sched-model", 1, 0, OPT_SCHED_MODEL}, // For plugins in alsa-lib. {"disable-resample", 0, 0, OPT_DISABLE_RESAMPLE}, {"disable-channels", 0, 0, OPT_DISABLE_CHANNELS}, @@ -90,6 +97,8 @@ static int xfer_libasound_parse_opt(struct xfer_context *xfer, int key, state->msec_for_stop_threshold = arg_parse_decimal_num(optarg, &err); else if (key == OPT_WAITER_TYPE) state->waiter_type_literal = arg_duplicate_string(optarg, &err); + else if (key == OPT_SCHED_MODEL) + state->sched_model_literal = arg_duplicate_string(optarg, &err); else if (key == OPT_DISABLE_RESAMPLE) state->no_auto_resample = true; else if (key == OPT_DISABLE_CHANNELS) @@ -151,6 +160,15 @@ int xfer_libasound_validate_opts(struct xfer_context *xfer) } } + state->sched_model = SCHED_MODEL_IRQ; + if (state->sched_model_literal != NULL) { + if (!strcmp(state->sched_model_literal, "timer")) { + state->sched_model = SCHED_MODEL_TIMER; + state->mmap = true; + state->nonblock = true; + } + } + if (state->waiter_type_literal != NULL) { if (state->test_nowait) { fprintf(stderr, @@ -161,7 +179,8 @@ int xfer_libasound_validate_opts(struct xfer_context *xfer) if (!state->nonblock && !state->mmap) { fprintf(stderr, "An option for waiter type should be used " - "with nonblock or mmap options.\n"); + "with nonblock or mmap or timer-based " + "scheduling options.\n"); return -EINVAL; } state->waiter_type = @@ -196,6 +215,37 @@ static int set_access_hw_param(struct libasound_state *state) return err; } +static int disable_period_wakeup(struct libasound_state *state) +{ + int err; + + if (snd_pcm_type(state->handle) != SND_PCM_TYPE_HW) { + logging(state, + "Timer-based scheduling is only available for 'hw' " + "PCM plugin.\n"); + return -ENXIO; + } + + if (!snd_pcm_hw_params_can_disable_period_wakeup(state->hw_params)) { + logging(state, + "This hardware doesn't support the mode of no-period-" + "wakeup. In this case, timer-based scheduling is not " + "available.\n"); + return -EIO; + } + + err = snd_pcm_hw_params_set_period_wakeup(state->handle, + state->hw_params, 0); + if (err < 0) { + logging(state, + "Fail to disable period wakeup so that the hardware " + "generates no IRQs during transmission of data " + "frames.\n"); + } + + return err; +} + static int open_handle(struct xfer_context *xfer) { struct libasound_state *state = xfer->private_data; @@ -229,7 +279,11 @@ static int open_handle(struct xfer_context *xfer) if (err < 0) return err; - // TODO: Applying NO_PERIOD_WAKEUP should be done here. + if (state->sched_model == SCHED_MODEL_TIMER) { + err = disable_period_wakeup(state); + if (err < 0) + return err; + } if (xfer->dump_hw_params) { logging(state, "Available HW Params of node: %s\n", @@ -575,6 +629,7 @@ static int xfer_libasound_pre_process(struct xfer_context *xfer, snd_pcm_uframes_t *frames_per_buffer) { struct libasound_state *state = xfer->private_data; + unsigned int flag; int err; err = open_handle(xfer); @@ -606,24 +661,43 @@ static int xfer_libasound_pre_process(struct xfer_context *xfer, return err; // Assign I/O operation. - if (*access == SND_PCM_ACCESS_RW_INTERLEAVED || - *access == SND_PCM_ACCESS_RW_NONINTERLEAVED) { - state->ops = &xfer_libasound_irq_rw_ops; - } else if (*access == SND_PCM_ACCESS_MMAP_INTERLEAVED || - *access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) { - if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE) - state->ops = &xfer_libasound_irq_mmap_r_ops; - else - state->ops = &xfer_libasound_irq_mmap_w_ops; + err = snd_pcm_hw_params_get_period_wakeup(state->handle, + state->hw_params, &flag); + if (err < 0) + return err; + + if (flag) { + if (*access == SND_PCM_ACCESS_RW_INTERLEAVED || + *access == SND_PCM_ACCESS_RW_NONINTERLEAVED) { + state->ops = &xfer_libasound_irq_rw_ops; + } else if (*access == SND_PCM_ACCESS_MMAP_INTERLEAVED || + *access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) { + if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE) + state->ops = &xfer_libasound_irq_mmap_r_ops; + else + state->ops = &xfer_libasound_irq_mmap_w_ops; + } else { + return -ENXIO; + } } else { - return -ENXIO; + if (*access == SND_PCM_ACCESS_MMAP_INTERLEAVED || + *access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) { + if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE) + state->ops = &xfer_libasound_timer_mmap_r_ops; + else + state->ops = &xfer_libasound_timer_mmap_w_ops; + } else { + return -ENXIO; + } } + if (state->ops->private_size > 0) { state->private_data = malloc(state->ops->private_size); if (state->private_data == NULL) return -ENOMEM; memset(state->private_data, 0, state->ops->private_size); } + err = state->ops->pre_process(state); if (err < 0) return err; @@ -639,8 +713,11 @@ static int xfer_libasound_pre_process(struct xfer_context *xfer, return err; } - if (xfer->verbose > 0) + if (xfer->verbose > 0) { snd_pcm_dump(state->handle, state->log); + logging(state, "Scheduling model:\n"); + logging(state, " %s\n", sched_model_labels[state->sched_model]); + } if (state->use_waiter) { // NOTE: This should be after configuring sw_params due to @@ -733,7 +810,8 @@ static void xfer_libasound_post_process(struct xfer_context *xfer) pcm_state = snd_pcm_state(state->handle); if (pcm_state != SND_PCM_STATE_OPEN && pcm_state != SND_PCM_STATE_DISCONNECTED) { - if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE) { + if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE || + state->ops == &xfer_libasound_timer_mmap_w_ops) { err = snd_pcm_drop(state->handle); if (err < 0) logging(state, "snd_pcm_drop(): %s\n", @@ -774,8 +852,10 @@ static void xfer_libasound_destroy(struct xfer_context *xfer) free(state->node_literal); free(state->waiter_type_literal); + free(state->sched_model_literal); state->node_literal = NULL; state->waiter_type_literal = NULL; + state->sched_model_literal = NULL; if (state->hw_params) snd_pcm_hw_params_free(state->hw_params); diff --git a/axfer/xfer-libasound.h b/axfer/xfer-libasound.h index 20b0d74..dfe8577 100644 --- a/axfer/xfer-libasound.h +++ b/axfer/xfer-libasound.h @@ -15,6 +15,12 @@ #define logging(state, ...) \ snd_output_printf(state->log, __VA_ARGS__) +enum sched_model { + SCHED_MODEL_IRQ = 0, + SCHED_MODEL_TIMER, + SCHED_MODEL_COUNT, +}; + struct xfer_libasound_ops; struct libasound_state { @@ -31,6 +37,7 @@ struct libasound_state { char *node_literal; char *waiter_type_literal; + char *sched_model_literal; unsigned int msec_per_period; unsigned int msec_per_buffer; @@ -54,6 +61,9 @@ struct libasound_state { enum waiter_type waiter_type; struct waiter_context *waiter; + + // For scheduling type. + enum sched_model sched_model; }; // For internal use in 'libasound' module. @@ -76,4 +86,7 @@ extern const struct xfer_libasound_ops xfer_libasound_irq_rw_ops; extern const struct xfer_libasound_ops xfer_libasound_irq_mmap_r_ops; extern const struct xfer_libasound_ops xfer_libasound_irq_mmap_w_ops; +extern const struct xfer_libasound_ops xfer_libasound_timer_mmap_w_ops; +extern const struct xfer_libasound_ops xfer_libasound_timer_mmap_r_ops; + #endif