Old 07-18-2018, 08:30 AM   #1
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,054
Default Adapting my "nudge notes" script to Julian Sader's method

Hey all!

I'm about to adapt all my MIDI scripts to Julian's method (Get/SetAllEvts), since it's more efficient with huge amounts of MIDI data.

However, I'm having some issues, when there are other events like channel pressure, program changes, text events, notation events...
When nuding the notes, they just disappear. Apparently they are not captured, when getting the channel message, since they only contain the midi notes and control changes. Is there an elegant way to include these values as well and re-write them to the take?

Code:
function nudgenotes(newPosition)
	for i = 0, reaper.CountSelectedMediaItems(0)-1 do -- loop through all selected items
		item = reaper.GetSelectedMediaItem(0, i) -- get current selected item
		for t = 0, reaper.CountTakes(item)-1 do -- loop through all takes within each selected item
			take = reaper.GetTake(item, t) -- get current take
			if reaper.TakeIsMIDI(take) then -- make sure, that take is MIDI
				if reaper.MIDI_EnumSelNotes(take, -1) ~= -1 then -- check, if there are selected notes
					notesSelected = true -- set notesSelected to true
				end
				gotAllOK, MIDIstring = reaper.MIDI_GetAllEvts(take, "") -- write MIDI events to MIDIstring, get all events okay
					if not gotAllOK then reaper.ShowMessageBox("Error while loading MIDI", "Error", 0) return(false) end -- if getting the MIDI data failed
				MIDIlen = MIDIstring:len() -- get string length
				tableEvents = {} -- initialize table, MIDI events will temporarily be stored in this table until they are concatenated into a string again
				stringPos = 1 -- position in MIDIstring while parsing through events 
				while stringPos < MIDIlen-12 do -- parse through all events in the MIDI string, one-by-one, excluding the final 12 bytes, which provides REAPER's All-notes-off end-of-take message
					offset, flags, msg, stringPos = string.unpack("i4Bs4", MIDIstring, stringPos) -- unpack MIDI-string on stringPos
					if notesSelected == true then -- if there is a note selection
						if msg:len() == 3 then -- if msg consists of 3 bytes (= channel message)
							msg_b1_nib1 = msg:byte(1)>>4 -- save 1st nibble of status byte (contains info about the data type) to msg_b1_nib1, >>4 shifts the channel nibble into oblivion
							if msg_b1_nib1 == 9  and flags == 1 then  -- if status byte is a note on event
								table.insert(tableEvents, string.pack("i4Bs4", offset+newPosition, flags, msg)) -- move the note on event by newPosition
								table.insert(tableEvents, string.pack("i4Bs4", -newPosition, flags, "")) -- put an empty event after the note on event, to maintain the distance to the next event
							elseif msg_b1_nib1 == 8 and flags == 1 then  -- if status byte is a note off event
								table.insert(tableEvents, string.pack("i4Bs4", offset+newPosition, flags, msg)) -- move the note off event by newPosition
								table.insert(tableEvents, string.pack("i4Bs4", -newPosition, flags, "")) -- put an empty event after the note on event, to maintain the distance to the next event
							else
								table.insert(tableEvents, string.pack("i4Bs4", offset, flags, msg))
							end
						end
					else 
						if msg:len() == 3 then -- if msg consists of 3 bytes (= channel message)
							msg_b1_nib1 = msg:byte(1)>>4 -- save 1st nibble of status byte (contains info about the data type) to msg_b1_nib1, >>4 shifts the channel nibble into oblivion
							if msg_b1_nib1 == 9  then  -- if status byte is a note on event
								table.insert(tableEvents, string.pack("i4Bs4", offset+newPosition, flags, msg)) -- move the note on event by newPosition
								table.insert(tableEvents, string.pack("i4Bs4", -newPosition, flags, "")) -- put an empty event after the note on event, to maintain the distance to the next event
							elseif msg_b1_nib1 == 8  then  -- if status byte is a note off event
								table.insert(tableEvents, string.pack("i4Bs4", offset+newPosition, flags, msg)) -- move the note off event by newPosition
								table.insert(tableEvents, string.pack("i4Bs4", -newPosition, flags, "")) -- put an empty event after the note on event, to maintain the distance to the next event
							else
								table.insert(tableEvents, string.pack("i4Bs4", offset, flags, msg))
							end
						end
					end
				end
			end
		end
	end
	reaper.MIDI_SetAllEvts(take, table.concat(tableEvents) .. MIDIstring:sub(-12))
	reaper.MIDI_Sort(take)
	reaper.UpdateArrange()
end
__________________
My Reascripts forum thread | My Reascripts on GitHub
If you like or use my scripts, please support the Ukraine: Ukraine Crisis Relief Fund | DirectRelief | Save The Children | Razom
_Stevie_ is offline   Reply With Quote
Old 07-18-2018, 09:20 AM   #2
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: Calgary, AB, Canada
Posts: 6,551
Default

Gah. So much nesting. Can't read.

You should really, seriously, desperately, consider breaking that into multiple functions.
__________________
I'm no longer using Reaper or working on scripts for it. Sorry. :(
Default 5.0 Nitpicky Edition / GUI library for Lua scripts / Theory Helper / Radial Menu / Donate
Lokasenna is offline   Reply With Quote
Old 07-18-2018, 09:23 AM   #3
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,054
Default

Yeah, I hear ya!
Gladly taking any advice. I'm not coding for very long

I thought, nesting would be more efficient than calling functions all the time.
__________________
My Reascripts forum thread | My Reascripts on GitHub
If you like or use my scripts, please support the Ukraine: Ukraine Crisis Relief Fund | DirectRelief | Save The Children | Razom
_Stevie_ is offline   Reply With Quote
Old 07-18-2018, 09:35 AM   #4
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: Calgary, AB, Canada
Posts: 6,551
Default

By a teeny, teeny, TEENY tiny amount, yes. But never enough to be noticeable.

Breaking them up into smaller functions makes the code easier to read, easier to fix, and easier to change. As a general rule of thumb (there are, of course, no Rules), consider breaking the code off into a separate function every time you're running the same code more than a couple of times, or if the code inside the loop starts getting to be (arbitrary number) more than a dozen or so lines. Our brains start having trouble remembering where they are as the function gets bigger, tracking all of the variables going on, etc.

In your case, each of the for loops could be moved out to a function, as well as the while loop in the middle. The section starting from MIDI_EnumSelNotes is a great candidate as well.

You'll probably have to start using local variables if you go this route, which is also a good thing to do all the time anyway, so breaking the functions out will require paying attention to which parameters need to be passed, where each value actually needs to be grabbed, etc.

PM me if you want, I'm happy to help.
__________________
I'm no longer using Reaper or working on scripts for it. Sorry. :(
Default 5.0 Nitpicky Edition / GUI library for Lua scripts / Theory Helper / Radial Menu / Donate

Last edited by Lokasenna; 07-18-2018 at 09:40 AM.
Lokasenna is offline   Reply With Quote
Old 07-18-2018, 09:55 AM   #5
juliansader
Human being with feelings
 
Join Date: Jul 2009
Posts: 3,714
Default

You are skipping (and deleting) all events that are not three bytes longs, such as channel pressure and program select, and text or sysex.

Try this:
Code:
offset, flags, msg, stringPos = string.unpack("i4Bs4", MIDIstring, stringPos) -- unpack MIDI-string on stringPos
if #msg == 3 
and ((msg:byte(1)>>4) == 9 or (msg:byte(1)>>4) == 8) -- Is note-on or note-off?
and (flags&1 == 1 or not notesSelected) -- selected notes always move, unselected only move if no notes are selected
then
    table.insert(tableEvents, string.pack("i4Bs4", offset+newPosition, flags, msg)) -- move the note on event by newPosition
    table.insert(tableEvents, string.pack("i4Bs4", -newPosition, 0, "")) -- put an empty event after the note on event, to maintain the distance
else
    table.insert(tableEvents, string.pack("i4Bs4", offset, flags, msg))
end

Last edited by juliansader; 07-18-2018 at 10:10 AM.
juliansader is offline   Reply With Quote
Old 07-18-2018, 11:45 AM   #6
snooks
Banned
 
Join Date: Sep 2015
Posts: 1,650
Default

I think something like ...
Code:
local event_tick_offset = 0
...
  table.insert(table_events, string.pack("i4Bs4", offset + new_position - event_tick_offset, flags, msg)) -- move the note on event by newPosition
  event_tick_offset = new_position
else
  table.insert(table_events, string.pack("i4Bs4", offset - event_tick_offset, flags, msg))
  event_tick_offset = 0
...
... is nicer than inserting the empty event. I have also grown to hate camelCase for variable names which should be snake_case unless forced to do otherwise.
snooks is offline   Reply With Quote
Old 07-18-2018, 11:58 AM   #7
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,054
Default

Ah you guys rock! Thank you so much, will try that!

@Lokasenna: will pm you

@Julian, @snooks: awesome, will give it a go!

@snooks: yeah the camelcase is something I learnt during my studies.
Call it bad habit. I think I'm the only one doing it. Good idea to get rid of it :P It's really hard to read.
__________________
My Reascripts forum thread | My Reascripts on GitHub
If you like or use my scripts, please support the Ukraine: Ukraine Crisis Relief Fund | DirectRelief | Save The Children | Razom
_Stevie_ is offline   Reply With Quote
Old 07-18-2018, 12:24 PM   #8
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: Calgary, AB, Canada
Posts: 6,551
Default

I'm pretty bad for mixing cases, mostly because I can never decide which looks best. Mostly snake case these days, but there's an argument to be made for using different cases for different things - snake for variables, camel for function, etc.
__________________
I'm no longer using Reaper or working on scripts for it. Sorry. :(
Default 5.0 Nitpicky Edition / GUI library for Lua scripts / Theory Helper / Radial Menu / Donate
Lokasenna is offline   Reply With Quote
Old 07-18-2018, 01:59 PM   #9
snooks
Banned
 
Join Date: Sep 2015
Posts: 1,650
Default

Yeah, I've kind of settled on camelCase for methods/functions, PascalCase for class names and snake_case for variables. With two word minimum names (unless one will do) it's obvious what is what. Other people do it differently, but I just think this looks purdiest.
snooks is offline   Reply With Quote
Old 07-18-2018, 02:44 PM   #10
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,054
Default

@Julian

Now, that you fixed the code, I feel really dumb, haha. You really simplified it to the max, awesome!
__________________
My Reascripts forum thread | My Reascripts on GitHub
If you like or use my scripts, please support the Ukraine: Ukraine Crisis Relief Fund | DirectRelief | Save The Children | Razom
_Stevie_ is offline   Reply With Quote
Old 07-18-2018, 05:26 PM   #11
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,054
Default

this gives me question marks:

Code:
flags&1 == 1
what exactly does this mean?

And... fun fact: although, I forgot to check for muted notes (flags == 2) and muted selected events (flags == 3), they are happily moved together with the other notes (expected behavior).
__________________
My Reascripts forum thread | My Reascripts on GitHub
If you like or use my scripts, please support the Ukraine: Ukraine Crisis Relief Fund | DirectRelief | Save The Children | Razom
_Stevie_ is offline   Reply With Quote
Old 07-18-2018, 06:43 PM   #12
FnA
Human being with feelings
 
FnA's Avatar
 
Join Date: Jun 2012
Posts: 2,173
Default

Lol on the comment.

I don't know the tech lingo but here goes. It uses the bits as a sort of toggle indicator. 1 bit is selected, 2 bit is muted. As far as I know, 4,8,16, etc, are not used, in this case ("flags").

...00000011(flags = 3)
&
...00000001(1)
----------------
...00000001
FnA is offline   Reply With Quote
Old 07-18-2018, 07:58 PM   #13
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: Calgary, AB, Canada
Posts: 6,551
Default

It's called a bitwise comparison - you're performing logic at the level of individual bits. 1 is true, 0 is false.

Operators:
Code:
&: bitwise AND
|: bitwise OR
~: bitwise exclusive OR
>>: right shift
<<: left shift
~: unary bitwise NOT
Examples from Wikipedia's article on the topic:
Code:
    0101 (decimal 5)
AND 0011 (decimal 3)
  = 0001 (decimal 1)

    0011 (decimal 3)
AND 0010 (decimal 2)
  = 0010 (decimal 2)

   0101 (decimal 5)
OR 0011 (decimal 3)
 = 0111 (decimal 7)

   0010 (decimal 2)
OR 1000 (decimal 8)
 = 1010 (decimal 10)

    0101 (decimal 5)
XOR 0011 (decimal 3)
  = 0110 (decimal 6)

    0010 (decimal 2)
XOR 1010 (decimal 10)
  = 1000 (decimal 8)
The two shift operators basically add a 0 to shift the entire binary sequence in one direction or the other. Why? No idea.

As the use of the term "flags" suggests, binary numbers are really easy way to pass around a whole bunch of boolean parameters in one value. gfx.mouse_cap gives all of the various mouse buttons and keyboard modifiers the same way.
__________________
I'm no longer using Reaper or working on scripts for it. Sorry. :(
Default 5.0 Nitpicky Edition / GUI library for Lua scripts / Theory Helper / Radial Menu / Donate
Lokasenna is offline   Reply With Quote
Old 07-19-2018, 03:53 AM   #14
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,054
Default

Guys, this is incredible! So that way (on bit level), it's possible to check for 2 values at the same time? I have to digest that!
Of course this only works, since the nature of flags is binary? Or can any value
be retrieved as a binary value?
__________________
My Reascripts forum thread | My Reascripts on GitHub
If you like or use my scripts, please support the Ukraine: Ukraine Crisis Relief Fund | DirectRelief | Save The Children | Razom
_Stevie_ is offline   Reply With Quote
Old 07-19-2018, 04:48 AM   #15
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: Calgary, AB, Canada
Posts: 6,551
Default

Any number can be represented as binary, but bitwise comparison is only (well, mostly. I'm sure there's another reason) going to help in situations where the individual bits represent specific information.

You can also (I think, it's been a while) use the bitwise operators on normal integers; they don't have to be formatted as binary like flags is in the code here.
__________________
I'm no longer using Reaper or working on scripts for it. Sorry. :(
Default 5.0 Nitpicky Edition / GUI library for Lua scripts / Theory Helper / Radial Menu / Donate
Lokasenna is offline   Reply With Quote
Old 07-19-2018, 05:29 AM   #16
snooks
Banned
 
Join Date: Sep 2015
Posts: 1,650
Default

Yup, you can use bitwise operators on any whole number (called integers). Everything in computers is stored in binary form, but bitwise operators don't make much sense for anything other than integers so we're generally not allowed to use them with anything else. But the important thing to know is that when we write any number it becomes binary.
snooks is offline   Reply With Quote
Reply

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off

Forum Jump


All times are GMT -7. The time now is 02:52 AM.


Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2024, vBulletin Solutions Inc.