Section 6.14
OS 1.3.0
EN

Script Showcase: OXI E16 manual

Worked Lua script examples for the OXI E16: the general API, manual value ownership and a realistic SysEx editor.

6.14 Script Showcase

Example 1: General API

--@assign id=1 abbr="C/F" name="Coarse" desc="Coarse Tune" l=0 h=127 d=1 g=1 swap=
--@assign id=2 abbr="C/F" name="Fine" desc="Fine Tune" l=0 h=127 d=2 g=1 accel
--@assign id=3 abbr="Wave" name="Waveform" desc="Waveform Select" l=0 h=3 manual=true
--@assign id=4 abbr="Driv" name="Drive" desc="Drive Amount"
--@assign id=5 abbr="FX" name="FX Swap" desc="FX Turn" l=0 h=127 d=1 g=2
--@assign id=6 abbr="FX" name="FX Push" desc="FX Push Toggle" p=true g=2

local out = 0
local waves = {"sine", "sqre", "tria", "saw"}
local wave_step = 0
local fx_on = false
local my_val = 0

function page.onInit()
  page.setTitle("Synth Edit")
  slots.update(2, waves[wave_step + 1])
end

function controller.onEncoderTurn(enc)
  if enc.id == 3
    then local prev = wave_step
    wave_step = wave_step + enc.increment
    if wave_step < 0 then wave_step = 0 end
    if wave_step > 3 then wave_step = 3 end
    if wave_step ~= prev
      then local iv = wave_step * 16383 // 3
      controller.set(enc.id, "v", iv)
      slots.update(enc.index, waves[wave_step + 1])
      leds.update(enc.id, iv)
      midi.sendSysex(out, {0xF0, 0x7D, 0x01, 2, wave_step, 0xF7})
    end
  elseif enc.id == 4 then
    my_val = my_val + enc.increment
    if my_val < 0 then my_val = 0 end
    if my_val > 127 then my_val = 127 end
    leds.update(enc.id, my_val * 16383 // 127)
    slots.update(enc.index, string.format("%3d", my_val))
    midi.sendSysex(out, {0xF0, 0x7D, 0x09, my_val, 0xF7})
  elseif enc.id == 1 then
    midi.sendSysex(out, {0xF0, 0x7D, 0x10, enc.scaled, 0xF7})
  elseif enc.id == 2 then
    midi.sendSysex(out, {0xF0, 0x7D, 0x11, enc.scaled, 0xF7})
  elseif enc.id == 5 then
    midi.sendSysex(out, {0xF0, 0x7D, 0x20, enc.scaled, 0xF7})
  end
end
function controller.onEncoderPress(enc)
  if enc.id == 6 then
    fx_on = not fx_on
    local val = fx_on and 127 or 0
    midi.sendSysex(out, {0xF0, 0x7D, 0x01, 5, val, 0xF7})
    slots.update(enc.index, fx_on and "FX.Y" or "FX.N")
  end
end

function controller.onSysex(bytes)
  if bytes[2] ~= 0x7D then return end
  local pg = controller.getPage()

  if bytes[3] == 0x11 then
    local idx = bytes[4] + 1
    local value = bytes[5]
    local internal = (value * 16383 + 63) // 127
    controller.setByIndex(pg, idx, "v", internal)
  elseif bytes[3] == 0x12 then
    for i = 1, 16 do
      if bytes[3 + i] then
        local internal = (bytes[3 + i] * 16383 + 63) // 127
        controller.setByIndex(pg, i, "v", internal)
      end
    end
  end
end

function page.onPageChange(prev, curr)
  for i = 1, 16 do
    leds.reset(i)
    slots.reset(i)
  end
end

Example 2:

This example demonstrates manual value ownership, two turn destinations sharing one Script ID, writing values into another control's destinations, destination swapping, batched LED-ring overrides, dynamic slot labels, and periodic updates.

Place the two SINE assignments on encoder 1, the two TGT assignments on encoder 2, Wr1 on encoder 3, Wr2 on encoder 4, and Rate on encoder 5.

-- Manual mode test script.
--
-- Expected behavior:
--
-- Encoder 1 drives a script-owned sine-pattern LED ring.
-- - With encoder 2 destination 1 active, encoder 3 changes encoder 2's ring;
-- encoder 4 only changes the hidden destination 2 value.
-- - Swap encoder 2. Its ring jumps to destination 2's stored value. Encoder 4
-- now changes the visible ring, while encoder 3 only changes hidden dest 1.
-- - Encoder 2's target destinations stay managed so Swap behaves normally.
-- The sine control and both writer controls are manual; Lua applies their
-- increments and writes their internal values explicitly.
-- - Encoder 5 continuously sweeps up and down from system.update(). Turning
-- it changes the sweep frequency, proving that periodic Lua polling and a
-- manual Script control can work together.

--@assign id=201 abbr="SINE" name="Manual sine action A" l=0 h=127 d=1 g=1 swap=true manual=true
--@assign id=201 abbr="SINE" name="Manual sine action B" l=0 h=127 d=2 g=1 manual=true
--@assign id=202 abbr="TGT" name="Target destination 1" l=0 h=127 d=1 g=2 swap=true
--@assign id=203 abbr="TGT" name="Target destination 2" l=0 h=127 d=2 g=2
--@assign id=204 abbr="Wr1" name="Manual writer destination 1" l=0 h=127 manual=true
--@assign id=205 abbr="Wr2" name="Manual writer destination 2" l=0 h=127 manual=true
--@assign id=209 abbr="Rate" name="Polling LED frequency" l=0 h=127 manual=true

local SINE_ID = 201
local TARGET_DEST_1_ID = 202
local TARGET_DEST_2_ID = 203
local WRITE_DEST_1_ID = 204
local WRITE_DEST_2_ID = 205
local POLL_RATE_ID = 209

local step = 512
local sine_value = 4096
local dest_1_value = 2048
local dest_2_value = 14336
local writer_1_value = dest_1_value
local writer_2_value = dest_2_value
local tau = 6.283185307179586
local led_phase = 0
local led_phase_steps = 32

local POLL_LED_INDEX = 5
local poll_page = 1
local poll_led_value = 0
local poll_led_direction = 1
local poll_rate_value = 4096
local poll_led_step = 0

local function clamp14(value)
  if value < 0 then return 0 end
  if value > 16383 then return 16383 end
  return value
end

local function advance(value, increment)
  return clamp14(value + increment * step)
end

local function to7Bit(value)
  return math.floor(value * 127 / 16383 + 0.5)
end

local function updateWriterLabel(index, writer, value)
  slots.update(index, string.format("%d%03d", writer, to7Bit(value)))
end

local function updateSineLed()
  local angle = (led_phase % led_phase_steps) * tau / led_phase_steps
  local normalized = math.sin(angle) * 0.45 + 0.5
  local ring_value = math.floor(normalized * 16383 + 0.5)
  leds.update(SINE_ID, ring_value, 10)
end

local function updatePollRate()
  -- 64..2048 per 20 ms update: roughly 0.1 Hz to 3.1 Hz.
  poll_led_step = 64 + math.floor(poll_rate_value * 4096 / 16383 + 0.5)
end
function page.onInit()
  page.setTitle("All manual")
  poll_page = controller.getPage()
  system.setUpdateRate(20)

  controller.set(SINE_ID, {n="SINE", manual=true, v=sine_value})
  controller.set(TARGET_DEST_1_ID, {v=dest_1_value})
  controller.set(TARGET_DEST_2_ID, {v=dest_2_value})
  controller.set(WRITE_DEST_1_ID, {manual=true, v=writer_1_value})
  controller.set(WRITE_DEST_2_ID, {manual=true, v=writer_2_value})
  controller.set(POLL_RATE_ID, {manual=true, v=poll_rate_value})

  updatePollRate()
  updateSineLed()
  leds.update({
    {WRITE_DEST_1_ID, writer_1_value, 4},
    {WRITE_DEST_2_ID, writer_2_value, 8},
    {POLL_RATE_ID, poll_led_value, (poll_led_step / 16383) * 100},
  })
  updateWriterLabel(3, 1, writer_1_value)
  updateWriterLabel(4, 2, writer_2_value)
end

function page.onPageChange()
  for i = 1, 16 do
    slots.reset(i)
    leds.reset(i)
  end
end

function system.update()
  if controller.getPage() ~= poll_page then return end

  poll_led_value = poll_led_value + poll_led_direction * poll_led_step
  if poll_led_value >= 16383 then
    poll_led_value = 16383
    poll_led_direction = -1
  elseif poll_led_value <= 0 then
    poll_led_value = 0
    poll_led_direction = 1
  end

  leds.update(POLL_RATE_ID, poll_led_value, (poll_led_value / 16383) * 100)
end

Example 3: Realistic Sysex Editor

function controller.onEncoderTurn(enc)
  if enc.id == SINE_ID then
    sine_value = advance(sine_value, enc.increment)
    led_phase = (led_phase + enc.increment) % led_phase_steps
    controller.set(SINE_ID, "v", sine_value)
    updateSineLed()
    page.setTitle("Sine " .. sine_value)

  elseif enc.id == TARGET_DEST_1_ID then
    -- Managed target: firmware has already applied the increment.
    dest_1_value = enc.value
    page.setTitle("T1 " .. dest_1_value)

  elseif enc.id == TARGET_DEST_2_ID then
    -- Managed target: firmware has already applied the increment.
    dest_2_value = enc.value
    page.setTitle("T2 " .. dest_2_value)

  elseif enc.id == WRITE_DEST_1_ID then
    writer_1_value = advance(writer_1_value, enc.increment)
    dest_1_value = writer_1_value
    controller.set(WRITE_DEST_1_ID, "v", writer_1_value)
    controller.set(TARGET_DEST_1_ID, "v", dest_1_value)
    leds.update(WRITE_DEST_1_ID, writer_1_value, 4)
    updateWriterLabel(enc.index, 1, writer_1_value)
    page.setTitle("D1 " .. dest_1_value)

  elseif enc.id == WRITE_DEST_2_ID then
    writer_2_value = advance(writer_2_value, enc.increment)
    dest_2_value = writer_2_value
    controller.set(WRITE_DEST_2_ID, "v", writer_2_value)
    controller.set(TARGET_DEST_2_ID, "v", dest_2_value)
    leds.update(WRITE_DEST_2_ID, writer_2_value, 8)
    updateWriterLabel(enc.index, 2, writer_2_value)
    page.setTitle("D2 " .. dest_2_value)

  elseif enc.id == POLL_RATE_ID then
    poll_rate_value = advance(poll_rate_value, enc.increment)
    controller.set(POLL_RATE_ID, "v", poll_rate_value)
    updatePollRate()
    page.setTitle("Rate " .. to7Bit(poll_rate_value))
  end
end

Example 3: Realistic Sysex Editor

-- Yamaha TX81Z complete editor for OXI E16
-- Per-operator params (id 1-64), voice common (65-86), op enable pushes (87-90)

-- OP4 per-operator (id 1-16)
--@assign id=1  abbr="CRS"  name="OP4 Coarse"      l=0 h=63
--@assign id=2  abbr="Fine" name="OP4 Fine Freq"    l=0 h=15
--@assign id=3  abbr="FRng" name="OP4 Freq Range"   l=0 h=7
--@assign id=4  abbr="DET"  name="OP4 Detune"       l=0 h=6
--@assign id=5  abbr="AR"   name="OP4 Attack"       l=0 h=31
--@assign id=6  abbr="D1R"  name="OP4 Decay 1"      l=0 h=31
--@assign id=7  abbr="D2R"  name="OP4 Decay 2"      l=0 h=31
--@assign id=8  abbr="RR"   name="OP4 Release"      l=1 h=15
--@assign id=9  abbr="D1L"  name="OP4 Decay Lvl"    l=0 h=15
--@assign id=10 abbr="LS"   name="OP4 Level Scale"  l=0 h=99
--@assign id=11 abbr="RS"   name="OP4 Rate Scale"   l=0 h=3
--@assign id=12 abbr="EBS"  name="OP4 EG Bias Sens" l=0 h=7
--@assign id=13 abbr="KVS"  name="OP4 Key Vel Sens" l=0 h=7
--@assign id=14 abbr="OUT"  name="OP4 Output Level" l=0 h=99
--@assign id=15 abbr="SHFT" name="OP4 EG Shift"     l=0 h=3
--@assign id=16 abbr="OSW"  name="OP4 Osc Wave"     l=0 h=7

-- OP3 per-operator (id 17-32)
--@assign id=17 abbr="CRS"  name="OP3 Coarse"      l=0 h=63
--@assign id=18 abbr="Fine" name="OP3 Fine Freq"    l=0 h=15
--@assign id=19 abbr="FRng" name="OP3 Freq Range"   l=0 h=7
--@assign id=20 abbr="DET"  name="OP3 Detune"       l=0 h=6
--@assign id=21 abbr="AR"   name="OP3 Attack"       l=0 h=31
--@assign id=22 abbr="D1R"  name="OP3 Decay 1"      l=0 h=31
--@assign id=23 abbr="D2R"  name="OP3 Decay 2"      l=0 h=31
--@assign id=24 abbr="RR"   name="OP3 Release"      l=1 h=15
--@assign id=25 abbr="D1L"  name="OP3 Decay Lvl"    l=0 h=15
--@assign id=26 abbr="LS"   name="OP3 Level Scale"  l=0 h=99
--@assign id=27 abbr="RS"   name="OP3 Rate Scale"   l=0 h=3
--@assign id=28 abbr="EBS"  name="OP3 EG Bias Sens" l=0 h=7
--@assign id=29 abbr="KVS"  name="OP3 Key Vel Sens" l=0 h=7
--@assign id=30 abbr="OUT"  name="OP3 Output Level" l=0 h=99
--@assign id=31 abbr="SHFT" name="OP3 EG Shift"     l=0 h=3
--@assign id=32 abbr="OSW"  name="OP3 Osc Wave"     l=0 h=7

-- OP2 per-operator (id 33-48)
--@assign id=33 abbr="CRS"  name="OP2 Coarse"      l=0 h=63
--@assign id=34 abbr="Fine" name="OP2 Fine Freq"    l=0 h=15
--@assign id=35 abbr="FRng" name="OP2 Freq Range"   l=0 h=7
--@assign id=36 abbr="DET"  name="OP2 Detune"       l=0 h=6
--@assign id=37 abbr="AR"   name="OP2 Attack"       l=0 h=31
--@assign id=38 abbr="D1R"  name="OP2 Decay 1"      l=0 h=31
--@assign id=39 abbr="D2R"  name="OP2 Decay 2"      l=0 h=31
--@assign id=40 abbr="RR"   name="OP2 Release"      l=1 h=15
--@assign id=41 abbr="D1L"  name="OP2 Decay Lvl"    l=0 h=15
--@assign id=42 abbr="LS"   name="OP2 Level Scale"  l=0 h=99
--@assign id=43 abbr="RS"   name="OP2 Rate Scale"   l=0 h=3
--@assign id=44 abbr="EBS"  name="OP2 EG Bias Sens" l=0 h=7
--@assign id=45 abbr="KVS"  name="OP2 Key Vel Sens" l=0 h=7
--@assign id=46 abbr="OUT"  name="OP2 Output Level" l=0 h=99
--@assign id=47 abbr="SHFT" name="OP2 EG Shift"     l=0 h=3
--@assign id=48 abbr="OSW"  name="OP2 Osc Wave"     l=0 h=7

-- OP1 per-operator (id 49-64)
--@assign id=49 abbr="CRS"  name="OP1 Coarse"      l=0 h=63
--@assign id=50 abbr="Fine" name="OP1 Fine Freq"    l=0 h=15
--@assign id=51 abbr="FRng" name="OP1 Freq Range"   l=0 h=7
--@assign id=52 abbr="DET"  name="OP1 Detune"       l=0 h=6
--@assign id=53 abbr="AR"   name="OP1 Attack"       l=0 h=31
--@assign id=54 abbr="D1R"  name="OP1 Decay 1"      l=0 h=31
--@assign id=55 abbr="D2R"  name="OP1 Decay 2"      l=0 h=31
--@assign id=56 abbr="RR"   name="OP1 Release"      l=1 h=15
--@assign id=57 abbr="D1L"  name="OP1 Decay Lvl"    l=0 h=15
--@assign id=58 abbr="LS"   name="OP1 Level Scale"  l=0 h=99
--@assign id=59 abbr="RS"   name="OP1 Rate Scale"   l=0 h=3
--@assign id=60 abbr="EBS"  name="OP1 EG Bias Sens" l=0 h=7
--@assign id=61 abbr="KVS"  name="OP1 Key Vel Sens" l=0 h=7
--@assign id=62 abbr="OUT"  name="OP1 Output Level" l=0 h=99
--@assign id=63 abbr="SHFT" name="OP1 EG Shift"     l=0 h=3
--@assign id=64 abbr="OSW"  name="OP1 Osc Wave"     l=0 h=7

-- Voice common (id 65-81)
--@assign id=65 abbr="ALG"  name="Algorithm"        l=0 h=7
--@assign id=66 abbr="FB"   name="Feedback"         l=0 h=7
--@assign id=67 abbr="LSPD" name="LFO Speed"        l=0 h=99
--@assign id=68 abbr="LDLY" name="LFO Delay"        l=0 h=99
--@assign id=69 abbr="PMD"  name="PM Depth"          l=0 h=99
--@assign id=70 abbr="AMD"  name="AM Depth"          l=0 h=99
--@assign id=71 abbr="LSYN" name="LFO Key Sync"     l=0 h=1
--@assign id=72 abbr="LW"   name="LFO Wave"         l=0 h=3
--@assign id=73 abbr="PMS"  name="LFO PM Sens"      l=0 h=7
--@assign id=74 abbr="AMS"  name="LFO AM Sens"      l=0 h=3
--@assign id=75 abbr="MID"  name="Middle C"          l=0 h=48
--@assign id=76 abbr="PLAY" name="Play Mode"         l=0 h=1
--@assign id=77 abbr="PB"   name="Pitch Bend Range" l=0 h=12
--@assign id=78 abbr="PMOD" name="Portamento Mode"  l=0 h=1
--@assign id=79 abbr="PTIM" name="Portamento Time"  l=0 h=99
--@assign id=80 abbr="MWP"  name="MW Pitch Range"   l=0 h=99
--@assign id=81 abbr="MWA"  name="MW Amp Range"     l=0 h=99
-- Per-operator ratio/fixed toggle (id 82-85)
--@assign id=82 abbr="R/F"  name="OP4 Ratio/Fixed"  l=0 h=1
--@assign id=83 abbr="R/F"  name="OP3 Ratio/Fixed"  l=0 h=1
--@assign id=84 abbr="R/F"  name="OP2 Ratio/Fixed"  l=0 h=1
--@assign id=85 abbr="R/F"  name="OP1 Ratio/Fixed"  l=0 h=1

-- Reverb (id 86)
--@assign id=86 abbr="REV"  name="Reverb Rate"      l=0 h=7

-- Operator enable push toggles (id 87-90)
--@assign id=87 abbr="OP1"  name="OP1 Enable"       p=true
--@assign id=88 abbr="OP2"  name="OP2 Enable"       p=true
--@assign id=89 abbr="OP3"  name="OP3 Enable"       p=true
--@assign id=90 abbr="OP4"  name="OP4 Enable"       p=true

local out = 0
local channel = 0

local vced = {0xF0, 0x43, 0x10 + channel, 0x12, 0, 0, 0xF7}
local aced = {0xF0, 0x43, 0x10 + channel, 0x13, 0, 0, 0xF7}

-- 0 = VCED, 1 = ACED per script_id (1-86)
local param_type = {
  -- OP4 (1-16)
  0,1,1,0, 0,0,0,0,0,0,0,0,0,0,1,1,
  -- OP3 (17-32)
  0,1,1,0, 0,0,0,0,0,0,0,0,0,0,1,1,
  -- OP2 (33-48)
  0,1,1,0, 0,0,0,0,0,0,0,0,0,0,1,1,
  -- OP1 (49-64)
  0,1,1,0, 0,0,0,0,0,0,0,0,0,0,1,1,
  -- Voice common (65-81): all VCED
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  -- Ratio/Fixed (82-85): all ACED
  1,1,1,1,
  -- Reverb (86): ACED
  1,
}
-- Sysex address byte per script_id (1-86)
local param_num = {
  -- OP4 (1-16)
  11, 2, 1,12,  0, 1, 2, 3, 4, 5, 6, 7, 9,10, 4, 3,
  -- OP3 (17-32)
  24, 7, 6,25, 13,14,15,16,17,18,19,20,22,23, 9, 8,
  -- OP2 (33-48)
  37,12,11,38, 26,27,28,29,30,31,32,33,35,36,14,13,
  -- OP1 (49-64)
  50,17,16,51, 39,40,41,42,43,44,45,46,48,49,19,18,
  -- Voice common (65-81): VCED addresses
  0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B,
  0x3C,0x3D,0x3E,0x3F,0x40,0x41,0x42,0x47,0x48,
  -- Ratio/Fixed (82-85): ACED addresses Op4,Op3,Op2,Op1
  0x00,0x05,0x0A,0x0F,
  -- Reverb (86): ACED address
  0x14,
}

-- Operator enable state: Op1, Op2, Op3, Op4 (1=on, 0=off)
local op_en = {1, 1, 1, 1}

local function sendOpEnable()
  local packed = (op_en[1] * 8) + (op_en[2] * 4) + (op_en[3] * 2) + op_en[4]
  vced[5] = 0x5D
  vced[6] = packed
  midi.sendSysex(out, vced)
end

function page.onInit()
  page.setTitle("TX81Z")
end

function controller.onEncoderTurn(enc)
  if enc.id >= 1 and enc.id <= 86 then
    local msg = param_type[enc.id] == 1 and aced or vced
    msg[5] = param_num[enc.id]
    msg[6] = enc.scaled
    midi.sendSysex(out, msg)
  end
end

function controller.onEncoderPress(enc)
  -- Op enable toggles (id 87=OP1, 88=OP2, 89=OP3, 90=OP4)
  if enc.id >= 87 and enc.id <= 90 then
    local op = enc.id - 86  -- 1-4
    op_en[op] = 1 - op_en[op]
    sendOpEnable()
    slots.update(enc.index, op_en[op] == 1 and "ON" or "OFF")
  end
end
This page covers OS 1.3.0 (manual 1.3.0a, PDF pages 116 to 126).Download the PDF manualOXI E16 product page