diff --git a/src/arch/host/configs/library_defconfig b/src/arch/host/configs/library_defconfig index 1b9483ffe858..636f3603f0bb 100644 --- a/src/arch/host/configs/library_defconfig +++ b/src/arch/host/configs/library_defconfig @@ -14,6 +14,7 @@ CONFIG_COMP_MULTIBAND_DRC=y CONFIG_COMP_MUX=y CONFIG_COMP_RTNR=y CONFIG_COMP_SEL=y +CONFIG_COMP_SOUND_DOSE=y CONFIG_COMP_SRC=y CONFIG_COMP_SRC_IPC4_FULL_MATRIX=y CONFIG_COMP_STUBS=y diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index 0a4a78d53ca8..3b5ca2287696 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -17,6 +17,11 @@ if(NOT CONFIG_COMP_MODULE_SHARED_LIBRARY_BUILD) add_subdirectory(pcm_converter) add_subdirectory(pipeline) + if(CONFIG_COMP_BASEFW_IPC4 AND NOT CONFIG_LIBRARY) + add_local_sources(sof base_fw.c) + endif() + add_local_sources_ifdef(CONFIG_IPC4_BASE_FW_INTEL sof base_fw_intel.c) + # directories and files included conditionally (alphabetical order) if(CONFIG_COMP_ARIA) add_subdirectory(aria) @@ -24,13 +29,6 @@ if(NOT CONFIG_COMP_MODULE_SHARED_LIBRARY_BUILD) if(CONFIG_COMP_ASRC) add_subdirectory(asrc) endif() - if(CONFIG_COMP_BASEFW_IPC4 AND NOT CONFIG_LIBRARY) - add_local_sources(sof base_fw.c) - endif() - add_local_sources_ifdef(CONFIG_IPC4_BASE_FW_INTEL sof base_fw_intel.c) - if(CONFIG_COMP_CHAIN_DMA) - add_local_sources(sof chain_dma.c) - endif() if(CONFIG_COMP_COPIER) add_subdirectory(copier) endif() @@ -52,19 +50,17 @@ if(NOT CONFIG_COMP_MODULE_SHARED_LIBRARY_BUILD) if(CONFIG_COMP_IIR) add_subdirectory(eq_iir) endif() - if(CONFIG_COMP_KPB AND NOT CONFIG_LIBRARY_STATIC) - add_local_sources(sof - kpb.c - ) + if(CONFIG_COMP_LEVEL_MULTIPLIER) + add_subdirectory(level_multiplier) endif() if(CONFIG_COMP_MFCC) add_subdirectory(mfcc) endif() if(CONFIG_COMP_MIXER) - add_subdirectory(mixer) + add_subdirectory(mixer) endif() if(CONFIG_COMP_MIXIN_MIXOUT) - add_subdirectory(mixin_mixout) + add_subdirectory(mixin_mixout) endif() if(CONFIG_COMP_MODULE_ADAPTER) add_subdirectory(module_adapter) @@ -84,16 +80,20 @@ if(NOT CONFIG_COMP_MODULE_SHARED_LIBRARY_BUILD) if(CONFIG_COMP_SMART_AMP) add_subdirectory(smart_amp) endif() + if(CONFIG_COMP_SOUND_DOSE) + add_subdirectory(sound_dose) + endif() if(CONFIG_COMP_SRC) add_subdirectory(src) endif() if(CONFIG_COMP_TDFB) add_subdirectory(tdfb) endif() - if(CONFIG_COMP_TONE) - add_local_sources(sof - tone.c - ) + if(CONFIG_COMP_TEMPLATE_COMP) + add_subdirectory(template_comp) + endif() + if(CONFIG_COMP_TENSORFLOW) + add_subdirectory(tensorflow) endif() if(CONFIG_COMP_UP_DOWN_MIXER) add_subdirectory(up_down_mixer) @@ -101,18 +101,30 @@ if(NOT CONFIG_COMP_MODULE_SHARED_LIBRARY_BUILD) if(CONFIG_COMP_VOLUME) add_subdirectory(volume) endif() - if(CONFIG_COMP_TENSORFLOW) - add_subdirectory(tensorflow) - endif() if(CONFIG_DTS_CODEC) add_subdirectory(codec) endif() + # end of directories and files included conditionally (alphabetical order) + add_subdirectory(google) add_subdirectory(nxp) + if(CONFIG_COMP_CHAIN_DMA) + add_local_sources(sof chain_dma.c) + endif() + if(CONFIG_COMP_KPB AND NOT CONFIG_LIBRARY_STATIC) + add_local_sources(sof + kpb.c + ) + endif() if(CONFIG_INTEL_ADSP_MIC_PRIVACY) add_subdirectory(mic_privacy_manager) endif() + if(CONFIG_COMP_TONE) + add_local_sources(sof + tone.c + ) + endif() if(CONFIG_ZEPHYR_NATIVE_DRIVERS) list(APPEND base_files host-zephyr.c) sof_list_append_ifdef(CONFIG_COMP_DAI base_files dai-zephyr.c) @@ -120,9 +132,6 @@ if(NOT CONFIG_COMP_MODULE_SHARED_LIBRARY_BUILD) list(APPEND base_files host-legacy.c) sof_list_append_ifdef(CONFIG_COMP_DAI base_files dai-legacy.c) endif() - if(CONFIG_COMP_TEMPLATE_COMP) - add_subdirectory(template_comp) - endif() endif() ### Common files (also used in shared library build) diff --git a/src/audio/Kconfig b/src/audio/Kconfig index 62dc9938b92b..3a45c97640b1 100644 --- a/src/audio/Kconfig +++ b/src/audio/Kconfig @@ -2,9 +2,6 @@ menu "Audio components" -rsource "volume/Kconfig" -rsource "aria/Kconfig" - config COMP_BASEFW_IPC4 bool "BASEFW component" default y @@ -20,8 +17,6 @@ config IPC4_BASE_FW_INTEL This implements a set of additional IPC4 properties that extend the base spec. -rsource "copier/Kconfig" - config HOST_DMA_RELOAD_DELAY_ENABLE bool "Delay reloading DMA for host interfaces" default y @@ -79,8 +74,6 @@ config MODULE_MAX_CONNECTIONS Specifies the maximum number of sink and source connections a module may have to other modules. -rsource "up_down_mixer/Kconfig" - config COMP_BLOB bool "Large IPC data as compound message blobs" default y @@ -96,8 +89,6 @@ config MODULE_MAX_BLOB_SIZE Specify the maximum size of IPC4 module blob data that can be appended to each message. -rsource "src/Kconfig" - config COMP_STUBS bool "Build all selected third-party (3P) components with stubs" default n @@ -105,10 +96,6 @@ config COMP_STUBS Select to force all 3P blocks to link against stubs rather than their libraries. This should only be used in testing environments like fuzzers or CI. -rsource "eq_fir/Kconfig" - -rsource "eq_iir/Kconfig" - config COMP_TONE bool "Tone component" default n @@ -117,12 +104,6 @@ config COMP_TONE Select for Tone component. Warning: This component is deprecated and will be removed from SOF v2.8. -rsource "mixer/Kconfig" - -rsource "mixin_mixout/Kconfig" - -rsource "mux/Kconfig" - config COMP_KPB bool "KPB component" default y @@ -139,28 +120,6 @@ config KPB_FORCE_COPY_TYPE_NORMAL endif # COMP_KPB -rsource "google/Kconfig" - -rsource "nxp/Kconfig" - -rsource "selector/Kconfig" - -rsource "crossover/Kconfig" - -rsource "drc/Kconfig" - -rsource "multiband_drc/Kconfig" - -rsource "dcblock/Kconfig" - -rsource "smart_amp/Kconfig" - -rsource "asrc/Kconfig" - -rsource "tdfb/Kconfig" - -rsource "tensorflow/Kconfig" - config COMP_MODULE_ADAPTER bool "Module adapter" default y @@ -172,17 +131,36 @@ config COMP_MODULE_ADAPTER "src\include\sof\audio\module_adapter\interfaces.h". It is possible to link several different codecs and use them in parallel. -rsource "module_adapter/Kconfig" - +# --- Kconfig Sources (alphabetical order) --- +rsource "aria/Kconfig" +rsource "asrc/Kconfig" +rsource "codec/Kconfig" +rsource "copier/Kconfig" +rsource "crossover/Kconfig" +rsource "dcblock/Kconfig" +rsource "drc/Kconfig" +rsource "eq_fir/Kconfig" +rsource "eq_iir/Kconfig" +rsource "google/Kconfig" rsource "igo_nr/Kconfig" - -rsource "rtnr/Kconfig" - rsource "mfcc/Kconfig" - -rsource "codec/Kconfig" - +rsource "mixer/Kconfig" +rsource "mixin_mixout/Kconfig" +rsource "module_adapter/Kconfig" +rsource "multiband_drc/Kconfig" +rsource "mux/Kconfig" +rsource "nxp/Kconfig" +rsource "rtnr/Kconfig" +rsource "selector/Kconfig" +rsource "smart_amp/Kconfig" +rsource "sound_dose/Kconfig" +rsource "src/Kconfig" +rsource "tdfb/Kconfig" rsource "template_comp/Kconfig" +rsource "tensorflow/Kconfig" +rsource "up_down_mixer/Kconfig" +rsource "volume/Kconfig" +# --- End Kconfig Sources (alphabetical order) --- endmenu # "Audio components" diff --git a/src/audio/eq_iir/tune/sof_eq_plot.m b/src/audio/eq_iir/tune/sof_eq_plot.m index 874bfe596c3e..4a11cf25c25f 100644 --- a/src/audio/eq_iir/tune/sof_eq_plot.m +++ b/src/audio/eq_iir/tune/sof_eq_plot.m @@ -75,14 +75,17 @@ function sof_eq_plot(eq, fn) eq.f, eq.iir_eq_db + offs_iir, '--',... eq.f, eq.fir_eq_db + offs_fir, '--'); legend('Target', 'Combined', 'IIR', 'FIR', 'Location', 'NorthWest'); + eq.diff_db = eq.tot_eq_db + offs_tot - eq.err_db_s; end if eq.enable_fir && eq.enable_iir == 0 semilogx(eq.f, eq.err_db_s, eq.f, eq.fir_eq_db + offs_fir); legend('Target', 'FIR', 'Location', 'NorthWest'); + eq.diff_db = eq.fir_eq_db + offs_fir - eq.err_db_s; end if eq.enable_fir == 0 && eq.enable_iir semilogx(eq.f, eq.err_db_s, eq.f, eq.iir_eq_db + offs_iir); legend('Target', 'IIR', 'Location', 'NorthWest'); + eq.diff_db = eq.iir_eq_db + offs_iir - eq.err_db_s; end grid on; ax=axis; axis([eq.p_fmin eq.p_fmax min(max(ax(3:4), -40), 40)]); @@ -91,6 +94,15 @@ function sof_eq_plot(eq, fn) tstr = sprintf('Filter target vs. achieved response: %s', eq.name); title(tstr); +fh=figure(fn); fn = fn+1; +semilogx(eq.f, eq.diff_db) +grid on; +ax=axis; axis([eq.p_fmin eq.p_fmax -5 5]); +xlabel('Frequency (Hz)'); +ylabel('Magnitude (dB)'); +tstr = sprintf('Response difference mean abs %.4f dB: %s', mean(abs(eq.diff_db)), eq.name); +title(tstr); + %% FIR filter if length(eq.b_fir) > 1 % Response diff --git a/src/audio/eq_iir/tune/sof_export_c_eq_uint32t.m b/src/audio/eq_iir/tune/sof_export_c_eq_uint32t.m index 7c861a4d8bbb..c7c26437b458 100644 --- a/src/audio/eq_iir/tune/sof_export_c_eq_uint32t.m +++ b/src/audio/eq_iir/tune/sof_export_c_eq_uint32t.m @@ -1,4 +1,4 @@ -function sof_export_c_eq_uint32t(fn, blob8, vn, justeq) +function sof_export_c_eq_uint32t(fn, blob8, vn, justeq, howto) % sof_export_c_eq_uint32t(fn, blob8, vn) % @@ -8,10 +8,11 @@ function sof_export_c_eq_uint32t(fn, blob8, vn, justeq) % blob8 - blob data from EQ design % vn - variable name % justeq - export just the EQ wihtout ABI headers for direct use in FW +% howto - add a comment how the header file was created % SPDX-License-Identifier: BSD-3-Clause % -% Copyright (c) 2021, Intel Corporation. All rights reserved. +% Copyright (c) 2021-2025, Intel Corporation. % Write blob fh = fopen(fn, 'w'); @@ -23,10 +24,20 @@ function sof_export_c_eq_uint32t(fn, blob8, vn, justeq) year = datestr(now, 'yyyy'); fprintf(fh, '/* SPDX-License-Identifier: BSD-3-Clause\n'); fprintf(fh, ' *\n'); - fprintf(fh, ' * Copyright(c) %s Intel Corporation. All rights reserved.\n', year); + fprintf(fh, ' * Copyright(c) %s Intel Corporation.\n', year); fprintf(fh, ' */\n'); fprintf(fh, '\n'); + if nargin == 5 + fprintf(fh, '/* Created %s with command:\n', datestr(now, 'yyyy-mm-dd')); + fprintf(fh, ' * %s\n */\n\n', howto); + end + + fprintf(fh, '#include \n\n'); + + % Drop 8 bytes TLV header + blob8 = blob8(9:end); + % Pad blob length to multiple of four bytes n_orig = length(blob8); n_new = ceil(n_orig/4); @@ -53,7 +64,7 @@ function sof_export_c_eq_uint32t(fn, blob8, vn, justeq) numbers_remain = n_new - numbers_in_line * full_lines; n = 1; - fprintf(fh, 'uint32_t %s[%d] = {\n', vn, n_new); + fprintf(fh, 'static const uint32_t %s[%d] = {\n', vn, n_new); for i = 1:full_lines fprintf(fh, '\t'); for j = 1:numbers_in_line diff --git a/src/audio/sound_dose/CMakeLists.txt b/src/audio/sound_dose/CMakeLists.txt new file mode 100644 index 000000000000..d6cd9c10b798 --- /dev/null +++ b/src/audio/sound_dose/CMakeLists.txt @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: BSD-3-Clause + +if(CONFIG_COMP_SOUND_DOSE STREQUAL "m") + add_subdirectory(llext ${PROJECT_BINARY_DIR}/sound_dose_llext) + add_dependencies(app sound_dose) +else() + add_local_sources(sof sound_dose.c) + add_local_sources(sof sound_dose-generic.c) + add_local_sources(sof sound_dose-ipc4.c) +endif() diff --git a/src/audio/sound_dose/Kconfig b/src/audio/sound_dose/Kconfig new file mode 100644 index 000000000000..a15e9a2b724a --- /dev/null +++ b/src/audio/sound_dose/Kconfig @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: BSD-3-Clause + +config COMP_SOUND_DOSE + tristate "Sound Dose module" + default m if LIBRARY_DEFAULT_MODULAR + depends on COMP_MODULE_ADAPTER + depends on IPC_MAJOR_4 + select MATH_IIR + select MATH_IIR_DF1 + select MATH_EXP + help + Select this for Sound Dose SOF module. The purpose is + to calculate for audio playback MEL values (momentary + sound exposure level) to provide to user space the data + to compute the sound dose CSD as defined in EN 50332-3. diff --git a/src/audio/sound_dose/README.md b/src/audio/sound_dose/README.md new file mode 100644 index 000000000000..2a25ad19d1ba --- /dev/null +++ b/src/audio/sound_dose/README.md @@ -0,0 +1,75 @@ +Sound Dose + +The purpose of this component is to calculate with DSP offload the +headphone audio playback MEL values (momentary sound exposure level). +The calculated MEL values are notified to user space to be available +every one second. The user space should respond to notification with +bytes control get to get the data. The MEL values are used to +calculate the CSD value (cumulative sound dose). Low enough CSD value +ensures the music listening is safe the user's hearing. + +The calculation of MEL values and CSD values is defined in EN 50332-3 +stadard. The implementation in the Android OS is described in +https://source.android.com/docs/core/audio/sound-dose . + +The SOF Sound Dose component should be placed in topology as last +component before playback dai-copier. + +Currently it can be tested with next test topologies: +- sof-hda-benchmark-sound_dose32.tplg +- sof-mtl-sdw-benchmark-sound_dose32-sdw0.tplg +- sof-mtl-sdw-benchmark-sound_dose32-simplejack.tplg + +E.g. in the sdw topologies the controls for setting it up can be +seen with command: + +$ amixer -c0 controls | grep "Jack Out Sound Dose" +numid=33,iface=MIXER,name='Jack Out Sound Dose data bytes' +numid=32,iface=MIXER,name='Jack Out Sound Dose gain bytes' +numid=30,iface=MIXER,name='Jack Out Sound Dose setup bytes' +numid=31,iface=MIXER,name='Jack Out Sound Dose volume bytes' + +The above topologies program the setup bytes for acoustical +sensitivity of acoustical 100 dBSPL for 0 dBFS digital level. +The gain is set to 0 dB and volume to 0 dB. + +To test the basics copy a compatible test topology over the +normal topology file. Or alternative use module options +tplg_filename and tplg_path for module snd_sof. + +Get kcontrol_events from https://github.com/ujfalusi/kcontrol_events +and build and install it. + +Start playback of some music or test signal. Note that there is no SOF +volume control component in the test topology, so it plays out at max +volume. + +Start in other console kcontrol_events. The control 'Jack Out Sound +Dose data bytes' should get updates every one second. The structures +are defined in src/include/user/audio_feature.h and +src/include/user/sound_dose.h. + +The next steps require build of blobs for sof-ctl. The blobs can be +rebuilt with command + +cd src/audio/sound_dose/tune; octave --quiet --no-window-system sof_sound_dose_blobs.m + +To simulate effect of initial sensitivity setup, try command + +sof-ctl -c name='Jack Out Sound Dose setup bytes' -s /sound_dose/setup_sens_0db.txt +sof-ctl -c name='Jack Out Sound Dose setup bytes' -s /sound_dose/setup_sens_100db.txt + +To simulate adjusting codec volume control down 10 dB and up again 10 dB + +sof-ctl -c name='Jack Out Sound Dose volume bytes' -s ctl4/sound_dose/setup_vol_-10db.txt +sof-ctl -c name='Jack Out Sound Dose volume bytes' -s ctl4/sound_dose/setup_vol_0db.txt + +To force user's listening level 10 dB down after observing too large dose per the MSD value + +sof-ctl -c name='Jack Out Sound Dose gain bytes' -s ctl4/sound_dose/setup_gain_-10db.txt + +To restore user's listening level to non-attenuated + +sof-ctl -c name='Jack Out Sound Dose gain bytes' -s ctl4/sound_dose/setup_gain_0db.txt + +The above commands have impact to data shown by kcontrol_events. diff --git a/src/audio/sound_dose/llext/CMakeLists.txt b/src/audio/sound_dose/llext/CMakeLists.txt new file mode 100644 index 000000000000..705280b08648 --- /dev/null +++ b/src/audio/sound_dose/llext/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2025 Intel Corporation. +# SPDX-License-Identifier: Apache-2.0 + +sof_llext_build("sound_dose" + SOURCES ../sound_dose.c + ../sound_dose-generic.c + ../sound_dose-ipc4.c + LIB openmodules +) diff --git a/src/audio/sound_dose/llext/llext.toml.h b/src/audio/sound_dose/llext/llext.toml.h new file mode 100644 index 000000000000..b5bc25562bd2 --- /dev/null +++ b/src/audio/sound_dose/llext/llext.toml.h @@ -0,0 +1,6 @@ +#include +#define LOAD_TYPE "2" +#include "../sound_dose.toml" + +[module] +count = __COUNTER__ diff --git a/src/audio/sound_dose/sound_dose-generic.c b/src/audio/sound_dose/sound_dose-generic.c new file mode 100644 index 000000000000..fb5f63fc844a --- /dev/null +++ b/src/audio/sound_dose/sound_dose-generic.c @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2025 Intel Corporation. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sound_dose.h" + +LOG_MODULE_DECLARE(sound_dose, CONFIG_SOF_LOG_LEVEL); + +static void sound_dose_calculate_mel(const struct processing_module *mod, int frames) +{ + struct sound_dose_comp_data *cd = module_get_private_data(mod); + uint64_t energy_sum; + uint32_t log_arg; + int32_t tmp; + int ch; + + cd->frames_count += frames; + if (cd->frames_count < cd->report_count) + return; + + cd->frames_count = 0; + energy_sum = 0; + for (ch = 0; ch < cd->channels; ch++) { + energy_sum += cd->energy[ch]; + cd->energy[ch] = 0; + } + + /* Log2 argument is Q32.0 unsigned, log2 returns Q16.16 signed. + * Energy is Qx.30, so the argument 2^30 times scaled. Also to keep + * argument within uint32_t range, need to scale it down by 19. + * To compensate these, need to add 19 (Q16.16) and subtract 30 (Q16.16) + * from logarithm value. + */ + log_arg = (uint32_t)(energy_sum >> SOUND_DOSE_ENERGY_SHIFT); + log_arg = MAX(log_arg, 1); + tmp = base2_logarithm(log_arg); + tmp += SOUND_DOSE_LOG_FIXED_OFFSET; /* Compensate Q2.30 and energy shift */ + tmp += cd->log_offset_for_mean; /* logarithm subtract for mean */ + tmp = Q_MULTSR_32X32((int64_t)tmp, SOUND_DOSE_TEN_OVER_LOG2_10_Q29, + SOUND_DOSE_LOGOFFS_Q, SOUND_DOSE_LOGMULT_Q, SOUND_DOSE_LOGOFFS_Q); + cd->level_dbfs = tmp + SOUND_DOSE_WEIGHT_FILTERS_OFFS_Q16 + SOUND_DOSE_DFBS_OFFS_Q16; + + /* If stereo sum channel level values and subtract 3 dB, to generalize + * For stereo or more subtract -1.5 dB per channel. + */ + if (cd->channels > 1) + cd->level_dbfs += cd->channels * SOUND_DOSE_MEL_CHANNELS_SUM_FIX; + + sound_dose_report_mel(mod); +} + +#if CONFIG_FORMAT_S16LE + +/** + * sound_dose_s16() - Process S16_LE format. + * @mod: Pointer to module data. + * @source: Source for PCM samples data. + * @sink: Sink for PCM samples data. + * @frames: Number of audio data frames to process. + * + * This is the processing function for 16-bit signed integer PCM formats. The + * audio samples in every frame are re-order to channels order defined in + * component data channel_map[]. + * + * Return: Value zero for success, otherwise an error code. + */ +static int sound_dose_s16(const struct processing_module *mod, + struct sof_source *source, + struct sof_sink *sink, + uint32_t frames) +{ + struct sound_dose_comp_data *cd = module_get_private_data(mod); + struct iir_state_df1 *iir; + int32_t weighted; + int16_t sample; + int16_t const *x0, *x, *x_start, *x_end; + int16_t *y0, *y, *y_start, *y_end; + int x_size, y_size; + int source_samples_without_wrap; + int samples_without_wrap; + int samples = frames * cd->channels; + int bytes = frames * cd->frame_bytes; + int ret; + int ch; + int i; + const int channels = cd->channels; + + /* Get pointer to source data in circular buffer, get buffer start and size to + * check for wrap. The size in bytes is converted to number of s16 samples to + * control the samples process loop. If the number of bytes requested is not + * possible, an error is returned. + */ + ret = source_get_data_s16(source, bytes, &x0, &x_start, &x_size); + if (ret) + return ret; + + /* Similarly get pointer to sink data in circular buffer, buffer start and size. */ + ret = sink_get_buffer_s16(sink, bytes, &y0, &y_start, &y_size); + if (ret) + return ret; + + /* Set helper pointers to buffer end for wrap check. Then loop until all + * samples are processed. + */ + x_end = x_start + x_size; + y_end = y_start + y_size; + while (samples) { + /* Find out samples to process before first wrap or end of data. */ + source_samples_without_wrap = x_end - x0; + samples_without_wrap = y_end - y0; + samples_without_wrap = MIN(samples_without_wrap, source_samples_without_wrap); + samples_without_wrap = MIN(samples_without_wrap, samples); + for (ch = 0; ch < cd->channels; ch++) { + iir = &cd->iir[ch]; + x = x0++; + y = y0++; + for (i = 0; i < samples_without_wrap; i += channels) { + sample = sat_int16(Q_MULTSR_32X32((int64_t)cd->gain, *x, + SOUND_DOSE_GAIN_Q, + SOUND_DOSE_S16_Q, + SOUND_DOSE_S16_Q)); + *y = sample; + x += channels; + y += channels; + weighted = iir_df1(iir, ((int32_t)sample) << 16) >> 16; + + /* Update sound dose, energy is Q1.15 * Q1.15 --> Q2.30 */ + cd->energy[ch] += weighted * weighted; + } + } + + /* One of the buffers needs a wrap (or end of data), so check for wrap */ + x0 += samples_without_wrap; + y0 += samples_without_wrap; + x0 = (x0 >= x_end) ? x0 - x_size : x0; + y0 = (y0 >= y_end) ? y0 - y_size : y0; + + /* Update processed samples count for next loop iteration. */ + samples -= samples_without_wrap; + } + + /* Update the source and sink for bytes consumed and produced. Return success. */ + source_release_data(source, bytes); + sink_commit_buffer(sink, bytes); + + sound_dose_calculate_mel(mod, frames); + return 0; +} +#endif /* CONFIG_FORMAT_S16LE */ + +#if CONFIG_FORMAT_S32LE || CONFIG_FORMAT_S24LE + +/** + * sound_dose_s32() - Process S32_LE or S24_4LE format. + * @mod: Pointer to module data. + * @source: Source for PCM samples data. + * @sink: Sink for PCM samples data. + * @frames: Number of audio data frames to process. + * + * Processing function for signed integer 32-bit PCM formats. The same + * function works for s24 and s32 formats since the samples values are + * not modified in computation. The audio samples in every frame are + * re-order to channels order defined in component data channel_map[]. + * + * Return: Value zero for success, otherwise an error code. + */ +static int sound_dose_s32(const struct processing_module *mod, + struct sof_source *source, + struct sof_sink *sink, + uint32_t frames) +{ + struct sound_dose_comp_data *cd = module_get_private_data(mod); + struct iir_state_df1 *iir; + int32_t sample; + int32_t const *x0, *x, *x_start, *x_end; + int32_t *y0, *y, *y_start, *y_end; + int32_t weighted; + int x_size, y_size; + int source_samples_without_wrap; + int samples_without_wrap; + int samples = frames * cd->channels; + int bytes = frames * cd->frame_bytes; + int ret; + int ch; + int i; + const int channels = cd->channels; + + /* Get pointer to source data in circular buffer, get buffer start and size to + * check for wrap. The size in bytes is converted to number of s32 samples to + * control the samples process loop. If the number of bytes requested is not + * possible, an error is returned. + */ + ret = source_get_data_s32(source, bytes, &x0, &x_start, &x_size); + if (ret) + return ret; + + /* Similarly get pointer to sink data in circular buffer, buffer start and size. */ + ret = sink_get_buffer_s32(sink, bytes, &y0, &y_start, &y_size); + if (ret) + return ret; + + /* Set helper pointers to buffer end for wrap check. Then loop until all + * samples are processed. + */ + x_end = x_start + x_size; + y_end = y_start + y_size; + while (samples) { + /* Find out samples to process before first wrap or end of data. */ + source_samples_without_wrap = x_end - x0; + samples_without_wrap = y_end - y0; + samples_without_wrap = MIN(samples_without_wrap, source_samples_without_wrap); + samples_without_wrap = MIN(samples_without_wrap, samples); + for (ch = 0; ch < cd->channels; ch++) { + iir = &cd->iir[ch]; + x = x0++; + y = y0++; + for (i = 0; i < samples_without_wrap; i += channels) { + sample = sat_int32(Q_MULTSR_32X32((int64_t)cd->gain, *x, + SOUND_DOSE_GAIN_Q, + SOUND_DOSE_S32_Q, + SOUND_DOSE_S32_Q)); + *y = sample; + x += channels; + y += channels; + weighted = iir_df1(iir, sample) >> 16; + + /* Update sound dose, energy is Q1.15 * Q1.15 --> Q2.30 */ + cd->energy[ch] += weighted * weighted; + } + } + + /* One of the buffers needs a wrap (or end of data), so check for wrap */ + x0 += samples_without_wrap; + y0 += samples_without_wrap; + x0 = (x0 >= x_end) ? x0 - x_size : x0; + y0 = (y0 >= y_end) ? y0 - y_size : y0; + + /* Update processed samples count for next loop iteration. */ + samples -= samples_without_wrap; + } + + /* Update the source and sink for bytes consumed and produced. Return success. */ + source_release_data(source, bytes); + sink_commit_buffer(sink, bytes); + + sound_dose_calculate_mel(mod, frames); + return 0; +} +#endif /* CONFIG_FORMAT_S32LE || CONFIG_FORMAT_S24LE */ + +/* This struct array defines the used processing functions for + * the PCM formats + */ +const struct sound_dose_proc_fnmap sound_dose_proc_fnmap[] = { +#if CONFIG_FORMAT_S16LE + { SOF_IPC_FRAME_S16_LE, sound_dose_s16}, +#endif +#if CONFIG_FORMAT_S24LE + { SOF_IPC_FRAME_S24_4LE, sound_dose_s32}, +#endif +#if CONFIG_FORMAT_S32LE + { SOF_IPC_FRAME_S32_LE, sound_dose_s32}, +#endif +}; + +/** + * sound_dose_find_proc_func() - Find suitable processing function. + * @src_fmt: Enum value for PCM format. + * + * This function finds the suitable processing function to use for + * the used PCM format. If not found, return NULL. + * + * Return: Pointer to processing function for the requested PCM format. + */ +sound_dose_func sound_dose_find_proc_func(enum sof_ipc_frame src_fmt) +{ + int i; + + /* Find suitable processing function from map */ + for (i = 0; i < ARRAY_SIZE(sound_dose_proc_fnmap); i++) + if (src_fmt == sound_dose_proc_fnmap[i].frame_fmt) + return sound_dose_proc_fnmap[i].sound_dose_proc_func; + + return NULL; +} diff --git a/src/audio/sound_dose/sound_dose-ipc4.c b/src/audio/sound_dose/sound_dose-ipc4.c new file mode 100644 index 000000000000..fd3a8151e984 --- /dev/null +++ b/src/audio/sound_dose/sound_dose-ipc4.c @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2025 Intel Corporation. + +#include +#include +#include +#include +#include +#include + +#include "sound_dose.h" + +LOG_MODULE_DECLARE(sound_dose, CONFIG_SOF_LOG_LEVEL); + +static struct ipc_msg *sound_dose_notification_init(struct processing_module *mod, + uint32_t control_type_param_id, + uint32_t control_id) +{ + struct ipc_msg msg_proto; + struct comp_dev *dev = mod->dev; + struct comp_ipc_config *ipc_config = &dev->ipc_config; + union ipc4_notification_header *primary = + (union ipc4_notification_header *)&msg_proto.header; + struct sof_ipc4_notify_module_data *msg_module_data; + struct sof_ipc4_control_msg_payload *msg_payload; + struct ipc_msg *msg; + + /* Clear header, extension, and other ipc_msg members */ + memset_s(&msg_proto, sizeof(msg_proto), 0, sizeof(msg_proto)); + primary->r.notif_type = SOF_IPC4_MODULE_NOTIFICATION; + primary->r.type = SOF_IPC4_GLB_NOTIFICATION; + primary->r.rsp = SOF_IPC4_MESSAGE_DIR_MSG_REQUEST; + primary->r.msg_tgt = SOF_IPC4_MESSAGE_TARGET_FW_GEN_MSG; + msg = ipc_msg_w_ext_init(msg_proto.header, msg_proto.extension, + sizeof(struct sof_ipc4_notify_module_data) + + sizeof(struct sof_ipc4_control_msg_payload)); + if (!msg) + return NULL; + + msg_module_data = (struct sof_ipc4_notify_module_data *)msg->tx_data; + msg_module_data->instance_id = IPC4_INST_ID(ipc_config->id); + msg_module_data->module_id = IPC4_MOD_ID(ipc_config->id); + msg_module_data->event_id = SOF_IPC4_NOTIFY_MODULE_EVENTID_ALSA_MAGIC_VAL | + control_type_param_id; + msg_module_data->event_data_size = sizeof(struct sof_ipc4_control_msg_payload); + msg_payload = (struct sof_ipc4_control_msg_payload *)msg_module_data->event_data; + msg_payload->id = control_id; + msg_payload->num_elems = 0; + comp_dbg(dev, "instance_id = 0x%08x, module_id = 0x%08x", + msg_module_data->instance_id, msg_module_data->module_id); + return msg; +} + +int sound_dose_ipc_notification_init(struct processing_module *mod) +{ + struct sound_dose_comp_data *cd = module_get_private_data(mod); + + cd->msg = sound_dose_notification_init(mod, SOF_IPC4_BYTES_CONTROL_PARAM_ID, + SOF_SOUND_DOSE_PAYLOAD_PARAM_ID); + if (!cd->msg) { + comp_err(mod->dev, "Failed to initialize control notification."); + return -ENOMEM; + } + + return 0; +} + +void sound_dose_send_ipc_notification(const struct processing_module *mod) +{ + struct sound_dose_comp_data *cd = module_get_private_data(mod); + + ipc_msg_send(cd->msg, cd->msg->tx_data, false); +} + +/* This function handles the set_config() commands. The controls have the next purpose: + * - SOF_SOUND_DOSE_SETUP_PARAM_ID sets the acoustical sensitivity of the DAC, headphone + * amplifier and assumed worst-case loud headphones. E.g. 0 dBFS equals 100 dBSPL. The + * sensitivity is for max user volume. + * -SOF_SOUND_DOSE_VOLUME_PARAM_ID is set to new decibels value if volume is adjusted + * down from user maximum. + * - SOF_SOUND_DOSE_GAIN_PARAM_ID is normally set to 0 decibels value. If the user's + * listening is exceeding the safe MSD threshold the user's volume can be forced down + * with this gain. The bytes control is not visible in the mixer, so there is no simple + * way for user to force volume up with alsamixer or amixer utililities. The gain + * should be restored to 0 dB after the MSD value looks again safe. + * E.g. setting -10 dB value lowers user's listening volume with -10 dB and also lower + * the reported MEL values by -10 dB. + * + * These controls can be used as preferred by the MEL->MSD algorithm in user space. The + * controls offset the reported MEL values. The same offset can be also done in user + * space. And could use codec volume instead of SOF_SOUND_DOSE_GAIN_PARAM_ID if preferred. + */ +__cold static int +_sound_dose_set_config(struct processing_module *mod, uint32_t param_id, + uint32_t control_id, const void *data, uint32_t data_size) +{ + struct sound_dose_comp_data *cd = module_get_private_data(mod); + struct sound_dose_setup_config *new_setup; + struct sound_dose_volume_config *new_volume; + struct sound_dose_gain_config *new_gain; + struct comp_dev *dev = mod->dev; + uint8_t *dest; + size_t dest_size; + + assert_can_be_cold(); + + if (param_id != SOF_IPC4_BYTES_CONTROL_PARAM_ID) { + comp_warn(dev, "Not supported control type: %u", param_id); + return 0; + } + + comp_dbg(dev, "param_id = %u, control_id: %u", param_id, control_id); + + switch (control_id) { + case SOF_SOUND_DOSE_SETUP_PARAM_ID: + dest_size = sizeof(struct sound_dose_setup_config); + dest = (uint8_t *)&cd->setup; + break; + case SOF_SOUND_DOSE_VOLUME_PARAM_ID: + dest_size = sizeof(struct sound_dose_volume_config); + dest = (uint8_t *)&cd->vol; + break; + case SOF_SOUND_DOSE_GAIN_PARAM_ID: + dest_size = sizeof(struct sound_dose_gain_config); + dest = (uint8_t *)&cd->att; + break; + case SOF_SOUND_DOSE_PAYLOAD_PARAM_ID: + return 0; /* Just return, no need to set the audio feature data */ + default: + comp_warn(dev, "Ignored illegal control_id: %u", control_id); + return 0; + } + + if (data_size != dest_size) { + comp_err(dev, "Illegal fragment_size %u for %u:%u", + data_size, param_id, control_id); + return -EINVAL; + } + + switch (control_id) { + case SOF_SOUND_DOSE_SETUP_PARAM_ID: + new_setup = (struct sound_dose_setup_config *)data; + if (new_setup->sens_dbfs_dbspl < SOF_SOUND_DOSE_SENS_MIN_DB || + new_setup->sens_dbfs_dbspl > SOF_SOUND_DOSE_SENS_MAX_DB) { + comp_err(dev, "Illegal sensitivity = %d", new_setup->sens_dbfs_dbspl); + return -EINVAL; + } + break; + case SOF_SOUND_DOSE_VOLUME_PARAM_ID: + new_volume = (struct sound_dose_volume_config *)data; + if (new_volume->volume_offset < SOF_SOUND_DOSE_VOLUME_MIN_DB || + new_volume->volume_offset > SOF_SOUND_DOSE_VOLUME_MAX_DB) { + comp_err(dev, "Illegal volume = %d", new_volume->volume_offset); + return -EINVAL; + } + break; + case SOF_SOUND_DOSE_GAIN_PARAM_ID: + new_gain = (struct sound_dose_gain_config *)data; + if (new_gain->gain < SOF_SOUND_DOSE_GAIN_MIN_DB || + new_gain->gain > SOF_SOUND_DOSE_GAIN_MAX_DB) { + comp_err(dev, "Illegal gain = %d", new_gain->gain); + return -EINVAL; + } + cd->gain_update = true; + break; + } + + memcpy_s(dest, dest_size, data, data_size); + return 0; +} + +/* This function is the main set_config() handler. The two variants of bytes control + * are handled. The case where param_id is set to SOF_IPC4_BYTES_CONTROL_PARAM_ID + * with header sof_ipc4_control_msg_payload has the benefit of being able to pass + * the id of the control. It is useful when there are multiple instances of + * bytes control. The legacy case is where control instances are identified with + * param_id value. The first format with header must be used when a control supports + * a notification to user space. The legacy works only for simple set control usage. + */ +__cold int sound_dose_set_config(struct processing_module *mod, + uint32_t param_id, + enum module_cfg_fragment_position pos, + uint32_t data_offset_size, + const uint8_t *fragment, + size_t fragment_size, + uint8_t *response, + size_t response_size) +{ + int ret; + + assert_can_be_cold(); + + if (!fragment_size) { + comp_warn(mod->dev, "Zero fragment size for param_id %d", param_id); + return 0; + } + + if (param_id == SOF_IPC4_BYTES_CONTROL_PARAM_ID) { + struct sof_ipc4_control_msg_payload *msg_payload; + + msg_payload = (struct sof_ipc4_control_msg_payload *)fragment; + + ret = _sound_dose_set_config(mod, param_id, msg_payload->id, + msg_payload->data, msg_payload->num_elems); + } else { + ret = _sound_dose_set_config(mod, SOF_IPC4_BYTES_CONTROL_PARAM_ID, + param_id, fragment, fragment_size); + } + + return ret; +} + +/* This function copies the data for get_config() request by the driver. Only + * the SOF_SOUND_DOSE_PAYLOAD_PARAM_ID as control_id is supported. The audio + * feature payload for sound_dose is copied to the recipient. + */ +__cold static int +_sound_dose_get_config(struct processing_module *mod, uint32_t param_id, + uint32_t control_id, uint32_t *size_out, + uint8_t *data, size_t data_size) +{ + struct sound_dose_comp_data *cd = module_get_private_data(mod); + struct comp_dev *dev = mod->dev; + size_t payload_size; + + assert_can_be_cold(); + + if (param_id != SOF_IPC4_BYTES_CONTROL_PARAM_ID) { + comp_warn(dev, "Not supported control type: %u", param_id); + return 0; + } + + comp_dbg(dev, "param_id = %u, control_id: %u", param_id, control_id); + + if (control_id != SOF_SOUND_DOSE_PAYLOAD_PARAM_ID) { + comp_warn(dev, "Ignored get config control_id: %u", control_id); + memset(data, 0, data_size); + *size_out = 0; + return 0; + } + + payload_size = cd->abi->size; + memcpy_s(data, data_size, cd->abi->data, payload_size); + *size_out = payload_size; + + return 0; +} + +/* This is the main get_config() handler. As in set_config() case, the no-header + * way is legacy. The response get_config() after notify must use the + * sof_ipc4_control_msg_payload header. + */ +__cold int sound_dose_get_config(struct processing_module *mod, + uint32_t param_id, uint32_t *data_offset_size, + uint8_t *fragment, size_t fragment_size) +{ + int ret; + + assert_can_be_cold(); + + if (!fragment_size) { + comp_warn(mod->dev, "Zero fragment size for param_id %d", param_id); + return 0; + } + + if (param_id == SOF_IPC4_BYTES_CONTROL_PARAM_ID) { + struct sof_ipc4_control_msg_payload *msg_payload; + + /* Note: msg_payload will get overwrite as fragment. */ + msg_payload = (struct sof_ipc4_control_msg_payload *)fragment; + ret = _sound_dose_get_config(mod, param_id, msg_payload->id, + data_offset_size, fragment, + fragment_size); + } else { + ret = _sound_dose_get_config(mod, SOF_IPC4_BYTES_CONTROL_PARAM_ID, + param_id, data_offset_size, + fragment, fragment_size); + } + + return ret; +} diff --git a/src/audio/sound_dose/sound_dose.c b/src/audio/sound_dose/sound_dose.c new file mode 100644 index 000000000000..dcbac5c643c7 --- /dev/null +++ b/src/audio/sound_dose/sound_dose.c @@ -0,0 +1,375 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2025 Intel Corporation. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "sound_dose.h" +#include "sound_dose_iir_44k.h" +#include "sound_dose_iir_48k.h" + +/* UUID identifies the components. Use e.g. command uuidgen from package + * uuid-runtime, add it to uuid-registry.txt in SOF top level. + */ +SOF_DEFINE_REG_UUID(sound_dose); + +/* Creates logging data for the component */ +LOG_MODULE_REGISTER(sound_dose, CONFIG_SOF_LOG_LEVEL); + +/* Creates the component trace. Traces show in trace console the component + * info, warning, and error messages. + */ +DECLARE_TR_CTX(sound_dose_tr, SOF_UUID(sound_dose_uuid), LOG_LEVEL_INFO); + +void sound_dose_report_mel(const struct processing_module *mod) +{ + struct sound_dose_comp_data *cd = module_get_private_data(mod); + uint64_t tmp_h, tmp_l; + + cd->dose->current_sens_dbfs_dbspl = cd->setup.sens_dbfs_dbspl; + cd->dose->current_volume_offset = cd->vol.volume_offset; + cd->dose->current_gain = cd->att.gain; + cd->dose->dbfs_value = (int32_t)(((int64_t)cd->level_dbfs * 100) >> 16); + cd->dose->mel_value = cd->dose->dbfs_value + + cd->setup.sens_dbfs_dbspl + + cd->vol.volume_offset; + /* Multiply in two 32x32 -> 64 bits parts and do sum of the parts + * including the shift from Q26 to Q0. The simplified 96 bits + * equation would be time = ((tmp_h << 32) + tmp_l) >> 26. + */ + tmp_l = (cd->total_frames_count & 0xffffffff) * cd->rate_to_us_coef; + tmp_h = (cd->total_frames_count >> 32) * cd->rate_to_us_coef; + cd->feature->stream_time_us = (tmp_l >> 26) + ((tmp_h & ((1LL << 32) - 1)) << 6); +#if SOUND_DOSE_DEBUG + comp_info(mod->dev, "Time %d dBFS %d MEL %d", + (int32_t)(cd->feature->stream_time_us / 1000000), + cd->dose->dbfs_value, cd->dose->mel_value); +#endif + + sound_dose_send_ipc_notification(mod); +} + +int sound_dose_filters_init(struct processing_module *mod) +{ + struct sound_dose_comp_data *cd = module_get_private_data(mod); + struct sof_eq_iir_config *iir_config; + struct sof_eq_iir_header *iir_coef; + struct comp_dev *dev = mod->dev; + struct sof_abi_hdr *blob; + size_t iir_size, alloc_size; + int32_t *data; + int i; + + /* Initialize FIR */ + switch (cd->rate) { + case 48000: + blob = (struct sof_abi_hdr *)sound_dose_iir_48k; + iir_config = (struct sof_eq_iir_config *)blob->data; + cd->log_offset_for_mean = SOUND_DOSE_LOG2_INV_48K_Q16; + cd->rate_to_us_coef = SOUND_DOSE_1M_OVER_48K_Q26; + break; + case 44100: + blob = (struct sof_abi_hdr *)sound_dose_iir_44k; + iir_config = (struct sof_eq_iir_config *)blob->data; + cd->log_offset_for_mean = SOUND_DOSE_LOG2_INV_44K_Q16; + cd->rate_to_us_coef = SOUND_DOSE_1M_OVER_44K_Q26; + break; + default: + /* TODO: 96 kHz and 192 kHz with integer decimation factor. The A-weight is not + * defined above 20 kHz, so high frequency energy is not needed. Also it will + * help keep the module load reasonable. + */ + comp_err(dev, "error: unsupported sample rate %d", cd->rate); + return -EINVAL; + } + + /* Apply the first responses in the blobs */ + iir_coef = (struct sof_eq_iir_header *)&iir_config->data[iir_config->channels_in_config]; + iir_size = iir_delay_size_df1(iir_coef); + alloc_size = cd->channels * iir_size; + cd->delay_lines = rzalloc(SOF_MEM_FLAG_USER, alloc_size); + if (!cd->delay_lines) { + comp_err(dev, "Failed to allocate memory for weighting filters."); + return -ENOMEM; + } + + data = cd->delay_lines; + for (i = 0; i < cd->channels; i++) { + iir_init_coef_df1(&cd->iir[i], iir_coef); + iir_init_delay_df1(&cd->iir[i], &data); + cd->energy[i] = 0; + } + + cd->report_count = cd->rate; /* report every 1s, for 48k frames */ + return 0; +} + +void sound_dose_filters_free(struct sound_dose_comp_data *cd) +{ + rfree(cd->delay_lines); +} + +__cold static void sound_dose_setup_init(struct sound_dose_comp_data *cd) +{ + cd->setup.sens_dbfs_dbspl = 0; /* 0 dbFS is 100 dB */ + cd->vol.volume_offset = 0; /* Assume max vol */ + cd->att.gain = 0; /* No attenuation, 0 dB */ + cd->gain = SOUND_DOSE_GAIN_ONE_Q30; + cd->new_gain = SOUND_DOSE_GAIN_ONE_Q30; +} + +static int sound_dose_audio_feature_init(struct processing_module *mod) +{ + struct sound_dose_comp_data *cd = module_get_private_data(mod); + size_t alloc_size = sizeof(struct sof_abi_hdr) + + sizeof(struct sof_audio_feature) + + sizeof(struct sof_sound_dose); + + cd->abi = rzalloc(SOF_MEM_FLAG_USER, alloc_size); + if (!cd->abi) { + comp_err(mod->dev, "Failed to allocate audio feature data."); + return -ENOMEM; + } + + cd->abi->magic = SOF_IPC4_ABI_MAGIC; + cd->abi->abi = SOF_ABI_VERSION; + cd->abi->size = sizeof(struct sof_audio_feature) + sizeof(struct sof_sound_dose); + cd->feature = (struct sof_audio_feature *)cd->abi->data; + cd->feature->data_size = sizeof(struct sof_sound_dose); + cd->feature->type = SOF_AUDIO_FEATURE_SOUND_DOSE_MEL; + cd->feature->num_audio_features = 1; /* Single MEL value in audio feature data */ + cd->dose = (struct sof_sound_dose *)cd->feature->data; + return 0; +} + +/** + * sound_dose_init() - Initialize the component. + * @mod: Pointer to module data. + * + * This function is called when the instance is created. The + * macro __cold informs that the code that is non-critical + * is loaded to slower but large DRAM. + * + * Return: Zero if success, otherwise error code. + */ +__cold static int sound_dose_init(struct processing_module *mod) +{ + struct module_data *md = &mod->priv; + struct comp_dev *dev = mod->dev; + struct sound_dose_comp_data *cd; + int ret; + + comp_info(dev, "Initialize"); + + cd = rzalloc(SOF_MEM_FLAG_USER, sizeof(*cd)); + if (!cd) { + comp_err(dev, "Failed to allocate component data."); + return -ENOMEM; + } + + md->private = cd; + + sound_dose_setup_init(cd); + ret = sound_dose_audio_feature_init(mod); + if (ret) { + rfree(cd); + return ret; + } + + ret = sound_dose_ipc_notification_init(mod); + if (ret) { + rfree(cd->abi); + rfree(cd); + } + + return ret; +} + +/** + * sound_dose_process() - The audio data processing function. + * @mod: Pointer to module data. + * @sources: Pointer to audio samples data sources array. + * @num_of_sources: Number of sources in the array. + * @sinks: Pointer to audio samples data sinks array. + * @num_of_sinks: Number of sinks in the array. + * + * This is the processing function that is called for scheduled + * pipelines. The processing is controlled by the enable switch. + * + * Return: Zero if success, otherwise error code. + */ +static int sound_dose_process(struct processing_module *mod, + struct sof_source **sources, + int num_of_sources, + struct sof_sink **sinks, + int num_of_sinks) +{ + struct sound_dose_comp_data *cd = module_get_private_data(mod); + struct sof_source *source = sources[0]; /* One input in this example */ + struct sof_sink *sink = sinks[0]; /* One output in this example */ + struct comp_dev *dev = mod->dev; + int32_t arg; + int frames = source_get_data_frames_available(source); + int sink_frames = sink_get_free_frames(sink); + + comp_dbg(dev, "sound_dose_process()"); + + if (cd->gain_update) { + /* Convert dB * 100 to Q8.24 */ + arg = (int32_t)cd->att.gain * SOUND_DOSE_ONE_OVER_100_Q24; + + /* Calculate linear value as Q12.20 and convert to Q2.30 */ + cd->new_gain = Q_SHIFT_LEFT(sofm_db2lin_fixed(arg), 20, 30); + cd->gain_update = false; + } + + if (cd->new_gain < cd->gain) { + cd->gain = Q_MULTSR_32X32((int64_t)cd->gain, SOUND_DOSE_GAIN_DOWN_Q30, 30, 30, 30); + cd->gain = MAX(cd->gain, cd->new_gain); + } else if (cd->new_gain > cd->gain) { + cd->gain = Q_MULTSR_32X32((int64_t)cd->gain, SOUND_DOSE_GAIN_UP_Q30, 30, 30, 30); + cd->gain = MIN(cd->gain, SOUND_DOSE_GAIN_ONE_Q30); + } + + frames = MIN(frames, sink_frames); + cd->total_frames_count += frames; + return cd->sound_dose_func(mod, source, sink, frames); +} + +/** + * sound_dose_prepare() - Prepare the component for processing. + * @mod: Pointer to module data. + * @sources: Pointer to audio samples data sources array. + * @num_of_sources: Number of sources in the array. + * @sinks: Pointer to audio samples data sinks array. + * @num_of_sinks: Number of sinks in the array. + * + * Function prepare is called just before the pipeline is started. In + * this case the audio format parameters are for better code performance + * saved to component data to avoid to find out them in process. The + * processing function pointer is set to process the current audio format. + * + * Return: Value zero if success, otherwise error code. + */ +static int sound_dose_prepare(struct processing_module *mod, + struct sof_source **sources, int num_of_sources, + struct sof_sink **sinks, int num_of_sinks) +{ + struct sound_dose_comp_data *cd = module_get_private_data(mod); + struct comp_dev *dev = mod->dev; + enum sof_ipc_frame source_format; + + comp_dbg(dev, "sound_dose_prepare()"); + + /* The processing example in this component supports one input and one + * output. Generally there can be more. + */ + if (num_of_sources != 1 || num_of_sinks != 1) + return -EINVAL; + + /* get source data format */ + cd->frame_bytes = source_get_frame_bytes(sources[0]); + cd->channels = source_get_channels(sources[0]); + source_format = source_get_frm_fmt(sources[0]); + cd->rate = source_get_rate(sources[0]); + + cd->sound_dose_func = sound_dose_find_proc_func(source_format); + if (!cd->sound_dose_func) { + comp_err(dev, "No processing function found for format %d.", + source_format); + return -EINVAL; + } + + return sound_dose_filters_init(mod); +} + +/** + * sound_dose_reset() - Reset the component. + * @mod: Pointer to module data. + * + * The component reset is called when pipeline is stopped. The reset + * should return the component to same state as init. + * + * Return: Value zero, always success. + */ +static int sound_dose_reset(struct processing_module *mod) +{ + struct sound_dose_comp_data *cd = module_get_private_data(mod); + + comp_dbg(mod->dev, "sound_dose_reset()"); + + sound_dose_setup_init(cd); + return 0; +} + +/** + * sound_dose_free() - Free dynamic allocations. + * @mod: Pointer to module data. + * + * Component free is called when the pipelines are deleted. All + * dynamic allocations need to be freed here. The macro __cold + * instructs the build to locate this performance wise non-critical + * function to large and slower DRAM. + * + * Return: Value zero, always success. + */ +__cold static int sound_dose_free(struct processing_module *mod) +{ + struct sound_dose_comp_data *cd = module_get_private_data(mod); + + assert_can_be_cold(); + comp_dbg(mod->dev, "sound_dose_free()"); + + sound_dose_filters_free(cd); + if (cd->msg) { + rfree(cd->msg->tx_data); + rfree(cd->msg); + } + rfree(cd->abi); + rfree(cd); + return 0; +} + +/* This defines the module operations */ +static const struct module_interface sound_dose_interface = { + .init = sound_dose_init, + .prepare = sound_dose_prepare, + .process = sound_dose_process, + .set_configuration = sound_dose_set_config, + .get_configuration = sound_dose_get_config, + .reset = sound_dose_reset, + .free = sound_dose_free +}; + +/* This controls build of the module. If COMP_MODULE is selected in kconfig + * this is build as dynamically loadable module. + */ +#if CONFIG_COMP_SOUND_DOSE_MODULE + +#include +#include +#include + +static const struct sof_man_module_manifest mod_manifest __section(".module") __used = + SOF_LLEXT_MODULE_MANIFEST("SNDDOSE", &sound_dose_interface, 1, + SOF_REG_UUID(sound_dose), 4); + +SOF_LLEXT_BUILDINFO; + +#else + +DECLARE_MODULE_ADAPTER(sound_dose_interface, sound_dose_uuid, sound_dose_tr); +SOF_MODULE_INIT(sound_dose, sys_comp_module_sound_dose_interface_init); + +#endif diff --git a/src/audio/sound_dose/sound_dose.h b/src/audio/sound_dose/sound_dose.h new file mode 100644 index 000000000000..0366d1dc9b35 --- /dev/null +++ b/src/audio/sound_dose/sound_dose.h @@ -0,0 +1,158 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2025 Intel Corporation. + * + */ +#ifndef __SOF_AUDIO_SOUND_DOSE_H__ +#define __SOF_AUDIO_SOUND_DOSE_H__ + +#include +#include +#include +#include +#include + +#define SOUND_DOSE_DEBUG 0 + +#define SOUND_DOSE_1M_OVER_44K_Q26 1521742948 /* int32(1000/44.1 * 2^26) */ +#define SOUND_DOSE_1M_OVER_48K_Q26 1398101333 /* int32(1000/48 * 2^26) */ +#define SOUND_DOSE_ONE_OVER_100_Q24 167772 /* int32(0.01*2^24) */ +#define SOUND_DOSE_GAIN_ONE_Q30 1073741824 /* int32(2^30) */ +#define SOUND_DOSE_GAIN_UP_Q30 1079940603 /* int32(10^(+0.05/20)*2^30) */ +#define SOUND_DOSE_GAIN_DOWN_Q30 1067578625 /* int32(10^(-0.05/20)*2^30) */ +#define SOUND_DOSE_LOG2_INV_44K_Q16 -1011122 /* int32(log2(1/44.1e3) * 2^16) */ +#define SOUND_DOSE_LOG2_INV_48K_Q16 -1019134 /* int32(log2(1/48e3) * 2^16) */ +#define SOUND_DOSE_TEN_OVER_LOG2_10_Q29 1616142483 /* int32(10 / log2(10) * 2^29) */ +#define SOUND_DOSE_WEIGHT_FILTERS_OFFS_Q16 196608 /* int32(3 * 2^16) */ +#define SOUND_DOSE_DFBS_OFFS_Q16 197263 /* int32(3.01 * 2^16) */ +#define SOUND_DOSE_MEL_CHANNELS_SUM_FIX -98304 /* int32(-1.5 * 2^16) */ +#define SOUND_DOSE_ENERGY_SHIFT 19 /* Scale shift for 1s energy */ +#define SOUND_DOSE_LOG_FIXED_OFFSET (65536 * (SOUND_DOSE_ENERGY_SHIFT - 30)) + +#define SOUND_DOSE_S16_Q 15 /* Q1.15 samples */ +#define SOUND_DOSE_S32_Q 31 /* Q1.31 samples */ +#define SOUND_DOSE_GAIN_Q 30 /* Q2.30 gain */ +#define SOUND_DOSE_LOGOFFS_Q 16 /* see SOUND_DOSE_LOG2_INV_48K_Q16 */ +#define SOUND_DOSE_LOGMULT_Q 29 /* see SOUND_DOSE_TEN_OVER_LOG2_10_Q29 */ + +/** + * struct sound_dose_func - Function call pointer for process function + * @mod: Pointer to module data. + * @source: Source for PCM samples data. + * @sink: Sink for PCM samples data. + * @frames: Number of audio data frames to process. + */ +typedef int (*sound_dose_func)(const struct processing_module *mod, + struct sof_source *source, + struct sof_sink *sink, + uint32_t frames); + +/* Sound Dose component private data */ + +/** + * struct sound_dose_comp_data + * @sound_dose_func: Pointer to used processing function. + * @channels_order[]: Vector with desired sink channels order. + * @source_format: Source samples format. + * @frame_bytes: Number of bytes in an audio frame. + * @channels: Channels count. + * @enable: Control processing on/off, on - reorder channels + */ +struct sound_dose_comp_data { + struct iir_state_df1 iir[PLATFORM_MAX_CHANNELS]; + struct sound_dose_setup_config setup; + struct sound_dose_volume_config vol; + struct sound_dose_gain_config att; + struct sof_abi_hdr *abi; + struct sof_audio_feature *feature; + struct sof_sound_dose *dose; + struct ipc_msg *msg; + sound_dose_func sound_dose_func; + int64_t energy[PLATFORM_MAX_CHANNELS]; + int64_t total_frames_count; + int32_t log_offset_for_mean; + int32_t rate_to_us_coef; + int32_t *delay_lines; + int32_t level_dbfs; + int32_t new_gain; + int32_t gain; + bool gain_update; + int report_count; + int frames_count; + int frame_bytes; + int channels; + int rate; +}; + +/** + * struct sound_dose_proc_fnmap - processing functions for frame formats + * @frame_fmt: Current frame format + * @sound_dose_proc_func: Function pointer for the suitable processing function + */ +struct sound_dose_proc_fnmap { + enum sof_ipc_frame frame_fmt; + sound_dose_func sound_dose_proc_func; +}; + +/** + * sound_dose_find_proc_func() - Find suitable processing function. + * @src_fmt: Enum value for PCM format. + * + * This function finds the suitable processing function to use for + * the used PCM format. If not found, return NULL. + * + * Return: Pointer to processing function for the requested PCM format. + */ +sound_dose_func sound_dose_find_proc_func(enum sof_ipc_frame src_fmt); + +/** + * sound_dose_set_config() - Handle controls set request + * @mod: Pointer to module data. + * @param_id: Id to know control type, used to know ALSA control type. + * @pos: Position of the fragment in the large message. + * @data_offset_size: Size of the whole configuration if it is the first or only + * fragment. Otherwise it is offset of the fragment. + * @fragment: Message payload data. + * @fragment_size: Size of this fragment. + * @response_size: Size of response. + * + * This function handles the real-time controls. The ALSA controls have the + * param_id set to indicate the control type. The control ID, from topology, + * is used to separate the controls instances of same type. In control payload + * the num_elems defines to how many channels the control is applied to. + * + * Return: Zero if success, otherwise error code. + */ +int sound_dose_set_config(struct processing_module *mod, + uint32_t param_id, + enum module_cfg_fragment_position pos, + uint32_t data_offset_size, + const uint8_t *fragment, + size_t fragment_size, + uint8_t *response, + size_t response_size); + +/** + * sound_dose_get_config() - Handle controls get request + * @mod: Pointer to module data. + * @config_id: Configuration ID. + * @data_offset_size: Size of the whole configuration if it is the first or only + * fragment. Otherwise it is offset of the fragment. + * @fragment: Message payload data. + * @fragment_size: Size of this fragment. + * + * This function is used for controls get. + * + * Return: Zero if success, otherwise error code. + */ +int sound_dose_get_config(struct processing_module *mod, + uint32_t config_id, uint32_t *data_offset_size, + uint8_t *fragment, size_t fragment_size); + +void sound_dose_filters_free(struct sound_dose_comp_data *cd); +int sound_dose_filters_init(struct processing_module *mod); +void sound_dose_report_mel(const struct processing_module *mod); +int sound_dose_ipc_notification_init(struct processing_module *mod); +void sound_dose_send_ipc_notification(const struct processing_module *mod); + +#endif /* __SOF_AUDIO_SOUND_DOSE_H__ */ diff --git a/src/audio/sound_dose/sound_dose.toml b/src/audio/sound_dose/sound_dose.toml new file mode 100644 index 000000000000..95f58ae27e05 --- /dev/null +++ b/src/audio/sound_dose/sound_dose.toml @@ -0,0 +1,21 @@ +#ifndef LOAD_TYPE +#define LOAD_TYPE "0" +#endif + + REM # Sound Dose component module config + [[module.entry]] + name = "SNDDOSE" + uuid = UUIDREG_STR_SOUND_DOSE + affinity_mask = "0x1" + instance_count = "40" + domain_types = "0" + load_type = LOAD_TYPE + module_type = "9" + auto_start = "0" + sched_caps = [1, 0x00008000] + REM # pin = [dir, type, sample rate, size, container, channel-cfg] + pin = [0, 0, 0xfeef, 0xf, 0xf, 0x45ff, 1, 0, 0xfeef, 0xf, 0xf, 0x1ff] + REM # mod_cfg [PAR_0 PAR_1 PAR_2 PAR_3 IS_BYTES CPS IBS OBS MOD_FLAGS CPC OBLS] + mod_cfg = [0, 0, 0, 0, 4096, 1000000, 128, 128, 0, 0, 0] + + index = __COUNTER__ diff --git a/src/audio/sound_dose/sound_dose_iir_44k.h b/src/audio/sound_dose/sound_dose_iir_44k.h new file mode 100644 index 000000000000..d6fbf665f4eb --- /dev/null +++ b/src/audio/sound_dose/sound_dose_iir_44k.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2025 Intel Corporation. + */ + +/* Created 2025-08-15 with command: + * cd src/audio/sound_dose/tune + * octave --quiet --no-window-system sof_sound_dose_time_domain_filters.m + */ + +#include + +static const uint32_t sound_dose_iir_44k[51] = { + 0x34464f53, 0x00000000, 0x000000ac, 0x0301d001, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x000000ac, 0x00000002, 0x00000001, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000004, 0x00000004, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0xc04a8747, + 0x7fb56455, 0x2000c3eb, 0xbffe782a, 0x2000c3eb, + 0x00000000, 0x00004000, 0xc5d5ada3, 0x7a174530, + 0x1e9ac221, 0xc2ca7bbd, 0x1e9ac221, 0x00000000, + 0x00004000, 0xc382c10f, 0x7c63e3c7, 0x1e4cf2c0, + 0xc1d9d144, 0x1fe2eeaa, 0x00000000, 0x00004000, + 0xfe0cb792, 0xe92a46fc, 0x02d2f1b4, 0x10d57d25, + 0x18d70df1, 0xfffffffd, 0x000067d8 +}; diff --git a/src/audio/sound_dose/sound_dose_iir_48k.h b/src/audio/sound_dose/sound_dose_iir_48k.h new file mode 100644 index 000000000000..6dca9af5e4b8 --- /dev/null +++ b/src/audio/sound_dose/sound_dose_iir_48k.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2025 Intel Corporation. + */ + +/* Created 2025-08-15 with command: + * cd src/audio/sound_dose/tune + * octave --quiet --no-window-system sof_sound_dose_time_domain_filters.m + */ + +#include + +static const uint32_t sound_dose_iir_48k[51] = { + 0x34464f53, 0x00000000, 0x000000ac, 0x0301d001, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x000000ac, 0x00000002, 0x00000001, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000004, 0x00000004, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0xc0447c54, + 0x7fbb7275, 0x200247f9, 0xbffb700e, 0x200247f9, + 0x00000000, 0x00004000, 0xc56175ce, 0x7a8e6600, + 0x1eb83f18, 0xc28f81d0, 0x1eb83f18, 0x00000000, + 0x00004000, 0xc33b9ba4, 0x7caef0cf, 0x1e6ee6bd, + 0xc1b48ddf, 0x1fe4bfc4, 0x00000000, 0x00004000, + 0xff729f84, 0xf3612432, 0x01acb380, 0x0cd1120d, + 0x182fcd24, 0xfffffffd, 0x000067e0 +}; diff --git a/src/audio/sound_dose/tune/sof_sound_dose_blobs.m b/src/audio/sound_dose/tune/sof_sound_dose_blobs.m new file mode 100644 index 000000000000..7670303c2770 --- /dev/null +++ b/src/audio/sound_dose/tune/sof_sound_dose_blobs.m @@ -0,0 +1,95 @@ +% Export configuration blobs for Sound Dose + +% SPDX-License-Identifier: BSD-3-Clause +% +% Copyright (c) 2025, Intel Corporation. + +function sof_sound_dose_blobs() + + % Common definitions + sof_tools = '../../../../tools'; + sof_tplg = fullfile(sof_tools, 'topology'); + fn.tpath = fullfile(sof_tplg, 'topology2/include/components/sound_dose'); + str_comp = "sound_dose_config"; + str_comment = "Exported with script sof_sound_dose_blobs.m"; + str_howto = "cd src/audio/sound_dose/tune; octave sof_sound_dose_blobs.m"; + bytes_control_param_id = 202; + + % Set the parameters here + sof_tools = '../../../../tools'; + sof_tplg = fullfile(sof_tools, 'topology'); + sof_tplg_sound_dose = fullfile(sof_tplg, 'topology2/include/components/sound_dose'); + sof_ctl_sound_dose = fullfile(sof_tools, 'ctl/ipc4/sound_dose'); + + sof_sound_dose_paths(true); + + blob8 = sof_sound_dose_build_blob([10000 0], 0); + tplg2_fn = sprintf("%s/setup_sens_100db.conf", sof_tplg_sound_dose); + sof_tplg2_write(tplg2_fn, blob8, str_comp, str_comment, str_howto); + sof_alsactl_write([sof_ctl_sound_dose "/setup_sens_100db.txt"], blob8); + + blob8 = sof_sound_dose_build_blob([0 0], 0); + tplg2_fn = sprintf("%s/setup_sens_0db.conf", sof_tplg_sound_dose); + sof_tplg2_write(tplg2_fn, blob8, str_comp, str_comment, str_howto); + sof_alsactl_write([sof_ctl_sound_dose "/setup_sens_0db.txt"], blob8); + + blob8 = sof_sound_dose_build_blob([0 0], 1); + tplg2_fn = sprintf("%s/setup_vol_0db.conf", sof_tplg_sound_dose); + sof_tplg2_write(tplg2_fn, blob8, str_comp, str_comment, str_howto); + sof_alsactl_write([sof_ctl_sound_dose "/setup_vol_0db.txt"], blob8); + + blob8 = sof_sound_dose_build_blob([-1000 0], 1); + tplg2_fn = sprintf("%s/setup_vol_-10db.conf", sof_tplg_sound_dose); + sof_tplg2_write(tplg2_fn, blob8, str_comp, str_comment, str_howto); + sof_alsactl_write([sof_ctl_sound_dose "/setup_vol_-10db.txt"], blob8); + + blob8 = sof_sound_dose_build_blob([-1000 0], 2); + tplg2_fn = sprintf("%s/setup_gain_-10db.conf", sof_tplg_sound_dose); + sof_tplg2_write(tplg2_fn, blob8, str_comp, str_comment, str_howto); + sof_alsactl_write([sof_ctl_sound_dose "/setup_gain_-10db.txt"], blob8); + + blob8 = sof_sound_dose_build_blob([0 0], 2); + tplg2_fn = sprintf("%s/setup_gain_0db.conf", sof_tplg_sound_dose); + sof_tplg2_write(tplg2_fn, blob8, str_comp, str_comment, str_howto); + sof_alsactl_write([sof_ctl_sound_dose "/setup_gain_0db.txt"], blob8); + + blob8 = sof_sound_dose_build_blob([], bytes_control_param_id); + tplg2_fn = sprintf("%s/setup_data_init.conf", sof_tplg_sound_dose); + sof_tplg2_write(tplg2_fn, blob8, str_comp, str_comment, str_howto); + + sof_sound_dose_paths(false); +end + +function sof_sound_dose_paths(enable) + + common = '../../../../tools/tune/common'; + if enable + addpath(common); + else + rmpath(common); + end +end + +function blob8 = sof_sound_dose_build_blob(param_values, blob_param_id) + + blob_type = 0; + data_length = length(param_values); + data_size = 2 * data_length; + ipc_ver = 4; + [abi_bytes, abi_size] = sof_get_abi(data_size, ipc_ver, blob_type, blob_param_id); + blob_size = data_size + abi_size; + blob8 = uint8(zeros(1, blob_size)); + blob8(1:abi_size) = abi_bytes; + j = abi_size + 1; + for i = 1:data_length + blob8(j:j+1) = short2byte(int16(param_values(i))); + j=j+2; + end +end + +function bytes = short2byte(word) + sh = [0 -8]; + bytes = uint8(zeros(1,2)); + bytes(1) = bitand(bitshift(word, sh(1)), 255); + bytes(2) = bitand(bitshift(word, sh(2)), 255); +end diff --git a/src/audio/sound_dose/tune/sof_sound_dose_ref.m b/src/audio/sound_dose/tune/sof_sound_dose_ref.m new file mode 100644 index 000000000000..dc15ab9f3567 --- /dev/null +++ b/src/audio/sound_dose/tune/sof_sound_dose_ref.m @@ -0,0 +1,69 @@ +% Compute MEL every 1s +tc = 1.0; + +% Load sound clip +sound_clip = "/usr/share/sounds/alsa/Front_Right.wav"; +[x1, fs] = audioread(sound_clip); +if size(x1, 2) == 1 + x1 = repmat(x1, 1, 2); +end +sx = size(x1); +num_frames = sx(1); +num_channels = sx(2); + +% load A-weight filters +load sof_sound_dose_time_domain_filters.mat + +% Filter with IIR, FIR +x2 = filter(b_iir, a_iir, x1); +x3 = filter(b_fir, 1, x2); + +%figure +%plot(x1) + +%figure +%plot(x3) + +% compute RMS level in 1s chunks +np = 1; +nc = tc * fs; +num_chunks = floor(num_frames/nc); +l_dbfs_all = zeros(num_chunks, num_channels); +mel_all = zeros(num_chunks, 1); + +dbfs_offs = 3.01; +dbfs_offs_weight_filters = 3; +i1 = 1; +for n = 1:num_chunks + sum = 0; + i2 = i1 + nc - 1; + for ch = 1:num_channels + y = x3(i1:i2, ch); + sum = sum + mean(y.^2); + l_dbfs = 20*log10(sqrt(mean(y.^2))) + dbfs_offs + dbfs_offs_weight_filters; + l_dbfs_all(np, ch) = l_dbfs; + end + mel = 10*log10(sum) + dbfs_offs_weight_filters; + if num_channels > 1 + mel = mel - 1.5 * num_channels; + end + mel_all(np) = mel; + i1 = i1 + nc; + np = np + 1; +end + +figure(1) +plot(l_dbfs_all) +grid on +xlabel('Time (s)'); +ylabel('Level (dBSFS)'); + +figure(2) +plot(mel_all) +grid on +xlabel('Time (s)'); +ylabel('MEL (dB)'); + +mel_all + +mel_all_rnd = round(mel_all) diff --git a/src/audio/sound_dose/tune/sof_sound_dose_time_domain_filters.m b/src/audio/sound_dose/tune/sof_sound_dose_time_domain_filters.m new file mode 100644 index 000000000000..26e2b2e00fa5 --- /dev/null +++ b/src/audio/sound_dose/tune/sof_sound_dose_time_domain_filters.m @@ -0,0 +1,163 @@ +% Export time domain IIR and FIR filter to apply A-weight for sound dose. +% +% Usage: +% sof_sound_dose_time_domain_filters() + +% SPDX-License-Identifier: BSD-3-Clause +% +% Copyright (c) 2025, Intel Corporation. + +function sof_sound_dose_time_domain_filters(fs) + + if exist('OCTAVE_VERSION', 'builtin') + pkg load signal; + end + + sof_sound_dose_time_domain_filters_for_rate(48e3); + sof_sound_dose_time_domain_filters_for_rate(44.1e3); + +end + +function sof_sound_dose_time_domain_filters_for_rate(fs) + + sof_sound_dose = '../../sound_dose'; + sof_ctl = '../../../../tools/ctl'; + sound_dose_paths(true); + + + fs_str = sprintf('%dk', round(fs/1000)); + fir_str = sprintf('sound_dose_fir_%s', fs_str); + iir_str = sprintf('sound_dose_iir_%s', fs_str); + prm.fir_coef_fn = fullfile(sof_sound_dose, [fir_str '.h']); + prm.iir_coef_fn = fullfile(sof_sound_dose, [iir_str '.h']); + prm.fir_blob_fn = fullfile(sof_ctl, ['ipc4/eq_fir/' fir_str '.txt']); + prm.iir_blob_fn = fullfile(sof_ctl, ['ipc4/eq_iir/' iir_str '.txt']); + prm.fir_blob_vn = fir_str; + prm.iir_blob_vn = iir_str; + prm.ipc_ver = 4; + eq = sound_dose_filters(fs); + export_filters(eq, prm); + + sound_dose_paths(false); +end + +function export_filters(eq, prm) + + b_fir = 1; + b_iir = 1; + a_iir = 1; + howto = 'cd src/audio/sound_dose/tune; octave --quiet --no-window-system sof_sound_dose_time_domain_filters.m'; + + % Export FIR + if eq.enable_fir + bq = sof_eq_fir_blob_quant(eq.b_fir); + channels_in_config = 2; % Setup max 2 channels EQ + assign_response = [0 0]; % Same response for L and R + num_responses = 1; % One response + bm = sof_eq_fir_blob_merge(channels_in_config, ... + num_responses, ... + assign_response, ... + bq); + bp = sof_eq_fir_blob_pack(bm, prm.ipc_ver); + sof_export_c_eq_uint32t(prm.fir_coef_fn, bp, prm.fir_blob_vn, 0, howto); + sof_alsactl_write(prm.fir_blob_fn, bp); + b_fir = eq.b_fir; + end + + % Export IIR + if eq.enable_iir + coefq = sof_eq_iir_blob_quant(eq.p_z, eq.p_p, eq.p_k); + channels_in_config = 2; % Setup max 2 channels EQ + assign_response = [0 0]; % Same response for L and R + num_responses = 1; % One response + coefm = sof_eq_iir_blob_merge(channels_in_config, ... + num_responses, ... + assign_response, ... + coefq); + coefp = sof_eq_iir_blob_pack(coefm, prm.ipc_ver); + sof_export_c_eq_uint32t(prm.iir_coef_fn, coefp, prm.iir_blob_vn, 0, howto); + sof_alsactl_write(prm.iir_blob_fn, coefp); + [b_iir, a_iir] = zp2tf( eq.p_z, eq.p_p, eq.p_k); + end + + % Export mat file + save sof_sound_dose_time_domain_filters.mat b_fir b_iir a_iir; + +end + +function eq = sound_dose_filters(fs) + + np = 200; + f_hi = 0.95 * fs/2; + fv = logspace(log10(10), log10(f_hi), np); + ref_a_weight = func_a_weight_db(fv); + + eq = sof_eq_defaults(); + eq.fs = fs; + eq.enable_fir = 0; + eq.enable_iir = 1; + eq.iir_norm_type = '1k'; % At 1 kHz -3 dB + eq.iir_norm_offs_db = -3; + eq.fir_norm_type = '1k'; % At 1 kHz 0 dB, total -3 dB gain to avoid clip + eq.fir_norm_offs_db = 0; + eq.p_fmin = 10; + eq.p_fmax = 30e3; + + eq.fir_minph = 1; + eq.fir_beta = 4; + eq.fir_length = 31; + eq.fir_autoband = 0; + eq.fmin_fir = 300; + eq.fmax_fir = f_hi; + + eq.raw_f = fv; + eq.raw_m_db = zeros(1, length(fv)); + eq.target_f = fv; + eq.target_m_db = ref_a_weight; + + eq.peq = [ ... + eq.PEQ_HP1 12 0 0; ... + eq.PEQ_HP1 20 0 0; ... + eq.PEQ_HP2 280 0 0; ... + eq.PEQ_PN2 245 -5.45 0.5; ... + eq.PEQ_HS1 13000 -3 0; ... + eq.PEQ_HS1 14000 -3 0; ... + ]; + + eq = sof_eq_compute(eq); + sof_eq_plot(eq, 1); + figure(3); + axis([10 20e3 -60 10]); + + +end + +% See https://en.wikipedia.org/wiki/A-weighting +% IEC 61672-1:2013 Electroacoustics - Sound level meters, +% Part 1: Specifications. IEC. 2013. + +function a_weight = func_a_weight_db(fv) + a_weight = 20*log10(func_ra(fv)) - 20*log10(func_ra(1000)); +end + +function y = func_ra(f) + f2 = f.^2; + f4 = f.^4; + c1 = 12194^2; + c2 = 20.6^2; + c3 = 107.7^2; + c4 = 737.9^2; + c5 = 12194^2; + y = c1 * f4 ./ ((f2 + c2).*sqrt((f2 + c3).*(f2 + c4).*(f2 + c5))); +end + +function sound_dose_paths(enable) + switch enable + case true + addpath('../../eq_iir/tune'); + addpath('../../../../tools/tune/common'); + case false + rmpath('../../eq_iir/tune'); + rmpath('../../../../tools/tune/common'); + end +end diff --git a/src/include/sof/audio/component.h b/src/include/sof/audio/component.h index 0091419cc442..b76b31204816 100644 --- a/src/include/sof/audio/component.h +++ b/src/include/sof/audio/component.h @@ -899,6 +899,7 @@ void sys_comp_module_mux_interface_init(void); void sys_comp_module_asrc_interface_init(void); void sys_comp_module_rtnr_interface_init(void); void sys_comp_module_selector_interface_init(void); +void sys_comp_module_sound_dose_interface_init(void); void sys_comp_module_src_interface_init(void); void sys_comp_module_src_lite_interface_init(void); void sys_comp_module_tdfb_interface_init(void); diff --git a/src/include/user/audio_feature.h b/src/include/user/audio_feature.h new file mode 100644 index 000000000000..8e7791c0f3db --- /dev/null +++ b/src/include/user/audio_feature.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2025 Intel Corporation. + * + */ + +#ifndef __USER_AUDIO_FEATURE_H__ +#define __USER_AUDIO_FEATURE_H__ + +/** \brief Audio feature data types. */ +enum sof_audio_feature_type { + SOF_AUDIO_FEATURE_MFCC, /**< For Mel Frequency Cepstral Coefficients */ + SOF_AUDIO_FEATURE_SOUND_DOSE_MEL, /**< For Sound Dose MEL (loudness) values */ +}; + +/** \brief Header for audio features data. */ +struct sof_audio_feature { + uint64_t stream_time_us; /**< Timestamp, relative time in microseconds */ + enum sof_audio_feature_type type; /**< Type of audio feature, as above*/ + uint32_t num_audio_features; /**< Number of audio feature structs in data */ + size_t data_size; /**< Size of data without this header */ + uint32_t reserved[4]; /**< Reserved for future use */ + int32_t data[]; /**< Start of data */ +} __attribute__((packed)); + +#endif /* __USER_AUDIO_FEATURE_H__ */ diff --git a/src/include/user/sound_dose.h b/src/include/user/sound_dose.h new file mode 100644 index 000000000000..a2479eb79951 --- /dev/null +++ b/src/include/user/sound_dose.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2025 Intel Corporation. + * + */ + +#include +#include +#include + +#ifndef __USER_SOUND_DOSE_H__ +#define __USER_SOUND_DOSE_H__ + +#define SOF_SOUND_DOSE_SETUP_PARAM_ID 0 +#define SOF_SOUND_DOSE_VOLUME_PARAM_ID 1 +#define SOF_SOUND_DOSE_GAIN_PARAM_ID 2 +#define SOF_SOUND_DOSE_PAYLOAD_PARAM_ID 3 + +#define SOF_SOUND_DOSE_SENS_MIN_DB (-10 * 100) /* -10 to +130 dB */ +#define SOF_SOUND_DOSE_SENS_MAX_DB (130 * 100) +#define SOF_SOUND_DOSE_VOLUME_MIN_DB (-100 * 100) /* -100 to +40 dB */ +#define SOF_SOUND_DOSE_VOLUME_MAX_DB (40 * 100) +#define SOF_SOUND_DOSE_GAIN_MIN_DB (-100 * 100) /* -100 to 0 dB */ +#define SOF_SOUND_DOSE_GAIN_MAX_DB (0 * 100) + +struct sof_sound_dose { + int16_t mel_value; /* Decibels x100, e.g. 85 dB is 8500 */ + int16_t dbfs_value; /* Decibels x100 */ + int16_t current_sens_dbfs_dbspl; /* Decibels x100 */ + int16_t current_volume_offset; /* Decibels x100 */ + int16_t current_gain; /* Decibels x100 */ + uint16_t reserved16; /**< reserved for future use */ + uint32_t reserved32[4]; /**< reserved for future use */ +} __attribute__((packed)); + +struct sound_dose_setup_config { + int16_t sens_dbfs_dbspl; /* Decibels x100 */ + int16_t reserved; +} __attribute__((packed)); + +struct sound_dose_volume_config { + int16_t volume_offset; /* Decibels x100 */ + int16_t reserved; +} __attribute__((packed)); + +struct sound_dose_gain_config { + int16_t gain; /* Decibels x100 */ + int16_t reserved; +} __attribute__((packed)); + +#endif /* __USER_SOUND_DOSE_H__ */ diff --git a/tools/rimage/config/lnl.toml.h b/tools/rimage/config/lnl.toml.h index 6d1e0f0ef04a..3b72dc88ab28 100644 --- a/tools/rimage/config/lnl.toml.h +++ b/tools/rimage/config/lnl.toml.h @@ -138,5 +138,9 @@ #include