Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c4e2cde
init module
hivanov-nrel Aug 13, 2025
c384d7e
example and data
hivanov-nrel Sep 17, 2025
2fd5cd2
acoustics module functions
hivanov-nrel Sep 17, 2025
0c135a4
bug fix for older versions of matlab
hivanov-nrel Sep 17, 2025
8b3ac90
Examples: Acoustics, add original .m version of acoustics_example
simmsa Sep 23, 2025
f824efc
Examples: Acoustics, add nmfs_auditory_weighting section
simmsa Sep 28, 2025
12f331d
Examples: Acoustics, clean up typos and formatting
simmsa Sep 28, 2025
07e77cc
Acoustics: Fix typo in plot_spectrogram function name
simmsa Sep 28, 2025
ec21476
Acoustics: Add functions to validate repeated acoustics structures
simmsa Sep 28, 2025
61ad1df
Acoustics: Standardize apply_calibration docstring and input validation
simmsa Sep 28, 2025
2331400
Acoustics: Standardize band_aggregate docstring and input validation
simmsa Sep 28, 2025
7634526
Acoustics: Standardize band_sound_pressure_level docstring and valida…
simmsa Sep 28, 2025
12cdfb2
Acoustics: Standardize create_frequency_bands docstring and input val…
simmsa Sep 28, 2025
90a0e02
Acoustics: Fix unit typo db -> dB
simmsa Sep 28, 2025
313afa7
Acoustics: Standardize decidecade_sound_pressure_level docstring and …
simmsa Sep 28, 2025
0667820
Acoustics: Standardize sound_exposure_level docstring and validation
simmsa Sep 28, 2025
5175d91
Acoustics: Standardize sound_pressure_level docstring and input valid…
simmsa Sep 28, 2025
7613a3d
Acoustics: Standardize sound_pressure_spectral_density docstring and …
simmsa Sep 28, 2025
d9abbee
Acoustics: Standardize sound_pressure_spectral_density_level docstrin…
simmsa Sep 28, 2025
6b0c755
Acoustics: Standardize third_octave_sound_pressure_level docstring an…
simmsa Sep 28, 2025
7e4c876
Acoustics: Standardize time_aggregate docstring and validation
simmsa Sep 28, 2025
2d7531b
Acoustics: Standardize plot_spectra docstring and input validation
simmsa Sep 28, 2025
897b644
Acoustics: Standardize read_hydrophone docstring and input validation
simmsa Sep 28, 2025
1809132
Acoustics: Standardize read_iclisten docstring and input validation
simmsa Sep 28, 2025
c7bf6ff
Acoustics: Standardize read_soundtrap docstring
simmsa Sep 28, 2025
031b461
Acoustics: Add test for `export_audio` function
simmsa Sep 28, 2025
21636e6
Acoustics: Add export_audio function
simmsa Sep 28, 2025
e3cc9dd
Acoustics: Refactor plot_spectrogram to add smoothing and plot methods
simmsa Sep 28, 2025
c277a09
Examples: Acoustics, use refactored plot_spectrogram function
simmsa Sep 28, 2025
573f74c
Acoustics: Improve docstring formatting
simmsa Sep 28, 2025
9eb36ca
Acoustics: Standardize fft_hann docstring
simmsa Sep 29, 2025
04df266
Acoustics: Clean up `fft_hann` formatting
simmsa Sep 29, 2025
0c28110
Acoustics: Standardize fmax_warning docstring and input validation
simmsa Sep 29, 2025
c0020c3
Acoustics: Remove restrictive shape/size check
simmsa Sep 29, 2025
64ec704
Acoustics: Clean up whitespace
simmsa Sep 29, 2025
853c6b8
Examples: Acoustics, hardcode frequency range
simmsa Sep 29, 2025
8606e3c
Examples: Acoustics, add export_audio comment
simmsa Sep 29, 2025
1f87be2
Acoustics: Standardize minimum_frequency docstring and input validation
simmsa Sep 29, 2025
829acfa
Acoustics: Standardize validate_method docstring and input validation
simmsa Sep 29, 2025
bdae482
Acoustics: Fix edge cases in validate_method
simmsa Sep 29, 2025
39e7e61
Acoustics: Update validate_method error messages in tests
simmsa Sep 29, 2025
536d37a
Examples: Update acoustics example assets
simmsa Sep 29, 2025
ac3132a
Merge pull request #1 from simmsa/acoustics-standardization
hivanov-nrel Sep 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
622 changes: 622 additions & 0 deletions examples/acoustics_example.html

Large diffs are not rendered by default.

495 changes: 495 additions & 0 deletions examples/acoustics_example.m

Large diffs are not rendered by default.

Binary file added examples/acoustics_example.mlx
Binary file not shown.
Binary file added examples/data/acoustics/6247.230204150508.wav
Binary file not shown.
41 changes: 41 additions & 0 deletions examples/data/acoustics/6247_calibration.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
Frequency,Analog Sensitivity,
1,-223.49,
1.183,-220.8,
1.399,-218.13,
1.655,-215.41,
1.958,-212.68,
2.316,-209.91,
2.74,-207.12,
3.241,-204.29,
3.834,-201.45,
4.535,-198.58,
5.364,-195.69,
6.345,-192.79,
7.506,-189.85,
8.879,-186.9,
10.5,-183.93,
12.42,-180.93,
14.7,-177.92,
17.38,-174.97,
20.56,-172.16,
24.33,-169.61,
28.78,-167.69,
34.04,-166.52,
40.26,-165.96,
47.63,-165.81,
56.34,-165.85,
66.65,-165.95,
78.84,-166.05,
93.26,-166.13,
110.3,-166.2,
130.5,-166.25,
154.4,-166.28,
182.6,-166.29,
216,-166.29,
255.5,-166.27,
302.2,-166.24,
357.5,-166.17,
422.9,-166.03,
500.3,-165.79,
591.8,-165.47,
700,-164.87,
Binary file not shown.
63 changes: 63 additions & 0 deletions mhkit/acoustics/analysis/apply_calibration.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
function spsd_cal = apply_calibration(spsd,sensitivity_curve, fill_value)

% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Apply custom calibration to spectral density values
%
% Parameters
% ------------
% spsd: struct
% Mean square sound pressure spectral density in V^2/Hz
% spsd.data : Spectral density data [V^2/Hz]
% spsd.freq : Frequency vector [Hz]
% spsd.time : Time vector (if time series)
% sensitivity_curve: matrix
% Calibrated sensitivity curve in units of dB rel 1 V^2/uPa^2
% First column should be frequency, second column should be calibration values
% fill_value: float
% Value with which to fill missing values from the calibration curve [dB rel 1 V^2/uPa^2]
%
% Returns
% ---------
% spsd_cal: struct
% Calibrated spectral density in Pa^2/Hz, indexed by time and frequency
% spsd_cal.data : Calibrated spectral density data [Pa^2/Hz]
% spsd_cal.freq : Frequency vector [Hz]
% spsd_cal.time : Time vector (if time series)
% spsd_cal.name : Data name
% spsd_cal.units : Data units
%
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

arguments (Input)
spsd struct
sensitivity_curve {mustBeNumeric, mustBeFinite}
fill_value {mustBeNumeric, mustBeFinite}
end

arguments (Output)
spsd_cal struct
end

% Validate spsd structure
validate_spsd_struct(spsd, 'apply_calibration', ...
'required_fields', {{'freq'}});

spsd_cal = spsd;

% interpolate calibration
calibration = interp1(sensitivity_curve(:,1), ...
sensitivity_curve(:,2), spsd.freq, "linear");
% use first cal value to fill NaNs in lower freq
idx = find(spsd.freq < sensitivity_curve(1,1));
calibration(idx) = fillmissing(calibration(idx),'constant',sensitivity_curve(1,2));
% fill NaNs at high freq using specified fill_value
calibration = fillmissing(calibration,'constant',fill_value);

% subtract from sound pressure spectral density
sense_ratio = 10.^(calibration / 10); % V^2/uPa^2
spsd_cal.data = (spsd_cal.data ./ sense_ratio) / 1e12; % Pa^2/Hz
spsd_cal.name = "Calibrated Sound Pressure Spectral Density";
spsd_cal.units = "Pa^2/Hz";

end
98 changes: 98 additions & 0 deletions mhkit/acoustics/analysis/band_aggregate.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
function out = band_aggregate(spsdl, octave, fmin, fmax, method)

% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Reorganize spectral density level frequency tensor into fractional octave bands and applies a function to them
%
% Parameters
% ------------
% spsdl: struct
% Mean square sound pressure spectral density level in dB rel 1 uPa^2/Hz
% spsdl.data : Spectral density level data [dB rel 1 uPa^2/Hz]
% spsdl.freq : Frequency vector [Hz]
% spsdl.time : Time vector
% spsdl.fs : Sampling frequency [Hz]
% octave: (1,2) vector
% Octave and octave base to subdivide spectral density level by
% Set octave base to 2 for true octave band; set to base 10 for decidecade octave band
% Default = [3, 2] (true third octave)
% fmin: float
% Lower frequency band limit (lower limit of the hydrophone) [Hz]
% fmax: float
% Upper frequency band limit (Nyquist frequency) [Hz]
% method: string or cell
% Method to run on the binned data. Can be a string (e.g., "median") or a cell
% where the first element is the method and the second is its argument
% Options: [median, mean, min, max, sum, quantile, std, var, count, map]
%
% Returns
% ---------
% out: struct
% Frequency band-averaged sound pressure spectral density level
% out.data : Band-averaged spectral density level [dB re 1 uPa^2/Hz]
% out.freq : Center frequencies of bands [Hz]
% out.time : Time vector
% out.name : Data name
% out.units : Data units
%
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


arguments (Input)
spsdl struct
octave {mustBeVector} = [3,2]
fmin {mustBeNumeric} = 10
fmax {mustBeNumeric} = 100000
method = "median"
end

arguments (Output)
out struct
end

% Validate spsdl structure
validate_spsdl_struct(spsdl, 'band_aggregate', ...
'required_fields', {{'data', 'freq', 'time', 'name', 'units', 'fs'}});

if ~isnumeric(octave) || numel(octave) ~= 2
error('MHKiT:acoustics:band_aggregate:InvalidOctave', 'octave must be a vector of two positive integers');
end
if any(octave <= 0)
error('MHKiT:acoustics:band_aggregate:InvalidOctave', 'octave must contain positive integers');
end
if ~isnumeric(fmin) || fmin <= 0
error('MHKiT:acoustics:band_aggregate:InvalidFrequency', 'fmin must be a positive number');
end
if ~isnumeric(fmax) || fmax <= fmin
error('MHKiT:acoustics:band_aggregate:InvalidFrequency', 'fmax must be greater than fmin');
end

fmax = fmax_warning(spsdl.fs/2, fmax);

% validate method
[method_name, method_arg] = validate_method(method);
if isempty(method_arg)
mfunc = str2func(method_name);
elseif method_arg == "quantile"
mfunc = @(x)quantile(x,method_arg);
else
mfunc = method_arg;
end

% derive octave bins
[octave_bins, band] = create_frequency_bands(octave(1),octave(2),fmin,fmax);

% groupby and apply method
idx_bin = discretize(spsdl.freq, octave_bins);
temp = zeros(length(band.center_freq), length(spsdl.time));
for x=1:length(spsdl.time)
temp(:,x) = splitapply(mfunc,spsdl.data(:,x),idx_bin);
end

out.data = temp;
out.freq = band.center_freq(min(idx_bin):max(idx_bin));
out.time = spsdl.time;
out.name = spsdl.name;
out.units = spsdl.units;

end
81 changes: 81 additions & 0 deletions mhkit/acoustics/analysis/band_sound_pressure_level.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
function mspl = band_sound_pressure_level(spsd, octave, base, fmin, fmax)

% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Calculate band-averaged sound pressure levels from the mean square sound pressure spectral density (SPSD)
%
% Parameters
% ------------
% spsd: struct
% Mean square sound pressure spectral density in [Pa^2/Hz]
% spsd.data : Spectral density data [Pa^2/Hz]
% spsd.freq : Frequency vector [Hz]
% spsd.time : Time vector
% spsd.fs : Sampling frequency [Hz]
% octave: float
% Octave subdivision (1 = full octave, 3 = third-octave, etc.)
% base: float
% Octave base subdivision (2 = true octave, 10 = decade octave, etc.)
% fmin: float
% Lower frequency band limit (lower limit of the hydrophone) [Hz]
% fmax: float
% Upper frequency band limit (Nyquist frequency) [Hz]
%
% Returns
% ---------
% mspl: struct
% Sound pressure level [dB re 1 uPa] indexed by time and frequency of specified bandwidth
% mspl.data : Sound pressure level data [dB re 1 uPa]
% mspl.freq : Center frequencies of bands [Hz]
% mspl.time : Time vector
%
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

arguments (Input)
spsd struct
octave {mustBeInteger}
base {mustBeInteger} = 2
fmin {mustBeNumeric} = 10
fmax {mustBeNumeric} = 100000
end

arguments (Output)
mspl struct
end

% Validate spsd structure
validate_spsd_struct(spsd, 'band_sound_pressure_level', ...
'required_fields', {{'freq', 'time', 'fs'}});

reference = 1e-12; % Pa^2, = 1 uPa^2

fmax = fmax_warning(spsd.fs/2, fmax);

% Create frequency bands
[obins, band] = create_frequency_bands(octave, base, fmin, fmax);

nBands = numel(band.center_freq);
nTime = numel(spsd.time);
mspl.data = zeros(nBands, nTime);

for i = 1:nBands
band_range = [band.lower_limit(i), band.upper_limit(i)];
idx = find(spsd.freq >= band_range(1) & spsd.freq <= band_range(2));
if numel(idx) < 2
% Interpolate if only one frequency in band
spsd_slc = interp1(spsd.freq, spsd.data, band_range, 'linear', 'extrap');
x = band_range;
else
spsd_slc = spsd.data(idx, :);
x = spsd.freq(idx);
end
% Integrate spectral density by frequency for each time
for t = 1:nTime
mspl.data(i, t) = 10 * log10(trapz(x, spsd_slc(:, t)) / reference);
end
end

mspl.freq = band.center_freq;
mspl.time = spsd.time;

end
60 changes: 60 additions & 0 deletions mhkit/acoustics/analysis/create_frequency_bands.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
function [octave_bins, band] = create_frequency_bands(octave, base, fmin, fmax)

% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Calculate frequency bands based on the specified octave, minimum and maximum frequency limits
%
% Parameters
% ------------
% octave: double
% Octave to subdivide spectral density level by
% base: double, optional
% Octave base. Set to 2 for the true octave band; set to base 10 for
% the decidecade octave band. Default: 2
% fmin: double, optional
% Lower frequency band limit (lower limit of the hydrophone). Default is 10 Hz
% fmax: double, optional
% Upper frequency band limit (Nyquist frequency). Default is 100,000 Hz
%
% Returns
% ---------
% octave_bins: array
% Array of octave bin edges
% band: structure
% band.center_freq : Center frequencies [Hz]
% band.lower_limit : Lower frequency limits [Hz]
% band.upper_limit : Upper frequency limits [Hz]
%
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

arguments (Input)
octave double {mustBeFinite, mustBePositive}
base double {mustBeFinite, mustBePositive} = 2
fmin double {mustBeFinite, mustBePositive} = 10
fmax double {mustBeFinite, mustBePositive} = 100000
end

arguments (Output)
octave_bins double
band struct
end

bandwidth = base^(1 / octave);
half_bandwidth = base^(1 / (octave * 2));

% Calculate center frequencies
log_fmin = log10(fmin);
log_fmax = log10(fmax * bandwidth);
step = log10(bandwidth);
center_freq = 10 .^ (log_fmin : step : log_fmax);

lower_limit = center_freq / half_bandwidth;
upper_limit = center_freq * half_bandwidth;
octave_bins = [lower_limit, upper_limit(end)];

band = struct();
band.center_freq = center_freq;
band.lower_limit = lower_limit;
band.upper_limit = upper_limit;

end
Loading
Loading