Repository: pyfar/sofar (language: python)

Issue:
Road to sofar 1.0
**Progress**

- move SOFA conventions to single submodule. *sofa_conventions* now contains a complete definition of the SOFA conventions including test data in one place.
- Separate upgrading and verifying SOFA data in `Sofa.upgrade_convention` and `Sofa.verify`
- Add rules for upgrading conventions to sofa_conventions submodule
- Load SOFA conventions from new place sofaconventions.org/conventions
- Add private functions to check for congruency of conventions stored in SOFAtoolbox (GitHub) and on sofaconventions.org
- Improve verbosity of version information

**ToDo after code review**

- [x] tag sofa_conventions with v.2.1.0
- [x] make develop branch for sofa_conventions


Discussion / hints:

Fix patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 00000000..e0443744
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,207 @@
+# Use the latest 2.1 version of CircleCI pipeline process engine.
+# See: https://circleci.com/docs/2.0/configuration-reference
+version: 2.1
+
+# Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects.
+# See: https://circleci.com/docs/2.0/orb-intro/
+orbs:
+  # The python orb contains a set of prepackaged CircleCI configuration you can use repeatedly in your configuration files
+  # Orb commands and jobs help you with common scripting around a language/tool
+  # so you dont have to copy and paste it everywhere.
+  # See the orb documentation here: https://circleci.com/developer/orbs/orb/circleci/python
+  python: circleci/python@1.2
+
+
+executors:
+  python-docker: # declares a reusable executor
+    parameters:
+      version:
+        description: "version tag"
+        default: "latest"
+        type: string
+    docker:
+      - image: cimg/python:<<parameters.version>>
+
+# Define a job to be invoked later in a workflow.
+# See: https://circleci.com/docs/2.0/configuration-reference/#jobs
+jobs:
+  build_and_test:
+    parameters:
+      version:
+        description: "version tag"
+        default: "latest"
+        type: string
+    executor:
+      name: python-docker
+      version: <<parameters.version>>
+
+    steps:
+      - checkout
+      - python/install-packages:
+          pkg-manager: pip
+          # app-dir: ~/project/package-directory/  # If you're requirements.txt isn't in the root directory.
+          pip-dependency-file: requirements_dev.txt
+      - run:
+          name: Run tests
+          command: git submodule sync --recursive && git submodule update --recursive --init && pytest
+
+  flake:
+    parameters:
+      version:
+        description: "version tag"
+        default: "latest"
+        type: string
+    executor:
+      name: python-docker
+      version: <<parameters.version>>
+
+    steps:
+      - checkout
+      - python/install-packages:
+          pkg-manager: pip
+          # app-dir: ~/project/package-directory/  # If you're requirements.txt isn't in the root directory.
+          pip-dependency-file: requirements_dev.txt
+      - run:
+          name: Flake8
+          command: git submodule sync --recursive && git submodule update --recursive --init && flake8 sofar tests
+
+  test_documentation_build:
+    parameters:
+      version:
+        description: "version tag"
+        default: "latest"
+        type: string
+    executor:
+      name: python-docker
+      version: <<parameters.version>>
+
+    steps:
+      - checkout
+      - run:
+          name: Install System Dependencies
+          command: sudo apt-get update && sudo apt-get install -y texlive-latex-extra dvipng
+      - python/install-packages:
+          pkg-manager: pip
+          # app-dir: ~/project/package-directory/  # If you're requirements.txt isn't in the root directory.
+          pip-dependency-file: requirements_dev.txt
+      - run:
+          name: Sphinx
+          command: |
+            pip install -e .
+            cd docs/
+            make html SPHINXOPTS="-W"
+
+  test_pypi_publish:
+    parameters:
+      version:
+        description: "version tag"
+        default: "latest"
+        type: string
+    executor:
+      name: python-docker
+      version: <<parameters.version>>
+
+    steps:
+      - checkout
+      - python/install-packages:
+          pkg-manager: pip
+          # app-dir: ~/project/package-directory/  # If you're requirements.txt isn't in the root directory.
+          pip-dependency-file: requirements_dev.txt
+      - run:
+          name: deploy
+          command: |  # create whl, install twine and publish to Test PyPI
+            git submodule sync --recursive
+            git submodule update --recursive --init
+            python setup.py sdist bdist_wheel
+            twine upload dist/*
+
+# Invoke jobs via workflows
+# See: https://circleci.com/docs/2.0/configuration-reference/#workflows
+workflows:
+  test: # Test workflow
+    jobs:
+      # Run tests for all python versions
+      - build_and_test:
+          matrix:
+            parameters:
+              version:
+                - "3.8"
+                - "3.9"
+                - "3.10"
+      - flake:
+          matrix:
+            parameters:
+              version:
+                - "3.9"
+          requires:
+            - build_and_test
+
+      - test_documentation_build:
+          matrix:
+            parameters:
+              version:
+                - "3.9"
+          requires:
+            - build_and_test
+
+  test_and_publish:
+    # Test and publish on new git version tags
+    # This requires its own workflow to successfully trigger the test and build
+    jobs:
+      - build_and_test:
+          matrix:
+            parameters:
+              version:
+                - "3.8"
+                - "3.9"
+                - "3.10"
+          filters:
+            branches:
+              ignore: /.*/
+            # only act on version tags
+            tags:
+              only: /^v[0-9]+(\.[0-9]+)*$/
+
+      - flake:
+          matrix:
+            parameters:
+              version:
+                - "3.9"
+          requires:
+            - build_and_test
+          filters:
+            branches:
+              ignore: /.*/
+            # only act on version tags
+            tags:
+              only: /^v[0-9]+(\.[0-9]+)*$/
+
+      - test_documentation_build:
+          matrix:
+            parameters:
+              version:
+                - "3.9"
+          requires:
+            - build_and_test
+          filters:
+            branches:
+              ignore: /.*/
+            # only act on version tags
+            tags:
+              only: /^v[0-9]+(\.[0-9]+)*$/
+
+      - test_pypi_publish:
+          matrix:
+            parameters:
+              version:
+                - "3.9"
+          requires:
+            - build_and_test
+            - flake
+            - test_documentation_build
+          filters:
+            branches:
+              ignore: /.*/
+            # only act on version tags
+            tags:
+              only: /^v[0-9]+(\.[0-9]+)*$/
diff --git a/.gitignore b/.gitignore
index b02eed0f..9bfd4fa8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,7 +20,10 @@ develop-eggs
 lib
 lib64
 venv*/
+env/
 pyvenv*/
+.pytest_cache/
+**/__pycache__/
 
 # Installer logs
 pip-log.txt
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..d91dad24
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "sofar/sofa_conventions"]
+	path = sofar/sofa_conventions
+	url = https://github.com/pyfar/sofa_conventions.git
diff --git a/.readthedocs.yml b/.readthedocs.yml
index aa897706..049cde21 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -5,6 +5,11 @@
 # Required
 version: 2
 
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.10"
+
 # Build documentation in the docs/ directory with Sphinx
 sphinx:
   configuration: docs/conf.py
@@ -15,6 +20,5 @@ formats:
 
 # Optionally set the version of Python and requirements required to build your docs
 python:
-  version: 3.7
   install:
     - requirements: requirements_dev.txt
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index d215bd92..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-dist: bionic
-language: python
-python:
-- 3.7
-- 3.8
-- 3.9
-
-# Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
-install:
-- pip install -U pip
-- pip install -U tox-travis
-
-# Command to run tests, e.g. python setup.py test
-script:
-- tox
-
-# Deploy to PyPI
-deploy:
-  provider: pypi
-  username: __token__
-  password:
-    secure: J263wRkeZyqhp8suO85Q4rgiqY2Hfhtih3B8xkP7P7xIPtgT3HZ57gxTmE5UNRJC/N3S5khG+1yBbtBBhyEoS+Z3mqv8A392N/aL/gTxYPlRJ8iNMso6x9dmqwfF1F9l2QlYdwln9Q0KEJreRXgvy9hkdj+36R37LsgipJJesvzFgPyRXPuj2fj7bl5RpYy3klnuVNftlY0C1yZr3TA7LsUx7B4ATWQ+3+X29dCMMlqXxBRN9mUDR6moMBltpfUPkbr7CdJE9nfmsZZ9dH8EcAf9JginwyJ6HFDluaBGkOoP95lhcMz8h4TvIHix63fT1OszhWNgeXnkNPu3GWKjXLEW58xEMKsExJdq4NKCDFk2c0XJd2FVcaYwHynMjppXnZGp1j6kK/Dwoj6Xfy5ilTy1sgxFr5Cne0xBOTI4V7cWENjZy/AIaUzRcwRZpSDt81y5pXrHohBV3TyawfmJSQ735ZMOHjhnXk/PMgTobxvdhQug9Z+ZB6UeW5iwiSC6SjnxOLQCcBdms5c4f5E6jYgJbeDfAT6TOgQlB7HLcuNaFwR4FnO7qNLm/ksdF/Y85/3lKHeFqqm5vvIA2SRN/gLMfbJHpqze5VTN+xrl4qgRJNySydIinOJWqLZdP6vpyxvXcDRDGcMExNhXNv6DNrHKEpLx+jrq8kup22FFI10=
-  distributions: sdist bdist_wheel
-  skip_existing: true
-  skip_cleanup: true
-  on:
-    tags: true
diff --git a/AUTHORS.rst b/AUTHORS.rst
index c720cc3b..c60c1e6a 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -6,6 +6,7 @@ The sofar developers
 --------------------
 
 - Fabian Brinkmann
+- Marco Berzborn
 
 
 Funding
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 91bc85ee..b91fb3e5 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -52,10 +52,14 @@ Ready to contribute? Here's how to set up `sofar` for local development.
 1. Fork the `sofar` repo on GitHub.
 2. Clone your fork locally and cd into the sofar directory::
 
-    $ git clone https://github.com/pyfar/sofar.git
+    $ git clone --recursive https://github.com/pyfar/sofar.git
     $ cd sofar/
 
-3. Install your local copy into a virtualenv. Assuming you have Anaconda or Miniconda installed, this is how you set up your fork for local development::
+3. Note that some graphical Git interfaces can not do the recursive clone. If the folder sofar/sofa_conventions is empty try
+
+    $ git submodule update --init
+
+4. Install your local copy into a virtualenv. Assuming you have Anaconda or Miniconda installed, this is how you set up your fork for local development::
 
     $ conda create --name sofar python
     $ conda activate sofar
@@ -63,13 +67,13 @@ Ready to contribute? Here's how to set up `sofar` for local development.
     $ pip install -e .
     $ pip install -r requirements_dev.txt
 
-4. Create a branch for local development. Indicate the intention of your branch in its respective name (i.e. `feature/branch-name` or `bugfix/branch-name`)::
+5. Create a branch for local development. Indicate the intention of your branch in its respective name (i.e. `feature/branch-name` or `bugfix/branch-name`)::
 
     $ git checkout -b name-of-your-bugfix-or-feature
 
    Now you can make your changes locally.
 
-5. When you're done making changes, check that your changes pass flake8 and the
+6. When you're done making changes, check that your changes pass flake8 and the
    tests::
 
     $ flake8 sofar tests
@@ -77,13 +81,13 @@ Ready to contribute? Here's how to set up `sofar` for local development.
 
    flake8 test must pass without any warnings for `./sofar` and `./tests` using the default or a stricter configuration. Flake8 ignores `E123/E133, E226` and `E241/E242` by default. If necessary adjust the your flake8 and linting configuration in your IDE accordingly.
 
-6. Commit your changes and push your branch to GitHub::
+7. Commit your changes and push your branch to GitHub::
 
     $ git add .
     $ git commit -m "Your detailed description of your changes."
     $ git push origin name-of-your-bugfix-or-feature
 
-7. Submit a pull request through the GitHub website.
+8. Submit a pull request through the GitHub website.
 
 Pull Request Guidelines
 -----------------------
@@ -171,6 +175,14 @@ documentation. To show the warnings again use
 before building the documentation.
 
 
+Submodules
+~~~~~~~~~~
+
+To update the submodules containing the conventions and verification rules run
+
+$ git submodule update --remote sofar/sofa_conventions
+
+
 Deploying
 ~~~~~~~~~
 
@@ -185,11 +197,11 @@ Switch to main and run::
 
 $ bumpversion patch --verbose # possible: major / minor / patch
 
-Bumpversion will update all version strings, create and comitt tags by default
+Bumpversion will update all version strings, create and commit tags by default
 
 $ git push --follow-tags
 
-Travis will then deploy to PyPI if tests pass.
+Continuous integration will then deploy to PyPI if tests pass.
 
 - Merge main back into develop
 
diff --git a/HISTORY.rst b/HISTORY.rst
index de733e74..31c1ae66 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -1,6 +1,19 @@
 History
 =======
 
+1.0.0 (2022-12-16)
+------------------
+* Use SOFA conventions of version 2.1 from https://github.com/pyfar/sofa_conventions
+* Verify SOFA data against all rules defined in the SOFA standard AES69-2022
+* Add `Sofa.upgrade_convention` for upgrading outdated conventions. This now uses explicit upgrade rules from https://github.com/pyfar/sofa_conventions
+* Remove upgrade functionality from `Sofa.verify`, `sofar.write_sofa`, and `sofar.read_sofa` for a more clear separation of functionality
+* Add `Sofa.add_missing` to add missing default data to a SOFA object using the default values specified by the SOFA convention
+* Add default parameter value to `Sofa.info`
+* Make `sofar.update_conventions` a public function again
+* Improve documentation and verbosity of command line output
+* Add private function to check congruency of conventions stored as part of SOFAtoolbox and on sofaconventions.org
+* Move to Circle CI and improve testing
+
 0.3.1 (2022-03-21)
 ------------------
 * Improvement `sofar.read`: Files with unknown Convention versions can now be read by updating to the latest or a specific version.
diff --git a/MANIFEST.in b/MANIFEST.in
index 6c6c2efa..c0cc2fae 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -4,5 +4,9 @@ include HISTORY.rst
 include LICENSE
 include README.rst
 
-recursive-include sofar/conventions *.csv
-recursive-include sofar/conventions *.json
+include sofar/sofa_conventions/VERSION
+recursive-include sofar/sofa_conventions/conventions *.csv
+recursive-include sofar/sofa_conventions/conventions *.json
+recursive-include sofar/sofa_conventions/conventions/deprecated *.csv
+recursive-include sofar/sofa_conventions/conventions/deprecated *.json
+recursive-include sofar/sofa_conventions/rules *.json
diff --git a/README.rst b/README.rst
index 4774c30c..e5044deb 100644
--- a/README.rst
+++ b/README.rst
@@ -4,14 +4,16 @@ Readme
 
 Sofar is maybe the most complete Python package for the SOFA file format so
 far. SOFA files store spatially distributed acoustic data such as impulse
-responses or transfer functions. These are the sofar key features
+responses or transfer functions. They are defined by the AES69-2022 standard
+(see references). These are the key features of sofar
 
-* Based on the official SOFA conventions from the `Matlab/Octave API`_
-* Read and write support for SOFA files
+* Uses a complete definition of the AES69-2022 standard (see references) maintained at `sofa_conventions`_
+* Read, edit, and write SOFA files
 * Add custom attributes to SOFA files
-* Verify content of a SOFA file with respect to the data type and shape
+* Full Verification of the content of a SOFA files against AES69-2022
+* Upgrade data that uses outdated SOFA conventions
 * Open license allows unrestricted use
-* sofar is tested using continuous integration on Travis CI
+* sofar is tested using continuous integration on
 
 Installation
 ============
@@ -22,22 +24,36 @@ Use pip to install sofar
 
     $ pip install sofar
 
-(Requires Python >= 3.7)
+(Requires Python >= 3.8)
 
 Getting Started
 ===============
 
-Check out `read the docs`_ for example use cases and the complete
-documentation. Packages related to sofar are listed at `pyfar.org`_. For more
-information on the SOFA file format visit `sofaconventions.org`_.
+Check out `read the docs`_ for example use cases a quick introduction to SOFA
+and sofar, and the complete documentation. A more detailed introduction to SOFA
+is given by Majdak et. al. 2022 (see references below) Packages related to
+sofar are listed at `pyfar.org`_. For more information on the SOFA file format
+visit `sofaconventions.org`_.
 
 Contributing
 ============
 
 Refer to the `contribution guidelines`_ for more information.
 
-.. _Matlab/Octave API : https://github.com/sofacoustics/API_MO
+.. _sofa_conventions : https://github.com/pyfar/sofa_conventions
 .. _contribution guidelines: https://github.com/pyfar/sofar/blob/develop/CONTRIBUTING.rst
 .. _pyfar.org: https://pyfar.org
 .. _read the docs: https://sofar.readthedocs.io/en/latest
 .. _sofaconventions.org: https://sofaconventions.org
+
+References
+==========
+
+AES69-2022: *AES standard for file exchange - Spatial acoustic data file
+format*, Audio Engineering Society, Inc., New York, NY, USA.
+(https://www.aes.org/publications/standards/search.cfm?docID=99)
+
+P. Majdak, F. Zotter, F. Brinkmann, J. De Muynke, M. Mihocic, and M.
+Noisternig, "Spatially Oriented Format for Acoustics 2.1: Introduction and
+Recent Advances,” *J. Audio Eng. Soc.*, vol. 70, no. 7/8, pp. 565–584,
+Jul. 2022. DOI: https://doi.org/10.17743/jaes.2022.0026
diff --git a/docs/conf.py b/docs/conf.py
index 672deedb..cdf3f0cf 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -78,7 +78,7 @@
 #
 # This is also used if you do content translation via gettext catalogs.
 # Usually you set "language" from the command line for these cases.
-language = None
+language = 'en'
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
diff --git a/docs/examples.rst b/docs/examples.rst
index 17095504..015bceb8 100644
--- a/docs/examples.rst
+++ b/docs/examples.rst
@@ -171,7 +171,7 @@ Sofar handles this in two steps.
 2. Missing dimensions are appended when writing the SOFA object to disk.
 
 You should now fill all mandatory entries of the SOFA object if you were
-for real. For this is example we'll cut it here for the sake of brevity. Let
+for real. For this example we'll cut it here for the sake of brevity. Let
 us, however, delete an optional entry that we do not need at this point
 
 .. code-block:: python
@@ -197,16 +197,17 @@ your data can (most likely) be read by other applications.
 
 This will check the following
 
-- Are all mandatory fields contained? If not mandatory fields are added
-  with their default value and a warning is raised.
-- Are the names of variables and attributes in accordance to the SOFA
-  standard? If not a warning is raised.
+- Are all mandatory data contained?
+- Are the names of variables and attributes in accordance with the SOFA
+  standard?
 - Are the data types in accordance with the SOFA standard?
 - Are the dimensions of the variables consistent and in accordance
   to the SOFA standard?
 - Are the values of attributes consistent and in accordance to the
   SOFA standard?
 
+If any violations are detected, an error is raised.
+
 Reading and writing SOFA objects
 ================================
 
@@ -234,10 +235,24 @@ And to see that the written and read files contain the same data you can check
     sf.equals(sofa, sofa_read)
     >>> True
 
+Upgrading SOFA files
+====================
+
+SOFA conventions might get updates to fix bugs in the conventions, in case
+new conventions are introduced, or in case conventions get deprecated. To find
+out if SOFA data from a file is up to data load it and call
+
+.. code-block:: python
+
+    sofa.upgrade_convention()
+
+which will list upgrade choices or let you know that the convention is already up
+to date.
+
 Next steps
 ==========
 
-For detailed information about sofar refer to the :ref:`sofar_documentation`.
+For detailed information about sofar refer to the :ref:`sofar_SOFA` and :ref:`sofar_functions` documentation.
 For examples on how to work with the data inside SOFA files refer to :ref:`working_with_sofa`.
 
 
diff --git a/docs/index.rst b/docs/index.rst
index 166caebd..34b87e95 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -22,7 +22,8 @@ Documentation
 .. toctree::
    :maxdepth: 2
 
-   sofar
+   sofar.Sofa
+   sofar.functions
 
 Contributing
 ===============
diff --git a/docs/sofar.Sofa.rst b/docs/sofar.Sofa.rst
new file mode 100644
index 00000000..2a9187de
--- /dev/null
+++ b/docs/sofar.Sofa.rst
@@ -0,0 +1,14 @@
+.. _sofar_SOFA:
+
+SOFA objects
+============
+
+This section documents sofar SOFA objects. Functions that work on SOFA objects
+are described in the :ref:`sofar_functions` guide. For examples on how to use
+sofar refer to the :ref:`quick_tour`.
+
+.. autoclass:: sofar.Sofa
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :exclude-members:
diff --git a/docs/sofar.functions.rst b/docs/sofar.functions.rst
new file mode 100644
index 00000000..22994288
--- /dev/null
+++ b/docs/sofar.functions.rst
@@ -0,0 +1,14 @@
+.. _sofar_functions:
+
+sofar functions
+===============
+
+This section documents general functions of the sofar package. Handling data in
+SOFA objects is described in :ref:`sofar_SOFA`. For examples on how to use sofa
+refer to the :ref:`quick_tour`.
+
+.. automodule:: sofar
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :exclude-members: Sofa
diff --git a/docs/sofar.rst b/docs/sofar.rst
deleted file mode 100644
index 6aa246f3..00000000
--- a/docs/sofar.rst
+++ /dev/null
@@ -1,13 +0,0 @@
-.. _sofar_documentation:
-
-sofar documentation
-===================
-
-This is the official sofar documentation. For examples on how to use sofa
-refer to the :ref:`quick_tour`.
-
-.. automodule:: sofar.sofar
-   :members:
-   :undoc-members:
-   :show-inheritance:
-   :exclude-members: update_conventions
diff --git a/docs/working_with_sofa_files.rst b/docs/working_with_sofa_files.rst
index c049dca4..b3dfddd1 100644
--- a/docs/working_with_sofa_files.rst
+++ b/docs/working_with_sofa_files.rst
@@ -116,7 +116,7 @@ formatting
 Next steps
 ==========
 
-For detailed information about sofar refer to the :ref:`sofar_documentation`.
+For detailed information about sofar refer to the :ref:`sofar_SOFA` and :ref:`sofar_functions` documentation.
 Pyfar also offers methods for digital signal processing that wont be detailed
 here. A god way to dive into that is the
 `pyfar documentation <https://pyfar.readthedocs.io/en/latest/>`_ and the
diff --git a/setup.cfg b/setup.cfg
index d2694e77..016ea55b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -19,6 +19,3 @@ exclude = .git, .tox, docs
 
 [aliases]
 test = pytest
-
-[tool:pytest]
-norecursedirs = private
diff --git a/setup.py b/setup.py
index 0b9768aa..f6958216 100644
--- a/setup.py
+++ b/setup.py
@@ -42,9 +42,9 @@
         'License :: OSI Approved :: MIT License',
         'Natural Language :: English',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
-        'Programming Language :: Python :: 3.9'
+        'Programming Language :: Python :: 3.9',
+        'Programming Language :: Python :: 3.10'
     ],
     description="Maybe the most complete python package for SOFA files so far",
     install_requires=requirements,
@@ -57,8 +57,14 @@
     setup_requires=setup_requirements,
     test_suite='tests',
     tests_require=test_requirements,
-    url='https://github.com/pyfar/sofar',
+    url="https://pyfar.org/",
+    download_url="https://pypi.org/project/sofar/",
+    project_urls={
+        "Bug Tracker": "https://github.com/pyfar/sofar/issues",
+        "Documentation": "https://sofar.readthedocs.io/",
+        "Source Code": "https://github.com/pyfar/sofar",
+    },
     version='0.3.1',
     zip_safe=False,
-    python_requires='>=3.7'
+    python_requires='>=3.8'
 )
diff --git a/sofar/__init__.py b/sofar/__init__.py
index d3250350..93dd459f 100644
--- a/sofar/__init__.py
+++ b/sofar/__init__.py
@@ -6,16 +6,21 @@
 __email__ = 'info@pyfar.org'
 __version__ = '0.3.1'
 
+from .sofa import Sofa
 
-from .sofar import (Sofa,
-                    list_conventions,
-                    read_sofa,
-                    write_sofa,
-                    equals)
+from .io import read_sofa, write_sofa
+
+from .utils import (list_conventions,
+                    equals,
+                    version)
+
+from .update_conventions import update_conventions
 
 
 __all__ = ['Sofa',
+           'update_conventions',
            'list_conventions',
            'read_sofa',
            'write_sofa',
-           'equals']
+           'equals',
+           'version']
diff --git a/sofar/conventions/FreeFieldDirectivityTF_1.0.json b/sofar/conventions/FreeFieldDirectivityTF_1.0.json
deleted file mode 100644
index a85cb588..00000000
--- a/sofar/conventions/FreeFieldDirectivityTF_1.0.json
+++ /dev/null
@@ -1,437 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "2.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "FreeFieldDirectivityTF",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "This conventions stores directivities of acoustic sources (instruments, loudspeakers, singers, talkers, etc) in the frequency domain for multiple musical notes in free field."
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "TF",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "We store frequency-dependent data here"
-    },
-    "GLOBAL:RoomType": {
-        "default": "free field",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "The room information can be arbitrary, but the spatial setup assumes free field."
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DatabaseName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Name of the database. Used for classification of the data"
-    },
-    "GLOBAL:Musician": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Narrative description of the musician such as position, behavior, or personal data if not data-protected, e.g., 'Christiane Schmidt sitting on the chair', or 'artificial excitation by R2D2'."
-    },
-    "GLOBAL:Description": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Narrative description of a measurement. For musical instruments/singers, the note (C1, D1, etc) or the dynamic (pp., ff., etc), or the string played, the playing style (pizzicato, legato, etc.), or the type of excitation (e.g., hit location of a cymbal). For loudspeakers, the system and driver units."
-    },
-    "GLOBAL:SourceType": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Narrative description of the acoustic source, e.g., 'Violin', 'Female singer', or '2-way loudspeaker'"
-    },
-    "GLOBAL:SourceManufacturer": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Narrative description of the manufacturer of the source, e.g., 'Stradivari, Lady Blunt, 1721' or 'LoudspeakerCompany'"
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "Position of the microphone array during the measurements."
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "Orientation of the microphone array"
-    },
-    "ListenerView:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "Up vector of the microphone array"
-    },
-    "ReceiverPosition": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, RC, RCM",
-        "type": "double",
-        "comment": "Positions of the microphones during the measurements (relative to the Listener)"
-    },
-    "ReceiverPosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "Position of the acoustic source (instrument)"
-    },
-    "SourcePosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Reference": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Narrative description of the spatial reference of the source position, e.g., for the trumpet, 'The bell'. Mandatory in order to provide a reference across different instruments"
-    },
-    "SourceView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "Orientation of the acoustic source (instrument)"
-    },
-    "SourceView:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourceView:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourceView:Reference": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Narrative description of the spatial reference of the source view, e.g., for the trumpet, 'Viewing direction of the bell'. Mandatory in order to provide a reference across different instruments"
-    },
-    "SourceUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "Up vector of the acoustic source (instrument)"
-    },
-    "SourceUp:Reference": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Narrative description of the spatial reference of the source up, e.g., for the trumpet, 'Along the keys, keys up'. Mandatory in order to provide a reference across different instruments"
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "A more detailed structure of the Source. In a simple settings, a single Emitter is considered that is collocated with the source."
-    },
-    "EmitterPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterDescription": {
-        "default": [
-            ""
-        ],
-        "flags": null,
-        "dimensions": "IS, MS",
-        "type": "string",
-        "comment": "A more detailed structure of the source. In a simple setting, a single Emitter is considered that is collocated with the source. In a more complicated setting, this may be the strings of a violin or the units of a loudspeaker."
-    },
-    "MIDINote": {
-        "default": 0,
-        "flags": null,
-        "dimensions": "I, M",
-        "type": "double",
-        "comment": "Defines the note played by the source during the measurement. The note is specified a MIDI note by the [https://www.midi.org/specifications-old/item/the-midi-1-0-specification MIDI specifications, version 1.0]. Not mandatory, but recommended for tonal instruments."
-    },
-    "Description": {
-        "default": [
-            ""
-        ],
-        "flags": null,
-        "dimensions": "MS",
-        "type": "string",
-        "comment": "This variable is used when the description varies with M."
-    },
-    "SourceTuningFrequency": {
-        "default": 440,
-        "flags": null,
-        "dimensions": "I, M",
-        "type": "double",
-        "comment": "Frequency (in Hertz) to which a musical instrument is tuned to corresponding to the note A4 (MIDINote=69). Recommended for tonal instruments."
-    },
-    "N": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "N",
-        "type": "double",
-        "comment": "Frequency values"
-    },
-    "N:LongName": {
-        "default": "frequency",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "N:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Units used for N"
-    },
-    "Data.Real": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "mrn",
-        "type": "double",
-        "comment": "Real part of the complex spectrum. The default value 0 indicates that all data fields are initialized with zero values."
-    },
-    "Data.Imag": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "MRN",
-        "type": "double",
-        "comment": "Imaginary part of the complex spectrum"
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/FreeFieldHRIR_1.0.json b/sofar/conventions/FreeFieldHRIR_1.0.json
deleted file mode 100644
index c8fe7178..00000000
--- a/sofar/conventions/FreeFieldHRIR_1.0.json
+++ /dev/null
@@ -1,333 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "2.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "FreeFieldHRIR",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "An extension of SimpleFreeFieldHRIR in order to consider more complex data sets described in spatially continuous representation. Each HRTF direction corresponds to an emitter, and a consistent measurement for a single listener and all directions is described by a set of the emitter positions surrounding the listener."
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "FIR-E",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ListenerShortName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "ID of the subject from the database"
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomType": {
-        "default": "free field",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DatabaseName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Name of the database to which these data belong"
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            [
-                0,
-                0.09,
-                0
-            ],
-            [
-                0,
-                -0.09,
-                0
-            ]
-        ],
-        "flags": "m",
-        "dimensions": "RCI, RCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "Source position is assumed to be the ListenerPosition in order to reflect Emitters surrounding the Listener"
-    },
-    "SourcePosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, ECI, ECM",
-        "type": "double",
-        "comment": "Radius in 'spherical harmonics', Position in 'cartesian' and 'spherical'"
-    },
-    "EmitterPosition:Type": {
-        "default": "spherical harmonics",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Can be 'spherical harmonics', 'cartesian', or 'spherical'"
-    },
-    "EmitterPosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.IR": {
-        "default": [
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "mrne",
-        "type": "double",
-        "comment": ""
-    },
-    "Data.SamplingRate": {
-        "default": 48000,
-        "flags": "m",
-        "dimensions": "I, M",
-        "type": "double",
-        "comment": ""
-    },
-    "Data.SamplingRate:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.Delay": {
-        "default": [
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IRE, MRE",
-        "type": "double",
-        "comment": "Additional delay of each IR (in samples)"
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/FreeFieldHRTF_1.0.json b/sofar/conventions/FreeFieldHRTF_1.0.json
deleted file mode 100644
index 77247dc4..00000000
--- a/sofar/conventions/FreeFieldHRTF_1.0.json
+++ /dev/null
@@ -1,340 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "2.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "FreeFieldHRTF",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "This conventions is for HRTFs created under conditions where room information is irrelevant and stored as SH coefficients"
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "TF-E",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ListenerShortName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "ID of the subject from the database"
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomType": {
-        "default": "free field",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DatabaseName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Name of the database to which these data belong"
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            [
-                0,
-                0.09,
-                0
-            ],
-            [
-                0,
-                -0.09,
-                0
-            ]
-        ],
-        "flags": "m",
-        "dimensions": "RCI, RCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "Source position is assumed to be the ListenerPosition in order to reflect Emitters surrounding the Listener"
-    },
-    "SourcePosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, ECI, ECM",
-        "type": "double",
-        "comment": "Radius in 'spherical harmonics', Position in 'cartesian' and 'spherical'"
-    },
-    "EmitterPosition:Type": {
-        "default": "spherical harmonics",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Can be 'spherical harmonics', 'cartesian', or 'spherical'"
-    },
-    "EmitterPosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "N": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "N",
-        "type": "double",
-        "comment": ""
-    },
-    "N:LongName": {
-        "default": "frequency",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "N:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.Real": {
-        "default": [
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "mrne",
-        "type": "double",
-        "comment": ""
-    },
-    "Data.Imag": {
-        "default": [
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "MRNE",
-        "type": "double",
-        "comment": ""
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/GeneralFIR-E_2.0.json b/sofar/conventions/GeneralFIR-E_2.0.json
deleted file mode 100644
index 05f53119..00000000
--- a/sofar/conventions/GeneralFIR-E_2.0.json
+++ /dev/null
@@ -1,270 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "2.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "GeneralFIR-E",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "This conventions stores IRs for general purposes, i.e., only the mandatory, SOFA general metadata are pre-defined"
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "2.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "FIR-E",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "We use FIR datatype which in addition depends on Emitters (E)"
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomType": {
-        "default": "free field",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "The room information can be arbitrary"
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, RC, RCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "SourcePosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, EC, ECM",
-        "type": "double",
-        "comment": "Each speaker is represented as an emitter. Use EmitterPosition to represent the position of a particular speaker. Size of EmitterPosition determines E"
-    },
-    "EmitterPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.IR": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "mrne",
-        "type": "double",
-        "comment": "Impulse responses"
-    },
-    "Data.SamplingRate": {
-        "default": 48000,
-        "flags": "m",
-        "dimensions": "I, M",
-        "type": "double",
-        "comment": "Sampling rate of the samples in Data.IR and Data.Delay"
-    },
-    "Data.SamplingRate:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Unit of the sampling rate"
-    },
-    "Data.Delay": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "IRE, MRE",
-        "type": "double",
-        "comment": "Additional delay of each IR (in samples)"
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/GeneralFIRE_1.0.json b/sofar/conventions/GeneralFIRE_1.0.json
deleted file mode 100644
index e68b85e2..00000000
--- a/sofar/conventions/GeneralFIRE_1.0.json
+++ /dev/null
@@ -1,270 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "GeneralFIRE",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "This conventions stores IRs for general purposes, i.e., only the mandatory, SOFA general metadata are pre-defined"
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "FIRE",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "We use FIR datatype which in addition depends on Emitters (E)"
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomType": {
-        "default": "free field",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "The room information can be arbitrary"
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "rCI, rCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "SourcePosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "eCI, eCM",
-        "type": "double",
-        "comment": "Each speaker is represented as an emitter. Use EmitterPosition to represent the position of a particular speaker. Size of EmitterPosition determines E"
-    },
-    "EmitterPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.IR": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "mREn",
-        "type": "double",
-        "comment": "Impulse responses"
-    },
-    "Data.SamplingRate": {
-        "default": 48000,
-        "flags": "m",
-        "dimensions": "I",
-        "type": "double",
-        "comment": "Sampling rate of the samples in Data.IR and Data.Delay"
-    },
-    "Data.SamplingRate:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Unit of the sampling rate"
-    },
-    "Data.Delay": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "IRE, MRE",
-        "type": "double",
-        "comment": "Additional delay of each IR (in samples)"
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/GeneralFIR_1.0.json b/sofar/conventions/GeneralFIR_1.0.json
deleted file mode 100644
index 9ad776c7..00000000
--- a/sofar/conventions/GeneralFIR_1.0.json
+++ /dev/null
@@ -1,295 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "GeneralFIR",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "This conventions stores IRs for general purposes, i.e., only the mandatory, SOFA general metadata are pre-defined"
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "FIR",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "We store IRs here"
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomType": {
-        "default": "free field",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "The room information can be arbitrary"
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "rCI, rCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "In order to store different directions/positions around the listener, SourcePosition is assumed to vary"
-    },
-    "SourcePosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "eCI, eCM",
-        "type": "double",
-        "comment": ""
-    },
-    "EmitterPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": null,
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView:Type": {
-        "default": "cartesian",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView:Units": {
-        "default": "metre",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.IR": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "mRn",
-        "type": "double",
-        "comment": "Impulse responses"
-    },
-    "Data.SamplingRate": {
-        "default": 48000,
-        "flags": "m",
-        "dimensions": "I",
-        "type": "double",
-        "comment": "Sampling rate of the samples in Data.IR and Data.Delay"
-    },
-    "Data.SamplingRate:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Unit of the sampling rate"
-    },
-    "Data.Delay": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "IR, MR",
-        "type": "double",
-        "comment": "Additional delay of each IR (in samples)"
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/GeneralFIR_2.0.json b/sofar/conventions/GeneralFIR_2.0.json
deleted file mode 100644
index 15f37911..00000000
--- a/sofar/conventions/GeneralFIR_2.0.json
+++ /dev/null
@@ -1,295 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "2.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "GeneralFIR",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "This conventions stores IRs for general purposes, i.e., only the mandatory, SOFA general metadata are pre-defined"
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "2.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "FIR",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "We store IRs here"
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomType": {
-        "default": "free field",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "The room information can be arbitrary"
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, RC, RCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "In order to store different directions/positions around the listener, SourcePosition is assumed to vary"
-    },
-    "SourcePosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "eCI, eCM",
-        "type": "double",
-        "comment": ""
-    },
-    "EmitterPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": null,
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView:Type": {
-        "default": "cartesian",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView:Units": {
-        "default": "metre",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.IR": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "mrn",
-        "type": "double",
-        "comment": "Impulse responses"
-    },
-    "Data.SamplingRate": {
-        "default": 48000,
-        "flags": "m",
-        "dimensions": "I, M",
-        "type": "double",
-        "comment": "Sampling rate of the samples in Data.IR and Data.Delay"
-    },
-    "Data.SamplingRate:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Unit of the sampling rate"
-    },
-    "Data.Delay": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "IR, MR",
-        "type": "double",
-        "comment": "Additional delay of each IR (in samples)"
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/GeneralTF-E_1.0.json b/sofar/conventions/GeneralTF-E_1.0.json
deleted file mode 100644
index 8cd2afcb..00000000
--- a/sofar/conventions/GeneralTF-E_1.0.json
+++ /dev/null
@@ -1,277 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "2.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "GeneralTF-E",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "This conventions stores TFs depending in the Emiiter for general purposes, i.e., only the mandatory, SOFA general metadata are pre-defined. This convention is based on GeneralTF"
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "TF-E",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "We store frequency-dependent data depending on the emitter here"
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomType": {
-        "default": "free field",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "The room information can be arbitrary"
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, RC, RCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "In order to store different directions/positions around the listener, SourcePosition is assumed to vary"
-    },
-    "SourcePosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, EC, ECM",
-        "type": "double",
-        "comment": ""
-    },
-    "EmitterPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "N": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "N",
-        "type": "double",
-        "comment": "Frequency values"
-    },
-    "N:LongName": {
-        "default": "frequency",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "N:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Unit of the values given in N"
-    },
-    "Data.Real": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "mrne",
-        "type": "double",
-        "comment": "The real part of the complex spectrum"
-    },
-    "Data.Imag": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "MRNE",
-        "type": "double",
-        "comment": "The imaginary part of the complex spectrum"
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/GeneralTF_1.0.json b/sofar/conventions/GeneralTF_1.0.json
deleted file mode 100644
index d92f6d46..00000000
--- a/sofar/conventions/GeneralTF_1.0.json
+++ /dev/null
@@ -1,277 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "GeneralTF",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "This conventions stores TFs for general purposes, i.e., only the mandatory, SOFA general metadata are pre-defined. This convention is based on GeneralFIR."
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "TF",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "We store frequency-dependent data here"
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomType": {
-        "default": "free field",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "The room information can be arbitrary"
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "rCI, rCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "In order to store different directions/positions around the listener, SourcePosition is assumed to vary"
-    },
-    "SourcePosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "eCI, eCM",
-        "type": "double",
-        "comment": ""
-    },
-    "EmitterPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "N": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "N",
-        "type": "double",
-        "comment": "Frequency values"
-    },
-    "N:LongName": {
-        "default": "frequency",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "N:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Unit of the values given in N"
-    },
-    "Data.Real": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "mRn",
-        "type": "double",
-        "comment": "The real part of the complex spectrum"
-    },
-    "Data.Imag": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "MRN",
-        "type": "double",
-        "comment": "The imaginary part of the complex spectrum"
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/GeneralTF_2.0.json b/sofar/conventions/GeneralTF_2.0.json
deleted file mode 100644
index 875826db..00000000
--- a/sofar/conventions/GeneralTF_2.0.json
+++ /dev/null
@@ -1,277 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "GeneralTF",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "This conventions stores TFs for general purposes, i.e., only the mandatory, SOFA general metadata are pre-defined. This convention is based on GeneralFIR."
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "2.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "TF",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "We store frequency-dependent data here"
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomType": {
-        "default": "free field",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "The room information can be arbitrary"
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, RC, RCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "In order to store different directions/positions around the listener, SourcePosition is assumed to vary"
-    },
-    "SourcePosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "eCI, eCM",
-        "type": "double",
-        "comment": ""
-    },
-    "EmitterPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "N": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "N",
-        "type": "double",
-        "comment": "Frequency values"
-    },
-    "N:LongName": {
-        "default": "frequency",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "N:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Unit of the values given in N"
-    },
-    "Data.Real": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "mrn",
-        "type": "double",
-        "comment": "The real part of the complex spectrum"
-    },
-    "Data.Imag": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "MRN",
-        "type": "double",
-        "comment": "The imaginary part of the complex spectrum"
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/MultiSpeakerBRIR_0.3.json b/sofar/conventions/MultiSpeakerBRIR_0.3.json
deleted file mode 100644
index 3304f531..00000000
--- a/sofar/conventions/MultiSpeakerBRIR_0.3.json
+++ /dev/null
@@ -1,376 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "MultiSpeakerBRIR",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "This convention is for BRIRs recorded in reverberant conditions from multiple loudspeaker sources at a number of listener orientations."
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "0.3",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "FIRE",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "We use FIR datatype which in addition depends on Emitters (E)"
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomType": {
-        "default": "reverberant",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DatabaseName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "name of the database to which these data belong"
-    },
-    "GLOBAL:ListenerShortName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "ID of the subject from the database"
-    },
-    "GLOBAL:RoomDescription": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "narrative description of the room"
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            [
-                0,
-                0.09,
-                0
-            ],
-            [
-                0,
-                -0.09,
-                0
-            ]
-        ],
-        "flags": "m",
-        "dimensions": "rCI, rCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "SourcePosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "eCI, eCM",
-        "type": "double",
-        "comment": "Each speaker is represented as an emitter. Use EmitterPosition to represent the position of a particular speaker. Size of EmitterPosition determines E"
-    },
-    "EmitterPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": null,
-        "dimensions": "ECI, ECM",
-        "type": "double",
-        "comment": "When EmitterUp provided, EmitterView must be provided as well"
-    },
-    "EmitterView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": null,
-        "dimensions": "ECI, ECM",
-        "type": "double",
-        "comment": "When EmitterView provided, EmitterUp must be provided as well"
-    },
-    "EmitterView:Type": {
-        "default": "cartesian",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterView:Units": {
-        "default": "metre",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.IR": {
-        "default": [
-            1,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "mREn",
-        "type": "double",
-        "comment": ""
-    },
-    "Data.SamplingRate": {
-        "default": 48000,
-        "flags": "m",
-        "dimensions": "I",
-        "type": "double",
-        "comment": ""
-    },
-    "Data.SamplingRate:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.Delay": {
-        "default": [
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IRE, MRE",
-        "type": "double",
-        "comment": ""
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/SimpleFreeFieldHRIR_1.0.json b/sofar/conventions/SimpleFreeFieldHRIR_1.0.json
deleted file mode 100644
index 5a271843..00000000
--- a/sofar/conventions/SimpleFreeFieldHRIR_1.0.json
+++ /dev/null
@@ -1,369 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "2.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "SimpleFreeFieldHRIR",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "This convention set is for HRIRs recorded under free-field conditions or other IRs created under conditions where room information is irrelevant"
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "FIR",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomType": {
-        "default": "free field",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DatabaseName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "name of the database to which these data belong"
-    },
-    "GLOBAL:ListenerShortName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "ID of the subject from the database"
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            [
-                0,
-                0.09,
-                0
-            ],
-            [
-                0,
-                -0.09,
-                0
-            ]
-        ],
-        "flags": "m",
-        "dimensions": "rCI, rCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "Source position is assumed to vary for different directions/positions around the listener"
-    },
-    "SourcePosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "eCI, eCM",
-        "type": "double",
-        "comment": ""
-    },
-    "EmitterPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourceUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": null,
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "SourceView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": null,
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "SourceView:Type": {
-        "default": "cartesian",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourceView:Units": {
-        "default": "metre",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.IR": {
-        "default": [
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "mRn",
-        "type": "double",
-        "comment": ""
-    },
-    "Data.SamplingRate": {
-        "default": 48000,
-        "flags": "m",
-        "dimensions": "I, M",
-        "type": "double",
-        "comment": ""
-    },
-    "Data.SamplingRate:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.Delay": {
-        "default": [
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IR, MR",
-        "type": "double",
-        "comment": ""
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/SimpleFreeFieldHRSOS_1.0.json b/sofar/conventions/SimpleFreeFieldHRSOS_1.0.json
deleted file mode 100644
index 354aa02d..00000000
--- a/sofar/conventions/SimpleFreeFieldHRSOS_1.0.json
+++ /dev/null
@@ -1,349 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "2.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "SimpleFreeFieldHRSOS",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "This convention set follows SimpleFreeFieldHRIR but the data is stored as second-order section (SOS) coefficients."
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "SOS",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Filters described as second-order section (SOS) coefficients"
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomType": {
-        "default": "free field",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DatabaseName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "name of the database to which these data belong"
-    },
-    "GLOBAL:ListenerShortName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "ID of the subject from the database"
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            [
-                0,
-                0.09,
-                0
-            ],
-            [
-                0,
-                -0.09,
-                0
-            ]
-        ],
-        "flags": "m",
-        "dimensions": "rCI, rCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "Source position is assumed to vary for different directions/positions around the listener"
-    },
-    "SourcePosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "eCI, eCM",
-        "type": "double",
-        "comment": ""
-    },
-    "EmitterPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.SOS": {
-        "default": [
-            [
-                [
-                    0,
-                    0,
-                    0,
-                    1,
-                    0,
-                    0
-                ],
-                [
-                    0,
-                    0,
-                    0,
-                    1,
-                    0,
-                    0
-                ]
-            ]
-        ],
-        "flags": "m",
-        "dimensions": "mRn",
-        "type": "double",
-        "comment": "Filter coefficients as SOS coefficients."
-    },
-    "Data.SamplingRate": {
-        "default": 48000,
-        "flags": "m",
-        "dimensions": "I, M",
-        "type": "double",
-        "comment": "Sampling rate of the coefficients in Data.SOS and the delay in Data.Delay"
-    },
-    "Data.SamplingRate:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.Delay": {
-        "default": [
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IR, MR",
-        "type": "double",
-        "comment": "Broadband delay (in samples resulting from SamplingRate)"
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/SimpleFreeFieldHRTF_2.0.json b/sofar/conventions/SimpleFreeFieldHRTF_2.0.json
deleted file mode 100644
index e6438b72..00000000
--- a/sofar/conventions/SimpleFreeFieldHRTF_2.0.json
+++ /dev/null
@@ -1,340 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "2.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "SimpleFreeFieldHRTF",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "This conventions is for HRTFs created under conditions where room information is irrelevant"
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "2.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "TF",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ListenerShortName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "ID of the subject from the database"
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomType": {
-        "default": "free field",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DatabaseName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "name of the database to which these data belong"
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            [
-                0,
-                0.09,
-                0
-            ],
-            [
-                0,
-                -0.09,
-                0
-            ]
-        ],
-        "flags": "m",
-        "dimensions": "rCI, rCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "Source position is assumed to vary for different directions/positions around the listener"
-    },
-    "SourcePosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "eCI, eCM",
-        "type": "double",
-        "comment": ""
-    },
-    "EmitterPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "N": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "N",
-        "type": "double",
-        "comment": ""
-    },
-    "N:LongName": {
-        "default": "frequency",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "N:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.Real": {
-        "default": [
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "mRn",
-        "type": "double",
-        "comment": ""
-    },
-    "Data.Imag": {
-        "default": [
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "MRN",
-        "type": "double",
-        "comment": ""
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/SimpleFreeFieldSOS_1.0.json b/sofar/conventions/SimpleFreeFieldSOS_1.0.json
deleted file mode 100644
index b9c522e7..00000000
--- a/sofar/conventions/SimpleFreeFieldSOS_1.0.json
+++ /dev/null
@@ -1,349 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "SimpleFreeFieldSOS",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "This convention set follows SimpleFreeFieldHRIR but the data is stored as second-order section (SOS) coefficients."
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "SOS",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Filters described as second-order section (SOS) coefficients"
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomType": {
-        "default": "free field",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DatabaseName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "name of the database to which these data belong"
-    },
-    "GLOBAL:ListenerShortName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "ID of the subject from the database"
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            [
-                0,
-                0.09,
-                0
-            ],
-            [
-                0,
-                -0.09,
-                0
-            ]
-        ],
-        "flags": "m",
-        "dimensions": "rCI, rCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "Source position is assumed to vary for different directions/positions around the listener"
-    },
-    "SourcePosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "eCI, eCM",
-        "type": "double",
-        "comment": ""
-    },
-    "EmitterPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.SOS": {
-        "default": [
-            [
-                [
-                    0,
-                    0,
-                    0,
-                    1,
-                    0,
-                    0
-                ],
-                [
-                    0,
-                    0,
-                    0,
-                    1,
-                    0,
-                    0
-                ]
-            ]
-        ],
-        "flags": "m",
-        "dimensions": "mRn",
-        "type": "double",
-        "comment": "Filter coefficients as SOS coefficients."
-    },
-    "Data.SamplingRate": {
-        "default": 48000,
-        "flags": "m",
-        "dimensions": "I",
-        "type": "double",
-        "comment": "Sampling rate of the coefficients in Data.SOS and the delay in Data.Delay"
-    },
-    "Data.SamplingRate:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.Delay": {
-        "default": [
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IR, MR",
-        "type": "double",
-        "comment": "Broadband delay (in samples resulting from SamplingRate)"
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/SimpleHeadphoneIR_1.0.json b/sofar/conventions/SimpleHeadphoneIR_1.0.json
deleted file mode 100644
index 51f62821..00000000
--- a/sofar/conventions/SimpleHeadphoneIR_1.0.json
+++ /dev/null
@@ -1,396 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "2.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "SimpleHeadphoneIR",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Conventions for IRs with a 1-to-1 correspondence between emitter and receiver. The main application for this convention is to store headphone IRs recorded for each emitter and each ear."
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "FIR",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "We will store IRs here"
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomType": {
-        "default": "free field",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Room type is not relevant here"
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DatabaseName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Correspondence to a database"
-    },
-    "GLOBAL:ListenerShortName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Correspondence to a subject from the database"
-    },
-    "GLOBAL:ListenerDescription": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Narrative description of the listener (or mannequin)"
-    },
-    "GLOBAL:SourceDescription": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Narrative description of the headphones"
-    },
-    "GLOBAL:SourceManufacturer": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Name of the headphones manufacturer"
-    },
-    "GLOBAL:SourceModel": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Name of the headphone model. Must uniquely describe the headphones of the manufacturer"
-    },
-    "GLOBAL:SourceURI": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "URI of the headphone specifications"
-    },
-    "GLOBAL:ReceiverDescription": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Narrative description of the microphones"
-    },
-    "GLOBAL:EmitterDescription": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Narrative description of the headphone drivers"
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            [
-                0,
-                0.09,
-                0
-            ],
-            [
-                0,
-                -0.09,
-                0
-            ]
-        ],
-        "flags": "m",
-        "dimensions": "rCI, rCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": "Default: Headphones are located at the position of the listener"
-    },
-    "SourcePosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            [
-                0,
-                0.09,
-                0
-            ],
-            [
-                0,
-                -0.09,
-                0
-            ]
-        ],
-        "flags": "m",
-        "dimensions": "eCI, eCM",
-        "type": "double",
-        "comment": "Default: Reflects the correspondence of each emitter to each receiver"
-    },
-    "EmitterPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourceManufacturer": {
-        "default": [
-            ""
-        ],
-        "flags": null,
-        "dimensions": "MS",
-        "type": "string",
-        "comment": "Optional M-dependent version of the attribute SourceManufucturer"
-    },
-    "SourceModel": {
-        "default": [
-            ""
-        ],
-        "flags": null,
-        "dimensions": "MS",
-        "type": "string",
-        "comment": "Optional M-dependent version of the attribute SourceModel"
-    },
-    "ReceiverDescription": {
-        "default": [
-            ""
-        ],
-        "flags": null,
-        "dimensions": "MS",
-        "type": "string",
-        "comment": "Optional M-dependent version of the attribute ReceiverDescription"
-    },
-    "EmitterDescription": {
-        "default": [
-            ""
-        ],
-        "flags": null,
-        "dimensions": "MS",
-        "type": "string",
-        "comment": "Optional M-dependent version of the attribute EmitterDescription"
-    },
-    "MeasurementDate": {
-        "default": 0,
-        "flags": null,
-        "dimensions": "M",
-        "type": "double",
-        "comment": "Optional M-dependent date and time of the measurement"
-    },
-    "Data.IR": {
-        "default": [
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "mRn",
-        "type": "double",
-        "comment": ""
-    },
-    "Data.SamplingRate": {
-        "default": 48000,
-        "flags": "m",
-        "dimensions": "I, M",
-        "type": "double",
-        "comment": ""
-    },
-    "Data.SamplingRate:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.Delay": {
-        "default": [
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IR, MR",
-        "type": "double",
-        "comment": ""
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/SingleRoomDRIR_0.3.json b/sofar/conventions/SingleRoomDRIR_0.3.json
deleted file mode 100644
index 4997d406..00000000
--- a/sofar/conventions/SingleRoomDRIR_0.3.json
+++ /dev/null
@@ -1,360 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "SingleRoomDRIR",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "This convention stores arbitrary number of receivers while providing an information about the room. The main application is to store DRIRs for a single room."
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "0.3",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "FIR",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomType": {
-        "default": "reverberant",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DatabaseName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomDescription": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "RCI, RCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "SourcePosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "eCI, eCM",
-        "type": "double",
-        "comment": ""
-    },
-    "EmitterPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourceUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "SourceView": {
-        "default": [
-            -1,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "SourceView:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourceView:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.IR": {
-        "default": [
-            0
-        ],
-        "flags": "m",
-        "dimensions": "mrn",
-        "type": "double",
-        "comment": ""
-    },
-    "Data.SamplingRate": {
-        "default": 48000,
-        "flags": "m",
-        "dimensions": "I",
-        "type": "double",
-        "comment": ""
-    },
-    "Data.SamplingRate:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.Delay": {
-        "default": [
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IR, MR",
-        "type": "double",
-        "comment": ""
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/SingleRoomMIMOSRIR_1.0.json b/sofar/conventions/SingleRoomMIMOSRIR_1.0.json
deleted file mode 100644
index cbeab71e..00000000
--- a/sofar/conventions/SingleRoomMIMOSRIR_1.0.json
+++ /dev/null
@@ -1,527 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "2.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "SingleRoomMIMOSRIR",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Single-room multiple-input multiple-output spatial room impulse responses, depending on Emitters"
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "FIR-E",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Shall be FIR-E"
-    },
-    "GLOBAL:RoomType": {
-        "default": "shoebox",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Shall be 'shoebox' or 'dae'"
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomDescription": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "narrative description of the room"
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ListenerShortName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ListenerDescription": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ReceiverShortName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ReceiverDescription": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SourceShortName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SourceDescription": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:EmitterShortName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:EmitterDescription": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DatabaseName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Name of the database. Used for classification of the data."
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, RCI, RCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Can be of any type enabling both spatially discrete and spatially continuous representations."
-    },
-    "ReceiverPosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": null,
-        "dimensions": "RCI, RCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": null,
-        "dimensions": "RCI, RCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverView:Type": {
-        "default": "cartesian",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverView:Units": {
-        "default": "metre",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "MC",
-        "type": "double",
-        "comment": ""
-    },
-    "SourcePosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourceView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "SourceUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "SourceView:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourceView:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, ECI, ECM",
-        "type": "double",
-        "comment": "Can be of any type enabling both spatially discrete and spatially continuous representations."
-    },
-    "EmitterPosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": null,
-        "dimensions": "ECI, ECM",
-        "type": "double",
-        "comment": ""
-    },
-    "EmitterUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": null,
-        "dimensions": "ECI, ECM",
-        "type": "double",
-        "comment": ""
-    },
-    "EmitterView:Type": {
-        "default": "cartesian",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterView:Units": {
-        "default": "metre",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "RoomCornerA": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "RoomCornerB": {
-        "default": [
-            1,
-            2,
-            3
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "RoomCorners": {
-        "default": 0,
-        "flags": null,
-        "dimensions": "II",
-        "type": "double",
-        "comment": "The value of this attribute is to be ignored. It only exist to for RoomCorners:Type and RoomCorners:Units"
-    },
-    "RoomCorners:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "RoomCorners:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "Data.IR": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "mrne",
-        "type": "double",
-        "comment": "Impulse responses"
-    },
-    "Data.SamplingRate": {
-        "default": 48000,
-        "flags": "m",
-        "dimensions": "I, M",
-        "type": "double",
-        "comment": "Sampling rate of the samples in Data.IR and Data.Delay"
-    },
-    "Data.SamplingRate:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Unit of the sampling rate"
-    },
-    "Data.Delay": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "IRI, MRI, MRE",
-        "type": "double",
-        "comment": "Additional delay of each IR (in samples)"
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/SingleRoomSRIR_1.0.json b/sofar/conventions/SingleRoomSRIR_1.0.json
deleted file mode 100644
index abcf2ca6..00000000
--- a/sofar/conventions/SingleRoomSRIR_1.0.json
+++ /dev/null
@@ -1,527 +0,0 @@
-{
-    "GLOBAL:Conventions": {
-        "default": "SOFA",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Version": {
-        "default": "2.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SOFAConventions": {
-        "default": "SingleRoomSRIR",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "For measuring SRIRs in a single room with a single excitation source (e.g., a loudspeaker) and a listener containing an arbitrary number of omnidirectional receivers (e.g., a microphone array)."
-    },
-    "GLOBAL:SOFAConventionsVersion": {
-        "default": "1.0",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DataType": {
-        "default": "FIR",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Shall be FIR"
-    },
-    "GLOBAL:RoomType": {
-        "default": "shoebox",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Shall be 'shoebox' or 'dae'"
-    },
-    "GLOBAL:Title": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateCreated": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DateModified": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIName": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:APIVersion": {
-        "default": "",
-        "flags": "rm",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:AuthorContact": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Organization": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:License": {
-        "default": "No license provided, ask the author for permission",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ApplicationVersion": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Comment": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:RoomDescription": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "narrative description of the room"
-    },
-    "GLOBAL:History": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:References": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:Origin": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ListenerShortName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ListenerDescription": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ReceiverShortName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:ReceiverDescription": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SourceShortName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:SourceDescription": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:EmitterShortName": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:EmitterDescription": {
-        "default": "",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "GLOBAL:DatabaseName": {
-        "default": "",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Name of the database. Used for classification of the data."
-    },
-    "ListenerPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerPosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerPosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "ListenerView:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ListenerView:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, RCI, RCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverPosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Can be of any type enabling both spatially discrete and spatially continuous representations."
-    },
-    "ReceiverPosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": null,
-        "dimensions": "RCI, RCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": null,
-        "dimensions": "RCI, RCM",
-        "type": "double",
-        "comment": ""
-    },
-    "ReceiverView:Type": {
-        "default": "cartesian",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "ReceiverView:Units": {
-        "default": "metre",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "MC",
-        "type": "double",
-        "comment": ""
-    },
-    "SourcePosition:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourcePosition:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourceView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "SourceUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": "m",
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "SourceView:Type": {
-        "default": "cartesian",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "SourceView:Units": {
-        "default": "metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterPosition": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": "m",
-        "dimensions": "eCI, eCM",
-        "type": "double",
-        "comment": ""
-    },
-    "EmitterPosition:Type": {
-        "default": "spherical",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Shall be 'cartesian' or 'spherical', restricting to spatially discrete emitters."
-    },
-    "EmitterPosition:Units": {
-        "default": "degree, degree, metre",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "EmitterView": {
-        "default": [
-            1,
-            0,
-            0
-        ],
-        "flags": null,
-        "dimensions": "ECI, ECM",
-        "type": "double",
-        "comment": ""
-    },
-    "EmitterUp": {
-        "default": [
-            0,
-            0,
-            1
-        ],
-        "flags": null,
-        "dimensions": "ECI, ECM",
-        "type": "double",
-        "comment": ""
-    },
-    "EmitterView:Type": {
-        "default": "cartesian",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Shall be 'cartesian' or 'spherical', restricting to spatially discrete emitters."
-    },
-    "EmitterView:Units": {
-        "default": "metre",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": ""
-    },
-    "RoomCornerA": {
-        "default": [
-            0,
-            0,
-            0
-        ],
-        "flags": null,
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "RoomCornerB": {
-        "default": [
-            1,
-            2,
-            3
-        ],
-        "flags": null,
-        "dimensions": "IC, MC",
-        "type": "double",
-        "comment": ""
-    },
-    "RoomCorners": {
-        "default": 0,
-        "flags": null,
-        "dimensions": "II",
-        "type": "double",
-        "comment": "The value of this attribute is to be ignored. It only exist to for RoomCorners:Type and RoomCorners:Units"
-    },
-    "RoomCorners:Type": {
-        "default": "cartesian",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Type of coordinate system for RoomCornerA and RoomCornerB"
-    },
-    "RoomCorners:Units": {
-        "default": "metre",
-        "flags": null,
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Unit of coordinate system for RoomCornerA and RoomCornerB"
-    },
-    "Data.IR": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "mrn",
-        "type": "double",
-        "comment": "Impulse responses"
-    },
-    "Data.SamplingRate": {
-        "default": 48000,
-        "flags": "m",
-        "dimensions": "I, M",
-        "type": "double",
-        "comment": "Sampling rate of the samples in Data.IR and Data.Delay"
-    },
-    "Data.SamplingRate:Units": {
-        "default": "hertz",
-        "flags": "m",
-        "dimensions": null,
-        "type": "attribute",
-        "comment": "Unit of the sampling rate"
-    },
-    "Data.Delay": {
-        "default": 0,
-        "flags": "m",
-        "dimensions": "IR, MR",
-        "type": "double",
-        "comment": "Additional delay of each IR (in samples)"
-    }
-}
\ No newline at end of file
diff --git a/sofar/conventions/source/FreeFieldDirectivityTF_1.0.csv b/sofar/conventions/source/FreeFieldDirectivityTF_1.0.csv
deleted file mode 100644
index 4af90045..00000000
--- a/sofar/conventions/source/FreeFieldDirectivityTF_1.0.csv
+++ /dev/null
@@ -1,58 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm		attribute
-GLOBAL:Version	2.0	rm		attribute
-GLOBAL:SOFAConventions	FreeFieldDirectivityTF	rm		attribute	This conventions stores directivities of acoustic sources (instruments, loudspeakers, singers, talkers, etc) in the frequency domain for multiple musical notes in free field.
-GLOBAL:SOFAConventionsVersion	1.0	rm		attribute
-GLOBAL:DataType	TF	rm		attribute	We store frequency-dependent data here
-GLOBAL:RoomType	free field	m		attribute	The room information can be arbitrary, but the spatial setup assumes free field.
-GLOBAL:Title		m		attribute
-GLOBAL:DateCreated		m		attribute
-GLOBAL:DateModified		m		attribute
-GLOBAL:APIName		rm		attribute
-GLOBAL:APIVersion		rm		attribute
-GLOBAL:AuthorContact		m		attribute
-GLOBAL:Organization		m		attribute
-GLOBAL:License	No license provided, ask the author for permission	m		attribute
-GLOBAL:ApplicationName				attribute
-GLOBAL:ApplicationVersion				attribute
-GLOBAL:Comment		m		attribute
-GLOBAL:History				attribute
-GLOBAL:References				attribute
-GLOBAL:Origin				attribute
-GLOBAL:DatabaseName		m		attribute	Name of the database. Used for classification of the data
-GLOBAL:Musician				attribute	Narrative description of the musician such as position, behavior, or personal data if not data-protected, e.g., 'Christiane Schmidt sitting on the chair', or 'artificial excitation by R2D2'.
-GLOBAL:Description				attribute	Narrative description of a measurement. For musical instruments/singers, the note (C1, D1, etc) or the dynamic (pp., ff., etc), or the string played, the playing style (pizzicato, legato, etc.), or the type of excitation (e.g., hit location of a cymbal). For loudspeakers, the system and driver units.
-GLOBAL:SourceType		m		attribute	Narrative description of the acoustic source, e.g., 'Violin', 'Female singer', or '2-way loudspeaker'
-GLOBAL:SourceManufacturer		m		attribute	Narrative description of the manufacturer of the source, e.g., 'Stradivari, Lady Blunt, 1721' or 'LoudspeakerCompany'
-ListenerPosition	[0 0 0] 	m	IC, MC	double	Position of the microphone array during the measurements.
-ListenerPosition:Type	cartesian	m		attribute
-ListenerPosition:Units	metre	m		attribute
-ListenerView	[1 0 0]	m	IC, MC	double	Orientation of the microphone array
-ListenerView:Type	cartesian	m		attribute
-ListenerView:Units	metre	m		attribute
-ListenerUp	[0 0 1]	m	IC, MC	double	Up vector of the microphone array
-ReceiverPosition	[0 0 1]	m	IC, RC, RCM	double	Positions of the microphones during the measurements (relative to the Listener)
-ReceiverPosition:Type	spherical	m		attribute
-ReceiverPosition:Units	degree, degree, metre	m		attribute
-SourcePosition	[0 0 0] 	m	IC, MC	double	Position of the acoustic source (instrument)
-SourcePosition:Type	cartesian	m		attribute
-SourcePosition:Units	metre	m		attribute
-SourcePosition:Reference		m		attribute	Narrative description of the spatial reference of the source position, e.g., for the trumpet, 'The bell'. Mandatory in order to provide a reference across different instruments
-SourceView	[1 0 0]	m	IC, MC	double	Orientation of the acoustic source (instrument)
-SourceView:Type	cartesian	m		attribute
-SourceView:Units	metre	m		attribute
-SourceView:Reference		m		attribute	Narrative description of the spatial reference of the source view, e.g., for the trumpet, 'Viewing direction of the bell'. Mandatory in order to provide a reference across different instruments
-SourceUp	[0 0 1]	m	IC, MC	double	Up vector of the acoustic source (instrument)
-SourceUp:Reference		m		attribute	Narrative description of the spatial reference of the source up, e.g., for the trumpet, 'Along the keys, keys up'. Mandatory in order to provide a reference across different instruments
-EmitterPosition	[0 0 0]	m	IC, MC	double	A more detailed structure of the Source. In a simple settings, a single Emitter is considered that is collocated with the source.
-EmitterPosition:Type	cartesian	m		attribute
-EmitterPosition:Units	metre	m		attribute
-EmitterDescription	{''}		IS, MS	string	A more detailed structure of the source. In a simple setting, a single Emitter is considered that is collocated with the source. In a more complicated setting, this may be the strings of a violin or the units of a loudspeaker.
-MIDINote	0		I, M	double	Defines the note played by the source during the measurement. The note is specified a MIDI note by the [https://www.midi.org/specifications-old/item/the-midi-1-0-specification MIDI specifications, version 1.0]. Not mandatory, but recommended for tonal instruments.
-Description	{''}		MS	string	This variable is used when the description varies with M.
-SourceTuningFrequency	440		I, M	double	Frequency (in Hertz) to which a musical instrument is tuned to corresponding to the note A4 (MIDINote=69). Recommended for tonal instruments.
-Data.Real	0	m	mrn	double	Real part of the complex spectrum. The default value 0 indicates that all data fields are initialized with zero values.
-Data.Imag	0	m	MRN	double	Imaginary part of the complex spectrum
-N	0	m	N	double	Frequency values
-N:LongName	frequency	m		attribute
-N:Units	hertz	m		attribute	Units used for N
diff --git a/sofar/conventions/source/FreeFieldHRIR_1.0.csv b/sofar/conventions/source/FreeFieldHRIR_1.0.csv
deleted file mode 100644
index 1b837bdd..00000000
--- a/sofar/conventions/source/FreeFieldHRIR_1.0.csv
+++ /dev/null
@@ -1,43 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	2.0	rm	 	attribute
-GLOBAL:SOFAConventions	FreeFieldHRIR	rm	  	attribute	An extension of SimpleFreeFieldHRIR in order to consider more complex data sets described in spatially continuous representation. Each HRTF direction corresponds to an emitter, and a consistent measurement for a single listener and all directions is described by a set of the emitter positions surrounding the listener.
-GLOBAL:SOFAConventionsVersion	1.0	rm	  	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Comment			 	attribute
-GLOBAL:DataType	FIR-E	rm	  	attribute
-GLOBAL:History			 	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:ListenerShortName		m		attribute	ID of the subject from the database
-GLOBAL:Organization		m	  	attribute
-GLOBAL:References				attribute
-GLOBAL:RoomType	free field	m	  	attribute
-GLOBAL:Origin				attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:Title		m		attribute
-ListenerPosition	[0 0 0] 	m	IC, MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ReceiverPosition	[0 0.09 0; 0 -0.09 0]	m	RCI, RCM	double
-ReceiverPosition:Type	cartesian	m	  	attribute
-ReceiverPosition:Units	metre	m	  	attribute
-SourcePosition	[0 0 0]	m	IC, MC	double	Source position is assumed to be the ListenerPosition in order to reflect Emitters surrounding the Listener
-SourcePosition:Type	spherical	m	  	attribute
-SourcePosition:Units	degree, degree, metre	m	  	attribute
-EmitterPosition	[0 0 0]	m	IC, ECI, ECM	double	Radius in 'spherical harmonics', Position in 'cartesian' and 'spherical'
-EmitterPosition:Type	spherical harmonics	m	  	attribute	Can be 'spherical harmonics', 'cartesian', or 'spherical'
-EmitterPosition:Units	degree, degree, metre	m	  	attribute
-GLOBAL:DatabaseName		m	  	attribute	Name of the database to which these data belong
-ListenerUp	[0 0 1]	m	IC, MC	double
-ListenerView	[1 0 0]	m	IC, MC	double
-ListenerView:Type	cartesian	m		attribute
-ListenerView:Units	metre	m		attribute
-Data.IR	[0 0]	m	mrne	double
-Data.SamplingRate	48000	m	I, M	double
-Data.SamplingRate:Units	hertz	m		attribute
-Data.Delay	[0 0]	m	IRE, MRE	double	Additional delay of each IR (in samples)
diff --git a/sofar/conventions/source/FreeFieldHRTF_1.0.csv b/sofar/conventions/source/FreeFieldHRTF_1.0.csv
deleted file mode 100644
index 3e20f7a4..00000000
--- a/sofar/conventions/source/FreeFieldHRTF_1.0.csv
+++ /dev/null
@@ -1,44 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	2.0	rm	 	attribute
-GLOBAL:SOFAConventions	FreeFieldHRTF	rm	  	attribute	This conventions is for HRTFs created under conditions where room information is irrelevant and stored as SH coefficients
-GLOBAL:SOFAConventionsVersion	1.0	rm	  	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Comment			 	attribute
-GLOBAL:DataType	TF-E	rm	  	attribute
-GLOBAL:History			 	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:ListenerShortName		m		attribute	ID of the subject from the database
-GLOBAL:Organization		m	  	attribute
-GLOBAL:References				attribute
-GLOBAL:RoomType	free field	m	  	attribute
-GLOBAL:Origin				attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:Title		m		attribute
-ListenerPosition	[0 0 0] 	m	IC, MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ReceiverPosition	[0 0.09 0; 0 -0.09 0]	m	RCI, RCM	double
-ReceiverPosition:Type	cartesian	m	  	attribute
-ReceiverPosition:Units	metre	m	  	attribute
-SourcePosition	[0 0 0]	m	IC, MC	double	Source position is assumed to be the ListenerPosition in order to reflect Emitters surrounding the Listener
-SourcePosition:Type	spherical	m	  	attribute
-SourcePosition:Units	degree, degree, metre	m	  	attribute
-EmitterPosition	[0 0 0]	m	IC, ECI, ECM	double	Radius in 'spherical harmonics', Position in 'cartesian' and 'spherical'
-EmitterPosition:Type	spherical harmonics	m	  	attribute	Can be 'spherical harmonics', 'cartesian', or 'spherical'
-EmitterPosition:Units	degree, degree, metre	m	  	attribute
-GLOBAL:DatabaseName		m	  	attribute	Name of the database to which these data belong
-ListenerUp	[0 0 1]	m	IC, MC	double
-ListenerView	[1 0 0]	m	IC, MC	double
-ListenerView:Type	cartesian	m		attribute
-ListenerView:Units	metre	m		attribute
-Data.Real	[0 0]	m	mrne	double
-Data.Imag	[0 0]	m	MRNE	double
-N	0	m	N	double
-N:LongName	frequency			attribute
-N:Units	hertz	m		attribute
diff --git a/sofar/conventions/source/GeneralFIR-E_2.0.csv b/sofar/conventions/source/GeneralFIR-E_2.0.csv
deleted file mode 100644
index 9bc63d52..00000000
--- a/sofar/conventions/source/GeneralFIR-E_2.0.csv
+++ /dev/null
@@ -1,37 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	2.0	rm	 	attribute
-GLOBAL:SOFAConventions	GeneralFIR-E	rm	  	attribute	This conventions stores IRs for general purposes, i.e., only the mandatory, SOFA general metadata are pre-defined
-GLOBAL:SOFAConventionsVersion	2.0	rm	  	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Comment		m	 	attribute
-GLOBAL:DataType	FIR-E	rm	  	attribute	We use FIR datatype which in addition depends on Emitters (E)
-GLOBAL:History			 	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:Organization		m	  	attribute
-GLOBAL:References				attribute
-GLOBAL:RoomType	free field	m	  	attribute	The room information can be arbitrary
-GLOBAL:Origin				attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:Title		m		attribute
-ListenerPosition	[0 0 0] 	m	IC, MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ReceiverPosition	[0 0 0]	m	IC, RC, RCM	double
-ReceiverPosition:Type	cartesian	m	  	attribute
-ReceiverPosition:Units	metre	m	  	attribute
-SourcePosition	[0 0 1]	m	IC, MC	double
-SourcePosition:Type	spherical	m	  	attribute
-SourcePosition:Units	degree, degree, metre	m	  	attribute
-EmitterPosition	[0 0 0]	m	IC, EC, ECM	double	Each speaker is represented as an emitter. Use EmitterPosition to represent the position of a particular speaker. Size of EmitterPosition determines E
-EmitterPosition:Type	cartesian	m	  	attribute
-EmitterPosition:Units	metre	m	  	attribute
-Data.IR	0	m	mrne	double	Impulse responses
-Data.SamplingRate	48000	m	I, M	double	Sampling rate of the samples in Data.IR and Data.Delay
-Data.SamplingRate:Units	hertz	m		attribute	Unit of the sampling rate
-Data.Delay	0	m	IRE, MRE	double	Additional delay of each IR (in samples)
diff --git a/sofar/conventions/source/GeneralFIRE_1.0.csv b/sofar/conventions/source/GeneralFIRE_1.0.csv
deleted file mode 100644
index c865baef..00000000
--- a/sofar/conventions/source/GeneralFIRE_1.0.csv
+++ /dev/null
@@ -1,37 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	1.0	rm	 	attribute
-GLOBAL:SOFAConventions	GeneralFIRE	rm	  	attribute	This conventions stores IRs for general purposes, i.e., only the mandatory, SOFA general metadata are pre-defined
-GLOBAL:SOFAConventionsVersion	1.0	rm	  	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Comment		m	 	attribute
-GLOBAL:DataType	FIRE	rm	  	attribute	We use FIR datatype which in addition depends on Emitters (E)
-GLOBAL:History			 	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:Organization		m	  	attribute
-GLOBAL:References				attribute
-GLOBAL:RoomType	free field	m	  	attribute	The room information can be arbitrary
-GLOBAL:Origin				attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:Title		m		attribute
-ListenerPosition	[0 0 0] 	m	IC, MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ReceiverPosition	[0 0 0]	m	rCI, rCM	double
-ReceiverPosition:Type	cartesian	m	  	attribute
-ReceiverPosition:Units	metre	m	  	attribute
-SourcePosition	[0 0 1]	m	IC, MC	double
-SourcePosition:Type	spherical	m	  	attribute
-SourcePosition:Units	degree, degree, metre	m	  	attribute
-EmitterPosition	[0 0 0]	m	eCI, eCM	double	Each speaker is represented as an emitter. Use EmitterPosition to represent the position of a particular speaker. Size of EmitterPosition determines E
-EmitterPosition:Type	cartesian	m	  	attribute
-EmitterPosition:Units	metre	m	  	attribute
-Data.IR	0	m	mREn	double	Impulse responses
-Data.SamplingRate	48000	m	I	double	Sampling rate of the samples in Data.IR and Data.Delay
-Data.SamplingRate:Units	hertz	m		attribute	Unit of the sampling rate
-Data.Delay	0	m	IRE, MRE	double	Additional delay of each IR (in samples)
diff --git a/sofar/conventions/source/GeneralFIR_1.0.csv b/sofar/conventions/source/GeneralFIR_1.0.csv
deleted file mode 100644
index 6bfd637b..00000000
--- a/sofar/conventions/source/GeneralFIR_1.0.csv
+++ /dev/null
@@ -1,40 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	1.0	rm	 	attribute
-GLOBAL:SOFAConventions	GeneralFIR	rm	  	attribute	This conventions stores IRs for general purposes, i.e., only the mandatory, SOFA general metadata are pre-defined
-GLOBAL:SOFAConventionsVersion	1.0	rm	  	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Comment		m	 	attribute
-GLOBAL:DataType	FIR	rm	  	attribute	We store IRs here
-GLOBAL:History			 	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:Organization		m	  	attribute
-GLOBAL:References				attribute
-GLOBAL:RoomType	free field	m	  	attribute	The room information can be arbitrary
-GLOBAL:Origin				attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:Title		m		attribute
-ListenerPosition	[0 0 0] 	m	IC, MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ReceiverPosition	[0 0 0]	m	rCI, rCM	double
-ReceiverPosition:Type	cartesian	m	  	attribute
-ReceiverPosition:Units	metre	m	  	attribute
-SourcePosition	[0 0 1]	m	IC, MC	double	In order to store different directions/positions around the listener, SourcePosition is assumed to vary
-SourcePosition:Type	spherical	m	  	attribute
-SourcePosition:Units	degree, degree, metre	m	  	attribute
-EmitterPosition	[0 0 0]	m	eCI, eCM	double
-EmitterPosition:Type	cartesian	m	  	attribute
-EmitterPosition:Units	metre	m	  	attribute
-Data.IR	0	m	mRn	double	Impulse responses
-Data.SamplingRate	48000	m	I	double	Sampling rate of the samples in Data.IR and Data.Delay
-Data.SamplingRate:Units	hertz	m		attribute	Unit of the sampling rate
-Data.Delay	0	m	IR, MR	double	Additional delay of each IR (in samples)
-ListenerView	[1 0 0]		IC, MC	double
-ListenerView:Type	cartesian			attribute
-ListenerView:Units	metre			attribute
diff --git a/sofar/conventions/source/GeneralFIR_2.0.csv b/sofar/conventions/source/GeneralFIR_2.0.csv
deleted file mode 100644
index 9845f4b1..00000000
--- a/sofar/conventions/source/GeneralFIR_2.0.csv
+++ /dev/null
@@ -1,40 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	2.0	rm	 	attribute
-GLOBAL:SOFAConventions	GeneralFIR	rm	  	attribute	This conventions stores IRs for general purposes, i.e., only the mandatory, SOFA general metadata are pre-defined
-GLOBAL:SOFAConventionsVersion	2.0	rm	  	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Comment		m	 	attribute
-GLOBAL:DataType	FIR	rm	  	attribute	We store IRs here
-GLOBAL:History			 	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:Organization		m	  	attribute
-GLOBAL:References				attribute
-GLOBAL:RoomType	free field	m	  	attribute	The room information can be arbitrary
-GLOBAL:Origin				attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:Title		m		attribute
-ListenerPosition	[0 0 0] 	m	IC, MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ReceiverPosition	[0 0 0]	m	IC, RC, RCM	double
-ReceiverPosition:Type	cartesian	m	  	attribute
-ReceiverPosition:Units	metre	m	  	attribute
-SourcePosition	[0 0 1]	m	IC, MC	double	In order to store different directions/positions around the listener, SourcePosition is assumed to vary
-SourcePosition:Type	spherical	m	  	attribute
-SourcePosition:Units	degree, degree, metre	m	  	attribute
-EmitterPosition	[0 0 0]	m	eCI, eCM	double
-EmitterPosition:Type	cartesian	m	  	attribute
-EmitterPosition:Units	metre	m	  	attribute
-Data.IR	0	m	mrn	double	Impulse responses
-Data.SamplingRate	48000	m	I, M	double	Sampling rate of the samples in Data.IR and Data.Delay
-Data.SamplingRate:Units	hertz	m		attribute	Unit of the sampling rate
-Data.Delay	0	m	IR, MR	double	Additional delay of each IR (in samples)
-ListenerView	[1 0 0]		IC, MC	double
-ListenerView:Type	cartesian			attribute
-ListenerView:Units	metre			attribute
diff --git a/sofar/conventions/source/GeneralTF-E_1.0.csv b/sofar/conventions/source/GeneralTF-E_1.0.csv
deleted file mode 100644
index 50fba449..00000000
--- a/sofar/conventions/source/GeneralTF-E_1.0.csv
+++ /dev/null
@@ -1,38 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	2.0	rm	 	attribute
-GLOBAL:SOFAConventions	GeneralTF-E	rm	  	attribute	This conventions stores TFs depending in the Emiiter for general purposes, i.e., only the mandatory, SOFA general metadata are pre-defined. This convention is based on GeneralTF
-GLOBAL:SOFAConventionsVersion	1.0	rm	  	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Comment		m	 	attribute
-GLOBAL:DataType	TF-E	rm	  	attribute	We store frequency-dependent data depending on the emitter here
-GLOBAL:History			 	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:Organization		m	  	attribute
-GLOBAL:References				attribute
-GLOBAL:RoomType	free field	m	  	attribute	The room information can be arbitrary
-GLOBAL:Origin				attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:Title		m		attribute
-ListenerPosition	[0 0 0] 	m	IC, MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ReceiverPosition	[0 0 0]	m	IC, RC, RCM	double
-ReceiverPosition:Type	cartesian	m	  	attribute
-ReceiverPosition:Units	metre	m	  	attribute
-SourcePosition	[0 0 1]	m	IC, MC	double	In order to store different directions/positions around the listener, SourcePosition is assumed to vary
-SourcePosition:Type	spherical	m	  	attribute
-SourcePosition:Units	degree, degree, metre	m	  	attribute
-EmitterPosition	[0 0 0]	m	IC, EC, ECM	double
-EmitterPosition:Type	cartesian	m	  	attribute
-EmitterPosition:Units	metre	m	  	attribute
-Data.Real	0	m	mrne	double	The real part of the complex spectrum
-Data.Imag	0	m	MRNE	double	The imaginary part of the complex spectrum
-N	0	m	N	double	Frequency values
-N:LongName	frequency			attribute
-N:Units	hertz	m		attribute	Unit of the values given in N
diff --git a/sofar/conventions/source/GeneralTF_1.0.csv b/sofar/conventions/source/GeneralTF_1.0.csv
deleted file mode 100644
index 3a14a5de..00000000
--- a/sofar/conventions/source/GeneralTF_1.0.csv
+++ /dev/null
@@ -1,38 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	1.0	rm	 	attribute
-GLOBAL:SOFAConventions	GeneralTF	rm	  	attribute	This conventions stores TFs for general purposes, i.e., only the mandatory, SOFA general metadata are pre-defined. This convention is based on GeneralFIR.
-GLOBAL:SOFAConventionsVersion	1.0	rm	  	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Comment		m	 	attribute
-GLOBAL:DataType	TF	rm	  	attribute	We store frequency-dependent data here
-GLOBAL:History			 	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:Organization		m	  	attribute
-GLOBAL:References				attribute
-GLOBAL:RoomType	free field	m	  	attribute	The room information can be arbitrary
-GLOBAL:Origin				attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:Title		m		attribute
-ListenerPosition	[0 0 0] 	m	IC, MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ReceiverPosition	[0 0 0]	m	rCI, rCM	double
-ReceiverPosition:Type	cartesian	m	  	attribute
-ReceiverPosition:Units	metre	m	  	attribute
-SourcePosition	[0 0 1]	m	IC, MC	double	In order to store different directions/positions around the listener, SourcePosition is assumed to vary
-SourcePosition:Type	spherical	m	  	attribute
-SourcePosition:Units	degree, degree, metre	m	  	attribute
-EmitterPosition	[0 0 0]	m	eCI, eCM	double
-EmitterPosition:Type	cartesian	m	  	attribute
-EmitterPosition:Units	metre	m	  	attribute
-Data.Real	0	m	mRn	double	The real part of the complex spectrum
-Data.Imag	0	m	MRN	double	The imaginary part of the complex spectrum
-N	0	m	N	double	Frequency values
-N:LongName	frequency	m		attribute
-N:Units	hertz	m		attribute	Unit of the values given in N
diff --git a/sofar/conventions/source/GeneralTF_2.0.csv b/sofar/conventions/source/GeneralTF_2.0.csv
deleted file mode 100644
index e30250d6..00000000
--- a/sofar/conventions/source/GeneralTF_2.0.csv
+++ /dev/null
@@ -1,38 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	1.0	rm	 	attribute
-GLOBAL:SOFAConventions	GeneralTF	rm	  	attribute	This conventions stores TFs for general purposes, i.e., only the mandatory, SOFA general metadata are pre-defined. This convention is based on GeneralFIR.
-GLOBAL:SOFAConventionsVersion	2.0	rm	  	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Comment		m	 	attribute
-GLOBAL:DataType	TF	rm	  	attribute	We store frequency-dependent data here
-GLOBAL:History			 	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:Organization		m	  	attribute
-GLOBAL:References				attribute
-GLOBAL:RoomType	free field	m	  	attribute	The room information can be arbitrary
-GLOBAL:Origin				attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:Title		m		attribute
-ListenerPosition	[0 0 0] 	m	IC, MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ReceiverPosition	[0 0 0]	m	IC, RC, RCM	double
-ReceiverPosition:Type	cartesian	m	  	attribute
-ReceiverPosition:Units	metre	m	  	attribute
-SourcePosition	[0 0 1]	m	IC, MC	double	In order to store different directions/positions around the listener, SourcePosition is assumed to vary
-SourcePosition:Type	spherical	m	  	attribute
-SourcePosition:Units	degree, degree, metre	m	  	attribute
-EmitterPosition	[0 0 0]	m	eCI, eCM	double
-EmitterPosition:Type	cartesian	m	  	attribute
-EmitterPosition:Units	metre	m	  	attribute
-Data.Real	0	m	mrn	double	The real part of the complex spectrum
-Data.Imag	0	m	MRN	double	The imaginary part of the complex spectrum
-N	0	m	N	double	Frequency values
-N:LongName	frequency	m		attribute
-N:Units	hertz	m		attribute	Unit of the values given in N
diff --git a/sofar/conventions/source/MultiSpeakerBRIR_0.3.csv b/sofar/conventions/source/MultiSpeakerBRIR_0.3.csv
deleted file mode 100644
index 753f5295..00000000
--- a/sofar/conventions/source/MultiSpeakerBRIR_0.3.csv
+++ /dev/null
@@ -1,48 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	1.0	rm	 	attribute
-GLOBAL:SOFAConventions	MultiSpeakerBRIR	rm	  	attribute	This convention is for BRIRs recorded in reverberant conditions from multiple loudspeaker sources at a number of listener orientations.
-GLOBAL:SOFAConventionsVersion	0.3	rm	  	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Comment		m	 	attribute
-GLOBAL:DataType	FIRE	rm	  	attribute	We use FIR datatype which in addition depends on Emitters (E)
-GLOBAL:History			 	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:Organization		m	  	attribute
-GLOBAL:References				attribute
-GLOBAL:RoomType	reverberant	m	  	attribute
-GLOBAL:Origin				attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:Title		m		attribute
-ListenerPosition	[0 0 0] 	m	IC, MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ReceiverPosition	[0 0.09 0; 0 -0.09 0]	m	rCI, rCM	double
-ReceiverPosition:Type	cartesian	m	  	attribute
-ReceiverPosition:Units	metre	m	  	attribute
-SourcePosition	[0 0 1]	m	IC, MC	double
-SourcePosition:Type	spherical	m	  	attribute
-SourcePosition:Units	degree, degree, metre	m	  	attribute
-EmitterPosition	[0 0 0]	m	eCI, eCM	double	Each speaker is represented as an emitter. Use EmitterPosition to represent the position of a particular speaker. Size of EmitterPosition determines E
-EmitterPosition:Type	cartesian	m	  	attribute
-EmitterPosition:Units	metre	m	  	attribute
-GLOBAL:DatabaseName		m	  	attribute	name of the database to which these data belong
-GLOBAL:ListenerShortName		m	  	attribute	ID of the subject from the database
-GLOBAL:RoomDescription				attribute	narrative description of the room
-ListenerUp	[0 0 1]	m	IC, MC	double
-ListenerView	[1 0 0]	m	IC, MC	double
-ListenerView:Type	cartesian	m	  	attribute
-ListenerView:Units	metre	m	  	attribute
-EmitterUp	[0 0 1]		ECI, ECM	double	When EmitterUp provided, EmitterView must be provided as well
-EmitterView	[1 0 0]		ECI, ECM	double	When EmitterView provided, EmitterUp must be provided as well
-EmitterView:Type	cartesian		  	attribute
-EmitterView:Units	metre		  	attribute
-Data.IR	[1 1]	m	mREn	double
-Data.SamplingRate	48000	m	I	double
-Data.SamplingRate:Units	hertz	m		attribute
-Data.Delay	[0 0]	m	IRE, MRE	double
diff --git a/sofar/conventions/source/SimpleFreeFieldHRIR_1.0.csv b/sofar/conventions/source/SimpleFreeFieldHRIR_1.0.csv
deleted file mode 100644
index b845b5d6..00000000
--- a/sofar/conventions/source/SimpleFreeFieldHRIR_1.0.csv
+++ /dev/null
@@ -1,47 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	2.0	rm	 	attribute
-GLOBAL:SOFAConventions	SimpleFreeFieldHRIR	rm	  	attribute	This convention set is for HRIRs recorded under free-field conditions or other IRs created under conditions where room information is irrelevant
-GLOBAL:SOFAConventionsVersion	1.0	rm	  	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Comment			 	attribute
-GLOBAL:DataType	FIR	rm	  	attribute
-GLOBAL:History			 	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:Organization		m	  	attribute
-GLOBAL:References				attribute
-GLOBAL:RoomType	free field	m	  	attribute
-GLOBAL:Origin				attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:Title		m		attribute
-ListenerPosition	[0 0 0] 	m	IC, MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ReceiverPosition	[0 0.09 0; 0 -0.09 0]	m	rCI, rCM	double
-ReceiverPosition:Type	cartesian	m	  	attribute
-ReceiverPosition:Units	metre	m	  	attribute
-SourcePosition	[0 0 1]	m	IC, MC	double	Source position is assumed to vary for different directions/positions around the listener
-SourcePosition:Type	spherical	m	  	attribute
-SourcePosition:Units	degree, degree, metre	m	  	attribute
-EmitterPosition	[0 0 0]	m	eCI, eCM	double
-EmitterPosition:Type	cartesian	m	  	attribute
-EmitterPosition:Units	metre	m	  	attribute
-GLOBAL:DatabaseName		m	  	attribute	name of the database to which these data belong
-GLOBAL:ListenerShortName		m	  	attribute	ID of the subject from the database
-ListenerUp	[0 0 1]	m	IC, MC	double
-ListenerView	[1 0 0]	m	IC, MC	double
-ListenerView:Type	cartesian	m		attribute
-ListenerView:Units	metre	m		attribute
-Data.IR	[0 0]	m	mRn	double
-Data.SamplingRate	48000	m	I, M	double
-Data.SamplingRate:Units	hertz	m		attribute
-Data.Delay	[0 0]	m	IR, MR	double
-SourceUp	[0 0 1]		IC, MC	double
-SourceView	[1 0 0]		IC, MC	double
-SourceView:Type	cartesian			attribute
-SourceView:Units	metre			attribute
diff --git a/sofar/conventions/source/SimpleFreeFieldHRSOS_1.0.csv b/sofar/conventions/source/SimpleFreeFieldHRSOS_1.0.csv
deleted file mode 100644
index 9aecc490..00000000
--- a/sofar/conventions/source/SimpleFreeFieldHRSOS_1.0.csv
+++ /dev/null
@@ -1,43 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	2.0	rm	 	attribute
-GLOBAL:SOFAConventions	SimpleFreeFieldHRSOS	rm	  	attribute	This convention set follows SimpleFreeFieldHRIR but the data is stored as second-order section (SOS) coefficients.
-GLOBAL:SOFAConventionsVersion	1.0	rm	  	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Comment			 	attribute
-GLOBAL:DataType	SOS	rm	  	attribute	Filters described as second-order section (SOS) coefficients
-GLOBAL:History			 	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:Organization		m	  	attribute
-GLOBAL:References				attribute
-GLOBAL:RoomType	free field	m	  	attribute
-GLOBAL:Origin				attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:Title		m		attribute
-ListenerPosition	[0 0 0] 	m	IC, MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ReceiverPosition	[0 0.09 0; 0 -0.09 0]	m	rCI, rCM	double
-ReceiverPosition:Type	cartesian	m	  	attribute
-ReceiverPosition:Units	metre	m	  	attribute
-SourcePosition	[0 0 1]	m	IC, MC	double	Source position is assumed to vary for different directions/positions around the listener
-SourcePosition:Type	spherical	m	  	attribute
-SourcePosition:Units	degree, degree, metre	m	  	attribute
-EmitterPosition	[0 0 0]	m	eCI, eCM	double
-EmitterPosition:Type	cartesian	m	  	attribute
-EmitterPosition:Units	metre	m	  	attribute
-GLOBAL:DatabaseName		m	  	attribute	name of the database to which these data belong
-GLOBAL:ListenerShortName		m	  	attribute	ID of the subject from the database
-ListenerUp	[0 0 1]	m	IC, MC	double
-ListenerView	[1 0 0]	m	IC, MC	double
-ListenerView:Type	cartesian	m		attribute
-ListenerView:Units	metre	m		attribute
-Data.SOS	permute([0 0 0 1 0 0; 0 0 0 1 0 0], [3 1 2]);	m	mRn	double	Filter coefficients as SOS coefficients.
-Data.SamplingRate	48000	m	I, M	double	Sampling rate of the coefficients in Data.SOS and the delay in Data.Delay
-Data.SamplingRate:Units	hertz	m		attribute
-Data.Delay	[0 0]	m	IR, MR	double	Broadband delay (in samples resulting from SamplingRate)
diff --git a/sofar/conventions/source/SimpleFreeFieldHRTF_2.0.csv b/sofar/conventions/source/SimpleFreeFieldHRTF_2.0.csv
deleted file mode 100644
index 327c97f1..00000000
--- a/sofar/conventions/source/SimpleFreeFieldHRTF_2.0.csv
+++ /dev/null
@@ -1,44 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	2.0	rm	 	attribute
-GLOBAL:SOFAConventions	SimpleFreeFieldHRTF	rm	  	attribute	This conventions is for HRTFs created under conditions where room information is irrelevant
-GLOBAL:SOFAConventionsVersion	2.0	rm	  	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Comment			 	attribute
-GLOBAL:DataType	TF	rm	  	attribute
-GLOBAL:History			 	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:ListenerShortName		m		attribute	ID of the subject from the database
-GLOBAL:Organization		m	  	attribute
-GLOBAL:References				attribute
-GLOBAL:RoomType	free field	m	  	attribute
-GLOBAL:Origin				attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:Title		m		attribute
-ListenerPosition	[0 0 0] 	m	IC, MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ReceiverPosition	[0 0.09 0; 0 -0.09 0]	m	rCI, rCM	double
-ReceiverPosition:Type	cartesian	m	  	attribute
-ReceiverPosition:Units	metre	m	  	attribute
-SourcePosition	[0 0 1]	m	IC, MC	double	Source position is assumed to vary for different directions/positions around the listener
-SourcePosition:Type	spherical	m	  	attribute
-SourcePosition:Units	degree, degree, metre	m	  	attribute
-EmitterPosition	[0 0 0]	m	eCI, eCM	double
-EmitterPosition:Type	cartesian	m	  	attribute
-EmitterPosition:Units	metre	m	  	attribute
-GLOBAL:DatabaseName		m	  	attribute	name of the database to which these data belong
-ListenerUp	[0 0 1]	m	IC, MC	double
-ListenerView	[1 0 0]	m	IC, MC	double
-ListenerView:Type	cartesian	m		attribute
-ListenerView:Units	metre	m		attribute
-Data.Real	[0 0]	m	mRn	double
-Data.Imag	[0 0]	m	MRN	double
-N	0	m	N	double
-N:LongName	frequency			attribute
-N:Units	hertz	m		attribute
diff --git a/sofar/conventions/source/SimpleFreeFieldSOS_1.0.csv b/sofar/conventions/source/SimpleFreeFieldSOS_1.0.csv
deleted file mode 100644
index f576df1d..00000000
--- a/sofar/conventions/source/SimpleFreeFieldSOS_1.0.csv
+++ /dev/null
@@ -1,43 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	1.0	rm	 	attribute
-GLOBAL:SOFAConventions	SimpleFreeFieldSOS	rm	  	attribute	This convention set follows SimpleFreeFieldHRIR but the data is stored as second-order section (SOS) coefficients.
-GLOBAL:SOFAConventionsVersion	1.0	rm	  	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Comment			 	attribute
-GLOBAL:DataType	SOS	rm	  	attribute	Filters described as second-order section (SOS) coefficients
-GLOBAL:History			 	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:Organization		m	  	attribute
-GLOBAL:References				attribute
-GLOBAL:RoomType	free field	m	  	attribute
-GLOBAL:Origin				attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:Title		m		attribute
-ListenerPosition	[0 0 0] 	m	IC, MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ReceiverPosition	[0 0.09 0; 0 -0.09 0]	m	rCI, rCM	double
-ReceiverPosition:Type	cartesian	m	  	attribute
-ReceiverPosition:Units	metre	m	  	attribute
-SourcePosition	[0 0 1]	m	IC, MC	double	Source position is assumed to vary for different directions/positions around the listener
-SourcePosition:Type	spherical	m	  	attribute
-SourcePosition:Units	degree, degree, metre	m	  	attribute
-EmitterPosition	[0 0 0]	m	eCI, eCM	double
-EmitterPosition:Type	cartesian	m	  	attribute
-EmitterPosition:Units	metre	m	  	attribute
-GLOBAL:DatabaseName		m	  	attribute	name of the database to which these data belong
-GLOBAL:ListenerShortName		m	  	attribute	ID of the subject from the database
-ListenerUp	[0 0 1]	m	IC, MC	double
-ListenerView	[1 0 0]	m	IC, MC	double
-ListenerView:Type	cartesian	m		attribute
-ListenerView:Units	metre	m		attribute
-Data.SOS	permute([0 0 0 1 0 0; 0 0 0 1 0 0], [3 1 2]);	m	mRn	double	Filter coefficients as SOS coefficients.
-Data.SamplingRate	48000	m	I	double	Sampling rate of the coefficients in Data.SOS and the delay in Data.Delay
-Data.SamplingRate:Units	hertz	m		attribute
-Data.Delay	[0 0]	m	IR, MR	double	Broadband delay (in samples resulting from SamplingRate)
diff --git a/sofar/conventions/source/SimpleHeadphoneIR_1.0.csv b/sofar/conventions/source/SimpleHeadphoneIR_1.0.csv
deleted file mode 100644
index 253d91da..00000000
--- a/sofar/conventions/source/SimpleHeadphoneIR_1.0.csv
+++ /dev/null
@@ -1,51 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	2.0	rm	 	attribute
-GLOBAL:SOFAConventions	SimpleHeadphoneIR	rm	  	attribute	Conventions for IRs with a 1-to-1 correspondence between emitter and receiver. The main application for this convention is to store headphone IRs recorded for each emitter and each ear.
-GLOBAL:SOFAConventionsVersion	1.0	rm	  	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Comment		m	 	attribute
-GLOBAL:DataType	FIR	rm	  	attribute	We will store IRs here
-GLOBAL:History			 	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:Organization		m	  	attribute
-GLOBAL:References				attribute
-GLOBAL:RoomType	free field	m	  	attribute	Room type is not relevant here
-GLOBAL:Origin				attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:Title		m		attribute
-ListenerPosition	[0 0 0] 	m	IC, MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ReceiverPosition	[0 0.09 0; 0 -0.09 0]	m	rCI, rCM	double
-ReceiverPosition:Type	cartesian	m	  	attribute
-ReceiverPosition:Units	metre	m	  	attribute
-SourcePosition	[0 0 0]	m	IC, MC	double	Default: Headphones are located at the position of the listener
-SourcePosition:Type	spherical	m	  	attribute
-SourcePosition:Units	degree, degree, metre	m	  	attribute
-EmitterPosition	[0 0.09 0; 0 -0.09 0]	m	eCI, eCM	double	Default: Reflects the correspondence of each emitter to each receiver
-EmitterPosition:Type	cartesian	m	  	attribute
-EmitterPosition:Units	metre	m	  	attribute
-Data.IR	[0 0]	m	mRn	double
-Data.SamplingRate	48000	m	I, M	double
-Data.SamplingRate:Units	hertz	m		attribute
-Data.Delay	[0 0]	m	IR, MR	double
-GLOBAL:DatabaseName		m	  	attribute	Correspondence to a database
-GLOBAL:ListenerShortName		m	  	attribute	Correspondence to a subject from the database
-GLOBAL:ListenerDescription		m		attribute	Narrative description of the listener (or mannequin)
-GLOBAL:SourceDescription		m		attribute	Narrative description of the headphones
-GLOBAL:SourceManufacturer		m		attribute	Name of the headphones manufacturer
-SourceManufacturer	{''}		MS	string	Optional M-dependent version of the attribute SourceManufucturer
-GLOBAL:SourceModel		m		attribute	Name of the headphone model. Must uniquely describe the headphones of the manufacturer
-SourceModel	{''}		MS	string	Optional M-dependent version of the attribute SourceModel
-GLOBAL:SourceURI		m		attribute	URI of the headphone specifications
-GLOBAL:ReceiverDescription		m		attribute	Narrative description of the microphones
-ReceiverDescription	{''}		MS	string	Optional M-dependent version of the attribute ReceiverDescription
-GLOBAL:EmitterDescription		m		attribute	Narrative description of the headphone drivers
-EmitterDescription	{''}		MS	string	Optional M-dependent version of the attribute EmitterDescription
-MeasurementDate	0		M	double	Optional M-dependent date and time of the measurement
diff --git a/sofar/conventions/source/SingleRoomDRIR_0.3.csv b/sofar/conventions/source/SingleRoomDRIR_0.3.csv
deleted file mode 100644
index 0658a79a..00000000
--- a/sofar/conventions/source/SingleRoomDRIR_0.3.csv
+++ /dev/null
@@ -1,47 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	1.0	rm	 	attribute
-GLOBAL:SOFAConventions	SingleRoomDRIR	rm	  	attribute	This convention stores arbitrary number of receivers while providing an information about the room. The main application is to store DRIRs for a single room.
-GLOBAL:SOFAConventionsVersion	0.3	rm	  	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Comment		m	 	attribute
-GLOBAL:DataType	FIR	rm	  	attribute
-GLOBAL:History			 	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:Organization		m	  	attribute
-GLOBAL:References				attribute
-GLOBAL:RoomType	reverberant	m	  	attribute
-GLOBAL:Origin				attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:Title		m		attribute
-ListenerPosition	[0 0 0] 	m	IC, MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ReceiverPosition	[0 0 0]	m	RCI, RCM	double
-ReceiverPosition:Type	cartesian	m	  	attribute
-ReceiverPosition:Units	metre	m	  	attribute
-SourcePosition	[0 0 0]	m	IC, MC	double
-SourcePosition:Type	cartesian	m	  	attribute
-SourcePosition:Units	metre	m	  	attribute
-EmitterPosition	[0 0 0]	m	eCI, eCM	double
-EmitterPosition:Type	cartesian	m	  	attribute
-EmitterPosition:Units	metre	m	  	attribute
-GLOBAL:DatabaseName		m	  	attribute
-GLOBAL:RoomDescription		m	  	attribute
-ListenerUp	[0 0 1]	m	IC, MC	double
-ListenerView	[1 0 0]	m	IC, MC	double
-ListenerView:Type	cartesian	m		attribute
-ListenerView:Units	metre	m		attribute
-SourceUp	[0 0 1]	m	IC, MC	double
-SourceView	[-1 0 0]	m	IC, MC	double
-SourceView:Type	cartesian	m		attribute
-SourceView:Units	metre	m		attribute
-Data.IR	[0]	m	mrn	double
-Data.SamplingRate	48000	m	I	double
-Data.SamplingRate:Units	hertz	m		attribute
-Data.Delay	[0]	m	IR, MR	double
diff --git a/sofar/conventions/source/SingleRoomMIMOSRIR_1.0.csv b/sofar/conventions/source/SingleRoomMIMOSRIR_1.0.csv
deleted file mode 100644
index 05c8ca88..00000000
--- a/sofar/conventions/source/SingleRoomMIMOSRIR_1.0.csv
+++ /dev/null
@@ -1,68 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	2.0	rm	 	attribute
-GLOBAL:SOFAConventions	SingleRoomMIMOSRIR	rm	  	attribute	Single-room multiple-input multiple-output spatial room impulse responses, depending on Emitters
-GLOBAL:SOFAConventionsVersion	1.0	rm	  	attribute
-GLOBAL:DataType	FIR-E	rm	  	attribute	Shall be FIR-E
-GLOBAL:RoomType	shoebox	m	  	attribute	Shall be 'shoebox' or 'dae'
-GLOBAL:Title		m		attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Organization		m	  	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:Comment			 	attribute
-GLOBAL:RoomDescription				attribute	narrative description of the room
-GLOBAL:History			 	attribute
-GLOBAL:References				attribute
-GLOBAL:Origin				attribute
-GLOBAL:ListenerShortName				attribute
-GLOBAL:ListenerDescription				attribute
-ListenerPosition	[0 0 0] 	m	MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ListenerView	[1 0 0]	m	IC, MC	double
-ListenerUp	[0 0 1]	m	IC, MC	double
-ListenerView:Type	cartesian	m		attribute
-ListenerView:Units	metre	m		attribute
-GLOBAL:ReceiverShortName				attribute
-GLOBAL:ReceiverDescription				attribute
-ReceiverPosition	[0 0 0]	m	IC, RCI, RCM	double
-ReceiverPosition:Type	spherical	m	  	attribute	Can be of any type enabling both spatially discrete and spatially continuous representations.
-ReceiverPosition:Units	degree, degree, metre	m	  	attribute
-ReceiverView	[1 0 0]		RCI, RCM	double
-ReceiverUp	[0 0 1]		RCI, RCM	double
-ReceiverView:Type	cartesian			attribute
-ReceiverView:Units	metre			attribute
-GLOBAL:SourceShortName				attribute
-GLOBAL:SourceDescription				attribute
-SourcePosition	[0 0 1]	m	MC	double
-SourcePosition:Type	cartesian	m	  	attribute
-SourcePosition:Units	metre	m	  	attribute
-SourceView	[1 0 0]	m	IC, MC	double
-SourceUp	[0 0 1]	m	IC, MC	double
-SourceView:Type	cartesian	m		attribute
-SourceView:Units	metre	m		attribute
-GLOBAL:EmitterShortName				attribute
-GLOBAL:EmitterDescription				attribute
-EmitterPosition	[0 0 0]	m	IC, ECI, ECM	double	Can be of any type enabling both spatially discrete and spatially continuous representations.
-EmitterPosition:Type	spherical	m	  	attribute
-EmitterPosition:Units	degree, degree, metre	m	  	attribute
-EmitterView	[1 0 0]		ECI, ECM	double
-EmitterUp	[0 0 1]		ECI, ECM	double
-EmitterView:Type	cartesian			attribute
-EmitterView:Units	metre			attribute
-Data.IR	0	m	mrne	double	Impulse responses
-Data.SamplingRate	48000	m	I, M	double	Sampling rate of the samples in Data.IR and Data.Delay
-Data.SamplingRate:Units	hertz	m		attribute	Unit of the sampling rate
-Data.Delay	0	m	IRI, MRI, MRE	double	Additional delay of each IR (in samples)
-RoomCornerA	[0 0 0]	m	IC, MC	double
-RoomCornerB	[1 2 3]	m	IC, MC	double
-RoomCorners	0		II	double	The value of this attribute is to be ignored. It only exist to for RoomCorners:Type and RoomCorners:Units
-RoomCorners:Type	cartesian	m		attribute
-RoomCorners:Units	metre	m		attribute
-GLOBAL:DatabaseName		m		attribute	Name of the database. Used for classification of the data.
diff --git a/sofar/conventions/source/SingleRoomSRIR_1.0.csv b/sofar/conventions/source/SingleRoomSRIR_1.0.csv
deleted file mode 100644
index 80393ea5..00000000
--- a/sofar/conventions/source/SingleRoomSRIR_1.0.csv
+++ /dev/null
@@ -1,68 +0,0 @@
-Name	Default	Flags	Dimensions	Type	Comment
-GLOBAL:Conventions	SOFA	rm	  	attribute
-GLOBAL:Version	2.0	rm	 	attribute
-GLOBAL:SOFAConventions	SingleRoomSRIR	rm	  	attribute	For measuring SRIRs in a single room with a single excitation source (e.g., a loudspeaker) and a listener containing an arbitrary number of omnidirectional receivers (e.g., a microphone array).
-GLOBAL:SOFAConventionsVersion	1.0	rm	  	attribute
-GLOBAL:DataType	FIR	rm	  	attribute	Shall be FIR
-GLOBAL:RoomType	shoebox	m	  	attribute	Shall be 'shoebox' or 'dae'
-GLOBAL:Title		m		attribute
-GLOBAL:DateCreated		m	 	attribute
-GLOBAL:DateModified		m	 	attribute
-GLOBAL:APIName		rm	  	attribute
-GLOBAL:APIVersion		rm	  	attribute
-GLOBAL:AuthorContact		m	  	attribute
-GLOBAL:Organization		m	  	attribute
-GLOBAL:License	No license provided, ask the author for permission	m	  	attribute
-GLOBAL:ApplicationName			  	attribute
-GLOBAL:ApplicationVersion			  	attribute
-GLOBAL:Comment			 	attribute
-GLOBAL:RoomDescription				attribute	narrative description of the room
-GLOBAL:History			 	attribute
-GLOBAL:References				attribute
-GLOBAL:Origin				attribute
-GLOBAL:ListenerShortName				attribute
-GLOBAL:ListenerDescription				attribute
-ListenerPosition	[0 0 0] 	m	MC	double
-ListenerPosition:Type	cartesian	m	  	attribute
-ListenerPosition:Units	metre	m	  	attribute
-ListenerView	[1 0 0]	m	IC, MC	double
-ListenerUp	[0 0 1]	m	IC, MC	double
-ListenerView:Type	cartesian	m		attribute
-ListenerView:Units	metre	m		attribute
-GLOBAL:ReceiverShortName				attribute
-GLOBAL:ReceiverDescription				attribute
-ReceiverPosition	[0 0 0]	m	IC, RCI, RCM	double
-ReceiverPosition:Type	spherical	m	  	attribute	Can be of any type enabling both spatially discrete and spatially continuous representations.
-ReceiverPosition:Units	degree, degree, metre	m	  	attribute
-ReceiverView	[1 0 0]		RCI, RCM	double
-ReceiverUp	[0 0 1]		RCI, RCM	double
-ReceiverView:Type	cartesian			attribute
-ReceiverView:Units	metre			attribute
-GLOBAL:SourceShortName				attribute
-GLOBAL:SourceDescription				attribute
-SourcePosition	[0 0 1]	m	MC	double
-SourcePosition:Type	cartesian	m	  	attribute
-SourcePosition:Units	metre	m	  	attribute
-SourceView	[1 0 0]	m	IC, MC	double
-SourceUp	[0 0 1]	m	IC, MC	double
-SourceView:Type	cartesian	m		attribute
-SourceView:Units	metre	m		attribute
-GLOBAL:EmitterShortName				attribute
-GLOBAL:EmitterDescription				attribute
-EmitterPosition	[0 0 0]	m	eCI, eCM	double
-EmitterPosition:Type	spherical	m	  	attribute	Shall be 'cartesian' or 'spherical', restricting to spatially discrete emitters.
-EmitterPosition:Units	degree, degree, metre	m	  	attribute
-EmitterView	[1 0 0]		ECI, ECM	double
-EmitterUp	[0 0 1]		ECI, ECM	double
-EmitterView:Type	cartesian			attribute	Shall be 'cartesian' or 'spherical', restricting to spatially discrete emitters.
-EmitterView:Units	metre			attribute
-Data.IR	0	m	mrn	double	Impulse responses
-Data.SamplingRate	48000	m	I, M	double	Sampling rate of the samples in Data.IR and Data.Delay
-Data.SamplingRate:Units	hertz	m		attribute	Unit of the sampling rate
-Data.Delay	0	m	IR, MR	double	Additional delay of each IR (in samples)
-RoomCornerA	[0 0 0]		IC, MC	double
-RoomCornerB	[1 2 3]		IC, MC	double
-RoomCorners	0		II	double	The value of this attribute is to be ignored. It only exist to for RoomCorners:Type and RoomCorners:Units
-RoomCorners:Type	cartesian			attribute	Type of coordinate system for RoomCornerA and RoomCornerB
-RoomCorners:Units	metre			attribute	Unit of coordinate system for RoomCornerA and RoomCornerB
-GLOBAL:DatabaseName		m		attribute	Name of the database. Used for classification of the data.
diff --git a/sofar/io.py b/sofar/io.py
new file mode 100644
index 00000000..6300c1e1
--- /dev/null
+++ b/sofar/io.py
@@ -0,0 +1,370 @@
+import contextlib
+import os
+import numpy as np
+from netCDF4 import Dataset, chartostring, stringtochar
+import warnings
+import packaging
+import sofar as sf
+from .utils import _verify_convention_and_version, _atleast_nd
+
+
+def read_sofa(filename, verify=True, verbose=True):
+    """
+    Read SOFA file from disk and convert it to SOFA object.
+
+    Numeric data is returned as floats or numpy float arrays unless they have
+    missing data, in which case they are returned as numpy masked arrays.
+
+    Parameters
+    ----------
+    filename : str
+        The filename. '.sofa' is appended to the filename, if it is not
+        explicitly given.
+    verify : bool, optional
+        Verify and update the SOFA object by calling :py:func:`~Sofa.verify`.
+        This helps to find potential errors in the default values and is thus
+        recommended. If reading a file does not work, try to call `Sofa` with
+        ``verify=False``. The default is ``True``.
+    verbose : bool, optional
+        Print the names of detected custom variables and attributes. The
+        default is ``True``
+
+    Returns
+    -------
+    sofa : Sofa
+        The SOFA object filled with the default values of the convention.
+
+    Notes
+    -----
+
+    1. Missing dimensions are appended when writing the SOFA object to disk.
+       E.g., if ``sofa.Data_IR`` is of shape (1, 2) it is written as an array
+       of shape (1, 2, 1) because the SOFA standard AES69-2020 defines it as a
+       three dimensional array with the dimensions (`M: measurements`,
+       `R: receivers`, `N: samples`)
+    2. When reading data from a SOFA file, array data is always returned as
+       numpy arrays and singleton trailing dimensions are discarded (numpy
+       default). I.e., ``sofa.Data_IR`` will again be an array of shape (1, 2)
+       after writing and reading to and from disk.
+    3. One dimensional arrays with only one element will be converted to scalar
+       values. E.g. ``sofa.Data_SamplingRate`` is stored as an array of shape
+       (1, ) inside SOFA files (according to the SOFA standard AES69-2020) but
+       will be a scalar inside SOFA objects after reading from disk.
+    """
+
+    # check the filename
+    if not filename.endswith('.sofa'):
+        raise ValueError("Filename must end with .sofa")
+    if not os.path.isfile(filename):
+        raise ValueError(f"{filename} does not exist")
+
+    # attributes that are skipped
+    skip = ["_Encoding"]
+
+    # init list of all and custom attributes
+    all_attr = []
+    custom = []
+
+    # open new NETCDF4 file for reading
+    with Dataset(filename, "r", format="NETCDF4") as file:
+
+        # get convention name and version
+        convention = getattr(file, "SOFAConventions")
+        all_attr.append("GLOBAL_SOFAConventions")
+        version_in = getattr(file, "SOFAConventionsVersion")
+        all_attr.append("GLOBAL_SOFAConventionsVersion")
+
+        # check if convention and version exist
+        version_out = _verify_convention_and_version(
+            version_in, version_in, convention)
+
+        # get SOFA object with default values
+        sofa = sf.Sofa(convention, version=version_out, verify=verify)
+
+        # allow writing read only attributes
+        sofa._protected = False
+
+        # load global attributes
+        for attr in file.ncattrs():
+
+            if attr in ["SOFAConventionsVersion", "SOFAConventions"]:
+                # convention and version were already set above
+                continue
+
+            value = getattr(file, attr)
+            all_attr.append(f"GLOBAL_{attr}")
+
+            if not hasattr(sofa, f"GLOBAL_{attr}"):
+                sofa._add_custom_api_entry(
+                    f"GLOBAL_{attr}", value, None, None, "attribute")
+                custom.append(f"GLOBAL_{attr}")
+                sofa._protected = False
+            else:
+                setattr(sofa, f"GLOBAL_{attr}", value)
+
+        # load data
+        for var in file.variables.keys():
+
+            value = _format_value_from_netcdf(file[var][:], var)
+            all_attr.append(var.replace(".", "_"))
+
+            if hasattr(sofa, var.replace(".", "_")):
+                setattr(sofa, var.replace(".", "_"), value)
+            else:
+                dimensions = "".join(list(file[var].dimensions))
+                # SOFA only uses dtypes 'double' and 'S1' but netCDF has more
+                dtype = "string" if file[var].datatype == "S1" else "double"
+                sofa._add_custom_api_entry(var.replace(".", "_"), value, None,
+                                           dimensions, dtype)
+                custom.append(var.replace(".", "_"))
+                sofa._protected = False
+
+            # load variable attributes
+            for attr in [a for a in file[var].ncattrs() if a not in skip]:
+
+                value = getattr(file[var], attr)
+                all_attr.append(var.replace(".", "_") + "_" + attr)
+
+                if not hasattr(sofa, var.replace(".", "_") + "_" + attr):
+                    sofa._add_custom_api_entry(
+                        var.replace(".", "_") + "_" + attr, value, None,
+                        None, "attribute")
+                    custom.append(var.replace(".", "_") + "_" + attr)
+                    sofa._protected = False
+                else:
+                    setattr(sofa, var.replace(".", "_") + "_" + attr, value)
+
+    # remove fields from initial Sofa object that were not contained in NetCDF
+    # file (initial Sofa object contained mandatory and optional fields)
+    attrs = [attr for attr in sofa.__dict__.keys() if not attr.startswith("_")]
+    for attr in attrs:
+        if attr not in all_attr:
+            delattr(sofa, attr)
+
+    # do not allow writing read only attributes any more
+    sofa._protected = True
+
+    # notice about custom entries
+    if custom and verbose:
+        print(("SOFA file contained custom entries\n"
+               "----------------------------------\n"
+               f"{', '.join(custom)}"))
+
+    # update api
+    if verify:
+        try:
+            sofa.verify(mode="read")
+        except: # noqa (No error handling - just improved verbosity)
+            raise ValueError((
+                "The SOFA object could not be verified, maybe due to erroneous"
+                " data. Call sofa=sofar.read_sofa(filename, verify=False) and "
+                "then sofa.verify() to get more information"))
+
+    return sofa
+
+
+def write_sofa(filename: str, sofa: sf.Sofa, compression=4):
+    """
+    Write a SOFA object to disk as a SOFA file.
+
+    Parameters
+    ----------
+    filename : str
+        The filename. '.sofa' is appended to the filename, if it is not
+        explicitly given.
+    sofa : object
+        The SOFA object that is written to disk
+    compression : int
+        The level of compression with ``0`` being no compression and ``9``
+        being the best compression. The default of ``9`` optimizes the file
+        size but increases the time for writing files to disk.
+
+    Notes
+    -----
+
+    1. Missing dimensions are appended when writing the SOFA object to disk.
+       E.g., if ``sofa.Data_IR`` is of shape (1, 2) it is written as an array
+       of shape (1, 2, 1) because the SOFA standard AES69-2020 defines it as a
+       three dimensional array with the dimensions (`M: measurements`,
+       `R: receivers`, `N: samples`)
+    2. When reading data from a SOFA file, array data is always returned as
+       numpy arrays and singleton trailing dimensions are discarded (numpy
+       default). I.e., ``sofa.Data_IR`` will again be an array of shape (1, 2)
+       after writing and reading to and from disk.
+    3. One dimensional arrays with only one element will be converted to scalar
+       values. E.g. ``sofa.Data_SamplingRate`` is stored as an array of shape
+       (1, ) inside SOFA files (according to the SOFA standard AES69-2020) but
+       will be a scalar inside SOFA objects after reading from disk.
+    """
+    _write_sofa(filename, sofa, compression, verify=True)
+
+
+def _write_sofa(filename: str, sofa: sf.Sofa, compression=4, verify=True):
+    """
+    Private write function for writing invalid SOFA files for testing. See
+    write_sofa for documentation.
+    """
+
+    # check the filename
+    if not filename.endswith('.sofa'):
+        raise ValueError("Filename must end with .sofa")
+
+    # check if the latest version is used for writing and warn otherwise
+    # if case required for writing SOFA test data that violates the conventions
+    if sofa.GLOBAL_SOFAConventions != "invalid-value":
+        latest = sf.Sofa(sofa.GLOBAL_SOFAConventions)
+        latest = latest.GLOBAL_SOFAConventionsVersion
+        current = sofa.GLOBAL_SOFAConventionsVersion
+
+        if packaging.version.parse(current) < packaging.version.parse(latest):
+            warnings.warn(("Writing SOFA object with outdated Convention "
+                           f"version {current}. Use version='latest' to write "
+                           f"data with version {latest}."))
+
+    # setting the netCDF compression parameter
+    use_zlib = compression != 0
+
+    # update the dimensions
+    if verify:
+        sofa.verify(mode="write")
+
+    # list of all attribute names
+    all_keys = [key for key in sofa.__dict__.keys() if not key.startswith("_")]
+
+    # open new NETCDF4 file for writing
+    with Dataset(filename, "w", format="NETCDF4") as file:
+
+        # write dimensions
+        for dim in sofa._api:
+            file.createDimension(dim, sofa._api[dim])
+
+        # write global attributes
+        keys = [key for key in all_keys if key.startswith("GLOBAL_")]
+        for key in keys:
+            setattr(file, key[7:], str(getattr(sofa, key)))
+
+        # write data
+        for key in all_keys:
+
+            # skip attributes
+            # Note: This definition of attribute is blurry:
+            # lax definition:
+            #   sofa._convention[key]["type"] == "attribute":
+            # strict definition:
+            #   ("_" in key and not key.startswith("Data_")) or \
+            #       key.count("_") > 1
+            #
+            # The strict definition is implicitly included in the SOFA standard
+            # since underscores only occur for variables starting with Data_
+            if sofa._convention[key]["type"] == "attribute":
+                continue
+
+            # get the data and type and shape
+            value, dtype = _format_value_for_netcdf(
+                getattr(sofa, key), key, sofa._convention[key]["type"],
+                sofa._dimensions[key], sofa._api["S"])
+
+            # create variable and write data
+            shape = tuple(list(sofa._dimensions[key]))
+            tmp_var = file.createVariable(
+                key.replace("Data_", "Data."), dtype, shape,
+                zlib=use_zlib, complevel=compression)
+            if dtype == "f8":
+                tmp_var[:] = value
+            else:
+                tmp_var[:] = stringtochar(value)
+                tmp_var._Encoding = "ascii"
+
+            # write variable attributes
+            sub_keys = [k for k in all_keys if k.startswith(f"{key}_")]
+            for sub_key in sub_keys:
+                setattr(tmp_var, sub_key[len(key)+1:],
+                        str(getattr(sofa, sub_key)))
+
+
+def _format_value_for_netcdf(value, key, dtype, dimensions, S):
+    """
+    Format value from SOFA object for saving in a NETCDF4 file.
+
+    Parameters
+    ----------
+    value : str, array like
+        The value to be formatted
+    key : str
+        The name of the current attribute. Needed for verbose errors.
+    dtype : str
+        The the data type of value
+    dimensions : str
+        The intended dimensions from ``sofa.dimensions``
+    S : int
+        Length of the string array.
+
+    Returns
+    -------
+    value : str, numpy array
+        The formatted value.
+    netcdf_dtype : str
+        The data type as a string for writing to a NETCDF4 file ('attribute',
+        'f8', or 'S1').
+    """
+    # copy value
+    with contextlib.suppress(AttributeError):
+        value = value.copy()
+
+    # parse data
+    if dtype == "attribute":
+        value = str(value)
+        netcdf_dtype = "attribute"
+    elif dtype == "double":
+        value = _atleast_nd(value, len(dimensions))
+        netcdf_dtype = "f8"
+    elif dtype == "string":
+        value = np.array(value, dtype=f"S{str(S)}")
+        value = _atleast_nd(value, len(dimensions))
+        netcdf_dtype = 'S1'
+    else:
+        raise ValueError(f"Unknown type {dtype} for {key}")
+
+    return value, netcdf_dtype
+
+
+def _format_value_from_netcdf(value, key):
+    """
+    Format value from NETCDF4 file for saving in a SOFA object
+
+    Parameters
+    ----------
+    value : np.array of dtype float or S
+        The value to be formatted
+    key : str
+        The variable name of the current value. Needed for verbose errors.
+
+    Returns
+    -------
+    value : str, number, numpy array
+        The formatted value.
+    """
+
+    if "float" in str(value.dtype) or "int" in str(value.dtype):
+        if np.ma.is_masked(value):
+            warnings.warn(f"Entry {key} contains missing data")
+        else:
+            # Convert to numpy array or scalar
+            value = np.asarray(value)
+    elif str(value.dtype)[1] in ["S", "U"]:
+        # string arrays are stored in masked arrays with empty strings '' being
+        # masked. Convert to regular arrays with unmasked empty strings
+        if str(value.dtype)[1] == "S":
+            value = chartostring(value, encoding="ascii")
+        value = np.atleast_1d(value).astype("U")
+    else:
+        raise TypeError(
+            f"{key}: value.dtype is {value.dtype} but must be float, S or, U")
+
+    # convert arrays to scalars if they do not store data that is usually used
+    # as scalar metadata, e.g., the SamplingRate
+    data_keys = ["Data_IR", "Data_Real", "Data_Imag", "Data_SOS" "Data_Delay"]
+    if value.size == 1 and key not in data_keys:
+        value = value[0]
+
+    return value
diff --git a/sofar/sofa.py b/sofar/sofa.py
new file mode 100644
index 00000000..665e905c
--- /dev/null
+++ b/sofar/sofa.py
@@ -0,0 +1,1771 @@
+import os
+import re
+import json
+from datetime import datetime
+import platform
+import numpy as np
+import warnings
+from copy import deepcopy
+import sofar as sf
+from .utils import (_nd_newaxis, _atleast_nd, _get_conventions,
+                    _verify_convention_and_version)
+
+
+class Sofa():
+    """Create a new SOFA object.
+
+    Parameters
+    ----------
+    convention : str
+        The name of the convention from which the SOFA file is created. See
+        :py:func:`~sofar.list_conventions`.
+    mandatory : bool, optional
+        If ``True``, only the mandatory data of the convention will be
+        returned. The default is ``False``, which returns mandatory and
+        optional data.
+    version : str, optional
+        The version of the convention as a string, e.g., ``' 2.0'``. The
+        default is ``'latest'``. Also see :py:func:`~sofar.list_conventions`.
+    verify : bool, optional
+        Verify the SOFA object by calling :py:func:`~Sofa.verify`. This helps
+        to find potential errors in the default values and is thus recommended
+        If creating a file does not work, try to call `Sofa` with
+        ``verify=False``. The default is ``True``.
+
+    Returns
+    -------
+    sofa : Sofa
+        A SOFA object filled with the default values of the convention.
+
+    Examples
+    --------
+    Create a new SOFA object with default values
+
+    .. code-block:: python
+
+        import sofar as sf
+
+        # create SOFA object
+        sofa = sf.Sofa("SimpleFreeFieldHRIR")
+
+    Add data as a list
+
+    .. code-block:: python
+
+        sofa.Data_IR = [1, 1]
+
+    Data can be entered as numbers, numpy arrays or lists. Note the following
+
+    1. Lists are converted to numpy arrays with at least two dimensions, i.e.,
+       ``sofa.Data_IR`` is converted to a numpy array of shape (1, 2)
+    2. Missing dimensions are appended when writing the SOFA object to disk,
+       i.e., ``sofa.Data_IR`` is written as an array of shape (1, 2, 1) because
+       the SOFA standard AES69-2020 defines it as a three dimensional array
+       with the dimensions (`M: measurements`, `R: receivers`, `N: samples`)
+    3. When reading data from a SOFA file, array data is always returned as
+       numpy arrays and singleton trailing dimensions are discarded (numpy
+       default). I.e., ``sofa.Data_IR`` will again be an array of shape (1, 2)
+       after writing and reading to and from disk.
+    4. One dimensional arrays with only one element will be converted to scalar
+       values. E.g. ``sofa.Data_SamplingRate`` is stored as an array of shape
+       (1, ) inside SOFA files (according to the SOFA standard AES69-2020) but
+       will be a scalar inside SOFA objects after reading from disk.
+
+
+    For more examples refer to the `Quick tour of SOFA and sofar` at
+    https://sofar.readthedocs.io/en/latest/
+    """
+
+    # these have to be set here, because they are used in __setattr__ and
+    # Python checks if they exist upon class creation
+
+    # don't allow adding attributes and deleting/writing read only attributes
+    _protected = False
+    # list of read only attributes (filled upon init)
+    _read_only_attr = []
+
+    def __init__(self, convention, mandatory=False, version="latest",
+                 verify=True):
+        """See class docstring"""
+
+        # get convention
+        self._convention = self._load_convention(convention, version)
+
+        # update read only attributes
+        self._read_only_attr = [
+            key for key in self._convention.keys()
+            if self._read_only(self._convention[key]["flags"])]
+
+        # add attributes with default values
+        self._convention_to_sofa(mandatory)
+
+        # add and update the API
+        # (mandatory=False can not be verified because some conventions have
+        # default values that have optional variables as dependencies)
+        if verify and not mandatory:
+            self.verify(mode="read")
+
+        self._protected = True
+
+    def __setattr__(self, name: str, value):
+        # don't allow new attributes to be added outside the class
+        if self._protected and not hasattr(self, name):
+            raise TypeError(f"{name} is an invalid attribute")
+
+        # don't allow setting read only attributes
+        if name in self._read_only_attr and self._protected:
+            raise TypeError(f"{name} is a read only attribute")
+
+        # convert to numpy array or scalar
+        if not isinstance(value, (str, dict, np.ndarray)):
+            value = np.atleast_2d(value)
+            if value.size == 1:
+                value = value.flatten()[0]
+
+        super.__setattr__(self, name, value)
+
+    def __delattr__(self, name: str):
+        # can't delete non existing attributes
+        if not hasattr(self, name):
+            raise TypeError(f"{name} is not an attribute")
+        # delete anything if not frozen, delete non mandatory
+        if not self._protected or \
+                not self._mandatory(self._convention[name]["flags"]):
+            super().__delattr__(name)
+
+            # check if custom field as to be deleted
+            if hasattr(self, "_custom"):
+                if name in self._custom:
+                    self._custom.pop(name)
+        else:
+            raise TypeError(
+                f"{name} is a mandatory attribute that can not be deleted")
+
+    def __repr__(self):
+        return (f"sofar.SOFA object: {self.GLOBAL_SOFAConventions} "
+                f"{self.GLOBAL_SOFAConventionsVersion}")
+
+    @property
+    def list_dimensions(self):
+        """
+        Print the dimensions of the SOFA object
+
+        See :py:func:`~Sofa.inspect` to see the shapes of the data inside the
+        SOFA object and :py:func:`~Sofa.get_dimension` to get the size/value
+        of a specific dimensions as integer number.
+
+        The SOFA file standard defines the following dimensions that are used
+        to define the shape of the data entries:
+
+        M
+            number of measurements
+        N
+            number of samles, frequencies, SOS coefficients
+            (depending on self.GLOBAL_DataType)
+        R
+            Number of receivers or SH coefficients
+            (depending on ReceiverPosition_Type)
+        E
+            Number of emitters or SH coefficients
+            (depending on EmitterPosition_Type)
+        S
+            Maximum length of a string in a string array
+        C
+            Size of the coordinate dimension. This is always three.
+        I
+            Single dimension. This is always one.
+
+        """
+
+        # Check if the dimensions can be updated
+        self._update_dimensions()
+
+        # get verbose description for dimesion N
+        if self.GLOBAL_DataType.startswith("FIR"):
+            N_verbose = "samples"
+        elif self.GLOBAL_DataType.startswith("TF"):
+            N_verbose = "frequencies"
+        elif self.GLOBAL_DataType.startswith("SOS"):
+            N_verbose = "SOS coefficients"
+
+        # get verbose description for dimensions R and E
+        R_verbose = "receiver spherical harmonics coefficients" if \
+            'harmonic' in self.ReceiverPosition_Type else "receiver"
+        E_verbose = "emitter spherical harmonics coefficients" if \
+            'harmonic' in self.EmitterPosition_Type else "emitter"
+
+        dimensions = {
+            "M": "measurements",
+            "N": N_verbose,
+            "R": R_verbose,
+            "E": E_verbose,
+            "S": "maximum string length",
+            "C": "coordinate dimensions, fixed",
+            "I": "single dimension, fixed"}
+
+        info_str = ""
+        for key, value in self._api.items():
+            dim_info = dimensions[key] if key in dimensions \
+                else "custom dimension"
+
+            info_str += f"{key} = {value} {dim_info}"
+
+            if dim_info != "custom ":
+                for key2, value2 in self._convention.items():
+                    dim = value2["dimensions"]
+                    if dim is not None and key.lower() in dim:
+                        info_str += \
+                            f" (set by {key2} of dimension {dim.upper()})"
+                        break
+
+            info_str += "\n"
+
+        print(info_str)
+
+    def get_dimension(self, dimension):
+        """
+        Get size of a SOFA dimension
+
+        SOFA dimensions specify the shape of the data contained in a SOFA
+        object. For a list of all dimensions see :py:func:`~list_dimensions`.
+
+        Parameters
+        ----------
+        dimension : str
+            The dimension as a string, e.g., ``'N'``.
+
+        Returns
+        -------
+        size : int
+            the size of the queried dimension.
+        """
+
+        # Check if the dimensions can be updated
+        self._update_dimensions()
+
+        if dimension not in self._api:
+            raise ValueError((
+                f"{dimension} is not a valid dimension. "
+                "See Sofa.list_dimensions for a list of valid dimensions."))
+
+        return self._api[dimension]
+
+    def _update_dimensions(self):
+        """
+        Call verify and raise an error if the dimensions could not be updated.
+
+        used in Sofa.list_dimensions and Sofa.get_dimension
+        """
+
+        issues = self.verify(issue_handling="return")
+        if issues is not None and ("data of wrong type" in issues or
+                                   "variables of wrong shape" in issues or
+                                   not hasattr(self, "_api")):
+            raise ValueError(("Dimensions can not be shown because variables "
+                              "of wrong type or shape were detected. "
+                              "Call Sofa.verify() for more information."))
+
+    def info(self, info="all"):
+        """
+        Print information about the convention of a SOFA object.
+
+        Prints the variable type (attribute, double, string), shape, flags
+        (mandatory, read only) and comment (if any) for each or selected
+        entries.
+
+        Parameters
+        ----------
+        info : str
+            Specifies the kind of information that is printed:
+
+            ``'all'`` ``'mandatory'`` ``'optional'`` ``'read only'`` ``'data'``
+                Print the name, type, shape, and flags and comment for all or
+                selected entries of the SOFA object. ``'data'`` does not show
+                entries of type attribute.
+            key
+                If key is the name of an object attribute, all information for
+                attribute will be printed.
+        """
+
+        # update the private attribute `_convention` to make sure the required
+        # meta data is in place
+        self._update_convention(version="match")
+
+        # list of all attributes
+        keys = [k for k in self.__dict__.keys() if not k.startswith("_")]
+
+        # start printing the information
+        info_str = (
+            f"{self.GLOBAL_SOFAConventions} "
+            F"{self.GLOBAL_SOFAConventionsVersion} "
+            f"(SOFA version {self.GLOBAL_Version})\n")
+        info_str += "-" * len(info_str) + "\n"
+
+        if info in ["all", "mandatory", "optional", "read only", "data"]:
+
+            info_str += f"showing {info} entries : type (shape), flags\n\n"
+
+            for key in keys:
+
+                # check if field should be skipped
+                flags = self._convention[key]["flags"]
+                if (not self._mandatory(flags) and info == "mandatory") \
+                        or \
+                        (self._mandatory(flags) and info == "optional") \
+                        or \
+                        (not self._read_only(flags) and info == "read only") \
+                        or \
+                        (self._convention[key]['type'] == "attribute" and
+                         info == "data"):
+                    continue
+
+                info_str += f"{key} : {self._convention[key]['type']}"
+
+                if self._convention[key]['dimensions']:
+                    info_str += \
+                        f" ({self._convention[key]['dimensions'].upper()})"
+
+                if self._mandatory(flags):
+                    info_str += ", mandatory"
+                else:
+                    info_str += ", optional"
+                if self._read_only(flags):
+                    info_str += ", read only"
+
+                if self._convention[key]['comment']:
+                    info_str += f"\n    {self._convention[key]['comment']}\n"
+                else:
+                    info_str += "\n"
+
+        elif info in keys:
+
+            for key in [k for k in keys if info in k]:
+                comment = str(self._convention[key]['comment'])
+
+                info_str += (
+                    f"{key}\n"
+                    f"    type: {self._convention[key]['type']}\n"
+                    f"    mandatory: "
+                    f"{self._mandatory(self._convention[key]['flags'])}\n"
+                    f"    read only: "
+                    f"{self._read_only(self._convention[key]['flags'])}\n"
+                    f"    default: {self._convention[key]['default']}\n"
+                    f"    shape: "
+                    f"{str(self._convention[key]['dimensions']).upper()}\n"
+                    f"    comment: {comment}\n")
+        else:
+            raise ValueError(f"info='{info}' is invalid")
+
+        print(info_str)
+
+    def inspect(self, file=None, issue_handling="print"):
+        """
+        Get information about data inside a SOFA object.
+
+        Prints the values of all attributes and variables with six or less
+        entries and the shapes and type of all numeric and string variables.
+        When printing the values of arrays, single dimensions are discarded for
+        easy of display, i.e., an array of shape (1, 3, 2) will be displayed as
+        an array of shape (3, 2).
+
+        Parameters
+        ----------
+        file : str
+            Full path of a file under which the information is to be stored in
+            plain text. The default ``None`` only print the information to the
+            console.
+        issue_handling : str, optional
+            Defines how issues detected during verification of the SOFA object
+            are handeled (see :py:func:`~sofar.sofar.Sofa.verify`)
+
+            ``'raise'``
+                Warnings and errors are raised if issues are detected
+            ``'print'``
+                Issues are printed without raising warnings and errors
+            ``'return'``
+                Issues are returned as string but neither raised nor printed
+            ``'ignore'``
+                Issues are ignored, i.e., not raised, printed, or returned.
+
+            The default is ``print'``.
+        """
+
+        # update the private attribute `_convention` to make sure the required
+        # meta data is in place
+        self.verify(issue_handling=issue_handling)
+
+        # list of all attributes
+        keys = [k for k in self.__dict__.keys() if not k.startswith("_")]
+
+        # start printing the information
+        info_str = (
+            f"{self.GLOBAL_SOFAConventions} "
+            F"{self.GLOBAL_SOFAConventionsVersion} "
+            f"(SOFA version {self.GLOBAL_Version})\n")
+        info_str += "-" * len(info_str) + "\n"
+
+        for key in keys:
+
+            info_str += key + " : "
+            value = getattr(self, key)
+
+            # information for attributes and scalars
+            if self._convention[key]["type"] == "attribute" or value.size == 1:
+                info_str += str(value) + "\n"
+            # information for variables
+            else:
+                # get shape and dimension
+                shape = value.shape
+                dimension = self._dimensions[key]
+
+                # pad shape if required (trailing single dimensions are
+                # discarded following the numpy default)
+                while len(shape) < len(dimension):
+                    shape += (1, )
+
+                # make verbose shape, e.g., '(M=100, R=2, N=128, '
+                shape_verbose = "("
+                for s, d in zip(shape, dimension):
+                    shape_verbose += f"{d}={s}, "
+
+                # add shape information
+                info_str += shape_verbose[:-2] + ")\n"
+                # add value information if not too much
+                if value.size < 7:
+                    info_str += "  " + \
+                        str(np.squeeze(value)).replace("\n", "\n  ") + "\n"
+
+        # write to text file
+        if file is not None:
+            with open(file, 'w') as f_id:
+                f_id.write(info_str + "\n")
+
+        # output to console
+        print(info_str)
+
+    def add_missing(self, mandatory=True, optional=True, verbose=True):
+        """
+        Add missing data with default values.
+
+        Data might be missing in SOFA objects if the creator did not include it
+        or if a new data was suggested for a newer version of a SOFA
+        convention. Use this function to add the data with its default values.
+
+        mandatory : Bool
+            Add missing mandatory data. The default is ``True``.
+        optional : Bool
+            Add missing optional data. The default is ``True``.
+        verbose : Bool
+            Print the information about added data to the console. The default
+            is ``True``.
+        """
+
+        # initialize
+        self._update_convention(version="match")
+        added = "Added the following missing data with their default values:\n"
+
+        # current data
+        keys = [key for key in self.__dict__.keys() if not key.startswith("_")]
+
+        self._protected = False
+
+        # loop data in convention
+        for key in self._convention.keys():
+            is_mandatory = self._mandatory(self._convention[key]["flags"])
+            add_data = (is_mandatory and mandatory) or \
+                (not is_mandatory and optional)
+            # add with default
+            if key not in keys and add_data:
+                setattr(self, key, self._convention[key]["default"])
+                added += f"- {key} "
+                added += f"({'mandatory' if is_mandatory else 'optional'})\n"
+
+        self._protected = True
+
+        if verbose:
+            if "-" in added:
+                print(added)
+            else:
+                print("All mandatory data contained.")
+
+    def add_variable(self, name, value, dtype, dimensions):
+        """
+        Add custom variable to the SOFA object, i.e., numeric or string arrays.
+
+        Parameters
+        ----------
+        name : str
+            Name of the new variable.
+        value : any
+            value to be added (see `dtype` for restrictions).
+        dtype : str
+            Type of the entry to be added in netCDF style:
+
+            ``'double'``
+                Use this to store numeric data that can be provided as number
+                list or numpy array.
+            ``'string'``
+                Use this to store string variables as numpy string arrays of
+                type ``'U'`` or ``'S'``.
+
+        dimensions : str
+            The shape of the new entry as a string. See
+            ``self.info('dimensions')``.
+
+        Examples
+        --------
+        .. code-block:: python
+
+            import sofar as sf
+            sofa = sf.Sofa("GeneralTF")
+
+            # add numeric data
+            sofa.add_variable("Temperature", 25.1, "double", "MI")
+
+            # add GLOBAL and Variable attribtue
+            sofa.add_entry(
+                "GLOBAL_DateMeasured", "8.08.2021", "attribute", None)
+            sofa.add_entry(
+                "Temperature_Units", "degree Celsius", "attribute", None)
+
+            # add a string data
+            sofa.add_variable(
+                "Comment", "Measured with wind screen", "string", "MS")
+        """
+
+        self._add_entry(name, value, dtype, dimensions)
+
+    def add_attribute(self, name, value):
+        """
+        Add custom attribute to the SOFA object.
+
+        Parameters
+        ----------
+        name : str
+            Name of the new attribute.
+        value : str
+            value to be added.
+
+        Examples
+        --------
+        .. code-block:: python
+
+            import sofar as sf
+            sofa = sf.Sofa("GeneralTF")
+
+            # add GLOBAL and Variable attribtue
+            sofa.add_attribute("GLOBAL_DateMeasured", "8.08.2021")
+            sofa.add_attribute("Data_Real_Units", "Pascal")
+
+        """
+
+        self._add_entry(name, value, 'attribute', None)
+
+    def delete(self, name):
+        """
+        Delete variable or attribute from SOFA object.
+
+        Note that mandatory data can not be deleted. Call
+        :py:func:`Sofa.info("optional") <sofar.sofar.Sofa.info>` to list all
+        optional variables and attributes.
+
+        Parameters
+        ----------
+        name : str
+            Name of the variable or attribute to be deleted
+        """
+        delattr(self, name)
+
+    def _add_entry(self, name, value, dtype, dimensions):
+        """
+        Add custom data to a SOFA object. See add_variable and add_attribute
+        for more information.
+        """
+
+        # check input
+        if hasattr(self, name):
+            raise ValueError(f"Entry {name} already exists")
+        if dtype not in ["attribute", "double", "string"]:
+            raise ValueError(
+                f"dtype is {dtype} but must be attribute, double, or string")
+        if "_" in name and dtype != "attribute":
+            raise ValueError(("underscores '_' in the name are only "
+                              "allowed for attributes"))
+        if dtype == "attribute":
+            if name.count("_") != 1 or \
+                    (name.startswith("Data_") and (name.count("_") == 0 or
+                                                   name.count("_") > 2)):
+                raise ValueError((f"The name of {name} must have the "
+                                  "form VariableName_AttributeName"))
+            if not name.startswith("GLOBAL_") and \
+                    name[:name.rindex("_")] not in self._convention:
+                raise ValueError((f"Adding Attribute {name} requires "
+                                  f"variable {name[:name.rindex('_')]}"))
+        if dimensions is None and dtype != "attribute":
+            raise ValueError(("dimensions must be provided for entries of "
+                              "dtype double and string"))
+        if dimensions is not None:
+            dimensions = dimensions.upper()
+            for dimension in dimensions:
+                if dimension not in "ERMNCIS":
+                    warnings.warn(
+                        f"Added custom dimension {dimensions} to SOFA object")
+
+        # add attribute to class
+        self._add_custom_api_entry(name, value, None, dimensions, dtype)
+
+    def _add_custom_api_entry(self, key, value, flags, dimensions, dtype):
+        """
+        Add custom entry to the sofa._convention and permanently save it in
+        sofa._custom
+
+        Parameters
+        ----------
+        key : str
+            name of the entry
+        value: any
+            Value of the entry
+        flags, dimensions, dtype : any
+            as in sofa._convention
+        dimensions : string
+            Dimensions in case of numeric or string array
+        dtype : string
+            double, string, or attribute
+        """
+        # create custom API if it not exists
+        self._protected = False
+
+        # lower case letters to indicate custom dimensions
+        if dimensions is not None:
+            dimensions = [d.upper() if d.upper() in "ERMNCIS" else d.lower()
+                          for d in dimensions]
+            dimensions = "".join(dimensions)
+
+        # add user entry to custom API, if not contained in the convention
+        if not hasattr(self, "_convention") or key not in self._convention:
+            if not hasattr(self, "_custom"):
+                self._custom = {}
+
+            self._custom[key] = {
+                "flags": flags,
+                "dimensions": dimensions,
+                "type": dtype,
+                "default": None,
+                "comment": ""}
+        self._update_convention(version="match")
+
+        # add attribute to object
+        setattr(self, key, value)
+        self._protected = True
+
+    def upgrade_convention(self, target=None, verify=True):
+        """
+        Upgrade Sofa data to newer conventions.
+
+        Calling this with the default arguments returns a list of possible
+        conventions to which the data will be upgraded. If the data is up to
+        date the list will be empty.
+
+        Parameters
+        ----------
+        target : str, optional
+            The convention and version to which the data should be upgraded as
+            a string. For example ``'SimpleFreeFieldHRIR_1.0'`` would upgrade
+            the data to the SOFA-Convention `SimpleFreeFieldHRIR` version 1.0.
+            The default is ``None`` which returns a list of possible
+            conventions to which the data can be updated.
+        verify : bool, optional
+            Flag to specify if the data should be verified after the upgrade
+            using :py:func:`~Sofa.verify`. The default is ``True``.
+
+        Returns
+        -------
+        target : list of strings
+            List with available conventions to which the data can be updated.
+            If the data is up to data, the list will be empty. `target` is only
+            returned if `target` is ``None``.
+        """
+
+        # check input ---------------------------------------------------------
+        self._update_convention(version="match")
+
+        # get deprecations and information about Sofa object
+        _, _, deprecations, upgrade = self._verification_rules()
+        convention_current = self.GLOBAL_SOFAConventions
+        version_current = self.GLOBAL_SOFAConventionsVersion
+        sofa_version_current = self.GLOBAL_Version
+
+        # check if convention is deprecated -----------------------------------
+        is_deprecated = False
+
+        if convention_current in deprecations["GLOBAL:SOFAConventions"]:
+            is_deprecated = True
+        elif convention_current in upgrade:
+            for from_to in upgrade[convention_current]["from_to"]:
+                if version_current in from_to[0]:
+                    is_deprecated = True
+                    break
+
+        # check for upgrades --------------------------------------------------
+        if is_deprecated:
+            version_matched = False
+            # check if upgrade is available for this convention
+            if convention_current not in upgrade:
+                print((f"Convention {convention_current} v{version_current} is"
+                       " outdated but is missing upgrade rules"))
+                return
+
+            # check if upgrade is available for this version
+            for from_to in upgrade[convention_current]["from_to"]:
+                if version_current in from_to[0]:
+                    version_matched = True
+                    targets = from_to[1]
+
+                    if target in targets:
+                        target_id = from_to[2]
+                    else:
+                        if target is not None:
+                            print(f"{target} is invalid.")
+
+                        upgrades = (
+                            f"{convention_current} v{version_current} "
+                            "can be upgraded to:\n")
+                        for t in targets:
+                            t = t.split("_")
+                            upgrades += f"- {t[0]} v{t[1]}\n"
+                        print(upgrades)
+                        return targets
+                    break
+            if not version_matched:
+                print((f"Convention {convention_current} v{version_current} is"
+                       " outdated but is missing upgrade rules"))
+        else:
+            print((f"Convention {convention_current} v{version_current} "
+                   "is up to date"))
+            return
+
+        # get information to upgrade ------------------------------------------
+        upgrade = upgrade[self.GLOBAL_SOFAConventions][target_id]
+        convention_target, version_target = target.split("_")
+
+        # upgrade -------------------------------------------------------------
+        self._convention = self._load_convention(
+            convention_target, version_target)
+        sofa_version_target = self._convention["GLOBAL_Version"]["default"]
+
+        print((f"Upgrading {convention_current} v{version_current} to "
+               f"{convention_target} v{version_target} (SOFA version "
+               f"{sofa_version_current} to {sofa_version_target})"))
+
+        # upgrade convention
+        keys = ["GLOBAL_SOFAConventions",
+                "GLOBAL_SOFAConventionsVersion",
+                "GLOBAL_Version",
+                "GLOBAL_DataType"]
+
+        self._protected = False
+        for key in keys:
+            setattr(self, key, self._convention[key]["default"])
+
+        # move data
+        for source, move in upgrade["move"].items():
+            # get info
+            source_sofar = source.replace(".", '_').replace(":", "_")
+            target_sofar = move["target"].replace(".", '_').replace(":", "_")
+            move_info = f"- Moving {source_sofar} to {target_sofar}."
+            # get data
+            data = getattr(self, source_sofar)
+            # delete from Sofa object
+            delattr(self, source_sofar)
+            # Check move axis
+            moveaxis = upgrade["move"][source]["moveaxis"]
+            if moveaxis is not None:
+                # add dimensions if required
+                # (sofar discards trailing singular dimensions)
+                while data.ndim < np.max(moveaxis) + 1:
+                    data = data[..., None]
+                # move the axis
+                data = np.moveaxis(data, moveaxis[0], moveaxis[1])
+                move_info += f" Moving axis {moveaxis[0]} to {moveaxis[1]}."
+            # Check deprecated dimensions
+            deprecated_dimensions = \
+                upgrade["move"][source]["deprecated_dimensions"]
+            if deprecated_dimensions is not None:
+                move_info += (
+                    f" WARNING: Dimensions {', '.join(deprecated_dimensions)} "
+                    "are now deprecated.")
+            # add data
+            setattr(self, target_sofar, data)
+            # print info
+            print(move_info)
+
+        if not upgrade["move"]:
+            print("- No data to move")
+
+        # remove data
+        for target in upgrade["remove"]:
+            target_sofar = target.replace(".", '_').replace(":", "_")
+            delattr(self, target_sofar)
+            print(f"- Deleting {target_sofar}.")
+
+        if not upgrade["remove"]:
+            print("- No data to remove")
+
+        # check for missing mandatory data
+        self.add_missing(True, False)
+        self._protected = True
+
+        # display general message
+        if upgrade["message"] is not None:
+            print(upgrade["message"])
+
+        if verify:
+            self.verify()
+
+    def verify(self, issue_handling="raise", mode="write"):
+        """
+        Verify a SOFA object against the SOFA standard.
+
+        This function updates the API, and checks the following
+
+        - Are all mandatory data contained? If `issue_handling` is ``"raise"``
+          missing mandatory data raises an error. Otherwise mandatory data are
+          added with their default value and a warning is given.
+        - Are the names of variables and attributes in accordance to the SOFA
+          standard?
+        - Are the data types in accordance with the SOFA standard?
+        - Are the dimensions of the variables consistent and in accordance
+          to the SOFA standard?
+        - Are the values of attributes consistent and in accordance to the
+          SOFA standard?
+
+        A detailed set of validation rules can be found at
+        https://github.com/pyfar/sofar/tree/main/sofar/verification_rules
+
+        .. note::
+            :py:func:`~verify` is automatically called when you create a new
+            SOFA object, read a SOFA file from disk, and write a SOFA file to
+            disk (using the default parameters).
+
+        The API of a SOFA object consists of four parts, that are stored
+        dictionaries in private attributes. This is required for writing data
+        with :py:func:`~sofa.write_sofa` and should usually not be manipulated
+        outside of :py:func:`~verify`
+
+        self._convention
+            The SOFA convention with default values, variable dimensions, flags
+            and comments. These data are read from the official SOFA
+            conventions contained in the SOFA Matlab/Octave API.
+        self._dimensions
+            The detected dimensions of the data inside the SOFA object.
+        self._api
+            The size of the dimensions (see py:func:`~list_dimensions`). This
+            specifies the dimensions of the data inside the SOFA object.
+        self._custom
+            Stores information of custom variables that are not defined by the
+            convention. The format is the same as in `self._convention`.
+
+        Parameters
+        ----------
+        issue_handling : str, optional
+            Defines how detected issues are handeled
+
+            ``'raise'``
+                Warnings and errors are raised if issues are detected
+            ``'print'``
+                Issues are printed without raising warnings and errors
+            ``'return'``
+                Issues are returned as string but neither raised nor printed
+
+            The default is ``'raise'``.
+        mode : str, optional
+            The SOFA standard is more strict for writing data than for reading
+            data.
+
+            ``'write'``
+                All units (e.g. 'meter') must be written be lower case.
+            ``'read'``
+                Units can contain upper case letters (e.g. 'Meter')
+
+            The default is ``'write'``
+
+        Returns
+        -------
+        issues : str, None
+            Detected issues as a string. None if no issues were detected. Note
+            that this is only returned if ``issue_handling='return'`` (see
+            above)
+
+        """
+        # NOTE: This function collects warnings and errors and tries to output
+        # them in a block. This makes the code slightly more complicated but
+        # is more convenient for the user and with respect to a potential
+        # future web based tool for verifying SOFA files.
+
+        # initialize warning and error messages
+        error_msg = "\nERRORS\n------\n"
+        warning_msg = "\nWARNINGS\n--------\n"
+
+        # ---------------------------------------------------------------------
+        # 0. update the convention
+        self._update_convention("match")
+
+        # ---------------------------------------------------------------------
+        # 1. check if the mandatory attributes are contained
+        missing = ""
+        keys = [key for key in self.__dict__.keys() if not key.startswith("_")]
+
+        for key in self._convention.keys():
+            if self._mandatory(self._convention[key]["flags"]) \
+                    and key not in keys:
+
+                if issue_handling != "raise":
+                    # add missing data with default value
+                    self._protected = False
+                    setattr(self, key, self._convention[key]["default"])
+                    self._protected = True
+
+                # prepare to raise warning
+                missing += "- " + key + "\n"
+
+        if missing:
+            if issue_handling == "raise":
+                error_msg += ("Detected missing mandatory data "
+                              "call sofa.add_missing() to fix this):\n")
+                error_msg += missing
+            else:
+                warning_msg += "Added mandatory data with default values:\n"
+                warning_msg += missing
+
+        # ---------------------------------------------------------------------
+        # 2. verify data type
+        current_error = ""
+        for key in keys:
+
+            # handle dimensions
+            dimensions = self._convention[key]["dimensions"]
+            dtype = self._convention[key]["type"]
+
+            # check data type
+            value = getattr(self, key)
+
+            if dtype == "attribute":
+                if not isinstance(value, str):
+                    current_error += \
+                        f"- {key} must be string but is {type(value)}\n"
+
+            elif dtype == "double":
+                # multiple checks needed because sofar does not force the user
+                # to initally pass data as numpy arrays
+                if not isinstance(value,
+                                  (np.int_, np.float_, np.double, np.ndarray)):
+                    current_error += (f"- {key} must be int, float or numpy "
+                                      f"array but is {type(value)}\n")
+
+                if isinstance(value, np.ndarray) and not (
+                        str(value.dtype).startswith('int') or
+                        str(value.dtype).startswith('float')):
+                    current_error += (f"- {key} must be int or float "
+                                      f"but is {type(value.dtype)}\n")
+
+            elif dtype == "string":
+                # multiple checks needed because sofar does not force the user
+                # to initally pass data as numpy arrays
+                if not isinstance(value, (str, np.ndarray)):
+                    current_error += (f"- {key} must be string or numpy array "
+                                      f"but is {type(value)}\n")
+
+                if isinstance(value, np.ndarray) and not (
+                        str(value.dtype).startswith('<U') or
+                        str(value.dtype).startswith('<S')):
+                    current_error += (f"- {key} must be U or S "
+                                      f"but is {type(value.dtype)}\n")
+
+            else:
+                # Could only be tested by manipulating JSON convention files
+                # (Could take different data types in the future and convert to
+                # numpy double arrays.)
+                current_error += (
+                    f"- {key}: Error in convention. Type must be "
+                    f"double, string, or attribute but is {dtype}\n")
+
+        if current_error:
+            error_msg += "Detected data of wrong type:\n"
+            error_msg += current_error
+
+        # if an error ocurred up to here, it has to be handled. Otherwise
+        # detecting the dimensions might fail. Warnings are not reported until
+        # the end
+        if error_msg != "\nERRORS\n------\n" and issue_handling != "ignore":
+            _, issues = self._verify_handle_issues(
+                    "\nWARNINGS\n--------\n", error_msg, issue_handling)
+
+            if issue_handling == "print":
+                return
+            else:  # (issue_handling == "return"):
+                return issues
+
+        # ---------------------------------------------------------------------
+        # 3. Verify names of entries
+
+        # check attributes without variables
+        current_error = ""
+        for key in keys:
+
+            if self._convention[key]["type"] != "attribute" or \
+                    key.count("_") == 0:
+                continue
+
+            if (key[:key.rindex("_")] not in self._convention and
+                    not key.startswith("GLOBAL_")):
+                current_error += "- " + key + "\n"
+
+        if current_error:
+            error_msg += "Detected attributes with missing variables:\n"
+            error_msg += current_error
+
+        # check number of underscores
+        current_error = ""
+        for key in keys:
+
+            if self._convention[key]["type"] != "attribute":
+                continue
+
+            # the case above caught attributes with too many underscores
+            if key.count("_") == 0:
+                current_error += "- " + key + "\n"
+
+        if current_error:
+            error_msg += (
+                "Detected attribute names with too many or little underscores."
+                " Names must have the form Variable_Attribute, Data_Attribute "
+                "(one underscore), or Data_Variable_Attribute (two "
+                "underscores):\n")
+            error_msg += current_error
+
+        # check numeric variables
+        current_error = ""
+        for key in keys:
+
+            if self._convention[key]["type"] == "attribute":
+                continue
+
+            if "_" in key.replace("Data_", ""):
+                current_error += "- " + key + "\n"
+
+        if current_error:
+            error_msg += (
+                "Detected variable names with too many underscores."
+                "Underscores are only allowed for the variable Data:\n")
+            error_msg += current_error
+
+        # check reserved names
+        current_error = ""
+        for key in keys:
+
+            # AES69-2020 Sec. 4.7.1
+            if key.startswith("PRIVATE") or key.startswith("API"):
+                current_error += "- " + key + "\n"
+            if (key.startswith("GLOBAL") and not key.startswith("GLOBAL_")) or\
+                    (key.startswith("GLOBAL") and
+                     self._convention[key]["type"] != "attribute"):
+                current_error += "- " + key + "\n"
+
+        if current_error:
+            error_msg += (
+                "Detected variable or attribute with reserved key words "
+                "PRIVATE, API, or GLOBAL:\n")
+            error_msg += current_error
+
+        # check names of custom data (shall not have the same name as
+        # normative data contained in the convention AES69-2022 Sec. 5.3)
+        current_error = ""
+        if hasattr(self, "_custom"):
+
+            # get normative variables and attributes
+            normative = sf.Sofa(
+                self.GLOBAL_SOFAConventions,
+                version=self.GLOBAL_SOFAConventionsVersion)._convention.keys()
+            normative = [n.replace("GLOBAL_", "") for n in normative]
+
+            # compare against custom
+            for key in self._custom.keys():
+                if key.replace("GLOBAL_", "") in normative:
+                    current_error += "- " + key + "\n"
+
+        if current_error:
+            error_msg += (
+                "Detected custom variable or attribute with reserved names. "
+                "Custom data shall not have the same name as data contained in"
+                " the convention itself:\n")
+            error_msg += current_error
+
+        # ---------------------------------------------------------------------
+        # 4. Get dimensions (E, R, M, N, S, c, I, and custom)
+
+        # initialize required API fields
+        self._protected = False
+        self._dimensions = {}
+        self._api = {}
+        self._protected = True
+
+        # get keys for checking the dimensions (all SOFA variables)
+        keys = [key for key in self.__dict__.keys()
+                if key in self._convention
+                and self._convention[key]["dimensions"] is not None]
+        if hasattr(self, "_custom"):
+            keys_custom = [key for key in self._custom.keys()
+                           if not key.startswith("_")
+                           and self._custom[key]["dimensions"] is not None]
+            keys += keys_custom
+
+        S = 0
+        for key in keys:
+
+            value = getattr(self, key)
+            dimensions = self._convention[key]["dimensions"]
+
+            # - dimensions are given as string, e.g., 'mRN', or 'IC, MC'
+            # - defined by lower case letters in `dimensions`
+            for id, dim in enumerate(dimensions.split(", ")[0]):
+                if dim not in "ICS" and dim.islower():
+                    # numeric data
+                    self._api[dim.upper()] = \
+                        _nd_newaxis(value, 4).shape[id]
+                if dim == "S":
+                    # string data
+                    S = max(S, np.max(self._get_size_and_shape_of_string_var(
+                        value, key)[0]))
+
+        # add fixed sizes
+        self._api["C"] = 3
+        self._api["I"] = 1
+        self._api["S"] = S
+
+        # ---------------------------------------------------------------------
+        # 5. verify dimensions of data
+        current_error = ""
+        for key in keys:
+
+            # handle dimensions
+            dimensions = self._convention[key]["dimensions"]
+            dtype = self._convention[key]["type"]
+
+            # get value and actual shape
+            try:
+                value = getattr(self, key).copy()
+            except AttributeError:
+                value = getattr(self, key)
+
+            if dtype in ["attribute", "string"]:
+                # string or string array like data
+                shape_act = self._get_size_and_shape_of_string_var(
+                    value, key)[1]
+            elif len(dimensions.split(",")[0]) > 1:
+                # multidimensional array like data
+                shape_act = _atleast_nd(value, 4).shape
+            else:
+                # scalar of single dimensional array like data
+                shape_act = (np.array(value).size, )
+
+            shape_matched = False
+            for dim in dimensions.split(", "):
+
+                # get the reference shape ('S' translates to a shape of 1,
+                # because the strings are stored in an array whose shape does
+                # not reflect the max. lengths of the actual strings inside it)
+                shape_ref = tuple(
+                    [self._api[d.upper()] if d != "S" else 1 for d in dim])
+
+                # get shape for comparison to correct length by cropping and
+                # appending singelton dimensions if required
+                shape_compare = shape_act[:len(shape_ref)]
+                for _ in range(len(shape_ref) - len(shape_compare)):
+                    shape_compare += (1, )
+
+                # check if the shapes match and write to API
+                if shape_compare == shape_ref:
+                    shape_matched = True
+                    self._dimensions[key] = dim.upper()
+                    break
+
+            if not shape_matched:
+                # get possible dimensions in verbose form, i.e., "(M=2, C=3)""
+                dimensions_verbose = []
+                for dim in dimensions.upper().replace(" ", "").split(","):
+                    dimensions_verbose.append(
+                        f"({', '.join([f'{d}={self._api[d]}' for d in dim])})")
+
+                current_error += (
+                    f"- {key} has shape {shape_compare} but must "
+                    f"have {', '.join(dimensions_verbose)}\n")
+
+        if current_error:
+            error_msg += "Detected variables of wrong shape:\n"
+            error_msg += current_error
+
+        # ---------------------------------------------------------------------
+        # 6. check restrictions on the content of SOFA files
+        rules, unit_aliases, deprecations, _ = self._verification_rules()
+
+        current_error = ""
+        for key in rules.keys():
+
+            # convert to sofar format
+            key_sofar = key.replace(".", "_").replace(":", "_")
+
+            if not hasattr(self, key_sofar):
+                continue
+
+            # actual and possible values for the current key
+            test = getattr(self, key_sofar)
+            ref = rules[key]["value"]
+
+            # test if the value is valid
+            if not self._verify_value(test, ref, unit_aliases, key_sofar):
+                current_error += (f"- {key_sofar} is {test} "
+                                  f"but must be {', '.join(ref)}\n")
+
+            # get lower case value for of test for verifying specific
+            # dependencies
+            if isinstance(test, str) and test.lower() in \
+                    ["cartesian", "spherical", "spherical harmonics"]:
+                test = test.lower()
+
+            # check general dependencies
+            items = rules[key]["general"] if "general" in rules[key] else []
+
+            for key_dep in items:
+
+                # convert key to sofar format
+                key_dep = key_dep.replace(".", "_").replace(":", "_")
+
+                # check if dependency is contained in SOFA object hard to test,
+                # for mandatory fields (added by sofar by default).
+                if not hasattr(self, key_dep):
+                    current_error += (f"- {key_dep} must be given if "
+                                      f"{key_sofar} is in SOFA object\n")
+                    continue
+
+            # check specific dependencies
+            if "specific" in rules[key] \
+                    and test in rules[key]["specific"]:
+                items = rules[key]["specific"][test].items()
+            else:
+                items = {}.items()
+
+            for key_dep, ref_dep in items:
+
+                if key_dep == "_dimensions":
+                    # requires specific dimension(s) to have a vertain size
+                    for dim in rules[key]["specific"][test]["_dimensions"]:
+                        # possible sizes
+                        dim_ref = \
+                            rules[key]["specific"][test]["_dimensions"][dim]["value"]  # noqa
+                        # current size
+                        dim_act = self._api[dim]
+                        # verbose error string for possible sizes
+                        dim_str = \
+                            rules[key]["specific"][test]["_dimensions"][dim]["value_str"]  # noqa
+                        # perform the check
+                        if dim_act not in dim_ref:
+                            current_error += \
+                                (f"- Dimension {dim} is of size {dim_act} "
+                                 f"but must be {dim_str} if {key_sofar} "
+                                 f"is {test}\n")
+                else:
+
+                    # convert name from NetCDF format to sofar format
+                    key_dep_sofar = key_dep.replace(".", "_").replace(":", "_")
+
+                    # check if dependency is contained in SOFA object
+                    if not hasattr(self, key_dep_sofar):
+                        current_error += (f"- {key_dep_sofar} must be given if"
+                                          f" {key_sofar} is {test}\n")
+                        continue
+
+                    # check if dependency has the correct value
+                    if ref_dep is None:
+                        continue
+
+                    # convert name from NetCDF format to sofar format
+                    test_dep = getattr(self, key_dep_sofar)
+
+                    if not self._verify_value(
+                            test_dep, ref_dep, unit_aliases, key_dep_sofar):
+                        current_error += (
+                            f"- {key_dep_sofar} is {test_dep} but must be "
+                            f"{', '.join(ref_dep)} if {key_sofar} is {test}\n")
+
+        # ---------------------------------------------------------------------
+        # 7. check write only restrictions: units shall be in lower-case
+        # (could be tested in _verify_unit but this way a more verbose error
+        # message can be generated)
+
+        if mode == "write":
+            keys = [k for k in self.__dict__.keys() if k.endswith("Units")]
+            for key in keys:
+                unit = getattr(self, key)
+                if unit.lower() != unit:
+                    current_error += (f"- {key} is {unit} but must contain "
+                                      "only lower case letters when writing "
+                                      "SOFA files to disk.\n")
+
+        if current_error:
+            error_msg += "Detected violations of the SOFA convention:\n"
+            error_msg += current_error
+
+        # ---------------------------------------------------------------------
+        # 8. check deprecations
+        # (so far there are only deprecations for the convention)
+        if self.GLOBAL_SOFAConventions in \
+                deprecations["GLOBAL:SOFAConventions"]:
+            msg = ("Detected deprecations:\n"
+                   f"- GLOBAL_SOFAConventions is "
+                   f"{self.GLOBAL_SOFAConventions}, which is deprecated. Use "
+                   "Sofa.upgrade_convention() to upgrade to "
+                   f"{deprecations['GLOBAL:SOFAConventions'][self.GLOBAL_SOFAConventions]}")  # noqa
+            if mode == "write":
+                error_msg += msg
+            else:
+                warning_msg += msg
+
+        # warn if preliminary conventions versions are used
+        if float(self.GLOBAL_SOFAConventionsVersion) < 1.0:
+            warning_msg += (
+                "\n\nDetected preliminary conventions version "
+                f"{self.GLOBAL_SOFAConventionsVersion}:\n - Upgrade data to "
+                "version >= 1.0 if possible. Preliminary conventions might "
+                "change in the future, which could invalidate data that was "
+                "written before the changes.")
+
+        # ---------------------------------------------------------------------
+        # 9. handle warnings and errors
+        if issue_handling != "ignore":
+            error_occurred, issues = self._verify_handle_issues(
+                    warning_msg, error_msg, issue_handling)
+
+            if error_occurred:
+                if issue_handling == "print":
+                    return
+                elif issue_handling == "return":
+                    return issues
+
+    @staticmethod
+    def _verify_value(test, ref, unit_aliases, key):
+        """
+        Check a value agains the SOFA standard for Sofa.verify()
+
+        Parameters
+        ----------
+        test :
+            the value under test
+        ref :
+            the value enforced by the SOFA standard
+        unit_aliases :
+            dict of aliases for units from _verification_rules()
+        key :
+            The name of the current attribute, e.g., "GLOBAL_DataType"
+
+        Returns
+        -------
+        ``True`` if `test` and `ref` agree, ``False`` otherwise
+        """
+
+        # General case of valid value
+        if ref is None or test in ref:
+            return True
+
+        # only string data should remain for verification
+        if not isinstance(test, str):
+            raise TypeError((
+                "At this point, only string data should remain. Please report "
+                "the issue: github.com/pyfar/sofar/issues"))
+
+        # case sensitive check for DataType and SOFAConventions
+        if key in ["GLOBAL_DataType", "GLOBAL_SOFAConventions"]:
+            if test in ref:
+                return True
+            else:
+                return False
+
+        # general case insensitive test
+        test = test.lower()
+        if test in ref:
+            return True
+
+        # if we are not checking a unit, the test value is invalid
+        if key.endswith("Units"):
+            return sf.Sofa._verify_unit(test, ref, unit_aliases)
+        else:
+            return False
+
+    @staticmethod
+    def _verify_unit(test, ref, unit_aliases):
+        """
+        Verify if a unit string agrees with AES69
+
+        Parameters:
+        -----------
+        test : string
+            Current unit string (single units or multiple units separated by
+            commas, commas plus spaces, or spaces).
+        ref : list
+            List of length one containing the LOWER reference case unit string,
+            i.e., only the keys from unit_aliases are allowed words.
+        unit_aliases : dict
+            dict of aliases for units from _verification_rules()
+
+        Returns
+        -------
+        verified : bool
+        """
+        # check format of reference units
+        if not isinstance(ref, list) \
+                or len(ref) != 1 \
+                or not isinstance(ref[0], str):
+            raise TypeError("ref must be a list of length 1 containing a str")
+
+        # Following the SOFA standard AES69-2020, units may be separated by
+        # `, ` (comma and space), `,` (comma only), and ` ` (space only).
+        # (regexp ', ?' matches ', ' and ',')
+        units_ref = re.split(', ?', ref[0])
+        units_test = re.split(', ?', test)
+
+        # check if number of units agree
+        if len(units_ref) != len(units_test):
+            return False
+
+        # check if units are valid
+        for unit_test, unit_ref in zip(units_test, units_ref):
+            if unit_test not in unit_aliases \
+                    or unit_aliases[unit_test] != unit_ref:
+                return False
+
+        return True
+
+    @staticmethod
+    def _get_reference_unit(test, unit_aliases):
+        """
+        Return units in reference for, .e.g.,
+        "meter" is converted to "metre" and
+        "degrees degrees,meter" is converted to "degree, degree, metre"
+
+        Parameters:
+        -----------
+        test : string
+            Current unit string MUST be valid, i.e., tested with
+            Sofa._verify_unit (single units or multiple units separated by
+            commas, commas plus spaces, or spaces).
+        unit_aliases : dict
+            dict of aliases for units from _verification_rules()
+
+        Returns
+        -------
+        reference_units : str
+        """
+        # Following the SOFA standard AES69-2020, units may be separated by
+        # `, ` (comma and space), `,` (comma only), and ` ` (space only).
+        # (regexp ', ?' matches ', ' and ',')
+        units_test = re.split(', ?| ', test.lower())
+
+        # get list of reference units
+        units = [unit_aliases[u] for u in units_test]
+        # get reference unit string
+        units = ", ".join(units) if units[0] != "cubic" else " ".join(units)
+
+        return units
+
+    @staticmethod
+    def _verify_handle_issues(warning_msg, error_msg, issue_handling):
+        """Handle warnings and errors from Sofa.verify"""
+
+        # handle warnings
+        if warning_msg != "\nWARNINGS\n--------\n":
+            if issue_handling == "raise":
+                warnings.warn(UserWarning(warning_msg))
+            elif issue_handling == "print":
+                print(warning_msg)
+        else:
+            warning_msg = None
+
+        # handle errors
+        if error_msg != "\nERRORS\n------\n":
+            if issue_handling == "raise":
+                raise ValueError(error_msg)
+            elif issue_handling == "print":
+                print(error_msg)
+        else:
+            error_msg = None
+
+        # flag indicating if an error occurred
+        error_occurred = error_msg is not None
+
+        # verbose issue message
+        if warning_msg and error_msg:
+            issues = error_msg + "\n" + warning_msg
+        elif warning_msg:
+            issues = warning_msg
+        elif error_msg:
+            issues = error_msg
+        else:
+            issues = None
+
+        return error_occurred, issues
+
+    @staticmethod
+    def _verification_rules():
+        """
+        Return dictionaries to verify SOFA objects in Sofa.verify(). For
+        detailed information see folder 'sofa_conventions'.
+
+        Returns:
+        rules : dict
+            All general and specific verification rules
+        unit_aliases : dict
+            Aliases for specific units allowed in SOFA
+        deprecations : dict
+            Deprecated conventions and their substitute
+        upgrade : dict
+            Rules for upgrading deprecated conventions
+        """
+
+        base = os.path.join(
+            os.path.dirname(__file__), "sofa_conventions", "rules")
+
+        with open(os.path.join(base, "rules.json"), "r") as file:
+            rules = json.load(file)
+        with open(os.path.join(base, "unit_aliases.json"), "r") as file:
+            unit_aliases = json.load(file)
+        with open(os.path.join(base, "deprecations.json"), "r") as file:
+            deprecations = json.load(file)
+        with open(os.path.join(base, "upgrade.json"), "r") as file:
+            upgrade = json.load(file)
+
+        return rules, unit_aliases, deprecations, upgrade
+
+    def copy(self):
+        """Return a copy of the SOFA object."""
+        return deepcopy(self)
+
+    def _update_convention(self, version):
+        """
+        Add SOFA convention to SOFA object in private attribute `_convention`.
+        If The object already contains a convention, it will be overwritten.
+
+        Parameters
+        ----------
+        version : str
+            ``'latest'``
+                Use the latest API and upgrade the SOFA file if required.
+            ``'match'``
+                Match the version of the sofa file.
+            str
+                Version string, e.g., ``'1.0'``.
+        """
+
+        # verify convention and version
+        c_current = self.GLOBAL_SOFAConventions
+        v_current = str(self.GLOBAL_SOFAConventionsVersion)
+
+        v_new = _verify_convention_and_version(
+                version, v_current, c_current)
+
+        # load and add convention and version
+        convention = self._load_convention(
+            c_current, v_new)
+        self._convention = convention
+
+        if v_current != v_new:
+            self._protected = False
+            self.GLOBAL_SOFAConventionsVersion = v_new
+            self._protected = True
+
+        # feedback in case of up/downgrade
+        if float(v_current) < float(v_new):
+            warnings.warn(("Upgraded SOFA object from "
+                           f"version {v_current} to {v_new}"))
+        elif float(v_current) > float(v_new):
+            warnings.warn(("Downgraded SOFA object from "
+                           f"version {v_current} to {v_new}"))
+
+        # check if custom fields can be added
+        if hasattr(self, "_custom"):
+            for key in self._custom:
+                self._convention[key] = self._custom[key]
+
+    def _load_convention(self, convention, version):
+        """
+        Load SOFA convention from json file.
+
+        Parameters
+        ----------
+        convention : str
+            The name of the convention from which the SOFA file is created. See
+            :py:func:`~sofar.list_conventions`.
+        version : str
+            ``'latest'``
+                Use the latest API and upgrade the SOFA file if required.
+            str
+                Version string, e.g., ``'1.0'``.
+
+        Returns
+        -------
+        convention : dict
+            The SOFA convention as a dictionary
+        """
+        # check input
+        if not isinstance(convention, str):
+            raise TypeError(("Convention must be a string "
+                             f"but is of type {type(convention)}"))
+
+        # get and check path to json file
+        paths = _get_conventions("path")
+        path = [path for path in paths
+                if os.path.basename(path).startswith(convention + "_")]
+
+        if not len(path):
+            raise ValueError(
+                (f"Convention '{convention}' not found. See "
+                 "sofar.list_conventions() for available conventions."))
+
+        # get available versions as strings
+        versions = [p.split('_')[-1][:-5] for p in path]
+
+        # select the correct version
+        if version == "latest":
+            versions = np.array([float(v) for v in versions])
+            path = path[np.argmax(versions)]
+        else:
+            if version not in versions:
+                raise ValueError((
+                    f"Version {version} not found. "
+                    f"Available versions are {versions}"))
+            path = path[versions.index(version)]
+
+        # read convention from json file
+        with open(path, "r") as file:
+            convention = json.load(file)
+
+        # replace ':' and '.' in key names by '_'
+        convention = {
+            key.replace(':', '_'): value for key, value in convention.items()}
+        convention = {
+            key.replace('.', '_'): value for key, value in convention.items()}
+
+        return convention
+
+    def _convention_to_sofa(self, mandatory):
+        """
+        Use SOFA convention to create attributes with default values.
+
+        Parameters
+        ----------
+        mandatory : bool
+            Flag to indicate if only mandatory fields are to be included.
+        """
+
+        # populate the SOFA file
+        for key in self._convention.keys():
+
+            # skip optional fields if requested
+            if not self._mandatory(self._convention[key]["flags"]) \
+                    and mandatory:
+                continue
+
+            # get the default value
+            default = self._convention[key]["default"]
+            if isinstance(default, list):
+                ndim = len(self._convention[key]["dimensions"].split(", ")[0])
+                default = _atleast_nd(default, ndim)
+
+            # create attribute with default value
+            setattr(self, key, default)
+
+        # write API and date specific fields (some read only)
+        now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+        self._protected = False
+        self.GLOBAL_DateCreated = now
+        self.GLOBAL_DateModified = now
+        self.GLOBAL_APIName = "sofar SOFA API for Python (pyfar.org)"
+        self.GLOBAL_APIVersion = sf.version()
+        self.GLOBAL_ApplicationName = "Python"
+        self.GLOBAL_ApplicationVersion = (
+            f"{platform.python_version()} "
+            f"[{platform.python_implementation()} - "
+            f"{platform.python_compiler()}]")
+        self._protected = True
+
+    @staticmethod
+    def _get_size_and_shape_of_string_var(value, key):
+        """
+        String variables can be strings, list of strings, or numpy arrays of
+        strings. This functions returns the length of the longest string S
+        inside the string variable and the shape of the string variable as
+        required by the SOFA definition. Note that the shape is the shape of
+        the array that holds the strings. NETCDF stores all string variables in
+        arrays.
+        """
+
+        if isinstance(value, str):
+            S = len(value)
+            shape = (1, 1)
+        elif isinstance(value, list):
+            S = len(max(value, key=len))
+            shape = np.array(value).shape
+        elif isinstance(value, np.ndarray):
+            S = max(np.vectorize(len)(value))
+            shape = value.shape
+        else:
+            raise TypeError((f"{key} must be a string, numpy string array, "
+                             "or list of strings"))
+
+        return S, shape
+
+    @staticmethod
+    def _mandatory(flags):
+        """
+        Check if a field is mandatory
+
+        Parameters
+        ----------
+        flags : None, str
+            The flags from convention[key]["flags"]
+
+        Returns
+        -------
+        is_mandatory : bool
+        """
+        # skip optional fields if requested
+        if flags is None:
+            is_mandatory = False
+        elif "m" not in flags:
+            is_mandatory = False
+        else:
+            is_mandatory = True
+
+        return is_mandatory
+
+    @staticmethod
+    def _read_only(flags):
+        """
+        Check if a field is read only
+
+        Parameters
+        ----------
+        flags : None, str
+            The flags from convention[key]["flags"]
+
+        Returns
+        -------
+        is_read_only : bool
+        """
+        # skip optional fields if requested
+        if flags is None:
+            is_read_only = False
+        elif "r" not in flags:
+            is_read_only = False
+        else:
+            is_read_only = True
+
+        return is_read_only
diff --git a/sofar/sofa_conventions b/sofar/sofa_conventions
new file mode 160000
index 00000000..5d7b6198
--- /dev/null
+++ b/sofar/sofa_conventions
@@ -0,0 +1,1 @@
+Subproject commit 5d7b6198e0a08554190767f8896594a05094d3d5
diff --git a/sofar/sofar.py b/sofar/sofar.py
deleted file mode 100644
index 64c973c8..00000000
--- a/sofar/sofar.py
+++ /dev/null
@@ -1,2531 +0,0 @@
-import os
-import re
-import glob
-import json
-import requests
-from datetime import datetime
-import platform
-import numpy as np
-import numpy.testing as npt
-from packaging.version import parse as version_parse
-import warnings
-from bs4 import BeautifulSoup
-from netCDF4 import Dataset, stringtochar, chartostring
-from copy import deepcopy
-import sofar as sf
-
-
-class Sofa():
-    """Create a new SOFA object.
-
-    Parameters
-    ----------
-    convention : str
-        The name of the convention from which the SOFA file is created. See
-        :py:func:`~sofar.list_conventions`.
-    mandatory : bool, optional
-        If ``True``, only the mandatory data of the convention will be
-        returned. The default is ``False``, which returns mandatory and
-        optional data.
-    version : str, optional
-        The version of the convention as a string, e.g., ``' 2.0'``. The
-        default is ``'latest'``. Also see :py:func:`~sofar.list_conventions`.
-    verify : bool, optional
-        Verify the SOFA object by calling :py:func:`~Sofa.verify`. This helps
-        to find potential errors in the default values and is thus recommended
-        If creating a file does not work, try to call `Sofa` with
-        ``verify=False``. The default is ``True``.
-
-    Returns
-    -------
-    sofa : Sofa
-        A SOFA object filled with the default values of the convention.
-
-    Examples
-    --------
-    Create a new SOFA object with default values
-
-    .. code-block:: python
-
-        import sofar as sf
-
-        # create SOFA object
-        sofa = sf.Sofa("SimpleFreeFieldHRIR")
-
-    Add data as a list
-
-    .. code-block:: python
-
-        sofa.Data_IR = [1, 1]
-
-    Data can be entered as numbers, numpy arrays or lists. Note the following
-
-    1. Lists are converted to numpy arrays with at least two dimensions, i.e.,
-       ``sofa.Data_IR`` is converted to a numpy array of shape (1, 2)
-    2. Missing dimensions are appended when writing the SOFA object to disk,
-       i.e., ``sofa.Data_IR`` is written as an array of shape (1, 2, 1) because
-       the SOFA standard AES69-2020 defines it as a three dimensional array
-       with the dimensions (`M: measurements`, `R: receivers`, `N: samples`)
-    3. When reading data from a SOFA file, array data is always returned as
-       numpy arrays and singleton trailing dimensions are discarded (numpy
-       default). I.e., ``sofa.Data_IR`` will again be an array of shape (1, 2)
-       after writing and reading to and from disk.
-    4. One dimensional arrays with only one element will be converted to scalar
-       values. E.g. ``sofa.Data_SamplingRate`` is stored as an array of shape
-       (1, ) inside SOFA files (according to the SOFA standard AES69-2020) but
-       will be a scalar inside SOFA objects after reading from disk.
-
-
-    For more examples refer to the `Quick tour of SOFA and sofar` at
-    https://sofar.readthedocs.io/en/latest/
-    """
-
-    # these have to be set here, because they are used in __setattr__ and
-    # Python checks if they exist upon class creation
-
-    # don't allow adding attributes and deleting/writing read only attributes
-    _protected = False
-    # list of read only attributes (filled upon init)
-    _read_only_attr = []
-
-    def __init__(self, convention, mandatory=False, version="latest",
-                 verify=True):
-        """See class docstring"""
-
-        # get convention
-        self._convention = self._load_convention(convention, version)
-
-        # update read only attributes
-        self._read_only_attr = [
-            key for key in self._convention.keys()
-            if self._read_only(self._convention[key]["flags"])]
-
-        # add attributes with default values
-        self._convention_to_sofa(mandatory)
-
-        # add and update the API
-        if verify:
-            self.verify(version)
-
-        self._protected = True
-
-    def __setattr__(self, name: str, value):
-        # don't allow new attributes to be added outside the class
-        if self._protected and not hasattr(self, name):
-            raise TypeError(f"{name} is an invalid attribute")
-
-        # don't allow setting read only attributes
-        if name in self._read_only_attr and self._protected:
-            raise TypeError(f"{name} is a read only attribute")
-
-        # convert to numpy array or scalar
-        if not isinstance(value, (str, dict, np.ndarray)):
-            value = np.atleast_2d(value)
-            if value.size == 1:
-                value = value.flatten()[0]
-
-        super.__setattr__(self, name, value)
-
-    def __delattr__(self, name: str):
-        # can't delete non existing attributes
-        if not hasattr(self, name):
-            raise TypeError(f"{name} is not an attribute")
-        # delete anything if not frozen, delete non mandatory
-        if not self._protected or \
-                not self._mandatory(self._convention[name]["flags"]):
-            super().__delattr__(name)
-
-            # check if custom field as to be deleted
-            if hasattr(self, "_custom"):
-                if name in self._custom:
-                    self._custom.pop(name)
-        else:
-            raise TypeError(
-                f"{name} is a mandatory attribute that can not be deleted")
-
-    def __repr__(self):
-        return (f"sofar.SOFA object: {self.GLOBAL_SOFAConventions} "
-                f"{self.GLOBAL_SOFAConventionsVersion}")
-
-    @property
-    def list_dimensions(self):
-        """
-        Print the dimensions of the SOFA object
-
-        The SOFA file standard defines the following dimensions:
-
-        M
-            number of measurements
-        N
-            number of samles, frequencies, SOS coefficients
-            (depending on self.GLOBAL_DataType)
-        R
-            Number of receivers or SH coefficients
-            (depending on ReceiverPosition_Type)
-        E
-            Number of emitters or SH coefficients
-            (depending on EmitterPosition_Type)
-        S
-            Maximum length of a string in a string array
-        C
-            Size of the coordinate dimension. This is always three.
-        I
-            Single dimension. This is always one.
-
-        see :py:func:`~Sofa.info` to see the shapes of the data inside the SOFA
-        object.
-        """
-
-        # Check if the dimensions can be updated
-        self._update_dimensions()
-
-        # get verbose description for dimesion N
-        if self.GLOBAL_DataType.startswith("FIR"):
-            N_verbose = "samples"
-        elif self.GLOBAL_DataType.startswith("TF"):
-            N_verbose = "frequencies"
-        elif self.GLOBAL_DataType.startswith("SOS"):
-            N_verbose = "SOS coefficients"
-        else:
-            # This line can not be tested. An invalid DataType would be cached
-            # in self.verify above. This to make sure we don't miss something
-            # in case new DataTypes are added to SOFA in the future.
-            raise ValueError((
-                "GLOBAL_DataType start with 'FIR', 'TF', "
-                f"or 'SOS' but not with {self.GLOBAL_DataType}"))
-
-        # get verbose description for dimensions R and E
-        R_verbose = "receiver spherical harmonics coefficients" if \
-            'harmonic' in self.ReceiverPosition_Type else "receiver"
-        E_verbose = "emitter spherical harmonics coefficients" if \
-            'harmonic' in self.EmitterPosition_Type else "emitter"
-
-        dimensions = {
-            "M": "measurements",
-            "N": N_verbose,
-            "R": R_verbose,
-            "E": E_verbose,
-            "S": "maximum string length",
-            "C": "coordinate dimensions, fixed",
-            "I": "single dimension, fixed"}
-
-        info_str = ""
-        for key, value in self._api.items():
-            dim_info = dimensions[key] if key in dimensions \
-                else "custom dimension"
-
-            info_str += f"{key} = {value} {dim_info}"
-
-            if dim_info != "custom ":
-                for key2, value2 in self._convention.items():
-                    dim = value2["dimensions"]
-                    if dim is not None and key.lower() in dim:
-                        info_str += \
-                            f" (set by {key2} of dimension {dim.upper()})"
-                        break
-
-            info_str += "\n"
-
-        print(info_str)
-
-    def get_dimension(self, dimension):
-        """
-        Get size of a SOFA dimension
-
-        SOFA dimensions specify the shape of the data contained in a SOFA
-        object. For a list of all dimensions see :py:func:`~list_dimensions`.
-
-        Parameters
-        ----------
-        dimension : str
-            The dimension as a string, e.g., ``'N'``.
-
-        Returns
-        -------
-        size : int
-            the size of the queried dimension.
-        """
-
-        # Check if the dimensions can be updated
-        self._update_dimensions()
-
-        if dimension not in self._api:
-            raise ValueError((
-                f"{dimension} is not a valid dimension. "
-                "See Sofa.list_dimensions for a list of valid dimensions."))
-
-        return self._api[dimension]
-
-    def _update_dimensions(self):
-        """
-        Call verify and raise an error if the dimensions could not be updated.
-
-        used in Sofa.list_dimensions and Sofa.get_dimension
-        """
-
-        issues = self.verify(version="match", issue_handling="return")
-        if issues is not None and ("data of wrong type" in issues or
-                                   "variables of wrong shape" in issues or
-                                   not hasattr(self, "_api")):
-            raise ValueError(("Dimensions can not be shown because variables "
-                              "of wrong type or shape were detected. "
-                              "Call Sofa.verify() for more information."))
-
-    def info(self, info):
-        """
-        Print information about the convention of a SOFA object.
-
-        Prints the variable type (attribute, double, string), shape, flags
-        (mandatory, read only) and comment (if any) for each or selected
-        entries.
-
-        Parameters
-        ----------
-        info : str
-            Specifies the kind of information that is printed:
-
-            ``'all'`` ``'mandatory'`` ``'optional'`` ``'read only'`` ``'data'``
-                Print the name, type, shape, and flags and comment for all or
-                selected entries of the SOFA object. ``'data'`` does not show
-                entries of type attribute.
-            key
-                If key is the name of an object attribute, all information for
-                attribute will be printed.
-        """
-
-        # update the private attribute `_convention` to make sure the required
-        # meta data is in place
-        self._update_convention(version="match")
-
-        # list of all attributes
-        keys = [k for k in self.__dict__.keys() if not k.startswith("_")]
-
-        # start printing the information
-        info_str = (
-            f"{self.GLOBAL_SOFAConventions} "
-            F"{self.GLOBAL_SOFAConventionsVersion} "
-            f"(SOFA version {self.GLOBAL_Version})\n")
-        info_str += "-" * len(info_str) + "\n"
-
-        if info in ["all", "mandatory", "optional", "read only", "data"]:
-
-            info_str += f"showing {info} entries : type (shape), flags\n\n"
-
-            for key in keys:
-
-                # check if field should be skipped
-                flags = self._convention[key]["flags"]
-                if (not self._mandatory(flags) and info == "mandatory") \
-                        or \
-                        (self._mandatory(flags) and info == "optional") \
-                        or \
-                        (not self._read_only(flags) and info == "read only") \
-                        or \
-                        (self._convention[key]['type'] == "attribute" and
-                         info == "data"):
-                    continue
-
-                info_str += f"{key} : {self._convention[key]['type']}"
-
-                if self._convention[key]['dimensions']:
-                    info_str += \
-                        f" ({self._convention[key]['dimensions'].upper()})"
-
-                if self._mandatory(flags):
-                    info_str += ", mandatory"
-                else:
-                    info_str += ", optional"
-                if self._read_only(flags):
-                    info_str += ", read only"
-
-                if self._convention[key]['comment']:
-                    info_str += f"\n    {self._convention[key]['comment']}\n"
-                else:
-                    info_str += "\n"
-
-        elif info in keys:
-
-            for key in [k for k in keys if info in k]:
-                comment = str(self._convention[key]['comment'])
-
-                info_str += (
-                    f"{key}\n"
-                    f"    type: {self._convention[key]['type']}\n"
-                    f"    mandatory: "
-                    f"{self._mandatory(self._convention[key]['flags'])}\n"
-                    f"    read only: "
-                    f"{self._read_only(self._convention[key]['flags'])}\n"
-                    f"    default: {self._convention[key]['default']}\n"
-                    f"    shape: "
-                    f"{str(self._convention[key]['dimensions']).upper()}\n"
-                    f"    comment: {comment}\n")
-        else:
-            raise ValueError(f"info='{info}' is invalid")
-
-        print(info_str)
-
-    def inspect(self, file=None, issue_handling="print"):
-        """
-        Get information about data inside a SOFA object.
-
-        Prints the values of all attributes and variables with six or less
-        entries and the shapes and type of all numeric and string variables.
-        When printing the values of arrays, single dimensions are discarded for
-        easy of display, i.e., an array of shape (1, 3, 2) will be displayed as
-        an array of shape (3, 2).
-
-        Parameters
-        ----------
-        file : str
-            Full path of a file under which the information is to be stored in
-            plain text. The default ``None`` does only print the information.
-        issue_handling : str, optional
-            Defines how issues detected during verification of the SOFA object
-            are handeled (see :py:func:`~sofar.sofar.Sofa.verify`)
-
-            ``'raise'``
-                Warnings and errors are raised if issues are detected
-            ``'print'``
-                Issues are printed without raising warnings and errors
-            ``'return'``
-                Issues are returned as string but neither raised nor printed
-            ``'ignore'``
-                Issues are ignored, i.e., not raised, printed, or returned.
-
-            The default is ``print'``.
-        """
-
-        # update the private attribute `_convention` to make sure the required
-        # meta data is in place
-        self.verify(version="match", issue_handling=issue_handling)
-
-        # list of all attributes
-        keys = [k for k in self.__dict__.keys() if not k.startswith("_")]
-
-        # start printing the information
-        info_str = (
-            f"{self.GLOBAL_SOFAConventions} "
-            F"{self.GLOBAL_SOFAConventionsVersion} "
-            f"(SOFA version {self.GLOBAL_Version})\n")
-        info_str += "-" * len(info_str) + "\n"
-
-        for key in keys:
-
-            info_str += key + " : "
-            value = getattr(self, key)
-
-            # information for attributes and scalars
-            if self._convention[key]["type"] == "attribute" or value.size == 1:
-                info_str += str(value) + "\n"
-            # information for variables
-            else:
-                # get shape and dimension
-                shape = value.shape
-                dimension = self._dimensions[key]
-
-                # pad shape if required (trailing single dimensions are
-                # discarded following the numpy default)
-                while len(shape) < len(dimension):
-                    shape += (1, )
-
-                # make verbose shape, e.g., '(M=100, R=2, N=128, '
-                shape_verbose = "("
-                for s, d in zip(shape, dimension):
-                    shape_verbose += f"{d}={s}, "
-
-                # add shape information
-                info_str += shape_verbose[:-2] + ")\n"
-                # add value information if not too much
-                if value.size < 7:
-                    info_str += "  " + \
-                        str(np.squeeze(value)).replace("\n", "\n  ") + "\n"
-
-        # write to text file
-        if file is not None:
-            with open(file, 'w') as f_id:
-                f_id.write(info_str + "\n")
-
-        # output to console
-        print(info_str)
-
-    def add_variable(self, name, value, dtype, dimensions):
-        """
-        Add custom variable to the SOFA object, i.e., numeric or string arrays.
-
-        Parameters
-        ----------
-        name : str
-            Name of the new variable.
-        value : any
-            value to be added (see `dtype` for restrictions).
-        dtype : str
-            Type of the entry to be added in netCDF style:
-
-            ``'double'``
-                Use this to store numeric data that can be provided as number
-                list or numpy array.
-            ``'string'``
-                Use this to store string variables as numpy string arrays of
-                type ``'U'`` or ``'S'``.
-
-        dimensions : str
-            The shape of the new entry as a string. See
-            ``self.info('dimensions')``.
-
-        Examples
-        --------
-        .. code-block:: python
-
-            import sofar as sf
-            sofa = sf.Sofa("GeneralTF")
-
-            # add numeric data
-            sofa.add_variable("Temperature", 25.1, "double", "MI")
-
-            # add GLOBAL and Variable attribtue
-            sofa.add_entry(
-                "GLOBAL_DateMeasured", "8.08.2021", "attribute", None)
-            sofa.add_entry(
-                "Temperature_Units", "degree Celsius", "attribute", None)
-
-            # add a string data
-            sofa.add_variable(
-                "Comment", "Measured with wind screen", "string", "MS")
-        """
-
-        self._add_entry(name, value, dtype, dimensions)
-
-    def add_attribute(self, name, value):
-        """
-        Add custom attribute to the SOFA object.
-
-        Parameters
-        ----------
-        name : str
-            Name of the new attribute.
-        value : str
-            value to be added.
-
-        Examples
-        --------
-        .. code-block:: python
-
-            import sofar as sf
-            sofa = sf.Sofa("GeneralTF")
-
-            # add GLOBAL and Variable attribtue
-            sofa.add_attribute("GLOBAL_DateMeasured", "8.08.2021")
-            sofa.add_attribute("Data_Real_Units", "Pascal")
-
-        """
-
-        self._add_entry(name, value, 'attribute', None)
-
-    def delete(self, name):
-        """
-        Delete variable or attribute from SOFA object.
-
-        Note that mandatory data can not be deleted. Call
-        :py:func:`Sofa.info("optional") <sofar.sofar.Sofa.info>` to list all
-        optional variables and attributes.
-
-        Parameters
-        ----------
-        name : str
-            Name of the variable or attribute to be deleted
-        """
-        delattr(self, name)
-
-    def _add_entry(self, name, value, dtype, dimensions):
-        """
-        Add custom data to a SOFA object. See add_variable and add_attribute
-        for more information.
-        """
-
-        # check input
-        if hasattr(self, name):
-            raise ValueError(f"Entry {name} already exists")
-        if dtype not in ["attribute", "double", "string"]:
-            raise ValueError(
-                f"dtype is {dtype} but must be attribute, double, or string")
-        if "_" in name and dtype != "attribute":
-            raise ValueError(("underscores '_' in the name are only "
-                              "allowed for attributes"))
-        if dtype == "attribute":
-            if name.count("_") != 1 or \
-                    (name.startswith("Data_") and (name.count("_") == 0 or
-                                                   name.count("_") > 2)):
-                raise ValueError((f"The name of {name} must have the "
-                                  "form VariableName_AttributeName"))
-            if not name.startswith("GLOBAL_") and \
-                    name[:name.rindex("_")] not in self._convention:
-                raise ValueError((f"Adding Attribute {name} requires "
-                                  f"variable {name[:name.rindex('_')]}"))
-        if dimensions is None and dtype != "attribute":
-            raise ValueError(("dimensions must be provided for entries of "
-                              "dtype double and string"))
-        if dimensions is not None:
-            dimensions = dimensions.upper()
-            for dimension in dimensions:
-                if dimension not in "ERMNCIS":
-                    warnings.warn(
-                        f"Added custom dimension {dimensions} to SOFA object")
-
-        # add attribute to class
-        _add_custom_api_entry(
-            self, name, value, None, dimensions, dtype)
-
-    def verify(self, version="latest", issue_handling="raise"):
-        """
-        Verify a SOFA object against the SOFA standard.
-
-        This function updates the API, and checks the following
-
-        - Are all mandatory fields contained? If not mandatory fields are added
-          with their default value and a warning is raised.
-        - Are the names of variables and attributes in accordance to the SOFA
-          standard? If not a warning is raised.
-        - Are the data types in accordance with the SOFA standard?
-        - Are the dimensions of the variables consistent and in accordance
-          to the SOFA standard?
-        - Are the values of attributes consistent and in accordance to the
-          SOFA standard?
-
-        .. note::
-            :py:func:`~verify` is automatically called when you create a new
-            SOFA object, read a SOFA file from disk, and write a SOFA file to
-            disk (using the default parameters).
-
-        The API of a SOFA object consists of four parts, that are stored
-        dictionaries in private attributes. This is required for writing data
-        with :py:func:`~sofa.write_sofa` and should usually not be manipulated
-        outside of :py:func:`~verify`
-
-        self._convention
-            The SOFA convention with default values, variable dimensions, flags
-            and comments. These data are read from the official SOFA
-            conventions contained in the SOFA Matlab/Octave API.
-        self._dimensions
-            The detected dimensions of the data inside the SOFA object.
-        self._api
-            The size of the dimensions (see py:func:`~list_dimensions`). This
-            specifies the dimensions of the data inside the SOFA object.
-        self._custom
-            Stores information of custom variables that are not defined by the
-            convention. The format is the same as in `self._convention`.
-
-        Parameters
-        ----------
-        version : str, optional
-            The version to which the API is updated.
-
-            ``'latest'``
-                Use the latest API and upgrade the SOFA file if required.
-            ``'match'``
-                Match the version of the sofa file.
-            str
-                Version string, e.g., ``'1.0'``. Note that this might downgrade
-                the SOFA object
-
-            The default is ``'latest'``
-        issue_handling : str, optional
-            Defines how detected issues are handeled
-
-            ``'raise'``
-                Warnings and errors are raised if issues are detected
-            ``'print'``
-                Issues are printed without raising warnings and errors
-            ``'return'``
-                Issues are returned as string but neither raised nor printed
-            ``'ignore'``
-                Issues are ignored, i.e., not raised, printed, or returned.
-
-            The default is ``'raise'``.
-
-        Returns
-        -------
-        issues : str, None
-            Detected issues as a string. None if no issues were detected. Note
-            that this is only returned if ``issue_handling='return'`` (see
-            above)
-
-        """
-        # NOTE: This function collects warnings and errors and tries to output
-        # them in a block. This makes the code slightly more complicated but
-        # is more convenient for the user and with respect to a potential
-        # future web based tool for verifying SOFA files.
-
-        # initialize warning and error messages
-        error_msg = "\nERRORS\n------\n"
-        warning_msg = "\nWARNINGS\n--------\n"
-
-        # ---------------------------------------------------------------------
-        # 0. update the convention
-        self._update_convention(version)
-
-        # ---------------------------------------------------------------------
-        # 1. check if the mandatory attributes are contained
-        current_warning = ""
-        keys = [key for key in self.__dict__.keys() if not key.startswith("_")]
-
-        for key in self._convention.keys():
-            if self._mandatory(self._convention[key]["flags"]) \
-                    and key not in keys:
-                # add missing data with default value
-                self._protected = False
-                setattr(self, key, self._convention[key]["default"])
-                self._protected = True
-
-                # prepare to raise warning
-                current_warning += "- " + key + "\n"
-
-        if current_warning:
-            warning_msg += "Added mandatory data with default values:\n"
-            warning_msg += current_warning
-
-        # ---------------------------------------------------------------------
-        # 2. verify data type
-        current_error = ""
-        for key in keys:
-
-            # handle dimensions
-            dimensions = self._convention[key]["dimensions"]
-            dtype = self._convention[key]["type"]
-
-            # check data type
-            value = getattr(self, key)
-
-            if dtype == "attribute":
-                if not isinstance(value, str):
-                    current_error += \
-                        f"- {key} must be string but is {type(value)}\n"
-
-            elif dtype == "double":
-                # multiple checks needed because sofar does not force the user
-                # to initally pass data as numpy arrays
-                if not isinstance(value,
-                                  (np.int_, np.float_, np.double, np.ndarray)):
-                    current_error += (f"- {key} must be int, float or numpy "
-                                      f"array but is {type(value)}\n")
-
-                if isinstance(value, np.ndarray) and not (
-                        str(value.dtype).startswith('int') or
-                        str(value.dtype).startswith('float')):
-                    current_error += (f"- {key} must be int or float "
-                                      f"but is {type(value.dtype)}\n")
-
-            elif dtype == "string":
-                # multiple checks needed because sofar does not force the user
-                # to initally pass data as numpy arrays
-                if not isinstance(value, (str, np.ndarray)):
-                    current_error += (f"- {key} must be string or numpy array "
-                                      f"but is {type(value)}\n")
-
-                if isinstance(value, np.ndarray) and not (
-                        str(value.dtype).startswith('<U') or
-                        str(value.dtype).startswith('<S')):
-                    current_error += (f"- {key} must be U or S "
-                                      f"but is {type(value.dtype)}\n")
-
-            else:
-                # Could only be tested by manipulating JSON convention files
-                # (Could take different data types in the future and convert to
-                # numpy double arrays.)
-                current_error += (
-                    f"- {key}: Error in convention. Type must be "
-                    f"double, string, or attribute but is {dtype}\n")
-
-        if current_error:
-            error_msg += "Detected data of wrong type:\n"
-            error_msg += current_error
-
-        # if an error ocurred up to here, it has to be handled. Otherwise
-        # detecting the dimensions might fail. Warnings are not reported until
-        # the end
-        if error_msg != "\nERRORS\n------\n" and issue_handling != "ignore":
-            _, issues = self._verify_handle_issues(
-                    "\nWARNINGS\n--------\n", error_msg, issue_handling)
-
-            if issue_handling == "print":
-                return
-            else:  # (issue_handling == "return"):
-                return issues
-
-        # ---------------------------------------------------------------------
-        # 3. Verify names of entries
-
-        # check attributes without variables
-        current_error = ""
-        for key in keys:
-
-            if self._convention[key]["type"] != "attribute" or \
-                    key.count("_") == 0:
-                continue
-
-            if (key[:key.rindex("_")] not in self._convention and
-                    not key.startswith("GLOBAL_")):
-                current_error += "- " + key + "\n"
-
-        if current_error:
-            error_msg += "Detected attributes with missing variables:\n"
-            error_msg += current_error
-
-        # check number of underscores
-        current_error = ""
-        for key in keys:
-
-            if self._convention[key]["type"] != "attribute":
-                continue
-
-            # the case above caught attributes with too many underscores
-            if key.count("_") == 0:
-                current_error += "- " + key + "\n"
-
-        if current_error:
-            error_msg += (
-                "Detected attribute names with too many or little underscores."
-                " Names must have the form Variable_Attribute, Data_Attribute "
-                "(one underscore), or Data_Variable_Attribute (two "
-                "underscores):\n")
-            error_msg += current_error
-
-        # check numeric variables
-        current_error = ""
-        for key in keys:
-
-            if self._convention[key]["type"] == "attribute":
-                continue
-
-            if "_" in key.replace("Data_", ""):
-                current_error += "- " + key + "\n"
-
-        if current_error:
-            error_msg += (
-                "Detected variable names with too many underscores."
-                "Underscores are only allowed for the variable Data:\n")
-            error_msg += current_error
-
-        # ---------------------------------------------------------------------
-        # 4. Get dimensions (E, R, M, N, S, c, I, and custom)
-
-        # initialize required API fields
-        self._protected = False
-        self._dimensions = {}
-        self._api = {}
-        self._protected = True
-
-        # get keys for checking the dimensions (all SOFA variables)
-        keys = [key for key in self.__dict__.keys()
-                if key in self._convention
-                and self._convention[key]["dimensions"] is not None]
-        if hasattr(self, "_custom"):
-            keys_custom = [key for key in self._custom.keys()
-                           if not key.startswith("_")
-                           and self._custom[key]["dimensions"] is not None]
-            keys += keys_custom
-
-        S = 0
-        for key in keys:
-
-            value = getattr(self, key)
-            dimensions = self._convention[key]["dimensions"]
-
-            # - dimensions are given as string, e.g., 'mRN', or 'IC, MC'
-            # - defined by lower case letters in `dimensions`
-            for id, dim in enumerate(dimensions.split(", ")[0]):
-                if dim not in "ICS" and dim.islower():
-                    # numeric data
-                    self._api[dim.upper()] = \
-                        _nd_newaxis(value, 4).shape[id]
-                if dim == "S":
-                    # string data
-                    S = max(S, np.max(self._get_size_and_shape_of_string_var(
-                        value, key)[0]))
-
-        # add fixed sizes
-        self._api["C"] = 3
-        self._api["I"] = 1
-        self._api["S"] = S
-
-        # ---------------------------------------------------------------------
-        # 5. verify dimensions of data
-        current_error = ""
-        for key in keys:
-
-            # handle dimensions
-            dimensions = self._convention[key]["dimensions"]
-            dtype = self._convention[key]["type"]
-
-            # get value and actual shape
-            try:
-                value = getattr(self, key).copy()
-            except AttributeError:
-                value = getattr(self, key)
-
-            if dtype in ["attribute", "string"]:
-                # string or string array like data
-                shape_act = self._get_size_and_shape_of_string_var(
-                    value, key)[1]
-            elif len(dimensions.split(",")[0]) > 1:
-                # multidimensional array like data
-                shape_act = _atleast_nd(value, 4).shape
-            else:
-                # scalar of single dimensional array like data
-                shape_act = (np.array(value).size, )
-
-            shape_matched = False
-            for dim in dimensions.split(", "):
-
-                # get the reference shape ('S' translates to a shape of 1,
-                # because the strings are stored in an array whose shape does
-                # not reflect the max. lengths of the actual strings inside it)
-                shape_ref = tuple(
-                    [self._api[d.upper()] if d != "S" else 1 for d in dim])
-
-                # get shape for comparison to correct length by cropping and
-                # appending singelton dimensions if required
-                shape_compare = shape_act[:len(shape_ref)]
-                for _ in range(len(shape_ref) - len(shape_compare)):
-                    shape_compare += (1, )
-
-                # check if the shapes match and write to API
-                if shape_compare == shape_ref:
-                    shape_matched = True
-                    self._dimensions[key] = dim.upper()
-                    break
-
-            if not shape_matched:
-                # get possible dimensions in verbose form, i.e., "(M=2, C=3)""
-                dimensions_verbose = []
-                for dim in dimensions.upper().replace(" ", "").split(","):
-                    dimensions_verbose.append(
-                        f"({', '.join([f'{d}={self._api[d]}' for d in dim])})")
-
-                current_error += (
-                    f"- {key} has shape {shape_compare} but must "
-                    f"have {', '.join(dimensions_verbose)}\n")
-
-        if current_error:
-            error_msg += "Detected variables of wrong shape:\n"
-            error_msg += current_error
-
-        # ---------------------------------------------------------------------
-        # 6. check restrictions on the content of SOFA files
-        data, data_type, api, convention, unit_aliases = _sofa_restrictions()
-
-        # general restrictions on data
-        current_error = ""
-        for key in data.keys():
-
-            ref = data[key]["value"]
-            if hasattr(self, key):
-
-                # test if the value is valid
-                test = getattr(self, key)
-                if not self._verify_value(test, ref, unit_aliases):
-                    current_error += \
-                        f"- {key} is {test} but must be {', '.join(ref)}\n"
-
-                # check dependencies
-                if "dependency" not in data[key]:
-                    continue
-
-                for key_dep, ref_dep in data[key]["dependency"].items():
-
-                    # check if dependency is contained in SOFA object
-                    # hard to test, because mandatory fields are added by sofar
-                    # this is more to be future proof
-                    if not hasattr(self, key_dep):
-                        current_error += (f"- {key_dep} must be given if "
-                                          f"{key} is in SOFA object\n")
-                        continue
-
-                    # check if dependency has the correct value
-                    test_dep = getattr(self, key_dep)
-                    if not (isinstance(ref, list) and
-                            isinstance(ref_dep, list)):
-                        continue
-
-                    idx = ref.index(test)
-                    if not self._verify_value(test_dep, ref_dep[idx],
-                                              unit_aliases):
-                        current_error += (
-                            f"- {key_dep} is {test_dep} but must be "
-                            f"{ref_dep[idx]} if {key} is {test}\n")
-
-        # restriction posed by GLOBAL_DataType
-        if self.GLOBAL_DataType.startswith("FIR"):
-            data_str = "FIR"
-        elif self.GLOBAL_DataType.startswith("TF"):
-            data_str = "TF"
-        elif self.GLOBAL_DataType.startswith("SOS"):
-            data_str = "SOS"
-        else:
-            # the data type was tested before. This is to prevent redundant
-            # errors in the next for loop
-            data_str = False
-
-        if data_str:
-            for key, value in data_type[data_str].items():
-
-                # hard to test. included to detect problems with future
-                # conventions
-                if not hasattr(self, key):
-                    current_error += (
-                        f"- {key} must be contained if"
-                        f" GLOBAL_DataType={self.GLOBAL_DataType}\n")
-
-                if value is not None and getattr(self, key) not in value[0]:
-                    current_error += (f"{key} is {getattr(self, key)} but "
-                                      f"must be {value[1]}\n")
-
-        # restrictions on the API
-        for key, value in api.items():
-            if hasattr(self, key) and getattr(self, key) == value["value"]:
-                size = getattr(self, "_api")[value["API"][0]]
-                if size not in value["API"][1]:
-                    current_error += \
-                        (f"- Dimension {value['API'][0]} is of size {size} "
-                         f"but must be {value['API'][2]} if "
-                         f"{key} is {getattr(self, key)}\n")
-
-        # restrictions from the SOFA convention (on the data and API)
-        if self.GLOBAL_SOFAConventions in convention:
-            for key, ref in convention[self.GLOBAL_SOFAConventions].items():
-
-                if key == "API":
-                    for dimension, size in ref.items():
-                        if self._api[dimension] != size:
-                            current_error += \
-                    (f"- Dimension {dimension} is of size "  # noqa
-                     f"{self._api[dimension]} but must be {size} if "
-                     f"GLOBAL_SOFAConventions is {key}\n")
-                else:
-                    value = getattr(self, key)
-                    if value not in ref:
-                        current_error += \
-                            f"{key} is {value} but must be {ref}\n"
-
-        if current_error:
-            error_msg += "Detected violations of the SOFA convention:\n"
-            error_msg += current_error
-
-        # handle warnings and errors
-        if issue_handling != "ignore":
-            error_occurred, issues = self._verify_handle_issues(
-                    warning_msg, error_msg, issue_handling)
-
-            if error_occurred:
-                if issue_handling == "print":
-                    return
-                elif issue_handling == "return":
-                    return issues
-
-    @staticmethod
-    def _verify_value(test, ref, unit_aliases):
-        """
-        Check a value agains the SOFA standard for Sofa.verify()
-
-        Parameters
-        ----------
-        test :
-            the value under test
-        ref :
-            the value enforced by the SOFA standard
-        unit_aliases :
-            dict of aliases for units from _sofa_restrictions()
-
-        Returns
-        -------
-        ``True`` if `test` and `ref` agree, ``False`` otherwise
-        """
-
-        value_valid = True
-
-        # Don't check the value if ref is None or test in ref
-        if ref is not None and test not in ref:
-
-            # in case test is a string it might be a unit and unit aliases
-            # according to the SOFA standard must be checked
-
-            # Following the SOFA standard AES69-2020, units may be separated by
-            # `, ` (comma and space), `,` (comma only), and ` ` (space only).
-            # (regexp ', ?' matches ', ' and ',')
-            ref = re.split(', ?| ', ref) if isinstance(ref, str) else ref
-            units = re.split(', ?| ', test) if isinstance(test, str) else []
-
-            # check if number of units agree
-            if not units or len(ref) != len(units):
-                value_valid = False
-                return value_valid
-
-            # check if units are valid
-            for unit, unit_ref in zip(units, ref):
-                if unit != unit_ref and (unit not in unit_aliases
-                                         or unit_aliases[unit] != unit_ref):
-                    value_valid = False
-                    break
-
-        return value_valid
-
-    @staticmethod
-    def _verify_handle_issues(warning_msg, error_msg, issue_handling):
-        """Handle warnings and errors from Sofa.verify"""
-
-        # handle warnings
-        if warning_msg != "\nWARNINGS\n--------\n":
-            if issue_handling == "raise":
-                warnings.warn(warning_msg)
-            elif issue_handling == "print":
-                print(warning_msg)
-        else:
-            warning_msg = None
-
-        # handle errors
-        if error_msg != "\nERRORS\n------\n":
-            if issue_handling == "raise":
-                raise ValueError(error_msg)
-            elif issue_handling == "print":
-                print(error_msg)
-        else:
-            error_msg = None
-
-        # flag indicating if an error occurred
-        error_occurred = error_msg is not None
-
-        # verbose issue message
-        if warning_msg and error_msg:
-            issues = error_msg + "\n" + warning_msg
-        elif warning_msg:
-            issues = warning_msg
-        elif error_msg:
-            issues = error_msg
-        else:
-            issues = None
-
-        return error_occurred, issues
-
-    def copy(self):
-        """Return a copy of the SOFA object."""
-        return deepcopy(self)
-
-    def _update_convention(self, version):
-        """
-        Add SOFA convention to SOFA object in private attribute `_convention`.
-        If The object already contains a convention, it will be overwritten.
-
-        Parameters
-        ----------
-        version : str
-            ``'latest'``
-                Use the latest API and upgrade the SOFA file if required.
-            ``'match'``
-                Match the version of the sofa file.
-            str
-                Version string, e.g., ``'1.0'``.
-        """
-
-        # verify convention and version
-        c_current = self.GLOBAL_SOFAConventions
-        v_current = str(self.GLOBAL_SOFAConventionsVersion)
-
-        v_new = _verify_convention_and_version(
-                version, v_current, c_current)
-
-        # load and add convention and version
-        convention = self._load_convention(
-            c_current, v_new)
-        self._convention = convention
-
-        if v_current != v_new:
-            self._protected = False
-            self.GLOBAL_SOFAConventionsVersion = v_new
-            self._protected = True
-
-        # feedback in case of up/downgrade
-        if float(v_current) < float(v_new):
-            warnings.warn(("Upgraded SOFA object from "
-                           f"version {v_current} to {v_new}"))
-        elif float(v_current) > float(v_new):
-            warnings.warn(("Downgraded SOFA object from "
-                           f"version {v_current} to {v_new}"))
-
-        # check if custom fields can be added
-        if hasattr(self, "_custom"):
-            for key in self._custom:
-                self._convention[key] = self._custom[key]
-
-    def _load_convention(self, convention, version):
-        """
-        Load SOFA convention from json file.
-
-        Parameters
-        ----------
-        convention : str
-            The name of the convention from which the SOFA file is created. See
-            :py:func:`~sofar.list_conventions`.
-        version : str
-            ``'latest'``
-                Use the latest API and upgrade the SOFA file if required.
-            str
-                Version string, e.g., ``'1.0'``.
-
-        Returns
-        -------
-        convention : dict
-            The SOFA convention as a dictionary
-        """
-        # check input
-        if not isinstance(convention, str):
-            raise TypeError(("Convention must be a string "
-                             f"but is of type {type(convention)}"))
-
-        # get and check path to json file
-        paths = _get_conventions("path")
-        path = [path for path in paths
-                if os.path.basename(path).startswith(convention + "_")]
-
-        if not len(path):
-            raise ValueError(
-                (f"Convention '{convention}' not found. See "
-                 "sofar.list_conventions() for available conventions."))
-
-        # get available versions as strings
-        versions = [p.split('_')[-1][:-5] for p in path]
-
-        # select the correct version
-        if version == "latest":
-            versions = np.array([float(v) for v in versions])
-            path = path[np.argmax(versions)]
-        else:
-            if version not in versions:
-                raise ValueError((
-                    f"Version {version} not found. "
-                    f"Available versions are {versions}"))
-            path = path[versions.index(version)]
-
-        # read convention from json file
-        with open(path, "r") as file:
-            convention = json.load(file)
-
-        # replace ':' and '.' in key names by '_'
-        convention = {
-            key.replace(':', '_'): value for key, value in convention.items()}
-        convention = {
-            key.replace('.', '_'): value for key, value in convention.items()}
-
-        return convention
-
-    def _convention_to_sofa(self, mandatory):
-        """
-        Use SOFA convention to create attributes with default values.
-
-        Parameters
-        ----------
-        mandatory : bool
-            Flag to indicate if only mandatory fields are to be included.
-        """
-
-        # populate the SOFA file
-        for key in self._convention.keys():
-
-            # skip optional fields if requested
-            if not self._mandatory(self._convention[key]["flags"]) \
-                    and mandatory:
-                continue
-
-            # get the default value
-            default = self._convention[key]["default"]
-            if isinstance(default, list):
-                ndim = len(self._convention[key]["dimensions"].split(", ")[0])
-                default = _atleast_nd(default, ndim)
-
-            # create attribute with default value
-            setattr(self, key, default)
-
-        # write API and date specific fields (some read only)
-        now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
-        self._protected = False
-        self.GLOBAL_DateCreated = now
-        self.GLOBAL_DateModified = now
-        self.GLOBAL_APIName = "sofar SOFA API for Python (pyfar.org)"
-        self.GLOBAL_APIVersion = sf.__version__
-        self.GLOBAL_ApplicationName = "Python"
-        self.GLOBAL_ApplicationVersion = (
-            f"{platform.python_version()} "
-            f"[{platform.python_implementation()} - "
-            f"{platform.python_compiler()}]")
-        self._protected = True
-
-    @staticmethod
-    def _get_size_and_shape_of_string_var(value, key):
-        """
-        String variables can be strings, list of strings, or numpy arrays of
-        strings. This functions returns the length of the longest string S
-        inside the string variable and the shape of the string variable as
-        required by the SOFA definition. Note that the shape is the shape of
-        the array that holds the strings. NETCDF stores all string variables in
-        arrays.
-        """
-
-        if isinstance(value, str):
-            S = len(value)
-            shape = (1, 1)
-        elif isinstance(value, list):
-            S = len(max(value, key=len))
-            shape = np.array(value).shape
-        elif isinstance(value, np.ndarray):
-            S = max(np.vectorize(len)(value))
-            shape = value.shape
-        else:
-            raise TypeError((f"{key} must be a string, numpy string array, "
-                             "or list of strings"))
-
-        return S, shape
-
-    @staticmethod
-    def _mandatory(flags):
-        """
-        Check if a field is mandatory
-
-        Parameters
-        ----------
-        flags : None, str
-            The flags from convention[key]["flags"]
-
-        Returns
-        -------
-        is_mandatory : bool
-        """
-        # skip optional fields if requested
-        if flags is None:
-            is_mandatory = False
-        elif "m" not in flags:
-            is_mandatory = False
-        else:
-            is_mandatory = True
-
-        return is_mandatory
-
-    @staticmethod
-    def _read_only(flags):
-        """
-        Check if a field is read only
-
-        Parameters
-        ----------
-        flags : None, str
-            The flags from convention[key]["flags"]
-
-        Returns
-        -------
-        is_read_only : bool
-        """
-        # skip optional fields if requested
-        if flags is None:
-            is_read_only = False
-        elif "r" not in flags:
-            is_read_only = False
-        else:
-            is_read_only = True
-
-        return is_read_only
-
-
-def _update_conventions(conventions_path=None):
-    """
-    Update SOFA conventions.
-
-    A SOFA convention defines the kind of data and the data format that is
-    stored in a SOFA file. Updating the conventions is done in two steps:
-
-    1.
-        Download official SOFA conventions as csv files from
-        https://github.com/sofacoustics/API_MO/tree/master/API_MO/conventions.
-    2.
-        Convert csv files to json files for easier handling
-
-    The csv and json files are stored at sofar/conventions. Sofar works only on
-    the json files. To get a list of all currently available SOFA conventions
-    and their paths see :py:func:`~sofar.list_conventions`.
-
-    .. note::
-        If the official convention contain errors, calling this function might
-        break sofar. Be sure that you want to do this.
-
-    Parameters
-    ----------
-    conventions_path : str, optional
-        Path to the folder where the conventions are saved. The default is
-        ``None``, which saves the conventions inside the sofar package.
-        Conventions saved under a different path can not be used by sofar.
-    """
-
-    # url for parsing and downloading the convention files
-    url = ("https://github.com/sofacoustics/API_MO/tree/"
-           "master/API_MO/conventions")
-    url_raw = ("https://raw.githubusercontent.com/sofacoustics/API_MO/"
-               "master/API_MO/conventions")
-    ext = 'csv'
-
-    print(f"Reading SOFA conventions from {url} ...")
-
-    # get file names of conventions from the SOFA Matlab/Octave API
-    page = requests.get(url).text
-    soup = BeautifulSoup(page, 'html.parser')
-    conventions = [os.path.split(node.get('href'))[1]
-                   for node in soup.find_all('a')
-                   if node.get('href').endswith(ext)]
-
-    # directory handling
-    if conventions_path is None:
-        conventions_path = os.path.join(os.path.dirname(__file__),
-                                        "conventions")
-    if not os.path.isdir(os.path.join(conventions_path, "source")):
-        os.mkdir(os.path.join(conventions_path, "source"))
-
-    # Loop and download conventions if they changed
-    updated = False
-    for convention in conventions:
-
-        # exclude these conventions
-        if convention.startswith(("General_", "GeneralString_")):
-            continue
-
-        filename_csv = os.path.join(conventions_path, "source", convention)
-
-        # download SOFA convention definitions to package diretory
-        data = requests.get(url_raw + "/" + convention)
-        # remove trailing tabs
-        data = data.content.replace(b"\t\n", b"\n").replace(b"\r\n", b"\n")
-
-        # check if convention needs to be added or updated
-        update = False
-        if not os.path.isfile(filename_csv):
-            update = True
-            updated = f"- added new convention: {convention}"
-        else:
-            with open(filename_csv, "rb") as file:
-                data_current = b"".join(file.readlines())
-                data_current = data_current.replace(b"\r\n", b"\n")
-            if data_current != data:
-                update = True
-                updated = f"- updated existing convention: {convention}"
-
-        # update convention
-        if update:
-            with open(filename_csv, "wb") as file:
-                file.write(data)
-            print(updated)
-
-    # compile json files from csv file
-    # (this is also done if nothing changed. It won't affect the content of
-    #  the json files but the time-stamp will be updated)
-    _compile_conventions()
-
-    if updated:
-        print("... done.")
-    else:
-        print("... conventions already up to date.")
-
-
-def _compile_conventions(conventions_path=None):
-    """
-    Compile SOFA conventions (json files) from source conventions (csv files
-    from SOFA API_MO), i.e., only do step 2 from `_update_conventions`. This is
-    a helper function for debugging and developing and might break sofar.
-
-    Parameters
-    ----------
-    conventions_path : str
-        Path to the folder containing the conventions as json files (might be
-        empty) and the source convention as csv files in the subfolder `source`
-        (must not be empty). The default is ``None``, which uses the
-        default location inside the sofar package.
-    """
-    # directory handling
-    if conventions_path is None:
-        conventions_path = os.path.join(os.path.dirname(__file__),
-                                        "conventions")
-    if not os.path.isdir(os.path.join(conventions_path, "source")):
-        raise ValueError("conventions_path must contain the folder 'source'")
-
-    # get list of source conventions
-    csv_files = glob.glob(os.path.join(
-        conventions_path, "source", "*.csv"))
-    csv_files = [os.path.split(csv_file)[1] for csv_file in csv_files]
-
-    for csv_file in csv_files:
-
-        # directories for reading and writing
-        json_file = os.path.join(conventions_path, csv_file[:-3] + "json")
-        csv_file = os.path.join(conventions_path, "source", csv_file)
-
-        # convert SOFA conventions from csv to json
-        convention_dict = sf.sofar._convention_csv2dict(csv_file)
-        with open(json_file, 'w') as file:
-            json.dump(convention_dict, file, indent=4)
-
-
-def list_conventions():
-    """
-    List available SOFA conventions by printing to the console.
-    """
-    print(_get_conventions("string"))
-
-
-def _get_conventions(return_type):
-    """
-    Get available SOFA conventions.
-
-    Parameters
-    ----------
-    return_type : string, optional
-        ``'path'``
-            Return a list with the full paths and filenames of the convention
-            files (json files)
-        ``'path_source'``
-            Return a list with the full paths and filenames of the source
-            convention files from API_MO (csv files)
-        ``'name'``
-            Return a list of the convention names without version
-        ``'name_version'``
-            Return a list of tuples containing the convention name and version.
-        ``'string'``
-            Returns a string that lists the names and versions of all
-            conventions.
-
-    Returns
-    -------
-    See parameter `return_type`.
-    """
-    # directory containing the SOFA conventions
-    if return_type == "path_source":
-        directory = os.path.join(
-            os.path.dirname(__file__), "conventions", "source")
-        reg_str = "*.csv"
-    else:
-        directory = os.path.join(os.path.dirname(__file__), "conventions")
-        reg_str = "*.json"
-
-    # SOFA convention files
-    paths = [file for file in glob.glob(os.path.join(directory, reg_str))]
-
-    conventions_str = "Available SOFA conventions:\n"
-
-    conventions = []
-    versions = []
-    for path in paths:
-        fileparts = os.path.basename(path).split(sep="_")
-        conventions += [fileparts[0]]
-        versions += [fileparts[1][:-5]]
-        conventions_str += f"{conventions[-1]} (Version {versions[-1]})\n"
-
-    if return_type is None:
-        return
-    elif return_type.startswith("path"):
-        return paths
-    elif return_type == "name":
-        return conventions
-    elif return_type == "name_version":
-        return [(n, v) for n, v in zip(conventions, versions)]
-    elif return_type == "string":
-        return conventions_str
-    else:
-        raise ValueError(f"return_type {return_type} is invalid")
-
-
-def read_sofa(filename, verify=True, version="latest", verbose=True):
-    """
-    Read SOFA file from disk and convert it to SOFA object.
-
-    Numeric data is returned as floats or numpy float arrays unless they have
-    missing data, in which case they are returned as numpy masked arrays.
-
-    Parameters
-    ----------
-    filename : str
-        The filename. '.sofa' is appended to the filename, if it is not
-        explicitly given.
-    verify : bool, optional
-        Verify and update the SOFA object by calling :py:func:`~Sofa.verify`.
-        This helps to find potential errors in the default values and is thus
-        recommended. If reading a file does not work, try to call `Sofa` with
-        ``verify=False``. The default is ``True``.
-    version : str, optional
-        Control if the SOFA file convention is changed.
-
-        ``'latest'``
-            Update the conventions to the latest version
-        ``'match'``
-            Do not change the conventions version, i.e. match the version
-            of the SOFA file that is being read.
-        str
-            Force specific version, e.g., ``'1.0'``. Note that this might
-            downgrade the SOFA object.
-
-        The default is ``'latest'``
-    verbose : bool, optional
-        Print the names of detected custom variables and attributes. The
-        default is ``True``
-
-    Returns
-    -------
-    sofa : Sofa
-        The SOFA object filled with the default values of the convention.
-
-    Notes
-    -----
-
-    1. Missing dimensions are appended when writing the SOFA object to disk.
-       E.g., if ``sofa.Data_IR`` is of shape (1, 2) it is written as an array
-       of shape (1, 2, 1) because the SOFA standard AES69-2020 defines it as a
-       three dimensional array with the dimensions (`M: measurements`,
-       `R: receivers`, `N: samples`)
-    2. When reading data from a SOFA file, array data is always returned as
-       numpy arrays and singleton trailing dimensions are discarded (numpy
-       default). I.e., ``sofa.Data_IR`` will again be an array of shape (1, 2)
-       after writing and reading to and from disk.
-    3. One dimensional arrays with only one element will be converted to scalar
-       values. E.g. ``sofa.Data_SamplingRate`` is stored as an array of shape
-       (1, ) inside SOFA files (according to the SOFA standard AES69-2020) but
-       will be a scalar inside SOFA objects after reading from disk.
-    """
-
-    # check the filename
-    if not filename.endswith('.sofa'):
-        raise ValueError("Filename must end with .sofa")
-    if not os.path.isfile(filename):
-        raise ValueError(f"{filename} does not exist")
-
-    # attributes that are skipped
-    skip = ["_Encoding"]
-
-    # init list of all and custom attributes
-    all_attr = []
-    custom = []
-
-    # open new NETCDF4 file for reading
-    with Dataset(filename, "r", format="NETCDF4") as file:
-
-        # get convention name and version
-        convention = getattr(file, "SOFAConventions")
-        all_attr.append("GLOBAL_SOFAConventions")
-        version_in = getattr(file, "SOFAConventionsVersion")
-        all_attr.append("GLOBAL_SOFAConventionsVersion")
-
-        # check if convention and version exist
-        version_out = _verify_convention_and_version(
-            version, version_in, convention)
-
-        # get SOFA object with default values
-        sofa = sf.Sofa(convention, version=version_out, verify=verify)
-
-        # allow writing read only attributes
-        sofa._protected = False
-
-        # load global attributes
-        for attr in file.ncattrs():
-
-            if attr in ["SOFAConventionsVersion", "SOFAConventions"]:
-                # convention and version were already set above
-                continue
-
-            value = getattr(file, attr)
-            all_attr.append("GLOBAL_" + attr)
-
-            if not hasattr(sofa, "GLOBAL_" + attr):
-                _add_custom_api_entry(sofa, "GLOBAL_" + attr, value, None,
-                                      None, "attribute")
-                custom.append("GLOBAL_" + attr)
-                sofa._protected = False
-            else:
-                setattr(sofa, "GLOBAL_" + attr, value)
-
-        # load data
-        for var in file.variables.keys():
-
-            value = _format_value_from_netcdf(file[var][:], var)
-            all_attr.append(var.replace(".", "_"))
-
-            if hasattr(sofa, var.replace(".", "_")):
-                setattr(sofa, var.replace(".", "_"), value)
-            else:
-                dimensions = "".join([d for d in file[var].dimensions])
-                # SOFA only uses dtypes 'double' and 'S1' but netCDF has more
-                dtype = "string" if file[var].datatype == "S1" else "double"
-                _add_custom_api_entry(sofa, var.replace(".", "_"), value, None,
-                                      dimensions, dtype)
-                custom.append(var.replace(".", "_"))
-                sofa._protected = False
-
-            # load variable attributes
-            for attr in [a for a in file[var].ncattrs() if a not in skip]:
-
-                value = getattr(file[var], attr)
-                all_attr.append(var.replace(".", "_") + "_" + attr)
-
-                if not hasattr(sofa, var.replace(".", "_") + "_" + attr):
-                    _add_custom_api_entry(
-                        sofa, var.replace(".", "_") + "_" + attr, value, None,
-                        None, "attribute")
-                    custom.append(var.replace(".", "_") + "_" + attr)
-                    sofa._protected = False
-                else:
-                    setattr(sofa, var.replace(".", "_") + "_" + attr, value)
-
-    # remove fields from initial Sofa object that were not contained in NetCDF
-    # file (initial Sofa object contained mandatory and optional fields)
-    attrs = [attr for attr in sofa.__dict__.keys() if not attr.startswith("_")]
-    for attr in attrs:
-        if attr not in all_attr:
-            delattr(sofa, attr)
-
-    # do not allow writing read only attributes any more
-    sofa._protected = True
-
-    # notice about custom entries
-    if custom and verbose:
-        print(("SOFA file contained custom entries\n"
-               "----------------------------------\n"
-               f"{', '.join(custom)}"))
-
-    # update api
-    if verify:
-        try:
-            sofa.verify(version)
-        except: # noqa (No error handling - just improved verbosity)
-            raise ValueError((
-                "The SOFA object could not be verified, maybe due to errornous"
-                " data. Call sofa=sofar.read_sofa(filename, verify=False) and "
-                "than sofa.verify() to get more information"))
-
-    return sofa
-
-
-def write_sofa(filename: str, sofa: Sofa, version="latest", compression=4):
-    """
-    Write a SOFA object to disk as a SOFA file.
-
-    Parameters
-    ----------
-    filename : str
-        The filename. '.sofa' is appended to the filename, if it is not
-        explicitly given.
-    sofa : object
-        The SOFA object that is written to disk
-    version : str
-        The SOFA object is verified and updated with :py:func:`~Sofa.verify`
-        before writing to disk. Version specifies, which version of the
-        convention is used:
-
-        ``'latest'``
-            Use the latest version upgrade the SOFA file if required.
-        ``'match'``
-            Match the version of the SOFA object.
-        str
-            Version string, e.g., ``'1.0'``.
-
-        The default is ``'latest'``.
-    compression : int
-        The level of compression with ``0`` being no compression and ``9``
-        being the best compression. The default of ``9`` optimizes the file
-        size but increases the time for writing files to disk.
-
-    Notes
-    -----
-
-    1. Missing dimensions are appended when writing the SOFA object to disk.
-       E.g., if ``sofa.Data_IR`` is of shape (1, 2) it is written as an array
-       of shape (1, 2, 1) because the SOFA standard AES69-2020 defines it as a
-       three dimensional array with the dimensions (`M: measurements`,
-       `R: receivers`, `N: samples`)
-    2. When reading data from a SOFA file, array data is always returned as
-       numpy arrays and singleton trailing dimensions are discarded (numpy
-       default). I.e., ``sofa.Data_IR`` will again be an array of shape (1, 2)
-       after writing and reading to and from disk.
-    3. One dimensional arrays with only one element will be converted to scalar
-       values. E.g. ``sofa.Data_SamplingRate`` is stored as an array of shape
-       (1, ) inside SOFA files (according to the SOFA standard AES69-2020) but
-       will be a scalar inside SOFA objects after reading from disk.
-    """
-
-    # check the filename
-    if not filename.endswith('.sofa'):
-        raise ValueError("Filename must end with .sofa")
-
-    # setting the netCDF compression parameter
-    zlib = False if compression == 0 else True
-
-    # update the dimensions
-    sofa.verify(version)
-
-    # list of all attribute names
-    all_keys = [key for key in sofa.__dict__.keys() if not key.startswith("_")]
-
-    # open new NETCDF4 file for writing
-    with Dataset(filename, "w", format="NETCDF4") as file:
-
-        # write dimensions
-        for dim in sofa._api:
-            file.createDimension(dim, sofa._api[dim])
-
-        # write global attributes
-        keys = [key for key in all_keys if key.startswith("GLOBAL_")]
-        for key in keys:
-            setattr(file, key[7:], str(getattr(sofa, key)))
-
-        # write data
-        for key in all_keys:
-
-            # skip attributes
-            # Note: This definition of attribute is blurry:
-            # lax definition:
-            #   sofa._convention[key]["type"] == "attribute":
-            # strict definition:
-            #   ("_" in key and not key.startswith("Data_")) or \
-            #       key.count("_") > 1
-            #
-            # The strict definition is implicitly included in the SOFA standard
-            # since underscores only occur for variables starting with Data_
-            if sofa._convention[key]["type"] == "attribute":
-                continue
-
-            # get the data and type and shape
-            value, dtype = _format_value_for_netcdf(
-                getattr(sofa, key), key, sofa._convention[key]["type"],
-                sofa._dimensions[key], sofa._api["S"])
-
-            # create variable and write data
-            shape = tuple([dim for dim in sofa._dimensions[key]])
-            tmp_var = file.createVariable(
-                key.replace("Data_", "Data."), dtype, shape,
-                zlib=zlib, complevel=compression)
-            if dtype == "f8":
-                tmp_var[:] = value
-            else:
-                tmp_var[:] = stringtochar(value)
-                tmp_var._Encoding = "ascii"
-
-            # write variable attributes
-            sub_keys = [k for k in all_keys if k.startswith(key + "_")]
-            for sub_key in sub_keys:
-                setattr(tmp_var, sub_key[len(key)+1:],
-                        str(getattr(sofa, sub_key)))
-
-
-def equals(sofa_a, sofa_b, verbose=True, exclude=None):
-    """
-    Compare two SOFA objects against each other.
-
-    Parameters
-    ----------
-    sofa_a : Sofa
-        SOFA object
-    sofa_b : Sofa
-        SOFA object
-    verbose : bool, optional
-        Print differences to the console. The default is True.
-    exclude : str, optional
-        Specify what fields should be excluded from the comparison
-
-        ``'GLOBAL'``
-            Exclude all global attributes, i.e., fields starting with 'GLOBAL:'
-        ``'DATE'``
-            Exclude date attributs, i.e., fields that contain 'Date'
-        ``'ATTR'``
-            Exclude all attributes, i.e., fields that contain ':'
-
-        The default is None, which does not exclude anything.
-
-    Returns
-    -------
-    is_identical : bool
-        ``True`` if sofa_a and sofa_b are identical, ``False`` otherwise.
-    """
-
-    is_identical = True
-
-    # get and filter keys
-    # ('_*' are SOFA object private variables, '__' are netCDF attributes)
-    keys_a = [k for k in sofa_a.__dict__.keys() if not k.startswith("_")]
-    keys_b = [k for k in sofa_b.__dict__.keys() if not k.startswith("_")]
-
-    if exclude is not None:
-        if exclude.upper() == "GLOBAL":
-            keys_a = [k for k in keys_a if not k.startswith("GLOBAL_")]
-            keys_b = [k for k in keys_b if not k.startswith("GLOBAL_")]
-        elif exclude.upper() == "ATTR":
-            keys_a = [k for k in keys_a if
-                      sofa_a._convention[k]["type"] != "attribute"]
-            keys_b = [k for k in keys_b if
-                      sofa_b._convention[k]["type"] != "attribute"]
-        elif exclude.upper() == "DATE":
-            keys_a = [k for k in keys_a if "Date" not in k]
-            keys_b = [k for k in keys_b if "Date" not in k]
-        else:
-            raise ValueError(
-                f"exclude is {exclude} but must be GLOBAL, DATE, or ATTR")
-
-    # check for equal length
-    if len(keys_a) != len(keys_b):
-        is_identical = _equals_raise_warning((
-            f"not identical: sofa_a has {len(keys_a)} attributes for "
-            f"comparison and sofa_b has {len(keys_b)}."), verbose)
-
-        return is_identical
-
-    # check if the keys match
-    if set(keys_a) != set(keys_b):
-        is_identical = _equals_raise_warning(
-            "not identical: sofa_a and sofa_b do not have the ame attributes",
-            verbose)
-
-        return is_identical
-
-    # compare the data inside the SOFA object
-    for key in keys_a:
-
-        # get data and types
-        a = getattr(sofa_a, key)
-        b = getattr(sofa_b, key)
-        type_a = sofa_a._convention[key]["type"]
-        type_b = sofa_b._convention[key]["type"]
-
-        # compare attributes
-        if type_a == "attribute" and type_b == "attribute":
-
-            # handling versions (might be integer, float, or string)
-            if not isinstance(a, str) or not isinstance(a, str):
-                a = str(float(a))
-                b = str(float(b))
-
-            # compare
-            if a != b:
-                is_identical = _equals_raise_warning(
-                    f"not identical: different values for {key}", verbose)
-
-        # compare double variables
-        elif type_a == "double" and type_b == "double":
-
-            try:
-                npt.assert_allclose(np.squeeze(a), np.squeeze(b))
-            except AssertionError:
-                is_identical = _equals_raise_warning(
-                    "not identical: different values for {key}", verbose)
-
-        # compare string variables
-        elif type_a == "string" and type_b == "string":
-            try:
-                assert np.all(
-                    np.squeeze(a).astype("S") == np.squeeze(b).astype("S"))
-            except AssertionError:
-                is_identical = _equals_raise_warning(
-                    "not identical: different values for {key}", verbose)
-        else:
-            is_identical = _equals_raise_warning(
-                (f"not identical: {key} has different data types "
-                 f"({type_a}, {type_b})"), verbose)
-
-    return is_identical
-
-
-def _equals_raise_warning(message, verbose):
-    if verbose:
-        warnings.warn(message)
-    return False
-
-
-def _convention_csv2dict(file: str):
-    """
-    Read SOFA convention from csv file and convert to json file. The csv files
-    are taken from the official Matlab/Octave SOFA API.
-
-    Parameters
-    ----------
-    file : str
-        filename of the SOFA convention
-
-    Returns
-    -------
-    convention : dict
-        SOFA convention as nested dictionary. Each attribute is a sub
-        dictionary with the keys `default`, `flags`, `dimensions`, `type`, and
-        `comment`.
-    """
-
-    # read the file
-    # (encoding should be changed to utf-8 after the SOFA conventions repo is
-    # clean.)
-    # TODO: add explicit test for this function that checks the output
-    with open(file, 'r', encoding="windows-1252") as fid:
-        lines = fid.readlines()
-
-    # write into dict
-    convention = {}
-    for idl, line in enumerate(lines):
-
-        try:
-            # separate by tabs
-            line = line.strip().split("\t")
-            # parse the line entry by entry
-            for idc, cell in enumerate(line):
-                # detect empty cells and leading trailing white spaces
-                cell = None if cell.replace(' ', '') == '' else cell.strip()
-                # nothing to do for empty cells
-                if cell is None:
-                    line[idc] = cell
-                    continue
-                # parse text cells that do not contain arrays
-                if cell[0] != '[':
-                    # check for numbers
-                    try:
-                        cell = float(cell) if '.' in cell else int(cell)
-                    except ValueError:
-                        pass
-
-                    line[idc] = cell
-                    continue
-
-                # parse array cell
-                # remove brackets
-                cell = cell[1:-1]
-
-                if ';' not in cell:
-                    # get rid of white spaces
-                    cell = cell.strip()
-                    cell = cell.replace(' ', ',')
-                    cell = cell.replace(' ', '')
-                    # create flat list of integers and floats
-                    numbers = cell.split(',')
-                    cell = [float(n) if '.' in n else int(n) for n in numbers]
-                else:
-                    # create a nested list of integers and floats
-                    # separate multidimensional arrays
-                    cell = cell.split(';')
-                    cell_nd = [None] * len(cell)
-                    for idx, cc in enumerate(cell):
-                        # get rid of white spaces
-                        cc = cc.strip()
-                        cc = cc.replace(' ', ',')
-                        cc = cc.replace(' ', '')
-                        numbers = cc.split(',')
-                        cell_nd[idx] = [float(n) if '.' in n else int(n)
-                                        for n in numbers]
-
-                    cell = cell_nd
-
-                # write parsed cell to line
-                line[idc] = cell
-
-            # first line contains field names
-            if idl == 0:
-                fields = line[1:]
-                continue
-
-            # add blank comment if it does not exist
-            if len(line) == 5:
-                line.append("")
-            # convert empty defaults from None to ""
-            if line[1] is None:
-                line[1] = ""
-
-            # make sure some unusual default values are converted for json
-            if line[1] == "permute([0 0 0 1 0 0; 0 0 0 1 0 0], [3 1 2]);":
-                # Field Data.SOS in SimpleFreeFieldHRSOS and SimpleFreeFieldSOS
-                line[1] = [[[0, 0, 0, 1, 0, 0], [0, 0, 0, 1, 0, 0]]]
-            if line[1] == "{''}":
-                line[1] = ['']
-            # convert versions to strings
-            if "Version" in line[0] and not isinstance(line[1], str):
-                line[1] = str(float(line[1]))
-
-            # write second to last line
-            convention[line[0]] = {}
-            for ff, field in enumerate(fields):
-                convention[line[0]][field.lower()] = line[ff + 1]
-
-        except: # noqa
-            raise ValueError((f"Failed to parse line {idl}, entry {idc} in: "
-                              f"{file}: \n{line}\n"))
-
-    # reorder the fields to be nicer to read and understand
-    # 1. Move everything to the end that is not GLOBAL
-    keys = [key for key in convention.keys()]
-    for key in keys:
-        if "GLOBAL" not in key:
-            convention[key] = convention.pop(key)
-    # 1. Move Data entries to the end
-    for key in keys:
-        if key.startswith("Data"):
-            convention[key] = convention.pop(key)
-
-    return convention
-
-
-def _format_value_for_netcdf(value, key, dtype, dimensions, S):
-    """
-    Format value from SOFA object for saving in a NETCDF4 file.
-
-    Parameters
-    ----------
-    value : str, array like
-        The value to be formatted
-    key : str
-        The name of the current attribute. Needed for verbose errors.
-    dtype : str
-        The the data type of value
-    dimensions : str
-        The intended dimensions from ``sofa.dimensions``
-    S : int
-        Length of the string array.
-
-    Returns
-    -------
-    value : str, numpy array
-        The formatted value.
-    netcdf_dtype : str
-        The data type as a string for writing to a NETCDF4 file ('attribute',
-        'f8', or 'S1').
-    """
-    # copy value
-    try:
-        value = value.copy()
-    except AttributeError:
-        pass
-
-    # parse data
-    if dtype == "attribute":
-        value = str(value)
-        netcdf_dtype = "attribute"
-    elif dtype == "double":
-        value = _atleast_nd(value, len(dimensions))
-        netcdf_dtype = "f8"
-    elif dtype == "string":
-        value = np.array(value, dtype="S" + str(S))
-        value = _atleast_nd(value, len(dimensions))
-        netcdf_dtype = 'S1'
-    else:
-        raise ValueError(f"Unknown type {dtype} for {key}")
-
-    return value, netcdf_dtype
-
-
-def _format_value_from_netcdf(value, key):
-    """
-    Format value from NETCDF4 file for saving in a SOFA object
-
-    Parameters
-    ----------
-    value : np.array of dtype float or S
-        The value to be formatted
-    key : str
-        The variable name of the current value. Needed for verbose errors.
-
-    Returns
-    -------
-    value : str, number, numpy array
-        The formatted value.
-    """
-
-    if "float" in str(value.dtype) or "int" in str(value.dtype):
-        if np.ma.is_masked(value):
-            warnings.warn(f"Entry {key} contains missing data")
-        else:
-            # Convert to numpy array or scalar
-            value = np.asarray(value)
-    elif str(value.dtype)[1] in ["S", "U"]:
-        # string arrays are stored in masked arrays with empty strings '' being
-        # masked. Convert to regular arrays with unmasked empty strings
-        if str(value.dtype)[1] == "S":
-            value = chartostring(value, encoding="ascii")
-        value = np.atleast_1d(value).astype("U")
-    else:
-        raise TypeError(
-            f"{key}: value.dtype is {value.dtype} but must be float, S or, U")
-
-    # convert arrays to scalars if they do not store data that is usually used
-    # as scalar metadata, e.g., the SamplingRate
-    data_keys = ["Data_IR", "Data_Real", "Data_Imag", "Data_SOS" "Data_Delay"]
-    if value.size == 1 and key not in data_keys:
-        value = value[0]
-
-    return value
-
-
-def _add_custom_api_entry(sofa, key, value, flags, dimensions, dtype):
-    """
-    Add custom entry to the sofa._convention and permanently save it in
-    sofa._custom
-
-    Parameters
-    ----------
-    sofa : Sofa
-    key : str
-        name of the entry
-    flags, dimensions, dtype : any
-        as in sofa._convention
-    """
-    # create custom API if it not exists
-    sofa._protected = False
-    if not hasattr(sofa, "_custom"):
-        sofa._custom = {}
-
-    # lower case letters to indicate custom dimensions
-    if dimensions is not None:
-        dimensions = [d.upper() if d.upper() in "ERMNCIS" else d.lower()
-                      for d in dimensions]
-        dimensions = "".join(dimensions)
-
-    # add user entry to custom API
-    sofa._custom[key] = {
-        "flags": flags,
-        "dimensions": dimensions,
-        "type": dtype,
-        "default": None,
-        "comment": ""}
-    sofa._update_convention(version="match")
-
-    # add attribute to object
-    setattr(sofa, key, value)
-    sofa._protected = True
-
-
-def _verify_convention_and_version(version, version_in, convention):
-    """
-    Verify if convention and version exist and return version
-
-    Parameters
-    ----------
-    version : str
-        'latest', 'match', version string (e.g., '1.0')
-    version_in : str
-        The version to be checked against
-    convention : str
-        The name of the convention to be checked
-
-    Returns
-    -------
-    version_out : str
-        The version to be used depending on `version`, and `version_in`
-    """
-
-    # check if the convention exists in sofar
-    if convention not in _get_conventions("name"):
-        raise ValueError(
-            f"Convention '{convention}' does not exist")
-
-    name_version = _get_conventions("name_version")
-
-    if version == "latest":
-        # get list of versions as floats
-        version_out = [float(versions[1]) for versions in name_version
-                       if versions[0] == convention]
-        # get latest version as string
-        version_out = str(version_out[np.argmax(version_out)])
-
-        if version_parse(version_out) > version_parse(version_in):
-            print(("Updated conventions version from "
-                   f"{version_in} to {version_out}"))
-    else:
-        # check which version is wanted
-        if version == "match":
-            match = version_in
-        else:
-            match = version
-
-        version_out = None
-        for versions in name_version:
-            # check if convention and version match
-            if versions[0] == convention \
-                    and str(float(versions[1])) == match:
-                version_out = str(float(versions[1]))
-
-        if version_out is None:
-            raise ValueError((
-                f"Version {match} does not exist for convention {convention}. "
-                "Try to access the data with version='latest'"))
-
-    return version_out
-
-
-def _atleast_nd(array, ndim):
-    """
-    Get numpy array with specified number of dimensions. Dimensions are
-    appended at the end if ndim > 3.
-    """
-    try:
-        array = array.copy()
-    except AttributeError:
-        array = array
-
-    if ndim == 1:
-        array = np.atleast_1d(array)
-    if ndim == 2:
-        array = np.atleast_2d(array)
-    if ndim >= 3:
-        array = np.atleast_3d(array)
-    for _ in range(ndim - array.ndim):
-        array = array[..., np.newaxis]
-    return array
-
-
-def _nd_newaxis(array, ndim):
-    """Append dimensions to the end of an array until array.ndim == ndim"""
-    array = np.array(array)
-
-    for _ in range(ndim - array.ndim):
-        array = array[..., np.newaxis]
-    return array
-
-
-def _sofa_restrictions():
-    """
-    Return dictionaries to check restrictions on the data posed by SOFA.
-
-    The check is done in SOFA.verify(). This is not a private class method,
-    to save additional indention that would make the code harder to read and
-    write.
-
-    Returns:
-    data : dict
-        General restrictions on the data of any SOFA convention
-    data_type : dict
-        Restriction depending on GLOBAL_DataType
-    api : dict
-        Restrictions on the API depending on specific fields of a SOFA file
-    """
-
-    # definition of valid coordinate systems and units
-    coords_min = ["cartesian", "spherical"]
-    coords_full = coords_min + ["spherical harmonics"]
-    units_min = ["metre", "degree, degree, metre"]
-    units_full = units_min + [units_min[1]]
-    unit_aliases = {
-        "metres": "metre",
-        "meter": "metre",
-        "meters": "metre",
-        "cubic metres": "cubic metre",
-        "cubic meter": "cubic metre",
-        "cubic meters": "cubic metre",
-        "degrees": "degree",
-        "seconds": "second"
-    }
-    # possible values for restricted dimensions in the API
-    sh_dimension = ([(N+1)**2 for N in range(200)],
-                    "(N+1)**2 where N is the spherical harmonics order")
-    sos_dimension = ([6 * (N + 1) for N in range(1000)],
-                     "an integer multiple of 6 greater 0")
-
-    # restrictions on the data
-    # - if `value` is None it in only checked if the SOFA object has the attr
-    # - if `value` is a list, it is also checked if the actual value is in
-    #   `value`
-    # - if there is a list of values for a dependency the value of the SOFA
-    #   object has to match the value of the list at a certain index. The index
-    #   is determined by the value of the parent.
-    data = {
-        # Global --------------------------------------------------------------
-        # GLOBAL_SOFAConventions?
-        # Check value of GLOBAL_DataType
-        # (FIRE and TFE are legacy data types from SOFA version 1.0)
-        "GLOBAL_DataType": {
-            "value": ["FIR", "FIR-E", "FIRE", "TF", "TF-E", "TFE", "SOS"]},
-        "GLOBAL_RoomType": {
-            "value": ["free field", "reverberant", "shoebox", "dae"]},
-        "GLOBAL_SOFAConventions": {
-            "value": _get_conventions(return_type="name")},
-        # check NLongName
-        "N_LongName": {
-            "value": ["frequency"]},
-        # Listener ------------------------------------------------------------
-        # Check values and consistency of if ListenerPosition Type and Unit
-        "ListenerPosition_Type": {
-            "value": coords_min,
-            "dependency": {
-                "ListenerPosition_Units": units_min}},
-        # Check if dependencies of ListenerView are contained
-        "ListenerView": {
-            "value": None,
-            "dependency": {
-                "ListenerView_Type": None,
-                "ListenerView_Units": None}},
-        # Check values and consistency of if ListenerView Type and Unit
-        "ListenerView_Type": {
-            "value": coords_min,
-            "dependency": {
-                "ListenerView_Units": units_min}},
-        # Check if dependencies of ListenerUp are contained
-        "ListenerUp": {
-            "value": None,
-            "dependency": {
-                "ListenerView": None}},
-        # Receiver ------------------------------------------------------------
-        # Check values and consistency of if ReceiverPosition Type and Unit
-        "ReceiverPosition_Type": {
-            "value": coords_full,
-            "dependency": {
-                "ReceiverPosition_Units": units_full}},
-        # Check if dependencies of ReceiverView are contained
-        "ReceiverView": {
-            "value": None,
-            "dependency": {
-                "ReceiverView_Type": None,
-                "ReceiverView_Units": None}},
-        # Check values and consistency of if ReceiverView Type and Unit
-        "ReceiverView_Type": {
-            "value": coords_min,
-            "dependency": {
-                "ReceiverView_Units": units_min}},
-        # Check if dependencies of ReceiverUp are contained
-        "ReceiverUp": {
-            "value": None,
-            "dependency": {
-                "ReceiverView": None}},
-        # Source --------------------------------------------------------------
-        # Check values and consistency of if SourcePosition Type and Unit
-        "SourcePosition_Type": {
-            "value": coords_min,
-            "dependency": {
-                "SourcePosition_Units": units_min}},
-        # Check if dependencies of SourceView are contained
-        "SourceView": {
-            "value": None,
-            "dependency": {
-                "SourceView_Type": None,
-                "SourceView_Units": None}},
-        # Check values and consistency of if SourceView Type and Unit
-        "SourceView_Type": {
-            "value": coords_min,
-            "dependency": {
-                "SourceView_Units": units_min}},
-        # Check if dependencies of SourceUp are contained
-        "SourceUp": {
-            "value": None,
-            "dependency": {
-                "SourceView": None}},
-        # Emitter -------------------------------------------------------------
-        # Check values and consistency of if EmitterPosition Type and Unit
-        "EmitterPosition_Type": {
-            "value": coords_full,
-            "dependency": {
-                "EmitterPosition_Units": units_full}},
-        # Check if dependencies of EmitterView are contained
-        "EmitterView": {
-            "value": None,
-            "dependency": {
-                "EmitterView_Type": None,
-                "EmitterView_Units": None}},
-        # Check values and consistency of if EmitterView Type and Unit
-        "EmitterView_Type": {
-            "value": coords_min,
-            "dependency": {
-                "EmitterView_Units": units_min}},
-        # Check if dependencies of EmitterUp are contained
-        "EmitterUp": {
-            "value": None,
-            "dependency": {
-                "EmitterView": None}},
-        # Room ----------------------------------------------------------------
-        "RoomVolume": {
-            "value": None,
-            "dependency": {
-                "RoomVolume_Units": None}},
-        "RoomTemperature": {
-            "value": None,
-            "dependency": {
-                "RoomTemperature_Units": None}},
-        "RoomVolume_Units": {
-            "value": ["cubic metre"]},
-        "RoomTemperature_Units": {
-            "value": ["Kelvin"]}
-    }
-
-    # restrictions arising from GLOBAL_DataType
-    # - if `value` is None it is only checked if the SOFA object has the attr
-    # - if `value` is a list, it is also checked if the actual value is in
-    #   `value`
-    data_type = {
-        "FIR": {
-            "Data_IR": None,
-            "Data_Delay": None,
-            "Data_SamplingRate": None,
-            "Data_SamplingRate_Units": (["hertz"], "hertz")},
-        "TF": {
-            "Data_Real": None,
-            "Data_Imag": None,
-            "N": None,
-            # "N_LongName": (["frequency"], "frequency"),  # optional parameter
-            "N_Units": (["hertz"], "hertz")},
-        "SOS": {
-            "Data_SOS": None,
-            "Data_Delay": None,
-            "Data_SamplingRate": None,
-            "Data_SamplingRate_Units": (["hertz"], "hertz")}
-    }
-
-    # restrictions on the API
-    api = {
-        # Check dimension R if using spherical harmonics for the Receiver
-        # (assuming SH orders < 200)
-        "ReceiverPosition_Type": {
-            "value": "spherical harmonics",
-            "API": ("R", ) + sh_dimension},
-        # Check dimension E if using spherical harmonics for the Emitter
-        # (assuming SH orders < 200)
-        "EmitterPosition_Type": {
-            "value": "spherical harmonics",
-            "API": ("E", ) + sh_dimension},
-        # Checking the dimension of N if having SOS data
-        # (assuming up to 1000 second order sections)
-        "GLOBAL_DataType": {
-            "value": "SOS",
-            "API": ("N", ) + sos_dimension}
-    }
-
-    # restrictions from the convention. Values of fields will be checked.
-    # Must contain testing the API. If this would be tested under api={}, the
-    # entry GLOBAL_SOFAConventions would be repeated.
-    convention = {
-        "GeneralFIR": {
-            "GLOBAL_DataType": ["FIR"]},
-        "GeneralFIR-E": {
-            "GLOBAL_DataType": ["FIR-E"]},
-        "GeneralFIRE": {  # SOFA version 1.0 legacy
-            "GLOBAL_DataType": ["FIRE"]},
-        "GeneralTF": {
-            "GLOBAL_DataType": ["TF"]},
-        "GeneralTF-E": {
-            "GLOBAL_DataType": ["TF-E"]},
-        "SimpleFreeFieldHRIR": {
-            "GLOBAL_DataType": ["FIR"],
-            "GLOBAL_RoomType": ["free field"],
-            "EmitterPosition_Type": coords_min,
-            "API": {"E": 1}},
-        "SimpleFreeFieldHRTF": {
-            "GLOBAL_DataType": ["TF"],
-            "GLOBAL_RoomType": ["free field"],
-            "EmitterPosition_Type": coords_min,
-            "API": {"E": 1}},
-        "SimpleFreeFieldHRSOS": {
-            "GLOBAL_DataType": ["SOS"],
-            "GLOBAL_RoomType": ["free field"],
-            "EmitterPosition_Type": coords_min,
-            "API": {"E": 1}},
-        "FreeFieldHRIR": {
-            "GLOBAL_DataType": ["FIR-E"],
-            "GLOBAL_RoomType": ["free field"]},
-        "FreeFieldHRTF": {
-            "GLOBAL_DataType": ["TF-E"],
-            "GLOBAL_RoomType": ["free field"]},
-        "SimpleHeadphoneIR": {
-            "GLOBAL_DataType": ["FIR"]},
-        "SingleRoomSRIR": {
-            "GLOBAL_DataType": ["FIR"]},
-        "SingleRoomMIMOSRIR": {
-            "GLOBAL_DataType": ["FIR-E"]},
-        "FreeFieldDirectivityTF": {
-            "GLOBAL_DataType": ["TF"]}
-    }
-
-    return data, data_type, api, convention, unit_aliases
diff --git a/sofar/update_conventions.py b/sofar/update_conventions.py
new file mode 100644
index 00000000..cf22c59c
--- /dev/null
+++ b/sofar/update_conventions.py
@@ -0,0 +1,414 @@
+import contextlib
+import os
+import glob
+import json
+import requests
+from bs4 import BeautifulSoup
+
+
+def update_conventions(conventions_path=None, assume_yes=False):
+    """
+    Update SOFA conventions.
+
+    SOFA convention define what data is stored in a SOFA file and how it is
+    stored. Updating makes sure that sofar is using the latest conventions.
+    This is done in three steps
+
+    1.
+        Download official SOFA conventions as csv files from
+        https://www.sofaconventions.org/conventions/ and
+        https://www.sofaconventions.org/conventions/deprecated/.
+    2.
+        Convert csv files to json files to be read by sofar.
+    3.
+        Notify which conventions were newly added or updated.
+
+    The csv and json files are stored at sofar/conventions. Sofar works only on
+    the json files. To get a list of all currently available SOFA conventions
+    and their paths see :py:func:`~sofar.list_conventions`.
+
+    .. note::
+        If the official convention contain errors, calling this function might
+        break sofar. If this is the case sofar must be re-installed, e.g., by
+        running ``pip install --force-reinstall sofar``. Be sure that you want
+        to do this.
+
+    Parameters
+    ----------
+    conventions_path : str, optional
+        Path to the folder where the conventions are saved. The default is
+        ``None``, which saves the conventions inside the sofar package.
+        Conventions saved under a different path can not be used by sofar. This
+        parameter was added mostly for testing and debugging.
+    response : bool, optional
+
+        ``True``
+            Updating the conventions must be confirmed by typing "y".
+        ``False``
+            The conventions are updated without confirmation.
+
+        The default is ``True``
+    """
+
+    if not assume_yes:
+        # these lines were only tested manually. I was too lazy to write a test
+        # coping with keyboard input
+        print(("Are you sure that you want to update the conventions? "
+               "Read the documentation before continuing. "
+               "If updateing breaks sofar it has to be re-installed"
+               "(y/n)"))
+        response = input()
+        if response != "y":
+            print("Updating the conventions was canceled.")
+            return
+
+    # url for parsing and downloading the convention files
+    urls = ("https://www.sofaconventions.org/conventions/",
+            "https://www.sofaconventions.org/conventions/deprecated/")
+    ext = 'csv'
+
+    print(f"Reading SOFA conventions from {urls[0]} ...")
+
+    # get file names of conventions from sofaconventions.org
+    page = requests.get(urls[0]).text
+    soup = BeautifulSoup(page, 'html.parser')
+    standardized = [os.path.split(node.get('href'))[1]
+                    for node in soup.find_all('a')
+                    if node.get('href').endswith(ext)]
+    page = requests.get(urls[1]).text
+    soup = BeautifulSoup(page, 'html.parser')
+    deprecated = [os.path.split(node.get('href'))[1]
+                  for node in soup.find_all('a')
+                  if node.get('href').endswith(ext)]
+
+    conventions = standardized + deprecated
+
+    # directory handling
+    if conventions_path is None:
+        conventions_path = os.path.join(
+            os.path.dirname(__file__), "sofa_conventions", "conventions")
+    if not os.path.isdir(conventions_path):
+        os.mkdir(conventions_path)
+    if not os.path.isdir(os.path.join(conventions_path, "deprecated")):
+        os.mkdir(os.path.join(conventions_path, "deprecated"))
+
+    # Loop and download conventions if they changed
+    updated = False
+    for convention in conventions:
+
+        # exclude these conventions
+        if convention.startswith(("General_", "GeneralString_")):
+            continue
+
+        # get filename and url
+        is_standardized = convention in standardized
+        standardized_csv = os.path.join(conventions_path, convention)
+        deprecated_csv = os.path.join(
+                conventions_path, "deprecated", convention)
+        url = (
+            f"{urls[0]}/{convention}"
+            if is_standardized
+            else f"{urls[1]}/{convention}"
+        )
+
+        # download SOFA convention definitions to package directory
+        data = requests.get(url)
+        # remove windows style line breaks and trailing tabs
+        data = data.content.replace(b"\r\n", b"\n").replace(b"\t\n", b"\n")
+
+        # check if convention needs to be added or updated
+        if is_standardized and not os.path.isfile(standardized_csv):
+            # add new standardized convention
+            updated = True
+            with open(standardized_csv, "wb") as file:
+                file.write(data)
+            print(f"- added convention: {convention[:-4]}")
+        if is_standardized and os.path.isfile(standardized_csv):
+            # check for update of a standardized convention
+            with open(standardized_csv, "rb") as file:
+                data_current = b"".join(file.readlines())
+                data_current = data_current.replace(
+                    b"\r\n", b"\n").replace(b"\t\n", b"\n")
+            if data_current != data:
+                updated = True
+                with open(standardized_csv, "wb") as file:
+                    file.write(data)
+                print(f"- updated convention: {convention[:-4]}")
+        elif not is_standardized and os.path.isfile(standardized_csv):
+            # deprecate standardized convention
+            updated = True
+            with open(deprecated_csv, "wb") as file:
+                file.write(data)
+            os.remove(standardized_csv)
+            os.remove(f"{standardized_csv[:-3]}json")
+            print(f"- deprecated convention: {convention[:-4]}")
+        elif not is_standardized and os.path.isfile(deprecated_csv):
+            # check for update of a deprecated convention
+            with open(deprecated_csv, "rb") as file:
+                data_current = b"".join(file.readlines())
+                data_current = data_current.replace(
+                    b"\r\n", b"\n").replace(b"\t\n", b"\n")
+            if data_current != data:
+                updated = True
+                with open(deprecated_csv, "wb") as file:
+                    file.write(data)
+                print(f"- updated deprecated convention: {convention[:-4]}")
+        elif not is_standardized and not os.path.isfile(deprecated_csv):
+            # add new deprecation
+            updated = True
+            with open(deprecated_csv, "wb") as file:
+                file.write(data)
+            print(f"- added deprecated convention: {convention[:-4]}")
+
+    if updated:
+        # compile json files from csv file
+        _compile_conventions(conventions_path)
+        print("... done.")
+    else:
+        print("... conventions already up to date.")
+
+
+def _compile_conventions(conventions_path=None):
+    """
+    Compile SOFA conventions (json files) from source conventions (csv files
+    from SOFA SOFAtoolbox), i.e., only do step 2 from `update_conventions`.
+    This is a helper function for debugging and developing and might break
+    sofar.
+
+    Parameters
+    ----------
+    conventions_path : str
+        Path to the `conventions`folder containing csv and json files. The
+        default ``None`` uses the default location inside the sofar package.
+    """
+    # directory handling
+    if conventions_path is None:
+        conventions_path = os.path.join(
+            os.path.dirname(__file__), "sofa_conventions", "conventions")
+    if not os.path.isdir(conventions_path):
+        raise ValueError(f"{conventions_path} does not exist")
+
+    # get list of source conventions
+    csv_files = glob.glob(os.path.join(conventions_path, "*.csv")) + \
+        glob.glob(os.path.join(conventions_path, "deprecated", "*.csv"))
+
+    for csv_file in csv_files:
+
+        # convert SOFA conventions from csv to json
+        convention_dict = _convention_csv2dict(csv_file)
+        with open(f"{csv_file[:-3]}json", 'w') as file:
+            json.dump(convention_dict, file, indent=4)
+
+
+def _convention_csv2dict(file: str):
+    """
+    Read a SOFA convention as csv file from the official Matlab/Octave API for
+    SOFA (SOFAtoolbox) and convert them to a Python dictionary. The dictionary
+    can be written for example to a json file using
+
+    import json
+
+    with open(filename, 'w') as file:
+        json.dump(dictionary, file, indent=4)
+
+    Parameters
+    ----------
+    file : str
+        filename of the SOFA convention
+
+    Returns
+    -------
+    convention : dict
+        SOFA convention as nested dictionary. Each attribute is a sub
+        dictionary with the keys `default`, `flags`, `dimensions`, `type`, and
+        `comment`.
+    """
+
+    # read the file
+    # (encoding should be changed to utf-8 after the SOFA conventions repo is
+    # clean.)
+    # TODO: add explicit test for this function that checks the output
+    with open(file, 'r', encoding="windows-1252") as fid:
+        lines = fid.readlines()
+
+    # write into dict
+    convention = {}
+    for idl, line in enumerate(lines):
+
+        try:
+            # separate by tabs
+            line = line.strip().split("\t")
+            # parse the line entry by entry
+            for idc, cell in enumerate(line):
+                # detect empty cells and leading trailing white spaces
+                cell = None if cell.replace(' ', '') == '' else cell.strip()
+                # nothing to do for empty cells
+                if cell is None:
+                    line[idc] = cell
+                    continue
+                # parse text cells that do not contain arrays
+                if cell[0] != '[':
+                    # check for numbers
+                    with contextlib.suppress(ValueError):
+                        cell = float(cell) if '.' in cell else int(cell)
+                    line[idc] = cell
+                    continue
+
+                # parse array cell
+                # remove brackets
+                cell = cell[1:-1]
+
+                if ';' not in cell:
+                    # get rid of white spaces
+                    cell = cell.strip()
+                    cell = cell.replace(' ', ',')
+                    cell = cell.replace(' ', '')
+                    # create flat list of integers and floats
+                    numbers = cell.split(',')
+                    cell = [float(n) if '.' in n else int(n) for n in numbers]
+                else:
+                    # create a nested list of integers and floats
+                    # separate multidimensional arrays
+                    cell = cell.split(';')
+                    cell_nd = [None] * len(cell)
+                    for idx, cc in enumerate(cell):
+                        # get rid of white spaces
+                        cc = cc.strip()
+                        cc = cc.replace(' ', ',')
+                        cc = cc.replace(' ', '')
+                        numbers = cc.split(',')
+                        cell_nd[idx] = [float(n) if '.' in n else int(n)
+                                        for n in numbers]
+
+                    cell = cell_nd
+
+                # write parsed cell to line
+                line[idc] = cell
+
+            # first line contains field names
+            if idl == 0:
+                fields = line[1:]
+                continue
+
+            # add blank comment if it does not exist
+            if len(line) == 5:
+                line.append("")
+            # convert empty defaults from None to ""
+            if line[1] is None:
+                line[1] = ""
+
+            # make sure some unusual default values are converted for json
+            if line[1] == "permute([0 0 0 1 0 0; 0 0 0 1 0 0], [3 1 2]);":
+                # Field Data.SOS in SimpleFreeFieldHRSOS and SimpleFreeFieldSOS
+                line[1] = [[[0, 0, 0, 1, 0, 0], [0, 0, 0, 1, 0, 0]]]
+            if line[1] == "permute([0 0 0 1 0 0], [3 1 2]);":
+                # Field Data.SOS in GeneralSOS
+                line[1] = [[[0, 0, 0, 1, 0, 0]]]
+            if line[1] == "{''}":
+                line[1] = ['']
+            # convert versions to strings
+            if "Version" in line[0] and not isinstance(line[1], str):
+                line[1] = str(float(line[1]))
+
+            # write second to last line
+            convention[line[0]] = {}
+            for ff, field in enumerate(fields):
+                convention[line[0]][field.lower()] = line[ff + 1]
+
+        except: # noqa
+            raise ValueError((f"Failed to parse line {idl}, entry {idc} in: "
+                              f"{file}: \n{line}\n"))
+
+    # reorder the fields to be nicer to read and understand
+    # 1. Move everything to the end that is not GLOBAL
+    keys = list(convention.keys())
+    for key in keys:
+        if "GLOBAL" not in key:
+            convention[key] = convention.pop(key)
+    # 1. Move Data entries to the end
+    for key in keys:
+        if key.startswith("Data"):
+            convention[key] = convention.pop(key)
+
+    return convention
+
+
+def _check_congruency(save_dir=None):
+    """
+    SOFA conventions are stored in two different places - is this a good idea?
+    They should be identical, but let's find out.
+
+    Prints warnings about incongruent conventions
+
+    Parameters
+    ----------
+    save : str
+        directory to save diverging conventions for further inspections
+    """
+
+    urls = ["https://www.sofaconventions.org/conventions/",
+            ("https://raw.githubusercontent.com/sofacoustics/SOFAtoolbox/"
+             "master/SOFAtoolbox/conventions/")]
+    subdirs = ["sofaconventions", "sofatoolbox"]
+
+    # check save_dir
+    if save_dir is not None:
+        if not os.path.isdir(save_dir):
+            raise ValueError(f"{save_dir} does not exist")
+        for subdir in subdirs:
+            if not os.path.isdir(os.path.join(save_dir, subdir)):
+                os.makedirs(os.path.join(save_dir, subdir))
+
+    # get file names of conventions from sofaconventions.org and SOFAtoolbox
+    url = urls[0]
+    page = requests.get(url).text
+    soup = BeautifulSoup(page, 'html.parser')
+    sofaconventions = [os.path.split(node.get('href'))[1]
+                       for node in soup.find_all('a')
+                       if node.get('href').endswith(".csv")]
+
+    url = ("https://github.com/sofacoustics/SOFAtoolbox/tree/"
+           "master/SOFAtoolbox/conventions/")
+    page = requests.get(url).text
+    soup = BeautifulSoup(page, 'html.parser')
+    sofatoolbox = [os.path.split(node.get('href'))[1]
+                   for node in soup.find_all('a')
+                   if node.get('href').endswith(".csv")]
+
+    # check if lists are identical. Remove items not contained in both lists
+    report = ""
+    for convention in sofaconventions:
+        if convention.startswith(("General_", "GeneralString_")):
+            sofaconventions.remove(convention)
+        elif convention not in sofatoolbox:
+            sofaconventions.remove(convention)
+            report += (f"- {convention} is missing in SOFAtoolbox\n")
+    for convention in sofatoolbox:
+        if convention.startswith(("General_", "GeneralString_")):
+            sofatoolbox.remove(convention)
+        elif convention not in sofaconventions:
+            sofatoolbox.remove(convention)
+            report += (f"- {convention} is missing on sofaconventions.org\n")
+
+    # Loop and download conventions to check if they are identical
+    for convention in sofaconventions:
+
+        # download SOFA convention definitions to package directory
+        data = [requests.get(url + convention) for url in urls]
+        # remove trailing tabs and windows style line breaks
+        data = [d.content.replace(b"\r\n", b"\n").replace(b"\t\n", b"\n")
+                for d in data]
+
+        # check for equality
+        if data[0] != data[1]:
+            report += f"- {convention} differs across platforms\n"
+
+            # save diverging files
+            if save_dir is not None:
+                for subdir, d in zip(subdirs, data):
+                    filename = os.path.join(save_dir, subdir, convention)
+                    with open(filename, "wb") as file:
+                        file.write(d)
+
+    if report:
+        print("Diverging conventions across platforms:\n" + report)
diff --git a/sofar/utils.py b/sofar/utils.py
new file mode 100644
index 00000000..2375c60b
--- /dev/null
+++ b/sofar/utils.py
@@ -0,0 +1,335 @@
+import os
+import glob
+import numpy as np
+import numpy.testing as npt
+from packaging.version import parse as version_parse
+import warnings
+import sofar as sf
+
+
+def version():
+    """Return version of sofar and SOFA conventions"""
+
+    sofa_conventions = os.path.join(
+        os.path.dirname(__file__), "sofa_conventions", 'VERSION')
+    with open(sofa_conventions) as file:
+        sofa_conventions = file.readline().strip()
+
+    return (f"sofar v{sf.__version__} implementing "
+            f"SOFA standard {sofa_conventions}")
+
+
+def _verify_convention_and_version(version, version_in, convention):
+    """
+    Verify if convention and version exist and return version
+
+    Parameters
+    ----------
+    version : str
+        'latest', 'match', version string (e.g., '1.0')
+    version_in : str
+        The version to be checked against
+    convention : str
+        The name of the convention to be checked
+
+    Returns
+    -------
+    version_out : str
+        The version to be used depending on `version`, and `version_in`
+    """
+
+    # check if the convention exists in sofar
+    if convention not in _get_conventions("name"):
+        raise ValueError(
+            f"Convention '{convention}' does not exist")
+
+    name_version = _get_conventions("name_version")
+
+    if version == "latest":
+        # get list of versions as floats
+        version_out = [float(versions[1]) for versions in name_version
+                       if versions[0] == convention]
+        # get latest version as string
+        version_out = str(version_out[np.argmax(version_out)])
+
+        if version_parse(version_out) > version_parse(version_in):
+            print(("Updated conventions version from "
+                   f"{version_in} to {version_out}"))
+    else:
+        # check which version is wanted
+        match = version_in if version == "match" else version
+        version_out = None
+        for versions in name_version:
+            # check if convention and version match
+            if versions[0] == convention \
+                    and str(float(versions[1])) == match:
+                version_out = str(float(versions[1]))
+
+        if version_out is None:
+            raise ValueError((
+                f"Version {match} does not exist for convention {convention}. "
+                "Try to access the data with version='latest'"))
+
+    return version_out
+
+
+def list_conventions():
+    """
+    List available SOFA conventions by printing to the console.
+    """
+    print(_get_conventions("string"))
+
+
+def _get_conventions(return_type, conventions_path=None):
+    """
+    Get available SOFA conventions.
+
+    Parameters
+    ----------
+    return_type : string, optional
+        ``'path'``
+            Return a list with the full paths and filenames of the convention
+            files (json files)
+        ``'path_source'``
+            Return a list with the full paths and filenames of the source
+            convention files from API_MO (csv files)
+        ``'name'``
+            Return a list of the convention names without version
+        ``'name_version'``
+            Return a list of tuples containing the convention name and version.
+        ``'string'``
+            Returns a string that lists the names and versions of all
+            conventions.
+    conventions_path : str, optional
+        The path to the the `conventions` folder containing the csv and json
+        files.
+
+    Returns
+    -------
+    See parameter `return_type`.
+    """
+    # directory containing the SOFA conventions
+    if conventions_path is None:
+        conventions_path = os.path.join(
+            os.path.dirname(__file__), "sofa_conventions", 'conventions')
+
+    reg_str = "*.csv" if return_type == "path_source" else "*.json"
+
+    # SOFA convention files
+    standardized = list(glob.glob(os.path.join(conventions_path, reg_str)))
+    deprecated = list(
+        glob.glob(os.path.join(conventions_path, "deprecated", reg_str)))
+    paths = standardized + deprecated
+
+    conventions_str = "Available SOFA conventions:\n"
+
+    conventions = []
+    versions = []
+    for path in paths:
+        fileparts = os.path.basename(path).split(sep="_")
+        conventions += [fileparts[0]]
+        versions += [fileparts[1][:-5]]
+        conventions_str += f"{conventions[-1]} (Version {versions[-1]})\n"
+
+    if return_type is None:
+        return
+    elif return_type.startswith("path"):
+        return paths
+    elif return_type == "name":
+        return conventions
+    elif return_type == "name_version":
+        return list(zip(conventions, versions))
+    elif return_type == "string":
+        return conventions_str
+    else:
+        raise ValueError(f"return_type {return_type} is invalid")
+
+
+def equals(sofa_a, sofa_b, verbose=True, exclude=None):
+    """
+    Compare two SOFA objects against each other.
+
+    Parameters
+    ----------
+    sofa_a : Sofa
+        SOFA object
+    sofa_b : Sofa
+        SOFA object
+    verbose : bool, optional
+        Print differences to the console. The default is True.
+    exclude : str, optional
+        Specify what fields should be excluded from the comparison
+
+        ``'GLOBAL'``
+            Exclude all global attributes, i.e., fields starting with 'GLOBAL:'
+        ``'DATE'``
+            Exclude date attributs, i.e., fields that contain 'Date'
+        ``'ATTR'``
+            Exclude all attributes, i.e., fields that contain ':'
+
+        The default is None, which does not exclude anything.
+
+    Returns
+    -------
+    is_identical : bool
+        ``True`` if sofa_a and sofa_b are identical, ``False`` otherwise.
+    """
+
+    is_identical = True
+
+    # get and filter keys
+    # ('_*' are SOFA object private variables, '__' are netCDF attributes)
+    keys_a = [k for k in sofa_a.__dict__.keys() if not k.startswith("_")]
+    keys_b = [k for k in sofa_b.__dict__.keys() if not k.startswith("_")]
+
+    if exclude is not None:
+        if exclude.upper() == "GLOBAL":
+            keys_a = [k for k in keys_a if not k.startswith("GLOBAL_")]
+            keys_b = [k for k in keys_b if not k.startswith("GLOBAL_")]
+        elif exclude.upper() == "ATTR":
+            keys_a = [k for k in keys_a if
+                      sofa_a._convention[k]["type"] != "attribute"]
+            keys_b = [k for k in keys_b if
+                      sofa_b._convention[k]["type"] != "attribute"]
+        elif exclude.upper() == "DATE":
+            keys_a = [k for k in keys_a if "Date" not in k]
+            keys_b = [k for k in keys_b if "Date" not in k]
+        else:
+            raise ValueError(
+                f"exclude is {exclude} but must be GLOBAL, DATE, or ATTR")
+
+    # check for equal length
+    if len(keys_a) != len(keys_b):
+        return _equals_raise_warning((
+            f"not identical: sofa_a has {len(keys_a)} attributes for "
+            f"comparison and sofa_b has {len(keys_b)}."), verbose)
+
+    # check if the keys match
+    if set(keys_a) != set(keys_b):
+        return _equals_raise_warning(
+            "not identical: sofa_a and sofa_b do not have the ame attributes",
+            verbose)
+
+    # compare the data inside the SOFA object
+    for key in keys_a:
+
+        # get data and types
+        a = getattr(sofa_a, key)
+        b = getattr(sofa_b, key)
+        type_a = sofa_a._convention[key]["type"]
+        type_b = sofa_b._convention[key]["type"]
+
+        # compare attributes
+        if type_a == "attribute" and type_b == "attribute":
+
+            # compare
+            if a != b:
+                is_identical = _equals_raise_warning(
+                    f"not identical: different values for {key}", verbose)
+
+        # compare double variables
+        elif type_a == "double" and type_b == "double":
+
+            try:
+                npt.assert_allclose(np.squeeze(a), np.squeeze(b))
+            except AssertionError:
+                is_identical = _equals_raise_warning(
+                    "not identical: different values for {key}", verbose)
+
+        # compare string variables
+        elif type_a == "string" and type_b == "string":
+            try:
+                assert np.all(
+                    np.squeeze(a).astype("S") == np.squeeze(b).astype("S"))
+            except AssertionError:
+                is_identical = _equals_raise_warning(
+                    "not identical: different values for {key}", verbose)
+        else:
+            is_identical = _equals_raise_warning(
+                (f"not identical: {key} has different data types "
+                 f"({type_a}, {type_b})"), verbose)
+
+    return is_identical
+
+
+def _equals_raise_warning(message, verbose):
+    if verbose:
+        warnings.warn(message)
+    return False
+
+
+def _atleast_nd(array, ndim):
+    """
+    Get numpy array with specified number of dimensions. Dimensions are
+    appended at the end if ndim > 3.
+    """
+    try:
+        array = array.copy()
+    except AttributeError:
+        array = array
+
+    if ndim == 1:
+        array = np.atleast_1d(array)
+    if ndim == 2:
+        array = np.atleast_2d(array)
+    if ndim >= 3:
+        array = np.atleast_3d(array)
+    for _ in range(ndim - array.ndim):
+        array = array[..., np.newaxis]
+    return array
+
+
+def _nd_newaxis(array, ndim):
+    """Append dimensions to the end of an array until array.ndim == ndim"""
+    array = np.array(array)
+
+    for _ in range(ndim - array.ndim):
+        array = array[..., np.newaxis]
+    return array
+
+
+def _complete_sofa(convention="GeneralTF"):
+    """
+    Generate SOFA file with all required data for testing verification rules.
+    """
+
+    sofa = sf.Sofa(convention)
+    # Listener meta data
+    sofa.add_variable("ListenerView", [1, 0, 0], "double", "IC")
+    sofa.add_attribute("ListenerView_Type", "cartesian")
+    sofa.add_attribute("ListenerView_Units", "metre")
+    sofa.add_variable("ListenerUp", [0, 0, 1], "double", "IC")
+    # Receiver meta data
+    sofa.add_variable("ReceiverView", [1, 0, 0], "double", "IC")
+    sofa.add_attribute("ReceiverView_Type", "cartesian")
+    sofa.add_attribute("ReceiverView_Units", "metre")
+    sofa.add_variable("ReceiverUp", [0, 0, 1], "double", "IC")
+    # Source meta data
+    sofa.add_variable("SourceView", [1, 0, 0], "double", "IC")
+    sofa.add_attribute("SourceView_Type", "cartesian")
+    sofa.add_attribute("SourceView_Units", "metre")
+    sofa.add_variable("SourceUp", [0, 0, 1], "double", "IC")
+    # Emitter meta data
+    sofa.add_variable("EmitterView", [1, 0, 0], "double", "IC")
+    sofa.add_attribute("EmitterView_Type", "cartesian")
+    sofa.add_attribute("EmitterView_Units", "metre")
+    sofa.add_variable("EmitterUp", [0, 0, 1], "double", "IC")
+    sofa.add_attribute("GLOBAL_EmitterDescription", "what an emitter")
+    sofa.add_variable("EmitterDescriptions", ["emitter array"], "string", "MS")
+    # Room meta data
+    sofa.add_attribute("GLOBAL_RoomShortName", "Hall")
+    sofa.add_attribute("GLOBAL_RoomDescription", "Wooden floor")
+    sofa.add_attribute("GLOBAL_RoomLocation", "some where nice")
+    sofa.add_variable("RoomTemperature", 0, "double", "I")
+    sofa.add_attribute("RoomTemperature_Units", "kelvin")
+    sofa.add_attribute("GLOBAL_RoomGeometry", "some/file")
+    sofa.add_variable("RoomVolume", 200, "double", "I")
+    sofa.add_attribute("RoomVolume_Units", "cubic metre")
+    sofa.add_variable("RoomCornerA", [0, 0, 0], "double", "IC")
+    sofa.add_variable("RoomCornerB", [1, 1, 1], "double", "IC")
+    sofa.add_variable("RoomCorners", 0, "double", "I")
+    sofa.add_attribute("RoomCorners_Type", "cartesian")
+    sofa.add_attribute("RoomCorners_Units", "metre")
+
+    sofa.verify()
+    return sofa


Test patch:
diff --git a/pytest.ini b/pytest.ini
index 2d33f226..94aaf7d4 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,4 +1,5 @@
 # pytest.ini
 [pytest]
-filterwarnings =
-    ignore::UserWarning
\ No newline at end of file
+norecursedirs = private
+filterwarnings = ignore::UserWarning
+testpaths = tests
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/test_io.py b/tests/test_io.py
new file mode 100644
index 00000000..f7c10536
--- /dev/null
+++ b/tests/test_io.py
@@ -0,0 +1,336 @@
+import sofar as sf
+from sofar.utils import (_get_conventions,
+                         _verify_convention_and_version,
+                         _atleast_nd,
+                         _nd_newaxis)
+from sofar.io import (_format_value_for_netcdf,
+                      _format_value_from_netcdf)
+import os
+from tempfile import TemporaryDirectory
+import pytest
+from pytest import raises
+import numpy as np
+import numpy.testing as npt
+from netCDF4 import Dataset
+
+
+def test_read_sofa(capfd):
+
+    temp_dir = TemporaryDirectory()
+    filename = os.path.join(temp_dir.name, "test.sofa")
+    sofa = sf.Sofa("SimpleFreeFieldHRIR")
+
+    # test defaults
+    sf.write_sofa(filename, sofa)
+    sofa = sf.read_sofa(filename)
+    assert hasattr(sofa, "_api")
+
+    # reading without updating API
+    sofa = sf.read_sofa(filename, verify=False)
+    assert not hasattr(sofa, "_api")
+
+    # read non-existing file
+    with raises(ValueError, match="test.sofa does not exist"):
+        sf.read_sofa("test.sofa")
+
+    # read file of unknown convention
+    sofa = sf.Sofa("SimpleFreeFieldHRIR")
+    sf.write_sofa(filename, sofa)
+    with Dataset(filename, "r+", format="NETCDF4") as file:
+        setattr(file, "SOFAConventions", "Funky")
+    with raises(ValueError, match="Convention 'Funky' does not exist"):
+        sf.read_sofa(filename)
+
+    # read file of unknown version (stored in file)
+    sofa = sf.Sofa("SimpleFreeFieldHRIR")
+    sf.write_sofa(filename, sofa)
+    with Dataset(filename, "r+", format="NETCDF4") as file:
+        setattr(file, "SOFAConventionsVersion", "0.1")
+    # ValueError when version should be matched
+    with raises(ValueError, match="Version 0.1 does not exist for"):
+        sf.read_sofa(filename)
+
+    # read file containing a variable with wrong shape
+    sofa = sf.Sofa("SimpleFreeFieldHRIR")
+    sf.write_sofa(filename, sofa)
+    # create variable with wrong shape
+    with Dataset(filename, "r+", format="NETCDF4") as file:
+        file.createDimension('A', 10)
+        var = file.createVariable("Data_IR", "f8", ('I', 'A'))
+        var[:] = np.zeros((1, 10)).astype("double")
+    # reading data with update API generates an error
+    with raises(ValueError, match="The SOFA object could not be"):
+        sf.read_sofa(filename)
+    # data can be read without updating API
+    sf.read_sofa(filename, verify=False)
+
+    # test assertion for wrong filename
+    with raises(ValueError, match="Filename must end with .sofa"):
+        sf.read_sofa('sofa.exe')
+
+
+def test_read_sofa_custom_data():
+    """Test if sofa files with custom data are loaded correctly"""
+
+    temp_dir = TemporaryDirectory()
+    filename = os.path.join(temp_dir.name, "test.sofa")
+    sofa = sf.Sofa("SimpleFreeFieldHRIR")
+
+    # GLOBAL attribute
+    sofa.add_attribute('GLOBAL_Warming', 'critical')
+    sf.write_sofa(filename, sofa)
+    sofa = sf.read_sofa(filename)
+    assert sofa.GLOBAL_Warming == 'critical'
+
+
+def test_write_sofa_assertion():
+    """Test assertion for wrong filename ending"""
+
+    sofa = sf.Sofa("SimpleFreeFieldHRIR")
+    with raises(ValueError, match="Filename must end with .sofa"):
+        sf.write_sofa("sofa.exe", sofa)
+
+
+def test_write_sofa_outdated_version():
+    """Test the warning for writing SOFA files with outdated versions"""
+
+    # generate test data and directory
+    tmp = TemporaryDirectory()
+    sofa = sf.Sofa("GeneralTF", version="1.0")
+
+    # write with outdated version
+    with pytest.warns(UserWarning, match="Writing SOFA object with outdated"):
+        sf.write_sofa(os.path.join(tmp.name, "outdated.sofa"), sofa)
+
+
+def test_write_sofa_compression():
+    """Test writing SOFA files with compression"""
+
+    # create temporary directory
+    temp_dir = TemporaryDirectory()
+
+    # create test data
+    sofa = sf.Sofa('SimpleFreeFieldHRIR')
+    sofa.Data_IR = np.zeros((1, 2, 2048))
+    sofa.Data_IR[0, 0] = np.array([1, 0, -1, 0] * 512)
+
+    filesize = None
+
+    for compression in range(10):
+        # write with current compression level
+        filename = os.path.join(temp_dir.name, f"test_{0}.sofa")
+        sf.write_sofa(filename, sofa, compression=compression)
+
+        # get and compare the file sizes
+        print(f"Assessing compression level {compression}")
+        if compression > 0:
+            assert os.stat(filename).st_size <= filesize
+        filesize = os.stat(filename).st_size
+
+
+# mandatory=True can not be tested because some conventions have default values
+# that have optional variables as dependencies
+@pytest.mark.parametrize("mandatory", [(False)])
+def test_roundtrip(mandatory):
+    """"
+    Cyclic test of create, write, read functions
+
+    1. create_sofa
+    2. write_sofa
+    3. read_sofa
+    4. compare SOFA from 1. and 3.
+    """
+
+    temp_dir = TemporaryDirectory()
+    names_versions = _get_conventions(return_type="name_version")
+
+    _, _, deprecations, _ = sf.Sofa._verification_rules()
+
+    for name, version in names_versions:
+        print(f"Testing: {name} {version}")
+
+        if name in deprecations["GLOBAL:SOFAConventions"]:
+            # deprecated conventions can not be written
+            sofa = sf.Sofa(name, mandatory, version, verify=False)
+            with pytest.warns(UserWarning, match="deprecations"):
+                sofa.verify(mode="read")
+        else:
+            # test full round-trip for other conventions
+            file = os.path.join(temp_dir.name, name + ".sofa")
+            sofa = sf.Sofa(name, mandatory, version)
+            sf.write_sofa(file, sofa)
+            sofa_r = sf.read_sofa(file)
+            identical = sf.equals(sofa, sofa_r, verbose=True, exclude="DATE")
+            assert identical
+
+
+def test_roundtrip_multidimensional_string_variable():
+    """
+    Test writing and reading multidimensional string variables (Wringting
+    string variables with one dimension is done in the other roundtrip test).
+    """
+
+    temp_dir = TemporaryDirectory()
+    file = os.path.join(temp_dir.name, "HeadphoneIR.sofa")
+
+    sofa = sf.Sofa("SimpleHeadphoneIR")
+    # add dummy matrix that contains 4 measurements
+    sofa.Data_IR = np.zeros((4, 2, 10))
+    # add (4, 1) string variable
+    sofa.SourceManufacturer = [["someone"], ["else"], ["did"], ["this"]]
+    # remove other string variables for simplicity
+    delattr(sofa, "SourceModel")
+    delattr(sofa, "ReceiverDescriptions")
+    delattr(sofa, "EmitterDescriptions")
+    delattr(sofa, "MeasurementDate")
+
+    # read write and assert
+    sf.write_sofa(file, sofa)
+    sofa_r = sf.read_sofa(file)
+
+    identical = sf.equals(sofa, sofa_r, exclude="DATE")
+    assert identical
+
+
+def test_format_value_for_netcdf():
+
+    # string and None dimensions (a.k.a NETCDF attribute)
+    value, dtype = _format_value_for_netcdf(
+        "string", "test_attr", "attribute", None, 12)
+    assert value == "string"
+    assert dtype == "attribute"
+
+    # int that should be converted to a string
+    value, dtype = _format_value_for_netcdf(
+        1, "test_attr", "attribute", None, 12)
+    assert value == "1"
+    assert dtype == "attribute"
+
+    # float that should be converted to a string
+    value, dtype = _format_value_for_netcdf(
+        0.2, "test_attr", "attribute", None, 12)
+    assert value == "0.2"
+    assert dtype == "attribute"
+
+    # string and IS dimensions
+    value, dtype = _format_value_for_netcdf(
+        "string", "TestVar", "string", "IS", 12)
+    assert value == np.array("string", dtype="S12")
+    assert dtype == "S1"
+    assert value.ndim == 2
+
+    # single entry array and none Dimensions
+    value, dtype = _format_value_for_netcdf(
+        ["string"], "TestVar", "string", "IS", 12)
+    assert value == np.array(["string"], dtype="S12")
+    assert dtype == "S1"
+    assert value.ndim == 2
+
+    # array of strings
+    value, dtype = _format_value_for_netcdf(
+        [["a"], ["bc"]], "TestVar", "string", "MS", 12)
+    assert all(value == np.array([["a"], ["bc"]], "S12"))
+    assert dtype == "S1"
+    assert value.ndim == 2
+
+    # test with list
+    value, dtype = _format_value_for_netcdf(
+        [0, 0], "TestVar", "double", "MR", 12)
+    npt.assert_allclose(value, np.array([0, 0])[np.newaxis, ])
+    assert dtype == "f8"
+    assert value.ndim == 2
+
+    # test with numpy array
+    value, dtype = _format_value_for_netcdf(
+        np.array([0, 0]), "TestVar", "double", "MR", 12)
+    npt.assert_allclose(value, np.array([0, 0])[np.newaxis, ])
+    assert dtype == "f8"
+    assert value.ndim == 2
+
+    # unknown data type
+    with raises(ValueError, match="Unknown type int for TestVar"):
+        value, dtype = _format_value_for_netcdf(1, "TestVar", "int", "MR", 12)
+
+
+def test_format_value_from_netcdf():
+
+    # single string (emulate NetCDF binary format)
+    value_in = np.array(["s", "t", "r"], dtype="S1")
+    value = _format_value_from_netcdf(value_in, "Some_Attribute")
+    assert value == "str"
+
+    # array of strings (emulate NetCDF binary format)
+    value_in = np.array([["s", "t", "r", "1"], ["s", "t", "r", "2"]],
+                        dtype="S1")
+    value = _format_value_from_netcdf(value_in, "Some_Attribute")
+    assert all(value == np.array(["str1", "str2"], dtype="U"))
+
+    # single string (emulate NetCDF ascii encoding)
+    value_in = np.array(["str"], dtype="U")
+    value = _format_value_from_netcdf(value_in, "Some_Attribute")
+    assert value == "str"
+
+    # array of strings (emulate NetCDF binary format)
+    value_in = np.array(["str1", "str2"], dtype="U")
+    value = _format_value_from_netcdf(value_in, "Some_Attribute")
+    assert all(value == np.array(["str1", "str2"], dtype="U"))
+
+    # numerical array that can be scalar
+    value = _format_value_from_netcdf(
+        np.array([44100], dtype="float"), "Data_SamplingRate")
+    assert value == 44100.
+
+    # numerical array that can not be scalar
+    value = _format_value_from_netcdf(
+        np.array([44100], dtype="float"), "Data_IR")
+    assert value == np.array(44100., dtype="float")
+
+    # masked array with missing data
+    array = np.ma.masked_array([1, 2], mask=[0, 1], dtype="float")
+    with pytest.warns(UserWarning, match="Entry Data_IR contains missing"):
+        value = _format_value_from_netcdf(array, "Data_IR")
+    npt.assert_allclose(value, array)
+
+    # test with invalid data dtype
+    with raises(TypeError, match="Data_IR: value.dtype is complex"):
+        _format_value_from_netcdf(
+            np.array([44100], dtype="complex"), "Data_IR")
+
+
+def test_verify_convention_and_version():
+
+    # test different possibilities for version
+    version = _verify_convention_and_version("latest", "1.0", "GeneralTF")
+    assert version == "2.0"
+
+    version = _verify_convention_and_version("2.0", "1.0", "GeneralTF")
+    assert version == "2.0"
+
+    version = _verify_convention_and_version("match", "1.0", "GeneralTF")
+    assert version == "1.0"
+
+    # test assertions
+    with raises(ValueError, match="Convention 'Funky' does not exist"):
+        _verify_convention_and_version("latest", "1.0", "Funky")
+    with raises(ValueError, match="Version 1.1 does not exist"):
+        _verify_convention_and_version("match", "1.1", "GeneralTF")
+    with raises(ValueError, match="Version 1.2 does not exist"):
+        _verify_convention_and_version("1.2", "1.0", "GeneralTF")
+
+
+def test_atleast_nd():
+    # test with single dimension array
+    for ndim in range(1, 6):
+        array = _atleast_nd(1, ndim)
+        assert array.ndim == ndim
+        assert array.flatten() == np.array([1])
+
+    # test with two-dimensional array
+    for ndim in range(1, 6):
+        array = _atleast_nd(np.atleast_2d(1), ndim)
+        assert array.ndim == max(2, ndim)
+        assert array.flatten() == np.array([1])
+
+
+def test_nd_newaxis():
+    assert _nd_newaxis([1, 2, 3, 4, 5, 6], 2).shape == (6, 1)
diff --git a/tests/test_sofa.py b/tests/test_sofa.py
new file mode 100644
index 00000000..815eff93
--- /dev/null
+++ b/tests/test_sofa.py
@@ -0,0 +1,359 @@
+"""Tests for sofar.Sofa (test for Sofa.verifycontained in test_sofa_verify)"""
+import sofar as sf
+import os
+from tempfile import TemporaryDirectory
+import pytest
+from pytest import raises
+import numpy as np
+
+
+def test_create_sofa_object(capfd):
+    # test assertion for type of convention parameter
+    with raises(TypeError, match="Convention must be a string"):
+        sf.Sofa(1)
+    # test assertion for invalid conventions
+    with raises(ValueError, match="Convention 'invalid' not found"):
+        sf.Sofa("invalid")
+
+    # test creation with defaults
+    sofa = sf.Sofa("GeneralTF")
+    assert isinstance(sofa, sf.Sofa)
+    assert sofa.GLOBAL_SOFAConventionsVersion == "2.0"
+    assert hasattr(sofa, '_convention')
+    assert hasattr(sofa, '_dimensions')
+    assert hasattr(sofa, '_api')
+
+    # assert __repr__
+    print(sofa)
+    out, _ = capfd.readouterr()
+    assert out == "sofar.SOFA object: GeneralTF 2.0\n"
+
+    # test returning only mandatory fields
+    sofa_all = sf.Sofa("GeneralTF")
+    sofa_man = sf.Sofa("GeneralTF", mandatory=True)
+    assert len(sofa_all.__dict__) > len(sofa_man.__dict__)
+
+    # test version parameter
+    sofa = sf.Sofa("GeneralTF", version="1.0")
+    assert str(sofa.GLOBAL_SOFAConventionsVersion) == "1.0"
+
+    # test invalid version
+    with raises(ValueError, match="Version 0.25 not found. Available"):
+        sf.Sofa("GeneralTF", version="0.25")
+
+    # test without updating the api
+    sofa = sf.Sofa("GeneralTF", verify=False)
+    assert hasattr(sofa, '_convention')
+    assert not hasattr(sofa, '_dimensions')
+    assert not hasattr(sofa, '_api')
+
+
+def test_set_attributes_of_sofa_object():
+    sofa = sf.Sofa("GeneralTF")
+
+    # set attribute
+    assert (sofa.ListenerPosition == [0, 0, 0]).all()
+    sofa.ListenerPosition = np.array([1, 1, 1])
+    assert (sofa.ListenerPosition == [1, 1, 1]).all()
+
+    # set read only attribute
+    with raises(TypeError, match="GLOBAL_Version is a read only"):
+        sofa.GLOBAL_Version = 1
+
+    # set non-existing attribute
+    with raises(TypeError, match="new is an invalid attribute"):
+        sofa.new = 1
+
+
+def test_delete_attribute_from_sofa_object():
+    sofa = sf.Sofa("GeneralTF")
+
+    # delete optional attribute
+    delattr(sofa, "GLOBAL_ApplicationName")
+
+    # delete mandatory attribute
+    with raises(TypeError, match="GLOBAL_Version is a mandatory"):
+        delattr(sofa, "GLOBAL_Version")
+
+    # delete not existing attribute
+    with raises(TypeError, match="new is not an attribute"):
+        delattr(sofa, "new")
+
+
+def test_copy_sofa_object():
+    sofa_org = sf.Sofa("GeneralTF")
+    sofa_cp = sofa_org.copy()
+
+    assert sf.equals(sofa_org, sofa_cp, verbose=False)
+    assert id(sofa_org) != id(sofa_cp)
+
+
+def test_list_dimensions(capfd):
+
+    # test FIR Data
+    sofa = sf.Sofa("GeneralFIR")
+    sofa.list_dimensions
+    out, _ = capfd.readouterr()
+    assert "N = 1 samples (set by Data_IR of dimension MRN)" in out
+
+    # test TF Data
+    sofa = sf.Sofa("GeneralTF")
+    sofa.list_dimensions
+    out, _ = capfd.readouterr()
+    assert "N = 1 frequencies (set by Data_Real of dimension MRN)" in out
+
+    # test SOS Data
+    sofa = sf.Sofa("SimpleFreeFieldHRSOS")
+    sofa.list_dimensions
+    out, _ = capfd.readouterr()
+    assert "N = 6 SOS coefficients (set by Data_SOS of dimension MRN)" in out
+
+    # test non spherical harmonics data
+    sofa = sf.Sofa("GeneralFIR")
+    sofa.list_dimensions
+    out, _ = capfd.readouterr()
+    assert "E = 1 emitter" in out
+    assert "R = 1 receiver" in out
+
+    sofa.EmitterPosition_Type = "spherical harmonics"
+    sofa.ReceiverPosition_Type = "spherical harmonics"
+    sofa.EmitterPosition_Units = "degree, degree, metre"
+    sofa.ReceiverPosition_Units = "degree, degree, metre"
+    sofa.list_dimensions
+    out, _ = capfd.readouterr()
+    assert "E = 1 emitter spherical harmonics coefficients" in out
+    assert "R = 1 receiver spherical harmonics coefficients" in out
+
+    # test assertion in case of variables with wrong type or shape
+    sofa = sf.Sofa("GeneralFIR")
+    sofa.Data_IR = "test"
+    with raises(ValueError, match="Dimensions can not be shown"):
+        sofa.list_dimensions
+    sofa.Data_IR = [1, 2, 3, 4]
+    with raises(ValueError, match="Dimensions can not be shown"):
+        sofa.list_dimensions
+
+
+def test_get_dimension():
+    """Test getting the size of dimensions"""
+
+    # test FIR Data
+    sofa = sf.Sofa("GeneralFIR")
+    size = sofa.get_dimension("N")
+    assert size == 1
+
+    # test with wrong dimension
+    with raises(ValueError, match="Q is not a valid dimension"):
+        size = sofa.get_dimension("Q")
+
+
+def test_info(capfd):
+
+    sofa = sf.Sofa("SimpleFreeFieldHRIR")
+
+    # test with wrong info string
+    with raises(
+            ValueError, match="info='invalid' is invalid"):
+        sofa.info("invalid")
+
+    # test with default parameter
+    sofa.info()
+    out, _ = capfd.readouterr()
+    assert "showing all entries" in out
+
+    # test listing all entry names
+    for info in ["all", "mandatory", "optional", "read only", "data"]:
+        sofa.info(info)
+        out, _ = capfd.readouterr()
+        assert f"showing {info} entries" in out
+
+    # list information for specific entry
+    sofa.info("ListenerPosition")
+    out, _ = capfd.readouterr()
+    assert "ListenerPosition\n    type: double" in out
+    assert "ListenerPosition_Type\n    type: attribute" in out
+    assert "ListenerPosition_Units\n    type: attribute" in out
+
+
+def test_inspect(capfd):
+
+    temp_dir = TemporaryDirectory()
+    file = os.path.join(temp_dir.name, "info.txt")
+
+    sofa = sf.Sofa("SimpleFreeFieldHRIR")
+
+    # inspect file
+    sofa.inspect(file)
+
+    # check terminal output
+    out, _ = capfd.readouterr()
+    assert "GLOBAL_SOFAConventions : SimpleFreeFieldHRIR" in out
+    assert ("ReceiverPosition : (R=2, C=3, I=1)\n"
+            "  [[ 0.    0.09  0.  ]\n"
+            "   [ 0.   -0.09  0.  ]]") in out
+
+    # check text file
+    with open(file, "r") as f_id:
+        text = f_id.readlines()
+    assert out == "".join(text)
+
+
+def test_add_entry():
+
+    sofa = sf.Sofa("GeneralTF")
+
+    tmp_dir = TemporaryDirectory()
+
+    # test adding a single variable entry
+    sofa.add_variable("Temperature", 25.1, "double", "MI")
+    entry = {"flags": None, "dimensions": "MI", "type": "double",
+             "default": None, "comment": ""}
+    assert sofa.Temperature == 25.1
+    assert sofa._custom["Temperature"] == entry
+    assert sofa._convention["Temperature"] == entry
+
+    # test adding string variable, global and local attributes
+    sofa.add_variable("Mood", "good", "string", "MS")
+    assert sofa.Mood == "good"
+    sofa.add_attribute("GLOBAL_Mood", "good")
+    assert sofa.GLOBAL_Mood == "good"
+    sofa.add_attribute("Temperature_Units", "degree celsius")
+    assert sofa.Temperature_Units == "degree celsius"
+
+    # check if everything can be verified and written, and read correctly
+    sf.write_sofa(os.path.join(tmp_dir.name, "tmp.sofa"), sofa)
+    sofa_read = sf.read_sofa(os.path.join(tmp_dir.name, "tmp.sofa"))
+    assert sf.equals(sofa, sofa_read)
+
+    # test deleting an entry
+    delattr(sofa, "Temperature_Units")
+    assert not hasattr(sofa, "Temperature_Units")
+    assert "Temperature_Units" not in sofa._custom
+
+    # test adding missing entry defined in convention
+    sofa._protected = False
+    delattr(sofa, "ListenerPosition")
+    sofa._protected = True
+    sofa.add_variable("ListenerPosition", [0, 0, 0], "double", "IC")
+    assert "ListenerPosition" not in sofa._custom
+
+    # test assertions
+    # add existing entry
+    with raises(ValueError, match="Entry Temperature already exists"):
+        sofa.add_variable("Temperature", 25.1, "double", "MI")
+    # entry violating the naming convention
+    with raises(ValueError, match="underscores '_' in the name"):
+        sofa.add_variable("Temperature_Celsius", 25.1, "double", "MI")
+    with raises(ValueError, match="The name of Data"):
+        sofa.add_attribute("Data_Time_measured", "midnight")
+    # entry with wrong type
+    with raises(ValueError, match="dtype is float but must be"):
+        sofa.add_variable("TemperatureCelsius", 25.1, "float", "MI")
+    # variable without dimensions
+    with raises(ValueError, match="dimensions must be provided"):
+        sofa.add_variable("TemperatureCelsius", 25.1, "double", None)
+    # invalid dimensins
+    with pytest.warns(UserWarning, match="Added custom dimension T"):
+        sofa.add_variable("TemperatureCelsius", [25.1, 25.2], "double", "T")
+    # attribute with missing variable
+    with raises(ValueError, match="Adding Attribute Variable"):
+        sofa.add_attribute("Variable_Unit", "celsius")
+
+
+@pytest.mark.parametrize("mandatory,optional",
+                         [(True, False), (False, True), [True, True]])
+@pytest.mark.parametrize("verbose", [True, False])
+def test_add_missing(
+        mandatory, optional, verbose, capfd):
+
+    sofa = sf.Sofa("GeneralTF")
+
+    # attributes for testing
+    man = "GLOBAL_AuthorContact"
+    opt = "GLOBAL_History"
+
+    # remove data before adding it again
+    sofa._protected = False
+    sofa.delete(man)
+    sofa.delete(opt)
+    sofa._protected = False
+
+    # add missing data
+    sofa.add_missing(mandatory, optional, verbose)
+    out, _ = capfd.readouterr()
+
+    if mandatory and optional:
+        assert hasattr(sofa, man) and hasattr(sofa, opt)
+    elif mandatory:
+        assert hasattr(sofa, man) and not hasattr(sofa, opt)
+    elif optional:
+        assert not hasattr(sofa, man) and hasattr(sofa, opt)
+
+    if verbose:
+        if mandatory and optional:
+            assert man in out and opt in out
+        elif mandatory:
+            assert man in out and opt not in out
+        elif optional:
+            assert man not in out and opt in out
+    else:
+        assert out == ""
+
+
+def test_delete_entry():
+
+    sofa = sf.Sofa("SimpleHeadphoneIR")
+    assert hasattr(sofa, "GLOBAL_History")
+    assert hasattr(sofa, "SourceManufacturer")
+    # delete one optional attribute and variable
+    sofa.delete("GLOBAL_History")
+    sofa.delete("SourceManufacturer")
+    # check if data were removed
+    assert not hasattr(sofa, "GLOBAL_History")
+    assert not hasattr(sofa, "SourceManufacturer")
+
+
+def test__update_conventions():
+    """Tested in test_sofa_verify.pi::test_version"""
+    pass
+
+
+def test__get_size_and_shape_of_string_var():
+
+    # test with string
+    S, shape = sf.Sofa._get_size_and_shape_of_string_var("four", "key")
+    assert S == 4
+    assert shape == (1, 1)
+
+    # test with single string list
+    S, shape = sf.Sofa._get_size_and_shape_of_string_var(["four"], "key")
+    assert S == 4
+    assert shape == (1, )
+
+    # test with list of strings
+    S, shape = sf.Sofa._get_size_and_shape_of_string_var(
+        ["four", "fivee"], "key")
+    assert S == 5
+    assert shape == (2, )
+
+    # test with numpy strings array
+    S, shape = sf.Sofa._get_size_and_shape_of_string_var(
+        np.array(["four", "fivee"], dtype="S256"), "key")
+    assert S == 5
+    assert shape == (2, )
+
+    # test with wrong type
+    with raises(TypeError, match="key must be a string"):
+        sf.Sofa._get_size_and_shape_of_string_var(1, "key")
+
+
+def test___mandatory():
+    assert sf.Sofa._mandatory("rm")
+    assert not sf.Sofa._mandatory("r")
+    assert not sf.Sofa._mandatory(None)
+
+
+def test___readonly():
+    assert sf.Sofa._read_only("rm")
+    assert not sf.Sofa._read_only("m")
+    assert not sf.Sofa._read_only(None)
diff --git a/tests/test_sofa_upgrade_conventions.py b/tests/test_sofa_upgrade_conventions.py
new file mode 100644
index 00000000..32e3911d
--- /dev/null
+++ b/tests/test_sofa_upgrade_conventions.py
@@ -0,0 +1,93 @@
+import sofar as sf
+from sofar.utils import _get_conventions
+import pytest
+import os
+import json
+
+# load deprecation rules
+base = os.path.join(
+    os.path.dirname(__file__), "..", "sofar", "sofa_conventions", "rules")
+
+with open(os.path.join(base, "upgrade.json"), "r") as file:
+    upgrade = json.load(file)
+
+# load convention paths
+paths = _get_conventions("paths")
+
+
+def test_printouts(capfd):
+    """
+    Test console print for calling upgrade with up to data and deprecated data
+    """
+
+    # test printouts with up to data convention -------------------------------
+    sofa = sf.Sofa("SimpleFreeFieldHRIR")
+    sofa.upgrade_convention()
+    out, _ = capfd.readouterr()
+    assert "is up to date" in out
+
+    # test printouts with outdated conventions --------------------------------
+    sofa = sf.Sofa("SimpleFreeFieldTF", verify=False)
+    out, _ = capfd.readouterr()
+
+    # calling with default arguments to get information only
+    sofa.upgrade_convention()
+    out, _ = capfd.readouterr()
+    assert "can be upgraded" in out
+    assert "SimpleFreeFieldHRTF v1.0" in out
+    assert "invalid" not in out
+
+    # calling with invalid target
+    sofa.upgrade_convention("SimpleFreeFieldHRTF_0.1")
+    out, _ = capfd.readouterr()
+    assert "can be upgraded" in out
+    assert "invalid" in out
+    assert "SimpleFreeFieldHRTF v1.0" in out
+
+    # calling with valid target (nothing to move, remove, no message)
+    sofa.upgrade_convention("SimpleFreeFieldHRTF_1.0")
+    out, _ = capfd.readouterr()
+    assert "can be upgraded" not in out
+    assert "invalid" not in out
+    assert "Upgrading SimpleFreeFieldTF v1.0" in out
+    assert "to SimpleFreeFieldHRTF v1.0" in out
+    assert "No data to move" in out
+    assert "No data to remove" in out
+    assert "All mandatory data contained" in out
+
+    # calling with valid target (including message)
+    sofa = sf.Sofa("SimpleFreeFieldHRIR", version="0.4", verify=False)
+    out, _ = capfd.readouterr()
+    sofa.upgrade_convention("SimpleFreeFieldHRIR_1.0")
+    out, _ = capfd.readouterr()
+    assert "Consider to add the optional data 'SourceUp'" in out
+
+    # calling with valid target (things to move)
+    sofa = sf.Sofa("MultiSpeakerBRIR", verify=False)
+    out, _ = capfd.readouterr()
+    sofa.upgrade_convention("SingleRoomMIMOSRIR_1.0")
+    out, _ = capfd.readouterr()
+    assert "Moving Data_IR" in out
+
+
+@pytest.mark.parametrize("path", paths)
+def test_upgrade_conventions(path, capfd):
+
+    # extract information for testing
+    convention, version = os.path.basename(path).split("_")
+    version = version[:-5]
+    deprecated = "deprecated" in path
+
+    # get SOFA object and targets for upgrading
+    sofa = sf.Sofa(convention, version=version, verify=False)
+    out, _ = capfd.readouterr()
+    targets = sofa.upgrade_convention()
+    out, _ = capfd.readouterr()
+
+    if targets:
+        for target in targets:
+            sofa.upgrade_convention(target)
+            out, _ = capfd.readouterr()
+            assert "Upgrading" in out
+    else:
+        assert not deprecated
diff --git a/tests/test_sofa_verify.py b/tests/test_sofa_verify.py
new file mode 100644
index 00000000..e6f05e0a
--- /dev/null
+++ b/tests/test_sofa_verify.py
@@ -0,0 +1,472 @@
+"""Tests for sofar.Sofa.verify()"""
+import sofar as sf
+from sofar.utils import _complete_sofa
+import os
+from glob import glob
+import pytest
+from pytest import raises
+import numpy as np
+
+# get verification rules
+_, unit_aliases, _, _ = sf.Sofa._verification_rules()
+
+# directory containing the verificatin data
+basedir = os.path.join(os.path.dirname(__file__), "..", "sofar",
+                       "sofa_conventions", "data")
+
+# files for testing the verification rules
+restricted_values = glob(
+    os.path.join(basedir, "restricted_values", "*.sofa"))
+general_dependencies = glob(
+    os.path.join(basedir, "general_dependencies", "*.sofa"))
+specific_dependencies = glob(
+    os.path.join(basedir, "specific_dependencies", "*.sofa"))
+deprecations = glob(
+    os.path.join(basedir, "deprecations", "*.sofa"))
+restricted_dimensions = glob(
+    os.path.join(basedir, "restricted_dimensions", "*.sofa"))
+
+
+# general tests ---------------------------------------------------------------
+def test_verify_value():
+    # example alias for testing as returned by Sofa._verification_rules()
+    unit_aliases = {"meter": "metre",
+                    "degrees": "degree"}
+
+    # Simple pass: no restriction on value
+    assert sf.Sofa._verify_value("goofy", None, unit_aliases, "Some_Units")
+
+    # simple pass: single unit
+    assert sf.Sofa._verify_value(
+        "meter", ["metre"], unit_aliases, "Some_Units")
+
+    # complex pass: list of units
+    assert sf.Sofa._verify_value("degrees, degrees, meter",
+                                 ["degree, degree, metre"],
+                                 unit_aliases, "Some_Units")
+
+    # complex pass: list of units with other separators allowed by AES69
+    assert sf.Sofa._verify_value("degrees,degrees, meter",
+                                 ["degree, degree, metre"],
+                                 unit_aliases, "Some_Units")
+
+    # simple fail: single unit
+    assert not sf.Sofa._verify_value("centimeter", ["metre"],
+                                     unit_aliases, "Some_Units")
+
+    # complex fail: list of units
+    assert not sf.Sofa._verify_value("rad, rad, metre",
+                                     ["degree, degree, metre"],
+                                     unit_aliases, "Some_Units")
+
+
+def test_issue_handling(capfd):
+    """Test different methods for handling issues during verification"""
+
+    error_msg = "\nERRORS\n------\n"
+    warning_msg = "\nWARNINGS\n--------\n"
+
+    # no issue
+    issue_handling = "raise"
+    error_occurred, issues = sf.Sofa._verify_handle_issues(
+        warning_msg, error_msg, issue_handling)
+    assert not error_occurred
+    assert issues is None
+
+    # raise
+    issue_handling = "raise"
+    with pytest.warns(UserWarning, match="warning"):
+        sf.Sofa._verify_handle_issues(
+            warning_msg + "warning", error_msg, issue_handling)
+    with raises(ValueError, match="error"):
+        sf.Sofa._verify_handle_issues(
+            warning_msg, error_msg + "error", issue_handling)
+
+    # report warning
+    issue_handling = "report"
+    with pytest.warns(None) as warning:
+        _, issues = sf.Sofa._verify_handle_issues(
+            warning_msg + "warning", error_msg, issue_handling)
+    assert "warning" in issues
+    assert "ERROR" not in issues
+    assert len(warning) == 0
+    # report error
+    _, issues = sf.Sofa._verify_handle_issues(
+            warning_msg, error_msg + "error", issue_handling)
+    assert "error" in issues
+    assert "WARNING" not in issues
+
+    # test invalid data for netCDF attribute
+    issue_handling = "ignore"
+    sofa = sf.Sofa("GeneralFIR")
+    sofa.GLOBAL_Comment = [1, 2, 3]
+    issues = sofa.verify(issue_handling=issue_handling)
+
+    assert issues is None
+    assert capfd.readouterr() == ("", "")
+
+
+def test_case_insensitivity():
+    """
+    Reading applications shall ignore the case of
+    - units and
+    - types of coordinate systems
+    """
+
+    # data type (must be case sensitive) --------------------------------------
+    sofa = sf.Sofa("SimpleFreeFieldHRIR")
+    sofa._protected = False
+    sofa.GLOBAL_DataType = "fir"
+    sofa._protected = True
+    with raises(ValueError, match="GLOBAL_DataType is fir"):
+        sofa.verify()
+
+    # room type ---------------------------------------------------------------
+    sofa = sf.Sofa("FreeFieldHRIR")
+    sofa.GLOBAL_RoomType = "Free field"
+    assert sofa.verify() is None
+
+    # units -------------------------------------------------------------------
+    # example alias for testing as returned by Sofa._verification_rules()
+    unit_aliases = {"meter": "metre",
+                    "degrees": "degree"}
+
+    # case insensitive testing
+    assert sf.Sofa._verify_value(
+        "Meter", ["meter"], unit_aliases, "Some_Units")
+    assert sf.Sofa._verify_value("DegrEes, dEgreeS, MeTer",
+                                 ["degree, degree, metre"],
+                                 unit_aliases, "Some_Units")
+
+    sofa = sf.Sofa("FreeFieldDirectivityTF")
+    sofa.N_Units = "HertZ"
+    assert sofa.verify(issue_handling="return", mode="read") is None
+
+    # coordinate types --------------------------------------------------------
+    sofa = sf.Sofa("SimpleFreeFieldHRIR")
+    sofa.ListenerPosition_Type = "Cartesian"
+    assert sofa.verify(issue_handling="return") is None
+
+
+# 1. check if the mandatory attributes are contained --------------------------
+def test_missing_default_attributes(capfd):
+
+    # test missing default attribute
+    sofa = sf.Sofa("GeneralTF")
+    sofa._protected = False
+    delattr(sofa, "GLOBAL_Conventions")
+    sofa._protected = True
+
+    # raises error
+    with raises(ValueError, match="Detected missing mandatory data"):
+        sofa.verify(issue_handling="raise")
+
+    # prints warning and adds data
+    sofa.verify(issue_handling="print")
+    out, _ = capfd.readouterr()
+
+    assert "Added mandatory data with default values" in out
+    assert sofa.GLOBAL_Conventions == "SOFA"
+
+
+# 2. verify data type ---------------------------------------------------------
+def test_data_types(capfd):
+
+    # test invalid data for netCDF attribute
+    sofa = sf.Sofa("GeneralFIR")
+    sofa.GLOBAL_Comment = [1, 2, 3]
+    with raises(ValueError, match="- GLOBAL_Comment must be string"):
+        sofa.verify()
+
+    # test invalid data for netCDF double variable
+    sofa = sf.Sofa("GeneralFIR")
+    sofa.Data_IR = np.array("test")
+    with raises(ValueError, match="- Data_IR must be int or float"):
+        sofa.verify()
+
+    sofa.Data_IR = "1"
+    with raises(ValueError, match="- Data_IR must be int, float"):
+        sofa.verify()
+
+    sofa.Data_IR = 1+1j
+    with raises(ValueError, match="- Data_IR must be int, float"):
+        sofa.verify()
+
+    # test invalid data with issue_handling "print" and "return"
+    sofa = sf.Sofa("GeneralFIR")
+    sofa.Data_IR = np.array("test")
+    out, _ = capfd.readouterr()
+    issues = sofa.verify(issue_handling="print")
+    out, _ = capfd.readouterr()
+    assert issues is None
+    assert "- Data_IR must be int or float" in out
+
+    issues = sofa.verify(issue_handling="return")
+    out, _ = capfd.readouterr()
+    assert "- Data_IR must be int or float" in issues
+    assert "- Data_IR must be int or float" not in out
+
+    # test valid data
+    sofa.Data_IR = np.array([1])
+    sofa.verify()
+    sofa.Data_IR = [1]
+    sofa.verify()
+    sofa.Data_IR = 1
+    sofa.verify()
+
+    # test invalid data for netCDF attribute
+    sofa = sf.Sofa("GeneralFIR")
+    sofa.GLOBAL_History = 1
+    with raises(ValueError, match="- GLOBAL_History must be string"):
+        sofa.verify()
+
+    # test invalid data for netCDF string variable
+    sofa = sf.Sofa("SimpleHeadphoneIR")
+    sofa.SourceModel = 1
+    with raises(ValueError, match="- SourceModel must be string"):
+        sofa.verify()
+
+    sofa.SourceModel = np.array(1)
+    with raises(ValueError, match="- SourceModel must be U or S"):
+        sofa.verify()
+
+    # test valid data
+    sofa.SourceModel = ["test"]
+    sofa.verify()
+    sofa.SourceModel = np.array(["test"])
+    sofa.verify()
+
+
+# 3. Verify names of entries --------------------------------------------------
+def test_wrong_name():
+
+    # attribute with missing variable
+    sofa = sf.Sofa("GeneralTF")
+    sofa._protected = False
+    sofa.IR_Type = "pressure"
+    sofa._custom = {"IR_Type": {"default": None,
+                                "flags": None,
+                                "dimensions": None,
+                                "type": "attribute",
+                                "comment": ""}}
+    sofa._protected = True
+
+    with raises(ValueError, match="Detected attributes with missing"):
+        sofa.verify()
+
+    # attribute with no underscore
+    sofa = sf.Sofa("GeneralTF")
+    sofa._protected = False
+    sofa.IRType = "pressure"
+    sofa._custom = {"IRType": {"default": None,
+                               "flags": None,
+                               "dimensions": None,
+                               "type": "attribute",
+                               "comment": ""}}
+    sofa._protected = True
+
+    with raises(ValueError, match="Detected attribute names with too many"):
+        sofa.verify()
+
+    # variable with underscore
+    sofa = sf.Sofa("GeneralTF")
+    sofa._protected = False
+    sofa.IR_Data = 1
+    sofa._custom = {"IR_Data": {"default": None,
+                                "flags": None,
+                                "dimensions": "IM",
+                                "type": "double",
+                                "comment": ""}}
+    sofa._protected = True
+
+    with raises(ValueError, match="Detected variable names with too many"):
+        sofa.verify()
+
+    # variables with reserved names
+    sofa = sf.Sofa("GeneralTF")
+    sofa.add_variable("APIfunk", 1, "double", "I")
+    with raises(ValueError, match="with reserved key words"):
+        sofa.verify()
+
+    sofa = sf.Sofa("GeneralTF")
+    sofa.add_variable("PRIVATEtreasure", 1, "double", "I")
+    with raises(ValueError, match="with reserved key words"):
+        sofa.verify()
+
+    sofa = sf.Sofa("GeneralTF")
+    sofa.add_variable("GLOBALdata", 1, "double", "I")
+    with raises(ValueError, match="with reserved key words"):
+        sofa.verify()
+
+
+def test_custom_data_name():
+    """
+    Custom entries can not have the names of data contained in the convention.
+    """
+
+    sofa = sf.Sofa("GeneralTF")
+    # add variable Origin, although GLOBAL_Origin exists
+    sofa.add_variable("Origin", 1, "double", 'I')
+    with raises(ValueError,
+                match="custom variable or attribute with reserved names"):
+        sofa.verify()
+
+
+# 4 + 5. get and verify dimensions of data ------------------------------------
+def test_wrong_shape():
+
+    # test attribute with wrong shape
+    sofa = sf.Sofa("GeneralTF")
+    sofa.ListenerPosition = 1
+    with raises(ValueError, match=("- ListenerPosition has shape")):
+        sofa.verify()
+
+
+# 6. check restrictions on the content of SOFA files --------------------------
+def test_rules_error_messages():
+    """
+    Test the exact error messages raised by violated rules from rules.json. The
+    remaining test check only if errors are raised.
+    """
+
+    # wrong value
+    sofa = _complete_sofa()
+    sofa.GLOBAL_RoomType = "pentagon"
+    error = "GLOBAL_RoomType is pentagon but must be free field, reverberant"
+    with raises(ValueError, match=error):
+        sofa.verify()
+
+    # missing general dependency
+    sofa = _complete_sofa()
+    sofa.delete("ListenerView_Type")
+    error = "ListenerView_Type must be given if ListenerView is in SOFA object"
+    with raises(ValueError, match=error):
+        sofa.verify()
+
+    # wrong dimensions
+    sofa = sf.Sofa("SimpleFreeFieldHRSOS")
+    sofa.Data_SOS = np.zeros((1, 2, 1))
+    error = ("Dimension N is of size 1 but must be an integer multiple of 6 "
+             "greater 0 if GLOBAL_DataType is SOS")
+    with raises(ValueError, match=error):
+        sofa.verify()
+
+    # missing specific dependency
+    sofa = _complete_sofa()
+    sofa.GLOBAL_RoomType = "reverberant"
+    sofa.delete("GLOBAL_RoomDescription")
+    error = ("GLOBAL_RoomDescription must be given "
+             "if GLOBAL_RoomType is reverberant")
+    with raises(ValueError, match=error):
+        sofa.verify()
+
+    # wrong value for specific dependency
+    sofa = _complete_sofa()
+    sofa.ListenerPosition_Type = "spherical"
+    error = ("ListenerPosition_Units is metre but must be degree, degree, "
+             "metre if ListenerPositionType is spherical")
+
+
+@pytest.mark.parametrize("file", restricted_values)
+def test_restricted_values(file):
+    """Test all rules that restrict data to certain values"""
+
+    try:
+        # load file without verification
+        sofa = sf.read_sofa(file, verify=False)
+        # test verification
+        with raises(ValueError, match="invalid-value"):
+            sofa.verify()
+    except ValueError as VE:
+        assert str(VE) == "Convention 'invalid-value' does not exist"
+
+
+@pytest.mark.parametrize("file", general_dependencies)
+def test_general_dependencies(file):
+    """Test all rules that restrict data to certain values"""
+
+    # load file without verification
+    sofa = sf.read_sofa(file, verify=False)
+
+    # test verification
+    with raises(ValueError, match="missing|must be given"):
+        sofa.verify()
+
+
+@pytest.mark.parametrize("file", specific_dependencies)
+def test_specific_dependencies(file):
+
+    # load file without verification
+    sofa = sf.read_sofa(file, verify=False)
+
+    # extract match for error message ('invalid-value' or 'missing')
+    match = file[file.rfind("=")+1:file.rfind(".")]
+
+    if match == "missing":
+        match += "|must be given"
+
+    with raises(ValueError, match=match):
+        sofa.verify()
+
+
+@pytest.mark.parametrize("file", restricted_dimensions)
+def test_restricted_dimensions(file):
+
+    # load file without verification
+    sofa = sf.read_sofa(file, verify=False)
+
+    # extract data for error message from filename
+    match = f"Dimension {file[-8]} is of size {file[-6]} but must be"
+
+    # test verification
+    with raises(ValueError, match=match):
+        sofa.verify()
+
+
+# 7. check write only restrictions --------------------------------------------
+def test_read_and_write_mode():
+
+    # Unit with uppercase is ok when reading but not ok when writing
+    sofa = sf.Sofa("SimpleFreeFieldHRIR")
+    sofa.ListenerPosition_Units = "Meter"
+
+    assert sofa.verify(mode="read", issue_handling="return") is None
+    with raises(ValueError, match="lower case letters when writing"):
+        sofa.verify(mode="write")
+
+
+# 8. check deprecations -------------------------------------------------------
+@pytest.mark.parametrize("file", deprecations)
+def test_deprecations(file):
+    """
+    Test if deprecations raise warnings in read mode and errors in write mode.
+    """
+
+    # read file without verification
+    sofa = sf.read_sofa(file, verify=False)
+
+    # check if deprecated and substitute convention exist in sofar
+    conventions = sf.utils._get_conventions("name")
+    deprecated = sofa.GLOBAL_SOFAConventions
+
+    if deprecated not in conventions:
+        return
+
+    # check warnings and errors
+    sofa = sf.Sofa(deprecated, verify=False)
+
+    msg = ("Detected deprecations:\n"
+           f"- GLOBAL_SOFAConventions is {deprecated}, which is deprecated. ")
+
+    with pytest.warns(UserWarning, match=msg):
+        sofa.verify(mode="read")
+
+    with raises(ValueError, match=msg):
+        sofa.verify(mode="write")
+
+
+def test_preliminary_conventions_version():
+    """Test if using a convention version < 1.0 issues a warning"""
+
+    with pytest.warns(UserWarning, match="Detected preliminary"):
+        sf.Sofa("SingleRoomDRIR", version="0.3")
diff --git a/tests/test_sofar.py b/tests/test_sofar.py
deleted file mode 100644
index a47b7df1..00000000
--- a/tests/test_sofar.py
+++ /dev/null
@@ -1,1244 +0,0 @@
-# %%
-import sofar as sf
-from sofar.sofar import (_update_conventions,
-                         _compile_conventions,
-                         _get_conventions,
-                         _format_value_for_netcdf,
-                         _format_value_from_netcdf,
-                         _atleast_nd,
-                         _nd_newaxis,
-                         _verify_convention_and_version)
-import os
-import json
-from tempfile import TemporaryDirectory
-import pytest
-from pytest import raises
-import numpy as np
-import numpy.testing as npt
-from copy import deepcopy
-from netCDF4 import Dataset
-from distutils import dir_util
-
-
-def test_list_conventions(capfd):
-
-    # check output to console using pytest default fixture capfd
-    sf.list_conventions()
-    out, _ = capfd.readouterr()
-    assert "Available SOFA conventions:" in out
-
-
-def test_get_conventions(capfd):
-
-    # check the return type
-    paths = _get_conventions(return_type="path")
-    assert isinstance(paths, list)
-    assert os.path.isfile(paths[0])
-    assert "source" not in paths[0]
-    assert paths[0].endswith(".json")
-    out, _ = capfd.readouterr()
-    assert out == ""
-
-    paths = _get_conventions(return_type="path_source")
-    assert isinstance(paths, list)
-    assert os.path.isfile(paths[0])
-    assert "source" in paths[0]
-    assert paths[0].endswith(".csv")
-    out, _ = capfd.readouterr()
-    assert out == ""
-
-    names = _get_conventions(return_type="name")
-    assert isinstance(names, list)
-    assert not os.path.isfile(names[0])
-
-    names_versions = _get_conventions(return_type="name_version")
-    assert isinstance(names_versions, list)
-    assert isinstance(names_versions[0], tuple)
-
-    with raises(ValueError, match="return_type None is invalid"):
-        _get_conventions(return_type="None")
-
-
-def test_update_conventions(capfd):
-
-    # create temporary directory and copy existing conventions
-    temp_dir = TemporaryDirectory()
-    dir_util.copy_tree(
-        os.path.join(os.path.dirname(__file__), "..", "sofar", "conventions"),
-        temp_dir.name)
-
-    # modify and delete selected conventions to verbose feedback
-    os.remove(os.path.join(temp_dir.name, "source", "GeneralTF_2.0.csv"))
-    with open(os.path.join(
-            temp_dir.name, "source", "GeneralFIR_2.0.csv"), "w") as fid:
-        fid.write("test")
-
-    # first run to test if conventions were updated
-    _update_conventions(conventions_path=temp_dir.name)
-    out, _ = capfd.readouterr()
-    assert "added new convention: GeneralTF_2.0.csv" in out
-    assert "updated existing convention: GeneralFIR_2.0.csv" in out
-
-    # second run to make sure that up to date conventions are not overwritten
-    _update_conventions(conventions_path=temp_dir.name)
-    out, _ = capfd.readouterr()
-    assert "added" not in out
-    assert "updated" not in out
-
-
-def test_compile_conventions():
-    """Test compiling the json conventions from the csv files."""
-
-    # create temporary directory and copy existing source conventions
-    temp_dir = TemporaryDirectory()
-    os.mkdir(os.path.join(temp_dir.name, "source"))
-    dir_util.copy_tree(os.path.join(
-        os.path.dirname(__file__), "..", "sofar", "conventions", "source"),
-        os.path.join(temp_dir.name, "source"))
-
-    # compile conventions
-    _compile_conventions(temp_dir.name)
-
-    # get list of reference json files
-    paths = _get_conventions("path")
-    conventions = [os.path.split(path)[1] for path in paths]
-    path = os.path.split(paths[0])[0]
-
-    for convention in conventions:
-        # load conventions
-        ref = os.path.join(path, convention)
-        with open(ref, "r") as file:
-            ref_data = json.load(file)
-
-        test = os.path.join(temp_dir.name, convention)
-        with open(test, "r") as file:
-            test_data = json.load(file)
-
-        # compare conventions
-        assert ref_data == test_data
-
-
-def test_create_sofa_object(capfd):
-    # test assertion for type of convention parameter
-    with raises(TypeError, match="Convention must be a string"):
-        sf.Sofa(1)
-    # test assertion for invalid conventions
-    with raises(ValueError, match="Convention 'invalid' not found"):
-        sf.Sofa("invalid")
-
-    # test creation with defaults
-    sofa = sf.Sofa("GeneralTF")
-    assert isinstance(sofa, sf.sofar.Sofa)
-    assert sofa.GLOBAL_SOFAConventionsVersion == "2.0"
-    assert hasattr(sofa, '_convention')
-    assert hasattr(sofa, '_dimensions')
-    assert hasattr(sofa, '_api')
-
-    # assert __repr__
-    print(sofa)
-    out, _ = capfd.readouterr()
-    assert out == "sofar.SOFA object: GeneralTF 2.0\n"
-
-    # test returning only mandatory fields
-    sofa_all = sf.Sofa("GeneralTF")
-    sofa_man = sf.Sofa("GeneralTF", mandatory=True)
-    assert len(sofa_all.__dict__) > len(sofa_man.__dict__)
-
-    # test version parameter
-    sofa = sf.Sofa("GeneralTF", version="1.0")
-    assert str(sofa.GLOBAL_SOFAConventionsVersion) == "1.0"
-
-    # test invalid version
-    with raises(ValueError, match="Version 0.25 not found. Available"):
-        sf.Sofa("GeneralTF", version="0.25")
-
-    # test without updating the api
-    sofa = sf.Sofa("GeneralTF", verify=False)
-    assert hasattr(sofa, '_convention')
-    assert not hasattr(sofa, '_dimensions')
-    assert not hasattr(sofa, '_api')
-
-
-def test_set_attributes_of_sofa_object():
-    sofa = sf.Sofa("GeneralTF")
-
-    # set attribute
-    assert (sofa.ListenerPosition == [0, 0, 0]).all()
-    sofa.ListenerPosition = np.array([1, 1, 1])
-    assert (sofa.ListenerPosition == [1, 1, 1]).all()
-
-    # set read only attribute
-    with raises(TypeError, match="GLOBAL_Version is a read only"):
-        sofa.GLOBAL_Version = 1
-
-    # set non-existing attribute
-    with raises(TypeError, match="new is an invalid attribute"):
-        sofa.new = 1
-
-
-def test_sofa_delete_attribute():
-    sofa = sf.Sofa("GeneralTF")
-
-    # delete optional attribute
-    delattr(sofa, "GLOBAL_ApplicationName")
-
-    # delete mandatory attribute
-    with raises(TypeError, match="GLOBAL_Version is a mandatory"):
-        delattr(sofa, "GLOBAL_Version")
-
-    # delete not existing attribute
-    with raises(TypeError, match="new is not an attribute"):
-        delattr(sofa, "new")
-
-
-def test_copy_sofa_object():
-    sofa_org = sf.Sofa("GeneralTF")
-    sofa_cp = sofa_org.copy()
-
-    assert sf.equals(sofa_org, sofa_cp, verbose=False)
-    assert id(sofa_org) != id(sofa_cp)
-
-
-def test_sofa_verify_version():
-
-    # test the default "latest"
-    sofa = sf.Sofa("GeneralTF", version="1.0")
-    assert str(sofa.GLOBAL_SOFAConventionsVersion) == "1.0"
-    with pytest.warns(UserWarning, match="Upgraded"):
-        sofa.verify()
-    assert str(sofa.GLOBAL_SOFAConventionsVersion) == "2.0"
-
-    # test "match"
-    sofa = sf.Sofa("GeneralTF", version="1.0")
-    assert str(sofa.GLOBAL_SOFAConventionsVersion) == "1.0"
-    sofa.verify(version="match")
-    assert str(sofa.GLOBAL_SOFAConventionsVersion) == "1.0"
-
-    # test downgrading
-    sofa = sf.Sofa("GeneralTF")
-    assert str(sofa.GLOBAL_SOFAConventionsVersion) == "2.0"
-    with pytest.warns(UserWarning, match="Downgraded"):
-        sofa.verify(version="1.0")
-    assert str(sofa.GLOBAL_SOFAConventionsVersion) == "1.0"
-
-
-def test_sofa_verify_missing_default_attributes():
-
-    # test missing default attribute
-    sofa = sf.Sofa("GeneralTF")
-    sofa._protected = False
-    delattr(sofa, "GLOBAL_Conventions")
-    sofa._protected = True
-    with pytest.warns(UserWarning, match="Added mandatory data with default"):
-        sofa.verify()
-    assert sofa.GLOBAL_Conventions == "SOFA"
-
-
-def test_sofa_verify_data_types(capfd):
-
-    # test invalid data for netCDF attribute
-    sofa = sf.Sofa("GeneralFIR")
-    sofa.GLOBAL_Comment = [1, 2, 3]
-    with raises(ValueError, match="- GLOBAL_Comment must be string"):
-        sofa.verify()
-
-    # test invalid data for netCDF double variable
-    sofa = sf.Sofa("GeneralFIR")
-    sofa.Data_IR = np.array("test")
-    with raises(ValueError, match="- Data_IR must be int or float"):
-        sofa.verify()
-
-    sofa.Data_IR = "1"
-    with raises(ValueError, match="- Data_IR must be int, float"):
-        sofa.verify()
-
-    sofa.Data_IR = 1+1j
-    with raises(ValueError, match="- Data_IR must be int, float"):
-        sofa.verify()
-
-    # test invalid data with issue_handling "print" and "return"
-    sofa = sf.Sofa("GeneralFIR")
-    sofa.Data_IR = np.array("test")
-    out, _ = capfd.readouterr()
-    issues = sofa.verify(issue_handling="print")
-    out, _ = capfd.readouterr()
-    assert issues is None
-    assert "- Data_IR must be int or float" in out
-
-    issues = sofa.verify(issue_handling="return")
-    out, _ = capfd.readouterr()
-    assert "- Data_IR must be int or float" in issues
-    assert "- Data_IR must be int or float" not in out
-
-    # test valid data
-    sofa.Data_IR = np.array([1])
-    sofa.verify()
-    sofa.Data_IR = [1]
-    sofa.verify()
-    sofa.Data_IR = 1
-    sofa.verify()
-
-    # test invalid data for netCDF attribute
-    sofa = sf.Sofa("GeneralFIR")
-    sofa.GLOBAL_History = 1
-    with raises(ValueError, match="- GLOBAL_History must be string"):
-        sofa.verify()
-
-    # test invalid data for netCDF string variable
-    sofa = sf.Sofa("SimpleHeadphoneIR")
-    sofa.SourceModel = 1
-    with raises(ValueError, match="- SourceModel must be string"):
-        sofa.verify()
-
-    sofa.SourceModel = np.array(1)
-    with raises(ValueError, match="- SourceModel must be U or S"):
-        sofa.verify()
-
-    # test valid data
-    sofa.SourceModel = ["test"]
-    sofa.verify()
-    sofa.SourceModel = np.array(["test"])
-    sofa.verify()
-
-
-def test_sofa_verify_wrong_shape():
-
-    # test attribute with wrong shape
-    sofa = sf.Sofa("GeneralTF")
-    sofa.ListenerPosition = 1
-    with raises(ValueError, match=("- ListenerPosition has shape")):
-        sofa.verify()
-
-
-def test_sofa_verify_wrong_name():
-
-    # attribute with missing variable
-    sofa = sf.Sofa("GeneralTF")
-    sofa._protected = False
-    sofa.IR_Type = "pressure"
-    sofa._custom = {"IR_Type": {"default": None,
-                                "flags": None,
-                                "dimensions": None,
-                                "type": "attribute",
-                                "comment": ""}}
-    sofa._protected = True
-
-    with raises(ValueError, match="Detected attributes with missing"):
-        sofa.verify()
-
-    # attribute with no underscore
-    sofa = sf.Sofa("GeneralTF")
-    sofa._protected = False
-    sofa.IRType = "pressure"
-    sofa._custom = {"IRType": {"default": None,
-                               "flags": None,
-                               "dimensions": None,
-                               "type": "attribute",
-                               "comment": ""}}
-    sofa._protected = True
-
-    with raises(ValueError, match="Detected attribute names with too many"):
-        sofa.verify()
-
-    # variable with underscore
-    sofa = sf.Sofa("GeneralTF")
-    sofa._protected = False
-    sofa.IR_Data = 1
-    sofa._custom = {"IR_Data": {"default": None,
-                                "flags": None,
-                                "dimensions": "IM",
-                                "type": "double",
-                                "comment": ""}}
-    sofa._protected = True
-
-    with raises(ValueError, match="Detected variable names with too many"):
-        sofa.verify()
-
-
-# test everything from sofar._sofa_restrictions explicitly to make sure
-# all possible error in SOFA files are caught
-@pytest.mark.parametrize("key,value,msg", [
-    ("GLOBAL_DataType", "image", "GLOBAL_DataType is image but must be FIR,"),
-    ("GLOBAL_SOFAConventions", "FIR", ""),         # message tested above
-    ("GLOBAL_RoomType", "Living room", ""),        # message tested above
-    ("ListenerPosition_Type", "cylindrical", ""),  # message tested above
-    ("ListenerPosition_Units", "A",
-     "ListenerPosition_Units is A but must be metre if ListenerPosition_Type"),
-    ("ListenerView_Type", "cylindrical", ""),      # message tested above
-    ("ListenerView_Units", "A", ""),               # message tested above
-    ("ReceiverPosition_Type", "cylindrical", ""),  # message tested above
-    ("ReceiverPosition_Units", "A", ""),           # message tested above
-    ("ReceiverView_Type", "cylindrical", ""),      # message tested above
-    ("ReceiverView_Units", "A", ""),               # message tested above
-    ("SourcePosition_Type", "cylindrical", ""),    # message tested above
-    ("SourcePosition_Units", "A", ""),             # message tested above
-    ("SourceView_Type", "cylindrical", ""),        # message tested above
-    ("SourceView_Units", "A", ""),                 # message tested above
-    ("EmitterPosition_Type", "cylindrical", ""),   # message tested above
-    ("EmitterPosition_Units", "A", ""),            # message tested above
-    ("EmitterView_Type", "cylindrical", ""),       # message tested above
-    ("EmitterView_Units", "A", ""),                # message tested above
-    ("RoomVolume_Units", "square metre", ""),      # message tested above
-    ("RoomTemperature_Units", "Celsius", ""),      # message tested above
-])
-def test_sofa_verify_restrictions_data_wrong_value(key, value, msg):
-    """
-    Test assertions for generally restricted data values.
-    """
-
-    sofa = sf.Sofa("SingleRoomSRIR")
-    # add variables for testing certain dependencies. If cases in case the
-    # variables get added to the convnetion some time later.
-    if not hasattr(sofa, "RoomVolume"):
-        sofa.add_variable("RoomVolume", 200, "double", 'I')
-        sofa.add_attribute("RoomVolume_Units", "cubic metre")
-    if not hasattr(sofa, "RoomTemperature"):
-        sofa.add_variable("RoomTemperature", 100, "double", 'I')
-        sofa.add_attribute("RoomTemperature_Units", "Kelvin")
-    sofa._protected = False
-    setattr(sofa, key, value)
-    sofa._protected = True
-    with raises(ValueError, match=msg):
-        sofa.verify()
-
-
-# can't test everything from sofar._sofa_restrictions explicitly because
-# mandatory fields are added by default
-@pytest.mark.parametrize("key,msg", [
-    ("ReceiverView", "ReceiverView must be given if ReceiverUp"),
-    ("RoomVolume_Units", ""),                      # message tested above
-    ("RoomTemperature_Units", ""),                 # message tested above
-])
-def test_sofa_verify_restrictions_data_missing_attribute(key, msg):
-    """
-    Test assertions for removing optional fields that become mandatory due to
-    another field.
-    """
-
-    sofa = sf.Sofa("SingleRoomSRIR")
-    # add variables for testing certain dependencies. If cases in case the
-    # variables get added to the convnetion some time later.
-    if not hasattr(sofa, "RoomVolume"):
-        sofa.add_variable("RoomVolume", 200, "double", 'I')
-        sofa.add_attribute("RoomVolume_Units", "cubic metre")
-    if not hasattr(sofa, "RoomTemperature"):
-        sofa.add_variable("RoomTemperature", 100, "double", 'I')
-        sofa.add_attribute("RoomTemperature_Units", "Kelvin")
-    sofa._protected = False
-    delattr(sofa, key)
-    sofa._protected = True
-    with raises(ValueError, match=msg):
-        sofa.verify()
-
-
-# test everything from sofar._sofa_restrictions explicitly to make sure
-# all possible error in SOFA files are caught
-@pytest.mark.parametrize("convention,key,value,msg", [
-    ("GeneralFIR", "Data_SamplingRate_Units", "hz",
-     "Data_SamplingRate_Units is hz but must be hertz"),
-    ("GeneralTF", "N_LongName", "f",
-     "N_LongName is f but must be frequency"),
-    ("GeneralTF", "N_Units", "hz",
-     "N_Units is hz but must be hertz"),
-])
-def test_sofa_verify_restrictions_data_type(convention, key, value, msg):
-    """Test assertions for values that are restricted by GLOBAL_DataType."""
-
-    sofa = sf.Sofa(convention)
-    setattr(sofa, key, value)
-    with raises(ValueError, match=msg):
-        sofa.verify()
-
-
-def test_sofa_verify_restrictions_api_spherical_harmonics():
-    """
-    Test assertions for incorrect number of emitters/receiver in case of
-    spherical harmonics coordinate systems
-    """
-
-    sofa = sf.Sofa("GeneralFIR-E")
-    sofa.ReceiverPosition_Type = "spherical harmonics"
-    sofa.ReceiverPosition_Units = "degree, degree, metre"
-    sofa.Data_IR = np.zeros((1, 2, 1, 1))
-    sofa.Data_Delay = np.zeros((1, 2, 1))
-    with raises(ValueError, match="Dimension R is of size 2 but must be"):
-        sofa.verify()
-
-    sofa = sf.Sofa("GeneralFIR-E")
-    sofa.EmitterPosition_Type = "spherical harmonics"
-    sofa.EmitterPosition_Units = "degree, degree, metre"
-    sofa.Data_IR = np.zeros((1, 1, 1, 2))
-    sofa.Data_Delay = np.zeros((1, 1, 2))
-    with raises(ValueError, match="Dimension E is of size 2 but must be"):
-        sofa.verify()
-
-
-def test_sofa_verify_restrictions_api_second_order_sections():
-    """
-    Test assertions for incorrect number of N in case of
-    SOS data type
-    """
-
-    sofa = sf.Sofa("SimpleFreeFieldHRSOS")
-    sofa.Data_SOS = np.zeros((1, 2, 1))
-    with raises(ValueError, match="Dimension N is of size 1 but must be"):
-        sofa.verify()
-
-
-@pytest.mark.parametrize('convention,kwargs,msg', [
-    ("GeneralFIR", [("GLOBAL_DataType", "FIR-E")], "GLOBAL_DataType is FIR-E"),
-    ("GeneralFIR-E", [("GLOBAL_DataType", "FIR")], "GLOBAL_DataType is FIR"),
-    ("GeneralFIRE", [("GLOBAL_DataType", "FIR")], "GLOBAL_DataType is FIR"),
-    ("GeneralTF", [("GLOBAL_DataType", "TF-E")], "GLOBAL_DataType is TF-E"),
-    ("GeneralTF-E", [("GLOBAL_DataType", "TF")], "GLOBAL_DataType is TF"),
-
-    ("SimpleFreeFieldHRIR", [("GLOBAL_DataType", "FIR-E")],
-     "GLOBAL_DataType is FIR-E"),
-    ("SimpleFreeFieldHRIR", [("GLOBAL_RoomType", "echoic")],
-     "GLOBAL_RoomType is echoic"),
-    ("SimpleFreeFieldHRIR", [("EmitterPosition", np.zeros((2, 3)))],
-     "Dimension E is of size 2 but must be 1 if GLOBAL"),
-    ("SimpleFreeFieldHRIR",
-     [("EmitterPosition_Type", "spherical harmonics"),
-      ("EmitterPosition_Units", "degree, degree, metre")],
-     "EmitterPosition_Type is spherical harmonics"),
-
-    ("SimpleFreeFieldHRTF", [("GLOBAL_DataType", "TF-E")],
-     "GLOBAL_DataType is TF-E"),
-    ("SimpleFreeFieldHRTF", [("GLOBAL_RoomType", "echoic")],
-     "GLOBAL_RoomType is echoic"),
-    ("SimpleFreeFieldHRTF", [("EmitterPosition", np.zeros((2, 3)))],
-     "Dimension E is of size 2 but must be 1 if GLOBAL"),
-    ("SimpleFreeFieldHRTF",
-     [("EmitterPosition_Type", "spherical harmonics"),
-      ("EmitterPosition_Units", "degree, degree, metre")],
-     "EmitterPosition_Type is spherical harmonics"),
-
-    # DataType for SimpleFreeFieldHRSOS can not be checked. It raises an error
-    # beforehands
-    ("SimpleFreeFieldHRSOS", [("GLOBAL_RoomType", "echoic")],
-     "GLOBAL_RoomType is echoic"),
-    ("SimpleFreeFieldHRSOS", [("EmitterPosition", np.zeros((2, 3)))],
-     "Dimension E is of size 2 but must be 1 if GLOBAL"),
-    ("SimpleFreeFieldHRSOS",
-     [("EmitterPosition_Type", "spherical harmonics"),
-      ("EmitterPosition_Units", "degree, degree, metre")],
-     "EmitterPosition_Type is spherical harmonics"),
-
-    # Can not be tested, because it is not yet defined
-    # ("FreeFieldHRIR", [("GLOBAL_DataType", "FIR")],
-    #  "GLOBAL_DataType is FIR"),
-    ("FreeFieldHRTF", [("GLOBAL_DataType", "TF")],
-     "GLOBAL_DataType is TF"),
-    ("SimpleHeadphoneIR", [("GLOBAL_DataType", "FIR-E")],
-     "GLOBAL_DataType is FIR-E"),
-    ("SingleRoomSRIR", [("GLOBAL_DataType", "FIR-E")],
-     "GLOBAL_DataType is FIR-E"),
-    ("SingleRoomMIMOSRIR", [("GLOBAL_DataType", "FIR")],
-     "GLOBAL_DataType is FIR"),
-    ("FreeFieldDirectivityTF", [("GLOBAL_DataType", "TF-E")],
-     "GLOBAL_DataType is TF-E"),
-])
-def test_sofa_verify_restrictions_convention(convention, kwargs, msg):
-
-    sofa = sf.Sofa(convention)
-    sofa._protected = False
-    for key_value in kwargs:
-        setattr(sofa, key_value[0], key_value[1])
-    with raises(ValueError, match=msg):
-        sofa.verify()
-
-
-def test_verify_value():
-    # example alias for testing as returned by sf.sofa._sofa_restrictions()
-    unit_aliases = {"meter": "metre",
-                    "degrees": "degree"}
-
-    # Simple pass: no restriction on value
-    assert sf.Sofa._verify_value("goofy", None, unit_aliases)
-
-    # simple pass: single unit
-    assert sf.Sofa._verify_value("meter", "metre", unit_aliases)
-
-    # complex pass: list of units
-    assert sf.Sofa._verify_value("degrees, degrees, meter",
-                                 "degree, degree, metre", unit_aliases)
-
-    # complex pass: list of units with other separators allowed by AES69
-    assert sf.Sofa._verify_value("degrees,degrees meter",
-                                 "degree, degree, metre", unit_aliases)
-
-    # simple fail: single unit
-    assert not sf.Sofa._verify_value("centimeter", "metre", unit_aliases)
-
-    # complex fail: list of units
-    assert not sf.Sofa._verify_value("rad, rad, metre",
-                                     "degree, degree, metre", unit_aliases)
-
-
-def test_sofa_verify_ignore(capfd):
-    """Test the ignore option of Sofa.verify"""
-
-    # test invalid data for netCDF attribute
-    sofa = sf.Sofa("GeneralFIR")
-    sofa.GLOBAL_Comment = [1, 2, 3]
-    issues = sofa.verify(issue_handling="ignore")
-
-    assert issues is None
-    assert capfd.readouterr() == ("", "")
-
-
-def test_verify_issue_handling(capfd):
-    """Test different methods for handling issues during verification"""
-
-    error_msg = "\nERRORS\n------\n"
-    warning_msg = "\nWARNINGS\n--------\n"
-
-    # no issue
-    issue_handling = "raise"
-    error_occurred, issues = sf.Sofa._verify_handle_issues(
-        warning_msg, error_msg, issue_handling)
-    assert not error_occurred
-    assert issues is None
-
-    # raise
-    issue_handling = "raise"
-    with pytest.warns(UserWarning, match="warning"):
-        sf.Sofa._verify_handle_issues(
-            warning_msg + "warning", error_msg, issue_handling)
-    with raises(ValueError, match="error"):
-        sf.Sofa._verify_handle_issues(
-            warning_msg, error_msg + "error", issue_handling)
-
-    # report warning
-    issue_handling = "report"
-    with pytest.warns(None) as warning:
-        _, issues = sf.Sofa._verify_handle_issues(
-            warning_msg + "warning", error_msg, issue_handling)
-    assert "warning" in issues
-    assert "ERROR" not in issues
-    assert len(warning) == 0
-    # report error
-    _, issues = sf.Sofa._verify_handle_issues(
-            warning_msg, error_msg + "error", issue_handling)
-    assert "error" in issues
-    assert "WARNING" not in issues
-
-
-def test_list_dimensions(capfd):
-
-    # test FIR Data
-    sofa = sf.Sofa("GeneralFIR")
-    sofa.list_dimensions
-    out, _ = capfd.readouterr()
-    assert "N = 1 samples (set by Data_IR of dimension MRN)" in out
-
-    # test TF Data
-    sofa = sf.Sofa("GeneralTF")
-    sofa.list_dimensions
-    out, _ = capfd.readouterr()
-    assert "N = 1 frequencies (set by Data_Real of dimension MRN)" in out
-
-    # test SOS Data
-    sofa = sf.Sofa("SimpleFreeFieldSOS")
-    sofa.list_dimensions
-    out, _ = capfd.readouterr()
-    assert "N = 6 SOS coefficients (set by Data_SOS of dimension MRN)" in out
-
-    # test non spherical harmonics data
-    sofa = sf.Sofa("GeneralFIR")
-    sofa.list_dimensions
-    out, _ = capfd.readouterr()
-    assert "E = 1 emitter" in out
-    assert "R = 1 receiver" in out
-
-    sofa.EmitterPosition_Type = "spherical harmonics"
-    sofa.ReceiverPosition_Type = "spherical harmonics"
-    sofa.EmitterPosition_Units = "degree, degree, metre"
-    sofa.ReceiverPosition_Units = "degree, degree, metre"
-    sofa.list_dimensions
-    out, _ = capfd.readouterr()
-    assert "E = 1 emitter spherical harmonics coefficients" in out
-    assert "R = 1 receiver spherical harmonics coefficients" in out
-
-    # test assertion in case of variables with wrong type or shape
-    sofa = sf.Sofa("GeneralFIR")
-    sofa.Data_IR = "test"
-    with raises(ValueError, match="Dimensions can not be shown"):
-        sofa.list_dimensions
-    sofa.Data_IR = [1, 2, 3, 4]
-    with raises(ValueError, match="Dimensions can not be shown"):
-        sofa.list_dimensions
-
-
-def test_get_dimension():
-    """Test getting the size of dimensions"""
-
-    # test FIR Data
-    sofa = sf.Sofa("GeneralFIR")
-    size = sofa.get_dimension("N")
-    assert size == 1
-
-    # test with wrong dimension
-    with raises(ValueError, match="Q is not a valid dimension"):
-        size = sofa.get_dimension("Q")
-
-
-def test_info(capfd):
-
-    sofa = sf.Sofa("SimpleFreeFieldHRIR")
-
-    # test with wrong info string
-    with raises(
-            ValueError, match="info='invalid' is invalid"):
-        sofa.info("invalid")
-
-    # test listing all entry names
-    for info in ["all", "mandatory", "optional", "read only", "data"]:
-        sofa.info(info)
-        out, _ = capfd.readouterr()
-        assert f"showing {info} entries" in out
-
-    # list information for specific entry
-    sofa.info("ListenerPosition")
-    out, _ = capfd.readouterr()
-    assert "ListenerPosition\n    type: double" in out
-    assert "ListenerPosition_Type\n    type: attribute" in out
-    assert "ListenerPosition_Units\n    type: attribute" in out
-
-
-def test_inspect(capfd):
-
-    temp_dir = TemporaryDirectory()
-    file = os.path.join(temp_dir.name, "info.txt")
-
-    sofa = sf.Sofa("SimpleFreeFieldHRIR")
-
-    # inspect file
-    sofa.inspect(file)
-
-    # check terminal output
-    out, _ = capfd.readouterr()
-    assert "GLOBAL_SOFAConventions : SimpleFreeFieldHRIR" in out
-    assert ("ReceiverPosition : (R=2, C=3, I=1)\n"
-            "  [[ 0.    0.09  0.  ]\n"
-            "   [ 0.   -0.09  0.  ]]") in out
-
-    # check text file
-    with open(file, "r") as f_id:
-        text = f_id.readlines()
-    assert out == "".join(text)
-
-
-def test_read_sofa(capfd):
-
-    temp_dir = TemporaryDirectory()
-    filename = os.path.join(temp_dir.name, "test.sofa")
-    sofa = sf.Sofa("SimpleFreeFieldHRIR")
-
-    # test defaults
-    sf.write_sofa(filename, sofa)
-    sofa = sf.read_sofa(filename)
-    assert hasattr(sofa, "_api")
-
-    # reading without updating API
-    sofa = sf.read_sofa(filename, verify=False)
-    assert not hasattr(sofa, "_api")
-
-    # read non-existing file
-    with raises(ValueError, match="test.sofa does not exist"):
-        sf.read_sofa("test.sofa")
-
-    # read file of unknown convention
-    sofa = sf.Sofa("SimpleFreeFieldHRIR")
-    sf.write_sofa(filename, sofa)
-    with Dataset(filename, "r+", format="NETCDF4") as file:
-        setattr(file, "SOFAConventions", "Funky")
-    with raises(ValueError, match="Convention 'Funky' does not exist"):
-        sf.read_sofa(filename)
-
-    # read file of unknown version (stored in file)
-    sofa = sf.Sofa("SimpleFreeFieldHRIR")
-    sf.write_sofa(filename, sofa)
-    with Dataset(filename, "r+", format="NETCDF4") as file:
-        setattr(file, "SOFAConventionsVersion", "0.1")
-    # ValueError when version should be matched
-    with raises(ValueError, match="Version 0.1 does not exist for"):
-        sf.read_sofa(filename, version="match")
-    # output when version should be updated to latest
-    sf.read_sofa(filename, version="latest")
-    out, _ = capfd.readouterr()
-    assert "Updated conventions version from 0.1 to 1.0" in out
-
-    # read file of unknown version (user specified)
-    sofa = sf.Sofa("SimpleFreeFieldHRIR")
-    sf.write_sofa(filename, sofa)
-    with raises(ValueError, match="Version 0.25 does not exist for"):
-        sf.read_sofa(filename, version="0.25")
-
-    # read file containing a variable with wrong shape
-    sofa = sf.Sofa("SimpleFreeFieldHRIR")
-    sf.write_sofa(filename, sofa)
-    # create variable with wrong shape
-    with Dataset(filename, "r+", format="NETCDF4") as file:
-        file.createDimension('A', 10)
-        var = file.createVariable("Data_IR", "f8", ('I', 'A'))
-        var[:] = np.zeros((1, 10)).astype("double")
-    # reading data with update API generates an error
-    with raises(ValueError, match="The SOFA object could not be"):
-        sf.read_sofa(filename)
-    # data can be read without updating API
-    sf.read_sofa(filename, verify=False)
-
-    # test assertion for wrong filename
-    with raises(ValueError, match="Filename must end with .sofa"):
-        sf.read_sofa('sofa.exe')
-
-
-def test_read_sofa_custom_data():
-    """Test if sofa files with custom data are loaded correctly"""
-
-    temp_dir = TemporaryDirectory()
-    filename = os.path.join(temp_dir.name, "test.sofa")
-    sofa = sf.Sofa("SimpleFreeFieldHRIR")
-
-    # GLOBAL attribute
-    sofa.add_attribute('GLOBAL_Warming', 'critical')
-    sf.write_sofa(filename, sofa)
-    sofa = sf.read_sofa(filename)
-    assert sofa.GLOBAL_Warming == 'critical'
-
-
-def test_write_sofa_assertion():
-    """Test assertion for wrong filename ending"""
-
-    sofa = sf.Sofa("SimpleFreeFieldHRIR")
-    with raises(ValueError, match="Filename must end with .sofa"):
-        sf.write_sofa("sofa.exe", sofa)
-
-
-def test_write_sofa_compression():
-    """Test writing SOFA files with compression"""
-
-    # create temporary directory
-    temp_dir = TemporaryDirectory()
-
-    # create test data
-    sofa = sf.Sofa('SimpleFreeFieldHRIR')
-    sofa.Data_IR = np.zeros((1, 2, 2048))
-    sofa.Data_IR[0, 0] = np.array([1, 0, -1, 0] * 512)
-
-    filesize = None
-
-    for compression in range(10):
-        # write with current compression level
-        filename = os.path.join(temp_dir.name, f"test_{0}.sofa")
-        sf.write_sofa(filename, sofa, compression=compression)
-
-        # get and compare the file sizes
-        print(f"Assessing compression level {compression}")
-        if compression > 0:
-            assert os.stat(filename).st_size <= filesize
-        filesize = os.stat(filename).st_size
-
-
-@pytest.mark.parametrize("mandatory", [(False), (True)])
-def test_roundtrip(mandatory):
-    """"
-    Cyclic test of create, write, read functions
-
-    1. create_sofa
-    2. write_sofa
-    3. read_sofa
-    4. compare SOFA from 1. and 3.
-    """
-
-    temp_dir = TemporaryDirectory()
-    names_versions = _get_conventions(return_type="name_version")
-
-    for name, version in names_versions:
-        print(f"Testing: {name}")
-        file = os.path.join(temp_dir.name, name + ".sofa")
-        sofa = sf.Sofa(name, mandatory, version)
-        sf.write_sofa(file, sofa, version)
-        sofa_r = sf.read_sofa(file, version=version)
-        identical = sf.equals(sofa, sofa_r, verbose=True, exclude="DATE")
-        assert identical
-
-
-def test_roundtrip_multidimensional_string_variable():
-    """
-    Test writing and reading multidimensional string variables (Wringting
-    string variables with one dimension is done in the other roundtrip test).
-    """
-
-    temp_dir = TemporaryDirectory()
-    file = os.path.join(temp_dir.name, "HeadphoneIR.sofa")
-
-    sofa = sf.Sofa("SimpleHeadphoneIR")
-    # add dummy matrix that contains 4 measurements
-    sofa.Data_IR = np.zeros((4, 2, 10))
-    # add (4, 1) string variable
-    sofa.SourceManufacturer = [["someone"], ["else"], ["did"], ["this"]]
-    # remove other string variables for simplicity
-    delattr(sofa, "SourceModel")
-    delattr(sofa, "ReceiverDescription")
-    delattr(sofa, "EmitterDescription")
-    delattr(sofa, "MeasurementDate")
-
-    # read write and assert
-    sf.write_sofa(file, sofa)
-    sofa_r = sf.read_sofa(file)
-
-    identical = sf.equals(sofa, sofa_r, exclude="DATE")
-    assert identical
-
-
-def test_equals_global_parameters():
-
-    sofa_a = sf.Sofa("SimpleFreeFieldHRIR")
-
-    # check invalid
-    with raises(ValueError, match="exclude is"):
-        sf.equals(sofa_a, sofa_a, exclude="wrong")
-
-    # check identical objects
-    assert sf.equals(sofa_a, sofa_a)
-
-    # check different number of keys
-    sofa_b = deepcopy(sofa_a)
-    sofa_b._protected = False
-    delattr(sofa_b, "ReceiverPosition")
-    sofa_b._protected = True
-    with pytest.warns(UserWarning, match="not identical: sofa_a has"):
-        is_identical = sf.equals(sofa_a, sofa_b)
-        assert not is_identical
-
-    # check different keys
-    sofa_b = deepcopy(sofa_a)
-    sofa_b._protected = False
-    sofa_b.PositionReceiver = sofa_b.ReceiverPosition
-    delattr(sofa_b, "ReceiverPosition")
-    sofa_b._protected = True
-    with pytest.warns(UserWarning, match="not identical: sofa_a and sofa_b"):
-        is_identical = sf.equals(sofa_a, sofa_b)
-        assert not is_identical
-
-    # check mismatching data types
-    sofa_b = deepcopy(sofa_a)
-    sofa_b._protected = False
-    sofa_b._convention["ReceiverPosition"]["type"] = "int"
-    sofa_b._protected = True
-    with pytest.warns(UserWarning, match="not identical: ReceiverPosition"):
-        is_identical = sf.equals(sofa_a, sofa_b)
-        assert not is_identical
-
-    # check exclude GLOBAL attributes
-    sofa_b = deepcopy(sofa_a)
-    sofa_b._protected = False
-    delattr(sofa_b, "GLOBAL_Version")
-    sofa_b._protected = True
-    is_identical = sf.equals(sofa_a, sofa_b, exclude="GLOBAL")
-    assert is_identical
-
-    # check exclude Date attributes
-    sofa_b = deepcopy(sofa_a)
-    sofa_b._protected = False
-    delattr(sofa_b, "GLOBAL_DateModified")
-    sofa_b._protected = True
-    is_identical = sf.equals(sofa_a, sofa_b, exclude="DATE")
-    assert is_identical
-
-    # check exclude Date attributes
-    sofa_b = deepcopy(sofa_a)
-    sofa_b._protected = False
-    delattr(sofa_b, "GLOBAL_DateModified")
-    sofa_b._protected = True
-    is_identical = sf.equals(sofa_a, sofa_b, exclude="ATTR")
-    assert is_identical
-
-
-@pytest.mark.parametrize("value_a, value_b, attribute, fails", [
-    (1, "1", "GLOBAL_SOFAConventionsVersion", False),
-    (1, "1.0", "GLOBAL_SOFAConventionsVersion", False),
-    (1., "1", "GLOBAL_SOFAConventionsVersion", False),
-    ("1", "2", "GLOBAL_SOFAConventionsVersion", True),
-    ([[1, 2]], [1, 2], "Data_IR", False),
-    ([[1, 2]], [1, 3], "Data_IR", True),
-    ("HD 650", ["HD 650"], "SourceModel", False),
-    ("HD 650", np.array(["HD 650"], dtype="U"), "SourceModel", False),
-    ("HD 650", np.array(["HD 650"], dtype="S"), "SourceModel", False),
-    ("HD 650", "HD-650", "SourceModel", True)
-])
-def test_equals_attribute_values(value_a, value_b, attribute, fails):
-
-    # generate SOFA objects (SimpleHeadphoneIR has string variables)
-    sofa_a = sf.Sofa("SimpleHeadphoneIR")
-    sofa_a._protected = False
-    sofa_b = deepcopy(sofa_a)
-
-    # set parameters
-    setattr(sofa_a, attribute, value_a)
-    sofa_a._protected = True
-    setattr(sofa_b, attribute, value_b)
-    sofa_b._protected = True
-
-    # compare
-    if fails:
-        with pytest.warns(UserWarning):
-            assert not sf.equals(sofa_a, sofa_b)
-    else:
-        assert sf.equals(sofa_a, sofa_b)
-
-
-def test_add_entry():
-
-    sofa = sf.Sofa("GeneralTF")
-
-    tmp_dir = TemporaryDirectory()
-
-    # test adding a single variable entry
-    sofa.add_variable("Temperature", 25.1, "double", "MI")
-    entry = {"flags": None, "dimensions": "MI", "type": "double",
-             "default": None, "comment": ""}
-    assert sofa.Temperature == 25.1
-    assert sofa._custom["Temperature"] == entry
-    assert sofa._convention["Temperature"] == entry
-
-    # test adding string variable, global and local attributes
-    sofa.add_variable("Mood", "good", "string", "MS")
-    assert sofa.Mood == "good"
-    sofa.add_attribute("GLOBAL_Mood", "good")
-    assert sofa.GLOBAL_Mood == "good"
-    sofa.add_attribute("Temperature_Units", "degree Celsius")
-    assert sofa.Temperature_Units == "degree Celsius"
-
-    # check if everything can be verified and written, and read correctly
-    sf.write_sofa(os.path.join(tmp_dir.name, "tmp.sofa"), sofa)
-    sofa_read = sf.read_sofa(os.path.join(tmp_dir.name, "tmp.sofa"))
-    assert sf.equals(sofa, sofa_read)
-
-    # test deleting an entry
-    delattr(sofa, "Temperature_Units")
-    assert not hasattr(sofa, "Temperature_Units")
-    assert "Temperature_Units" not in sofa._custom
-
-    # test assertions
-    # add existing entry
-    with raises(ValueError, match="Entry Temperature already exists"):
-        sofa.add_variable("Temperature", 25.1, "double", "MI")
-    # entry violating the naming convention
-    with raises(ValueError, match="underscores '_' in the name"):
-        sofa.add_variable("Temperature_Celsius", 25.1, "double", "MI")
-    with raises(ValueError, match="The name of Data"):
-        sofa.add_attribute("Data_Time_measured", "midnight")
-    # entry with wrong type
-    with raises(ValueError, match="dtype is float but must be"):
-        sofa.add_variable("TemperatureCelsius", 25.1, "float", "MI")
-    # variable without dimensions
-    with raises(ValueError, match="dimensions must be provided"):
-        sofa.add_variable("TemperatureCelsius", 25.1, "double", None)
-    # invalid dimensins
-    with pytest.warns(UserWarning, match="Added custom dimension T"):
-        sofa.add_variable("TemperatureCelsius", [25.1, 25.2], "double", "T")
-    # attribute with missing variable
-    with raises(ValueError, match="Adding Attribute Variable"):
-        sofa.add_attribute("Variable_Unit", "Celsius")
-
-
-def test_delete_entry():
-
-    sofa = sf.Sofa("SimpleHeadphoneIR")
-    assert hasattr(sofa, "GLOBAL_History")
-    assert hasattr(sofa, "SourceManufacturer")
-    # delete one optional attribute and variable
-    sofa.delete("GLOBAL_History")
-    sofa.delete("SourceManufacturer")
-    # check if data were removed
-    assert not hasattr(sofa, "GLOBAL_History")
-    assert not hasattr(sofa, "SourceManufacturer")
-
-
-def test_get_size_and_shape_of_string_var():
-
-    # test with string
-    S, shape = sf.Sofa._get_size_and_shape_of_string_var("four", "key")
-    assert S == 4
-    assert shape == (1, 1)
-
-    # test with single string list
-    S, shape = sf.Sofa._get_size_and_shape_of_string_var(["four"], "key")
-    assert S == 4
-    assert shape == (1, )
-
-    # test with list of strings
-    S, shape = sf.Sofa._get_size_and_shape_of_string_var(
-        ["four", "fivee"], "key")
-    assert S == 5
-    assert shape == (2, )
-
-    # test with numpy strings array
-    S, shape = sf.Sofa._get_size_and_shape_of_string_var(
-        np.array(["four", "fivee"], dtype="S256"), "key")
-    assert S == 5
-    assert shape == (2, )
-
-    # test with wrong type
-    with raises(TypeError, match="key must be a string"):
-        sf.Sofa._get_size_and_shape_of_string_var(1, "key")
-
-
-def test_format_value_for_netcdf():
-
-    # string and None dimensions (a.k.a NETCDF attribute)
-    value, dtype = _format_value_for_netcdf(
-        "string", "test_attr", "attribute", None, 12)
-    assert value == "string"
-    assert dtype == "attribute"
-
-    # int that should be converted to a string
-    value, dtype = _format_value_for_netcdf(
-        1, "test_attr", "attribute", None, 12)
-    assert value == "1"
-    assert dtype == "attribute"
-
-    # float that should be converted to a string
-    value, dtype = _format_value_for_netcdf(
-        0.2, "test_attr", "attribute", None, 12)
-    assert value == "0.2"
-    assert dtype == "attribute"
-
-    # string and IS dimensions
-    value, dtype = _format_value_for_netcdf(
-        "string", "TestVar", "string", "IS", 12)
-    assert value == np.array("string", dtype="S12")
-    assert dtype == "S1"
-    assert value.ndim == 2
-
-    # single entry array and none Dimensions
-    value, dtype = _format_value_for_netcdf(
-        ["string"], "TestVar", "string", "IS", 12)
-    assert value == np.array(["string"], dtype="S12")
-    assert dtype == "S1"
-    assert value.ndim == 2
-
-    # array of strings
-    value, dtype = _format_value_for_netcdf(
-        [["a"], ["bc"]], "TestVar", "string", "MS", 12)
-    assert all(value == np.array([["a"], ["bc"]], "S12"))
-    assert dtype == "S1"
-    assert value.ndim == 2
-
-    # test with list
-    value, dtype = _format_value_for_netcdf(
-        [0, 0], "TestVar", "double", "MR", 12)
-    npt.assert_allclose(value, np.array([0, 0])[np.newaxis, ])
-    assert dtype == "f8"
-    assert value.ndim == 2
-
-    # test with numpy array
-    value, dtype = _format_value_for_netcdf(
-        np.array([0, 0]), "TestVar", "double", "MR", 12)
-    npt.assert_allclose(value, np.array([0, 0])[np.newaxis, ])
-    assert dtype == "f8"
-    assert value.ndim == 2
-
-    # unkonw data type
-    with raises(ValueError, match="Unknown type int for TestVar"):
-        value, dtype = _format_value_for_netcdf(1, "TestVar", "int", "MR", 12)
-
-
-def test_format_value_from_netcdf():
-
-    # single string (emulate NetCDF binary format)
-    value_in = np.array(["s", "t", "r"], dtype="S1")
-    value = _format_value_from_netcdf(value_in, "Some_Attribute")
-    assert value == "str"
-
-    # array of strings (emulate NetCDF binary format)
-    value_in = np.array([["s", "t", "r", "1"], ["s", "t", "r", "2"]],
-                        dtype="S1")
-    value = _format_value_from_netcdf(value_in, "Some_Attribute")
-    assert all(value == np.array(["str1", "str2"], dtype="U"))
-
-    # single string (emulate NetCDF ascii encoding)
-    value_in = np.array(["str"], dtype="U")
-    value = _format_value_from_netcdf(value_in, "Some_Attribute")
-    assert value == "str"
-
-    # array of strings (emulate NetCDF binary format)
-    value_in = np.array(["str1", "str2"], dtype="U")
-    value = _format_value_from_netcdf(value_in, "Some_Attribute")
-    assert all(value == np.array(["str1", "str2"], dtype="U"))
-
-    # numerical array that can be scalar
-    value = _format_value_from_netcdf(
-        np.array([44100], dtype="float"), "Data_SamplingRate")
-    assert value == 44100.
-
-    # numerical array that can not be scalar
-    value = _format_value_from_netcdf(
-        np.array([44100], dtype="float"), "Data_IR")
-    assert value == np.array(44100., dtype="float")
-
-    # masked array with missing data
-    array = np.ma.masked_array([1, 2], mask=[0, 1], dtype="float")
-    with pytest.warns(UserWarning, match="Entry Data_IR contains missing"):
-        value = _format_value_from_netcdf(array, "Data_IR")
-    npt.assert_allclose(value, array)
-
-    # test with invalid data dtype
-    with raises(TypeError, match="Data_IR: value.dtype is complex"):
-        _format_value_from_netcdf(
-            np.array([44100], dtype="complex"), "Data_IR")
-
-
-def test_is_mandatory():
-    assert sf.Sofa._mandatory("rm")
-    assert not sf.Sofa._mandatory("r")
-    assert not sf.Sofa._mandatory(None)
-
-
-def test_is_readonly():
-    assert sf.Sofa._read_only("rm")
-    assert not sf.Sofa._read_only("m")
-    assert not sf.Sofa._read_only(None)
-
-
-def test_verify_convention_and_version():
-
-    # test different possibilities for version
-    version = _verify_convention_and_version("latest", "1.0", "GeneralTF")
-    assert version == "2.0"
-
-    version = _verify_convention_and_version("2.0", "1.0", "GeneralTF")
-    assert version == "2.0"
-
-    version = _verify_convention_and_version("match", "1.0", "GeneralTF")
-    assert version == "1.0"
-
-    # test assertions
-    with raises(ValueError, match="Convention 'Funky' does not exist"):
-        _verify_convention_and_version("latest", "1.0", "Funky")
-    with raises(ValueError, match="Version 1.1 does not exist"):
-        _verify_convention_and_version("match", "1.1", "GeneralTF")
-    with raises(ValueError, match="Version 1.2 does not exist"):
-        _verify_convention_and_version("1.2", "1.0", "GeneralTF")
-
-
-def test_atleast_nd():
-    # test with single dimension array
-    for ndim in range(1, 6):
-        array = _atleast_nd(1, ndim)
-        assert array.ndim == ndim
-        assert array.flatten() == np.array([1])
-
-    # test with two-dimensional array
-    for ndim in range(1, 6):
-        array = _atleast_nd(np.atleast_2d(1), ndim)
-        assert array.ndim == max(2, ndim)
-        assert array.flatten() == np.array([1])
-
-
-def test_nd_newaxis():
-    assert _nd_newaxis([1, 2, 3, 4, 5, 6], 2).shape == (6, 1)
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 00000000..c36fe655
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,227 @@
+import shutil
+import sofar as sf
+from sofar.utils import _get_conventions
+from sofar.update_conventions import _compile_conventions
+import os
+import json
+from tempfile import TemporaryDirectory
+import pytest
+from pytest import raises
+import numpy as np
+from copy import deepcopy
+
+
+def test_list_conventions(capfd):
+
+    # check output to console using pytest default fixture capfd
+    sf.list_conventions()
+    out, _ = capfd.readouterr()
+    assert "Available SOFA conventions:" in out
+
+
+def test__get_conventions(capfd):
+
+    # check the return type
+    paths = _get_conventions(return_type="path")
+    assert isinstance(paths, list)
+    assert os.path.isfile(paths[0])
+    assert paths[0].endswith(".json")
+    out, _ = capfd.readouterr()
+    assert out == ""
+
+    paths = _get_conventions(return_type="path_source")
+    assert isinstance(paths, list)
+    assert os.path.isfile(paths[0])
+    assert paths[0].endswith(".csv")
+    out, _ = capfd.readouterr()
+    assert out == ""
+
+    names = _get_conventions(return_type="name")
+    assert isinstance(names, list)
+    assert not os.path.isfile(names[0])
+
+    names_versions = _get_conventions(return_type="name_version")
+    assert isinstance(names_versions, list)
+    assert isinstance(names_versions[0], tuple)
+
+    with raises(ValueError, match="return_type None is invalid"):
+        _get_conventions(return_type="None")
+
+
+def test_update_conventions(capfd):
+
+    # create temporary directory and copy existing conventions
+    temp_dir = TemporaryDirectory()
+    work_dir = os.path.join(temp_dir.name, "sofa_conventions", "conventions")
+    shutil.copytree(
+        os.path.join(
+            os.path.dirname(__file__), "..", "sofar", "sofa_conventions"),
+        os.path.join(temp_dir.name, "sofa_conventions"))
+
+    # delete standardized GeneralTF_2.0 to test adding
+    os.remove(os.path.join(work_dir, "GeneralTF_2.0.csv"))
+    os.remove(os.path.join(work_dir, "GeneralTF_2.0.json"))
+    # modify standardized GeneralFIR_1.0 to test updating
+    with open(os.path.join(work_dir, "GeneralFIR_1.0.csv"), "w") as fid:
+        fid.write("modified")
+    # move MultiSpeakerBRIR_0.3 to standardized to test deprecation
+    os.rename(
+        os.path.join(work_dir, "deprecated", "MultiSpeakerBRIR_0.3.csv"),
+        os.path.join(work_dir, "MultiSpeakerBRIR_0.3.csv"))
+    os.rename(
+        os.path.join(work_dir, "deprecated", "MultiSpeakerBRIR_0.3.json"),
+        os.path.join(work_dir, "MultiSpeakerBRIR_0.3.json"))
+    # modify deprecated convention to test updating
+    with open(os.path.join(work_dir, "deprecated",
+                           "SimpleFreeFieldHRIR_0.4.csv"), "w") as fid:
+        fid.write("modified")
+    # delete deprecated GeneralTF_2.0 to test adding
+    os.remove(os.path.join(
+        work_dir, "deprecated", "SimpleFreeFieldTF_0.4.csv"))
+    os.remove(os.path.join(
+        work_dir, "deprecated", "SimpleFreeFieldTF_0.4.json"))
+
+    # first run to test if conventions were updated
+    sf.update_conventions(conventions_path=work_dir, assume_yes=True)
+    out, _ = capfd.readouterr()
+    assert "added convention: GeneralTF_2.0" in out
+    assert "updated convention: GeneralFIR_1.0" in out
+    assert "deprecated convention: MultiSpeakerBRIR_0.3" in out
+    assert not os.path.isfile(
+        os.path.join(work_dir, "MultiSpeakerBRIR_0.3.csv"))
+    assert not os.path.isfile(
+        os.path.join(work_dir, "MultiSpeakerBRIR_0.3.json"))
+    assert "updated deprecated convention: SimpleFreeFieldHRIR_0.4" in out
+    assert "added deprecated convention: SimpleFreeFieldTF_0.4" in out
+
+    # second run to make sure that up to date conventions are not overwritten
+    sf.update_conventions(conventions_path=work_dir, assume_yes=True)
+    out, _ = capfd.readouterr()
+    assert "added" not in out
+    assert "updated" not in out
+    assert "deprecated" not in out
+
+
+def test__compile_conventions():
+    """Test compiling the json conventions from the csv files."""
+
+    # create temporary directory and copy existing source conventions
+    temp_dir = TemporaryDirectory()
+    shutil.copytree(
+        os.path.join(os.path.dirname(__file__), "..", "sofar",
+                     "sofa_conventions", "conventions"),
+        os.path.join(temp_dir.name, "conventions"))
+
+    # compile conventions
+    _compile_conventions(os.path.join(temp_dir.name, "conventions"))
+
+    # get list of reference json files
+    paths_ref = _get_conventions("path")
+    paths_test = _get_conventions(
+        "path", os.path.join(temp_dir.name, "conventions"))
+
+    for path_ref, path_test in zip(paths_ref, paths_test):
+
+        # load reference conventions
+        with open(path_ref, "r") as file:
+            ref_data = json.load(file)
+
+        with open(path_test, "r") as file:
+            test_data = json.load(file)
+
+        # compare conventions
+        assert ref_data == test_data
+
+
+def test_equals_global_parameters():
+
+    sofa_a = sf.Sofa("SimpleFreeFieldHRIR")
+
+    # check invalid
+    with raises(ValueError, match="exclude is"):
+        sf.equals(sofa_a, sofa_a, exclude="wrong")
+
+    # check identical objects
+    assert sf.equals(sofa_a, sofa_a)
+
+    # check different number of keys
+    sofa_b = deepcopy(sofa_a)
+    sofa_b._protected = False
+    delattr(sofa_b, "ReceiverPosition")
+    sofa_b._protected = True
+    with pytest.warns(UserWarning, match="not identical: sofa_a has"):
+        is_identical = sf.equals(sofa_a, sofa_b)
+        assert not is_identical
+
+    # check different keys
+    sofa_b = deepcopy(sofa_a)
+    sofa_b._protected = False
+    sofa_b.PositionReceiver = sofa_b.ReceiverPosition
+    delattr(sofa_b, "ReceiverPosition")
+    sofa_b._protected = True
+    with pytest.warns(UserWarning, match="not identical: sofa_a and sofa_b"):
+        is_identical = sf.equals(sofa_a, sofa_b)
+        assert not is_identical
+
+    # check mismatching data types
+    sofa_b = deepcopy(sofa_a)
+    sofa_b._protected = False
+    sofa_b._convention["ReceiverPosition"]["type"] = "int"
+    sofa_b._protected = True
+    with pytest.warns(UserWarning, match="not identical: ReceiverPosition"):
+        is_identical = sf.equals(sofa_a, sofa_b)
+        assert not is_identical
+
+    # check exclude GLOBAL attributes
+    sofa_b = deepcopy(sofa_a)
+    sofa_b._protected = False
+    delattr(sofa_b, "GLOBAL_Version")
+    sofa_b._protected = True
+    is_identical = sf.equals(sofa_a, sofa_b, exclude="GLOBAL")
+    assert is_identical
+
+    # check exclude Date attributes
+    sofa_b = deepcopy(sofa_a)
+    sofa_b._protected = False
+    delattr(sofa_b, "GLOBAL_DateModified")
+    sofa_b._protected = True
+    is_identical = sf.equals(sofa_a, sofa_b, exclude="DATE")
+    assert is_identical
+
+    # check exclude Date attributes
+    sofa_b = deepcopy(sofa_a)
+    sofa_b._protected = False
+    delattr(sofa_b, "GLOBAL_DateModified")
+    sofa_b._protected = True
+    is_identical = sf.equals(sofa_a, sofa_b, exclude="ATTR")
+    assert is_identical
+
+
+@pytest.mark.parametrize("value_a, value_b, attribute, fails", [
+    ("1", "2", "GLOBAL_SOFAConventionsVersion", True),
+    ([[1, 2]], [1, 2], "Data_IR", False),
+    ([[1, 2]], [1, 3], "Data_IR", True),
+    ("HD 650", ["HD 650"], "SourceModel", False),
+    ("HD 650", np.array(["HD 650"], dtype="U"), "SourceModel", False),
+    ("HD 650", np.array(["HD 650"], dtype="S"), "SourceModel", False),
+    ("HD 650", "HD-650", "SourceModel", True)
+])
+def test_equals_attribute_values(value_a, value_b, attribute, fails):
+
+    # generate SOFA objects (SimpleHeadphoneIR has string variables)
+    sofa_a = sf.Sofa("SimpleHeadphoneIR")
+    sofa_a._protected = False
+    sofa_b = deepcopy(sofa_a)
+
+    # set parameters
+    setattr(sofa_a, attribute, value_a)
+    sofa_a._protected = True
+    setattr(sofa_b, attribute, value_b)
+    sofa_b._protected = True
+
+    # compare
+    if fails:
+        with pytest.warns(UserWarning):
+            assert not sf.equals(sofa_a, sofa_b)
+    else:
+        assert sf.equals(sofa_a, sofa_b)