From 4f31ce1847fad7823faa5b1ee1b813212f965f14 Mon Sep 17 00:00:00 2001 From: Seppo Ingalsuo Date: Fri, 10 Jul 2026 13:40:57 +0300 Subject: [PATCH] audio: eq_iir: tune: accept multiple measurement files in sof_ucm2_eq_generate Allow the meas_file argument to be a cell array of paths in addition to a single string. Each file is still loaded and column-averaged the same way it was before; the per-file traces are then averaged together into the single response the IIR + FIR fit consumes. All files must share the exact same frequency grid. A mismatch aborts before any fit is attempted with an error that names both the offending file and the reference (first) file, so it is obvious which measurement was captured on a different grid. The main use case is a series of measurements taken with the microphone at different positions in front of the speaker, so the tuning target is the spatial average of those positions instead of the exact frequency response at one arbitrary spot. Signed-off-by: Seppo Ingalsuo --- src/audio/eq_iir/tune/sof_ucm2_eq_generate.m | 103 ++++++++++++++----- 1 file changed, 79 insertions(+), 24 deletions(-) diff --git a/src/audio/eq_iir/tune/sof_ucm2_eq_generate.m b/src/audio/eq_iir/tune/sof_ucm2_eq_generate.m index 131ca99ed99e..2eb2fc34f291 100644 --- a/src/audio/eq_iir/tune/sof_ucm2_eq_generate.m +++ b/src/audio/eq_iir/tune/sof_ucm2_eq_generate.m @@ -25,6 +25,13 @@ function sof_ucm2_eq_generate(sys_vendor, product_name, endpoint, meas_file) % layout with no header row, matching % sof_ucm2_eq_example.xlsx. % +% A cell array of paths is also accepted, e.g. +% { 'mls-a.txt', 'mls-b.txt', 'mls-c.txt' }. Each file is +% loaded independently (per-file column averaging still +% applies) and the per-file magnitudes are then averaged +% together. All files must share the same frequency +% grid; a mismatch raises an error. +% % The binary IIR and FIR blobs consumed by UCM are written to % ./ucm2_blobs_sof/ipc4/eq_iir/___iir.bin % ./ucm2_blobs_sof/ipc4/eq_fir/___fir.bin @@ -149,31 +156,79 @@ function sof_ucm2_eq_generate(sys_vendor, product_name, endpoint, meas_file) %% Measurement loading %% ----------------------------------------------------------------------- function eq = load_measurement(eq, meas_file) -if ~exist(meas_file, 'file') - error('Measurement file not found: %s', meas_file); -end -[~, ~, ext] = fileparts(meas_file); -switch lower(ext) -case {'.xls', '.xlsx', '.xlsm', '.ods'} - pkg load io; - meas = xlsread(meas_file); -otherwise - %% dlmread with no explicit delimiter auto-detects whitespace or - %% comma separation, which covers both the sof_mls_freq_resp.m - %% output and legacy whitespace-delimited measurement files. - meas = dlmread(meas_file); -end -if size(meas, 2) < 2 - error('%s must have at least a frequency column and one response column', ... - meas_file); -end -eq.raw_f = meas(:,1); -if size(meas, 2) == 2 - eq.raw_m_db = meas(:,2); +%% meas_file is either a single path or a cell array of paths. Each file +%% is loaded and its response columns (if more than one) are averaged +%% into a single trace, then the per-file traces are averaged together. +%% All files must share the exact same frequency grid; any mismatch +%% aborts before any fit is attempted. +if iscell(meas_file) + files = meas_file; +elseif ischar(meas_file) + files = { meas_file }; +else + error(['meas_file must be a char path or a cell array of ' ... + 'char paths (got %s)'], class(meas_file)); +end + +if isempty(files) + error('meas_file must not be an empty cell array'); +end +for k = 1:numel(files) + if ~ischar(files{k}) || isempty(files{k}) + error(['meas_file{%d} must be a non-empty char path ' ... + '(got %s)'], k, class(files{k})); + end +end + +f_ref = []; +m_stack = []; +for k = 1:numel(files) + fn = files{k}; + if ~exist(fn, 'file') + error('Measurement file not found: %s', fn); + end + [~, ~, ext] = fileparts(fn); + switch lower(ext) + case {'.xls', '.xlsx', '.xlsm', '.ods'} + pkg load io; + meas = xlsread(fn); + otherwise + %% dlmread with no explicit delimiter auto-detects whitespace or + %% comma separation, which covers both the sof_mls_freq_resp.m + %% output and legacy whitespace-delimited measurement files. + meas = dlmread(fn); + end + if size(meas, 2) < 2 + error('%s must have at least a frequency column and one response column', ... + fn); + end + f_k = meas(:, 1); + if size(meas, 2) == 2 + m_k = meas(:, 2); + else + m_k = mean(meas(:, 2:end), 2); + fprintf('Averaged %d response columns from %s\n', ... + size(meas, 2) - 1, fn); + end + if isempty(f_ref) + f_ref = f_k; + m_stack = m_k; + else + if ~isequal(size(f_k), size(f_ref)) || any(f_k(:) ~= f_ref(:)) + error(['Frequency grid of %s does not match %s. ' ... + 'All measurement files must share the same ' ... + '[freq, resp...] grid.'], fn, files{1}); + end + m_stack = [m_stack, m_k]; + end +end + +eq.raw_f = f_ref; +if size(m_stack, 2) == 1 + eq.raw_m_db = m_stack; else - %% Average of the response columns - eq.raw_m_db = mean(meas(:, 2:end), 2); - fprintf('Averaged %d response columns from %s\n', size(meas, 2) - 1, meas_file); + eq.raw_m_db = mean(m_stack, 2); + fprintf('Averaged %d measurement files\n', size(m_stack, 2)); end end