-------------------------------------------------------------------------------- --! @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/.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()