diff --git a/.gitignore b/.gitignore index cdbcc06..5221220 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,8 @@ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ +__pycache__/ + # Celery stuff celerybeat-schedule celerybeat.pid diff --git a/MANIFEST.in b/MANIFEST.in index 454970a..721dafd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include src/strauss/presets/*/*.yml -include src/strauss/presets/*/*/*.yml \ No newline at end of file +include src/strauss/presets/*/*/*.yml +include src/strauss/data/*.csv \ No newline at end of file diff --git a/README.md b/README.md index 94f738f..f062ccb 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,20 @@ ## Getting Started -Access the [full documentation here](https://strauss.readthedocs.io/) *(under construction!)* and read more about the associated [Audio Universe project here](https://www.audiouniverse.org/). +Access the [full documentation here](https://strauss.readthedocs.io/) and read more about the associated [Audio Universe project here](https://www.audiouniverse.org/). -*STRAUSS* is [PyPI hosted package](https://pypi.org/project/strauss/) and can be installed directly via `pip`: +*STRAUSS* is [PyPI hosted package](https://pypi.org/project/strauss/) and `pip` can be used for the default installation: -`pip install strauss` +`pip install 'strauss[default]'` For a standard install (without text-to speech support). +For development purposes, you can instead use: + +`pip install -e .` + +where the `-e` option allows a local install, such that you can modify and run the source code on the fly without needing to reinstall each time. + If you would like access to all the resources and explore the code directly, make a copy of the *STRAUSS* repository via SSH, `git clone git@github.com:james-trayford/strauss.git strauss` @@ -31,12 +37,6 @@ and install *STRAUSS* from your local repository using `pip` `pip install .` -For development purposes, you can instead use: - -`pip install -e .` - -where the `-e` option allows a local install, such that you can modify and run the source code on the fly without needing to reinstall each time. - We recommend using a conda environment to avoid package conflicts. Type `conda env create -f environment.yml` @@ -51,7 +51,7 @@ and activate the environment with *STRAUSS* can also be installed with text-to-speech (TTS) support, allowing audio captioning of sonifications and future accessibility features, via the [TTS module](https://github.com/coqui-ai/TTS). Due to the specific module requirements of this module, install can sometimes lead to incompatibilities with other modules and be slower, so is packaged with *STRAUSS* as an optional extra. If you'd like to use these features, its easy to directly from PyPI: -`pip install strauss[TTS]` +`pip install 'strauss[TTS]'` or if you're working from a local copy of the repository, as above, use diff --git a/docs/conf.py b/docs/conf.py index 0b88976..f4846fd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,7 +13,7 @@ import os import sys sys.path.insert(0, os.path.abspath('..')) - +sys.path.insert(0, os.path.abspath('../../src/strauss/presets')) # -- Project information ----------------------------------------------------- @@ -35,8 +35,12 @@ 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinx.ext.inheritance_diagram', 'sphinx.ext.autosummary','sphinx.ext.coverage', - 'sphinx.ext.napoleon'] + 'sphinx.ext.napoleon', "myst_parser", + "sphinx_exec_code"]#, 'sphinxcontrib.jquery'] +html_js_files = [ + 'js/custom.js' +] print(extensions) @@ -55,12 +59,16 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = 'nature' +# html_theme_path = [sphinx_pdj_theme.get_html_theme_path()] # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] -# enable markdown -#extensions.append('myst_parser') +myst_footnote_transition = False + +# html_theme_options = { +# 'page_width': '1200px' +# } diff --git a/docs/detailed.rst b/docs/detailed.rst index c3d20c4..42e08f0 100644 --- a/docs/detailed.rst +++ b/docs/detailed.rst @@ -1,32 +1,71 @@ .. _detailed: Detailed Documentation -^^^^^^^^^^^^^^^^^^^^^^ +###################### .. automodule:: strauss :imported-members: +User-Facing Modules +******************* + +These are the user-facing modules of the ``strauss`` code, that are used to define a ``Sonification``. + + Sources -******* +======= .. automodule:: strauss.sources :members: - + :show-inheritance: Score -***** +===== .. automodule:: strauss.score :members: - + :show-inheritance: Generator -********* +========= .. automodule:: strauss.generator :members: + :show-inheritance: Channels -******** +======== .. automodule:: strauss.channels :members: - + :show-inheritance: Sonification -************ +============ .. automodule:: strauss.sonification :members: + :show-inheritance: + +Ancillary Modules +***************** + +These are modules intended for internal use by the ``strauss`` module + +Utilities +========= + .. automodule:: strauss.utilities + :members: + :show-inheritance: +Stream +====== + .. automodule:: strauss.stream + :members: + :show-inheritance: +Notes +===== + .. automodule:: strauss.notes + :members: + :show-inheritance: +Filters +======= + .. automodule:: strauss.filters + :members: + :show-inheritance: +Text-to-Speech +============== + .. automodule:: strauss.tts_caption + :members: + :show-inheritance: diff --git a/docs/elements.rst b/docs/elements.rst index c9a407c..1d9e1ba 100644 --- a/docs/elements.rst +++ b/docs/elements.rst @@ -23,7 +23,7 @@ Source Class The :code:`Source` classes in Strauss are so named because they act as `sources of sound` in the sonification. Sources are used to represent the input data, by mapping this data to properties of sound (volume, position, frequency, etc). The choice of how to set up the sources depends on the data being sonified, and what the user wants to convey. For this, Strauss defines two generic classes that inherit the parent :code:`Source` class; :code:`Events` and :code:`Objects`, described below. -**Full documentation of the mappable parameters are coming soon!** See :obj:`./examples` for example mappings in jupyter notebooks. +**Full documentation of the mappable parameters is coming soon!** See :obj:`./examples` for example mappings in Jupyter Notebooks and Python scripts. `Events` '''''''' @@ -46,3 +46,46 @@ In this case, sound is produced continuously by the source, with the evolving pr Some examples of :code:`Objects` in scientific data could be; `A galaxy evolving, planets orbiting, a plant growing, a glacier flowing, a climate changing etc...` .. _score: + +Score Class +*********** + +With the audio :code:`Sources` defined, the :code:`Score` class allows us to place ‘musical’ constraints on the sound they produce to represent the underlying data. The duration of the output sonification is also specified via the Score with the timeline of the sonification scaled to fit this duration. + +.. _generator: + +Generator Class +*************** + +The :code:`Generator` class takes instruction from the two prior classes and generates audio for each individual source. This can be achieved using either the :code:`Sampler` or :code:`Synthesiser` child classes (along with the :code:`Spectraliser` special case), detailed below. + +`Sampler` +''''''''' + +This class generates audio by triggering **pre-recorded audio samples**. + +A directory of audio files is used to specify which sample to use for each note of the sampler. These samples are loaded into the sampler and are interpolated to allow arbitrary pitch shifting. Samples can also be looped in a number of ways to allow notes to sustain perpetually. + +`Synthesiser` +''''''''''''' + +This class instead **generates audio additively using mathematical functions** via an arbitrary number of oscillators. The strauss synthesiser supports a number of oscillator forms. + +`Spectraliser` +'''''''''''''' + +A special case of the :code:`Synthesiser`, this generator synthesises sound from an input spectrum, via an inverse Fast Fourier Transform (IFFT), with randomised phases. The user can specify the audible frequency range that the ‘spectralised’ audio is mapped over. + +.. _channels: + +Channels Class +************** + +Once sound has been produced for each :code: `source`, the final step is to mix the audio down into some multi-channel audio format. The :code:`Channels` class essentially represents a bank of virtual microphones, with 3D antennae patterns, that each correspond to a channel in the output file. + +.. _sonification: + +Sonification Class +****************** + +The top-level :code:`Sonification` class loads in all the above classes and produces the final sonification. Once :code:`Sources`, :code:`Score`, :code:`Generator` and :code:`Channels` classes are defined, the :code:`Sonification` class is invoked. The :code:`render()` method can then be run to produce the sonification. diff --git a/docs/examples.rst b/docs/examples.rst index b34a7a5..2ab7f6c 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -4,11 +4,48 @@ Examples ^^^^^^^^ -Here, we explain some of the example sonfications included in the :code:`examples/` directory of the Strauss repo. These are all in *Python Notebook* (:code:`.ipynb`) format. +Here, we explain some of the example sonfications included in the :code:`examples/` directory of the Strauss repo. These are all in both *Python Notebook* (:code:`.ipynb`) and *Python script* (:code:`.py`) format so you can choose which format you prefer to use. + +Audio Caption (:code:`AudioCaption.ipynb`) +****************************************** +The *Audio Caption* example demonstrates how to add audio captions to a sonification, using a text-to-speech (TTS) module. The TTS module is not included in the standard Strauss installation, but it can be installed by using pip install strauss[TTS]. This example uses the Strauss :code:`Sampler` to play a short sequence of glockenspiel notes, then generates an audio caption using a standard TTS voice. The notebook allows the user to try different voices and languages from TTS. + + +Day Sequence (:code:`DaySequence.ipynb`) +**************************************** +The *Day Sequence* sonification generates the sunrise to sunset sonification used in the `"Audio Universe: Tour of the Solar System" `_, an immersive planetarium show designed with sonifications so it can be enjoyed and understood irrespective of level of vision. Samples are downloaded from a Google drive to a local directory, and are played using the Strauss :code:`Sampler`. This spatialisation can be mapped to any audio setup, and a :code:`5.1` system was used for the planetarium, but for the example we use :code:`stereo`. + + +Earth System (:code:`EarthSystem.ipynb`) +**************************************** +The *Earth System* sonification represents the ratio of ocean to land along lines of longitude as the Earth spins through three rotation cycles. We use the Strauss :code:`Synthesiser` to generate chords. A low pass filter is employed to generate a brighter sound to represent a high water fraction and a duller sound for high land fraction. + +A video of this sequence is available starting at 2:17 in `this video `_. + +Light Curve Soundfonts (:code:`LightCurveSoundfonts.ipynb`) +************************************************************ +The *Light Curve Soundfonts* example demonstrates how to use imported soundfont files to use virtual musical instruments in a sonification. Soundfont files are widely available online. We download flute and guitar sounds from `Soundfonts 4 U `_. We load the sounds into the Strauss :code:`Sampler`, and select a preset. The soundfonts are used to sonify a light curve for the variable star 55 Cancri, creating the audio equivalent of a scatter plot. The note length and volume envelope can be adjusted to improve the articulation of individual data points. To demonstrate an alternative approach, we use an :code: `Object` source type, where we evolve a sound over time to represent the data. This is analogous to a line graph, representing a continuous data series. We use a held chord, changing the cutoff frequency of the low-pass filter to create a "brighter" timbre when the star is brighter and a "duller" sound when the star is darker. + + +Planetary Orbits (:code:`PlanetaryOrbits.ipynb`) +************************************************ +The *Planetary Orbits* example generates sonifications used in the "Audio Universe: Tour of the Solar System" planetarium show. Each planet in the Solar System is assigned a unique note, with the smallest planets having the highest notes and the largest planets having the lowest notes. The sonification length for each is set according to the planet's orbital period, and the volume is varied by orbital azimuth. The audio system is set as 'stereo' by default but for the planetarium '5.1' is used. This creates the effect of the planets moving in orbits at relative speeds to represent the real relative motions. + +A video of this sequence with the audio is available `here `_. + +Sonifying Data 1D (:code:`SonifyingData1D.ipynb`) +************************************************* +The *Sonifying Data 1D* example demonstrates some generic techniques for sonifying one-dimensional data series. We construct some mock data with features and noise. For all examples we use the Strauss :code:`Synthesiser` to create a 30 second, mono sonification. We demonstrate a variety of ways to map y as a function of x, using the change in some expressive property of sound (e.g. pitch_shift, volume and filter-cutoff) as a function of time. + + +Spectral Data (:code:`SpectralData.ipynb`) +****************************************** +The *Spectral Data* sonification demonstrates use of the Strauss :code:`Spectraliser` to represent data. We use a direct spectralisation approach where the sound is generated by treating the 1D data as a sound spectrum. This uses a direct inverse Fourier transform, which is relatively intuitive for spectral data, especially where the spectral features are similar to those that can be identified in sound. We use Planetary Nebulae data, objects dominated by strong emission lines, to demonstrate this. We plot the spectra vs wavelength and spectra vs frequency, and use the Strauss :code:`Synthesiser` to create a 30 second, mono sonification. We set the ranges for the mapped parameters and render the sonification. A second example uses an "Object" type sonification with an evolving Spectrum to sonify an image. We represent the image by evolving from left to right, with higher features in the y-axis having a higher pitch. + Stars Appearing (:code:`StarsAppearing.ipynb`) ********************************************** -The *Stars Appearing* sonification demonstrates the generation of a sonification that was used directly in `a planetarium show for visually impaired children `_. This is intended to represent the appearance of stars in the night sky to an observer. Over time, the sky darkens and our eyes adjust, allowing us to see more and more stars. To represent this, the brightest stars appear first, with dimmer stars appearing later. Data on the colours of the stars is used to set the note used for each stars sound, with bluer stars higher notes and redder stars lower notes. We use the Strauss :code:`Sampler` to play a glockenspiel sound for each star as it appears. The actual positions of stars in the sky is used to spatialise the audio, with westerly stars positioned in the right speaker and easterly stars in the left. This spatialisation can be mapped to any audio setup, and a :code:`5.1` system was used fo the planetarium, but for the example we use :code:`stereo` +The *Stars Appearing* sonification demonstrates the generation of a sonification that was used directly in the "Audio Universe: Tour of the Solar System" planetarium show. This is intended to represent the appearance of stars in the night sky to an observer. Over time, the sky darkens and our eyes adjust, allowing us to see more and more stars. To represent this, the brightest stars appear first, with dimmer stars appearing later. Data on the colours of the stars is used to set the note used for each stars sound, with bluer stars having higher notes and redder stars having lower notes. We use the Strauss :code:`Sampler` to play a glockenspiel sound for each star as it appears. The actual positions of stars in the sky is used to spatialise the audio, with westerly stars positioned in the right speaker and easterly stars in the left. This spatialisation can be mapped to any audio setup, and a :code:`5.1` system was used for the planetarium, but for the example we use :code:`stereo`. A video of this sequence with the audio is available `here `_. diff --git a/docs/index.rst b/docs/index.rst index d569c87..5fbe803 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,5 @@ -.. strauss documentation master file, created by +.. strauss + documentation master file, created by sphinx-quickstart on Tue Oct 26 14:56:02 2021. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. @@ -8,7 +9,7 @@ Welcome to the STRAUSS documentation! Strauss is a python toolkit for data *"sonification"* - the representation of data using sound - with both scientific and outreach applications. -The code aims to make rich and evocative sonification straightforward, with a number of presets and examples enabling a quick start. At the same time, it is intended to be flexible enough to allow high level of control over the sonification and various expressive elements of sound and harmony if required. You can read about the associated `Audio Universe project here `_ for outreach and examples using Strauss. +The code aims to make rich and evocative sonification straightforward, with a number of presets and examples enabling a quick start. At the same time, it is intended to be flexible enough to allow high level of control over the sonification and various expressive elements of sound and harmony if required. The project is described in more detail in the paper `Introducing STRAUSS: a flexible sonification Python package `_, presented at the 28th Proceedings of the `International Community of Auditory Displays (2023) `_. You can read about the associated `Audio Universe project here `_ for examples of using Strauss for a variety of applications. .. note:: Strauss and its documentation are currently in development, with more details and features coming soon. Look out for / follow the repo our first numbered release, and accompanying article! @@ -19,10 +20,11 @@ The code aims to make rich and evocative sonification straightforward, with a nu motivation start elements + params detailed examples todo - + Indices and tables ================== diff --git a/docs/js/custom.js b/docs/js/custom.js new file mode 100644 index 0000000..184b5ae --- /dev/null +++ b/docs/js/custom.js @@ -0,0 +1,3 @@ +$(document).ready(function () { + $('a.external').attr('target', '_blank'); +}); diff --git a/docs/motivation.rst b/docs/motivation.rst index 56c9895..9084128 100644 --- a/docs/motivation.rst +++ b/docs/motivation.rst @@ -8,21 +8,19 @@ Why sonification? So, given the dominance of visualisation techniques, *why use sonification?* Here are three motivating reasons: -**1) Accessibility**: For many people, visualisation is inherently inaccessible. Around 4% of the world population are visually impaired (VI) according to `IAPB statistics `_ [1]. Combining visualisation and sonification techniques ensures broader accessibility of science and data presentation. - -**2) Unique Applications**: There are applications for which sonification is uniquely suited. Sound is a time varying signal so can represent *time-series data* intuitively, or data with rapidly or subtly varying rates of change. The human ear can cover about 10 octaves in frequency, while the visible range of light only covers around 1 octave. Used well, sonification can provide a new perspective on your data, and potentially new insights into your data that are missed with standard, visual approaches. - -**3) Enhancing Visuals**: for scientific images or video data without narration, typically nothing is being conveyed through sound. Combining sonification with your visualisation provides a new channel for conveying data, either enhancing what is already shown, or expressing new variables that are absent from the visual. For communicating to a broader audience, sonification can reveal the beauty and complexity in data, and is active area of interest `in academic music `_. +**1) Enhancing Visuals**: for scientific images or video data without narration, typically nothing is being conveyed through sound. Combining sonification with your visualisation provides a new channel for conveying data, either enhancing what is already shown, or expressing new variables that are absent from the visual. For communicating to a broader audience, sonification can reveal the beauty and complexity in data, and is active area of interest `in academic music `_. +**2) Unique Applications**: There are applications for which sonification is uniquely suited. Sound is a time varying signal so can represent *time-series data* intuitively, or data with rapidly or subtly varying rates of change. The human ear can cover about 10 octaves in frequency, while the visible range of light only covers around 1 octave. Used well, sonification can provide a new perspective on your data, and potentially new insights into your data that are missed with standard, visual approaches. + +**3) Accessibility**: For many people, visualisation is inherently inaccessible. Around 4% of the world population are visually impaired (VI) according to `IAPB statistics `_ [1]. Combining visualisation and sonification techniques ensures broader accessibility of science and data presentation. Strauss approach **************** -Strauss is intended to be a flexible toolkit and engine for sonification, allowing detailed control over the sonification process if required. Casual users can run Strauss with preset parameters and setups, exemplified by the python notebook examples (see :ref:`examples`), while more technical users can experiment with every aspect of the sonification (see :ref:`elements`). +Strauss is intended to be a flexible toolkit and engine for sonification, allowing detailed control over the sonification process if required. Casual users can run Strauss with preset parameters and setups, demonstrate by the Python Notebook and script examples (see :ref:`examples`), while more technical users can experiment with every aspect of the sonification (see :ref:`elements`). -Strauss incorporates *musical* elements, with the idea that *music theory* provides a system for organising abstract sounds and conveying meaning through them. These systems have -e been developed over centuries, and we are often encultured to them. Incorporating these systems allows us to exploit our understanding of music to convey information and create better sounding sonifications. No knowledge of music theory is required to use Strauss, and presets can again be used fror all musical aspects (see :ref:`score`). +Strauss incorporates *musical* elements, with the idea that *music theory* provides a system for organising abstract sounds and conveying meaning through them. These systems have been developed over centuries, and we are often encultured to them. Incorporating these systems allows us to exploit our understanding of music to convey information and create better sounding sonifications. No knowledge of music theory is required to use Strauss, and presets can again be used fror all musical aspects (see :ref:`score`). -Strauss is currently limited to a *western* view of music theory, though with ongoing development and expertise, the aim is to incorporate more culturally diverse musical and tonal systems. +Strauss is currently limited to a *Western* view of music theory, though with ongoing development and expertise, the aim is to incorporate more culturally diverse musical and tonal systems. References ********** diff --git a/docs/params.rst b/docs/params.rst new file mode 100644 index 0000000..b9d8b48 --- /dev/null +++ b/docs/params.rst @@ -0,0 +1,10 @@ +.. _params: + +Parameter Reference +################### + +.. exec_code:: + :filename: yamls_to_tables.py + +.. include:: tables.md + :parser: myst_parser.sphinx_ diff --git a/docs/start.rst b/docs/start.rst index a448209..a10ef7e 100644 --- a/docs/start.rst +++ b/docs/start.rst @@ -1,26 +1,12 @@ Getting Started ^^^^^^^^^^^^^^^ -This walkthrough will take you through a clean install of the code, including optional dependencies and trying your first sonification +This walkthrough will take you through a clean install of the code, including optional dependencies and trying your first sonification. There are also example notebooks available on `Google Colab `_ which you can run without installing Strauss on your local system. Installation ************ -the Strauss code can be downloaded from **GitHub** at `the repository url `_ - -Using :code:`git` make a copy of the STRAUSS repository via SSH, - -.. code-block:: bash - - git clone git@github.com:james-trayford/strauss.git strauss - -or HTTPS if you don't have SSH keys set up, - -.. code-block:: bash - - git clone https://github.com/james-trayford/strauss.git strauss - -throughout the documentation, I will refer to this as the **strauss repo** or **code directory**. +Strauss can be installed in three different ways, depending on whether you want to develop the code or simply use it as it is. It can be installed using pip install, with or without the option for development, or you can clone it from the GitHub repository. if you just want to use the code, STRAUSS may then be installed using pip, as @@ -37,12 +23,28 @@ If you want to develop the code, you can instead use where the :code:`-e` option allows a local install, such that you can modify and run the source code on the fly without needing to reinstall each time. -Example jupyter notebooks -************************* +Alternatively, the Strauss code can be downloaded from **GitHub** at `the repository url `_ + +Using :code:`git` make a copy of the STRAUSS repository via SSH, + +.. code-block:: bash + + git clone git@github.com:james-trayford/strauss.git strauss + +or HTTPS if you don't have SSH keys set up, + +.. code-block:: bash + + git clone https://github.com/james-trayford/strauss.git strauss + +throughout the documentation, I will refer to this as the **strauss repo** or **code directory**. + +Example jupyter notebooks/scripts +********************************* -There are a number of example applications of Strauss in the :code:`example` subdirectory of the :code:`strauss` repo. These are in Python Notebook (:code:`.ipynb`) format for an interactive, step-by-step . +There are a number of example applications of Strauss in the :code:`example` subdirectory of the :code:`strauss` repo. These are in Python Notebook (:code:`.ipynb`) format for an interactive, step-by-step experience. They are also provided in Python script format (.py) in the :code:`examples` directory. The Python scripts can be run from the command line. -In order to run the exampes, first ensure that :code:`jupyter` is installed on your system. These were developed in :code:`jupyter-lab`, which can also be installed using pip, as. +In order to run the notebook examples, first ensure that :code:`jupyter` is installed on your system. These were developed in :code:`jupyter-lab`, which can also be installed using pip, as: .. code-block:: bash @@ -53,10 +55,10 @@ Then, running :code:`jupyter-lab` in the :code:`strauss` should initiate the :co Running some examples ********************* -From the :code:`jupyter-lab` interface, a good starting point is the :code:`SonifyingData1D.ipynb` notebook. demonstrating various method of representing a single 1D dataset sonically, using a single :code:`Object`-type source representation. The code and instruction cells provide a step-by-step gude to setting up, rendering and saving a sonification with Strauss. +From the :code:`jupyter-lab` interface, a good starting point is the :code:`SonifyingData1D.ipynb` Notebook. This demonstrates various methods of representing a single 1D dataset sonically, using a single :code:`Object`-type source representation. The code and instruction cells provide a step-by-step gude to setting up, rendering and saving a sonification with Strauss. -For a multivariate :code:`Event`-type sonification, the :code:`StarsAppearing.ipynb` notebook provides a step-by-step example, and demonstrates realistic stereo imaging for panoramic data. The output from this example was used in the `*Audible Universe* 2021 planetarium show `_. +For a multivariate :code:`Event`-type sonification, the :code:`StarsAppearing.ipynb` notebook provides a step-by-step example, and demonstrates realistic stereo imaging for panoramic data. The output from this example was used in the `"Audio Universe: Tour of the Solar System" 2021 planetarium show `_. -For a multivariate, multi-source example using an :code:`Object`-type source representation, see ... +For a multivariate, multi-source example using an :code:`Object`-type source representation, see the :code:`PlanetaryOrbits.ipynb` Notebook, the output of which was also used in the "Audio Universe: Tour of the Solar System" planetarium show. -In addition to the above-mentioned examples, there are a number of other notebooks, each representing the diverse applications and uses of the Strauss code to sonify data in different ways. A more detailed overview of the example notebooks can be found in :ref:`examples`. +In addition to the above-mentioned examples, there are a number of other Notebooks, each representing the diverse applications and uses of the Strauss code to sonify data in different ways. A more detailed overview of the example Notebooks and scripts can be found in :ref:`examples`. diff --git a/docs/yamls_to_tables.py b/docs/yamls_to_tables.py new file mode 100644 index 0000000..af67837 --- /dev/null +++ b/docs/yamls_to_tables.py @@ -0,0 +1,82 @@ +# --- hide: start --- +import yaml +import os +from glob import glob +from pathlib import Path + + +generators = {'spec' : "`Spectraliser` Generator", + 'synth' : "`Synthesiser` Generator", + 'sampler' : "`Sampler` Generator"} + +p = Path("src", "strauss", "presets", "*", "default.yml") + +def read_yaml(filename): + with filename.open(mode='r') as fdata: + # try: + yamldict = yaml.safe_load(fdata) + # except yaml.YAMLError as err: + # print(err) + return yamldict + +tstr1 = "\n| Parameter | Description | Default Value | Default Range | Unit |\n" +tstr2 = "| ----------- | ----------- | ----------- | ----------- | ----------- |\n" + +def yaml_traverse(metadict, valdict, rdict, headlev=1): + if hasattr(metadict, 'keys'): + starttab = 1 + topstr = '' + tabstr = '' + secstr = '' + + for k in metadict.keys(): + # print (f">>>>>> {k}") + if hasattr(metadict[k], 'keys'): + secstr += '\n'+''.join(['#']*headlev) + f" `{k}` parameter group\n" + if not k in rdict: + rdict[k] = {} + secstr += yaml_traverse(metadict[k], valdict[k], rdict[k], headlev+1) + continue + if k == '_doc': + topstr += f"\n{metadict[k]}\n" + # print('\n',metadict[k]) + continue + if k not in rdict: + # unspecified => '-' + rdict[k] = '-' + else: + # lets avoid these line-breaking with special characters + rdict[k] = str(rdict[k]).replace(" ","").replace(",","\u2011").replace("-","\u2011") + if k+"_unit" not in rdict: + # unspecified => '-' + rdict[k+"_unit"] = '-' + # print(f"|`{k}` | _{metadict[k]}_ | `{str(valdict[k]).strip()}` | `{rdict[k]}` | {rdict[k+'_unit']}") + if starttab: + tabstr = tstr1 + tstr2 + tabstr + starttab = 0 + tabstr += f"| `{k}` | {str(metadict[k]).strip()} | {valdict[k]} | `{rdict[k]}` | {rdict[k+'_unit']}\n" + if k not in rdict: + rdict[k] = {} + return f'{topstr}{tabstr}{secstr}' + + else: + return + +with open('docs/tables.md', 'w') as outfile: + l = len(glob(str(p))) + i = 0 + for f in glob(str(p)): + p = Path(f) + ydat = read_yaml(p) + rdat = read_yaml(p.parents[0] / "ranges" / "default.yml") + # print(f"\n# {generators[p.parents[0].name]}\n") + ystr = yaml_traverse(ydat['_meta'], ydat, rdat, 2) + # print(ystr) + # print('---') + outfile.write(f"\n# {generators[p.parents[0].name]}\n") + outfile.write(ystr) + if i < l-1: + outfile.write('---') + i += 1 + +# --- hide: stop --- diff --git a/paper.md b/paper.md new file mode 100644 index 0000000..3eeb87e --- /dev/null +++ b/paper.md @@ -0,0 +1,116 @@ +--- +title: 'STRAUSS: Sonification Tools & Resources for Analysis Using Sound Synthesis' +tags: + - Python + - sonification + - data inspection + - astronomy +authors: + - name: James W. Trayford + orcid: 0000-0003-1530-1634 + corresponding: true # (This is how to denote the corresponding author) + equal-contrib: false + affiliation: 1 # (Multiple affiliations must be quoted) + - name: Samantha Youles + equal-contrib: true # (This is how you can denote equal contributions between multiple authors) + affiliation: 1 + - name: Chris Harrison + affiliation: 2 + equal-contrib: true # (This is how you can denote equal contributions between multiple authors) +affiliations: + - name: Institute of Cosmology and Gravitation, University of Portsmouth, Dennis Sciama Building, Burnaby Road, Portsmouth PO1 3FX, UK + index: 1 + + - name: School of Mathematics, Statistics and Physics, Newcastle University, NE1 7RU, UK + index: 2 +date: 23 August 2024 +bibliography: paper.bib + +# Optional fields if submitting to a AAS journal too, see this blog post: +# https://blog.joss.theoj.org/2018/12/a-new-collaboration-with-aas-publishing +aas-doi: 10.3847/xxxxx <- update this with the DOI from AAS once you know it. +aas-journal: Astrophysical Journal <- The name of the AAS journal. +--- + +# Summary + +Sonification, + +The forces on stars, galaxies, and dark matter under external gravitational +fields lead to the dynamical evolution of structures in the universe. The orbits +of these bodies are therefore key to understanding the formation, history, and +future state of galaxies. The field of "galactic dynamics," which aims to model +the gravitating components of galaxies to study their structure and evolution, +is now well-established, commonly taught, and frequently used in astronomy. +Aside from toy problems and demonstrations, the majority of problems require +efficient numerical tools, many of which require the same base code (e.g., for +performing numerical orbit integration). + +# Statement of need + +`Gala` is an Astropy-affiliated Python package for galactic dynamics. Python +enables wrapping low-level languages (e.g., C) for speed without losing +flexibility or ease-of-use in the user-interface. The API for `Gala` was +designed to provide a class-based and user-friendly interface to fast (C or +Cython-optimized) implementations of common operations such as gravitational +potential and force evaluation, orbit integration, dynamical transformations, +and chaos indicators for nonlinear dynamics. `Gala` also relies heavily on and +interfaces well with the implementations of physical units and astronomical +coordinate systems in the `Astropy` package [@astropy] (`astropy.units` and +`astropy.coordinates`). + +`Gala` was designed to be used by both astronomical researchers and by +students in courses on gravitational dynamics or astronomy. It has already been +used in a number of scientific publications [@Pearson:2017] and has also been +used in graduate courses on Galactic dynamics to, e.g., provide interactive +visualizations of textbook material [@Binney:2008]. The combination of speed, +design, and support for Astropy functionality in `Gala` will enable exciting +scientific explorations of forthcoming data releases from the *Gaia* mission +[@gaia] by students and experts alike. + +# Mathematics + +Single dollars ($) are required for inline mathematics e.g. $f(x) = e^{\pi/x}$ + +Double dollars make self-standing equations: + +$$\Theta(x) = \left\{\begin{array}{l} +0\textrm{ if } x < 0\cr +1\textrm{ else} +\end{array}\right.$$ + +You can also use plain \LaTeX for equations +\begin{equation}\label{eq:fourier} +\hat f(\omega) = \int_{-\infty}^{\infty} f(x) e^{i\omega x} dx +\end{equation} +and refer to \autoref{eq:fourier} from text. + +# Citations + +Citations to entries in paper.bib should be in +[rMarkdown](http://rmarkdown.rstudio.com/authoring_bibliographies_and_citations.html) +format. + +If you want to cite a software repository URL (e.g. something on GitHub without a preferred +citation) then you can do it with the example BibTeX entry below for @fidgit. + +For a quick reference, the following citation commands can be used: +- `@author:2001` -> "Author et al. (2001)" +- `[@author:2001]` -> "(Author et al., 2001)" +- `[@author1:2001; @author2:2001]` -> "(Author1 et al., 2001; Author2 et al., 2002)" + +# Figures + +Figures can be included like this: +![Caption for example figure.\label{fig:example}](figure.png) +and referenced from text using \autoref{fig:example}. + +Figure sizes can be customized by adding an optional second parameter: +![Caption for example figure.](figure.png){ width=20% } + +# Acknowledgements + +We acknowledge contributions from Brigitta Sipocz, Syrtis Major, and Semyeong +Oh, and support from Kathryn Johnston during the genesis of this project. + +# References~ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5ccbad9..8a85b18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ requires = [ "numpy", "pandas", "pychord", + "pyyaml", "scipy", "setuptools>=42", "sf2utils", @@ -12,4 +13,4 @@ requires = [ "wavio", "wheel" ] -build-backend = "setuptools.build_meta" \ No newline at end of file +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg index 557cfd4..57477f5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = strauss -version = 0.1.1 +version = 0.3.0 author = James Trayford author_email = james.trayford@port.ac.uk description = Sonification Tools and Resources for Astronomers Using Sound Synthesis @@ -28,8 +28,6 @@ install_requires = pyyaml scipy setuptools >= 4.2 - sphinx - tqdm wavio wheel sounddevice @@ -37,5 +35,12 @@ install_requires = [options.packages.find] where = src [options.extras_require] +default = + tqdm TTS = + tqdm TTS +docs = + sphinx + sphinx-exec-code + myst-parser diff --git a/src/strauss/__init__.py b/src/strauss/__init__.py index 2fc8e8d..1c6af7a 100644 --- a/src/strauss/__init__.py +++ b/src/strauss/__init__.py @@ -1,4 +1,4 @@ -"""STRAUSS (Sonification Tools and Resources for Astronomer Using Sound Synthesis) +"""STRAUSS (Sonification Tools and Resources for Analysis Using Sound Synthesis) This module provides a toolkit for *sonification*, i.e. the representation of data using sound.""" diff --git a/src/strauss/__pycache__/tts_caption.cpython-310.pyc b/src/strauss/__pycache__/tts_caption.cpython-310.pyc deleted file mode 100644 index e567f6a..0000000 Binary files a/src/strauss/__pycache__/tts_caption.cpython-310.pyc and /dev/null differ diff --git a/src/strauss/__pycache__/utilities.cpython-310.pyc b/src/strauss/__pycache__/utilities.cpython-310.pyc deleted file mode 100644 index 9b0e084..0000000 Binary files a/src/strauss/__pycache__/utilities.cpython-310.pyc and /dev/null differ diff --git a/src/strauss/channels.py b/src/strauss/channels.py index 800450d..5ef50cf 100644 --- a/src/strauss/channels.py +++ b/src/strauss/channels.py @@ -5,11 +5,6 @@ objects that are channeled to different speakers in the sonification output. -Todo: - * Allow microphones to have a :obj:`polar` as well as :obj:`azimuth` - value, to place them anywhere on a sphere around a listener. - * parameterise the :obj:`"soundsphere"` standard setup, for VR - applications (in development). """ import numpy as np @@ -31,7 +26,7 @@ class mic: mic_type (:obj:`str`): Type of microphone, choose from :obj:`"directional"` (collects using a cardioid antenna pattern), :obj:`"omni"` (collects sound from all directions equally) and - :obj:`"mute"` (collects no sound useful for e.g. muting + :obj:`"mute"` (collects no sound, useful for e.g. muting auxillary channels) label (:obj:`str`): A label for the mic channel (:obj:`int`) The index of the channel, corresponding to @@ -65,11 +60,11 @@ class audio_channels: Args: setup (:obj:`str`): Type of audio setup. Supported options are - :obj:`"mono"`, :obj:`"stereo"`, :obj:`"5p1"` and - :obj:`"7p1"`, or :obj:`"custom"`. + :obj:`"mono"`, :obj:`"stereo"`, :obj:`"5.1"` and + :obj:`"7.1"`, or :obj:`"custom"`. custom_setup (:obj:`dict`): Dictionary defining a customised audio setup, containing keys for :obj:`"azimuths"`, :obj:`"types"` - and :obj:`"labels"`, containing lists parametrising the first + and :obj:`"labels"`, containing lists parameterising the first three arguments of the :class:`mic` object, respectively in the order of their channel index. Also optionally an forder list to unscramble any channel order scrambling done by ffmpeg diff --git a/src/strauss/data/__init__.py b/src/strauss/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/strauss/data/params.csv b/src/strauss/data/params.csv new file mode 100644 index 0000000..f3879dc --- /dev/null +++ b/src/strauss/data/params.csv @@ -0,0 +1,30 @@ +freq,alpha_f,L_U,T_f +20,0.635,-31.5,78.1 +25,0.602,-27.2,68.7 +31.5,0.569,-23.1,59.5 +40,0.537,-19.3,51.1 +50,0.509,-16.1,44.0 +63,0.482,-13.1,37.5 +80,0.456,-10.4,31.5 +100,0.433,-8.2,26.5 +125,0.412,-6.3,22.1 +160,0.391,-4.6,17.9 +200,0.373,-3.2,14.4 +250,0.357,-2.1,11.4 +315,0.343,-1.2,8.6 +400,0.330,-0.5,6.2 +500,0.320,0.0,4.4 +630,0.311,0.4,3.0 +800,0.303,0.5,2.2 +1000,0.300,0.0,2.4 +1250,0.295,-2.7,3.5 +1600,0.292,-4.2,1.7 +2000,0.290,-1.2,-1.3 +2500,0.290,1.4,-4.2 +3150,0.289,2.3,-6.0 +4000,0.289,1.0,-5.4 +5000,0.289,-2.3,-1.5 +6300,0.293,-7.2,6.0 +8000,0.303,-11.2,12.6 +10000,0.323,-10.9,13.9 +12500,0.354,-3.5,12.3 diff --git a/src/strauss/filters.py b/src/strauss/filters.py index 9720836..25cbbdb 100644 --- a/src/strauss/filters.py +++ b/src/strauss/filters.py @@ -1,12 +1,46 @@ +"""The :obj:`filters` submodule: containing audio filter functions + +These are audio filters that can be applied to the audio signal in +frequency space to attenuate (filter out) frequencies. These can be +applied to individual ``Buffers`` as an evolvable parameter. + +Todo: + * Support More Filter Types + * Implement resonance or `'Q'` variation +""" import numpy as np import scipy.signal as sig def LPF1(data, cutoff, q, order=5): + """ + Low-pass filter data array given cutoff, q and LPF order + + Args: + data (array-like): Array containing signal for filtering + cutoff (:obj:`float`): Cutoff frequency + q (:obj:`float`): Filter quality-factor or 'Q' value + order (:obj:`int`): polynomial order of filter function + + Return + y (array-like): Filtered array for output + """ b, a = sig.butter(order, cutoff, btype='low', analog=False) y = sig.lfilter(b, a, data) return y def HPF1(data, cutoff, q, order=5): + """ + High-pass filter data array given cutoff, q and HPF order + + Args: + data (array-like): Array containing signal for filtering + cutoff (:obj:`float`): Cutoff frequency + q (:obj:`float`): Filter quality-factor or 'Q' value + order (:obj:`int`): polynomial order of filter function + + Return + y (array-like): Filtered array for output + """ b, a = sig.butter(order, cutoff, btype='high', analog=False) y = sig.lfilter(b, a, data) return y diff --git a/src/strauss/generator.py b/src/strauss/generator.py index c2d2a3b..47e20db 100644 --- a/src/strauss/generator.py +++ b/src/strauss/generator.py @@ -1,7 +1,7 @@ """ The :obj:`generator` submodule: creating sounds for the sonification. This submodule handles the actual generation of sound for the -sonfication, after parametrisation by the :obj:`Sources` and musical +sonification, after parameterisation by the :obj:`Sources` and musical choices dictated by the :obj:`Score`. Todo: @@ -22,8 +22,12 @@ from . import filters import numpy as np import scipy -# use FFTW backend in scipy -#scipy.fft.set_backend(pyfftw.interfaces.scipy_fft) +# can we use FFTW backend in scipy? +try: + import pyfftw + scipy.fft.set_backend(pyfftw.interfaces.scipy_fft) +except (OSError, ModuleNotFoundError): + pass from scipy.fft import fft, ifft, fftfreq import glob import copy @@ -41,7 +45,6 @@ # ignore wavfile read warning that complains due to WAV file metadata warnings.filterwarnings("ignore", message="Chunk \(non-data\) not understood, skipping it\.") - # TO DO: # - Ultimately have Synth and Sampler classes that own their own stream (stream.py) object # allowing ADSR volume and filter enveloping, LFO implementation etc. @@ -49,10 +52,39 @@ # musical choices and uses these to generate sound, but can be interfaced with directly. def forward_loopsamp(s, start, end): + """Produce array of sample indices for looping a sample forward. + + Sample indices between values `start` and `end` that will loop the sample + such that it loops "forward", i.e. start, start+1, ..., end-1, end, start, + ... etc. + + Args: + s (:obj:`ndarray`): array of input sample indices + start (:obj:`int`): Index of sample after which looping should commence + end (:obj:`int`): Index of sample after which audio loops + + Returns: + out (:obj:`ndarray`): array of output sample indices + """ delsamp = end-start return np.piecewise(s, [s < start, s >= start], [lambda x: x, lambda x: (x-start)%(delsamp) + start]) def forward_back_loopsamp(s, start, end): + """Produce array of sample indices for looping a sample forward-back. + + Sample indices between values `start` and `end` that will loop the sample + such that it loops "forward-back", i.e. `start, start+1, ..., end-1, end, + end-1, ..., start+1, start, start+1, ...` etc. + ... etc. + + Args: + s (:obj:`ndarray`): array of input sample indices + start (:obj:`int`): Index of sample after which looping should commence + end (:obj:`int`): Index of sample after which audio loops + + Returns: + out (:obj:`ndarray`): array of output sample indices + """ delsamp = end-start return np.piecewise(s, [s < start, s >= start], [lambda x: x, lambda x: end - abs((x-start)%(2*(delsamp)) - (delsamp))]) @@ -62,16 +94,23 @@ class Generator: Generators have common initialisation and methods that are defined by this parent class. - - Args: + + Attributes: + samprate (:obj:`int`): Samples per second of audio stream (Hz) + audbuff (:obj:`int`): Samples per audio buffer + preset (:obj:`dict`): Dictionary of parameters defining the + generator. + + """ + def __init__(self, params={}, samprate=48000): + """ + Args: params (`optional`, :obj:`dict`): any generator parameters that differ from the generator :obj:`preset`, where keys and - values are parameters names and values respectively. + values are parameter names and values respectively. samprate (`optional`, :obj:`int`): the sample rate of the generated audio in samples per second (Hz) - """ - def __init__(self, params={}, samprate=48000): - """universal generator initialisation""" + """ self.samprate = samprate # samples per buffer (use 30Hz as minimum) @@ -82,7 +121,7 @@ def __init__(self, params={}, samprate=48000): self.preset = self.modify_preset(params) def load_preset(self, preset='default'): - """ load parameters from a preset YAML file. + """Load parameters from a preset YAML file. Wrapper method for the :obj:`presets.synth.load_preset` or :obj:`presets.sampler.load_preset` functions. Always load the @@ -91,7 +130,7 @@ def load_preset(self, preset='default'): :obj:`preset` Args: - preset (:obj:`str`): name of the preset. built-in presets + preset (:obj:`str`): name of the preset. Built-in presets can be named directly and looks to import the preset from the :obj:`/presets//` directory as :obj:`.yml`, where :obj:`` is either @@ -109,7 +148,7 @@ def load_preset(self, preset='default'): self.modify_preset(preset) def modify_preset(self, parameters, cleargroup=[]): - """modify parameters within current preset + """Modify parameters within current preset method allows user to tweak generator parameters directly, using a dictionary of parameters and their values. subgroups @@ -132,7 +171,7 @@ def modify_preset(self, parameters, cleargroup=[]): del self.preset[grp][k] def preset_details(self, term="*"): - """ Print the names and descriptions of presets + """Print the names and descriptions of presets Wrapper for preset_details function. lists the name and description of built-in presets with names matching the search term. @@ -144,7 +183,7 @@ def preset_details(self, term="*"): getattr(presets, self.gtype).preset_details(name=term) def envelope(self, samp, params, etype='volume'): - """ Envelope function for modulating a single note + """Envelope function for modulating a single note The envelope function takes the pre-defined envelope parameters for the specified envelope type and returns the @@ -212,7 +251,7 @@ def envelope(self, samp, params, etype='volume'): return lvl*env def env_segment_curve(self, t, t1, y0, k): - """formula for segments of the envelope function + """Formula for segments of the envelope function Function to evaluate the segments of the envelope, allowing for curvature, i.e. concave & convex envelope segments. @@ -277,7 +316,7 @@ def tri(self,s,f,p): s (:obj:`array`-like): sample index f (:obj:`float`): samples per cycle p (:obj:`float` or :obj:`str`): if numerical, phase in units - of cycles, :obj:`'random'` indicates randomised. + of cycles :obj:`'random'` indicates randomised. Returns: v (:obj:`array`-like): values for each sample """ @@ -311,7 +350,7 @@ def lfo(self, samp, sampfrac, params, ltype='pitch'): cycles or :obj:`'random'` to indicate randomised. Note: - To modulate the frequency of an ocillator, use the + To modulate the frequency of an oscillator, use the :obj:`freq_shift` parameter, rather than :obj:`freq` Args: @@ -330,7 +369,6 @@ def lfo(self, samp, sampfrac, params, ltype='pitch'): env_dict = {} lfo_key = f'{ltype}_lfo' lfo_params = params[lfo_key] - env_dict['note_length'] = params['note_length'] env_dict['lfo_envelope'] = lfo_params @@ -368,19 +406,22 @@ class Synthesizer(Generator): the preset, and linearly combined to produce the sound. defines attribute :obj:`self.gtype = 'synth'`. - Args: - params (`optional`, :obj:`dict`): any generator parameters - that differ from the generator :obj:`preset`, where keys and - values are parameters names and values respectively. - samprate (`optional`, :obj:`int`): the sample rate of - the generated audio in samples per second (Hz) + Attributes: + gtype (:obj:`str`): Generator type Todo: * Add other synthesiser types, aside from additive (e.g. FM, vector, wavetable)? """ def __init__(self, params=None, samprate=48000): - + """ + Args: + params (`optional`, :obj:`dict`): any generator parameters + that differ from the generator :obj:`preset`, where keys + and values are parameters names and values respectively. + samprate (`optional`, :obj:`int`): the sample rate of + the generated audio in samples per second (Hz) + """ # default synth preset self.gtype = 'synth' self.preset = getattr(presets, self.gtype).load_preset() @@ -395,13 +436,17 @@ def __init__(self, params=None, samprate=48000): def setup_oscillators(self): """Setup and consolidate oscs into a two-variable function. - Reads the parametrisation of each oscillator from the preset, + Reads the parameterisation of each oscillator from the preset, specifying their waveform (:obj:`wave`), relative amplitude (:obj:`level`), detuning in cents (:obj:`det`) and :obj:`phase`, either a number in units of cycles, or a string specifying randomisation (:obj:`'random'`). Sets the :obj:`self.generate` method, using the :obj:`self.combine_oscs`. + + Note: + This is deprecated and will likely be removed from future + versions """ # oscdict = self.preset['oscillators'] # self.osclist = [] @@ -423,6 +468,9 @@ def setup_oscillators(self): def modify_preset(self, parameters, clear_oscs=True): """Synthesizer-specific wrapper for the modify_preset method. + This gives control over whether or not to clear the arbitrary + number of oscillators for synthesizer. + Args: parameters (:obj:`dict`): keys and items are the preset parameter names and new values. Nested dictionaries are @@ -444,7 +492,7 @@ def combine_oscs(self, s, f): Args: s (:obj:`array`-like): Sample index f (:obj:`float` or :obj:`str`): If numerical, frequency in - cycles per second, if string, note name in scientific + cycles per second, if string, note name in scientific pitch notation (e.g. :obj:`'A4'`) Returns: tot (:obj:`array`-like): values for each sample @@ -548,26 +596,13 @@ class Sampler(Generator): """Sampler generator class This generator class generates sound using pre-loaded audio - samples, representing d`ifferent notes. Presets define parameters - controlling how these defines + samples, representing different notes. Presets define parameters + controlling these defines attribute :obj:`self.gtype = 'sampler'`. - Args: - sampfiles (`required`, :obj:`str`): string pointing to samples - to load. This can either point to a directory containing - samples, where `"path/to/samples"` contains files named - as `samples_A#4.wav` (ie. `_.wav`), - or a *Soundfont* file, with extension `.sf2`. - params (`optional`, :obj:`dict`): any generator parameters - that differ from the generator :obj:`preset`, where keys and - values are parameters names and values respectively. - samprate (`optional`, :obj:`int`): the sample rate of - the generated audio in samples per second (Hz) - sf_preset (`optional`, :obj:`int`) if using a *Soundfont* - (`.sf2`) file, this is the number of the preset to use. - All `.sf2` files should contain at least one preset. When - given default `None` value, will print available presets - and select the first preset. Note presets are 1-indexed. + Attributes: + gtype (:obj:`str`): Generator type + Todo: * Add zone mapping for samples (e.g. allow a sample to define a range of notes played at different speeds). @@ -578,6 +613,24 @@ class Sampler(Generator): """ def __init__(self, sampfiles, params=None, samprate=48000, sf_preset=None): + """ + Args: + sampfiles (`required`, :obj:`str`): string pointing to samples + to load. This can either point to a directory containing + samples, where `"path/to/samples"` contains files named + as `samples_A#4.wav` (ie. `_.wav`), + or a *Soundfont* file, with extension `.sf2`. + params (`optional`, :obj:`dict`): any generator parameters + that differ from the generator :obj:`preset`, where keys and + values are parameters names and values respectively. + samprate (`optional`, :obj:`int`): the sample rate of + the generated audio in samples per second (Hz) + sf_preset (`optional`, :obj:`int`) if using a *Soundfont* + (`.sf2`) file, this is the number of the preset to use. + All `.sf2` files should contain at least one preset. When + given default `None` value, will print available presets + and select the first preset. Note presets are 1-indexed. + """ # default sampler preset self.gtype = 'sampler' self.preset = getattr(presets, self.gtype).load_preset() @@ -637,8 +690,11 @@ def __init__(self, sampfiles, params=None, samprate=48000, sf_preset=None): self.load_samples() def get_sfpreset_samples(self, sfpreset): - """Reading samples from a soundfont file along with metadata to - scale and tune notes. + """Reading samples from a soundfont file along with metadata. + + Read in the audio samples from a ``.sf2`` file to populate + available notes, mapping the MIDI key values to musical notes, + scaling and tuning samples as appropriate. Args: sf_preset (`optional`, :obj:`int`) The number of the *Soundfont* @@ -649,10 +705,10 @@ def get_sfpreset_samples(self, sfpreset): Returns: sfpre_dict (:obj:`dict`): dictionary of data required to load - soundfont samples in to the `Sampler`, including raw `samples`, - `sample_rate`, `original_pitch` of the samples, the `min_note` - and `max_note` in midi values to use the sample, and the - `sample_map`, assigning each sample to a note. + soundfont samples in to the `Sampler`, including raw `samples`, + `sample_rate`, `original_pitch` of the samples, the `min_note` + and `max_note` in midi values to use the sample, and the + `sample_map`, assigning each sample to a note. """ minmidi = np.inf maxmidi = -np.inf @@ -724,8 +780,8 @@ def reconstruct_samples(self, sfpre_dict): Return: sampdict (:obj:`dict`): output dictionary of mapped notes, with - values of arrays of sample values at the samplerate of the - `Generator`. + values of arrays of sample values at the samplerate of the + `Generator`. """ minkey = sfpre_dict['min_note'] maxkey = sfpre_dict['max_note'] @@ -761,7 +817,7 @@ def load_samples(self): Read audio samples in from a specified directory or via a dictionary of filepaths, generate interpolation functions for - each, and assign them to a named note in scientific notation + each, and assign them to a named note in scientific pitch notation (e.g. :obj:`'A4'`). """ self.samples = {} @@ -805,7 +861,7 @@ def forward_loopsamp(self, s, start, end): Returns: s_new (:obj:`array`-like): new sample indices to create a - forward-looping effect + forward-looping effect """ delsamp = end-start return np.piecewise(s, [s < start, s >= start], @@ -825,7 +881,7 @@ def forward_back_loopsamp(self, s, start, end): Returns: s_new (:obj:`array`-like): new sample indices to create a - back and forth looping effect + back and forth looping effect """ delsamp = end-start @@ -944,26 +1000,64 @@ def play(self, mapping): class Spectralizer(Generator): """Spectralizer generator class + + This generator class synthesises sound from a spectrum input + using an *inverse Fast Fourier Transform* (iFFT) algorithm. + Defining a minimum and maximum frequency in Hz, input spectrum + is interpolated between these points such that the output + audio signal has the requested length. Phases are randomised + to avoid phase correlations. + + Attributes: + gtype (:obj:`str`): Generator type + + Todo: + * Add other synthesiser types, aside from additive (e.g. FM, + vector, wavetable)? """ - def __init__(self, params=None, samprate=48000): + def __init__(self, params=None, samprate=48000): + """ + Args: + params (`optional`, :obj:`dict`): any generator parameters + that differ from the generator :obj:`preset`, where keys + and values are parameters names and values respectively. + samprate (`optional`, :obj:`int`): the sample rate of + the generated audio in samples per second (Hz) + """ # default synth preset self.gtype = 'spec' self.preset = getattr(presets, self.gtype).load_preset() self.preset['ranges'] = getattr(presets, self.gtype).load_ranges() + self.eq = utils.Equaliser() self.freqwarn = True - + # universal initialisation for generator objects: super().__init__(params, samprate) def spectrum_to_signal(self, spectrum, phases, new_nlen, mindx, maxdx, interp_type): """ Convert the input spectrum into sound signal + + Performs the inverse fast fourier transform to produce spectral + sonification. + + Args: + spectrum (:obj:`ndarray`): Values of the spectrum, ordered + from high to low frequency + phases (:obj:`ndarray`): Array of values of `[0,2*numpy.pi]` + representing the complex number argument + new_nlen (:obj:`int`): Number of samples needed to enclose + the output signal. + mindx (:obj:`int`): Index in total Fourier transform + represnting the minimum audio frequency + maxdx (:obj:`int`): Index in total Fourier transform + represnting the maximum audio frequency + interp_type (:obj:`str`): Interpolation approach, either + `"sample"` interpolating between samples, or + `"preserve_power"` where cumulative power is interpolated + and then differentiated to avoid missing power. """ - - # NOTE: interpolation around a delta function can lead to splitting power between adjacent - # frequencies and result in an artificial beating. This can be avoided by choosing values - # a length that places the spectrum on the grid exactly if interp_type == "sample": ps = np.interp(np.linspace(0,1,maxdx-mindx), np.linspace(0, 1, spectrum.size), spectrum) @@ -1002,7 +1096,6 @@ def play(self, mapping): (not nested, see :meth:`strauss.generator.modify_preset`) where group members are indicated using :obj:`'/'` notation (e.g. :obj:`{'volume_envelope/A': 0.5, ...`). - """ samprate = self.samprate audbuff = self.audbuff @@ -1028,15 +1121,27 @@ def play(self, mapping): spectra_multiples = (discrete_freqs - 1)/(spectrum.size - 1) # the minimum factor by which to increase the stream length to accomodate spectra in whole number multiples - buffer_factor = np.ceil(spectra_multiples)/spectra_multiples - + if params['fit_spec_multiples']: + buffer_factor = np.ceil(spectra_multiples)/spectra_multiples + else: + buffer_factor = 1 + # number of samples to generate including buffer new_nlen = int(buffer_factor * nlength) # the frequency bound indices which the spectrum will be mapped into mindx = int(params['min_freq'] * duration * buffer_factor) maxdx = int(params['max_freq'] * duration * buffer_factor) - + + if params['equal_loudness_normalisation']: + freqs = np.linspace(params['min_freq'], params['max_freq'], len(spectrum)) + norm = self.eq.get_relative_loudness_norm(freqs) + if not self.eq.factor_rms: + self.eq.factor_rms = [] + rms1 = np.sqrt(np.mean(spectrum**2)) + spectrum *= norm + self.eq.factor_rms.append(np.sqrt(np.mean(spectrum**2))/rms1) + # hardcode phase randomisation for now phases = 2*np.pi*np.random.random(new_nlen) @@ -1110,7 +1215,6 @@ def play(self, mapping): sstream.consolidate_buffers() - sstream.values /= abs(sstream.values).max() # get volume envelope @@ -1142,9 +1246,9 @@ def play(self, mapping): if hasattr(params['cutoff'], "__iter__"): # if static cutoff, use minimum buffer count sstream.bufferize(sstream.length/4) - else: - # 30 ms buffer (hardcoded for now) - sstream.bufferize(0.03) + else: + # 30 ms buffer (hardcoded for now) + sstream.bufferize(0.03) sstream.filt_sweep(getattr(filters, params['filter_type']), utils.const_or_evo_func(params['cutoff'])) return sstream diff --git a/src/strauss/notes.py b/src/strauss/notes.py index d6f89e1..45c12af 100644 --- a/src/strauss/notes.py +++ b/src/strauss/notes.py @@ -1,3 +1,19 @@ +"""The :obj:`notes` submodule: translating musical note representations + +This submodule contains functions for translating between different +representations of musical notes or musical chords, and representative +sound frequencies and MIDI notes. + +Attributes: + tuneC0 (:obj:`float`): The frequency in Hz of the ``C0`` musical + note + notecount (:obj:`int`): Semitone offset above C in an octave + notesharps (:obj:`list`): Names of musical notes using sharp notation + noteflats (:obj:`list`): Names of musical notes using flat notation. + semitone_dict (:obj:`dict`): Dictionary of note names to semitone + offsets above C. +""" + import numpy as np import pychord as chrd import re @@ -16,7 +32,15 @@ def parse_note(notename): """ Takes scientific pitch name and returns frequency in Hz. - flat and sharp numbers supported + Flat and sharp values supported. Assumes equal temperament + and A4 = 440 Hz tuning (ISO 16) + + Args: + notename (:obj:`str`): scientific pitch name, in format + , e.g. 'Ab4', 'E3' or 'F#2' + + Returns: + out (numerical): Frequency of note in Hertz """ nsplit = re.findall("(\D+|\d+)", notename) semi = semitone_dict[nsplit[0]]/12. @@ -24,6 +48,20 @@ def parse_note(notename): return tuneC0*pow(2.,semi+octv) def parse_chord(chordname, rootoct=3): + """ + Takes name of a chord and root octave to generate a valid + chord voicing as an array of frequencies in Hz, using the + `pychord` library + + Args: + chordname (:obj:`str`): Standard chord name, e.g. `'A7'` + or `'Dm7add9'` etc. + rootoct (:obj:`int`): Octave number + + Returns: + out (:obj:`ndarray`) array of frequencies constituting + chord + """ chord = chrd.Chord(chordname) notes = chord.components_with_pitch(rootoct) frqs = [] @@ -32,11 +70,35 @@ def parse_chord(chordname, rootoct=3): return np.array(frqs) def chord_notes(chordname, rootoct=3): + """ + Takes name of a chord and root octave to generate a valid + chord voicing as a list of note names, using the `pychord` + library + + Args: + chordname (:obj:`str`): Standard chord name, e.g. `'A7'` + or `'Dm7add9'` etc. + rootoct (:obj:`int`): Octave number + + Returns: + out (:obj:`list`): list of note names constituting chord + """ chord = chrd.Chord(chordname) notes = chord.components_with_pitch(int(rootoct)) return notes def mkey_to_note(val): + """ + Take MIDI key value and return the note name in scientific + notation + + Args: + val (:obj:`int`): MIDI key value + + Returns: + out (:obj:`str`): scientific pitch name, in format + ``, e.g. `'E3'` or `'F#2'` + """ from strauss.notes import notesharps octv = val // 12 - 1 semi = val % 12 diff --git a/src/strauss/presets/sampler/default.md b/src/strauss/presets/sampler/default.md new file mode 100644 index 0000000..f8568b2 --- /dev/null +++ b/src/strauss/presets/sampler/default.md @@ -0,0 +1,110 @@ +## Name +preset name +## Description +full description +## Note_Length +Numerical note length in s or "sample" for the sample length or "none" to last to the end of the +## Looping +"off" for no looping "forward" to loop forward "forwardback" to loop back and forth +## Loop_Start +If looping, start and end point of loop in seconds. +## Loop_End +If loop_end is longer than the sample, clip to end of the sample. +## Volume_Envelope +### A +Attack +### D +Decay +### S +Sustain +### R +Release +### Ac + +### Dc + +### Rc + +### Level + + +## Filter +Do we apply a filter? +## Filter_Type +filter type +## Cutoff +filter cutoff +## Pitch_Lfo +### Use +switch feature on or off +### Wave +type of waveform +### Amount +amount +### Freq +frequency +### Freq_Shift +frequency shift +### Phase +random +### A +Attack +### D +Decay +### S +Sustain +### R +Release +### Ac + +### Dc + +### Rc + +### Level + + +## Volume_Lfo +### Use +switch feature on or off +### Wave +type of waveform +### Amount +amount +### Freq +frequency +### Freq_Shift +frequency shift +### Phase +random +### A +Attack +### D +Decay +### S +Sustain +### R +Release +### Ac + +### Dc + +### Rc + +### Level + + +## Volume +Master Volume +## Pitch +Default pitch selection +## Azimuth +azimuth coordinate for center panning +## Polar +polar coordinate for center panning +## Pitch_Hi +pitch range maximum in semitones +## Pitch_Lo +pitch range minimum in semitones +## Pitch_Shift +default shift in semitones diff --git a/src/strauss/presets/sampler/default.yml b/src/strauss/presets/sampler/default.yml index 3bb3bf8..a766101 100644 --- a/src/strauss/presets/sampler/default.yml +++ b/src/strauss/presets/sampler/default.yml @@ -85,3 +85,100 @@ pitch_hi: 36 pitch_lo: 0 pitch_shift: 0. +_meta: + _doc: >- + The `Sampler` generator type can be used to modify and play audio samples (sound recordings) . + name: Preset name + description: Full description of the parameters selected for this preset, e.g. looping, volume and pitch envelopes, filters, etc. + note_length: >- + Numerical note length in seconds, or `'sample'` for the sample length, or `'none'` + to last to the end of the sonification. + looping: >- + Option to play the sample on a loop. `'off'` for no looping `'forward'` to loop forwards, `'forwardback'` to play the loop back and forth + loop_start: >- + If looping, starting point of loop in seconds. + loop_end: >- + If looping, ending point of loop in seconds. If loop_end is longer than the sample, clip to end of the sample. + volume_envelope: + _doc: >- + Define the note volume envelope applied to the samples. _'ADSR'_ is a common parametrisation in sound synthesis, + find out more e.g. [at this link](https://blg.native-instruments.com/adsr-explained/) + A: Attack, how long it takes for a sound to rise to 100% of the `level` after it’s triggered. + D: Decay, how long it takes for the sounds volume to die down to the `Sustain` value after the `Attack` period. + S: Sustain, the volume level (from 0 to 1.0) maintained after the `Decay` period, while the note is held. + R: Release, how long the tone takes to finally die away once the note is released. + Ac: Curvature of the attack portion of a note. Values from -1 to 1, positive indicates increases quickly then slow, + negative slowly then quick. a value of 0 is a linear attack, increasing in volume at a constant rate. + Dc: Curvature of the Decay portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, + negative slowly then quick. a value of 0 is a linear decay, decreasing in volume at a constant rate. + Rc: Curvature of the release portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, + negative slowly then quick. a value of 0 is a linear release, decreasing in volume at a constant rate. + level: Total amplitude level of the envelope from 0 to 1, contolling maximum volume of the note. + filter: >- + Apply a frequency filter to to the audio signal. Values are 'on' or 'off'. A filter affects the timbre by filtering out certain harmonics + filter_type: >- + Low pass filter (only allows frequencies lower than your cutoff to pass through) + High pass filter (only allows frequencies higher than your cutoff to pass through) + cutoff: >- + The cutoff frequency (or `'knee'`) of the filter, beyond which frequencies are attenuated. as a fraction of the maximum frequency + pitch_lfo: + _doc: >- + Controls for the `'Low Frequency Oscillator'` (LFO) used to modulate pitch of notes at rhythmic + frequencies. In music this is often referred to as `'vibrato'`. + use: Switch feature on or off + wave: Type of waveform used for the oscillator. As with `Synthesizer` oscillators can be sawtooth + (`'saw'`), square (`'square'`), sinusoid (`'sine'`), triangle (`'tri'`) or noise (`'noise'`). + amount: the amplitude of the maximal pitch oscillation from the underlying pitch + freq: Base frequency of the LFO oscillations. + freq_shift: Shift relative to the base LFO frequency. + phase: The phase of the LFO oscillations, defined in terms of fraction of a whole cycle + A: Attack, how long it takes for the LFO depth to rise to 100% of the `level` after it’s triggered. + D: Decay, how long it takes for the LFO depth to die down to the `Sustain` value after the `Attack` period. + S: Sustain, the LFO depth (from 0 to 1.0) maintained after the `Decay` period, while the note is held. + R: Release, how long LFO depth takes to finally die to 0 once the note is released. + Ac: Curvature of the attack portion of a note. Values from -1 to 1, positive indicates increases quickly then slow, + negative slowly then quick. a value of 0 is a linear attack, increasing in volume at a constant rate. + Dc: Curvature of the Decay portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, + negative slowly then quick. a value of 0 is a linear decay, decreasing in volume at a constant rate. + Rc: Curvature of the release portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, + negative slowly then quick. a value of 0 is a linear release, decreasing in volume at a constant rate. + level: Total level of the envelope from 0 to 1, contolling maximum depth of the LFO. + volume_lfo: + _doc: >- + Controls for the `'Low Frequency Oscillator'` (LFO) used to modulate volume of notes at rhythmic + frequencies. In music this is often referred to as `'tremolo'`. + use: switch volume LFO effects on or off + wave: Type of waveform used for the oscillator. As with `Synthesizer` oscillators can be sawtooth + (`'saw'`), square (`'square'`), sinusoid (`'sine'`), triangle (`'tri'`) or noise (`'noise'`). + amount: The amplitude of the maximal volume oscillation from the underlying pitch + freq: Base frequency of the LFO oscillations. + freq_shift: Shift relative to the base LFO frequency + phase: The phase of the LFO oscillations, defined in terms of fraction of a whole cycle. + A: Attack, how long it takes for the LFO depth to rise to 100% of the `level` after it’s triggered. + D: Decay, how long it takes for the LFO depth to die down to the `Sustain` value after the `Attack` period. + S: Sustain, the LFO depth (from 0 to 1.0) maintained after the `Decay` period, while the note is held. + R: Release, how long LFO depth takes to finally die to 0 once the note is released. + Ac: >- + Curvature of the attack portion of a note. Values from -1 to 1, positive indicates increases quickly then slow, + negative slowly then quick. a value of 0 is a linear attack, increasing in volume at a constant rate. + Dc: >- + Curvature of the Decay portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, + negative slowly then quick. a value of 0 is a linear decay, decreasing in volume at a constant rate. + Rc: >- + Curvature of the release portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, + negative slowly then quick. a value of 0 is a linear release, decreasing in volume at a constant rate. + level: Total level of the envelope from 0 to 1, contolling maximum depth of the LFO. + volume: >- + Master Volume of generator. + pitch: >- + Default pitch selection (used by all generators) + azimuth: >- + Azimuth coordinate for spatialising audio into differing channels + polar: >- + Polar coordinate for spatialising audio into differing channels + pitch_hi: >- + Pitch range maximum in semitones + pitch_lo: >- + Pitch range minimum in semitones + pitch_shift: >- + Default shift in semitones diff --git a/src/strauss/presets/sampler/ranges/default.md b/src/strauss/presets/sampler/ranges/default.md new file mode 100644 index 0000000..f6caa40 --- /dev/null +++ b/src/strauss/presets/sampler/ranges/default.md @@ -0,0 +1,62 @@ +## Note_Length +Numerical note length +## Note_Length_Units +Units for numerical note length, e.g. seconds +## Volume_Envelope +### A +Attack +### D +Decay +### S +Sustain +### R +Release +### Ac + +### Dc + +### Rc + +### Level + +### A_Unit +units for Attack +### D_Unit +units for Decay +### S_Unit +units for Sustain +### R_Unit +units for Release +### Ac_Unit +units for Ac +### Dc_Unit +units for Dc +### Rc_Unit +units for Rc +### Level_Unit +units for level + +## Cutoff +filter cutoff +## Cutoff_Unit +units for filter cutoff +## Volume +Master volume +## Volume_Unit +Units for master volume +## Pitch +Default pitch selection +## Pitch_Unit +Units for Default pitch selection +## Azimuth +azimuth coordinate for center panning +## Azimuth_Unit +units for azimuth coordinate for center panning +## Polar +polar coordinate for center panning +## Polar_Unit +units for polar coordinate for center panning +## Pitch_Shift +default shift in semitones +## Pitch_Shift_Unit +units for default shift in semitones diff --git a/src/strauss/presets/sampler/ranges/default.yml b/src/strauss/presets/sampler/ranges/default.yml index 7d71059..c5ee0b6 100644 --- a/src/strauss/presets/sampler/ranges/default.yml +++ b/src/strauss/presets/sampler/ranges/default.yml @@ -23,7 +23,54 @@ volume_envelope: Dc_unit: 'unitless' Rc_unit: 'unitless' level_unit: 'unitless' - + +volume_lfo: + A: [1e-2, 20.] + D: [1e-2, 20.] + S: [0., 1.] + R: [1e-2, 20] + Ac: [-1. ,1.] + Dc: [-1. ,1.] + Rc: [-1. ,1.] + level: [0., 1.] + amount: [0, 1] + freq: [1., 12.] + freq_shift: [0., 3.] + A_unit: 'seconds' + D_unit: 'seconds' + S_unit: 'unitless' + R_unit: 'seconds' + Ac_unit: 'unitless' + Dc_unit: 'unitless' + Rc_unit: 'unitless' + level_unit: 'unitless' + freq_unit: 'Hz' + freq_shift_unit: 'octave' + +pitch_lfo: + A: [1e-2, 20.] + D: [1e-2, 20.] + S: [0., 1.] + R: [1e-2, 20] + Ac: [-1. ,1.] + Dc: [-1. ,1.] + Rc: [-1. ,1.] + level: [0., 1.] + amount: [0, 2] + freq: [1., 12.] + freq_shift: [0., 3.] + A_unit: 'seconds' + D_unit: 'seconds' + S_unit: 'unitless' + R_unit: 'seconds' + Ac_unit: 'unitless' + Dc_unit: 'unitless' + Rc_unit: 'unitless' + level_unit: 'unitless' + amount_unit: 'semitones' + freq_unit: 'Hz' + freq_shift_unit: 'octave' + # filter cutoff cutoff: [0., 1.] cutoff_unit: 'unitless' @@ -43,5 +90,44 @@ polar: [0.,1.] polar_unit: 'unitless' # pitch range and default shift in semitones -pitch_shift: [0., 36.] +pitch_shift: [0., 24.] pitch_shift_unit: 'semitones' + +_meta: + note_length: >- + Numerical note length + note_length_units: >- + Units for numerical note length, e.g. seconds + volume_envelope: >- + define the note volume envelope applied to the samples + A,D,S & R correspond to 'attack', 'decay', 'sustain' and 'release' + volume_envelope: + A: Attack + D: Decay + S: Sustain + R: Release + Ac: "" + Dc: "" + Rc: "" + level: "" + A_unit: units for Attack + D_unit: units for Decay + S_unit: units for Sustain + R_unit: units for Release + Ac_unit: units for Ac + Dc_unit: units for Dc + Rc_unit: units for Rc + level_unit: units for level + cutoff: filter cutoff + cutoff_unit: units for filter cutoff + volume: Master volume + volume_unit: Units for master volume + pitch: Default pitch selection + pitch_unit: Units for Default pitch selection + azimuth: azimuth coordinate for center panning + azimuth_unit: units for azimuth coordinate for center panning + polar: polar coordinate for center panning + polar_unit: units for polar coordinate for center panning + pitch_shift: default shift in semitones + pitch_shift_unit: units for default shift in semitones + diff --git a/src/strauss/presets/sampler/staccato.md b/src/strauss/presets/sampler/staccato.md new file mode 100644 index 0000000..e5ef052 --- /dev/null +++ b/src/strauss/presets/sampler/staccato.md @@ -0,0 +1,16 @@ +## Name +preset name +## Description +full description +## Note_Length +Numerical note length in s or "sample" for the sample length or "none" to last to the end of the +## Volume_Envelope +### A +Attack +### D +Decay +### S +Sustain +### R +Release + diff --git a/src/strauss/presets/sampler/staccato.yml b/src/strauss/presets/sampler/staccato.yml index c14210f..5c0ad14 100644 --- a/src/strauss/presets/sampler/staccato.yml +++ b/src/strauss/presets/sampler/staccato.yml @@ -16,4 +16,19 @@ volume_envelope: A: 0.01 D: 0. S: 1. - R: 0.07 \ No newline at end of file + R: 0.07 + +_meta: + name: preset name + description: full description + note_length: >- + Numerical note length in s or "sample" for the sample length or "none" to last to the end of the + volume_envelope: >- + Define the note volume envelope applied to the samples. + A,D,S & R correspond to 'attack', 'decay', 'sustain' and 'release'. + volume_envelope: + A: Attack + D: Decay + S: Sustain + R: Release + diff --git a/src/strauss/presets/sampler/sustain.md b/src/strauss/presets/sampler/sustain.md new file mode 100644 index 0000000..f947ed3 --- /dev/null +++ b/src/strauss/presets/sampler/sustain.md @@ -0,0 +1,10 @@ +## Name +preset name +## Description +full description +## Looping +use the 'forwardback' looping mode by default, between 0.2 and 0.5 seconds for each sample +## Loop_Start +start of the loop in seconds +## Loop_End +end of the loop in seconds diff --git a/src/strauss/presets/sampler/sustain.yml b/src/strauss/presets/sampler/sustain.yml index ac8d9e5..577bb34 100644 --- a/src/strauss/presets/sampler/sustain.yml +++ b/src/strauss/presets/sampler/sustain.yml @@ -13,4 +13,12 @@ description: >- # use the 'forwardback' looping mode by default, between 0.2 and 0.5 seconds for each sample looping: "forwardback" loop_start: 0.2 -loop_end: 0.5 \ No newline at end of file +loop_end: 0.5 + +_meta: + name: preset name + description: full description + looping: use the 'forwardback' looping mode by default, between 0.2 and 0.5 seconds for each sample + loop_start: start of the loop in seconds + loop_end: end of the loop in seconds + diff --git a/src/strauss/presets/spec/default.md b/src/strauss/presets/spec/default.md new file mode 100644 index 0000000..f8485c8 --- /dev/null +++ b/src/strauss/presets/spec/default.md @@ -0,0 +1,116 @@ +## _Meta +### Name +preset name +### Description +full description +### Note_Length +Numerical note length in s +### Volume_Envelope +#### A +Attack +#### D +Decay +#### S +Sustain +#### R +Release +#### Ac + +#### Dc + +#### Rc + +#### Level + + +### Filter +Do we apply a filter? +### Filter_Type +filter type +### Cutoff +filter cutoff +### Pitch_Lfo +#### Use +switch feature on or off +#### Wave +type of waveform +#### Amount +amount +#### Freq +frequency +#### Freq_Shift +frequency shift +#### Phase +random +#### A +Attack +#### D +Decay +#### S +Sustain +#### R +Release +#### Ac + +#### Dc + +#### Rc + +#### Level + + +### Volume_Lfo +#### Use +switch feature on or off +#### Wave +type of waveform +#### Amount +amount +#### Freq +frequency +#### Freq_Shift +frequency shift +#### Phase +random +#### A +Attack +#### D +Decay +#### S +Sustain +#### R +Release +#### Ac + +#### Dc + +#### Rc + +#### Level + + +### Volume +Master Volume +### Interpolation_Type +How to interpolate and resample in the spectrum. "sample": interpolate spectrum values directly; "preserve_power": integrate, interpolate then differentiate to avoid missing power in narrow features. +### Regen_Phases +For an evolving spectrum, do we regenerate phases for each buffer, or keep the same? These have differing effects. +### Fit_Spec_Multiples +Whether or not to generate IFFT such that the spectrum sample points are hit exactly. +### Min_Freq +Minimum frequency in Hz +### Max_Freq +Maximum frequency in Hz +### Pitch +Default pitch selection +### Azimuth +azimuth coordinate for center panning +### Polar +polar coordinate for center panning +### Pitch_Hi +pitch range maximum in semitones +### Pitch_Lo +pitch range minimum in semitones +### Pitch_Shift +default shift in semitones + diff --git a/src/strauss/presets/spec/default.yml b/src/strauss/presets/spec/default.yml index 49622ad..d18367e 100644 --- a/src/strauss/presets/spec/default.yml +++ b/src/strauss/presets/spec/default.yml @@ -78,6 +78,12 @@ interpolation_type: 'sample' # These have differing effects regen_phases: true +# Whether or not to generate IFFT such that the spectrum sample points are hit exactly +fit_spec_multiples: true + +# Do we equalise the spectra for equal loudness? +equal_loudness_normalisation: false + # frequency limits in Hz min_freq: 50. max_freq: 2000. @@ -94,3 +100,114 @@ pitch_hi: 0.1 pitch_lo: 0 pitch_shift: 0. +_meta: + _doc: >- + The `Spectraliser` generator type can be used to represent a frequency spectrum, by mapping + any frequency range to an audible range, and generating a representative sound signal (using + an [IFFT approach](https://docs.scipy.org/doc/scipy/reference/generated/scipy.fft.ifft.html)). + In this approach, narrow spikes become tones at their frequency position, a sloped continuum + becomes coloured noise, etc. _Note_: this generator must take a `spectrum` input, as an array, + representing 'flux' or 'power' values of a spectrum, arranged from lowest to highest frequncy. + name: name for a particular preset. + description: full description of what a preset does. + note_length: >- + Numerical note length + volume_envelope: >- + volume_envelope: + _doc : >- + Define the note volume envelope applied to the samples A,D,S & R correspond to 'attack', + 'decay', 'sustain' and 'release'. _'ADSR'_ is a common parametrisation in sound synthesis, + Find out more e.g. [at this link](https://blg.native-instruments.com/adsr-explained/) + A: Attack, how long it takes for a sound to rise to 100% of the `level` after it’s triggered. + D: Decay, how long it takes for the sounds volume to die down to the `Sustain` value after the `Attack` period. + S: Sustain, the volume level (from 0 to 1.0) maintained after the `Decay` period, while the note is held. + R: Release, how long the tone takes to finally die away once the note is released. + Ac: Curvature of the attack portion of a note. Values from -1 to 1, positive indicates increases quickly then slow, + negative slowly then quick. a value of 0 is a linear attack, increasing in volume at a constant rate. + Dc: Curvature of the Decay portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, + negative slowly then quick. a value of 0 is a linear decay, decreasing in volume at a constant rate. + Rc: Curvature of the release portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, + negative slowly then quick. a value of 0 is a linear release, decreasing in volume at a constant rate. + level: Total amplitude level of the envelope from 0 to 1, contolling maximum volume of the note. + filter: >- + Do we apply a frequency filter to the audio signal? This can be used to change the balance of frequencies and manipulate + the 'timbre' of a note + filter_type: >- + Choose from available filter types + cutoff: >- + The cut-off frequency (or 'knee') of the filter, at which frequencies are attenuated beyond. + specified between 0 and 1 as a fraction of the audible range of notes we can hear (E0 to D#10). + pitch_lfo: + _doc: >- + Controls for the `'Low Frequency Oscillator'` (LFO) used to modulate pitch of notes at rhythmic + frequencies. In music this is often referred to as `'vibrato'`. + use: Switch feature on or off + wave: Type of waveform used for the oscillator. As with `Synthesizer` oscillators can be sawtooth + (`'saw'`), square (`'square'`), sinusoid (`'sine'`), triangle (`'tri'`) or noise (`'noise'`). + amount: the amplitude of the maximal pitch oscillation from the underlying pitch + freq: Base frequency of the LFO oscillations. + freq_shift: Shift relative to the base LFO frequency. + phase: The phase of the LFO oscillations, defined in terms of fraction of a whole cycle + A: Attack, how long it takes for the LFO depth to rise to 100% of the `level` after it’s triggered. + D: Decay, how long it takes for the LFO depth to die down to the `Sustain` value after the `Attack` period. + S: Sustain, the LFO depth (from 0 to 1.0) maintained after the `Decay` period, while the note is held. + R: Release, how long LFO depth takes to finally die to 0 once the note is released. + Ac: Curvature of the attack portion of a note. Values from -1 to 1, positive indicates increases quickly then slow, + negative slowly then quick. a value of 0 is a linear attack, increasing in volume at a constant rate. + Dc: Curvature of the Decay portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, + negative slowly then quick. a value of 0 is a linear decay, decreasing in volume at a constant rate. + Rc: Curvature of the release portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, + negative slowly then quick. a value of 0 is a linear release, decreasing in volume at a constant rate. + level: Total level of the envelope from 0 to 1, contolling maximum depth of the LFO. + + volume_lfo: + _doc: >- + Controls for the `'Low Frequency Oscillator'` (LFO) used to modulate volume of notes at rhythmic + frequencies. In music this is often referred to as `'tremolo'`. + use: switch volume LFO effects on or off + wave: Type of waveform used for the oscillator. As with `Synthesizer` oscillators can be sawtooth + (`'saw'`), square (`'square'`), sinusoid (`'sine'`), triangle (`'tri'`) or noise (`'noise'`). + amount: The amplitude of the maximal volume oscillation from the underlying pitch + freq: Base frequency of the LFO oscillations. + freq_shift: Shift relative to the base LFO frequency + phase: The phase of the LFO oscillations, defined in terms of fraction of a whole cycle. + A: Attack, how long it takes for the LFO depth to rise to 100% of the `level` after it’s triggered. + D: Decay, how long it takes for the LFO depth to die down to the `Sustain` value after the `Attack` period. + S: Sustain, the LFO depth (from 0 to 1.0) maintained after the `Decay` period, while the note is held. + R: Release, how long LFO depth takes to finally die to 0 once the note is released. + Ac: Curvature of the attack portion of a note. Values from -1 to 1, positive indicates increases quickly then slow, + negative slowly then quick. a value of 0 is a linear attack, increasing in volume at a constant rate. + Dc: Curvature of the Decay portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, + negative slowly then quick. a value of 0 is a linear decay, decreasing in volume at a constant rate. + Rc: Curvature of the release portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, + negative slowly then quick. a value of 0 is a linear release, decreasing in volume at a constant rate. + level: Total level of the envelope from 0 to 1, contolling maximum depth of the LFO. + volume: >- + Master Volume of generator + interpolation_type: >- + How to interpolate and resample points in the spectrum. "sample": interpolate spectrum values directly; + "preserve_power": integrate, interpolate then differentiate to avoid missing power in narrow features. + regen_phases: >- + Boolean, for an evolving spectrum, do we regenerate phases for each buffer, or keep the same? + fit_spec_multiples: >- + Boolean, whether or not to generate IFFT such that the spectrum sample points are hit exactly. + min_freq: >- + Minimum sound frequency used to represent the data + max_freq: >- + Maximum sound frequency used to represent the data + pitch: >- + Default pitch selection (used by all generators) + azimuth: >- + Azimuth coordinate for spatialising audio into differing channels + polar: >- + Polar coordinate for spatialising audio into differing channels + pitch_hi: >- + Pitch range maximum in semitones + pitch_lo: >- + Pitch range minimum in semitones + pitch_shift: >- + Default shift in semitones + equal_loudness_normalisation: >- + Boolean, whether or not the spectrum is _Equalised_ such that single tones at different frequencies should sound equally + loud (to the average listener, at a default loudness of 70 phon), following [ISO:226](https://www.iso.org/standard/83117.html). + \ No newline at end of file diff --git a/src/strauss/presets/spec/ranges/default.md b/src/strauss/presets/spec/ranges/default.md new file mode 100644 index 0000000..4c5bea3 --- /dev/null +++ b/src/strauss/presets/spec/ranges/default.md @@ -0,0 +1,70 @@ +## Note_Length +Numerical note length +## Note_Length_Units +Units for numerical note length, e.g. seconds +## Volume_Envelope +### A +Attack +### D +Decay +### S +Sustain +### R +Release +### Ac + +### Dc + +### Rc + +### Level + +### A_Unit +units for Attack +### D_Unit +units for Decay +### S_Unit +units for Sustain +### R_Unit +units for Release +### Ac_Unit +units for Ac +### Dc_Unit +units for Dc +### Rc_Unit +units for Rc +### Level_Unit +units for level + +## Cutoff +filter cutoff +## Cutoff_Unit +units for filter cutoff +## Volume +Master volume +## Volume_Unit +Units for master volume +## Min_Freq +Minimum frequency +## Max_Freq +Maximum frequency +## Min_Freq_Unit +Units for minimum frequency +## Max_Freq_Unit +Units for maximum frequency +## Pitch +Default pitch selection +## Pitch_Unit +Units for Default pitch selection +## Phi +phi coordinate for center panning +## Phi_Unit +units for phi coordinate for center panning +## Theta +theta coordinate for center panning +## Theta_Unit +units for theta coordinate for center panning +## Pitch_Shift +default shift in semitones +## Pitch_Shift_Unit +units for default shift in semitones diff --git a/src/strauss/presets/spec/ranges/default.yml b/src/strauss/presets/spec/ranges/default.yml index e593f07..f022e5b 100644 --- a/src/strauss/presets/spec/ranges/default.yml +++ b/src/strauss/presets/spec/ranges/default.yml @@ -23,13 +23,60 @@ volume_envelope: Dc_unit: 'unitless' Rc_unit: 'unitless' level_unit: 'unitless' - + +volume_lfo: + A: [1e-2, 20.] + D: [1e-2, 20.] + S: [0., 1.] + R: [1e-2, 20] + Ac: [-1. ,1.] + Dc: [-1. ,1.] + Rc: [-1. ,1.] + level: [0., 1.] + amount: [0, 1] + freq: [1., 12.] + freq_shift: [0., 3.] + A_unit: 'seconds' + D_unit: 'seconds' + S_unit: 'unitless' + R_unit: 'seconds' + Ac_unit: 'unitless' + Dc_unit: 'unitless' + Rc_unit: 'unitless' + level_unit: 'unitless' + freq_unit: 'Hz' + freq_shift_unit: 'octave' + +pitch_lfo: + A: [1e-2, 20.] + D: [1e-2, 20.] + S: [0., 1.] + R: [1e-2, 20] + Ac: [-1. ,1.] + Dc: [-1. ,1.] + Rc: [-1. ,1.] + level: [0., 1.] + amount: [0, 2] + freq: [1., 12.] + freq_shift: [0., 3.] + A_unit: 'seconds' + D_unit: 'seconds' + S_unit: 'unitless' + R_unit: 'seconds' + Ac_unit: 'unitless' + Dc_unit: 'unitless' + Rc_unit: 'unitless' + level_unit: 'unitless' + amount_unit: 'semitones' + freq_unit: 'Hz' + freq_shift_unit: 'octave' + # filter cutoff cutoff: [0., 1.] cutoff_unit: 'unitless' # Master volume -volume: [0,1.] +volume: [0., 1.] volume_unit: 'unitless' # frequency limits in Hz @@ -44,10 +91,95 @@ pitch_unit: 'unitless' # center panning: phi: [0., 1.] -phi_unit: 'unitless' +phi_unit: 'half-cycles (π)' theta: [0.,1.] -theta_unit: 'unitless' +theta_unit: 'cycles (2π)' # pitch range and default shift in semitones -pitch_shift: [0., 36.] +pitch_shift: [0., 24.] pitch_shift_unit: 'semitones' + +# Do we apply a filter, and if so specify the cutoff and filter type +filter: "off" +filter_type: "LPF1" +cutoff: 1. + +# or 'vibrato' +pitch_lfo: + amount: [0, 2] + freq: [1,12] + freq_shift: [0,3] + A: [1e-2, 10] + D: [1e-2, 10] + S: [0, 1] + R: [1e-2, 10] + freq_unit: "Hz" + amount_unit: "semitones" + +volume_lfo: + amount: [0, 1] + freq: [1,12] + freq_shift: [0,3] + A: [1e-2, 10] + D: [1e-2, 10] + S: [0, 1] + R: [1e-2, 10] + freq_unit: "Hz" + amount_unit: "fraction" + use: off + wave: 'sine' + amount: 0.5 + freq: 3 + freq_shift: 0 + phase: 'random' + A: 0. + D: 0.1 + S: 1. + R: 0. + Ac: 0. + Dc: 0. + Rc: 0. + level: 1 + +_meta: + note_length: >- + Numerical note length + note_length_units: >- + Units for numerical note length, e.g. seconds + volume_envelope: >- + define the note volume envelope applied to the samples + A,D,S & R correspond to 'attack', 'decay', 'sustain' and 'release' + volume_envelope: + A: Attack + D: Decay + S: Sustain + R: Release + Ac: "" + Dc: "" + Rc: "" + level: "" + A_unit: units for Attack + D_unit: units for Decay + S_unit: units for Sustain + R_unit: units for Release + Ac_unit: units for Ac + Dc_unit: units for Dc + Rc_unit: units for Rc + level_unit: units for level + cutoff: filter cutoff + cutoff_unit: units for filter cutoff + volume: Master volume + volume_unit: Units for master volume + min_freq: Minimum frequency + max_freq: Maximum frequency + min_freq_unit: Units for minimum frequency + max_freq_unit: Units for maximum frequency + pitch: Default pitch selection + pitch_unit: Units for Default pitch selection + phi: phi coordinate for center panning + phi_unit: units for phi coordinate for center panning + theta: theta coordinate for center panning + theta_unit: units for theta coordinate for center panning + pitch_shift: default shift in semitones + pitch_shift_unit: units for default shift in semitones + diff --git a/src/strauss/presets/synth/default.md b/src/strauss/presets/synth/default.md new file mode 100644 index 0000000..cf3eafd --- /dev/null +++ b/src/strauss/presets/synth/default.md @@ -0,0 +1,138 @@ +## _Meta +### Name +preset name +### Description +full description +### Oscillators +#### Osc1 +##### Form +waveform, choose from ['saw', 'square', 'sine', 'tri', 'noise'] +##### Level +intrinsic volume +##### Detune +change in tuning as a percentage of the input frequency +##### Phase +phase + +#### Osc2 +##### Form +waveform, choose from ['saw', 'square', 'sine', 'tri', 'noise'] +##### Level +intrinsic volume +##### Detune +change in tuning as a percentage of the input frequency +##### Phase +phase + +#### Osc3 +##### Form +waveform, choose from ['saw', 'square', 'sine', 'tri', 'noise'] +##### Level +intrinsic volume +##### Detune +change in tuning as a percentage of the input frequency +##### Phase +phase + + +### Note_Length +Numerical note length in s +### Volume_Envelope +#### A +Attack +#### D +Decay +#### S +Sustain +#### R +Release +#### Ac + +#### Dc + +#### Rc + +#### Level + + +### Filter +Do we apply a filter? +### Filter_Type +filter type +### Cutoff +filter cutoff +### Pitch_Lfo +#### Use +switch feature on or off +#### Wave +type of waveform +#### Amount +amount +#### Freq +frequency +#### Freq_Shift +frequency shift +#### Phase +random +#### A +Attack +#### D +Decay +#### S +Sustain +#### R +Release +#### Ac + +#### Dc + +#### Rc + +#### Level + + +### Volume_Lfo +#### Use +switch feature on or off +#### Wave +type of waveform +#### Amount +amount +#### Freq +frequency +#### Freq_Shift +frequency shift +#### Phase +random +#### A +Attack +#### D +Decay +#### S +Sustain +#### R +Release +#### Ac + +#### Dc + +#### Rc + +#### Level + + +### Volume +Master Volume +### Pitch +Default pitch selection +### Azimuth +azimuth coordinate for center panning +### Polar +polar coordinate for center panning +### Pitch_Hi +pitch range maximum in semitones +### Pitch_Lo +pitch range minimum in semitones +### Pitch_Shift +default shift in semitones + diff --git a/src/strauss/presets/synth/default.yml b/src/strauss/presets/synth/default.yml index a1ca87e..aa4d518 100644 --- a/src/strauss/presets/synth/default.yml +++ b/src/strauss/presets/synth/default.yml @@ -109,3 +109,119 @@ pitch_hi: 0.1 pitch_lo: 0 pitch_shift: 0. +_meta: + _doc: >- + The `Synth` generator type can be used to synthesise sound using mathematically + generated waveforms or `oscillators`. The preset can be used to modify the relative + frequency, phase and amplitude of these oscillators. + name: Name of the preset + description: Full description of the preset purpose and parameters. + oscillators: + _doc: >- + Oscillator information. Oscillator are denoted `osc`, allowing an arbitrary number + of oscillators to be combined to make the intrtinsic tone. The `default` preset + demontrates this using 3 sawtooth oscillators, slightly detuned from each other to + create a 'detuned saw' sound, hence the identically structured oscillators below. + osc1: + form: Type of waveform used for oscillator 1, choose from ['saw', 'square', 'sine', 'tri', 'noise'] + level: Amplitude of the oscillator from 0 to 1, contolling maximum volume of the note + detune: Change in tuning as a percentage of the input frequency + phase: The phase of the oscillator, defined in terms of fraction of a whole cycle + osc2: + form: Type of waveform used for oscillator 2, choose from ['saw', 'square', 'sine', 'tri', 'noise'] + level: Amplitude of the oscillator from 0 to 1, contolling maximum volume of the note + detune: Change in tuning as a percentage of the input frequency + phase: The phase of the oscillator, defined in terms of fraction of a whole cycle + osc3: + form: Type of waveform used for oscillator 3, choose from ['saw', 'square', 'sine', 'tri', 'noise'] + level: Amplitude of the oscillator from 0 to 1, contolling maximum volume of the note + detune: Change in tuning as a percentage of the input frequency + phase: The phase of the oscillator, defined in terms of fraction of a whole cycle + + note_length: >- + Numerical note length in seconds + volume_envelope: >- + Define the note volume envelope applied to the samples. + A,D,S & R correspond to 'attack', 'decay', 'sustain' and 'release'. + volume_envelope: + _doc: >- + Define the note volume envelope applied to the samples. _'ADSR'_ is a common parametrisation in sound synthesis, + find out more e.g. [at this link](https://blg.native-instruments.com/adsr-explained/) + A: Attack, how long it takes for a sound to rise to 100% of the `level` after it’s triggered. + D: Decay, how long it takes for the sounds volume to die down to the `Sustain` value after the `Attack` period. + S: Sustain, the volume level (from 0 to 1.0) maintained after the `Decay` period, while the note is held. + R: Release, how long the tone takes to finally die away once the note is released. + Ac: Curvature of the attack portion of a note. Values from -1 to 1, positive indicates increases quickly then slow, + negative slowly then quick. a value of 0 is a linear attack, increasing in volume at a constant rate. + Dc: Curvature of the Decay portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, + negative slowly then quick. a value of 0 is a linear decay, decreasing in volume at a constant rate. + Rc: Curvature of the release portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, + negative slowly then quick. a value of 0 is a linear release, decreasing in volume at a constant rate. + level: Total level of the envelope from 0 to 1, contolling maximum depth of the LFO. + filter: >- + Do we apply a frequency filter to the audio signal? This can be used to change the balance of frequencies and manipulate the 'timbre' of a note + filter_type: >- + Choose from available filter types + cutoff: >- + The cut-off frequency (or `knee`) of the filter, beyond which frequencies are attenuated. + Specified between 0 and 1 as a fraction of the audible range of notes we can hear (E0 to D#10). + pitch_lfo: + _doc: >- + Controls for the 'Low Frequency Oscillator' (LFO) used to modulate pitch of notes at rhythmic + frequencies. In music this is often referred to as 'vibrato' + use: Switch pitch LFO effects on or off + wave: >- + Type of waveform used for the oscillator. As with `Synthesizer` oscillators can be sawtooth + (`'saw'`), square (`'square'`), sinusoid (`'sine'`), triangle (`'tri'`) or noise (`'noise'`). + amount: The amplitude of the maximal pitch oscillation from the underlying pitch + freq: Base frequency of the LFO oscillations. + freq_shift: Shift relative to the bae LFO frequency + phase: The phase of the LFO oscillations, defined in terms of fraction of a whole cycle + A: Attack, how long it takes for the LFO depth to rise to 100% of the `level` after it’s triggered. + D: Decay, how long it takes for the LFO depth to die down to the `Sustain` value after the `Attack` period. + S: Sustain, the LFO depth (from 0 to 1.0) maintained after the `Decay` period, while the note is held. + R: Release, how long LFO depth takes to finally die to 0 once the note is released. + Ac: >- + "Curvature" of the attack portion of a note. Values from -1 to 1, positive indicates increases quickly then slow, negative slowly then quick. a value of 0 is a linear attack, increasing in volume at a constant rate. + Dc: >- + "Curvature" of the Decay portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, negative slowly then quick. a value of 0 is a linear decay, decreasing in volume at a constant rate. + Rc: >- + "Curvature" of the release portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, negative slowly then quick. a value of 0 is a linear release, decreasing in volume at a constant rate. + level: Total amplitude level of the envelope from 0 to 1, contolling maximum volume of the note + volume_lfo: + _doc: >- + Controls for the 'Low Frequency Oscillator' (LFO) used to modulate volume of notes at rhythmic + frequencies. In music this is often referred to as 'tremolo'. + use: Switch pitch LFO effects on or off + wave: >- + Type of waveform used for the oscillator. As with `Synthesizer` oscillators can be sawtooth + (`'saw'`), square (`'square'`), sinusoid (`'sine'`), triangle (`'tri'`) or noise (`'noise'`). + amount: the amplitude of the maximal volume oscillation from the underlying pitch + freq: Base frequency of the LFO oscillations. + freq_shift: shift relative to the bae LFO frequency + phase: The phase of the LFO oscillations, defined in terms of fraction of a whole cycle + A: Attack, how long it takes for a tone to sound after it’s triggered + D: Decay, how long it takes for the tone’s attack to die down after it’s triggered + S: Sustain, the volume/level of the sound while it’s being triggered + R: Release, how long the tone takes to go silent after the trigger is released + Ac: >- + "Curvature" of the attack portion of a note. Values from -1 to 1, positive indicates increases quickly then slow, negative slowly then quick. a value of 0 is a linear attack, increasing in volume at a constant rate. + Dc: >- + "Curvature" of the Decay portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, negative slowly then quick. a value of 0 is a linear decay, decreasing in volume at a constant rate. + Rc: >- + "Curvature" of the release portion of a note. Values from -1 to 1, positive indicates decreases quickly then slow, negative slowly then quick. a value of 0 is a linear release, decreasing in volume at a constant rate. + level: Total level of the envelope from 0 to 1, contolling maximum depth of the LFO. + volume: >- + Master Volume of synthesizer + pitch: >- + Default pitch selection + azimuth: >- + Azimuth coordinate for spatialising audio into differing channels + polar: >- + Polar coordinate for spatialising audio into differing channels + pitch_hi: >- + Pitch range maximum in semitones + pitch_lo: >- + Pitch range minimum in semitones + pitch_shift: >- + Default shift in semitones \ No newline at end of file diff --git a/src/strauss/presets/synth/pitch_mapper.md b/src/strauss/presets/synth/pitch_mapper.md new file mode 100644 index 0000000..eb75c6b --- /dev/null +++ b/src/strauss/presets/synth/pitch_mapper.md @@ -0,0 +1,22 @@ +## Name +preset name +## Description +full description +## Oscillators +### Osc1 +#### Form +waveform, choose from ['saw', 'square', 'sine', 'tri', 'noise'] +#### Level +intrinsic volume +#### Detune +change in tuning as a percentage of the input frequency +#### Phase +phase + + +## Pitch_Hi +pitch range maximum in semitones +## Pitch_Lo +pitch range minimum in semitones +## Pitch_Shift +default shift in semitones diff --git a/src/strauss/presets/synth/pitch_mapper.yml b/src/strauss/presets/synth/pitch_mapper.yml index 590080b..5031924 100644 --- a/src/strauss/presets/synth/pitch_mapper.yml +++ b/src/strauss/presets/synth/pitch_mapper.yml @@ -20,4 +20,21 @@ pitch_hi: 36 pitch_lo: 0 pitch_shift: 0. - +_meta: + name: preset name + description: full description + oscillators: >- + Oscillator information. + oscillators: + osc1: + form: waveform, choose from ['saw', 'square', 'sine', 'tri', 'noise'] + level: intrinsic volume + detune: change in tuning as a percentage of the input frequency + phase: phase + pitch_hi: >- + pitch range maximum in semitones + pitch_lo: >- + pitch range minimum in semitones + pitch_shift: >- + default shift in semitones + diff --git a/src/strauss/presets/synth/ranges/default.md b/src/strauss/presets/synth/ranges/default.md new file mode 100644 index 0000000..f6caa40 --- /dev/null +++ b/src/strauss/presets/synth/ranges/default.md @@ -0,0 +1,62 @@ +## Note_Length +Numerical note length +## Note_Length_Units +Units for numerical note length, e.g. seconds +## Volume_Envelope +### A +Attack +### D +Decay +### S +Sustain +### R +Release +### Ac + +### Dc + +### Rc + +### Level + +### A_Unit +units for Attack +### D_Unit +units for Decay +### S_Unit +units for Sustain +### R_Unit +units for Release +### Ac_Unit +units for Ac +### Dc_Unit +units for Dc +### Rc_Unit +units for Rc +### Level_Unit +units for level + +## Cutoff +filter cutoff +## Cutoff_Unit +units for filter cutoff +## Volume +Master volume +## Volume_Unit +Units for master volume +## Pitch +Default pitch selection +## Pitch_Unit +Units for Default pitch selection +## Azimuth +azimuth coordinate for center panning +## Azimuth_Unit +units for azimuth coordinate for center panning +## Polar +polar coordinate for center panning +## Polar_Unit +units for polar coordinate for center panning +## Pitch_Shift +default shift in semitones +## Pitch_Shift_Unit +units for default shift in semitones diff --git a/src/strauss/presets/synth/ranges/default.yml b/src/strauss/presets/synth/ranges/default.yml index 7d71059..3431fe8 100644 --- a/src/strauss/presets/synth/ranges/default.yml +++ b/src/strauss/presets/synth/ranges/default.yml @@ -23,7 +23,55 @@ volume_envelope: Dc_unit: 'unitless' Rc_unit: 'unitless' level_unit: 'unitless' - + +volume_lfo: + A: [1e-2, 20.] + D: [1e-2, 20.] + S: [0., 1.] + R: [1e-2, 20] + Ac: [-1. ,1.] + Dc: [-1. ,1.] + Rc: [-1. ,1.] + level: [0., 1.] + amount: [0, 1] + freq: [1., 12.] + freq_shift: [0., 3.] + A_unit: 'seconds' + D_unit: 'seconds' + S_unit: 'unitless' + R_unit: 'seconds' + Ac_unit: 'unitless' + Dc_unit: 'unitless' + Rc_unit: 'unitless' + level_unit: 'unitless' + freq_unit: 'Hz' + freq_shift_unit: 'octave' + +pitch_lfo: + A: [1e-2, 20.] + D: [1e-2, 20.] + S: [0., 1.] + R: [1e-2, 20] + Ac: [-1. ,1.] + Dc: [-1. ,1.] + Rc: [-1. ,1.] + level: [0., 1.] + amount: [0, 2] + freq: [1., 12.] + freq_shift: [0., 3.] + A_unit: 'seconds' + D_unit: 'seconds' + S_unit: 'unitless' + R_unit: 'seconds' + Ac_unit: 'unitless' + Dc_unit: 'unitless' + Rc_unit: 'unitless' + level_unit: 'unitless' + amount_unit: 'semitones' + freq_unit: 'Hz' + freq_shift_unit: 'octave' + + # filter cutoff cutoff: [0., 1.] cutoff_unit: 'unitless' @@ -43,5 +91,44 @@ polar: [0.,1.] polar_unit: 'unitless' # pitch range and default shift in semitones -pitch_shift: [0., 36.] +pitch_shift: [0., 24.] pitch_shift_unit: 'semitones' + +_meta: + note_length: >- + Numerical note length + note_length_units: >- + Units for numerical note length, e.g. seconds + volume_envelope: >- + define the note volume envelope applied to the samples + A,D,S & R correspond to 'attack', 'decay', 'sustain' and 'release' + volume_envelope: + A: Attack + D: Decay + S: Sustain + R: Release + Ac: "" + Dc: "" + Rc: "" + level: "" + A_unit: units for Attack + D_unit: units for Decay + S_unit: units for Sustain + R_unit: units for Release + Ac_unit: units for Ac + Dc_unit: units for Dc + Rc_unit: units for Rc + level_unit: units for level + cutoff: filter cutoff + cutoff_unit: units for filter cutoff + volume: Master volume + volume_unit: Units for master volume + pitch: Default pitch selection + pitch_unit: Units for Default pitch selection + azimuth: azimuth coordinate for center panning + azimuth_unit: units for azimuth coordinate for center panning + polar: polar coordinate for center panning + polar_unit: units for polar coordinate for center panning + pitch_shift: default shift in semitones + pitch_shift_unit: units for default shift in semitones + diff --git a/src/strauss/presets/synth/spectraliser.md b/src/strauss/presets/synth/spectraliser.md new file mode 100644 index 0000000..cb53ecb --- /dev/null +++ b/src/strauss/presets/synth/spectraliser.md @@ -0,0 +1,16 @@ +## Name +preset name +## Description +full description +## Oscillators +### Osc1 +#### Form +waveform, choose from ['saw', 'square', 'sine', 'tri', 'noise'] +#### Level +intrinsic volume +#### Detune +change in tuning as a percentage of the input frequency +#### Phase +phase + + diff --git a/src/strauss/presets/synth/spectraliser.yml b/src/strauss/presets/synth/spectraliser.yml index 9e8d77f..3a1d13a 100644 --- a/src/strauss/presets/synth/spectraliser.yml +++ b/src/strauss/presets/synth/spectraliser.yml @@ -14,4 +14,15 @@ oscillators: detune: 0. phase: 'random' - +_meta: + name: preset name + description: full description + oscillators: >- + Oscillator information + oscillators: + osc1: + form: waveform, choose from ['saw', 'square', 'sine', 'tri', 'noise'] + level: intrinsic volume + detune: change in tuning as a percentage of the input frequency + phase: phase + diff --git a/src/strauss/presets/synth/windy.md b/src/strauss/presets/synth/windy.md new file mode 100644 index 0000000..60f909d --- /dev/null +++ b/src/strauss/presets/synth/windy.md @@ -0,0 +1,22 @@ +## Name +preset name +## Description +full description +## Oscillators +### Osc1 +#### Form +waveform, choose from ['saw', 'square', 'sine', 'tri', 'noise'] +#### Level +intrinsic volume +#### Detune +change in tuning as a percentage of the input frequency +#### Phase +phase + + +## Filter +Do we apply a filter? +## Filter_Type +filter type +## Cutoff +filter cutoff diff --git a/src/strauss/presets/synth/windy.yml b/src/strauss/presets/synth/windy.yml index 3397cc6..4a6f918 100644 --- a/src/strauss/presets/synth/windy.yml +++ b/src/strauss/presets/synth/windy.yml @@ -18,4 +18,21 @@ filter: "on" filter_type: "LPF1" cutoff: 0.3 - +_meta: + name: preset name + description: full description + oscillators: >- + Oscillator information + oscillators: + osc1: + form: waveform, choose from ['saw', 'square', 'sine', 'tri', 'noise'] + level: intrinsic volume + detune: change in tuning as a percentage of the input frequency + phase: phase + filter: >- + Do we apply a filter? + filter_type: >- + filter type + cutoff: >- + filter cutoff + diff --git a/src/strauss/score.py b/src/strauss/score.py index 8c5d3ed..27783f9 100644 --- a/src/strauss/score.py +++ b/src/strauss/score.py @@ -37,28 +37,29 @@ class Score: D9_3 | Gmaj7_2"` plays each chord for 20s each. Chaining the same chord can be used to change intervals, (e.g. :obj:`chord_sequence = "F_3 | F_3 | C_4"` plays F for - 40s and C for 20s.) - - Args: - chord_sequence: (:obj:`str` or :obj:`list`): The chord or chord - sequence used for the sonification. If a string, parse using - :obj:`parse_chord_sequence`. If a :obj:`list`, each entry is - a :obj:`list` of strings or floats, representing the notes of a - chord. notes are represented as strings using scientific - notation, e.g. :obj:`[['C3','E3', 'G3'], ['C3', 'F3', 'A4']]`. If - floats, take values as note frequency in Hz. NOTE: currently - only supported in compination with the :obj:`Synthesiser` - generator class. - length: (:obj:`str` or :obj:`float`): the length of the - sonification. If a string, parse minutes and seconds from - format 'Xm Y.Zs'. If a float read as seconds. - pitch_binning (optional, :obj:`str`): pitch binning mode - choose - from 'adaptive', where sources are binned by the pitch mapping - such that each interval is represented the same fraction of the - time, and 'uniform' where the pitch binning is based on uniform - size bins in the mapped pitch parameter. + 40s and C for 20s.) """ def __init__(self, chord_sequence, length, pitch_binning='adaptive'): + """ + Args: + chord_sequence: (:obj:`str` or :obj:`list`): The chord or chord + sequence used for the sonification. If a string, parse using + :obj:`parse_chord_sequence`. If a :obj:`list`, each entry is + a :obj:`list` of strings or floats, representing the notes of a + chord. notes are represented as strings using scientific pitch + notation, e.g. :obj:`[['C3','E3', 'G3'], ['C3', 'F3', 'A4']]`. + If floats, take values as note frequency in Hz. NOTE: currently + only supported in combination with the :obj:`Synthesiser` + generator class. + length: (:obj:`str` or :obj:`float`): the length of the + sonification. If a string, parse minutes and seconds from + format 'Xm Y.Zs'. If a float, read as seconds. + pitch_binning (optional, :obj:`str`): pitch binning mode - choose + from 'adaptive', where sources are binned by the pitch mapping + such that each interval is represented the same fraction of the + time, and 'uniform' where the pitch binning is based on uniform + size bins in the mapped pitch parameter. + """ # check types to handle score length correctly if isinstance(length, str): regex = "([0-9]*)m\s*([0-9]*.[0-9]*)s" @@ -97,8 +98,9 @@ def parse_chord_sequence(chord_sequence): Returns: note_list (:obj:`list(list)`): the chord sequence represented as - a list of lists, where each sub-list is a chord comprised of - strings representing each note in scentific notation (e.g. 'A4') + a list of lists, where each sub-list is a chord comprised of + strings representing each note in scientific pitch notation + (e.g. 'A4') """ chord_list = chord_sequence.split("|") note_list = [] diff --git a/src/strauss/sonification.py b/src/strauss/sonification.py index ce17480..02801ba 100644 --- a/src/strauss/sonification.py +++ b/src/strauss/sonification.py @@ -19,7 +19,6 @@ from .tts_caption import render_caption import numpy as np import matplotlib.pyplot as plt -from tqdm import tqdm import sys import os import ffmpeg as ff @@ -34,6 +33,10 @@ import sounddevice as sd except (OSError, ModuleNotFoundError) as sderr: sd = NoSoundDevice(sderr) +try: + from tqdm import tqdm +except ModuleNotFoundError: + tqdm = list class Sonification: """Representing the overall sonification @@ -43,22 +46,6 @@ class Sonification: sonification for saving or playing in the :obj:`jupyter-notebook` environment - Args: - score (:class:`~strauss.score.Score`): Sonification :obj:`Score` - object - sources (:class:`~strauss.sources.Source`): Sonification - :obj:`Sources` child object (:class:`~strauss.sources.Events` - or :class:`~strauss.sources.Objects`) - generator (:class:`~strauss.generator.Generator`): Sonification - :obj:`Generator` child object - (:class:`~strauss.generator.Synthesizer` or - :class:`~strauss.generator.Sampler`) - audio_setup (:obj:`str`) The requested audio setup preset to - pass to :class:`~strauss.channels.audio_channels` - samprate (:obj:`int`) Integer sample rate in samples per second - (Hz), typically :obj:`44100` or :obj:`48000` for most audio - applications - ttsmodel (:obj:`str`) The text-to-speech model used for captions. Todo: * Support custom audio setups here too. @@ -66,6 +53,25 @@ class Sonification: def __init__(self, score, sources, generator, audio_setup='stereo', caption=None, samprate=48000, ttsmodel=Path('tts_models','en','jenny', 'jenny')): + """ + Args: + score (:class:`~strauss.score.Score`): Sonification :obj:`Score` + object + sources (:class:`~strauss.sources.Source`): Sonification + :obj:`Sources` child object (:class:`~strauss.sources.Events` + or :class:`~strauss.sources.Objects`) + generator (:class:`~strauss.generator.Generator`): Sonification + :obj:`Generator` child object + (:class:`~strauss.generator.Synthesizer` or + :class:`~strauss.generator.Sampler`) + audio_setup (:obj:`str`) The requested audio setup preset to + pass to :class:`~strauss.channels.audio_channels` + samprate (:obj:`int`) Integer sample rate in samples per second + (Hz), typically :obj:`44100` or :obj:`48000` for most audio + applications + ttsmodel (:obj:`str` or :obj:`PosixPath`) file path to the + text-to-speech model used for captions. + """ # sampling rate in Hz self.samprate = samprate @@ -112,8 +118,8 @@ def render(self, downsamp=1): Args: downsamp (optional, :obj:`int`): Optionally downsample - sources for multi-source sonifications for a quicker test - render by some integer factor. + sources for multi-source sonifications for a quicker test + render by some integer factor. """ # first determine if time is provided, if not assume all start at zero @@ -265,10 +271,6 @@ def save_combined(self, fname, ffmpeg_output=False, master_volume=1.): output to screen master_volume (:obj:`float`) Amplitude of the largest volume peak, from 0-1 - - Todo: - * Either find a way to avoid the need to unscramble channle - order, or find alternative to save wav files """ # setup list to house wav stream data inputs = [None]*len(self.out_channels) @@ -310,7 +312,7 @@ def save_combined(self, fname, ffmpeg_output=False, master_volume=1.): print("Saved.") def save(self, fname, master_volume=1.): - """ Save render as a combined multi-channel wav file + """ Save render as a combined multi-channel wav file. Can use this function to save sonification of any audio_setup to a 32-bit depth WAV using `scipy.io.wavfile` @@ -396,8 +398,14 @@ def notebook_display(self, show_waveform=True): display(ipd.Audio(outfmt,rate=self.out_channels['0'].samprate, autoplay=False)) def hear(self): - """ Play audio directly to the sound device, for command-line - playback. + """ Play audio directly to the sound device, for command-line playback. + + If available, use the ``sounddevice`` module to stream the sonification to + the sound device directly (speakers, headphones, etc.) via the underlying + ``PortAudio`` C-library. if unavaialable, raise error. + + Todo: + * Add more options to control the streamed audio """ channels = [] @@ -434,6 +442,14 @@ def hear(self): "\t 'sudo apt-get install libportaudio2.'\n") def _make_seamless(self, overlap_dur=0.05): + """ Make a seamlessly looping audio signal. + + Audio signal is made seamless by cross-fading end of signal back into start + over a duration (in seconds) defined by ``overlap_dur`` + + Args: + overlap_dur (:obj:`float`): cross-fade duration in seconds. + """ self.loop_channels = {} buffsize = int(overlap_dur*self.samprate) ramp = np.linspace(0,1, buffsize+1) diff --git a/src/strauss/sources.py b/src/strauss/sources.py index 813c182..1fb720b 100644 --- a/src/strauss/sources.py +++ b/src/strauss/sources.py @@ -46,7 +46,7 @@ 'volume_lfo/amount', 'pitch_lfo/freq', 'pitch_lfo/freq_shift', - 'pitch_lfo/amount'] + 'pitch_lfo/amount'] evolvable = ['polar', 'azimuth', @@ -79,10 +79,10 @@ (1e-2, 10), (1,12), (0,3), - (0,2), + (0,1), (1,12), (0,3), - (0,1)] + (0,2)] param_lim_dict = dict(zip(mappable, param_limits)) @@ -96,15 +96,24 @@ class Source: `Source` isn't used directly, instead use child classes `Events` or `Objects`. - Args: - mapped_quantities (:obj:`list(str)`): The subset of parameters to - which data will be mapped. - + Attributes: + mapped_quantities (:obj:`list(str)`): The subset of parameters to + which data will be mapped. + raw_mapping (:obj:`dict`): Housing the input mapped parameters + and data, with keys corresponding to :obj:`mapped_quantities`. + mapping (:obj:`dict`): processed mapping :obj:`dict` rescaled + to parameter ranges, or interpolation funtions for evolving + parameters. + Raises: - UnrecognisedProperty: if `mapped_quantities` entry not in `mappable`. - + UnrecognisedProperty: if `mapped_quantities` entry not in `mappable`. """ def __init__(self, mapped_quantities): + """ + Args: + mapped_quantities (:obj:`list(str)`): The subset of parameters to + which data will be mapped. + """ # check these are all mappable parameters @@ -131,7 +140,6 @@ def __init__(self, mapped_quantities): self.mapped_quantities = mapped_quantities self.raw_mapping = {} self.mapping = {} - self.mapping_evo = {} def apply_mapping_functions(self, map_funcs={}, map_lims={}, param_lims={}): """ Taking input data and mapping to parameters. @@ -141,7 +149,7 @@ def apply_mapping_functions(self, map_funcs={}, map_lims={}, param_lims={}): function (x' = x by default), descaling by the x' upper and lower limits and rescaling to the sonification parameter limits. These values are stored for non-evolving parameters, - while for evolving properties are converted to interpolation + while for evolving properties they are converted to interpolation functions. Args: @@ -254,7 +262,8 @@ class Events(Source): Child class of `Source`, for `Event`-type sources. Each `Event` is discrete in `time` with single data values mapped to each - sonification parameter. + sonification parameter. + """ def fromfile(self, datafile, coldict): """Take input data from ASCII file diff --git a/src/strauss/stream.py b/src/strauss/stream.py index 9c9bda6..797cd01 100644 --- a/src/strauss/stream.py +++ b/src/strauss/stream.py @@ -1,14 +1,46 @@ +""" The :obj:`stream` submodule: representing the sound signal + +Containing the ``Stream`` class to house the ``Sonificiation`` audio +signal for each channel in the ``Channels`` object. This can be +split into uniform segments or `buffers` via the ``Buffers`` object, +for processing. + +Todo: + * implement filter Q-parameter mapping +""" import numpy as np import wavio import matplotlib.pyplot as plt from scipy.signal.windows import hann -# To Do -# - implement filter Q-parameter mapping class Stream: - """ Stream object representing audio samples""" + """ + Stream object representing audio samples. + + Houses audio samples and associates metadata representing the + actual audio signal produced by the `Generator` class and output + via the `audio_channels` class. + + Attributes: + samprate (:obj:`int`): Samples per second of audio stream (Hz) + length (:obj:`float`): Duration of the stream in seconds + values (:obj:`ndarray`): Values of individual samples + samples (:obj:`ndarray`): Indices of each sample + samptype (:obj:`ndarray`): Time in seconds each sample occurs + buffers (:obj:`Buffers`): Buffered stream if generated + """ def __init__(self, length, samprate=44100, ltype='seconds'): - + """ + Args: + length (numerical): Number representing the length of the + stream either as an integer number of samples, or a value + of seconds + samprate (optional :obj:`int`): Samples per second of audio + stream (Hz) + ltype (optional :obj:`str`): quantity represented by + `length`, either duration in 'seconds' or precise number + of 'samples' + """ # variables we want to keep constant self.samprate = samprate self._nyqfrq = 0.5*self.samprate @@ -34,21 +66,42 @@ def __init__(self, length, samprate=44100, ltype='seconds'): self.samptime = self.samples / self.samprate def bufferize(self, bufflength=0.1): - """ wrapper to initialise Buffers subclass """ + """Wrapper to initialise Buffers subclass + + Args: + bufflength (optional, :obj:`float`): duration in seconds of + each buffer to be generated + """ self.buffers = Buffers(self, bufflength) def consolidate_buffers(self): - """ wrapper to reassign stream values to consolidated stream """ + """ + Wrapper to reassign stream values to consolidated buffers + + See :func:`~stream.Buffers.buffers.to_stream` + """ self.values = self.buffers.to_stream() def filt_sweep(self, ffunc, fmap, qmap=lambda x:x*0 + 0.1, flo=20, fhi=2.205e4, qlo=0.5, qhi=10): """ - ffunc: function that applies filter - fmap: mapping function representing filter cutoff sweep - qmap: mapping function for a filters Q parameter, default: lambda:None - flo: lowest frequency of sweep in Hz, default 20 - fhi: highest frequency of sweep in Hz, default 22.05 kHz + Apply time varying filter to buffered stream + + Args: + ffunc (function): function that applies filter + fmap (function): mapping function representing filter cutoff + sweep + qmap (optional, function): mapping function for a filters + Q parameter + flo (optional, :obj:`float`): lowest frequency of sweep in Hz, + default 20 + fhi (optional, :obj:`float`): lowest frequency of sweep in Hz, + default 22.05 kHz + qlo (optional, :obj:`float`): lowest 'Q' value of sweep, + default 0.5 + qhi (optional, :obj:`float`): lowest frequency of sweep, + default 10 + """ if not hasattr(self, "buffers"): Exception("needs bufferized stream, please run 'bufferize' method first.") @@ -77,21 +130,54 @@ def filt_sweep(self, ffunc, fmap, qmap=lambda x:x*0 + 0.1, self.consolidate_buffers() def get_sampfracs(self): + """ Get fractional position of the sample in total stream duration + """ self.sampfracs = np.linspace(0, 1, self.values.size) def save_wav(self, filename): - """ save audio stream to wav file, specified by filename""" + """Save audio stream to wav file, specified by filename + + Args: + filename (:obj:`str`): name of output WAV file + """ wavio.write(filename, self.values, self.samprate, sampwidth=3) def reset(self): - """ zero audio stream and buffers if present """ + """Zero audio stream and buffers if present.""" self.values *= 0. if hasattr(self, "buffers"): self.buffers.buffs_tile *= 0. self.buffers.buffs_olap *= 0. -class Buffers: +class Buffers: + """Audio buffers split into uniform discrete chunks or 'buffers'. + + Audio ~:class:`stream.Stream` as a discrete sequence of individual + 'buffers' of fixed duration (number of samples). This allows time + varying operations in frequency space, such as signal filtering. + Buffers are tiled in a 'brickwork' fashion so they always overlap + with another buffer. + + Attributes: + fade (:obj:`ndarray`): Window function for recombining + overlapping buffers + nsamp_padstream (:obj:`int`): Number of samples needed to split + the stream into discrete buffers of chosen length + nsamp_pad (:obj:`int`): Number of additional samples needed to + add to the original `Stream` size in this case + buffs_tile (:obj:`ndarray`): 2d array of buffers completely + enclosing the stream (number of buffers x samples per buffer) + buffs_olap (:obj:`ndarray`) 2d array of overlap buffers, + allowing for cross fading + """ def __init__(self, stream, bufflength=0.1): + """ + Args: + stream (~:class:`stream.Stream`): Stream object to be + represented using the buffers + bufflength (optional, :obj:`float`): duration in seconds of + each buffer to be generated + """ nbuff = stream.samprate*bufflength if nbuff < 20: Exception(f"Error: buffer length {nbuff} samples below " @@ -115,18 +201,27 @@ def __init__(self, stream, bufflength=0.1): # pad the stream up to an exact multiple of buffer sample length self.nsamp_padstream = self._nbuffs * self._nsamp_buff self.nsamp_pad = self.nsamp_padstream-stream._nsamp_stream - self.olap_pad = self.nsamp_pad-self._nsamp_halfbuff - self.olap_lim = min(stream._nsamp_stream, stream._nsamp_stream+self.olap_pad) + self._olap_pad = self.nsamp_pad-self._nsamp_halfbuff + self._olap_lim = min(stream._nsamp_stream, stream._nsamp_stream+self._olap_pad) # construct tile and overlap buffer arrays self.buffs_tile = np.pad(stream.values, (0,self.nsamp_pad) ).reshape((self._nbuffs, self._nsamp_buff)) - self.buffs_olap = np.pad(stream.values[self._nsamp_halfbuff:self.olap_lim], - (0,max(0, self.olap_pad)) + self.buffs_olap = np.pad(stream.values[self._nsamp_halfbuff:self._olap_lim], + (0,max(0, self._olap_pad)) ).reshape((self._nbuffs-1), self._nsamp_buff) def to_stream(self): - """ reconstruct stream by x-fading buffers """ + """Reconstruct stream by cross-fading buffers + + Takes the `self.buffs_tile` and `self.buffs_olap` arrays and using + the `self.fade` window function, add overlapping sample values + together to yield a 1d array of samples. + + Returns: + out (:obj:`ndarray`): 1d array of sample values representing the + new audio signal for the parent `Stream`. + """ # apply fades to buffers, first special edge cases... self.buffs_tile[0,self._nsamp_halfbuff:] *= self.fade[self._nsamp_halfbuff:] self.buffs_tile[-1,:self._nsamp_halfbuff] *= self.fade[:self._nsamp_halfbuff] diff --git a/src/strauss/tts_caption.py b/src/strauss/tts_caption.py index 8288228..cc47010 100644 --- a/src/strauss/tts_caption.py +++ b/src/strauss/tts_caption.py @@ -1,3 +1,10 @@ +"""The :obj:`tts_caption` submodule: tool for generating spoken captions + +This uses text-to-speech via the the ``TTS`` module to allow captions +represented as strings to be converted to spoken audio to precede the +sonification. +""" + from scipy.io import wavfile from scipy.interpolate import interp1d import numpy as np @@ -25,9 +32,9 @@ def render_caption(caption, samprate, model, caption_path): Args: caption (:obj:`str`): script to be spoken by the TTS voice samprate (:obj:`int`): samples per second - model (:obj:`str`): valid name of TTS voice from the underying TTS + model (:obj:`str`): valid name of TTS voice from the underlying TTS module - model (:obj:`str`): valid name of TTS voice from the underying TTS + model (:obj:`str`): valid name of TTS voice from the underlying TTS module caption_path (:obj:`str`): filepath for spoken caption output ''' diff --git a/src/strauss/utilities.py b/src/strauss/utilities.py index fdfb38a..6da9573 100644 --- a/src/strauss/utilities.py +++ b/src/strauss/utilities.py @@ -1,3 +1,9 @@ +""" The :obj:`utilities` submodule: useful functions for ``strauss`` + +This submodule is for useful utility functions used by other +``strauss`` modules. Generally these are not intended for direct +use by the user. +""" from functools import reduce import operator import numpy as np @@ -8,20 +14,86 @@ import sys from pathlib import Path +# Some utility classes (these may graduate to somewhere else eventually) + class NoSoundDevice: """ - drop-in replacement for sounddevice module if not working, - so can still use other functionality. + Drop-in replacement for sounddevice module if not working, + so we can still use other functionality. + + Attributes: + err (:obj:`Exception`): Error message from trying to import + sounddevice """ def __init__(self, err): self.err = err - def play(self, audio, rate, blocking=1): + def play(self, *args, **kwargs): + """Dummy function replacing `sounddevice.play` when unavailable. + + Args: + *args: arguments (ignored) + **kwargs: keyword-only arguments (ignored) + """ raise self.err - + +class Equaliser: + def __init__(self): + + self.factor_rms = None + parpath = Path(f"{Path(__file__).parent}","data","params.csv") + # Read in parameters for ISO 226:2024 standard. + pars = np.genfromtxt(parpath, delimiter=',', names=True) + self.parfuncs = {} + for c in pars.dtype.names: + if c == 'freq': + continue + self.parfuncs[c] = interp1d(np.log10(pars['freq']), + pars[c], + fill_value='extrapolate') + + def get_relative_loudness_norm(self, freq, phon=70.): + """ + Relative normalisation of sound frequencies + To compensate for pereceptual loudness, following + the ISO 226:2024 standard. + + Args: + freq (:obj:`array-like`) audio frequencies in Hz + phon (:obj:`float`) listening level for a 1 kHz + note + + Returns: + rnorm (:obj:`array-like`) volume normalisation + for spectra + """ + lfreq = np.log10(freq) + L_U = self.parfuncs['L_U'](lfreq) + alpha_f = self.parfuncs['alpha_f'](lfreq) + T_f = self.parfuncs['T_f'](lfreq) + + A = pow(4e-10, 0.3 - alpha_f) + B = pow(10., (phon)*3e-2) - pow(10, 7.2e-2) + C = pow(10., alpha_f * 0.1*(T_f + L_U)) + + L_f = 10*np.log10(A*B + C)/alpha_f - L_U + norm = pow(10., (L_f - phon)/20) + rnorm = norm/norm.max() + return rnorm + + # a load of utility functions used by STRAUSS def nested_dict_reassign(fromdict, todict): - """recurse through dictionaries and sub-dictionaries""" + """ + Recurse through dictionaries and sub-dictionaries in + `fromdict` and reassign equivalent values in `todict` + + Args + fromdict (:obj:`dict`): Dictionary containing values + to assign + todict (:obj:`dict`): Dictionary containing values + to be reassigned + """ for k, v in fromdict.items(): if isinstance(v, dict): # recurse through nested dictionaries @@ -31,7 +103,17 @@ def nested_dict_reassign(fromdict, todict): todict[k] = v def nested_dict_fill(fromdict, todict): - """recurse through dictionaries and sub-dictionaries""" + """ + Recurse through dictionaries and sub-dictionaries in + `fromdict` and assign to any entries missing from + `todict` + + Args: + fromdict (:obj:`dict`): Dictionary containing values + to assign + todict (:obj:`dict`): Dictionary containing values to + be reassigned + """ for k, v in fromdict.items(): if k not in todict: # assign todict value @@ -41,7 +123,19 @@ def nested_dict_fill(fromdict, todict): nested_dict_fill(todict[k], v) def nested_dict_idx_reassign(fromdict, todict, idx): - """recurse through dictionaries and sub-dictionaries""" + """ + Recurse through dictionaries and sub-dictionaries of + iterables in `fromdict` and index value idx to assign + or replact value in todict + + Args: + fromdict (:obj:`dict`): Dictionary containing values + to assign + todict (:obj:`dict`): Dictionary containing values + to be reassigned + idx (:obj:`dict`): Index value for retrieving value + from iterables + """ for k, v in fromdict.items(): if isinstance(v, dict): # recurse through nested dictionaries @@ -52,10 +146,15 @@ def nested_dict_idx_reassign(fromdict, todict, idx): def reassign_nested_item_from_keypath(dictionary, keypath, value): """ - dictionary: dict, dict object to reassign values of - keypath: str, 'a/b/c' corresponds to dict['a']['b']['c'] - or (for Windows systems): str, 'a\b\c' corresponds to dict['a']['b']['c'] - value: any, value to reassign dictionary value with + Reassign item in a nested dictionary to value using keypath syntax, + to traverse multiple dictionaries + + Args: + dictionary (:obj:`dict`): dict object to reassign values within + keypath (:obj:`str`): Using filepath syntax on given OS to + traverse dictionary, i.e 'a/b/c' ('a\\b\\c') corresponds to + dict['a']['b']['c'] on Unix (Windows). + value: value to reassign dictionary value with """ p = Path(keypath) keylist = list(p.parts) @@ -63,34 +162,79 @@ def reassign_nested_item_from_keypath(dictionary, keypath, value): get_item(dictionary, keylist[:-1])[keylist[-1]] = value def linear_to_nested_dict_reassign(fromdict, todict): - """iterate through a linear dictionary to reassign nested values - using keypaths (d1['a/b/c'] -> d2['a']['b']['c'], d1['a']->d2['a'])""" + """ + Iterate through a linear dictionary to reassign nested values + using keypaths (d1['a/b/c'] -> d2['a']['b']['c'], d1['a']->d2['a']) + + Args: + fromdict (:obj:`dict`): + Dictionary containing values to assign + todict (:obj:`dict`): + Dictionary containing values to be reassigned + """ for k, v in fromdict.items(): reassign_nested_item_from_keypath(todict, k, v) def const_or_evo_func(x): - """if x is callable, return x, else provide a function that returns x""" + """ + If x is callable, return x, else provide a function that just + returns x + + Args: + x: input value, either a numerical value or a + function + """ if callable(x): return x else: return lambda y: y*0 + x def const_or_evo(x,t): - """if x is callable, return x(t), else return x""" + """ + If x is callable, return x(t), else return x + + Args: + x: input value, either a numerical value or a + function + t (numerical): values to evaluate x function + """ if callable(x): return x(t) else: return x def rescale_values(x, oldlims, newlims): - """ rescale x values to range limits such that 0-1 is mapped to limits[0]-limits[1] """ + """ + Rescale x values defined by limits oldlims to new limits newlims + + Args: + x (array-like): Array of input values + oldlims (:obj:`tuple`): tuple representing the original limits + of `x` (low, high) + newlims (:obj:`tuple`): tuple representing the new limits + + Returns: + x_rs (array-like): Rescaled array + """ olo, ohi = oldlims nlo, nhi = newlims descale = np.clip((x - olo) / (ohi-olo), 0 , 1) return (nhi-nlo)*descale + nlo def resample(rate_in, samprate, wavobj): - """ resample audio from original samplerate to required samplerate """ + """ + Resample audio from original samplerate to required samplerate + + Args: + rate_in (:obj:`int`) sample rate of input wave object + samprate (:obj:`int`) desired sample rate for output + wavobj (:obj:`tuple`) sample rate, sample array tuple, output + by `scipy.io.wavfile` function + + Returns: + new_wavobj (:obj:`tuple`) as `wavobj`, with new sample rate + and resampled sample values + """ duration = wavobj.shape[0] / rate_in time_old = np.linspace(0, duration, wavobj.shape[0]) @@ -103,13 +247,17 @@ def resample(rate_in, samprate, wavobj): @contextmanager def suppress_stdout_stderr(): - """A context manager that redirects stdout and stderr to devnull""" + """ + A context manager that redirects stdout and stderr to devnull + """ with open(devnull, 'w') as fnull: with redirect_stderr(fnull) as err, redirect_stdout(fnull) as out: yield (err, out) class Capturing(list): - """ Context manager for handling stdout (see https://stackoverflow.com/a/16571630) """ + """ + Context manager for handling stdout (see https://stackoverflow.com/a/16571630) + """ def __enter__(self): self._stdout = sys.stdout sys.stdout = self._stringio = StringIO() diff --git a/yamls_to_tables.py b/yamls_to_tables.py new file mode 100644 index 0000000..a2f9221 --- /dev/null +++ b/yamls_to_tables.py @@ -0,0 +1,66 @@ + +import yaml +from glob import glob +from pathlib import Path + +generators = {'spec' : "`Spectraliser` Generator", + 'synth' : "`Synthesiser` Generator", + 'sampler' : "`Sampler` Generator"} + +# p = Path(__file__) +p = Path("src", "strauss", "presets", "*", "default.yml") + +def read_yaml(filename): + with filename.open(mode='r') as fdata: + # try: + yamldict = yaml.safe_load(fdata) + # except yaml.YAMLError as err: + # print(err) + return yamldict + +def yaml_traverse(metadict, valdict, rdict, headlev=1): + if hasattr(metadict, 'keys'): + topstr = '' + tabstr = '' + secstr = '' + tabstr += "\n| Parameter | Description | Default Value | Default Range | Unit |\n" + tabstr += "| ----------- | ----------- | ----------- | ----------- | ----------- |\n" + + for k in metadict.keys(): + # print (f">>>>>> {k}") + if hasattr(metadict[k], 'keys'): + secstr += '\n'+''.join(['#']*headlev) + f" `{k}` parameter group\n" + if not k in rdict: + rdict[k] = {} + secstr += yaml_traverse(metadict[k], valdict[k], rdict[k], headlev+1) + continue + if k == '_doc': + topstr += f"\n{metadict[k]}\n" + # print('\n',metadict[k]) + continue + if k not in rdict: + # unspecified => '-' + rdict[k] = '-' + else: + # lets avoid these line-breaking with special characters + rdict[k] = str(rdict[k]).replace(" ","").replace(",","\u2011").replace("-","\u2011") + if k+"_unit" not in rdict: + # unspecified => '-' + rdict[k+"_unit"] = '-' + # print(f"|`{k}` | _{metadict[k]}_ | `{str(valdict[k]).strip()}` | `{rdict[k]}` | {rdict[k+'_unit']}") + tabstr += f"| `{k}` | {str(metadict[k]).strip()} | {valdict[k]} | `{rdict[k]}` | {rdict[k+'_unit']}\n" + if k not in rdict: + rdict[k] = {} + return f'{topstr}{tabstr}{secstr}' + + else: + return + +for f in glob(str(p)): + p = Path(f) + ydat = read_yaml(p) + rdat = read_yaml(p.parents[0] / "ranges" / "default.yml") + print(f"\n# {generators[p.parents[0].name]}\n") + ystr = yaml_traverse(ydat['_meta'], ydat, rdat, 2) + print(ystr) + print('---')