<UVI4>
    <Program Name="Program" Bypass="0" Gain="1" Pan="0" DisplayName="Mapping Articulations" TransposeOctaves="0" TransposeSemiTones="0" OutputName="" Polyphony="16" NotePolyphony="0" ProgramPath="./MappingArticulations.M5p" LoopProgram="0" Streaming="1" BypassInsertFX="0">
        <ControlSignalSources/>
        <EventProcessors>
            <ScriptProcessor Name="EventProcessor0" Bypass="0" API_version="13">
                <Properties ScriptPath="./MappingArticulations.lua"/>
                <script><![CDATA[--------------------------------------------------------------------------------
--! @title MappingArticulations
--! @brief Switching mappings and articulations at run time
--! @category PerformanceArticulation
--!
--! Drives a @ref SampleMappingOscillator two ways: one menu swaps the whole
--! mapping — the expensive, disk-bound switch, with a progress bar — and another
--! picks the articulation within it, which costs nothing because it only changes
--! the `dim1` coordinate the next note-on carries.
--!
--! The mapping load is deliberately *not* in @ref onInit. A program restores its
--! own mapping with the preset, so there is nothing to load or wait for at init;
--! the script loads a mapping only when the player switches to another one. That
--! is also why the menu's callback opens with the `userReady` guard that
--! @ref ExIRLoader "IRLoader" explains: a persistent widget's `changed` fires
--! during preset restore too, and re-loading there would re-hit the disk for a
--! mapping the program already holds.
--!
--! Three details are specific to sample mapping, and the
--! @ref SampleMappingIntro guide covers all of them in depth.
--!
--! - **the status waits, the callback reports.** `getLoadingStatus()` drops to 0
--!   for the load and climbs back to 1, which both paces the wait and feeds the
--!   bar; `loadMapping`'s callback only says whether the mapping loaded at all.
--! - **`dim1` needs @ref postEvent.** @ref playNote has no parameter for it, so a
--!   note played through it always lands on layer 0.
--! - **purging is per layer.** The generic @ref purge does nothing on this
--!   oscillator; `purgeZones(dim1, -1)` frees one whole layer.
--!
--! @note The bundled patch is the part you cannot build by hand: a Sample Mapping
--! oscillator is not offered in the element browser. It ships with an empty
--! mapping path, so it loads silently and waits for you to point the Instrument
--! menu at your own mapping files — `Mappings/<name>.dmap`, next to the patch,
--! with layers ordered like `ARTICULATIONS`. No mapping ships with the
--! documentation.
--!
--! @asset MappingArticulations.uvip Bundled Sample Mapping patch (load in Falcon)
--! @demonstrates osc:loadMapping, osc:purgeZones, postEvent with dim1, Menu, OnOffButton, spawn
--------------------------------------------------------------------------------

local INSTRUMENTS = { "Violin", "Viola", "Cello" }

-- Menu entry i addresses layer dim1 = i - 1: a layer's dim1 is its position in
-- the mapping file, counted from 0.
local ARTICULATIONS = { "Sustain", "Staccato", "Pizzicato" }

local osc = Program.layers[1].keygroups[1].oscillators[1]

-- Flipped in onInit, once preset state has been restored. Any .changed that hits
-- the disk must check it.
local userReady = false

-- True while a mapping is being replaced: notes played against a half-loaded
-- mapping are silent, so we stop triggering rather than drop them on the floor.
local loading = false

local currentDim1 = 0

local instrumentMenu = Menu{"Instrument", INSTRUMENTS,
    backgroundColour = "333333",
    textColour       = "white",
}

local artMenu = Menu{"Articulation", ARTICULATIONS,
    backgroundColour = "333333",
    textColour       = "white",
}

local economy = OnOffButton{"Economy", false}

local progress = Slider{"loading", 0, 0, 1}
progress.visible = false

-- Keep only the selected articulation in memory when economy is on. A negative
-- dim2 means "the whole dim1 layer".
local function applyEconomy()
    for i = 1, #ARTICULATIONS do
        local dim1 = i - 1
        if economy.value and dim1 ~= currentDim1 then
            osc:purgeZones(dim1, -1)
        else
            osc:unpurgeZones(dim1, -1)
        end
    end
end

local function loadInstrument(name)
    if not userReady then return end

    -- Silence the layer before the mapping under it is replaced.
    postEvent{ type = Event.ControlChange, controller = 120, value = 0, layer = 1 }
    loading = true

    progress.visible = true
    progress:setValue(0, false)

    -- The callback answers one question: did the mapping load at all. The engine
    -- has already told the user which file it could not find.
    osc:loadMapping("Mappings/" .. name .. ".dmap", function(task)
        if not task.success then print("could not load", name) end
    end)

    -- Waiting is the status's job, and it needs its own thread: a widget callback
    -- cannot wait() itself.
    spawn(function()
        while osc:getLoadingStatus() < 1.0 do
            progress:setValue(osc:getLoadingStatus(), false)
            wait(30)
        end

        progress.visible = false
        loading = false
        applyEconomy()
    end)
end

instrumentMenu.changed = function(self)
    loadInstrument(self.selectedText)
end

artMenu.changed = function(self)
    currentDim1 = self.selected - 1
    if not userReady then return end
    applyEconomy()
end

economy.changed = function(self)
    if not userReady then return end
    applyEconomy()
end

function onNote(e)
    if loading then return end

    -- Adding dim1 to the incoming event is enough; postEvent turns it into a
    -- mapping dispatch. dim2 is left out so the round robin picks the variant.
    e.dim1 = currentDim1
    postEvent(e)
end

function onInit()
    currentDim1 = artMenu.selected - 1

    if osc == nil or osc.type ~= "SampleMappingOscillator" then
        print("no Sample Mapping oscillator in the first keygroup")
        return
    end

    -- The program's own mapping is already loaded: nothing to wait for here.
    userReady = true
end

makePerformanceView()
]]></script>
            </ScriptProcessor>
        </EventProcessors>
        <Inserts/>
        <Layers>
            <Layer Name="Layer 0" Bypass="0" Gain="1" Pan="0" Mute="0" MidiMute="0" Solo="0" DisplayName="Layer 1" OutputName="" LowKey="0" HighKey="127" CustomPolyphony="0" PlayMode="0" PortamentoTime="0.029999999" PortamentoMode="0" NumVoicesPerNote="1" VelocityCurve="0" BypassInsertFX="0">
                <Properties Color="ffff0000"/>
                <ControlSignalSources/>
                <BusRouters/>
                <Keygroups>
                    <Keygroup Name="Keygroup 0" Bypass="0" Gain="1" Pan="0" DisplayName="Keygroup 1" OutputName="" ExclusiveGroup="0" LowKey="0" HighKey="127" LowVelocity="1" HighVelocity="127" LowKeyFade="0" HighKeyFade="0" LowVelocityFade="0" HighVelocityFade="0" TriggerMode="0" TriggerSync="0" TriggerRule="0" LatchTrigger="0" FXPostGain="1" BypassInsertFX="0">
                        <Connections>
                            <SignalConnection Name="AmpEnvMod" Ratio="1" Source="Amp. Env" Destination="Gain" Mapper="" ConnectionMode="0" Bypass="0" Inverted="0"/>
                        </Connections>
                        <ControlSignalSources>
                            <DAHDSR Name="Amp. Env" Bypass="0" DelayTime="0" AttackTime="0.0089999996" AttackCurve="0.5" HoldTime="0" DecayTime="0.17399999" DecayCurve="-0.97000003" SustainLevel="1" ReleaseTime="0.20000000" ReleaseCurve="-0.97000003" DisplayName="Amp. Env" VelocityAmount="0" VelocitySens="0.75" Retrigger="1" NoteOffRetrigger="0"/>
                        </ControlSignalSources>
                        <Inserts/>
                        <BusRouters/>
                        <Oscillators>
                            <SampleMappingOscillator Name="Oscillator" Bypass="0" MappingPath="" InterpolationMode="1" RoundRobinMode="1" CoarseTune="0" FineTune="0" Gain="1" Pitch="0" NoteTracking="1" BaseNote="60" DisplayName="Sample Mapping"/>
                        </Oscillators>
                    </Keygroup>
                </Keygroups>
            </Layer>
        </Layers>
    </Program>
</UVI4>
