Devlog

Code your own libraries | Devlog 5

Welcome to another devlog for version 3 of Mirage, FrozenPlain’s sampler plugin. The primary goal of this free update is to firmly establish Mirage’s structure including CLAP and VST3 support. Progress is going well in many areas, but in this post I’m going to focus on my work on a new sample library format.

Mirage v3 will feature a new configuration format for Mirage libraries. With this new system, you can use the Lua programming language to define how audio files should be mapped into playable instruments. We shall use this method ourselves to make our own new products, but the whole system is also available to anyone who is comfortable writing code. It will be well documented, and the Lua programming language is friendly for beginners. This is a very different approach to what we were using before, where a Mirage library was a single, anonymous file in our own MDATA format, only able to be created by our internal tools. The MDATA format will still be fully supported but the new Lua-based system is the new, preferred method.

The challenge of mapping audio samples into playable instruments is one that others have worked on too. Perhaps the most similar format to this new one is SFZ. This format is not a great fit for Mirage’s use-case though. SFZ combines both sample mapping (corresponding audio files to the keyboard), and sound manipulation (configuring the parameters of filters, envelopes and effects). With Mirage, we have a friendly GUI that can be used the shape the sound – we do not want this to be done in a configuration file. Additionally, SFZ does not make use of a programming language meaning it can be laborious and repetitive to use. By contrast, Lua offers real programming language features: functions, variables and loops, that you can use to configure a sample library in a maintainable way. I hope that this extra power will allow for extra experimentation.

Of course, it would be nice to have a GUI that we could use to create sample libraries, but for now it’s out of the scope of this update.

Another benefit of this new format is that the audio files will be available to use without Mirage. You will be able to see the folder of samples (probably in FLAC format), and use them in your music in a raw form. Along these same lines, this new format opens the door for conversion to/from other open sample-library formats such as SFZ, decent sampler or multisample.

The new Lua format [draft]

This is a draft of the new Mirage library configuration format. None of these settings are confirmed yet. Mirage will be able to monitor these Lua files and update the instruments as soon as the file is saved. It will also show good error messages if something is wrong.

-- Mirage Library configuration script
-- 
-- This file is a documented example of how Mirage's new library configuration works. It's 
-- a configuration script written in Lua 5.4. It brings the power of a full programming 
-- language, meaning you can use features such as variables and functions to create a 
-- configuration that is easy-to-maintain and experiment with.
-- 
-- Mirage offers a handlful of functions that the Lua code can use to build a library. 
-- Part of this will involve specifying audio file paths relative to this Lua script.
-- 
-- This format is primarily concerned with mapping audio-files to the keyboard; Mirage 
-- handles more advanced sound shaping by offering a friendly GUI. The GUI also normally 
-- offers setting loop points, but this can sometimes be configured in the Lua instead.
-- 
-- Let's define the 3 heirarchical building blocks of a Mirage library.
-- 
-- Firstly, at the lowest level, are Regions. A Region is an area of the keyboard defined 
-- by 2 dimensions: a key-range and a velocity-range. A Region has an audio file attached 
-- to it. When a note is played in Mirage that fits within the given key-range and 
-- velociy-range, the audio file is played (there are various other more complicated 
-- trigger-criteria that be specified than just key & velocity).
-- 
-- Secondly, the next structure up the heirarchy is the Instrument. These are things that 
-- are picked on the GUI. Instruments contain 1 or more Regions. Instruments should be 
-- fully playable in themselves. Sometimes an Instrument is multisampled and complicated, 
-- sometimes it's just a single looped sample that is similar in function to an oscillator 
-- on a subtractive synth.
-- 
-- Finally, there is the top-level structure of this configuration, the Library. A library 
-- is a collection of Instruments - usually with a theme and an intended use-case.
-- 
-- Note there there is no 'group' structure as is often found in other sample-mapping 
-- formats such as SFZ. Instead we can create functions or use loops to apply similar 
-- configuration to a set of regions. Additionally, Mirage offers a helper function called 
-- mirage.apply_prototype(prototype, tbl) which allows new tables to be created that are 
-- based off an existing table. The first argument is the prototype table, and the second 
-- argument is another table that is filled with values from the prototype if they don't 
-- already exist in the second table. The modified second argument is returned. This 
-- function works in a 'deep' way - meaning that sub-tables are also handed in the same 
-- way.
-- 
-- Example usage of mirage.apply_prototype:
-- 
--   local round_robin_1 = {
--     trigger_criteria = {
--       round_robin_index = 0,
--     },
--     options = {
--       auto_map_key_range_group = "rr1",
--     },
--   }
-- 
--   -- Here, the table that's passed to add_region is round_robin_1 but with the second 
-- table lain on top of it
--   mirage.add_region(instrument, mirage.apply_prototype(round_robin_1, {
--     file = {
--       path = "samples/file_c3.flac",
--       root_key = 60,
--     },
--   }))
-- 
--   mirage.add_region(instrument, mirage.apply_prototype(round_robin_1, {
--     file = {
--       path = "samples/file_c#3.flac",
--       root_key = 61,
--     },
--   }))
-- 
-- Most of Lua's standard libraries are accessible to this script: table, string, math, 
-- utf8, package, coroutine. However, some libraries are not available to improve the 
-- safety of running Lua code from unknown sources. packages.loadlib is also not 
-- available.

-- Creates a new library. There should only be one. Return this at the end of the file.
local library = mirage.new_library({
    -- The name of the library. [required]
    name = "Rhythmic Evo",
    -- A few words to describe the library. [required]
    tagline = "Oscillating audio arabesque",
    -- The URL associated with the library.
    -- [optional, default: no url]
    url = "https://example.com/product",
    -- The name of the creator of this library. [required]
    author = "Example Name",
    -- The minor version of this library - backwards-compatible changes are allowed on a 
    -- library; this field represents that. Non-backwards-compatibile changes are not 
    -- allowed: you'd need to create a new library such as: "Strings 2".
    -- [optional, default: 1]
    minor_version = 1,
    -- Path relative to this script for the background image. It should be a jpg, png or 
    -- webp. [required]
    background_image_path = "images/background.jpg",
    -- Path relative to this script for the icon image. It should be a square jpg, png or 
    -- webp. [required]
    icon_image_path = "images/icon.png",
})
-- Creates a new instrument. First argument is a library, second argument is a table of 
-- config options.
local instrument = mirage.new_instrument(library, {
    -- The name of the instrument. [required]
    name = "Glimmering Tones",
    -- Words separated by slashes used to hierarchically categorise the instrument.
    -- [optional, default: no folders]
    folders = "Bass/Dark",
    -- A description of the instrument.
    -- [optional, default: no description]
    description = "Multi-sampled music box with 4 round robin layers.",
    -- An array of strings to denote properties of the instrument.
    -- [optional, default: no tags]
    tags = { "Texture", "Bright", "Airy" },
})
mirage.add_region(instrument, {
    -- The file for this region. [required]
    file = {
        -- A path to an audio file, relative to this current lua file. [required]
        path = "One-shots/Resonating String.flac",
        -- The pitch of the audio file as a number from 0 to 127 (a MIDI note number). On 
        -- a range from 0 to 127. [required]
        root_key = 60,
        -- The region of the file that can be looped. It should be an array of 4 integers: 
        -- { start, end, crossfade, is_ping_pong (1 or 0) }. Note that the end number is 
        -- not inclusive.
        -- [optional, default: no loop]
        loop = { 24, 6600, 100, 0 },
    },
    -- How this region should be triggered.
    -- [optional, default: defaults]
    trigger_criteria = {
        -- What event triggers this region. Must be one of: "note-on" or "note-off".
        -- [optional, default: note-on]
        trigger_event = "note-on",
        -- The pitch range of the keyboard that this region is mapped to. These should be 
        -- MIDI note numbers, from 0 to 127. Note that the end number is not inclusive.
        -- [optional, default: { 60, 64 }]
        key_range = { 60, 64 },
        -- The velocity range of the keyboard that this region is mapped to. This should 
        -- be an array of 2 numbers ranging from 0 to 100. The first number represets the 
        -- start of the velocity range and the second number represents 1-past the end of 
        -- the range.
        -- [optional, default: { 0, 100 }]
        velocity_range = { 0, 100 },
        -- Trigger this region only on this round-robin index. For example, if this index 
        -- is 0 and there are 2 other groups with round-robin indices of 1 and 2, then 
        -- this region will trigger on every third press of a key only.
        -- [optional, default: no round-robin]
        round_robin_index = 0,
    },
    -- Additional options for this region.
    -- [optional, default: defaults]
    options = {
        -- The start and end point, from 0 to 100, of the Timbre knob on Mirage's GUI that 
        -- this region should be heard. You should overlay this range with other 
        -- timbre_crossfade_regions. Mirage will create an even crossfade of all 
        -- overlapping sounds. Note that the end number is not inclusive.
        -- [optional, default: no timbre-crossfade]
        timbre_crossfade_region = { 0, 50 },
        -- For every region that matches this group, automatically set the start and end 
        -- values for each region's key range based on its root key. Only works if all 
        -- region's velocity range is the same.
        -- [optional, default: false]
        auto_map_key_range_group = "false",
        -- For exery region that has this same group, automatically smooth the boundry 
        -- between velocity regions by blending the sound of adjacent regions.
        -- [optional, default: false]
        velocity_range_blur_group = "true",
    },
})
return library
Back to list

Leave a Reply