Skip to content
Snippets Groups Projects
virtMicGeo.m 8.60 KiB
%% Calculate virtual microphone positions for specific geometries
% --------------------------------------------------
% Author: Achilles Kappis
% e-mail: axilleaz@protonmail.com
%
% Date: 18/09/2024 (DD/MM/YYYY)
%
% Copyright: MIT
% --------------------------------------------------
% Functionality: Calculate virtual microphone positions for specific
%                geometries.
% --------------------------------------------------
% Input
% 
% gType [char/string]: The type of the source geometry. At the moment the
%                      available options are "Single", "Array", "Cube" and
%                      "Dual". The last is an arrangement a distance apart
%                      that can be translated and rotated.
% 
% geoDim [numeric] (Optional): The dimensions of the geometry. This is a
%                              vector with three elements holding the
%                              dimensions of the setup in each Cartesian
%                              direction. Based on the "gType" some or all
%                              of them are used. For "gType" set either as
%                              "Array" or "Dual", the first value is used
%                              as the length of the "Array" or distance
%                              between the two position in "Dual". When
%                              "gType" is "Single" this argument is
%                              ignored. [Default: ones(3, 1)].
% 
% nSens [numeric]  (Optional): This can be either a real integer value
%                              denoting the number of sensors in the array
%                              or a vector with three elements denoting
%                              the number of sensor along each Cartesian
%                              dimension. If a dimension is equal to zero
%                              the corresponding value is ignored. If this
%                              is a scalar value the used must ensure that
%                              it is correctly divisible over the non-zero
%                              dimensions of the geometry uniformly (see
%                              Notes for info). For the "Array" geometry,
%                              only the first value (if "nSens" is a
%                              vector) is used and for the "Single"
%                              geometry the argument is ignored.
%                              [Default: 10 * ones(3, 1)].
% 
% xOff [numeric] (Optional): X-axis offset. [Default: 0].
% 
% yOff [numeric] (Optional): Y-axis offset. [Default: 0].
% 
% zOff [numeric] (Optional): Z-axis offset. [Default: 0].
% 
% orient [numeric] (Optional): This is a real vector holding the rotation
%                              for the geometry in degrees, along each
%                              Cartesian axis. The rotations are performed
%                              clockwise. [Default: zeros(3, 1)].
% 
% --------------------------------------------------
% Output
% 
% vPos [numeric]: The matrix with the source coordinates in the Cartesian
%                 system. The matrix has dimensions Nx3, where N is the
%                 number of sources and the other dimension correspond to
%                 the x, y and z coodrinates.
% 
% vPosMesh [numeric]: The matrix has dimensions nSens(1)xnSens(2)x3. It
%                     holds the x, y and z Cartesian coordinates for each
%                     position of the "Grid" geometry. If the geometry is 
%                     otherwise not "Grid", this is an empty array.
% 
% --------------------------------------------------
% Notes
% 
% - Dependencies: rotMat3d(): To rotate the geometry.
% 
% - If the number of sources are provided as a single integer, the user
%   must make sure that they can be uniformly distributed in the non-zero
%   dimensions of the geometry. For example, for the "Cube" geometry with
%   two non-zero dimensions, the square root of the number of sensors must
%   be an integer and for three non-zero dimensions its third root must be
%   an integer. For the "Array" configuration, there is not an issue as the
%   setup is one dimensional.
% 
% --------------------------------------------------
function [vPos, vPosMesh] = virtMicGeo(gType, geoDim, nSens, xOff, yOff, zOff, orient)
    % ====================================================
    % Check for number of arguments
    % ====================================================
    narginchk(1, 7);
    nargoutchk(0, 2);

    % ====================================================
    % Validate input arguments
    % ====================================================
    % Validate mandatory arguments
    validateattributes(gType, {'char', 'string'}, {'scalartext', 'nonempty'}, mfilename, "Geometry type", 1);
    validatestring(gType, ["Single", "Dual", "Array", "Cube"], mfilename, "Geometry type", 1);

    % Validate optional arguments
    if nargin > 1 && ~isempty(geoDim) && ~strcmpi(gType, "Single")
        validateattributes(geoDim, "numeric", {'vector', 'real', 'nonnan', 'finite', 'nonempty', 'positive'}, mfilename, "Geometry dimensions", 2);
        
        if numel(geoDim) > 3 || numel(geoDim) == 2
            error("The dimensions argument must be either a scalar or a vector with three elements");
        end

        if isscalar(geoDim)
            geoDim = geoDim * ones(3, 1);
        end
    else
        geoDim = ones(3, 1);
    end

    if nargin > 2 && ~isempty(nSens) && sum(strcmpi(gType, ["Single", "Dual"])) == 0
        validateattributes(nSens, "numeric", {'vector', 'real', 'nonempty', 'nonnan', 'nonnegative', 'integer', 'finite'}, mfilename, "Number of sensors in the geometry", 3);

        if numel(nSens) > 3 || numel(nSens) == 2
            error("The number of sensors must be either a scalar or a vector with three elements");
        end
        
        if isscalar(nSens) && strcmpi(gType, "Cube")
            if mod(nthroot(nSens, sum(geoDim ~= 0)), 1) ~= 0
                error("The number of sources is cannot be divided uniformly over the non-zero dimensions");
            else
                nSens = nthroot(nSens, sum(geoDim ~= 0)) * double(geoDim ~= 0);
            end
        end
    elseif strcmpi(gType, "Dual")
        nSens = [2, 0, 0];
    else
        nSens = 10 * ones(3, 1);
    end

    if nargin > 3 && ~isempty(xOff)
        validateattributes(xOff, "numeric", {'scalar', 'real', 'nonnan', 'finite'}, mfilename, "X-offset", 4);
    else
        xOff = 0;
    end

    if nargin > 4 && ~isempty(yOff)
        validateattributes(yOff, "numeric", {'scalar', 'real', 'nonnan', 'finite'}, mfilename, "Y-offset", 5);
    else
        yOff = 0;
    end

    if nargin > 5 && ~isempty(zOff)
        validateattributes(zOff, "numeric", {'scalar', 'real', 'nonnan', 'finite'}, mfilename, "Z-offset", 6);
    else
        zOff = 0;
    end

    if nargin > 6 && ~isempty(orient) && ~strcmpi(gType, "Single")
        validateattributes(orient, {'numeric'}, {'vector', 'nonempty', 'nonnan', 'finite', 'real', 'numel', 3}, mfilename, "Geometry rotation around the Cartesian axes", 7);
    else
        orient = zeros(3, 1);
    end


    % ====================================================
    % Calculate virtual microphone positions
    % ====================================================
    switch lower(gType)
        case "single"
            % Create a single point at offset coordinates and return
            vPos = zeros(1, 3);
        case "dual"
            vPos = [-geoDim(1)/2, 0, 0; ...
                     geoDim(1)/2, 0, 0];
        case "array"
            vPos = linspace(-geoDim(1)/2, geoDim(1)/2, nSens(1));
            vPos = [vPos(:), zeros(numel(vPos), 2)];
        case "cube"
            % Make we don't get empty arrays when dimensions are 0
            if nSens(1) == 0
                x = 0;
            else
                x = linspace(-geoDim(1)/2, geoDim(1)/2, nSens(1));
            end

            if nSens(2) == 0
                y = 0;
            else
                y = linspace(-geoDim(2)/2, geoDim(2)/2, nSens(2));
            end

            if nSens(3) == 0
                z = 0;
            else
                z = linspace(-geoDim(3)/2, geoDim(3)/2, nSens(3));
            end

            [x, y, z] = meshgrid(x, y, z);
            vPos = [x(:), y(:), z(:)];
        otherwise
            error("Oops... something went wrong here... not known geometry...!!!");
    end

    % Rotate
    vPos = vPos * rotMat3d(-orient(1), -orient(2), -orient(3), "Degs");

    % Translate
    vPos = vPos + [xOff, yOff, zOff];

    % For "Cube" geometry provide the coordinates in a "mesh format"
    if nargout > 1 && strcmpi(gType, "Cube")
        vPosMesh = cat(3, x, y, z);
    elseif nargout > 1
        vPosMesh = [];
    end
end