Home Reference Source Repository

src/sofa/parseSofa.js

/**
 * @fileOverview Parser functions for SOFA files
 * @author Jean-Philippe.Lambert@ircam.fr
 * @copyright 2015 IRCAM, Paris, France
 * @license BSD-3-Clause
 */

/**
 * Parses a SOFA JSON string with into an object with `name`, `data` and
 * `metaData` attributes.
 *
 * @see {@link stringifySofa}
 *
 * @param {String} sofaString in SOFA JSON format
 * @returns {Object} with `data` and `metaData` attributes
 * @throws {Error} when the parsing fails
 */
export function parseSofa(sofaString) {
  try {
    const sofa = JSON.parse(sofaString);
    const sofaSet = {};

    sofaSet.name = sofa.name;

    if (typeof sofa.attributes !== 'undefined') {
      sofaSet.metaData = {};
      const metaData = sofa.attributes.find( (e) => {
        return e.name === 'NC_GLOBAL';
      });
      if (typeof metaData !== 'undefined') {
        metaData.attributes.forEach( (e) => {
          sofaSet.metaData[e.name] = e.value[0];
        });
      }
    }

    if (typeof sofa.leaves !== 'undefined') {
      const data = sofa.leaves;
      data.forEach( (d) => {
        sofaSet[d.name] = {};
        d.attributes.forEach( (a) => {
          sofaSet[d.name][a.name] = a.value[0];
        });
        sofaSet[d.name].shape = d.shape;
        sofaSet[d.name].data = d.data;
      });
    }

    return sofaSet;
  } catch (error) {
    throw new Error(`Unable to parse SOFA string. ${error.message}`);
  }
}

/**
 * Generates a SOFA JSON string from an object.
 *
 * Note that the properties differ from either an {@link HrtfSet} and from
 * the result of the parsing of a SOFA JSON. In particular, the listener
 * attributes correspond to the reference for the filters; the source
 * positions are the positions in the data-base.
 *
 * @see {@link parseSofa}
 * @see {@link HrtfSet}
 *
 * @param {Object} sofaSet
 * @param {Coordinates} sofaSet.ListenerPosition
 * @param {CoordinateSystem} sofaSet.ListenerPositionType
 * @param {Coordinates} sofaSet.ListenerUp
 * @param {CoordinateSystem} sofaSet.ListenerUpType
 * @param {Coordinates} sofaSet.ListenerView
 * @param {CoordinateSystem} sofaSet.ListenerViewType
 * @param {Array.<Array.<Number>>} sofaSet.SourcePosition
 * @param {CoordinateSystem} sofaSet.SourcePositionType
 * @param {Number} sofaSet.DataSamplingRate
 * @param {Array.<Array.<Array.<Number>>>} sofaSet.DataIR
 * @param {Array.<Number>} sofaSet.RoomVolume
 * @returns {String} in SOFA JSON format
 * @throws {Error} when the export fails, because of missing data or
 * unknown coordinate system
 */
export function stringifySofa(sofaSet) {
  const sofa = {};

  if (typeof sofaSet.name !== 'undefined') {
    sofa.name = sofaSet.name;
  }

  if (typeof sofaSet.metaData !== 'undefined') {
    sofa.attributes = [];
    const ncGlobal = {
      name: 'NC_GLOBAL',
      attributes: [],
    };

    for (const attribute in sofaSet.metaData) {
      if (sofaSet.metaData.hasOwnProperty(attribute) ) {
        ncGlobal.attributes.push( {
          name: attribute,
          value: [ sofaSet.metaData[attribute] ],
        } );
      }
    }

    sofa.attributes.push(ncGlobal);
  }

  // always the same;
  const type = 'Float64';

  let attributes;

  sofa.leaves = [];

  [
    ['ListenerPosition', 'ListenerPositionType'],
    ['ListenerUp', 'ListenerUpType'],
    ['ListenerView', 'ListenerViewType'],
  ].forEach( (listenerAttributeAndType) => {
    const listenerAttributeName = listenerAttributeAndType[0];
    const listenerAttribute = sofaSet[ listenerAttributeName ];
    const listenerType = sofaSet[listenerAttributeAndType[1] ];
    if (typeof listenerAttribute !== 'undefined') {
      switch (listenerType) {
        case 'cartesian':
          attributes = [
            { name: 'Type', value: ['cartesian'] },
            { name: 'Units', value: ['metre, metre, metre'] },
          ];
          break;

        case 'spherical':
          attributes = [
            { name: 'Type', value: ['spherical'] },
            { name: 'Units', value: ['degree, degree, metre'] },
          ];
          break;

        default:
          throw new Error(`Unknown coordinate system type ` +
                          `${listenerType} for ${listenerAttribute}`);
      }
      // in SOFA, everything is contained by an array, even an array.
      sofa.leaves.push({
        name: listenerAttributeName,
        type,
        attributes,
        shape: [1, 3],
        data: [listenerAttribute],
      });
    }
  });

  if (typeof sofaSet.SourcePosition !== 'undefined') {
    switch (sofaSet.SourcePositionType) {
      case 'cartesian':
        attributes = [
          { name: 'Type', value: ['cartesian'] },
          { name: 'Units', value: ['metre, metre, metre'] },
        ];
        break;

      case 'spherical':
        attributes = [
          { name: 'Type', value: ['spherical'] },
          { name: 'Units', value: ['degree, degree, metre'] },
        ];
        break;

      default:
        throw new Error(`Unknown coordinate system type ` +
                        `${sofaSet.SourcePositionType}`);
    }
    sofa.leaves.push({
      name: 'SourcePosition',
      type,
      attributes,
      shape: [
        sofaSet.SourcePosition.length,
        sofaSet.SourcePosition[0].length,
      ],
      data: sofaSet.SourcePosition,
    });
  }

  if (typeof sofaSet.DataSamplingRate !== 'undefined') {
    sofa.leaves.push({
      name: 'Data.SamplingRate',
      type,
      attributes: [ {name: 'Unit', value: 'hertz'} ],
      shape: [1],
      data: [sofaSet.DataSamplingRate],
    });
  } else {
    throw new Error(`No data sampling-rate`);
  }

  if (typeof sofaSet.DataDelay !== 'undefined') {
    sofa.leaves.push({
      name: 'Data.Delay',
      type,
      attributes: [],
      shape: [1, sofaSet.DataDelay.length],
      data: [sofaSet.DataDelay],
    });
  }

  if (typeof sofaSet.DataIR !== 'undefined') {
    sofa.leaves.push({
      name: 'Data.IR',
      type,
      attributes: [],
      shape: [
        sofaSet.DataIR.length,
        sofaSet.DataIR[0].length,
        sofaSet.DataIR[0][0].length,
      ],
      data: sofaSet.DataIR,
    });
  } else {
    throw new Error(`No data IR`);
  }

  if (typeof sofaSet.RoomVolume !== 'undefined') {
    sofa.leaves.push({
      name: 'RoomVolume',
      type,
      attributes: [ { name: 'Units', value: ['cubic metre'] } ],
      shape: [1],
      data: [sofaSet.RoomVolume],
    });
  }

  sofa.nodes = [];

  return JSON.stringify(sofa);
}

/**
 * Prefix SOFA coordinate system with `sofa`.
 *
 * @param {String} system : either `cartesian` or `spherical`
 * @returns {String} either `sofaCartesian` or `sofaSpherical`
 * @throws {Error} if system is unknown
 */
export function conformSofaCoordinateSystem(system) {
  let type;

  switch (system) {
    case ('cartesian'):
      type = 'sofaCartesian';
      break;

    case ('spherical'):
      type = 'sofaSpherical';
      break;

    default:
      throw new Error(`Bad SOFA type ${system}`);
  }
  return type;
}

export default {
  parseSofa,
  conformSofaCoordinateSystem,
};