|
| 1 | +function sof_ucm2_eq_generate(sys_vendor, product_name, endpoint, meas_file) |
| 2 | + |
| 3 | +% SOF_UCM2_EQ_GENERATE Fit IIR + FIR endpoint EQ from a measurement. |
| 4 | +% |
| 5 | +% sof_ucm2_eq_generate(SYS_VENDOR, PRODUCT_NAME, ENDPOINT, MEAS_FILE) |
| 6 | +% |
| 7 | +% SYS_VENDOR DMI /sys/devices/virtual/dmi/id/sys_vendor value, e.g. |
| 8 | +% 'Acme Ltd.'. Kept verbatim (with spaces and case) for the |
| 9 | +% product_configs directory name. |
| 10 | +% PRODUCT_NAME DMI product_name value, e.g. 'Model 100'. Kept |
| 11 | +% verbatim for the .conf file name. |
| 12 | +% ENDPOINT Endpoint being tuned, e.g. 'speaker' or 'headphone'. Used |
| 13 | +% lower case in blob file names and capitalized in the |
| 14 | +% Define.PostMixer<Endpoint>Playback... UCM keys. |
| 15 | +% MEAS_FILE Path to a comma separated numbers text file whose first |
| 16 | +% column is the measurement frequency in Hz and whose |
| 17 | +% remaining columns are one or more measured magnitude |
| 18 | +% traces in dB. Multiple traces are averaged. |
| 19 | +% Such a file can be produced with sof_mls_freq_resp.m. |
| 20 | +% Excel workbooks (.xls, .xlsx, .xlsm) and OpenDocument |
| 21 | +% spreadsheets (.ods) are also accepted; the format is |
| 22 | +% detected from the file extension and imported via |
| 23 | +% xlsread from the Octave 'io' package. The workbook |
| 24 | +% is expected to contain the same [freq, resp...] numeric |
| 25 | +% layout with no header row, matching |
| 26 | +% sof_ucm2_eq_example.xlsx. |
| 27 | +% |
| 28 | +% The IIR and FIR blobs are written to |
| 29 | +% ./ucm2_blobs_sof/ipc4/eq_iir/<endpoint>_<vendor>_<product>_iir.{txt,bin} |
| 30 | +% ./ucm2_blobs_sof/ipc4/eq_fir/<endpoint>_<vendor>_<product>_fir.{txt,bin} |
| 31 | +% |
| 32 | +% and a UCM include file that will be picked up automatically by UCM is |
| 33 | +% written to |
| 34 | +% ./ucm2_blobs_sof/product_configs/<SYS_VENDOR>/<PRODUCT_NAME>.conf |
| 35 | +% |
| 36 | +% Run this to see a design example and produced configuration for ALSA UCMv2 |
| 37 | +% sof_ucm2_eq_generate('example', 'example', 'speaker', 'sof_ucm2_eq_example.txt'); |
| 38 | +% or |
| 39 | +% sof_ucm2_eq_generate('example', 'example', 'speaker', 'sof_ucm2_eq_example.xlsx'); |
| 40 | + |
| 41 | +% SPDX-License-Identifier: BSD-3-Clause |
| 42 | +% |
| 43 | +% Copyright (c) 2026, Intel Corporation. |
| 44 | + |
| 45 | +if nargin < 4 |
| 46 | + help sof_ucm2_eq_generate |
| 47 | + printf('\n'); |
| 48 | + error('Usage: sof_ucm2_eq_generate(sys_vendor, product_name, endpoint, meas_file)'); |
| 49 | +end |
| 50 | + |
| 51 | +%% Load the signal package up front so the script fails cleanly on a |
| 52 | +%% system missing it, before any output directories are created. The io |
| 53 | +%% package is only pulled in when the measurement file is a spreadsheet |
| 54 | +%% (see load_measurement below), so text-only users do not need it. |
| 55 | +pkg load signal; |
| 56 | + |
| 57 | +%% Derive blob base name and output paths from the DMI info + endpoint. |
| 58 | +endpoint_lc = lower(endpoint); |
| 59 | +base = sprintf('%s_%s_%s', endpoint_lc, sanitize_name(sys_vendor), ... |
| 60 | + sanitize_name(product_name)); |
| 61 | +out_root = 'ucm2_blobs_sof'; |
| 62 | +cpath4 = fullfile(out_root, 'ipc4'); |
| 63 | +iir_txt = fullfile('eq_iir', [base '_iir.txt']); |
| 64 | +iir_bin = fullfile('eq_iir', [base '_iir.bin']); |
| 65 | +fir_txt = fullfile('eq_fir', [base '_fir.txt']); |
| 66 | +fir_bin = fullfile('eq_fir', [base '_fir.bin']); |
| 67 | +conf_dir = fullfile(out_root, 'product_configs', sys_vendor); |
| 68 | +conf_file = fullfile(conf_dir, [product_name '.conf']); |
| 69 | +ensure_dir(fullfile(cpath4, 'eq_iir')); |
| 70 | +ensure_dir(fullfile(cpath4, 'eq_fir')); |
| 71 | +ensure_dir(conf_dir); |
| 72 | + |
| 73 | +sof_eq_paths(true); |
| 74 | + |
| 75 | +%% Base equalizer setup |
| 76 | +eq = sof_eq_defaults(); |
| 77 | +eq.fs = 48e3; |
| 78 | +eq.enable_iir = 1; |
| 79 | +eq.enable_fir = 1; |
| 80 | +eq.iir_norm_type = 'loudness'; |
| 81 | +eq.iir_norm_offs_db = -1; |
| 82 | +eq.fir_norm_type = 'loudness'; |
| 83 | +eq.fir_norm_offs_db = -1; |
| 84 | +eq.p_fmin = 20; |
| 85 | +eq.p_fmax = 20e3; |
| 86 | + |
| 87 | +%% Load measurement, fit IIR + FIR, compute the final response and export |
| 88 | +eq = load_measurement(eq, meas_file); |
| 89 | +eq = design_iir_stages(eq); |
| 90 | +eq = configure_fir(eq); |
| 91 | +eq = sof_eq_compute(eq); |
| 92 | +sof_eq_plot(eq, 1); |
| 93 | +export_blobs(eq, cpath4, iir_txt, iir_bin, fir_txt, fir_bin); |
| 94 | +write_product_conf(conf_file, sys_vendor, product_name, endpoint, base, eq); |
| 95 | + |
| 96 | +sof_eq_paths(false); |
| 97 | + |
| 98 | +end |
| 99 | + |
| 100 | +%% ----------------------------------------------------------------------- |
| 101 | +%% Measurement loading |
| 102 | +%% ----------------------------------------------------------------------- |
| 103 | +function eq = load_measurement(eq, meas_file) |
| 104 | +if ~exist(meas_file, 'file') |
| 105 | + error('Measurement file not found: %s', meas_file); |
| 106 | +end |
| 107 | +[~, ~, ext] = fileparts(meas_file); |
| 108 | +switch lower(ext) |
| 109 | +case {'.xls', '.xlsx', '.xlsm', '.ods'} |
| 110 | + pkg load io; |
| 111 | + meas = xlsread(meas_file); |
| 112 | +otherwise |
| 113 | + %% dlmread with no explicit delimiter auto-detects whitespace or |
| 114 | + %% comma separation, which covers both the sof_mls_freq_resp.m |
| 115 | + %% output and legacy whitespace-delimited measurement files. |
| 116 | + meas = dlmread(meas_file); |
| 117 | +end |
| 118 | +if size(meas, 2) < 2 |
| 119 | + error('%s must have at least a frequency column and one response column', ... |
| 120 | + meas_file); |
| 121 | +end |
| 122 | +eq.raw_f = meas(:,1); |
| 123 | +if size(meas, 2) == 2 |
| 124 | + eq.raw_m_db = meas(:,2); |
| 125 | +else |
| 126 | + %% Average of the response columns |
| 127 | + eq.raw_m_db = mean(meas(:, 2:end), 2); |
| 128 | + fprintf('Averaged %d response columns from %s\n', size(meas, 2) - 1, meas_file); |
| 129 | +end |
| 130 | +end |
| 131 | + |
| 132 | +%% ----------------------------------------------------------------------- |
| 133 | +%% Multi-stage IIR fit |
| 134 | +%% ----------------------------------------------------------------------- |
| 135 | +function eq = design_iir_stages(eq) |
| 136 | + |
| 137 | +%% Cap on the combined response shaping between the mid-band low-Q PN2 |
| 138 | +% (stage 1) and the bass LS2 (stage 2). If stage 1 pulls the mids down by |
| 139 | +% A dB, the LS2 upper bound is reduced so that bass_boost + |mid_atten| |
| 140 | +% does not exceed this limit. This keeps the bass-to-mid tilt from |
| 141 | +% becoming excessive (e.g. 26 dB total is already an aggressive shape). |
| 142 | +max_bass_plus_mid_atten_db = 26; |
| 143 | + |
| 144 | +%% Filter budget (5 biquads max): |
| 145 | +% 1) HP2 at 80 Hz to protect the speaker |
| 146 | +% 2) PN2 very-low-Q mid shaper -> fc [2000, 5000], gain [-20, +6], Q [0.1, 0.5] |
| 147 | +% 3) LS2 bass boost -> fc [120, 1000], gain [0, +12] |
| 148 | +% 4) PN2 fine correction (low band) -> fc [200, 4000], gain [-6, +12], Q [0.1, 1.0] |
| 149 | +% 5) PN2 fine correction (high band) -> fc [3000, 12000],gain [-6, +12], Q [0.1, 1.0] |
| 150 | +% Fitting order matters: the very-low-Q peak/notch is fit first so the |
| 151 | +% subsequent low shelf can settle on top of an already-flattened midrange |
| 152 | +% instead of chasing a mid bump with bass gain. Biquads 4 and 5 have |
| 153 | +% disjoint fc ranges (low-mid vs. high-mid) so they converge on |
| 154 | +% complementary biquads by construction rather than relying on the initial |
| 155 | +% seed alone. |
| 156 | +hp_fc = 80; |
| 157 | + |
| 158 | +opts = optimset('Display', 'notify', 'MaxIter', 300, 'MaxFunEvals', 2000, ... |
| 159 | + 'TolX', 1e-3, 'TolFun', 1e-3); |
| 160 | + |
| 161 | +peq_fixed = [eq.PEQ_HP2, hp_fc, 0, 0]; |
| 162 | + |
| 163 | +%% Stage 1: fit a very-low-Q peak/notch at a mid frequency first, on top of |
| 164 | +% the HP2. Doing this before the low shelf keeps the shelf from over- |
| 165 | +% compensating for a broad midrange bump. The gain upper bound is kept |
| 166 | +% low (+6 dB) so this stage stays a mid *attenuator* rather than a boost. |
| 167 | +% Params: [fc, gain, Q]. fc [2000, 5000] Hz, gain [-20, +6] dB, Q [0.1, 0.5]. |
| 168 | +fmin_fit = 400; |
| 169 | +fmax_fit = 4000; |
| 170 | +p1_0 = [2500, -6, 0.3]; |
| 171 | +p1_bounds = [2000, 5000; -20, +6; 0.1, 0.5]; |
| 172 | +p1 = fminsearch(@(p) stage_rms(peq_fixed, pn_row(p, p1_bounds, eq), eq, fmin_fit, fmax_fit), ... |
| 173 | + p1_0, opts); |
| 174 | +peq_fixed = [peq_fixed; pn_row(p1, p1_bounds, eq)]; |
| 175 | +fprintf('Stage 1 (very-low-Q PN2): fc=%.1f Hz g=%.2f dB Q=%.2f\n', ... |
| 176 | + clamp(p1(1), p1_bounds(1,1), p1_bounds(1,2)), ... |
| 177 | + clamp(p1(2), p1_bounds(2,1), p1_bounds(2,2)), ... |
| 178 | + clamp(p1(3), p1_bounds(3,1), p1_bounds(3,2))); |
| 179 | + |
| 180 | +%% Stage 2: fit the low shelf on top of the flattened midrange. |
| 181 | +% Params: [ls_fc, ls_g]. fc [120, 1000] Hz, gain [0, +12] dB. |
| 182 | +% The upper bass gain bound is shrunk when stage 1 attenuated the mids, |
| 183 | +% so the total bass-to-mid shaping stays within max_bass_plus_mid_atten_db. |
| 184 | +fmin_fit = 200; |
| 185 | +fmax_fit = 2000; |
| 186 | +ls0 = [200, 8]; |
| 187 | +ls_bounds = [120, 1000; 0, 12]; |
| 188 | +mid_atten_db = min(0, clamp(p1(2), p1_bounds(2,1), p1_bounds(2,2))); |
| 189 | +ls_bounds(2,2) = min(ls_bounds(2,2), max(0, max_bass_plus_mid_atten_db - abs(mid_atten_db))); |
| 190 | +if ls_bounds(2,2) < ls0(2) |
| 191 | + ls0(2) = ls_bounds(2,2); |
| 192 | +end |
| 193 | +fprintf('Stage 2 bass gain upper bound = %.2f dB (mid atten %.2f dB, cap %.1f dB)\n', ... |
| 194 | + ls_bounds(2,2), mid_atten_db, max_bass_plus_mid_atten_db); |
| 195 | +ls = fminsearch(@(p) stage_rms(peq_fixed, shelf_row(eq.PEQ_LS2, p, ls_bounds), ... |
| 196 | + eq, fmin_fit, fmax_fit), ls0, opts); |
| 197 | +peq_fixed = [peq_fixed; shelf_row(eq.PEQ_LS2, ls, ls_bounds)]; |
| 198 | +fprintf('Stage 2 (LS2): fc=%.1f Hz g=%.2f dB\n', clamp(ls(1), ls_bounds(1,1), ls_bounds(1,2)), ... |
| 199 | + clamp(ls(2), ls_bounds(2,1), ls_bounds(2,2))); |
| 200 | + |
| 201 | +%% Widen the fit band for the fine correction stages: below 400 Hz the LS2 |
| 202 | +% already dominates and above the mid we still want to shape the response |
| 203 | +% out to 8 kHz. |
| 204 | +fmin_fit = 400; |
| 205 | +fmax_fit = 8000; |
| 206 | + |
| 207 | +%% Stage 3: fine correction constrained to the low/mid band (fc <= 4 kHz). |
| 208 | +% Seeded at 800 Hz; the fc range prevents it from stealing work from the |
| 209 | +% high-band biquad in stage 4. |
| 210 | +% Params: [fc, gain, Q]. fc [200, 4000] Hz, gain [-6, +12] dB, Q [0.1, 1.0]. |
| 211 | +p2_0 = [800, -3, 1.0]; |
| 212 | +p2_bounds = [200, 4000; -6, 12; 0.1, 1.0]; |
| 213 | +p2 = fminsearch(@(p) stage_rms(peq_fixed, pn_row(p, p2_bounds, eq), eq, fmin_fit, fmax_fit), ... |
| 214 | + p2_0, opts); |
| 215 | +peq_fixed = [peq_fixed; pn_row(p2, p2_bounds, eq)]; |
| 216 | +fprintf('Stage 3 (fine PN2 #1): fc=%.1f Hz g=%.2f dB Q=%.2f\n', ... |
| 217 | + clamp(p2(1), p2_bounds(1,1), p2_bounds(1,2)), ... |
| 218 | + clamp(p2(2), p2_bounds(2,1), p2_bounds(2,2)), ... |
| 219 | + clamp(p2(3), p2_bounds(3,1), p2_bounds(3,2))); |
| 220 | + |
| 221 | +%% Stage 4: second fine correction constrained to the high band (fc >= 3 kHz). |
| 222 | +% Seeded at 5000 Hz. The disjoint fc range vs. stage 3 guarantees the two |
| 223 | +% biquads land on complementary parts of the spectrum instead of |
| 224 | +% duplicating each other. |
| 225 | +% Params: [fc, gain, Q]. fc [3000, 12000] Hz, gain [-6, +12] dB, Q [0.1, 1.0]. |
| 226 | +p3_0 = [5000, -3, 1.0]; |
| 227 | +p3_bounds = [3000, 12000; -6, 12; 0.1, 1.0]; |
| 228 | +p3 = fminsearch(@(p) stage_rms(peq_fixed, pn_row(p, p3_bounds, eq), eq, fmin_fit, fmax_fit), ... |
| 229 | + p3_0, opts); |
| 230 | +peq_fixed = [peq_fixed; pn_row(p3, p3_bounds, eq)]; |
| 231 | +fprintf('Stage 4 (fine PN2 #2): fc=%.1f Hz g=%.2f dB Q=%.2f\n', ... |
| 232 | + clamp(p3(1), p3_bounds(1,1), p3_bounds(1,2)), ... |
| 233 | + clamp(p3(2), p3_bounds(2,1), p3_bounds(2,2)), ... |
| 234 | + clamp(p3(3), p3_bounds(3,1), p3_bounds(3,2))); |
| 235 | + |
| 236 | +eq.peq = peq_fixed; |
| 237 | +end |
| 238 | + |
| 239 | +%% ----------------------------------------------------------------------- |
| 240 | +%% FIR configuration for mid-band residual correction |
| 241 | +%% ----------------------------------------------------------------------- |
| 242 | +function eq = configure_fir(eq) |
| 243 | +%% The IIR takes care of the coarse bass boost, the high-Q anti-resonance |
| 244 | +% and two mid-band shapers. Whatever mid-band delta vs. the target is |
| 245 | +% left over after the IIR (fir_compensate_iir = 1 in the defaults) is |
| 246 | +% picked up here by a short minimum-phase FIR limited to the mid band, |
| 247 | +% so it does not spend taps on the LF/HF regions the IIR already handles |
| 248 | +% or where the measurement is unreliable. |
| 249 | +eq.fir_length = 63; |
| 250 | +eq.fir_beta = 10; |
| 251 | +eq.fir_minph = 1; |
| 252 | +eq.fir_autoband = 0; |
| 253 | +eq.fmin_fir = 400; |
| 254 | +eq.fmax_fir = 12000; |
| 255 | +fprintf('FIR: length=%d taps, mid band [%d, %d] Hz\n', ... |
| 256 | + eq.fir_length, eq.fmin_fir, eq.fmax_fir); |
| 257 | +end |
| 258 | + |
| 259 | +%% ----------------------------------------------------------------------- |
| 260 | +%% IIR + FIR blob packing and export |
| 261 | +%% ----------------------------------------------------------------------- |
| 262 | +function export_blobs(eq, cpath, iir_txt, iir_bin, fir_txt, fir_bin) |
| 263 | +%% Two-channel blob with a single shared response. Both channels are |
| 264 | +%% assigned to response 0, which suits identical L/R drivers on a |
| 265 | +%% single endpoint. For endpoints with distinct per-channel tuning, |
| 266 | +%% pass num_responses > 1 and adjust assign_response accordingly. |
| 267 | +channels_in_config = 2; |
| 268 | +num_responses = 1; |
| 269 | +assign_response = [0 0]; |
| 270 | + |
| 271 | +%% IIR blob |
| 272 | +bq_iir = sof_eq_iir_blob_quant(eq.p_z, eq.p_p, eq.p_k); |
| 273 | +bm_iir = sof_eq_iir_blob_merge(channels_in_config, num_responses, ... |
| 274 | + assign_response, bq_iir); |
| 275 | +bp_iir = sof_eq_iir_blob_pack(bm_iir, 4); % IPC4 |
| 276 | +sof_alsactl_write(fullfile(cpath, iir_txt), bp_iir); |
| 277 | +sof_ucm_blob_write(fullfile(cpath, iir_bin), bp_iir); |
| 278 | + |
| 279 | +%% FIR blob |
| 280 | +bq_fir = sof_eq_fir_blob_quant(eq.b_fir); |
| 281 | +bm_fir = sof_eq_fir_blob_merge(channels_in_config, num_responses, ... |
| 282 | + assign_response, bq_fir); |
| 283 | +bp_fir = sof_eq_fir_blob_pack(bm_fir, 4); % IPC4 |
| 284 | +sof_alsactl_write(fullfile(cpath, fir_txt), bp_fir); |
| 285 | +sof_ucm_blob_write(fullfile(cpath, fir_bin), bp_fir); |
| 286 | +end |
| 287 | + |
| 288 | +%% ----------------------------------------------------------------------- |
| 289 | +%% Small helpers used by the IIR stages |
| 290 | +%% ----------------------------------------------------------------------- |
| 291 | +function peq = shelf_row(type, p, b) |
| 292 | +peq = [type, clamp(p(1), b(1,1), b(1,2)), clamp(p(2), b(2,1), b(2,2)), 0]; |
| 293 | +end |
| 294 | + |
| 295 | +function peq = pn_row(p, b, eq) |
| 296 | +peq = [eq.PEQ_PN2, clamp(p(1), b(1,1), b(1,2)), ... |
| 297 | + clamp(p(2), b(2,1), b(2,2)), ... |
| 298 | + clamp(p(3), b(3,1), b(3,2))]; |
| 299 | +end |
| 300 | + |
| 301 | +function e = stage_rms(peq_fixed, new_row, eq, fmin_fit, fmax_fit) |
| 302 | +eq.peq = [peq_fixed; new_row]; |
| 303 | +try |
| 304 | + eq2 = sof_eq_compute(eq); |
| 305 | +catch |
| 306 | + e = 1e6; |
| 307 | + return; |
| 308 | +end |
| 309 | +idx = eq2.f >= fmin_fit & eq2.f <= fmax_fit; |
| 310 | +resp = eq2.m_db_s(idx) + eq2.iir_eq_db(idx); |
| 311 | +resp = resp - mean(resp); % remove overall level, keep only shape |
| 312 | +e = sqrt(mean(resp .^ 2)); |
| 313 | +end |
| 314 | + |
| 315 | +function y = clamp(x, lo, hi) |
| 316 | +y = min(hi, max(lo, x)); |
| 317 | +end |
| 318 | + |
| 319 | +%% ----------------------------------------------------------------------- |
| 320 | +%% UCM product .conf generation and misc string / filesystem helpers |
| 321 | +%% ----------------------------------------------------------------------- |
| 322 | +function write_product_conf(conf_file, sys_vendor, product_name, endpoint, base, eq) |
| 323 | +ep_cap = capitalize(endpoint); |
| 324 | +iir_key = sprintf('Define.PostMixer%sPlaybackIirBlob', ep_cap); |
| 325 | +fir_key = sprintf('Define.PostMixer%sPlaybackFirBlob', ep_cap); |
| 326 | +iir_path = sprintf('/usr/share/alsa/ucm2/blobs/sof/ipc4/eq_iir/%s_iir.bin', base); |
| 327 | +fir_path = sprintf('/usr/share/alsa/ucm2/blobs/sof/ipc4/eq_fir/%s_fir.bin', base); |
| 328 | + |
| 329 | +fid = fopen(conf_file, 'w'); |
| 330 | +if fid < 0 |
| 331 | + error('Could not open %s for writing', conf_file); |
| 332 | +end |
| 333 | +fprintf(fid, '# Add bespoke %s equalizer for %s %s\n', endpoint, sys_vendor, product_name); |
| 334 | +fprintf(fid, '#\n'); |
| 335 | +fprintf(fid, '# IIR is defined as parametric equalizer and FIR carries the mid-band residual\n'); |
| 336 | +fprintf(fid, '# correction, see:\n'); |
| 337 | +fprintf(fid, '# https://github.com/thesofproject/sof/tree/main/src/audio/eq_iir/tune\n'); |
| 338 | +fprintf(fid, '#\n'); |
| 339 | +for i = 1:size(eq.peq, 1) |
| 340 | + fprintf(fid, '#\t%-10s %6.1f %+5.1f %4.2f\n', ... |
| 341 | + peq_type_name(eq, eq.peq(i, 1)), eq.peq(i, 2), eq.peq(i, 3), eq.peq(i, 4)); |
| 342 | +end |
| 343 | +fprintf(fid, '\n'); |
| 344 | +fprintf(fid, '%s "%s"\n', iir_key, iir_path); |
| 345 | +fprintf(fid, '%s "%s"\n', fir_key, fir_path); |
| 346 | +fclose(fid); |
| 347 | +fprintf('Wrote %s\n', conf_file); |
| 348 | +end |
| 349 | + |
| 350 | +function s = sanitize_name(s) |
| 351 | +% Lower case, replace any non-alphanumeric run with a single underscore, and |
| 352 | +% trim leading/trailing underscores. e.g. 'Acme Ltd.' -> 'acme_ltd', |
| 353 | +% 'Model 100' -> 'model_100'. |
| 354 | +s = lower(s); |
| 355 | +s = regexprep(s, '[^a-z0-9]+', '_'); |
| 356 | +s = regexprep(s, '^_+|_+$', ''); |
| 357 | +end |
| 358 | + |
| 359 | +function s = capitalize(s) |
| 360 | +if isempty(s) |
| 361 | + return; |
| 362 | +end |
| 363 | +s = [upper(s(1)), lower(s(2:end))]; |
| 364 | +end |
| 365 | + |
| 366 | +function ensure_dir(d) |
| 367 | +if ~exist(d, 'dir') |
| 368 | + [ok, msg] = mkdir(d); |
| 369 | + if ~ok |
| 370 | + error('mkdir %s failed: %s', d, msg); |
| 371 | + end |
| 372 | +end |
| 373 | +end |
| 374 | + |
| 375 | +function name = peq_type_name(eq, type_num) |
| 376 | +% Look up the PEQ_* field name whose value matches type_num, using the |
| 377 | +% constants that sof_eq_defaults() already stored on the eq struct. This |
| 378 | +% keeps the mapping in sync with sof_eq_define_parametric_eq.m without |
| 379 | +% duplicating the enum here. |
| 380 | +fns = fieldnames(eq); |
| 381 | +for k = 1:numel(fns) |
| 382 | + if strncmp(fns{k}, 'PEQ_', 4) && isnumeric(eq.(fns{k})) && ... |
| 383 | + isscalar(eq.(fns{k})) && eq.(fns{k}) == type_num |
| 384 | + name = fns{k}; |
| 385 | + return; |
| 386 | + end |
| 387 | +end |
| 388 | +name = sprintf('PEQ_%d', type_num); |
| 389 | +end |
0 commit comments