diff options
Diffstat (limited to 'sound/soc/sof/ipc4-pcm.c')
-rw-r--r-- | sound/soc/sof/ipc4-pcm.c | 573 |
1 files changed, 507 insertions, 66 deletions
diff --git a/sound/soc/sof/ipc4-pcm.c b/sound/soc/sof/ipc4-pcm.c index 96941bebc1f1..68258767aace 100644 --- a/sound/soc/sof/ipc4-pcm.c +++ b/sound/soc/sof/ipc4-pcm.c @@ -10,8 +10,37 @@ #include <sound/sof/ipc4/header.h> #include "sof-audio.h" #include "sof-priv.h" +#include "ops.h" #include "ipc4-priv.h" #include "ipc4-topology.h" +#include "ipc4-fw-reg.h" + +static int sof_ipc4_set_multi_pipeline_state(struct snd_sof_dev *sdev, u32 state, + struct ipc4_pipeline_set_state_data *trigger_list) +{ + struct sof_ipc4_msg msg = {{ 0 }}; + u32 primary, ipc_size; + + /* trigger a single pipeline */ + if (trigger_list->count == 1) + return sof_ipc4_set_pipeline_state(sdev, trigger_list->pipeline_ids[0], state); + + primary = state; + primary |= SOF_IPC4_MSG_TYPE_SET(SOF_IPC4_GLB_SET_PIPELINE_STATE); + primary |= SOF_IPC4_MSG_DIR(SOF_IPC4_MSG_REQUEST); + primary |= SOF_IPC4_MSG_TARGET(SOF_IPC4_FW_GEN_MSG); + msg.primary = primary; + + /* trigger multiple pipelines with a single IPC */ + msg.extension = SOF_IPC4_GLB_PIPE_STATE_EXT_MULTI; + + /* ipc_size includes the count and the pipeline IDs for the number of pipelines */ + ipc_size = sizeof(u32) * (trigger_list->count + 1); + msg.data_size = ipc_size; + msg.data_ptr = trigger_list; + + return sof_ipc_tx_message(sdev->ipc, &msg, ipc_size, NULL, 0); +} int sof_ipc4_set_pipeline_state(struct snd_sof_dev *sdev, u32 id, u32 state) { @@ -32,80 +61,238 @@ int sof_ipc4_set_pipeline_state(struct snd_sof_dev *sdev, u32 id, u32 state) } EXPORT_SYMBOL(sof_ipc4_set_pipeline_state); +static void +sof_ipc4_add_pipeline_to_trigger_list(struct snd_sof_dev *sdev, int state, + struct snd_sof_pipeline *spipe, + struct ipc4_pipeline_set_state_data *trigger_list) +{ + struct snd_sof_widget *pipe_widget = spipe->pipe_widget; + struct sof_ipc4_pipeline *pipeline = pipe_widget->private; + + if (pipeline->skip_during_fe_trigger) + return; + + switch (state) { + case SOF_IPC4_PIPE_RUNNING: + /* + * Trigger pipeline if all PCMs containing it are paused or if it is RUNNING + * for the first time + */ + if (spipe->started_count == spipe->paused_count) + trigger_list->pipeline_ids[trigger_list->count++] = + pipe_widget->instance_id; + break; + case SOF_IPC4_PIPE_RESET: + /* RESET if the pipeline is neither running nor paused */ + if (!spipe->started_count && !spipe->paused_count) + trigger_list->pipeline_ids[trigger_list->count++] = + pipe_widget->instance_id; + break; + case SOF_IPC4_PIPE_PAUSED: + /* Pause the pipeline only when its started_count is 1 more than paused_count */ + if (spipe->paused_count == (spipe->started_count - 1)) + trigger_list->pipeline_ids[trigger_list->count++] = + pipe_widget->instance_id; + break; + default: + break; + } +} + +static void +sof_ipc4_update_pipeline_state(struct snd_sof_dev *sdev, int state, int cmd, + struct snd_sof_pipeline *spipe, + struct ipc4_pipeline_set_state_data *trigger_list) +{ + struct snd_sof_widget *pipe_widget = spipe->pipe_widget; + struct sof_ipc4_pipeline *pipeline = pipe_widget->private; + int i; + + if (pipeline->skip_during_fe_trigger) + return; + + /* set state for pipeline if it was just triggered */ + for (i = 0; i < trigger_list->count; i++) { + if (trigger_list->pipeline_ids[i] == pipe_widget->instance_id) { + pipeline->state = state; + break; + } + } + + switch (state) { + case SOF_IPC4_PIPE_PAUSED: + switch (cmd) { + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + /* + * increment paused_count if the PAUSED is the final state during + * the PAUSE trigger + */ + spipe->paused_count++; + break; + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + /* + * decrement started_count if PAUSED is the final state during the + * STOP trigger + */ + spipe->started_count--; + break; + default: + break; + } + break; + case SOF_IPC4_PIPE_RUNNING: + switch (cmd) { + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + /* decrement paused_count for RELEASE */ + spipe->paused_count--; + break; + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + /* increment started_count for START/RESUME */ + spipe->started_count++; + break; + default: + break; + } + break; + default: + break; + } +} + +/* + * The picture below represents the pipeline state machine wrt PCM actions corresponding to the + * triggers and ioctls + * +---------------+ + * | | + * | INIT | + * | | + * +-------+-------+ + * | + * | + * | START + * | + * | + * +----------------+ +------v-------+ +-------------+ + * | | START | | HW_FREE | | + * | RUNNING <-------------+ PAUSED +--------------> + RESET | + * | | PAUSE | | | | + * +------+---------+ RELEASE +---------+----+ +-------------+ + * | ^ + * | | + * | | + * | | + * | PAUSE | + * +---------------------------------+ + * STOP/SUSPEND + * + * Note that during system suspend, the suspend trigger is followed by a hw_free in + * sof_pcm_trigger(). So, the final state during suspend would be RESET. + * Also, since the SOF driver doesn't support full resume, streams would be restarted with the + * prepare ioctl before the START trigger. + */ + static int sof_ipc4_trigger_pipelines(struct snd_soc_component *component, - struct snd_pcm_substream *substream, int state) + struct snd_pcm_substream *substream, int state, int cmd) { struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(component); struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); - struct snd_sof_widget *pipeline_widget; - struct snd_soc_dapm_widget_list *list; - struct snd_soc_dapm_widget *widget; - struct sof_ipc4_pipeline *pipeline; - struct snd_sof_widget *swidget; + struct snd_sof_pcm_stream_pipeline_list *pipeline_list; + struct sof_ipc4_fw_data *ipc4_data = sdev->private; + struct ipc4_pipeline_set_state_data *trigger_list; + struct snd_sof_pipeline *spipe; struct snd_sof_pcm *spcm; - int ret = 0; - int num_widgets; + int ret; + int i; + + dev_dbg(sdev->dev, "trigger cmd: %d state: %d\n", cmd, state); spcm = snd_sof_find_spcm_dai(component, rtd); if (!spcm) return -EINVAL; - list = spcm->stream[substream->stream].list; + pipeline_list = &spcm->stream[substream->stream].pipeline_list; - for_each_dapm_widgets(list, num_widgets, widget) { - swidget = widget->dobj.private; + /* nothing to trigger if the list is empty */ + if (!pipeline_list->pipelines || !pipeline_list->count) + return 0; - if (!swidget) - continue; + /* allocate memory for the pipeline data */ + trigger_list = kzalloc(struct_size(trigger_list, pipeline_ids, pipeline_list->count), + GFP_KERNEL); + if (!trigger_list) + return -ENOMEM; - /* - * set pipeline state for both FE and BE pipelines for RUNNING state. - * For PAUSE/RESET, set the pipeline state only for the FE pipeline. - */ - switch (state) { - case SOF_IPC4_PIPE_PAUSED: - case SOF_IPC4_PIPE_RESET: - if (!WIDGET_IS_AIF(swidget->id)) - continue; - break; - default: - break; - } + mutex_lock(&ipc4_data->pipeline_state_mutex); - /* find pipeline widget for the pipeline that this widget belongs to */ - pipeline_widget = swidget->pipe_widget; - pipeline = (struct sof_ipc4_pipeline *)pipeline_widget->private; + /* + * IPC4 requires pipelines to be triggered in order starting at the sink and + * walking all the way to the source. So traverse the pipeline_list in the order + * sink->source when starting PCM's and in the reverse order to pause/stop PCM's. + * Skip the pipelines that have their skip_during_fe_trigger flag set. If there is a fork + * in the pipeline, the order of triggering between the left/right paths will be + * indeterministic. But the sink->source trigger order sink->source would still be + * guaranteed for each fork independently. + */ + if (state == SOF_IPC4_PIPE_RUNNING || state == SOF_IPC4_PIPE_RESET) + for (i = pipeline_list->count - 1; i >= 0; i--) { + spipe = pipeline_list->pipelines[i]; + sof_ipc4_add_pipeline_to_trigger_list(sdev, state, spipe, trigger_list); + } + else + for (i = 0; i < pipeline_list->count; i++) { + spipe = pipeline_list->pipelines[i]; + sof_ipc4_add_pipeline_to_trigger_list(sdev, state, spipe, trigger_list); + } - if (pipeline->state == state) - continue; + /* return if all pipelines are in the requested state already */ + if (!trigger_list->count) { + ret = 0; + goto free; + } - /* first set the pipeline to PAUSED state */ - if (pipeline->state != SOF_IPC4_PIPE_PAUSED) { - ret = sof_ipc4_set_pipeline_state(sdev, pipeline_widget->instance_id, - SOF_IPC4_PIPE_PAUSED); - if (ret < 0) { - dev_err(sdev->dev, "failed to pause pipeline %d\n", - swidget->pipeline_id); - return ret; - } - } + /* no need to pause before reset or before pause release */ + if (state == SOF_IPC4_PIPE_RESET || cmd == SNDRV_PCM_TRIGGER_PAUSE_RELEASE) + goto skip_pause_transition; - pipeline->state = SOF_IPC4_PIPE_PAUSED; + /* + * set paused state for pipelines if the final state is PAUSED or when the pipeline + * is set to RUNNING for the first time after the PCM is started. + */ + ret = sof_ipc4_set_multi_pipeline_state(sdev, SOF_IPC4_PIPE_PAUSED, trigger_list); + if (ret < 0) { + dev_err(sdev->dev, "failed to pause all pipelines\n"); + goto free; + } - if (pipeline->state == state) - continue; + /* update PAUSED state for all pipelines just triggered */ + for (i = 0; i < pipeline_list->count ; i++) { + spipe = pipeline_list->pipelines[i]; + sof_ipc4_update_pipeline_state(sdev, SOF_IPC4_PIPE_PAUSED, cmd, spipe, + trigger_list); + } - /* then set the final state */ - ret = sof_ipc4_set_pipeline_state(sdev, pipeline_widget->instance_id, state); - if (ret < 0) { - dev_err(sdev->dev, "failed to set state %d for pipeline %d\n", - state, swidget->pipeline_id); - break; - } + /* return if this is the final state */ + if (state == SOF_IPC4_PIPE_PAUSED) + goto free; +skip_pause_transition: + /* else set the RUNNING/RESET state in the DSP */ + ret = sof_ipc4_set_multi_pipeline_state(sdev, state, trigger_list); + if (ret < 0) { + dev_err(sdev->dev, "failed to set final state %d for all pipelines\n", state); + goto free; + } - pipeline->state = state; + /* update RUNNING/RESET state for all pipelines that were just triggered */ + for (i = 0; i < pipeline_list->count; i++) { + spipe = pipeline_list->pipelines[i]; + sof_ipc4_update_pipeline_state(sdev, state, cmd, spipe, trigger_list); } +free: + mutex_unlock(&ipc4_data->pipeline_state_mutex); + kfree(trigger_list); return ret; } @@ -134,13 +321,14 @@ static int sof_ipc4_pcm_trigger(struct snd_soc_component *component, } /* set the pipeline state */ - return sof_ipc4_trigger_pipelines(component, substream, state); + return sof_ipc4_trigger_pipelines(component, substream, state, cmd); } static int sof_ipc4_pcm_hw_free(struct snd_soc_component *component, struct snd_pcm_substream *substream) { - return sof_ipc4_trigger_pipelines(component, substream, SOF_IPC4_PIPE_RESET); + /* command is not relevant with RESET, so just pass 0 */ + return sof_ipc4_trigger_pipelines(component, substream, SOF_IPC4_PIPE_RESET, 0); } static void ipc4_ssp_dai_config_pcm_params_match(struct snd_sof_dev *sdev, const char *link_name, @@ -183,7 +371,6 @@ static int sof_ipc4_pcm_dai_link_fixup(struct snd_soc_pcm_runtime *rtd, struct snd_mask *fmt = hw_param_mask(params, SNDRV_PCM_HW_PARAM_FORMAT); struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(component); struct sof_ipc4_copier *ipc4_copier; - struct snd_soc_dpcm *dpcm; if (!dai) { dev_err(component->dev, "%s: No DAI found with name %s\n", __func__, @@ -205,17 +392,6 @@ static int sof_ipc4_pcm_dai_link_fixup(struct snd_soc_pcm_runtime *rtd, rate->min = ipc4_copier->available_fmt.base_config->audio_fmt.sampling_frequency; rate->max = rate->min; - /* - * Set trigger order for capture to SND_SOC_DPCM_TRIGGER_PRE. This is required - * to ensure that the BE DAI pipeline gets stopped/suspended before the FE DAI - * pipeline gets triggered and the pipeline widgets are freed. - */ - for_each_dpcm_fe(rtd, SNDRV_PCM_STREAM_CAPTURE, dpcm) { - struct snd_soc_pcm_runtime *fe = dpcm->fe; - - fe->dai_link->trigger[SNDRV_PCM_STREAM_CAPTURE] = SND_SOC_DPCM_TRIGGER_PRE; - } - switch (ipc4_copier->dai_type) { case SOF_DAI_INTEL_SSP: ipc4_ssp_dai_config_pcm_params_match(sdev, (char *)rtd->dai_link->name, params); @@ -227,8 +403,273 @@ static int sof_ipc4_pcm_dai_link_fixup(struct snd_soc_pcm_runtime *rtd, return 0; } +static void sof_ipc4_pcm_free(struct snd_sof_dev *sdev, struct snd_sof_pcm *spcm) +{ + struct snd_sof_pcm_stream_pipeline_list *pipeline_list; + int stream; + + for_each_pcm_streams(stream) { + pipeline_list = &spcm->stream[stream].pipeline_list; + kfree(pipeline_list->pipelines); + pipeline_list->pipelines = NULL; + kfree(spcm->stream[stream].private); + spcm->stream[stream].private = NULL; + } +} + +static int sof_ipc4_pcm_setup(struct snd_sof_dev *sdev, struct snd_sof_pcm *spcm) +{ + struct snd_sof_pcm_stream_pipeline_list *pipeline_list; + struct sof_ipc4_fw_data *ipc4_data = sdev->private; + struct sof_ipc4_timestamp_info *stream_info; + bool support_info = true; + u32 abi_version; + u32 abi_offset; + int stream; + + abi_offset = offsetof(struct sof_ipc4_fw_registers, abi_ver); + sof_mailbox_read(sdev, sdev->fw_info_box.offset + abi_offset, &abi_version, + sizeof(abi_version)); + + if (abi_version < SOF_IPC4_FW_REGS_ABI_VER) + support_info = false; + + for_each_pcm_streams(stream) { + pipeline_list = &spcm->stream[stream].pipeline_list; + + /* allocate memory for max number of pipeline IDs */ + pipeline_list->pipelines = kcalloc(ipc4_data->max_num_pipelines, + sizeof(struct snd_sof_widget *), GFP_KERNEL); + if (!pipeline_list->pipelines) { + sof_ipc4_pcm_free(sdev, spcm); + return -ENOMEM; + } + + if (!support_info) + continue; + + stream_info = kzalloc(sizeof(*stream_info), GFP_KERNEL); + if (!stream_info) { + sof_ipc4_pcm_free(sdev, spcm); + return -ENOMEM; + } + + spcm->stream[stream].private = stream_info; + } + + return 0; +} + +static void sof_ipc4_build_time_info(struct snd_sof_dev *sdev, struct snd_sof_pcm_stream *spcm) +{ + struct sof_ipc4_copier *host_copier = NULL; + struct sof_ipc4_copier *dai_copier = NULL; + struct sof_ipc4_llp_reading_slot llp_slot; + struct sof_ipc4_timestamp_info *info; + struct snd_soc_dapm_widget *widget; + struct snd_sof_dai *dai; + int i; + + /* find host & dai to locate info in memory window */ + for_each_dapm_widgets(spcm->list, i, widget) { + struct snd_sof_widget *swidget = widget->dobj.private; + + if (!swidget) + continue; + + if (WIDGET_IS_AIF(swidget->widget->id)) { + host_copier = swidget->private; + } else if (WIDGET_IS_DAI(swidget->widget->id)) { + dai = swidget->private; + dai_copier = dai->private; + } + } + + /* both host and dai copier must be valid for time_info */ + if (!host_copier || !dai_copier) { + dev_err(sdev->dev, "host or dai copier are not found\n"); + return; + } + + info = spcm->private; + info->host_copier = host_copier; + info->dai_copier = dai_copier; + info->llp_offset = offsetof(struct sof_ipc4_fw_registers, llp_gpdma_reading_slots) + + sdev->fw_info_box.offset; + + /* find llp slot used by current dai */ + for (i = 0; i < SOF_IPC4_MAX_LLP_GPDMA_READING_SLOTS; i++) { + sof_mailbox_read(sdev, info->llp_offset, &llp_slot, sizeof(llp_slot)); + if (llp_slot.node_id == dai_copier->data.gtw_cfg.node_id) + break; + + info->llp_offset += sizeof(llp_slot); + } + + if (i < SOF_IPC4_MAX_LLP_GPDMA_READING_SLOTS) + return; + + /* if no llp gpdma slot is used, check aggregated sdw slot */ + info->llp_offset = offsetof(struct sof_ipc4_fw_registers, llp_sndw_reading_slots) + + sdev->fw_info_box.offset; + for (i = 0; i < SOF_IPC4_MAX_LLP_SNDW_READING_SLOTS; i++) { + sof_mailbox_read(sdev, info->llp_offset, &llp_slot, sizeof(llp_slot)); + if (llp_slot.node_id == dai_copier->data.gtw_cfg.node_id) + break; + + info->llp_offset += sizeof(llp_slot); + } + + if (i < SOF_IPC4_MAX_LLP_SNDW_READING_SLOTS) + return; + + /* check EVAD slot */ + info->llp_offset = offsetof(struct sof_ipc4_fw_registers, llp_evad_reading_slot) + + sdev->fw_info_box.offset; + sof_mailbox_read(sdev, info->llp_offset, &llp_slot, sizeof(llp_slot)); + if (llp_slot.node_id != dai_copier->data.gtw_cfg.node_id) { + dev_info(sdev->dev, "no llp found, fall back to default HDA path"); + info->llp_offset = 0; + } +} + +static int sof_ipc4_pcm_hw_params(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_sof_platform_stream_params *platform_params) +{ + struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(component); + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct sof_ipc4_timestamp_info *time_info; + struct snd_sof_pcm *spcm; + + spcm = snd_sof_find_spcm_dai(component, rtd); + time_info = spcm->stream[substream->stream].private; + /* delay calculation is not supported by current fw_reg ABI */ + if (!time_info) + return 0; + + time_info->stream_start_offset = SOF_IPC4_INVALID_STREAM_POSITION; + time_info->llp_offset = 0; + + sof_ipc4_build_time_info(sdev, &spcm->stream[substream->stream]); + + return 0; +} + +static int sof_ipc4_get_stream_start_offset(struct snd_sof_dev *sdev, + struct snd_pcm_substream *substream, + struct snd_sof_pcm_stream *stream, + struct sof_ipc4_timestamp_info *time_info) +{ + struct sof_ipc4_copier *host_copier = time_info->host_copier; + struct sof_ipc4_copier *dai_copier = time_info->dai_copier; + struct sof_ipc4_pipeline_registers ppl_reg; + u64 stream_start_position; + u32 dai_sample_size; + u32 ch, node_index; + u32 offset; + + if (!host_copier || !dai_copier) + return -EINVAL; + + if (host_copier->data.gtw_cfg.node_id == SOF_IPC4_INVALID_NODE_ID) + return -EINVAL; + + node_index = SOF_IPC4_NODE_INDEX(host_copier->data.gtw_cfg.node_id); + offset = offsetof(struct sof_ipc4_fw_registers, pipeline_regs) + node_index * sizeof(ppl_reg); + sof_mailbox_read(sdev, sdev->fw_info_box.offset + offset, &ppl_reg, sizeof(ppl_reg)); + if (ppl_reg.stream_start_offset == SOF_IPC4_INVALID_STREAM_POSITION) + return -EINVAL; + + stream_start_position = ppl_reg.stream_start_offset; + ch = dai_copier->data.out_format.fmt_cfg; + ch = SOF_IPC4_AUDIO_FORMAT_CFG_CHANNELS_COUNT(ch); + dai_sample_size = (dai_copier->data.out_format.bit_depth >> 3) * ch; + /* convert offset to sample count */ + do_div(stream_start_position, dai_sample_size); + time_info->stream_start_offset = stream_start_position; + + return 0; +} + +static snd_pcm_sframes_t sof_ipc4_pcm_delay(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(component); + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct sof_ipc4_timestamp_info *time_info; + struct sof_ipc4_llp_reading_slot llp; + snd_pcm_uframes_t head_ptr, tail_ptr; + struct snd_sof_pcm_stream *stream; + struct snd_sof_pcm *spcm; + u64 tmp_ptr; + int ret; + + spcm = snd_sof_find_spcm_dai(component, rtd); + if (!spcm) + return 0; + + stream = &spcm->stream[substream->stream]; + time_info = stream->private; + if (!time_info) + return 0; + + /* + * stream_start_offset is updated to memory window by FW based on + * pipeline statistics and it may be invalid if host query happens before + * the statistics is complete. And it will not change after the first initiailization. + */ + if (time_info->stream_start_offset == SOF_IPC4_INVALID_STREAM_POSITION) { + ret = sof_ipc4_get_stream_start_offset(sdev, substream, stream, time_info); + if (ret < 0) + return 0; + } + + /* + * HDaudio links don't support the LLP counter reported by firmware + * the link position is read directly from hardware registers. + */ + if (!time_info->llp_offset) { + tmp_ptr = snd_sof_pcm_get_stream_position(sdev, component, substream); + if (!tmp_ptr) + return 0; + } else { + sof_mailbox_read(sdev, time_info->llp_offset, &llp, sizeof(llp)); + tmp_ptr = ((u64)llp.reading.llp_u << 32) | llp.reading.llp_l; + } + + /* In two cases dai dma position is not accurate + * (1) dai pipeline is started before host pipeline + * (2) multiple streams mixed into one. Each stream has the same dai dma position + * + * Firmware calculates correct stream_start_offset for all cases including above two. + * Driver subtracts stream_start_offset from dai dma position to get accurate one + */ + tmp_ptr -= time_info->stream_start_offset; + + /* Calculate the delay taking into account that both pointer can wrap */ + div64_u64_rem(tmp_ptr, substream->runtime->boundary, &tmp_ptr); + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + head_ptr = substream->runtime->status->hw_ptr; + tail_ptr = tmp_ptr; + } else { + head_ptr = tmp_ptr; + tail_ptr = substream->runtime->status->hw_ptr; + } + + if (head_ptr < tail_ptr) + return substream->runtime->boundary - tail_ptr + head_ptr; + + return head_ptr - tail_ptr; +} + const struct sof_ipc_pcm_ops ipc4_pcm_ops = { + .hw_params = sof_ipc4_pcm_hw_params, .trigger = sof_ipc4_pcm_trigger, .hw_free = sof_ipc4_pcm_hw_free, .dai_link_fixup = sof_ipc4_pcm_dai_link_fixup, + .pcm_setup = sof_ipc4_pcm_setup, + .pcm_free = sof_ipc4_pcm_free, + .delay = sof_ipc4_pcm_delay }; |