Ensoniq Mirage MASOS template

Ok, I did not try to compile/run this code, so look at it carefully and see if it does what you want.
My suggestion is to save a version of whatever you have then give this a try. If you have any questions, ask me and I’ll see if I can help explain the functionality I changed.


mirageDeviceId = 9
previousParameterNumber = nil
lastValue = {}
maxBytes = 5
upArrow= 14
downArrow = 15
parameterSelect = 12
valueSelect = 13
endCommand = 127
arrowValue = 0
negativeArrowValue = 0
mainWorkerCounterCalls = 0

function mainWorker(ValueObject, Value)

  mainWorkerCounterCalls = mainWorkerCounterCalls + 1
  print("mainWorkerCounterCalls:" .. mainWorkerCounterCalls)

  local message = ValueObject:getMessage()
  local currentParameterNumber = message:getParameterNumber ()
  local curValue = message:getValue()
--
  if ( currentParameterNumber ~= previousParameterNumber ) then
    
    previousParameterNumber = currentParameterNumber
    print("we have parameterNumber: ", currentParameterNumber)

    byte1st = math.floor(currentParameterNumber / 10)
    byte2nd = currentParameterNumber % 10

    sysexMsg = { 15, 1, 1, parameterSelect, byte1st, byte2nd, valueSelect, endCommand }
    midi.sendSysex(PORT_1, sysexMsg)
   
  end
--

  if ( currentParameterNumber == previousParameterNumber ) then
    arrowValue = curValue - lastValue[currentParameterNumber]

    print (string.format("arrowValue = %d", arrowValue))

    if ( arrowValue < 0 ) then 
      print ('arrowValue is a negative number, sending message')
      -- for loop, for 5 bytes max
      negativeArrowValue = (0 - arrowValue)
    
      exp1 = 0 
      exp2 = negativeArrowValue

      print (string.format("Total down arrow bytes to send = %d", negativeArrowValue))
      for var = exp1,exp2,maxBytes do
   
        if ( negativeArrowValue > 5 ) then
 
          print("sending 5 arrowBytes,")
          negativeArrowValue = negativeArrowValue - 5
          sysexMsg = { 15, 1, 1, downArrow, downArrow, downArrow, downArrow, downArrow, endCommand }
          midi.sendSysex(PORT_1, sysexMsg)

        elseif ( negativeArrowValue < 5 ) and ( negativeArrowValue > 0) then

          print (negativeArrowValue, "Sending remaining bytes")
          sysexMsg = {15, 1, 1}

          for i = 1, negativeArrowValue do
            sysexMsg[3+i] = downArrow
          end

          sysexMsg[4+negativeArrowValue] = endCommand
          midi.sendSysex(PORT_1, sysexMsg)
          negativeArrowValue = 0

        end

      end

    elseif ( arrowValue > 0) then 
      print ('arrowValue is a postive number, sending message')

      exp1 = 0 
      exp2 = arrowValue

      print (string.format("Total up arrow bytes to send = %d", arrowValue))
      for var = exp1,exp2,maxBytes do
 
        if ( arrowValue > 5 ) then
 
          print("sending 5 arrowBytes,")
          arrowValue = arrowValue - 5
          sysexMsg = { 15, 1, 1, upArrow, upArrow, upArrow, upArrow, upArrow, endCommand }
          midi.sendSysex(PORT_1, sysexMsg)

        elseif ( arrowValue < 5 ) and ( arrowValue > 0) then
          print (arrowValue, "Sending remaining bytes")
          sysexMsg = {15, 1, 1}
          for i = 1, arrowValue do
            sysexMsg[3+i] = upArrow
          end
          sysexMsg[4+arrowValue] = endCommand
          midi.sendSysex(PORT_1, sysexMsg)
          arrowValue = 0
        end

      end
     
    else 
      print("nothing to do")
    end
    lastValue[currentParameterNumber] = curValue
   
  end

end
1 Like

wow thanks a lot!
we are traveling so can test only in the end of the week.
in the train I made a version with your suggestions. if, elseif, else…
Not sure why but i ended up with 137 lines of code end like 6 'end’s.

I will compare and learn from it.

really thanks a lot for your help.
looking forward to test it.

cheers

Tim

1 Like

Tim - you’re welcome. Note there was a typo (x2) in the above code. I fixed it above, but just in case –
the two “for loops” had an extra comma in them:

          for i = 1, arrowValue, do   <--- remove the comma after "arrowValue" and negativeArrowValue
            sysexMsg[3+i] = upArrow
          end

Also - if you get that code running and it is acceptable, attached is a ZIP file with a more compact version. I don’t like to optimize and condense code until I have a working version. Once I do, I try to look for duplication and try to see if I can clean it up. Note that I copied your initial Mirage info into the text file so I had a local reference while looking at the code.

masostest-2-working.zip (1.7 KB)

2 Likes

Wow what a peace of art! Looking forward to play with this in the end of the week.

Thanks a lot.

Cheers

Tim

1 Like

Ok I am testing the latest version with debug and it did not work due to the array being empty. ( or is it a list?)

It was fixed by adding a default if nil.

local arrowValue = curValue -  ( lastValue[currentParameterNumber] or 0 )

Might look into using default values.
But this might be good enought.

What is really nice that with @ oldgearguy version the first arrow up or down isn’t skipped anymore.

Here in the below example the value is at 21 .
I spin it back to 0 in a fast turn.
There a 2 arrows up to be found but the total remains -21 so kind of ok.

If the value jumps are larger you will see quite a lot of arrow down even you only spin the pot up.

Not sure why. Lets see if the Mirage can take it :slight_smile:

Updates: I did more testing now with the mirage. Good thing is the controls do work.
But only if you twist them slowly. Any bit to hard twitching will result in the Mirage and the Eone getting out of sync.

Also the Mirage can hang and needs to be restarted.

The device “rate” throttling even on the highest setting like 5000ms doesn’t do anything about this issue althought the messages look to be throttled.

This makes me think some kind of buffer is required.

So maybe an array needs to be filled with up and down arrows and then in periods the array is emptied but only when up and down arrows are cancelled out.
The emptying could happen on a clock just slow enough to not kill the Mirage.

Anyhow I am starting with a template.

1 Like

I made a template and it works.
One reason is that many parameters go only to 31.
The ones that go to 99 its possible to get out of sync but still manageable .
There are few parameters that go much higher and there its a problem.
Like [60] wavesample start. 00-FF 00-255.

But I am really happy.
The eone can control the Ensoniq Mirage!! Now both Soundprocess OS and MASOS the Sampling OS.

Thanks @oldergearguy !

1 Like

You are welcome. Glad you got it working.

The function willi be called for every turn, so the only real war to control the flow is using some kind of timer or delay between sysex calls.

I’ll see if i can come up with some suggestions

1 Like

yeah I am really happy.
I will extend the template and start to test/use it and see how it works in practice.

The lower ranged values might be ok using the current implementation.
That is, booleans, 0-31 and several multi options. If they stay in sync.

We might use a array to store the arrowValue only for larger max values going up to 255, 99 or 40 in a different variable instead of sending them.
Then use the timertick to check adjust and send that variable // arrows.
So its a kind of prehistoric buffer :slight_smile:
As well the positive or negative will cancel each other out so no redundant up or down arrows.
Preventing any unnecessary data being send out.

Really love what the Eone can do. With lua you can automate tasks etc.
Making multisamples on the Mirage isn’t an easytask but the eone can display all the calculated values for different scenarious.
What pitch to sample, at what samplerate etc.

1 Like

I have a partial implementation for throttling the speed sending sysex to a device.
It’s working well for 1 control. Just need to make it generic for all controls in both the positive and negative direction.

Also - since the encoders keep spinning even when you go past 0 or 127, the LUA code has to stop processing once you hit the minimum and maximum values for a control.

You can retrieve those using the :getMin() and :getMax()

2 Likes

Yeah that is a good idea.
Will check the min max values and see if I can add those.

Currious what you got for throttling.
Is that the code using the timer.tick in the tc template?

1 Like

The min/max will be a nice addition since it prevents you from both processing unnecessarily and from sending any extra data to the Mirage.

One question - your controls are all virtual, right? They are not also trying to send MIDI notes/CCs to the Mirage?

The throttling code is using a timer and is based on the concept of a queue or bucket. One process (the mainWorker() function called for every encoder turn) fills the queue and the timer process empties it at regular intervals.

I think I have it working for multiple controls right now, but I want to do some more testing and will send you something to look at/use by Monday

2 Likes

yeah everything virtual.
that is something i got from you.

All controls on 1 queue would be ideal as the arrows are anonymous i can imagine mayhem twisting 4 knobs at once. :slight_smile:

Looking forward to see what you come up with.
Not only for the template but in general you bring nice lua code.
and i am learning a lot from it.
And lua is getting quite fun now with the integrated debugger

Take care

Cheers

Tim

1 Like

When you have time, can you attach or PM me the complete project you have so far?
(download project)

The newer OS 3 allows you to take advantage of some things with regard to parameter number, control ID and such.

What I would like to see is how you defined all the controls so far. We might be able to make things easier to use and extend with a few simple tweaks.

thanks

2 Likes

Hi, @oldgearguy

Here is the complete project/template

https://app.electra.one/preset/869OXzeFJ3tseNenTAi0

Controls for parameters are virtual.
The parameter ID matches the parameter ID used on the Mirage frontpanel.

So byte1st, byte2nd is the conversion from Electraone’s virtual parameter id to the EnsoniqMirage Parameter id as seen on the reference card.
Where 90 is send as 09 00 and 9 as 00 09.

  if ( currentParameterNumber ~= previousParameterNumber ) then
    
    previousParameterNumber = currentParameterNumber
    print("we have parameterNumber: ", currentParameterNumber)

    -- common way to separate the tens digit and the ones digit
    local byte1st = math.floor(currentParameterNumber / 10)
    local byte2nd = currentParameterNumber % 10

    sysexMsg = { 15, 1, 1, parameterSelect, byte1st, byte2nd, valueSelect, endCommand }
    midi.sendSysex(PORT_1, sysexMsg)

Does this explain it to you?

1 Like

Thanks, I will look it over in the next day or so and hopefully provide some more suggestions

2 Likes

Here are al lot of notes after trying out what I got so far.
And I have been studying the manual carefully.
The Mirage is pretty complicated to use.
So I am in the process of understanding the thing properly.

    1. …there is a Program dump request message
      This command instruct the mirage to dump his current program for the upper or lower keyboard.
      So maybe an option is to load upper, then request the program data …

Get lower program
F0h 0Fh 01h 03h F7h
return header is F0 0F 01 05 00 0D

Get upper program
F0h 0Fh 01h 13h F7h
return header is F0h 0Fh 01h 15h 03h 0Ah

Strangely when I keep resending them the message length differs a few bytes every time on the webapp.
Sometimes it just returns some weird data like cc messages.
But in midiox the returned dump is always 512 bytes.
Strange as the manual states the dump should be 625 bytes in nybbles.
But I get 512 byes. 113 missing… so it might be just one program… need to check.

01 05 00 0D 03 0B 0E 0D 03 0B 00 0E 03 0B 02 0E 03 0B 00 0E 00 00 04 00 0A 0F 0F 03 06 0C 40 00 05 00 00 00 00 0F 01 0E 01 0E 01 0F 0F 00 00 00 00 00 00 00 00 03 0B 0E 0F 04 0B 00 00 04 0B 40 00 00 04 0B 00 00 00 00 03 00 0A 0F 0F 03 0C 03 06 0C 01 00 00 02 05 02 04 02 04 02 0F 0F 00 00 40 00 00 00 00 00 00 04 0B 0E 01 04 0B 02 02 04 0B 0E 01 04 0B 02 02 00 00 06 00 00 08 0F 03 00 00 40 06 0C 04 00 0F 02 00 03 0F 02 0F 02 0F 0F 00 00 00 00 00 00 00 00 04 0B 0E 03 04 0B 02 04 04 0B 40 0E 03 04 0B 02 04 00 00 03 00 0A 0F 0F 03 0C 03 06 0C 0A 00 00 05 0F 06 0E 06 0E 06 0F 0F 00 00 40 00 00 00 00 00 00 04 0B 0E 05 04 0B 02 06 04 0B 0E 05 04 0B 02 06 00 00 03 00 0A 0F 0F 03 0C 03 40 06 0C 00 01 00 07 0F 08 0E 08 0E 08 0F 0F 00 00 00 00 00 00 00 00 04 0B 0E 07 04 0B 02 08 04 0B 40 0E 07 04 0B 02 08 00 00 01 00 0A 0F 0F 03 0C 03 06 0C 03 01 00 09 0F 0A 0E 0A 0E 0A 0F 0F 00 00 40 00 00 00 00 00 00 04 0B 0E 09 04 0B 02 0A 04 0B 0E 09 04 0B 02 0A 00 00 01 00 0A 0F 0F 03 0C 03 40 06 0C 06 01 00 0B 0F 0C 0E 0C 0E 0C 0F 0F 00 00 00 00 00 00 00 00 04 0B 0E 0B 04 0B 02 0C 04 0B 40 0E 0B 04 0B 02 0C 00 00 01 00 0A 0F 0F 03 0C 03 06 0C 0C 01 00 0D 0F 0F 0E 0F 0E 0F 0F 0F 00 00 40 00 00 00 00 00 00 00 00 0F 03 00 08 0F 03 04 00 02 01 08 00 0B 01 00 01 04 02 00 02 0D 02 00 04 40 06 03 00 08 0F 03 00 0C 06 03 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 40 0F 0F 0F 0F 0F 0F 00 02 0D 02 01 02 00 00 02 02 09 00 08 01 0B 01 00 02 0D 02 00 04 06 03 00 08 40 0F 03 00 08 0F 03 00 00 00 00 00 00 00 00 00 00 00

    1. Programs are assign to Upper or Lower half.
      Called U+n or L+n, so U1 is Upper keyboard using program 1
      A program is like a patch, contains envelops, filter parameter data…

‘Program / 0’ button + N

Display shows L1, so here we select program 1, 2, 3, and 4
L3 = lower keyboard half uses program 3

Erich has been helpful a lot. He documented the coding of his Wavsyn editor.
If needed I can ask him for advise.

To switch to upper program we have to double select ‘Program / 0’ button without N

sending:
F0h 0Fh 01h 01h 00h 7Fh F7h

00h is program 00 key,

switch between upper and lower by sending 2 program 00 keys. But you do not know where you started so hard to say if you are on the upper or lower program, keyboard

So it looks we can just send a parameter request like

F0h 0Fh 01h 01h 0Ch 03h 07h 0Dh 7Fh F7h
03h 07h is Parameter [37] for Resonance

returns F0 0F 01 0D 10 25 08 04 F7
[25] is hex [37]…

And now we know which program we are on.

Erich wrote this on this:

A Few SYSEX Tidbits
Program numbers are 1-4 in the Mirage display, but stored internally as 0-3. when sending a sysex command to change them, you specify 0-3, but what is returned depends on whether it is a lower or upper program. If an upper program, bit 5 is set so you can get hex 10 for upper 0, hex 11 for upper 1, an so on.
Setting a program is horrible. You can’t just set “upper 1”. You have to know what you are starting from. If in upper, then you CAN just send a 0-3, but if in lower, you have to SWITCH to upper first, just like when using the front panel.
Reading a parameter will give you the program (upper or lower). This is handy.
select the resonance parameter: F0 0F 01 01 0C 03 07 0D 7F F7
0c means parameter number follows; 03 07 is 37. 0D means return the value.
returns F0 0F 01 0D 10 25 08 04 F7
Here is how to interpret the response string:
0D – returning a parameter value
10 – upper 1 program
25 – parameter 37
08 04 is the value, but sent as nibbles and in reverse order (common Mirage trick). 04 is upper half >byte, which means 64. 08 is just 8. 8 + 64 is 72, but resonance only goes to 40! That is, in the display. Internally it goes to 160, so you divide it by 4 to get the display value of 72/4 = 18.

    1. Every parameter with a range higher then 31 should be buffered.
      Other get out of sync. max 63 works but skips sometimes and 00-FF 0-255 are unusable.

I have been playing with the parameter

  • sample start [60]
  • sample end [61]
  • loop start [62]
  • loop end [63]

And I am a bit brainstorming how to do this.
value of 1 means 1 page, 1 page is 256 bytes and there are 256 pages per kb half. 256 pages * 256 = 65536 bytes aka 64KB.
the Mirage has 2x 64KB and 1x64KB can be allocated per keyboard half which can have 8 wave samples assigned.
Page 0 = first page
These values have a range from 0 to 255, 00 - FF and turning the knob doesn’t work well.
It looks the Mirage is getting busy and skips incoming messages.
These 4 parameters are related to each other.
What I found is

-1. Sample end [61] cannot be before Sample start [60] and is always at least 1 page more then [60]
-2. loop end [63] can be the same as loop start [62] which I think means a 1 page loop

parameter wave sample select [26] sets which wave sample you can edit.
But it looks I can let all the wave samples start at 00. have to test further what this does.

[SEQ REC] + [number] = upper wavesample select ( 1 - 8 )
[SEQ PLAY] + [number] = lower wavesample select ( 1 - 8 )

    1. MIX Mode / Chorus / single osc
mode
Single osc = mix mode [28] off DETUNE [33] = — mix [34] = 0 mix vel mod [35]= 1
chorus osc = mix mode [28] off DETUNE [33] = 1-20 mix [34] = 32 mix vel mod [35]= 1
vel mixing = mix mode [28] on DETUNE [33] = 0 mix [34] = 0 mix vel mod [35]= 1-13
wheel mix = mix mode [28] on DETUNE [33] = 0 mix [34] = — mix vel mod [35]= 0
  • 6.Wave sample assignment order: first lower 1 - 8, upper 1 - 8

Key assignment is further influenced by [28] mix mode and initial wave sample [27]
Mix Mode [on] osc 2 uses wave sample of osc 1 + 1
Wave samples are paired 1-2, 3-4, 5-6, 7-8 and only one top key per pair is honored.
-Topkey only 1, 3, 5, 7

    1. [27] initial wave sample = the first sample used in the sample assignment process.

if [27] initial wave sample = 3, topkey 1 and 2 are ignored.
So assignment = 3,5,5,6,7 and then 8

    1. wave sample start [60],wave sample end [60]

For each 8 wave sample per keyboard half
[60] wave sample start
[61] wave sample end
[62] loop start
[63] loop end
[64] loop end fine adj.
[65] loop OFF/ON
[69] relative amplitude
[72] top key
[66] wave sample rotate
[19] rotate current wave sample left by .n.
[20] rotate current wave sample right by .n.
[17] copy current wave sample to lower .n.
[18] copy current wave sample to upper .n.

    1. played pitch
      original pitch of the sample
      A keys is original pitch
      input sampling rate
      For this we can add overlay list to display the nearest notes and the pitch offset etc.
    1. LOAD SEQ = Masos function key + n

function = n
1 Cd Copy data from Source to destination
2 Fi Fade In from source start to Source End
3 Fo Fade Out from source start to Source End
4 Sc Scale the Source Data with a linear ramp function which ramps between Scale Start Factor and
Scale end Factor
5 Ad add source data to destination data, leave data in destination
6 In invert the source data
7 Rd reverse data from source start to source end
8 rP Replicate the first page of the source data on every page of the source data.

System reset = LOAD ALL ) = load upper + load lower + 0 + enter

#Midi cc
sustain on/off [89] sustain pedal mode
mod wheel 0…127 [84] midi controller enabled
Pitchbend 0…127 64 is center, [84] midi controller enabled

    1. Sysex message types
      front panel commands
      program dump request, n0 lower, n1 upper
      configuration dump request
      wave sample dump request [26] wave sample select
      wave sample dump request absolute – memory address
      wave sample manipulation commands

So this is enough for today :slight_smile:

1 Like

That’s a lot. It might be easier to just ship me a Mirage. :slight_smile:

Seriously though, let’s work on one thing at a time.
Even though it seems like creating the controls and getting editing to work is time consuming and challenging, processing sysex dumps can be another level of headaches.

2 Likes

Yeah don’t worry I just put down what I found out. Just to keep it in a place for future reference.
I am still figuring out how it should work finally and how far to go.

Your right first just getting the controls to works properly has the priority.

Are you looking for a Mirage? And which country are you located?
I ordered a second one but its broken. hope to fix it.

1 Like

I made a queue.
Data seems to tick slowly and follows the timer.
But do not understand at all the sysex data.
The Mirage is now replying with 0D messages…which are parameter status messages if you change a parameter on the frontpanel.

It works… not… or actually it doesn’t.

https://app.electra.one/preset/Im1Y6mHqy46btRSx1YNM

What I tried is instead of sending the sysex message add it to a queue.
Then send the message according the clock (timer).

Now we always lose 1 value.
Spinning back we cannot get to 0, but keep one.

mirageDeviceId = 9
previousParameterNumber = nil
lastValue = {}
maxBytes = 5
upArrow= 14
downArrow = 15
parameterSelect = 12
valueSelect = 13
endCommand = 127
mainWorkerCounterCalls = 0
queuWorkerCounterCalls = 0
queue = {}
queue.first = 0
queue.last = -1
queue.data = {}
timer.enable ()
timer.setBpm (10 * 16)

function insert(q, val)
   q.last = q.last + 1
   q.data[q.last] = val
end

function remove(q)
   if (q.first > q.last) then
      rval = -1
   else
      print("remove: q.data[q.first]= ", q.data[q.first], " q.first= ", q.first)
      local rval = q.data[q.first]
      midi.sendSysex(PORT_1, rval) 
      print("lets dump rval")
      for key, value in pairs(rval) do
       print(key, value)
      end
      print("Just send so removing: rval= ", rval)
      q.data[q.first] = nil
      q.first = q.first + 1
      print("remove: q.first= ", q.first)
   end
   return rval
end

function mainWorker(ValueObject, Value)

  -- if you are happy with the way it is working, we do not need these checks
  mainWorkerCounterCalls = mainWorkerCounterCalls + 1
  print("mainWorkerCounterCalls:" .. mainWorkerCounterCalls)

  local message = ValueObject:getMessage()
  local currentParameterNumber = message:getParameterNumber ()
  local curValue = message:getValue()

  if ( currentParameterNumber ~= previousParameterNumber ) then
    
    previousParameterNumber = currentParameterNumber
    print("we have parameterNumber: ", currentParameterNumber)

    -- common way to separate the tens digit and the ones digit
    local byte1st = math.floor(currentParameterNumber / 10)
    local byte2nd = currentParameterNumber % 10

    sysexMsg = { 15, 1, 1, parameterSelect, byte1st, byte2nd, valueSelect, endCommand }
    midi.sendSysex(PORT_1, sysexMsg)
   
  end

  if ( currentParameterNumber == previousParameterNumber ) then

    -- declare a local variable and set it to something
    local arrowDirection = upArrow

    local arrowValue = curValue - ( lastValue[currentParameterNumber] or 0 )
    print (string.format("arrowValue = %d", arrowValue))

    -- the default is upArrow (positive) so only do things for negative or zero cases
    if ( arrowValue == 0 ) then
      print("nothing to do")
      return
    elseif ( arrowValue < 0 ) then
      arrowValue = 0 - arrowValue
      arrowDirection = downArrow
    end

    local exp1 = 0 
    local exp2 = arrowValue

    print (string.format("Total arrow bytes to send = %d", arrowValue))
    for var = exp1,exp2,maxBytes do
 
      if ( arrowValue > 5 ) then
 
        print("sending 5 arrowBytes,")
        arrowValue = arrowValue - 5
        sysexMsg = { 15, 1, 1, arrowDirection, arrowDirection, arrowDirection, arrowDirection, arrowDirection, endCommand }
        midi.sendSysex(PORT_1, sysexMsg)

      elseif ( arrowValue < 5 ) and ( arrowValue > 0) then

        print (arrowValue, "Sending remaining bytes")
        sysexMsg = {15, 1, 1}

        for i = 1,arrowValue do
          sysexMsg[3+i] = arrowDirection
        end
        -- note that the variable i does not exist after the for loop so we cannot use it here
        sysexMsg[4+arrowValue] = endCommand

        midi.sendSysex(PORT_1, sysexMsg)

        -- finished sending bytes, zero out the value so we exit the for loop
        arrowValue = 0

      end

    end
    lastValue[currentParameterNumber] = curValue
  end

end

--here we send nothing but store the messages in a queue
function queWorker(ValueObject, Value)

  -- if you are happy with the way it is working, we do not need these checks
  queuWorkerCounterCalls = queuWorkerCounterCalls + 1
  print("queWorkerCounterCalls:" .. mainWorkerCounterCalls)

  local message = ValueObject:getMessage()
  local currentParameterNumber = message:getParameterNumber ()
  local curValue = message:getValue()

  if ( currentParameterNumber ~= previousParameterNumber ) then
    
    previousParameterNumber = currentParameterNumber
    print("we have parameterNumber: ", currentParameterNumber)

    -- common way to separate the tens digit and the ones digit
    local byte1st = math.floor(currentParameterNumber / 10)
    local byte2nd = currentParameterNumber % 10

    sysexMsg = { 15, 1, 1, parameterSelect, byte1st, byte2nd, valueSelect, endCommand }
    --midi.sendSysex(PORT_1, sysexMsg)
    insert(queue, sysexMsg)
  end

  if ( currentParameterNumber == previousParameterNumber ) then

    -- declare a local variable and set it to something
    local arrowDirection = upArrow

    local arrowValue = curValue - ( lastValue[currentParameterNumber] or 0 )
    print (string.format("arrowValue = %d", arrowValue))

    -- the default is upArrow (positive) so only do things for negative or zero cases
    if ( arrowValue == 0 ) then
      print("nothing to do")
      return
    elseif ( arrowValue < 0 ) then
      arrowValue = 0 - arrowValue
      arrowDirection = downArrow
    end

    local exp1 = 0 
    local exp2 = arrowValue

    print (string.format("Total arrow bytes to send = %d", arrowValue))
    for var = exp1,exp2,maxBytes do
 
      if ( arrowValue > 5 ) then
 
        print("sending 5 arrowBytes,")
        arrowValue = arrowValue - 5
        sysexMsg = { 15, 1, 1, arrowDirection, arrowDirection, arrowDirection, arrowDirection, arrowDirection, endCommand }
        insert(queue, sysexMsg)
        --midi.sendSysex(PORT_1, sysexMsg)

      elseif ( arrowValue < 5 ) and ( arrowValue > 0) then

        print (arrowValue, "Sending remaining bytes")
        sysexMsg = {15, 1, 1}

        for i = 1,arrowValue do
          sysexMsg[3+i] = arrowDirection
        end
        -- note that the variable i does not exist after the for loop so we cannot use it here
        sysexMsg[4+arrowValue] = endCommand
        insert(queue, sysexMsg)
        --midi.sendSysex(PORT_1, sysexMsg)

        -- finished sending bytes, zero out the value so we exit the for loop
        arrowValue = 0

      end

    end
    lastValue[currentParameterNumber] = curValue
  end

end


--the messages in the queue wil be send here and deleted from the queue
function timer.onTick ()
if (queue.first < queue.last) then
   remove(queue)
   end
end

if you are really intent on testing things now, here is some untested code for mainWorker() and a timer().

SUGGESTION – do a CTRL-A, CTRL-C of the entire LUA code you have now and save it off to a text file before putting these pieces in. I was going to integrate this and test before posting…
Note - I have the timer delay set to a variable at the top of this file. I was going to add a control at the end of the preset to be able to adjust this value so that you did not have to edit and reload the project every time to find the best value to use.

Pay attention to the globals I defined as well since they may be different than what you have.
Again - this is mostly tested, but I added some debugs and changed a few things around since the last time I tested it.

tmrPeriod = 50

timer.setPeriod(tmrPeriod)
timer.disable()


mirageDeviceId = 9

-- set to an impossible value to force mainWorker() to process first touched control
previousParameterNumber = -1

-- holds the value and direction (up or down) for each encoder
-- this is what the timer function uses to send to the Mirage
lastValue = {}
lastDirection = {}

-- holds the current value of each encoder as a user changes different controls
-- when a user comes back to a control, we know the current value to use in the next calculations
curEncValue = {}

oldValue = 0
maxBytes = 5
upArrow= 14
downArrow = 15

-- temporary texts to make reading debug statements easier
dbgTxt[14] = " Up"
dbgTxt[15] = " Down"

parameterSelect = 12
valueSelect = 13
endCommand = 127

-- which control is currently being changed
currentControl = 0



function timer.onTick()
  if (currentControl ~= 0) then
    local av = math.abs(lastValue[currentControl])
    local ad = lastDirection[currentControl]
    print("currentControl = " .. currentControl)
    if ( av > 5 ) then
 
      print(" *** Sending 5 arrowBytes, direction is" .. dbgTxt[ad])
      lastValue[currentControl] = av - 5
      sysexMsg = { 15, 1, 1, ad, ad, ad, ad, ad, endCommand }
      midi.sendSysex(PORT_1, sysexMsg)

    elseif ( av < 5 ) and ( av > 0) then

      print (" *** Sending remaining bytes " .. av .. dbgTxt[ad])
      sysexMsg = {15, 1, 1}

      for i = 1,av do
        sysexMsg[3+i] = ad
      end
      -- note that the variable i does not exist after the for loop so we cannot use it here
      sysexMsg[4+av] = endCommand

      midi.sendSysex(PORT_1, sysexMsg)

      -- finished sending bytes, zero out the value so we do not send extra bytes if called again
      lastValue[currentControl] = 0
      currentControl = 0

    else
      -- catch an edge case
      -- somehow we get called for the current control, but with no change in value
      print ("Sending nothing - Edge case")
      -- lastValue[currentControl] = 0
      currentControl = 0
    end
  end
end


function mainWorker(ValueObject, Value)

  local message = ValueObject:getMessage()
  local currentParameterNumber = message:getParameterNumber ()
  local curValue = message:getValue()

  -- worker function called, save off the parameter number
  currentControl = currentParameterNumber

  if ( currentParameterNumber ~= previousParameterNumber ) then
    
    -- do not have the timer running in case it catches us with partially filled in data
    timer.disable()

    -- we store the current value as the previous for the next time
    previousParameterNumber = currentParameterNumber

    -- common way to separate the tens digit and the ones digit
    local byte1st = math.floor(currentParameterNumber / 10)
    local byte2nd = currentParameterNumber % 10

    sysexMsg = { 15, 1, 1, parameterSelect, byte1st, byte2nd, valueSelect, endCommand }
    midi.sendSysex(PORT_1, sysexMsg)

    -- NOTE - this assumes your controls have a default value of 0
    -- if they start with a default of 64 or something else, different code needs to go here 

    -- get the last value for this control
    oldValue = (curEncValue[previousParameterNumber] or 0)

    timer.enable()

  end

  -- we need to keep track of the last value for each encoder
  -- there is probably a better way to pull this from the control
  -- so we do not have to keep storing it
  curEncValue[currentParameterNumber] = curValue
  
  if ( currentParameterNumber == previousParameterNumber ) then

    -- figure out how much the encoder has changed from last time to this time
    -- note that the value can be positive or negative at this point
    local delta = curValue - oldValue

    -- save off the current value
    oldValue = curValue
    
    -- declare a local variable and set it to something
    local arrowDirection = upArrow

    -- the default is upArrow (positive) so only do things for negative or zero cases
    if ( delta == 0 ) then
      print("nothing to do")
      return
    elseif ( delta < 0 ) then
      arrowDirection = downArrow
    end

    -- for this parameter, save off the last value sent and the direction
    lastValue[currentParameterNumber] = (lastValue[currentParameterNumber] or 0) + delta
    lastDirection[currentParameterNumber] = arrowDirection
  end

end
2 Likes