From 43d960b3ddbd68cad095c5e08ce19df4ac1c3b3c Mon Sep 17 00:00:00 2001 From: mahal raskin Date: Thu, 18 Jan 2024 15:58:13 +0100 Subject: [PATCH] Redo of Pull request #24: improved documentation --- .../Preparing-Sample-Sets.md | 80 +++++ .../Sampler Audio Unit and Node.md | 2 +- ...ng Sample Sets.md => Sampler-SFZ-files.md} | 311 +++++++----------- .../DunneAudioKit.docc/Sampler-descriptors.md | 124 +++++++ .../DunneAudioKit.docc/Sampler.md | 132 +------- Sources/DunneAudioKit/Sampler+SFZ.swift | 3 +- Sources/DunneAudioKit/Sampler.swift | 2 +- Sources/DunneAudioKit/StereoDelay.swift | 1 + 8 files changed, 330 insertions(+), 325 deletions(-) create mode 100644 Sources/DunneAudioKit/DunneAudioKit.docc/Preparing-Sample-Sets.md rename Sources/DunneAudioKit/DunneAudioKit.docc/{Preparing Sample Sets.md => Sampler-SFZ-files.md} (59%) create mode 100644 Sources/DunneAudioKit/DunneAudioKit.docc/Sampler-descriptors.md diff --git a/Sources/DunneAudioKit/DunneAudioKit.docc/Preparing-Sample-Sets.md b/Sources/DunneAudioKit/DunneAudioKit.docc/Preparing-Sample-Sets.md new file mode 100644 index 0000000..faebe30 --- /dev/null +++ b/Sources/DunneAudioKit/DunneAudioKit.docc/Preparing-Sample-Sets.md @@ -0,0 +1,80 @@ +# Sampler sample sets + + Preparing sets of samples for ``Sampler`` involves a few steps and lot of testing. + + We suggest to approach it in four steps: + 1. Preparing (or acquiring) sample files + 2. Compressing sample files + 3. Creating a SFZ metadata file + 4. Testing + + This document describes the process of preparing a set of demonstration samples, starting with the sample files included with [ROMPlayer](https://github.com/AudioKit/ROMPlayer). + + You can download the finished SFZ and samples from [this link](https://github.com/AudioKit/ROMPlayer/tree/master/RomPlayer/Sounds/sfz). + + ## Preparing/acquiring sample files + The demo samples were recorded and prepared by Matthew Fecher from a Yamaha TX81z hardware FM synthesizer module, using commercial sampling software called [SampleRobot](http://www.samplerobot.com). If you have *MainStage 3* on the Mac, you can use its excellent *autosampler* function instead. See + + **Important:** If you're planning to work with existing samples, or capture the output from a sample-based instrument, *give careful consideration to copyright issues*. See Matt Fecher's excellent summary [What Sounds Can You Use in your App?](https://github.com/AudioKit/ROMPlayer#what-sounds-can-you-use-in-your-app) *Be very careful with SoundFont files you find on the Internet.* Many are marked "public domain", but actually consist of unlicensed, illegally copied content. While such things are fine for your own personal use, distributing them publicly with your name attached (e.g. in an iOS app on the App Store) can land you in serious legal trouble. + + Turning a set of rough digital recordings into cleanly-playing, looping samples is a complex process in itself, which is beyond the scope of this document. For a quick introduction, see [The Secrets of Great Sounding Samples](http://tweakheadz.com/sampling-tips/). For in-depth exploration, look into YouTube videos by [John Lemkuhl aka PlugInGuru](https://www.youtube.com/user/thepluginguru), in particular [this one](https://youtu.be/o7rL38xrRSE), [this one](https://youtu.be/qPbf5nNyQYo) and [this one](https://youtu.be/Bx9PC8JJNGg). + + ## Sample file compression + ``Sampler`` reads `.wv` files compressed using the open-source [Wavpack](http://www.wavpack.com) software. On the Mac, you must first install the Wavpack command-line tools. Then you can use the following Python 2 script to compress a whole folder-full of `.wav` files: + + ```python + import os, subprocess + + for wav in os.listdir('.'): + if os.path.isfile(wav) and aif.endswith('.wav'): + print 'converting', wav + name = wav[:-4] + wv = name + '.wv' + subprocess.call(['/usr/local/bin/wavpack', '-q', '-r', '-b24', wav]) + #os.remove(wav) + ``` + Uncomment the last line if you're sure you want to delete WAV files after converting them. + + Note that the `wavpack` command-line program does not recognize the `.aif` file format, which is too bad because that's what *MainStage 3*'s autosampler produces. However, we can use the `afconvert` command-line utility built into macOS to convert `.aif` files to `.wav` like this: + + ```python + import os, subprocess + + for aif in os.listdir('.'): + if os.path.isfile(aif) and aif.endswith('.aif'): + print 'converting', aif + name = aif[:-4] + wav = name + '.wav' + wv = name + '.wv' + subprocess.call(['/usr/bin/afconvert', '-f', 'WAVE', '-d', 'LEI24', aif, wav]) + subprocess.call(['/usr/local/bin/wavpack', '-q', '-r', '-b24', wav]) + os.remove(wav) + #os.remove(aif) + ``` + + ## Creating a SFZ metadata file + Mapping of MIDI (note-number, velocity) pairs to sample files requires additional data, for which ``Sampler`` uses a simple subset of the [SFZ format](https://en.wikipedia.org/wiki/SFZ_(file_format)) declared at https://sfzformat.com/headers/. SFZ is essentially a text-based, open-standard alternative to the proprietary [SoundFont](https://en.wikipedia.org/wiki/SoundFont) format. + + In addition to key-mapping, SFZ files can also contain other important metadata such as loop-start and -end points for each sample file. + + The full SFZ standard is very rich, but at the time of writing, ``Sampler``'s SFZ import capability is limited to key mapping and loop metadata only. The import capability also is strict about the order of the opcodes: check the sourcecode of `Sampler+SFZ.swift` and place your `sample=YOURSAMPLENAME.YOURFILEFORMAT` as the last element in the `` line, otherwise the samples will not load. + + Since SFZ files are simply plain-text files, you can use an ordinary text editor to create them. + + You'll find more details and a simple example in the article . + + + ## Other methods to create SFZ files + + In [Software > Tools](https://sfzformat.com/software/tools/) of sfzformat.com you'll find some more tools to work with SFZ files. + + At the moment of this writing (January 2024) there is a freeware called (EXS2SFZ by bjoernbojahr)[https://www.bjoernbojahr.de/exs2sfz.html] that does a good job in coverting EXS files. They will not be directly usable by ``Sampler`` but are a good starting point to edit the SFZ files manually. The resulting SFZ have opcodes in group that ``Sampler`` wants in region and the other way round as well as they don't follow the strict order needed by ``Sampler``. + + At the other end of the scale, a company called Chicken Systems sells a very powerful tool called [Translator](http://www.chickensys.com/products2/translator/), which can convert both sample and metadata to and from a huge list of professional formats, including EXS24 (Apple), SoundFont (SF2 and SFZ), Kontakt 5 (Native Instruments), and many more. The full version costs $149 (USD), but if you're only interested in converting to SFZ, you can buy the "Special Edition" for just $79. + + + ## Testing + Whatever methods you use to create samples and metadata files, it's important to test, test, test, to make sure things are working the way you want. + + ## Going further + The subject of preparing sample sets is deep and complex, and this article has barely scratched the surface. We hope to provide additional online resources as time goes on, especially as ``Sampler``'s implementation expands and changes. Interested users, especially those with practical experience to share, are encouraged to get in touch with the AudioKit team to help with this process. diff --git a/Sources/DunneAudioKit/DunneAudioKit.docc/Sampler Audio Unit and Node.md b/Sources/DunneAudioKit/DunneAudioKit.docc/Sampler Audio Unit and Node.md index 9ddc90a..cfddcc2 100644 --- a/Sources/DunneAudioKit/DunneAudioKit.docc/Sampler Audio Unit and Node.md +++ b/Sources/DunneAudioKit/DunneAudioKit.docc/Sampler Audio Unit and Node.md @@ -19,4 +19,4 @@ The highest level **Sampler** Swift class wraps the Audio Unit code within an Au The **Sampler** class also includes utility functions to assist with loading sample data into the underlying C++ `Sampler` object (using **AVAudioFile**). -Additional utility functions are implemented in separate files as Swift *extensions*. `Sampler+SFZ.swift` adds a rudimentary facility to load whole sets of samples by interpreting a [SFZ file](https://en.wikipedia.org/wiki/SFZ_(file_format)). +Additional utility functions are implemented in separate files as Swift *extensions*. `Sampler+SFZ.swift` adds a rudimentary facility to load whole sets of samples by interpreting a SFZ file; read more about those in . diff --git a/Sources/DunneAudioKit/DunneAudioKit.docc/Preparing Sample Sets.md b/Sources/DunneAudioKit/DunneAudioKit.docc/Sampler-SFZ-files.md similarity index 59% rename from Sources/DunneAudioKit/DunneAudioKit.docc/Preparing Sample Sets.md rename to Sources/DunneAudioKit/DunneAudioKit.docc/Sampler-SFZ-files.md index a96415a..f24d5d3 100644 --- a/Sources/DunneAudioKit/DunneAudioKit.docc/Preparing Sample Sets.md +++ b/Sources/DunneAudioKit/DunneAudioKit.docc/Sampler-SFZ-files.md @@ -1,89 +1,142 @@ -# Preparing sample sets for Sampler +# Sampler with SFZ files -Preparing sets of samples for **Sampler** involves four steps: + Sampler has a naive implementation of reading SFZ files. This article shows a simple example of a SFZ file and a script for Mainstage 3. -1. Preparing (or acquiring) sample files -2. Compressing sample files -3. Creating a SFZ metadata file -4. Testing -This document describes the process of preparing a set of demonstration samples, starting with the sample files included with [ROMPlayer](https://github.com/AudioKit/ROMPlayer). +## Simple Example of a simple SFZ file + +If your sampling needs are not very complex, as in, you simply just need to load your `Sampler` with a variety samples, here is an example of a working SFZ File: -You can download the finished product from [this link](http://audiokit.io/downloads/ROMPlayerInstruments.zip). +``` + +default_path=samples/ + +key=33 + sample=A1.wv +key=34 + sample=A#1.wv +key=35 + sample=B1.wv +key=36 + sample=C2.wv +key=37 + sample=C#2.wv +key=38 + sample=D2.wv +key=39 + sample=D#2.wv +key=40 + sample=E2.wv +key=41 + sample=F2.wv +key=42 + sample=F#2.wv +key=43 + sample=G2.wv +key=44 + sample=G#2.wv +key=45 + sample=A2.wv +key=46 + sample=A#2.wv +key=47 + sample=B2.wv +key=48 + sample=C3.wv +key=49 + sample=C#3.wv +key=50 + sample=D3.wv +key=51 + sample=D#3.wv +key=52 + sample=E3.wv +key=53 + sample=F3.wv +key=54 + sample=F#3.wv +key=55 + sample=G3.wv +key=56 + sample=G#3.wv +key=57 + sample=A3.wv +key=58 + sample=A#3.wv +key=59 + sample=B3.wv +key=60 + sample=C4.wv +key=61 + sample=C#4.wv +key=62 + sample=D4.wv +key=63 + sample=D#4.wv +key=64 + sample=E4.wv +key=65 + sample=F4.wv +key=66 + sample=F#4.wv +key=67 + sample=G4.wv +key=68 + sample=G#4.wv +key=69 + sample=A4.wv +key=70 + sample=A#4.wv +key=71 + sample=B4.wv +lokey=72 hikey=80 pitch_keycenter=72 + sample=C5.wv +``` -## Preparing/acquiring sample files -The demo samples were recorded and prepared by Matthew Fecher from a Yamaha TX81z hardware FM synthesizer module, using commercial sampling software called [SampleRobot](http://www.samplerobot.com). If you have *MainStage 3* on the Mac, you can use its excellent *autosampler* function instead. +This SFZ file is an example of a piano sampler with samples matched note for note in most octaves. Let's go over from top to bottom: -**Important:** If you're planning to work with existing samples, or capture the output from a sample-based instrument, *give careful consideration to copyright issues*. See Matt Fecher's excellent summary [What Sounds Can You Use in your App?](https://github.com/AudioKit/ROMPlayer#what-sounds-can-you-use-in-your-app) *Be very careful with SoundFont files you find on the Internet.* Many are marked "public domain", but actually consist of unlicensed, illegally copied content. While such things are fine for your own personal use, distributing them publicly with your name attached (e.g. in an iOS app on the App Store) can land you in serious legal trouble. +`` -Turning a set of rough digital recordings into cleanly-playing, looping samples is a complex process in itself, which is beyond the scope of this document. For a quick introduction, see [The Secrets of Great Sounding Samples](http://tweakheadz.com/sampling-tips/). For in-depth exploration, look into YouTube videos by [John Lemkuhl aka PlugInGuru](https://www.youtube.com/user/thepluginguru), in particular [this one](https://youtu.be/o7rL38xrRSE), [this one](https://youtu.be/qPbf5nNyQYo) and [this one](https://youtu.be/Bx9PC8JJNGg). +This is a necessary SFZ keyword to denote that this is indeed a SFZ file. -## Sample file compression -**Sampler** reads `.wv` files compressed using the open-source [Wavpack](http://www.wavpack.com) software. On the Mac, you must first install the Wavpack command-line tools. Then you can use the following Python 2 script to compress a whole folder-full of `.wav` files: +`default_path=samples/` -```python -import os, subprocess +The path in which the samples you are describing in the SFZ file reside. In this example SFZ file, we have a folder named `samples` that is in the same directory as the SFZ file. You may name your folder any name, as long as it is described correctly in the SFZ file. *You will need to ensure that your folder of samples and the path is described correctly. If your SFZ file resides in a different directory, please be sure find the correct path for the folder of samples so that the SFZ can correctly find them* -for wav in os.listdir('.'): - if os.path.isfile(wav) and aif.endswith('.wav'): - print 'converting', wav - name = wav[:-4] - wv = name + '.wv' - subprocess.call(['/usr/local/bin/wavpack', '-q', '-r', '-b24', wav]) - #os.remove(wav) -``` -Uncomment the last line if you're sure you want to delete WAV files after converting them. +`key=33` -Note that the `wavpack` command-line program does not recognize the `.aif` file format, which is too bad because that's what *MainStage 3*'s autosampler produces. However, we can use the `afconvert` command-line utility built into macOS to convert `.aif` files to `.wav` like this: +For more information on the `` SFZ keyword, please read [here](https://sfzformat.com/headers/group). Here we are preparing the MIDI note 33 to be assigned to a sample. -```python -import os, subprocess +` sample=A1.wv>` -for aif in os.listdir('.'): - if os.path.isfile(aif) and aif.endswith('.aif'): - print 'converting', aif - name = aif[:-4] - wav = name + '.wav' - wv = name + '.wv' - subprocess.call(['/usr/bin/afconvert', '-f', 'WAVE', '-d', 'LEI24', aif, wav]) - subprocess.call(['/usr/local/bin/wavpack', '-q', '-r', '-b24', wav]) - os.remove(wav) - #os.remove(aif) -``` +For more information on the `` SFZ keyword, please read [here](https://sfzformat.com/headers/region). -## Creating a SFZ metadata file -Mapping of MIDI (note-number, velocity) pairs to sample files requires additional data, for which **Sampler** uses a simple subset of the [SFZ format](https://en.wikipedia.org/wiki/SFZ_(file_format)). SFZ is essentially a text-based, open-standard alternative to the proprietary [SoundFont](https://en.wikipedia.org/wiki/SoundFont) format. +Here we are assigning a specific sample you have collected to the above group/key. -In addition to key-mapping, SFZ files can also contain other important metadata such as loop-start and -end points for each sample file. +So now with: +`key=33` +` sample=A1.wv>` -The full SFZ standard is very rich, but at the time of writing, **Sampler**'s SFZ import capability is limited to key mapping and loop metadata only. +Our sampler will assign key 33 to the sample `A1.wv`. +In this example file, we are just continuing to assign 1 to 1 keys to samples. -### How the demo SFZ files were made -Matt originally provided `.esx` metadata files for use by Apple's ESX24 Sampler plugin included with Logic Pro X. These files use a proprietary binary format and are notoriously difficult to work with. +Lets look at the last 2 lines: -Fortunately, KVR user [vonRed](https://www.kvraudio.com/forum/memberlist.php?mode=viewprofile&u=134002) has provided a free tool called [esxtosfz.py](https://www.kvraudio.com/forum/viewtopic.php?t=399035), which does a reasonable job of reading `.esx` files and outputting equivalent `.sfz` files. *Note this tool is written in Python 3, which is not installed by default on Macs, but is [available here](https://www.python.org/downloads/mac-osx/).* +`lokey=72 hikey=80 pitch_keycenter=72 + sample=C5.wv` -The following Python 2 script will convert all `.esx` files in a folder to `.sfz` format: +`lokey` and `hikey` allows us to use one sample to map to multiple keys or MIDI notes. `pitch_keycenter` tells us where to center the key or MIDI note for the sample. In these two lines, we are assigning the sample `C5.wv` to MIDI notes (or keys) 72 *through* 80. The sampler will pitch shift the sample in order to accommodate the higher/lower notes. Be aware that small amounts of pitch shifting will be hard to discern, but anything past a Perfect 5th (7 semitones) will start to exhibit pitch shifting artifacts. Check out more information on [`lokey` and `hikey`](https://sfzformat.com/opcodes/hikey), and [`pitch_keycenter`](https://sfzformat.com/opcodes/pitch_keycenter). -```python -import os, subprocess +**IMPORTANT** -for exs in os.listdir('.'): - if os.path.isfile(exs) and exs.endswith('.exs'): - print 'converting', exs - sfz = exs[:-4] + '.sfz' - subprocess.call(['/usr/local/bin/python3', '/Users/shane/exs2sfz.py', exs, sfz, 'samples']) - #os.remove(exs) -``` +**In order for the Audiokit `Sampler` to load your samples correctly, in your `` declarations, the sample assignment MUST BE THE LAST ELEMENT of your `` declarations.** -## Other methods to create SFZ files -Since SFZ files are simply plain-text files, you can use an ordinary text editor to create them. +`` has other opcodes you can use such as `lovel` and `hivel`, if you do not place your `sample=YOURSAMPLENAME.YOURFILEFORMAT` as the last element in the `` line, the samples will not load! -At the other end of the scale, a company called Chicken Systems sells a very powerful tool called [Translator](http://www.chickensys.com/products2/translator/), which can convert both sample and metadata to and from a huge list of professional formats, including ESX24 (Apple), SoundFont (SF2 and SFZ), Kontakt 5 (Native Instruments), and many more. The full version costs $149 (USD), but if you're only interested in converting to SFZ, you can buy the "Special Edition" for just $79. ## Scripts for MainStage 3 Autosampler -The autosampler built into Apple's *MainStage 3* produces AIFF-C audio files and an EXS24 metadata file, in a newer format than vonRed's `esxtosfz.py` script can handle. However, all the necessary details are actually encoded right in the `.aif` sample files. The following Python script uses a simplistic parsing technique to pull the necessary numbers out of a set of `.aif` files and create a corresponding `.sfz` file: +The autosampler built into Apple's *MainStage 3* produces AIFF-C audio files and an EXS24 metadata file, in a newer format than vonRed's `exstosfz.py` script can handle. However, all the necessary details are actually encoded right in the `.aif` sample files. The following Python script uses a simplistic parsing technique to pull the necessary numbers out of a set of `.aif` files and create a corresponding `.sfz` file: ```python import sys, os @@ -210,139 +263,5 @@ while 1: This script makes use of the [chunk](https://docs.python.org/2/library/chunk.html) Python library, together with specific data gleaned from the [AIFF-C format specifications](http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/AIFF/AIFF.html). The obvious next step is to combine elements of both scripts, to produce a better version of the first one. -## Simple Example of a simple SFZ file - -If your sampling needs are not very complex, as in, you simply just need to load your `Sampler` with a variety samples, here is an example of a working SFZ File: - -``` - -default_path=samples/ - -key=33 - sample=A1.wv -key=34 - sample=A#1.wv -key=35 - sample=B1.wv -key=36 - sample=C2.wv -key=37 - sample=C#2.wv -key=38 - sample=D2.wv -key=39 - sample=D#2.wv -key=40 - sample=E2.wv -key=41 - sample=F2.wv -key=42 - sample=F#2.wv -key=43 - sample=G2.wv -key=44 - sample=G#2.wv -key=45 - sample=A2.wv -key=46 - sample=A#2.wv -key=47 - sample=B2.wv -key=48 - sample=C3.wv -key=49 - sample=C#3.wv -key=50 - sample=D3.wv -key=51 - sample=D#3.wv -key=52 - sample=E3.wv -key=53 - sample=F3.wv -key=54 - sample=F#3.wv -key=55 - sample=G3.wv -key=56 - sample=G#3.wv -key=57 - sample=A3.wv -key=58 - sample=A#3.wv -key=59 - sample=B3.wv -key=60 - sample=C4.wv -key=61 - sample=C#4.wv -key=62 - sample=D4.wv -key=63 - sample=D#4.wv -key=64 - sample=E4.wv -key=65 - sample=F4.wv -key=66 - sample=F#4.wv -key=67 - sample=G4.wv -key=68 - sample=G#4.wv -key=69 - sample=A4.wv -key=70 - sample=A#4.wv -key=71 - sample=B4.wv -lokey=72 hikey=80 pitch_keycenter=72 - sample=C5.wv -``` - -This SFZ file is an example of a piano sampler with samples matched note for note in most octaves. Let's go over from top to bottom: - -`` - -This is a necessary SFZ keyword to denote that this is indeed a SFZ file. - -`default_path=samples/` - -The path in which the samples you are describing in the SFZ file reside. In this example SFZ file, we have a folder named `samples` that is in the same directory as the SFZ file. You may name your folder any name, as long as it is described correctly in the SFZ file. *You will need to ensure that your folder of samples and the path is described correctly. If your SFZ file resides in a different directory, please be sure find the correct path for the folder of samples so that the SFZ can correctly find them* - -`key=33` - -For more information on the `` SFZ keyword, please read [here](https://sfzformat.com/headers/group). Here we are preparing the MIDI note 33 to be assigned to a sample. - -` sample=A1.wv>` - -For more information on the `` SFZ keyword, please read [here](https://sfzformat.com/headers/region). - -Here we are assigning a specific sample you have collected to the above group/key. - -So now with: -`key=33` -` sample=A1.wv>` - -Our sampler will assign key 33 to the sample `A1.wv`. - -In this example file, we are just continuing to assign 1 to 1 keys to samples. - -Lets look at the last 2 lines: - -`lokey=72 hikey=80 pitch_keycenter=72 - sample=C5.wv` - -`lokey` and `hikey` allows us to use one sample to map to multiple keys or MIDI notes. `pitch_keycenter` tells us where to center the key or MIDI note for the sample. In these two lines, we are assigning the sample `C5.wv` to MIDI notes (or keys) 72 *through* 80. The sampler will pitch shift the sample in order to accommodate the higher/lower notes. Be aware that small amounts of pitch shifting will be hard to discern, but anything past a Perfect 5th (7 semitones) will start to exhibit pitch shifting artifacts. Check out more information on [`lokey` and `hikey`](https://sfzformat.com/opcodes/hikey), and [`pitch_keycenter`](https://sfzformat.com/opcodes/pitch_keycenter). - -**IMPORTANT** - -**In order for the Audiokit `Sampler` to load your samples correctly, in your `` declarations, the sample assignment MUST BE THE LAST ELEMENT of your `` declarations.** - -`` has other opcodes you can use such as `lovel` and `hivel`, if you do not place your `sample=YOURSAMPLENAME.YOURFILEFORMAT` as the last element in the `` line, the samples will not load! - -## Testing -Whatever methods you use to create samples and metadata files, it's important to test, test, test, to make sure things are working the way you want. - -## Going further -The subject of preparing sample sets is deep and complex, and this article has barely scratched the surface. We hope to provide additional online resources as time goes on, especially as **Sampler**'s implementation expands and changes. Interested users, especially those with practical experience to share, are encouraged to get in touch with the AudioKit team to help with this process. +## How the demo SFZ files were made back in 2018 +Matt originally provided `.exs` metadata files for use by Apple's EXS24 Sampler plugin included with Logic Pro X. These files use a proprietary binary format and are notoriously difficult to work with. There used to be a Python script by KVR user vonRed called `exstosfz.py`but this is no longer maintained and only works with older EXS-Files. You may find this [archived Mercurial repository of exstosfz.py](https://bitbucket-archive.softwareheritage.org/projects/la/larromba/exstosfz.html) as reference. diff --git a/Sources/DunneAudioKit/DunneAudioKit.docc/Sampler-descriptors.md b/Sources/DunneAudioKit/DunneAudioKit.docc/Sampler-descriptors.md new file mode 100644 index 0000000..504d4de --- /dev/null +++ b/Sources/DunneAudioKit/DunneAudioKit.docc/Sampler-descriptors.md @@ -0,0 +1,124 @@ +# Sampler initialisation with descriptors + + +## Sample descriptors +When using `loadRawSampleData()` and `loadCompressedSampleFile()` to load individual samples, you will need to create instances of one of three Swift structure types as follows. + +The structures are defined as C structs in *Sampler_Typedefs.h* (which lives in the *AudioKit/Core/DunneCore/Sampler* folder in the main AudioKit repo). This file is simple enough to reproduce here: + + typedef struct + { + int noteNumber; + float noteHz; + + int min_note, max_note; + int min_vel, max_vel; + + bool bLoop; + float fLoopStart, fLoopEnd; + float fStart, fEnd; + + } SampleDescriptor; + + typedef struct + { + SampleDescriptor sd; + + float sampleRateHz; + bool bInterleaved; + int nChannels; + int nSamples; + float *pData; + + } SampleDataDescriptor; + + typedef struct + { + SampleDescriptor sd; + + const char* path; + + } SampleFileDescriptor; + +By the miracle of Swift/Objective-C bridging (see [Using Swift with Cocoa and Objective-C](https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithCAPIs.html)), each of these three structures is accessible from Swift as a similarly-named class, which you can create by simply providing values for all the properties, as you'll see in the examples below. + +## SampleDataDescriptor and loadRawSampleData() + +*SampleDataDescriptor*, which is required when calling `loadRawSampleData()`, has an *SampleDescriptor* property (as described above) plus several additional properties to provide all the information ``Sampler`` needs about the sample: + +* *sampleRateHz* is the sampling rate at which the sample data were acquired. If the sampler needs to play back the sample at a different rate, it will need to scale its playback rate based on the ratio of the two rates. +* *nChannels* will be 1 if the sample is monophonic, or 2 if stereo. Note the sampler can play back mono samples as stereo; it simply plays the same data to both output channels. (In the reverse case, only the Left channel data will sound.) +* *bInterleaved* should be set *true* only for stereo samples represented in memory as Left1, Right1, Left2, Right2, etc. Set *false* for mono samples, or non-interleaved stereo samples where all the Left samples come first, followed by all the Right samples. +* *pSamples* is a pointer to the raw sample data; it has the slightly-scary Swift type *UnsafeMutablePointer\*. + +Here's an example of creating a sample programmatically in Swift, and loading it using `loadRawSampleData()`: + + var myData = [Float](repeating: 0.0, count: 1000) + for i in 0..<1000 { + myData[i] = sin(2.0 * Float(i)/1000 * Float.pi) + } + let sampleRate = Float(Settings.sampleRate) + let desc = SampleDescriptor(noteNumber: 69, + noteHz: sampleRate/1000, + min_note: -1, + max_note: -1, + min_vel: -1, + max_vel: -1, + bLoop: true, + fLoopStart: 0, + fLoopEnd: 1, + fStart: 0, + fEnd: 0) + let ptr = UnsafeMutablePointer(mutating: myData) + let ddesc = SampleDataDescriptor(sd: desc, + sampleRateHz: sampleRate, + bInterleaved: false, + nChannels: 1, + nSamples: 1000, + pData: ptr) + sampler.loadRawSampleData(sdd: ddesc) + sampler.setLoop(thruRelease: true) + sampler.buildSimpleKeyMap() + +A few points to note about this example: + +* We get the scary-typed pointer by calling the pointer type's `init(mutating:)` function +* `Settings.sampleRate` provides the current audio sampling rate +* Since we have only one note, the `noteNumber` can be anything +* We can set `min_note` etc. to -1, because we call `buildSimpleKeyMap()` not `buildKeyMap()` +* `fLoopStart` and `fLoopEnd` are normally sample counts (i.e., we could specify 0.0 and 999.0 to loop over the whole sample), but values between 0 and 1 are interpreted as *fractions* of the full sample length. Hence we can just use 0 to mean "start of the sample" and 1 to mean "end of the sample". +* setting `fEnd` to 0 also means "end of the sample" +* To ensure the sampler keeps looping even after each note is released (very important with such short samples), we call `setLoop(thruRelease: true)`. + +## SampleFileDescriptor and loadCompressedSampleFile() +*SampleFileDescriptor*, used in calls to `loadCompressedSampleFile()` is very simple. Like *SampleDataDescriptor*, it has an *SampleDescriptor* property, to which it simply adds a `String` property `path`. Here's an example of using `loadCompressedSampleFile()`, taken from the Sampler demo program: + + private func loadCompressed(baseURL: URL, + noteNumber: MIDINoteNumber, + folderName: String, + fileEnding: String, + min_note: Int32 = -1, + max_note: Int32 = -1, + min_vel: Int32 = -1, + max_vel: Int32 = -1) + { + let folderURL = baseURL.appendingPathComponent(folderName) + let fileName = folderName + fileEnding + let fileURL = folderURL.appendingPathComponent(fileName) + let freq = float(PolyphonicNode.tuningTable.frequency(forNoteNumber: noteNumber)) + let sd = SampleDescriptor(noteNumber: Int32(noteNumber), + noteHz: freq, + min_note: min_note, + max_note: max_note, + min_vel: min_vel, + max_vel: max_vel, + bLoop: true, + fLoopStart: 0.2, + fLoopEnd: 0.3, + fStart: 0.0, + fEnd: 0.0) + let fdesc = SampleFileDescriptor(sd: sd, path: fileURL.path) + sampler.loadCompressedSampleFile(sfd: fdesc) + } + +Note in the last line of the code above, `sampler` is a ``Sampler`` instance. See the *Conductor.swift* file in the SamplerDemo macOS example for more context. diff --git a/Sources/DunneAudioKit/DunneAudioKit.docc/Sampler.md b/Sources/DunneAudioKit/DunneAudioKit.docc/Sampler.md index 24c0adc..55a4ce5 100644 --- a/Sources/DunneAudioKit/DunneAudioKit.docc/Sampler.md +++ b/Sources/DunneAudioKit/DunneAudioKit.docc/Sampler.md @@ -1,6 +1,9 @@ -# Sampler +# ``DunneAudioKit/Sampler`` +@Metadata { + @DocumentationExtension(mergeBehavior:append) +} -**Sampler** is a new, polyphonic sample-playback engine built from scratch in C++. It is 64-voice polyphonic and features a per-voice, stereo low-pass filter with resonance and ADSR envelopes for both amplitude and filter cutoff. Samples must be loaded into memory and remain resident there; it does not do streaming. It reads standard audio files via **AVAudioFile**, as well as a more efficient [Wavpack](http://www.wavpack.com/)-based compressed format. +**Sampler** is a polyphonic sample-playback engine built from scratch in C++. It is 64-voice polyphonic and features a per-voice, stereo low-pass filter with resonance and ADSR envelopes for both amplitude and filter cutoff. Samples must be loaded into memory and remain resident there; it does not do streaming. It reads standard audio files via **AVAudioFile**, as well as a more efficient Wavpack compressed format. ### Sampler vs AppleSampler @@ -13,8 +16,9 @@ 1. `loadRawSampleData()` allows use of sample data already in memory, e.g. data generated programmatically or read using custom file-reading code. 2. `loadSFZ()` loads entire sets of samples by interpreting a simplistic subset of the "SFZ" soundfont file format. +3. `loadRawSampleData()` and `loadCompressedSampleFile()` take a "descriptor" argument, whose many member variables define details like the sample's natural MIDI note-number and pitch (frequency), plus details about loop start and end points, if used. See more in . -`loadRawSampleData()` and `loadCompressedSampleFile()` take a "descriptor" argument (see next section below), whose many member variables define details like the sample's natural MIDI note-number and pitch (frequency), plus details about loop start and end points, if used. For `loadUsingSfzFile()` allows all this "metadata" to be encoded in a SFZ file, using a simple plain-text format which is easy to understand and edit manually. +For `loadUsingSfzFile()` allows all this "metadata" to be encoded in a SFZ file, using a simple plain-text format which is easy to understand and edit manually. More information on . The mapping of MIDI (note number, velocity) pairs to samples is done using some internal lookup tables, which can be populated in one of two ways: @@ -22,125 +26,3 @@ The mapping of MIDI (note number, velocity) pairs to samples is done using some 2. If you only have note-numbers for each sample, call `buildSimpleKeyMap()` to map each MIDI note-number (at any velocity) to the *nearest available* sample. **Important:** Before loading a new group of samples, you must call `unloadAllSamples()`. Otherwise, the new samples will be loaded *in addition* to the already-loaded ones. This wastes memory and worse, newly-loaded samples will usually not sound at all, because the sampler simply plays the first matching sample it finds. - -### Sample descriptors -When using `loadRawSampleData()` and `loadCompressedSampleFile()` to load individual samples, you will need to create instances of one of three Swift structure types as follows. - -The structures are defined as C structs in *Sampler_Typedefs.h* (which lives in the *AudioKit/Core/DunneCore/Sampler* folder in the main AudioKit repo). This file is simple enough to reproduce here: - - typedef struct - { - int noteNumber; - float noteHz; - - int min_note, max_note; - int min_vel, max_vel; - - bool bLoop; - float fLoopStart, fLoopEnd; - float fStart, fEnd; - - } SampleDescriptor; - - typedef struct - { - SampleDescriptor sd; - - float sampleRateHz; - bool bInterleaved; - int nChannels; - int nSamples; - float *pData; - - } SampleDataDescriptor; - - typedef struct - { - SampleDescriptor sd; - - const char* path; - - } SampleFileDescriptor; - -By the miracle of Swift/Objective-C bridging (see [Using Swift with Cocoa and Objective-C](https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithCAPIs.html)), each of these three structures is accessible from Swift as a similarly-named class, which you can create by simply providing values for all the properties, as you'll see in the examples below. - -## SampleDataDescriptor and loadRawSampleData() - -*SampleDataDescriptor*, which is required when calling `loadRawSampleData()`, has an *SampleDescriptor* property (as described above) plus several additional properties to provide all the information **Sampler** needs about the sample: - -* *sampleRateHz* is the sampling rate at which the sample data were acquired. If the sampler needs to play back the sample at a different rate, it will need to scale its playback rate based on the ratio of the two rates. -* *nChannels* will be 1 if the sample is monophonic, or 2 if stereo. Note the sampler can play back mono samples as stereo; it simply plays the same data to both output channels. (In the reverse case, only the Left channel data will sound.) -* *bInterleaved* should be set *true* only for stereo samples represented in memory as Left1, Right1, Left2, Right2, etc. Set *false* for mono samples, or non-interleaved stereo samples where all the Left samples come first, followed by all the Right samples. -* *pSamples* is a pointer to the raw sample data; it has the slightly-scary Swift type *UnsafeMutablePointer\*. - -Here's an example of creating a sample programmatically in Swift, and loading it using `loadRawSampleData()`: - - var myData = [Float](repeating: 0.0, count: 1000) - for i in 0..<1000 { - myData[i] = sin(2.0 * Float(i)/1000 * Float.pi) - } - let sampleRate = Float(Settings.sampleRate) - let desc = SampleDescriptor(noteNumber: 69, - noteHz: sampleRate/1000, - min_note: -1, - max_note: -1, - min_vel: -1, - max_vel: -1, - bLoop: true, - fLoopStart: 0, - fLoopEnd: 1, - fStart: 0, - fEnd: 0) - let ptr = UnsafeMutablePointer(mutating: myData) - let ddesc = SampleDataDescriptor(sd: desc, - sampleRateHz: sampleRate, - bInterleaved: false, - nChannels: 1, - nSamples: 1000, - pData: ptr) - sampler.loadRawSampleData(sdd: ddesc) - sampler.setLoop(thruRelease: true) - sampler.buildSimpleKeyMap() - -A few points to note about this example: - -* We get the scary-typed pointer by calling the pointer type's `init(mutating:)` function -* `Settings.sampleRate` provides the current audio sampling rate -* Since we have only one note, the `noteNumber` can be anything -* We can set `min_note` etc. to -1, because we call `buildSimpleKeyMap()` not `buildKeyMap()` -* `fLoopStart` and `fLoopEnd` are normally sample counts (i.e., we could specify 0.0 and 999.0 to loop over the whole sample), but values between 0 and 1 are interpreted as *fractions* of the full sample length. Hence we can just use 0 to mean "start of the sample" and 1 to mean "end of the sample". -* setting `fEnd` to 0 also means "end of the sample" -* To ensure the sampler keeps looping even after each note is released (very important with such short samples), we call `setLoop(thruRelease: true)`. - -## SampleFileDescriptor and loadCompressedSampleFile() -*SampleFileDescriptor*, used in calls to `loadCompressedSampleFile()` is very simple. Like *SampleDataDescriptor*, it has an *SampleDescriptor* property, to which it simply adds a `String` property `path`. Here's an example of using `loadCompressedSampleFile()`, taken from the Sampler demo program: - - private func loadCompressed(baseURL: URL, - noteNumber: MIDINoteNumber, - folderName: String, - fileEnding: String, - min_note: Int32 = -1, - max_note: Int32 = -1, - min_vel: Int32 = -1, - max_vel: Int32 = -1) - { - let folderURL = baseURL.appendingPathComponent(folderName) - let fileName = folderName + fileEnding - let fileURL = folderURL.appendingPathComponent(fileName) - let freq = float(PolyphonicNode.tuningTable.frequency(forNoteNumber: noteNumber)) - let sd = SampleDescriptor(noteNumber: Int32(noteNumber), - noteHz: freq, - min_note: min_note, - max_note: max_note, - min_vel: min_vel, - max_vel: max_vel, - bLoop: true, - fLoopStart: 0.2, - fLoopEnd: 0.3, - fStart: 0.0, - fEnd: 0.0) - let fdesc = SampleFileDescriptor(sd: sd, path: fileURL.path) - sampler.loadCompressedSampleFile(sfd: fdesc) - } - -Note in the last line of the code above, `sampler` is a **Sampler** instance. See the *Conductor.swift* file in the SamplerDemo macOS example for more context. diff --git a/Sources/DunneAudioKit/Sampler+SFZ.swift b/Sources/DunneAudioKit/Sampler+SFZ.swift index 803b01b..a8252c6 100644 --- a/Sources/DunneAudioKit/Sampler+SFZ.swift +++ b/Sources/DunneAudioKit/Sampler+SFZ.swift @@ -4,8 +4,7 @@ import AudioKit import AVFoundation import CDunneAudioKit -/// Super-naive code to read a .sfz file, as produced by vonRed's free ESX24-to-SFZ program -/// See https://bitbucket.org/vonred/exstosfz/downloads/ (you'll need Python 3 to run it). +/// Super-naive code to read a .sfz file extension SamplerData { /// Load an SFZ at the given location diff --git a/Sources/DunneAudioKit/Sampler.swift b/Sources/DunneAudioKit/Sampler.swift index 14bf0b8..6370516 100644 --- a/Sources/DunneAudioKit/Sampler.swift +++ b/Sources/DunneAudioKit/Sampler.swift @@ -5,7 +5,7 @@ import AudioKitEX import AVFoundation import CDunneAudioKit -/// Sampler +/// Sampler audio generation public class Sampler: Node { /// Connected nodes public var connections: [Node] { [] } diff --git a/Sources/DunneAudioKit/StereoDelay.swift b/Sources/DunneAudioKit/StereoDelay.swift index 1079ee3..2e44beb 100644 --- a/Sources/DunneAudioKit/StereoDelay.swift +++ b/Sources/DunneAudioKit/StereoDelay.swift @@ -6,6 +6,7 @@ import AVFoundation import CDunneAudioKit /// Stereo delay-line with stereo (linked dual mono) and ping-pong modes +/// /// TODO: This node needs tests public class StereoDelay: Node { let input: Node