diff --git a/Sound Fields/MATLAB/Functions/rectRoomPtSrc.m b/Sound Fields/MATLAB/Functions/rectRoomPtSrc.m new file mode 100644 index 0000000000000000000000000000000000000000..0247ce92fc58aee3cc0c63b201bb60cf200bb2e3 --- /dev/null +++ b/Sound Fields/MATLAB/Functions/rectRoomPtSrc.m @@ -0,0 +1,338 @@ +%% Calculate wavefield generated by point sourcesinside a rectangular room +% -------------------------------------------------- +% Author: Achilles Kappis +% e-mail: axilleaz@protonmail.com +% +% Date: 29/05/2025 (DD/MM/YYYY) +% +% Copyright: MIT +% -------------------------------------------------- +% Functionality: Calculate the wavefield generated by point sources at +% specified locations inside a rectangular room of specified +% dimensions and absorption. +% -------------------------------------------------- +% Input +% +% sPos [numeric]: The position of the sources in Cartesian coordinates. The +% variable should be a matrix with dimensions 3xN where N +% is the number of sources and the rows represent the x, y +% and z coordinates respectively. +% IMPORTANT: The coordinates of the sources must NOT lie +% outside the room (set in "roomSize" argument). +% +% rPos [numeric]: The position of the receivers in Cartesian coordinates. +% The variable should be a matrix with dimensions 3xM where +% M is the number of receivers and the rows represent the +% x, y and z coordinates respectively. +% IMPORTANT: The coordinates of the receivers must NOT lie +% outside the room (set in "roomSize" argument). +% +% roomDims [numeric]: The dimensions of the room. This can either be a +% scalar, in which case the room will correspond to a +% cubic enclosure, or a vector with three elements +% corresponding to the x, y and z dimensions of the +% room. The values must be positive scalars. +% +% f [numeric]: The frequencies for which to calculate the wave field. It +% must be a positive scalar or vector of positive values. +% +% nModes [numeric] (Optional): The (approximate) number of modes to be used +% for the calculation of the sound field. This +% value can either be a positive scalar +% denoting the total number of modes that will +% be used or a 3xN matrix where the first +% dimension will contain the mode index along +% each dimension and N corresponds to the +% number of total modes. See notes for the +% mode indices used when a scalar is provided. +% [Default: 16^3 = 4096]. +% +% dampRatio [numeric] (Optional): The damping ratio of the boundaries. This +% can be a scalar denoting that the damping +% ratio is identical for all frequencies or +% a vector with the number of elements +% matching the length of the "f" argument. +% The damping ratio at the natural +% frequencies of the room will be +% calculated through linear interpolation +% from the damping ratio values provided. +% [Default: 0.1]. +% +% Q [numeric] (Optional): The source strengths. This argument can be a +% scalar indicating that all sources have the same +% strength per frequency, a vector with N elements, +% each corresponding to the source of each source +% per frequency, or a matrix with dimensions NxF, +% where each element will correspond to the +% strength of each source for each frequency. +% [Default: 1]. +% +% c [numeric] (Optional): The speed of sound in m/s. [Default: 343]. +% +% rho [numeric (Optional): The density of the medium in kg/(m^3). +% [Default: 1.204]. +% +% -------------------------------------------------- +% Output +% +% modeIdx [numeric]: The mode of the indices in the order they are placed +% in the output arguments below (this helps to identify +% contribution of each mode). This is a 3xK matrix with +% the first dimension denoting the indices of each mode +% and K denoting the number of modes. +% +% waveField [numeric]: The complex pressures at the position of the +% receivers. The variable is an array of dimensions +% MxNxFxK where M is the number of receivers, N the +% number of sources and F the number of frequencies. +% +% srcWaveField [numeric]: The complex pressures at the positions of the +% receivers with the contribution of the individual +% modes being summed up. This should correspond to +% the contribution of each source to each receiving +% position per frequency. This is an MxNxF array. +% +% broadWaveField [numeric]: The complex pressures at the positions of the +% receivers per mode with the contribution per +% frequency being summed up. This corresponds to +% the broadband (in the frequencies provided) +% contribution of each mode at each position per +% source. This is an MxNxK array. +% +% srcBroadWaveField [numeric]: The complex pressures at the positions of +% the receivers with the contribution of each +% mode and frequency summed up. This is an MxN +% array and corresponds to the "total" (from a +% frequency perspective) transfer function +% from each source to each receiver +% +% sumWaveField [numeric]: The complex pressures at the positions of the +% receivers with the contribution of the sources +% being summed up. This corresponds to the sum of +% the contributions of each source to each mode per +% frequency and is an MxFxK array. +% +% sumSrcWaveField [numeric]: The complex pressures at the positions of the +% receivers with the contributions of the +% sources and the modes being summed up. This +% corresponds to the total complex pressure at +% each receiver position per frequency due to +% all the sources and modes and is an MxF array. +% +% sumBroadWaveField [numeric]: The complex pressures at the positions of +% the receivers with the contributions of the +% sources summed up for all frequencies. This +% correspond to the broadband (in the +% frequencies provided) contribution of each +% mode due to all sources and is an MxK array. +% +% totalWaveField [numeric]: This is the wavefield at each receiver position +% with the contribution of each mode, source and +% frequency summed up. In a sense this +% corresponds to the total complex pressure at +% each receiver position and is a vector with M +% elements. +% +% -------------------------------------------------- +% Notes +% +% The actual number of modes calculated when a scalar is provided for +% "nModes" is equal to ceil(nthroot(nModes, 3))^3 - 1. This value will +% result in a set of mode indices that will provide all available +% combinations of mode indices up to ceil(nthroot(nModes, 3)). As an +% example, if nModes = 5 we would get ceil(nthroot(5, 3))^3 - 1 == 7 and +% the modes used would be +% +% (1, 0, 0) | (0, 1, 0) | (0, 0, 1) +% (1, 1, 0) | (1, 0, 1) | (0, 1, 1) +% (1, 1, 1) +% +% +% +% Dependencies: - twoPtDist(): To calculate the distances between sources +% and receivers. +% -------------------------------------------------- +function [modeIdx, waveField, srcWaveField, broadWaveField, srcBroadWaveField, sumWaveField, sumSrcWaveField, sumBroadWaveField, totalWaveField] = rectRoomPtSrc(sPos, rPos, roomDims, f, nModes, dampRatio, Q, c, rho) + % ==================================================== + % Check for number of arguments + % ==================================================== + narginchk(4, 9); + nargoutchk(0, 9); + + % ==================================================== + % Validate input arguments + % ==================================================== + % Validate mandatory arguments + validateattributes(sPos, "numeric", {'2d', 'nrows', 3, 'finite', 'nonnan', 'nonempty', 'real'}, mfilename, "Source positions", 1); + validateattributes(rPos, "numeric", {'2d', 'nrows', 3, 'finite', 'nonnan', 'nonempty', 'real'}, mfilename, "Receiver positions", 2); + validateattributes(roomDims, "numeric", {'vector', 'finite', 'nonnan', 'nonempty', 'real', 'positive'}, mfilename, "Room dimensions", 3); + + if ~isscalar(roomDims) && numel(roomDims) ~= 3 + error("The dimensions argument must either be a scalar or a vector with three positive elements."); + end + + validateattributes(f, "numeric", {'vector', 'finite', 'nonnan', 'nonempty', 'real', 'positive'}, mfilename, "Frequencies", 4); + + % Validate optional arguments (and set their values) + if nargin > 8 && ~isempty(rho) + validateattributes(rho, "numeric", {'scalar', 'finite', 'positive', 'nonnan', 'nonempty', 'real'}, mfilename, "Medium density", 9); + else + rho = 1.204; + end + + if nargin > 7 && ~isempty(c) + validateattributes(c, "numeric", {'scalar', 'finite', 'positive', 'nonnan', 'nonempty', 'real'}, mfilename, "Speed of propagation", 8); + else + c = 343; + end + + if nargin > 6 && ~isempty(Q) + if isscalar(Q) + validateattributes(Q, "numeric", {'scalar', 'nonempty', 'real', 'finite', 'nonnan', 'nonnegative'}, mfilename, "Source strengths", 7); + elseif isvector(Q) + validateattributes(Q, "numeric", {'vector', 'numel', size(sPos, 2), 'nonempty', 'real', 'finite', 'nonnan', 'nonnegative'}, mfilename, "Source strengths", 7); + else + validateattributes(Q, "numeric", {'2d', 'size', [size(sPos, 2), length(f)] ,'nonempty', 'real', 'finite', 'nonnan', 'nonnegative'}, mfilename, "Source strengths", 7); + end + else + Q = 1; + end + + if nargin > 5 && ~isempty(dampRatio) + if isscalar(dampRatio) + validateattributes(dampRatio, "numeric", {'scalar', 'finite', 'nonempty', '<=', 1, 'real', 'nonnan', 'positive'}, mfilename, "Damping ratio", 6); + else + validateattributes(dampRatio, "numeric", {'vector', 'numel', length(f), 'finite', 'nonempty', '<=', 1, 'real', 'nonnan', 'positive'}, mfilename, "Damping ratio", 6); + end + else + dampRatio = 0.1; + end + + if nargin > 4 && ~isempty(nModes) + if ~isscalar(nModes) + validateattributes(nModes, "numeric", {'2d', 'nrows', 3, 'nonempty', 'integer', 'real', 'finite', 'nonnan', 'nonnegative'}, mfilename, "Modes", 5); + else + validateattributes(nModes, "numeric", {'scalar', 'nonempty', 'integer', 'real', 'finite', 'nonnan', 'nonnegative'}, mfilename, "Number of modes", 5); + end + else + nModes = 4096; + end + + % Check validity of source and receiver positions + if sum(~isbetween(sPos(1, :), -roomDims(1)/2, roomDims(1)/2)) > 0 || ... + sum(~isbetween(sPos(2, :), -roomDims(2)/2, roomDims(2)/2)) > 0 || ... + sum(~isbetween(sPos(3, :), -roomDims(3)/2, roomDims(3)/2)) > 0 + error("Sources outside the room where found."); + end + + if sum(~isbetween(rPos(1, :), -roomDims(1)/2, roomDims(1)/2)) > 0 || ... + sum(~isbetween(rPos(2, :), -roomDims(2)/2, roomDims(2)/2)) > 0 || ... + sum(~isbetween(rPos(3, :), -roomDims(3)/2, roomDims(3)/2)) > 0 + error("Receivers outside the room where found."); + end + + + % ==================================================== + % Pre-process + % ==================================================== + % Convert frequencies to circular frequencies + f = 2 * pi * f; + + % Create room dimensions vector + if isscalar(roomDims) + roomDims = roomDims * ones(3, 1); + end + + % Create matrix with mode indices + if isscalar(nModes) + modeIdx = ceil(nthroot(nModes, 3)) - 1; + modeIdx = combinations(0:modeIdx, 0:modeIdx, 0:modeIdx); + modeIdx = table2array(modeIdx); + modeIdx = modeIdx(2:end, :); + else + modeIdx = nModes; + end + modeIdx = modeIdx./roomDims; % Calculate the ratio of the index to the dimensions of the room (used in the calculations) + + % Calculate normalisation factors + normFact = sqrt(prod((modeIdx > 0) + 1, 2)); + + % Create vector with damping ratios + if isscalar(dampRatio) + dampRatio = dampRatio * ones(length(f), 1); + end + + % Create matrix with source strengths + if isscalar(Q) + Q = Q * ones(size(sPos, 2), length(f)); + elseif isvector(Q) + Q = repmat(Q(:), 1, length(f)); + end + + + % ==================================================== + % Calculate sound field + % ==================================================== + % Calculate eigenfrequencies + wEig = pi * c * sqrt(sum(modeIdx.^2, 2)); + + % Inter/extrapolate damping ratio at the eigenfrequencies + if length(f) > 1 + dampRatio = interp1(f, dampRatio, wEig, "linear", "extrap"); + end + + % Calculate the complex resonance terms + A = repmat(f(:).', length(wEig), 1)./ ... + (2 * dampRatio .* wEig .* f(:).' - ... + 1j * (repmat(wEig.^2, 1, length(f)) - repmat((f(:).').^2, length(wEig), 1))); + A = A * (rho * c^2)/prod(roomDims); % Multiply with the constant factor + + % Calculate the eigenfunction values at the source and receiver positions + modeIdx = modeIdx * pi; + + % Loop through frequencies + for fIdx = 1:length(f) + tmpA = A(:, fIdx); + + % Loop through the receivers + for rIdx = size(rPos, 2):-1:1 + rPsi = normFact .* prod(cos(modeIdx .* rPos(:, rIdx).'), 2); + + % Loop through the sources + parfor sIdx = 1:size(sPos, 2) + waveField(rIdx, sIdx, fIdx, :) = rPsi .* tmpA .* normFact .* prod(cos(modeIdx .* sPos(:, sIdx).'), 2) * Q(sIdx, fIdx); + end + end + end + + % ==================================================== + % Return additional output arguments + % ==================================================== + if nargout > 2 + srcWaveField = squeeze(sum(waveField, 4)); + end + + if nargout > 3 + broadWaveField = squeeze(sum(waveField, 3)); + end + + if nargout > 4 + srcBroadWaveField = squeeze(sum(broadWaveField, 3)); + end + + if nargout > 5 + sumWaveField = squeeze(sum(waveField, 2)); + end + + if nargout > 6 + sumSrcWaveField = squeeze(sum(sumWaveField, 3)); + end + + if nargout > 7 + sumBroadWaveField = squeeze(sum(sumWaveField, 2)); + end + + if nargout > 8 + totalWaveField = squeeze(sum(sumBroadWaveField, 2)); + end +end