Old 08-12-2018, 12:46 PM   #41
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

Well, maybe I should start with the things I don't understand in Julian's script (which is a lot):

Here, I see a table initialization. But I don't understand the for loops.
There is no data yet, why are the for loops executed?
Is it only for initialization purposes?

Code:
-- While parsing the MIDI string, the indices of the last note-ons for each channel/pitch/flag must be temporarily stored until a matching note-off is encountered. 
local runningNotes = {}
for channel = 0, 15 do
    runningNotes[channel] = {}
    for pitch = 0, 127 do
        runningNotes[channel][pitch] = {} -- Each pitch will have flags as keys.
    end
end

Why is the script already checking for runningNotes[channel][pitch][flags] ?
There's no data written, yet, or am I wrong?

And these 2 lines render me absolutely clueless:
tNotes[#tNotes+1] = {noteOnIndex = e}
runningNotes[channel][pitch][flags] = #tNotes

what is tNotes[]? and what is [#tNotes+1]? I know that "#" gets the length of a string.
But what is {noteOnIndex = e} ?

Sorry, but I feel like a dumbass, haha.


Code:
if eventType == 9 and msg:byte(3) ~= 0 then -- Note-ons
            local channel = msg:byte(1)&0x0F
            local pitch   = msg:byte(2)
            if runningNotes[channel][pitch][flags] then
                reaper.ShowMessageBox("The script encountered overlapping notes.\n\nSuch notes are not valid MIDI, and can not be parsed.", "ERROR", 0)
                return false
            else
                tNotes[#tNotes+1] = {noteOnIndex = e}
                runningNotes[channel][pitch][flags] = #tNotes
            end

Same here, I can make no sense out of:
local lastNoteOnIndex = runningNotes[channel][pitch][flags]
if lastNoteOnIndex then
tNotes[lastNoteOnIndex].noteOffIndex = e
runningNotes[channel][pitch][flags] = nil

Code:
       
        elseif eventType == 8 or (eventType == 9 and msg:byte(3) == 0) then
            local channel = msg:byte(1)&0x0F
            local pitch   = msg:byte(2)
            local lastNoteOnIndex = runningNotes[channel][pitch][flags]
            if lastNoteOnIndex then
                tNotes[lastNoteOnIndex].noteOffIndex = e
                runningNotes[channel][pitch][flags] = nil
end

I think I have way too many gaps in the table department!
__________________
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 08-12-2018, 07:19 PM   #42
FnA
Human being with feelings
 
FnA's Avatar
 
Join Date: Jun 2012
Posts: 2,173
Default

Here goes. Noob leading the noob...
Quote:
Originally Posted by _Stevie_ View Post
Code:
        if flags == 0 or flags == 1 then flags = 1 	-- unselected or selected? = select event
        else flags = 3 								-- muted or muted selected? = select muted event
        end
You can eliminate a few of those ifs.
If you know you want to select an event, you can use:
Code:
flags = flags|1
That will ensure that bit is 1 and not 0
0|1=1
1|1=1
2|1=3
3|1=3
Same with muted/&2. 0|2=2, etc...
If you know you want to unselect it, you can use:
Code:
flags = flags &~ 1
That will ensure that bit is 0 and not 1
0&~1=0
1&~1=0
2&~1=2
3&~1=2
------------------------------------------

Quote:
Originally Posted by _Stevie_ View Post
Well, maybe I should start with the things I don't understand in Julian's script (which is a lot):

Here, I see a table initialization. But I don't understand the for loops.
There is no data yet, why are the for loops executed?
Is it only for initialization purposes?

Code:
-- While parsing the MIDI string, the indices of the last note-ons for each channel/pitch/flag must be temporarily stored until a matching note-off is encountered. 
local runningNotes = {}
for channel = 0, 15 do
    runningNotes[channel] = {}
    for pitch = 0, 127 do
        runningNotes[channel][pitch] = {} -- Each pitch will have flags as keys.
    end
end

Why is the script already checking for runningNotes[channel][pitch][flags] ?
There's no data written, yet, or am I wrong?
I don't know if "initialization" is the technical word, but yes, they are just for building the runningNotes table and it's sub tables. channel and pitch in that case are local variables that only have use in those loops. Could have used i and j, for example. The tables get numeric keys from those variables. (May be a little complicated to use 0 as one of the keys, in some situations. See note on # below)

Quote:
Originally Posted by _Stevie_ View Post
And these 2 lines render me absolutely clueless:
tNotes[#tNotes+1] = {noteOnIndex = e}
runningNotes[channel][pitch][flags] = #tNotes

what is tNotes[]? and what is [#tNotes+1]? I know that "#" gets the length of a string.
But what is {noteOnIndex = e} ?
Hm. Sorry, maybe this example doesn't spell out everything you will have to do in your particular script. Actually, it probably is more complex, if viewed in it's entirety. Some of Julian's code is not in his extract. We don't know where e (a variable) or tNotes (a table) comes from or how it's used. I don't know exactly what script this is from, but in one of his scripts e is an index in a table, so I'm guessing what he probably did there is make a table constituted of the unpacked variables: offset, flags, msg, (which came from the long string retrieved by GetAllEvents), as elements (probably in a table: {offset, flags, msg} ). Then tNotes has the index where a particular event (table?) can be found to be manipulated. It probably increases by one for each rep of the while loop where the variables are unpacked from MIDIstring or whatever its named.

# can be used to give the length of a table. Actually it doesn't. It's pretty complicated. I don't know all the specifics. I think it doesn't include numbers less than 1, or if they are out of sequence, or have some missing. I think it works ok for the part that is sequential positive integers starting at 1. You should experiment with some simple examples in the IDE. Maybe google.

So I think tNotes[#tNotes+1] = {noteOnIndex = e} means
make a new element in tNotes after the last positive key in the sequence, compose of a table containing a key called noteOnIndex that has a value of e.

runningNotes is a table that has 16 tables (channel) in it that each have 128 tables (pitch) in them. He says each pitch will have flags as keys so I guess there is 4 elements in each pitch sub table. in his extract that flag key gets #tnotes assigned as value. Not sure why.
[edit. maybe the flags only get added to pitch table when needed and nilled out when not needed. or something. I also should probably mention that keys don't have to be numbers. they can be strings or some other data like a mediatrack or item pointer or...]

Whew, this post is taking longer than the script would have. You don't have to make the table EXACTLY as juliansader has. You might not need to store the {offset, flags, msg} to be manipulated later, maybe just the note ons that are currently on, then nil them out upon finding a note off of the same pitch/channel/mute class, or throw error if another note on is found before it gets erased.

I think you probably should get the hang of tables somehow. It's the most technical part of this script. If we spell it all out, there wont be much left for you to do. I mean, that's ok, if that's what you want to do...

Last edited by FnA; 08-12-2018 at 09:58 PM.
FnA is offline   Reply With Quote
Old 08-13-2018, 12:25 AM   #43
juliansader
Human being with feelings
 
Join Date: Jul 2009
Posts: 3,714
Default

Quote:
Originally Posted by _Stevie_ View Post
And these 2 lines render me absolutely clueless:
tNotes[#tNotes+1] = {noteOnIndex = e}
runningNotes[channel][pitch][flags] = #tNotes

what is tNotes[]? and what is [#tNotes+1]? I know that "#" gets the length of a string.
But what is {noteOnIndex = e} ?
As FnA noted, what you do in this step depends on the aim of you script. If you simply want to know whether the matching note-on was before or after the edit cursor, you only need to store the tick position of the note-on in runningNotes[channel][pitch][flags]. When you reach a note-off, it can check its matching note-on's position.

If you want to do more complicated manipulations, you can store whatever relevant note information (including note-on, note-off and notation information) in a separate tNotes table, and runningNotes[channel][pitch][flags] will then point to the index inside tNotes where this note information is being stored (and where the current note-off's info should also be stored).

Code:
tNotes[#tNotes+1] = {noteOnIndex = e}
Store whatever is relevant for you script in a new entry in tNotes. (For the script from which a copied the example, I only needed to store the index of the note-on. Other scripts might store note-on position, velocity, etc:
tNotes[#tNotes+1] = {vel = msg:byte(3), pos = runningTickPosition, etc...}

Code:
runningNotes[channel][pitch][flags] = #tNotes
Save the index of the note information that was created in the previous line. (Since a new entry was created in the previous line, #tNotes was automatically incremented, and now points to the previous line's entry.)


Quote:
Originally Posted by FnA View Post
edit. maybe the flags only get added to pitch table when needed and nilled out when not needed. or something. I also should probably mention that keys don't have to be numbers. they can be strings or some other data like a mediatrack or item pointer or...
The flags help to better distinguish overlapping notes, so that muted note-offs are correctly matched with muted note-ons, for example.
juliansader is offline   Reply With Quote
Old 08-13-2018, 03:50 AM   #44
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

Holy macaroni! Thanks guys! I experimented with an online LUA Compiler to better understand tables. This and your explanations got me wayyyyy closer to understand all this! I almost have the "before cursor" script done. There's still some edge case, that needs to be fixed, though. But all in all, I'm on the right track!
This feels really good after all these weeks....

FnA, with all this knowledge, you're definitely not a noob!
__________________
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 08-13-2018, 07:40 AM   #45
FnA
Human being with feelings
 
FnA's Avatar
 
Join Date: Jun 2012
Posts: 2,173
Default

I can't really get away with that excuse now, I suppose. But I'm not very far ahead of you compared to a lot of guys.

The IDE can show you a certain number of table entries when a script runs, like how it shows variables. Might be helpful.
FnA is offline   Reply With Quote
Old 08-13-2018, 10:32 AM   #46
Thonex
Human being with feelings
 
Join Date: May 2018
Location: Los Angeles
Posts: 1,721
Default

I haven't stared even looking at MIDI scripting in Reaper yet... but I'm bookmarking this thread for when I do

Great thread! Great community!
__________________
Cheers... Andrew K
Reaper v6.80+dev0621 - June 21 2023 • Catalina • Mac Mini 2020 6 core i7 • 64GB RAM • OS: Catalina • 4K monitor • RME RayDAT card with Sync Card and extended Light Pipe.
Thonex is offline   Reply With Quote
Old 08-13-2018, 11:51 AM   #47
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

Hey Andrew!

You can do a whole lot more than in other DAWs!
Actually, MIDI handling in Reaper/LUA is super easy, but the MIDI Get/Set Note functions are too slow for huge amounts of MIDI data.
That's why Cockos introduced the Get/SetAllEvts functions, which are ultra fast, but the downside is: you have to handle the MIDI raw chunk.
Julian Sader was the one who created the first scripts for that method.

If you haven't yet, check out his amazing MIDI scripts (in his signature).
They surpass a lot of what is available in other DAWs.
__________________
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 08-13-2018, 12:03 PM   #48
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

And now *drum roll*: the script seems to be working!

It's still unclean and needs refactoring! But I'll paste it anyway, maybe you guys can check it out, if everything works correctly.

I haven't included a fallback for overlapping notes, yet.

Code:
local runningNotes = {}
for channel = 0, 15 do
    runningNotes[channel] = {}
    for pitch = 0, 127 do
        runningNotes[channel][pitch] = {} -- Each pitch will have flags as keys.
    end
end

cursorPosition = reaper.GetCursorPosition()  -- get edit cursor position 
local sumOffset = 0 -- initialize sumOffset (adds all offsets to get the position of every event in ticks)

if reaper.CountSelectedMediaItems(0) > 1 then
	reaper.ShowMessageBox("Please select only one item", "Error" , 0) -- popup error message, if more than 1 item is selected
	return
elseif reaper.CountSelectedMediaItems(0) == 0 then 
	reaper.ShowMessageBox("Please select one item", "Error" , 0) -- popup error message, if no item is selected
	return
elseif reaper.CountSelectedMediaItems(0) == 1 then
	for i = 0, reaper.CountSelectedMediaItems(0)-1 do -- loop through all selected items
		item = reaper.GetSelectedMediaItem(0, i) -- get current selected item
		itemStart = reaper.GetMediaItemInfo_Value(item, "D_POSITION") -- get start of 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
			cursorPositionPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, cursorPosition) -- convert cursorPosition to PPQ
			itemStartPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, itemStart) -- convert itemStart to PPQ
			if reaper.TakeIsMIDI(take) then -- make sure, that take is MIDI
				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
					sumOffset = sumOffset+offset -- add all event offsets to get next start position of event on each iteration
					eventStart = itemStartPPQ+sumOffset -- calculate event start position based on item start position
					
					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

						local channel = msg:byte(1)&0x0F
						local pitch = msg:byte(2)
					
						-- note on before cursor position? = select
						if msg_b1_nib1 == 9 and eventStart < cursorPositionPPQ then 
							flags = flags|1
							runningNotes[channel][pitch] = true
		
						-- note on after cursor position? = unselect 
						elseif msg_b1_nib1 == 9 and eventStart >= cursorPositionPPQ then
							runningNotes[channel][pitch] = false
							flags = flags &~ 1

						-- note off anywhere and note-on before cursor? = select
						elseif msg_b1_nib1 == 8 and runningNotes[channel][pitch] then
							flags = flags|1
							runningNotes[channel][pitch] = false
						
						-- note off and note-on after cursor? = unselect
						elseif msg_b1_nib1 == 8 and not runningNotes[channel][pitch] then
						flags = flags &~ 1
						end
					end
					table.insert(tableEvents, string.pack("i4Bs4", offset, flags, msg)) -- re-pack MIDI string and write to table
				end
			end
		end
	end
end

reaper.MIDI_SetAllEvts(take, table.concat(tableEvents) .. MIDIstring:sub(-12))
reaper.MIDI_Sort(take)
reaper.UpdateArrange()

reaper.Undo_OnStateChange2(proj, "Select all notes before edit cursor")
__________________
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 08-13-2018, 12:50 PM   #49
Thonex
Human being with feelings
 
Join Date: May 2018
Location: Los Angeles
Posts: 1,721
Default

Quote:
Originally Posted by _Stevie_ View Post
Hey Andrew!

You can do a whole lot more than in other DAWs!
Actually, MIDI handling in Reaper/LUA is super easy, but the MIDI Get/Set Note functions are too slow for huge amounts of MIDI data.
That's why Cockos introduced the Get/SetAllEvts functions, which are ultra fast, but the downside is: you have to handle the MIDI raw chunk.
Julian Sader was the one who created the first scripts for that method.

If you haven't yet, check out his amazing MIDI scripts (in his signature).
They surpass a lot of what is available in other DAWs.

Very cool!! I'm still only using Reaper for my heavy lifting editing... but when I head back into MIDI land... I will give it the ol' college try. Are you finding your composing workflow faster than in Cubendo?

And regarding your script above.... :O .. 7 layers of abstraction... that would give me a headache too!! LOL Nice one!
__________________
Cheers... Andrew K
Reaper v6.80+dev0621 - June 21 2023 • Catalina • Mac Mini 2020 6 core i7 • 64GB RAM • OS: Catalina • 4K monitor • RME RayDAT card with Sync Card and extended Light Pipe.
Thonex is offline   Reply With Quote
Old 08-13-2018, 01:16 PM   #50
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

Quote:
Originally Posted by Thonex View Post
Very cool!! I'm still only using Reaper for my heavy lifting editing... but when I head back into MIDI land... I will give it the ol' college try. Are you finding your composing workflow faster than in Cubendo?
Well, I haven't coded everything that I need, yet! But once everything is set up, it will definitely be faster. Back in the Cubase days, I didn't even have the opportunity to implement everything, that I need (no API and LE and PLE are too limited in that regard).


Quote:
Originally Posted by Thonex View Post
And regarding your script above.... :O .. 7 layers of abstraction... that wold give me a headache too!! LOL Nice one!
I know :O I really have to comment that one properly. I'm worried, that I won't understand it in like 2 weeks... That's my main problem with such scripts!
But before, I definitely need to do some tyding. The first step was to get it working :P
__________________
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 08-13-2018, 02:14 PM   #51
FnA
Human being with feelings
 
FnA's Avatar
 
Join Date: Jun 2012
Posts: 2,173
Default

Congratulations. I'm stuck with phone only now but I might make it back to civilization later. One thing Julian pointed out once was that note on with velocity zero can be treated as note off. So you might see in his scripts an Or in the conditions to cover that. I did that in the join scripts too.
FnA is offline   Reply With Quote
Old 08-13-2018, 02:21 PM   #52
Thonex
Human being with feelings
 
Join Date: May 2018
Location: Los Angeles
Posts: 1,721
Default

Quote:
Originally Posted by FnA View Post
One thing Julian pointed out once was that note on with velocity zero can be treated as note off. So you might see in his scripts an Or in the conditions to cover that. I did that in the join scripts too.
Is that compatible with all synths? In other words Velocity zero = release of note?
__________________
Cheers... Andrew K
Reaper v6.80+dev0621 - June 21 2023 • Catalina • Mac Mini 2020 6 core i7 • 64GB RAM • OS: Catalina • 4K monitor • RME RayDAT card with Sync Card and extended Light Pipe.
Thonex is offline   Reply With Quote
Old 08-13-2018, 02:24 PM   #53
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

Thanks man!
Yes, I know what you mean. I was unsure what this condition meant and checked in Reaper's raw data view. The note just disappears. So, is it really a note off?

__________________
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 08-13-2018, 02:27 PM   #54
FnA
Human being with feelings
 
FnA's Avatar
 
Join Date: Jun 2012
Posts: 2,173
Default

Quote:
Originally Posted by Thonex View Post
Is that compatible with all synths? In other words Velocity zero = release of note?
I'm not sure if Reaper even handles it well, always. Lol. This thread had the discussion and a bit more things related to SetAllEvents method.

https://forum.cockos.com/showthread.php?t=186123
FnA is offline   Reply With Quote
Old 08-13-2018, 02:28 PM   #55
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

No worries man! Maybe Julian can elaborate a bit.
__________________
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 08-13-2018, 02:33 PM   #56
cfillion
Human being with feelings
 
cfillion's Avatar
 
Join Date: May 2015
Location: Québec, Canada
Posts: 4,967
Default

Quote:
Originally Posted by Thonex View Post
Is that compatible with all synths? In other words Velocity zero = release of note?
It's in the MIDI specification, at least...

Quote:
MIDI provides two roughly equivalent means of turning off a note (voice). A note may be turned off either by sending a Note-Off message for the same note number and channel, or by sending a Note-On message for that note and channel with a velocity value of zero. The advantage to using "Note-On at zero velocity" is that it can avoid sending additional status bytes when Running Status is employed.

Due to this efficiency, sending Note-On messages with velocity values of zero is the most commonly used method. [...] A receiver must be capable of recognizing either method of turning off a note, and should treat them identically.

Last edited by cfillion; 08-13-2018 at 03:05 PM.
cfillion is offline   Reply With Quote
Old 08-13-2018, 03:12 PM   #57
FnA
Human being with feelings
 
FnA's Avatar
 
Join Date: Jun 2012
Posts: 2,173
Default

One technical thing I wonder about:

Is it necessary to always unpack the string in forward direction? I guess: yes, because of variable length of string and it's bytes at the beginning (s4 in "i4Bs4") specifying how long it is?
FnA is offline   Reply With Quote
Old 08-13-2018, 03:27 PM   #58
Thonex
Human being with feelings
 
Join Date: May 2018
Location: Los Angeles
Posts: 1,721
Default

Quote:
Originally Posted by cfillion View Post
It's in the MIDI specification, at least...

Quote:
A receiver must be capable of recognizing either method of turning off a note, and should treat them identically.
Thanks cfillion,

Good to know. Hopefully both are equally compatible when dealing with the sustain pedal (CC64) as well.
__________________
Cheers... Andrew K
Reaper v6.80+dev0621 - June 21 2023 • Catalina • Mac Mini 2020 6 core i7 • 64GB RAM • OS: Catalina • 4K monitor • RME RayDAT card with Sync Card and extended Light Pipe.
Thonex is offline   Reply With Quote
Old 08-13-2018, 08:23 PM   #59
FnA
Human being with feelings
 
FnA's Avatar
 
Join Date: Jun 2012
Posts: 2,173
Default

Copied to a computer and tried it out. Seems to be coming along well.

Quote:
Originally Posted by _Stevie_ View Post
I haven't included a fallback for overlapping notes, yet.
Or something to do muted/unmuted notes separate?


PS, I noticed just now in the MIDI editors that higher channel note's starts get visually obscured by lower channel note's ends overlapping them. Actually an entire note, or several, can be obscured.

Last edited by FnA; 08-13-2018 at 09:10 PM.
FnA is offline   Reply With Quote
Old 08-14-2018, 08:10 AM   #60
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

Quote:
Originally Posted by FnA View Post
Copied to a computer and tried it out. Seems to be coming along well.
Awesome!

Quote:
Originally Posted by FnA View Post
Or something to do muted/unmuted notes separate?
What you mean exactly? I actually like your bitwise approach to set the flags.

Quote:
Originally Posted by FnA View Post
PS, I noticed just now in the MIDI editors that higher channel note's starts get visually obscured by lower channel note's ends overlapping them. Actually an entire note, or several, can be obscured.
Oh snap, I have to check that. I hope it's not a huge problem to fix.
__________________
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 08-14-2018, 08:46 AM   #61
FnA
Human being with feelings
 
FnA's Avatar
 
Join Date: Jun 2012
Posts: 2,173
Default

Whoops! Channels thing was just conversation. Something I noticed. Nothing specific to your script. I just tested it with overlapping notes on different channels, even though I knew it probably wouldn't be a problem. I guess I'm inclined to be forgiving towards the devs after doing this script stuff, even though the situation is not optimal.

You can still use the flags = flags|1 thing for selection. But you should compartmentalize the tables to treat muted and non muted as different entities almost like two channels because they can be intermixed that way. In other words you shouldn't treat two notes as overlapping notes if they differ in mute status. Or, you don't HAVE to.

But if you need to get the mute status for this reason you may find you could use specific numbers rather than |1 thingy. Depending on how it ends up being arranged.

Sorry if this is short. Kind of busy right now.
FnA is offline   Reply With Quote
Old 08-14-2018, 02:28 PM   #62
FnA
Human being with feelings
 
FnA's Avatar
 
Join Date: Jun 2012
Posts: 2,173
Default

Is that confusing? I'll add a little more.

It would work and be safe if you treat muted notes the same as nonmuted, if you force the exit like it sounds like you are planning when finding note on before off. It will require a few more conditions to treat the mute notes separately. In addition to the extra table spaces. A little more code lines.

So it's more work to handle them separate. But if you skip it, the script is limited in a way that it doesn't need to be, for overlapping note error prevention. Of course mute on mute overlap is the same as with nonmuted overlapping notes. So you do the same treatment with both of them. In parallel, I'll say.
FnA is offline   Reply With Quote
Old 08-14-2018, 03:45 PM   #63
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

Quote:
Originally Posted by FnA View Post
But you should compartmentalize the tables to treat muted and non muted as different entities almost like two channels because they can be intermixed that way.
I think, I'm seeing the issue now. Took a while, maybe because I haven't encounterted an edge case, yet.

I will have to put the muted and non-muted notes in 2 different tables, right?
__________________
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 08-14-2018, 05:54 PM   #64
FnA
Human being with feelings
 
FnA's Avatar
 
Join Date: Jun 2012
Posts: 2,173
Default

That or at least in a dedicated space in the same table. It's kind of flexible how you set it up, as long as you point to the right spots always. But I suppose you want to find an arrangement that is efficient to use and construct. I don't know too much about optimum use of tables. But I did read a short guide on optimization that said to avoid creating/growing them if possible. Reuse spaces rather than rebuilding. I guess that's another subject.

The function you have on top of the script builds a table with 16 sub tables that have 128 tables as elements. I didn't notice this last night, but when you do:
RunningNotes[channel][pitch] = false
You are replacing a table with just false, so you end up with a channel table with something like:
{ {},{},{},false,{},{} ... }

RunningNotes[channel][pitch][1]= false
Would put false IN that one of the 128 tables. Then, of course you need to look for it that way too.

It still works because you're pointing to the right spot each time, and you only need the one piece of info. You could put RunningNotes[channel][pitch][1]= false for nonmuted, and RunningNotes[channel][pitch][2]= false for muted. Setting up for that when you first build the table, by using {false,false} instead of {}. If you didn't need (want) to do the muted notes separate, maybe those 128 tables could have just all been falses. Maybe it would be more efficient to do 2*16 tables full of falses. Or 16*2. Heh. Dunno.
FnA is offline   Reply With Quote
Old 08-16-2018, 04:08 AM   #65
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

Quote:
Originally Posted by FnA View Post
RunningNotes[channel][pitch] = false
You are replacing a table with just false, so you end up with a channel table with something like:
{ {},{},{},false,{},{} ... }
Yep, that's right. The true or false acts as a flag, and gives information about whether a note-on has been previously found.
I first thought, that tables would work differently. Like: when a note-on is found THEN ONLY write the [channel] and [pitch] into a table. But the additional true / false check is faster, since it locates immediately the [channel] and [pitch] tables, without iterating through the whole table.

Quote:
RunningNotes[channel][pitch][1]= false
Would put false IN that one of the 128 tables. Then, of course you need to look for it that way too.

It still works because you're pointing to the right spot each time, and you only need the one piece of info. You could put RunningNotes[channel][pitch][1]= false for nonmuted, and RunningNotes[channel][pitch][2]= false for muted. Setting up for that when you first build the table, by using {false,false} instead of {}. If you didn't need (want) to do the muted notes separate, maybe those 128 tables could have just all been falses. Maybe it would be more efficient to do 2*16 tables full of falses. Or 16*2. Heh. Dunno.
Wow, very cool, great approach! I will check this out in a few.
__________________
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 08-16-2018, 10:25 AM   #66
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

Quote:
Originally Posted by FnA View Post
It still works because you're pointing to the right spot each time, and you only need the one piece of info. You could put RunningNotes[channel][pitch][1]= false for nonmuted, and RunningNotes[channel][pitch][2]= false for muted. Setting up for that when you first build the table, by using {false,false} instead of {}. If you didn't need (want) to do the muted notes separate, maybe those 128 tables could have just all been falses. Maybe it would be more efficient to do 2*16 tables full of falses. Or 16*2. Heh. Dunno.
What I did now, is: (I renamed the table)

Code:
local note_ons_to_select = {}
for channel = 0, 15 do
    note_ons_to_select[channel] = {}
    for pitch = 0, 127 do
	note_ons_to_select[channel][pitch] = {}
	for flag = 0, 1 do
	    note_ons_to_select[channel][pitch][flag] = false
	end
    end
end
I chose 0 for non-muted and 1 for muted notes, since tables start with 0.
I didn't get the {false,false} part. At least, it didn't work here.
Instead, I did a simple "= false". Or did I missunderstand you?

EDIT: hmm, I should actually take the same values for flags, as Reaper.
That way, I can just put note_ons_to_select[channel][pitch][flag].
I guess, that's what you were heading for.
__________________
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

Last edited by _Stevie_; 08-16-2018 at 10:38 AM.
_Stevie_ is offline   Reply With Quote
Old 08-16-2018, 04:19 PM   #67
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

Okay, new version ([0] didn't work in the table, so I chose [1] and [2], as you proposed, heh).
The script now seperates non-muted and muted notes. And it also checks for overlapping notes. However, it does check for them "on the fly" and stops the script, when overlapping notes are reached. Not too happy with that behavior.
Should I include an iteration to check only for overlapping notes beforehand?


Code:
-- create table for note-ons
local select_note_ons = {}
for channel = 0, 15 do
    select_note_ons[channel] = {}
    for pitch = 0, 127 do
		select_note_ons[channel][pitch] = {}
		select_note_ons[channel][pitch][1] = false -- non-muted notes false
		select_note_ons[channel][pitch][2] = false -- muted notes false
    end
end

cursorPosition = reaper.GetCursorPosition()  -- get edit cursor position 
local sumOffset = 0 -- initialize sumOffset (adds all offsets to get the position of every event in ticks)

if reaper.CountSelectedMediaItems(0) > 1 then
	reaper.ShowMessageBox("Please select only one item", "Error" , 0) -- popup error message, if more than 1 item is selected
	return
elseif reaper.CountSelectedMediaItems(0) == 0 then 
	reaper.ShowMessageBox("Please select one item", "Error" , 0) -- popup error message, if no item is selected
	return
elseif reaper.CountSelectedMediaItems(0) == 1 then
	for i = 0, reaper.CountSelectedMediaItems(0)-1 do -- loop through all selected items
		item = reaper.GetSelectedMediaItem(0, i) -- get current selected item
		itemStart = reaper.GetMediaItemInfo_Value(item, "D_POSITION") -- get start of 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
			cursorPositionPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, cursorPosition) -- convert cursorPosition to PPQ
			itemStartPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, itemStart) -- convert itemStart to PPQ
			if reaper.TakeIsMIDI(take) then -- make sure, that take is MIDI
				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
					sumOffset = sumOffset+offset -- add all event offsets to get next start position of event on each iteration
					eventStart = itemStartPPQ+sumOffset -- calculate event start position based on item start position
					
					if #msg == 3 then -- if msg consists of 3 bytes (= channel message)
						local 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
						local channel = msg:byte(1)&0x0F
						local pitch = msg:byte(2)
						
						-- note-on before cursor position? select
						if msg_b1_nib1 == 9 then -- note-on
							if select_note_ons[channel][pitch][1] or select_note_ons[channel][pitch][2] then
								reaper.ShowMessageBox("There are overlapping notes", "Error", 0)
								return false
							elseif eventStart < cursorPositionPPQ then
								if flags <= 1 then -- if non-muted event
									select_note_ons[channel][pitch][1] = true -- set non-muted 
								else -- if muted event
									select_note_ons[channel][pitch][2] = true -- set muted
								end
								flags = flags|1 -- select
							
							-- note-on after cursor position? unselect 
							elseif eventStart >= cursorPositionPPQ then 
								if flags <= 1 then -- if non-muted event
									select_note_ons[channel][pitch][1] = false -- clear non-muted 
								else 
									select_note_ons[channel][pitch][2] = false -- clear muted
								end
								flags = flags &~ 1 -- unselect
							end
						
						-- note-off anywhere and note-on before cursor? select
						elseif msg_b1_nib1 == 8 then -- note-off
							if select_note_ons[channel][pitch][1] then -- non-muted note-on?
								select_note_ons[channel][pitch][1] = false -- clear non-muted 
								flags = flags|1 -- select
							elseif select_note_ons[channel][pitch][2] then -- muted note-on?
								select_note_ons[channel][pitch][2] = false -- clear muted
								flags = flags|1 -- select

							-- note-off and note-on after cursor? unselect
							else
								flags = flags &~ 1 -- unselect
							end
						end
					end
					table.insert(tableEvents, string.pack("i4Bs4", offset, flags, msg)) -- re-pack MIDI string and write to table
				end
			end
		end
	end
end

reaper.MIDI_SetAllEvts(take, table.concat(tableEvents) .. MIDIstring:sub(-12))
reaper.MIDI_Sort(take)
reaper.UpdateArrange()

reaper.Undo_OnStateChange2(proj, "Select all notes before edit cursor")
__________________
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 08-16-2018, 04:36 PM   #68
FnA
Human being with feelings
 
FnA's Avatar
 
Join Date: Jun 2012
Posts: 2,173
Default

I was just about to post the following, when I saw your newest post. I hate it when that happens... I'll post it anyway. Keep that in mind. maybe it will give you something to think about while I examine your latest script.

------------------------------------------------

Quote:
Originally Posted by _Stevie_ View Post
What I did now, is: (I renamed the table)

Code:
local note_ons_to_select = {}
for channel = 0, 15 do
    note_ons_to_select[channel] = {}
    for pitch = 0, 127 do
	note_ons_to_select[channel][pitch] = {}
	for flag = 0, 1 do
	    note_ons_to_select[channel][pitch][flag] = false
	end
    end
end
I chose 0 for non-muted and 1 for muted notes, since tables start with 0.
I didn't get the {false,false} part. At least, it didn't work here.
Instead, I did a simple "= false". Or did I missunderstand you?
Just a little. Off by one, I'd say. I don't blame you. This is hard to communicate, while you're unfamiliar with tables, especially when I only have the phone internet.

I meant something like this:
Code:
local note_ons_to_select = {}
for channel = 0, 15 do
    note_ons_to_select[channel] = {}
    for pitch = 0, 127 do
	note_ons_to_select[channel][pitch] = {false,false}
    end
end
Tables don't "start with zero" really. juliansader probably did that to avoid repeatedly having to use little +1/-1 math problems in the main body of the script. Generally tables default to starting with 1 if you make something like:
t = {"a","b","c"}
t[1] will be "a"

but tables don't HAVE to follow 1,2,3...
They don't even have to be in sequence (or even use numbers as keys).

You can do:
Code:
t = {}
t[77] = true

if t[0] then x = true end
if t[1] then y = true end
if t[77] then z = true end
You will only see z in the IDE.

Quote:
Originally Posted by _Stevie_ View Post
EDIT: hmm, I should actually take the same values for flags, as Reaper.
That way, I can just put note_ons_to_select[channel][pitch][flag].
I guess, that's what you were heading for.
Well, I was hoping to show that there is more than one way of skinning this cat. In post #39 I hinted at maybe having 128 member table was not necessary for this particular script. Later, I thought maybe it would be easier to grasp if there was though, because it might look like there was a slot in this multi level matrix(?) waiting for any kind of note you could throw at it.

But this is getting kind of out of control, so I just went ahead and modified what you posted a little with as small a table as I think will serve this script. You might need that bigger table for some other scripts. You still have to work out the overlapping note exit part, and I chopped out the 1 item only messages, just so I didn't have to scroll so much. Seems to work for the selecting about the same as before.

It will expand the innermost(?) level table with each different pitch it finds. I think it will still work about the same if you replace nil with false. Then you could see on the side of the IDE some kind of results where notes end up. I think in this script you might be able to safely reuse the same table for each item, but sometimes you will want to rebuild it or clean it out for each item.

Code:
runningNotes = {}
for c=0,15 do
  runningNotes[c] = {}
  for m=0,2,2 do
    runningNotes[c][m] = {}
  end
end

cursorPosition = reaper.GetCursorPosition()  -- get edit cursor position 
local sumOffset = 0 -- initialize sumOffset (adds all offsets to get the position of every event in ticks)

if reaper.CountSelectedMediaItems(0) == 1 then
  for i = 0, reaper.CountSelectedMediaItems(0)-1 do -- loop through all selected items
    item = reaper.GetSelectedMediaItem(0, i) -- get current selected item
    itemStart = reaper.GetMediaItemInfo_Value(item, "D_POSITION") -- get start of 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
      cursorPositionPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, cursorPosition) -- convert cursorPosition to PPQ
      itemStartPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, itemStart) -- convert itemStart to PPQ
      if reaper.TakeIsMIDI(take) then -- make sure, that take is MIDI
        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
          sumOffset = sumOffset+offset -- add all event offsets to get next start position of event on each iteration
          eventStart = itemStartPPQ+sumOffset -- calculate event start position based on item start position
          
          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

            channel = msg:byte(1)&0x0F
            pitch = msg:byte(2)
          
            -- note on before cursor position? = select
            if msg_b1_nib1 == 9 and eventStart < cursorPositionPPQ then 
              flags = flags|1
              runningNotes[channel][flags&2][pitch] = true
    
            -- note on after cursor position? = unselect 
            elseif msg_b1_nib1 == 9 and eventStart >= cursorPositionPPQ then
              runningNotes[channel][flags&2][pitch] = nil
              flags = flags &~ 1

            -- note off anywhere and note-on before cursor? = select
            elseif msg_b1_nib1 == 8 and runningNotes[channel][flags&2][pitch] then
              flags = flags|1
              runningNotes[channel][flags&2][pitch] = nil
            
            -- note off and note-on after cursor? = unselect
            elseif msg_b1_nib1 == 8 and not runningNotes[channel][flags&2][pitch] then
            flags = flags &~ 1
            end
          end
          table.insert(tableEvents, string.pack("i4Bs4", offset, flags, msg)) -- re-pack MIDI string and write to table
        end
      end
    end
  end
end

reaper.MIDI_SetAllEvts(take, table.concat(tableEvents) .. MIDIstring:sub(-12))
reaper.MIDI_Sort(take)
reaper.UpdateArrange()

reaper.Undo_OnStateChange2(proj, "Select all notes before edit cursor")
FnA is offline   Reply With Quote
Old 08-16-2018, 06:35 PM   #69
FnA
Human being with feelings
 
FnA's Avatar
 
Join Date: Jun 2012
Posts: 2,173
Default

Hey, that's pretty good.

This gets triggered by a mute note overlapping a nonmute though:
Code:
if select_note_ons[channel][pitch][1] or select_note_ons[channel][pitch][2] then


You don't like it because it might do several items before finding an error?
Maybe you can have:
ERROR = false
at the start. Then if you find an error set it to true. You could store the takes and processed strings in a table , skipping SetAllEvents until later, then apply the SetallEvents to the takes (after doing string processing/error checking to all of them), if ERROR is still false.

I don't know if there could be a memory problem or something with huge items. Maybe someone else knows.

Nitpick:
local channel = msg:byte(1)&0x0F
local pitch = msg:byte(2)
could be moved into note on/off blocks, so they don't happen on other events.

Last edited by FnA; 08-16-2018 at 06:44 PM.
FnA is offline   Reply With Quote
Old 08-17-2018, 03:16 AM   #70
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

Quote:
Originally Posted by FnA View Post
Hey, that's pretty good.

This gets triggered by a mute note overlapping a nonmute though:
[code]
if select_note_ons[channel][pitch][1] or select_note_ons[channel][pitch][2] then
Hmm, have to check where the problem is.

Quote:
You don't like it because it might do several items before finding an error?
Maybe you can have:
ERROR = false
at the start. Then if you find an error set it to true. You could store the takes and processed strings in a table , skipping SetAllEvents until later, then apply the SetallEvents to the takes (after doing string processing/error checking to all of them), if ERROR is still false.
Awwww, brain was stuck! Thanks, that's exactly what I wanted. I somehow thought, that selecting in the loop already selects, but I completely forgot, that the last step is re-uploading the string, doh!

Quote:
Nitpick:
local channel = msg:byte(1)&0x0F
local pitch = msg:byte(2)
could be moved into note on/off blocks, so they don't happen on other events.
Good catch! Will change that as well.

Concerning the smaller table. I'm afraid, this missunderstanding is due to my lack of table knowledge... You reduced the table to 16*2, which is pretty cool. But what about all the pitches? You still assign them via [pitch], but does that mean, they aren'T put in a table anymore?
__________________
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 08-17-2018, 04:34 AM   #71
FnA
Human being with feelings
 
FnA's Avatar
 
Join Date: Jun 2012
Posts: 2,173
Default

Quote:
Originally Posted by _Stevie_ View Post
Concerning the smaller table. I'm afraid, this missunderstanding is due to my lack of table knowledge... You reduced the table to 16*2, which is pretty cool. But what about all the pitches? You still assign them via [pitch], but does that mean, they aren'T put in a table anymore?
They get put into one of the two mutestatus tables that are in the channel tables, depending on flags value. Kind of like t[77]=true example. Those two tables have keys: 0 and 2. It might seem a neat trick to use that [flags&2] to reduce number of small operations needed in main script loop, but could in the long run be more efficient to just check each note on/off mute status and assign a value to a variable that can be used several times. Then maybe it's not important for the keys to be 0 and 2. Maybe 0 and 1, 1 and 2, whatever. But you would have to tie that all together. Make the two tables have the right keys for whatever you come up with.
FnA is offline   Reply With Quote
Old 08-17-2018, 01:31 PM   #72
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

Okay, next version!

Man, you are using techniques, that make me scratch my head.
[yoda on] A lot to learn I have [/yoda off]

Code:
local cursor_position = reaper.GetCursorPosition()  -- get edit cursor position 

-- create table for note-ons
note_on_selection = {}
for c=0,15 do -- channel table
	note_on_selection[c] = {}
  for m=0,2,2 do -- mute table
    note_on_selection[c][m] = {}
  end
end

if reaper.CountSelectedMediaItems(0) == 0 then
	reaper.ShowMessageBox("Please select at least one item", "Error", 0)
	return false
else 
	for i = 0, reaper.CountSelectedMediaItems(0)-1 do -- loop through all selected items
		local item = reaper.GetSelectedMediaItem(0, i) -- get current selected item
		local item_start = reaper.GetMediaItemInfo_Value(item, "D_POSITION")
		take = reaper.GetActiveTake(item)
		local cursor_position_ppq = reaper.MIDI_GetPPQPosFromProjTime(take, cursor_position) -- convert cursor_position to PPQ
		local item_start_ppq = reaper.MIDI_GetPPQPosFromProjTime(take, item_start) -- convert item_start to PPQ
			
		if reaper.TakeIsMIDI(take) then
			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 -- get string length
			tableEvents = {} -- initialize table, MIDI events will temporarily be stored in this table until they are concatenated into a string again
			local stringPos = 1 -- position in MIDIstring while parsing through events 
			local sum_offset = 0 -- initialize sum_offset (adds all offsets to get the position of every event in ticks)

			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
				sum_offset = sum_offset+offset -- add all event offsets to get next start position of event on each iteration
				local event_start = item_start_ppq+sum_offset -- calculate event start position based on item start position
				local event_type = msg:byte(1)>>4 -- save 1st nibble of status byte (contains info about the data type) to event_type, >>4 shifts the channel nibble into oblivion
				
				if event_type == 9 and msg:byte(3) ~= 0 then -- if note-on and velocity is not 0
					local channel = msg:byte(1)&0x0F
					local pitch = msg:byte(2)
					
					if note_on_selection[channel][flags&2][pitch] then
						reaper.ShowMessageBox("Can't select, because overlapping notes were found", "Error", 0)
						return false

					-- note-on before cursor position? select	
					elseif event_start < cursor_position_ppq then
						flags = flags|1 -- select
						note_on_selection[channel][flags&2][pitch] = true -- tag note-on

					-- note-on after cursor position? unselect 
					elseif event_start >= cursor_position_ppq then 
						flags = flags &~ 1 -- unselect
						note_on_selection[channel][flags&2][pitch] = nil -- untag note-on
					end
				
				elseif event_type == 8 or (event_type == 9 and msg:byte(3) == 0) then -- if note-off
						
					local channel = msg:byte(1)&0x0F
					local pitch = msg:byte(2)

					-- note-off anywhere and note-on before cursor? select
					if note_on_selection[channel][flags&2][pitch] then -- matching note-on tagged for selection?
						flags = flags|1 -- select
						note_on_selection[channel][flags&2][pitch] = nil -- untag note-on
					
					-- note-off and note-on after cursor? unselect
					else
						flags = flags &~ 1 -- unselect
					end
				end
				table.insert(tableEvents, string.pack("i4Bs4", offset, flags, msg)) -- re-pack MIDI string and write to table
			end
		end
		reaper.MIDI_SetAllEvts(take, table.concat(tableEvents) .. MIDIstring:sub(-12))
	end
end
reaper.MIDI_Sort(take)
reaper.UpdateArrange()

reaper.Undo_OnStateChange2(proj, "Select all notes before edit cursor")
__________________
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 08-17-2018, 08:24 PM   #73
FnA
Human being with feelings
 
FnA's Avatar
 
Join Date: Jun 2012
Posts: 2,173
Default

Soon pass me up, you will. It looks real nice. Hopefully I'll try it tomorrow. It looks like it should maybe keep putting note on in the table after the cursor, for thorough overlap detection, however. [edit. Bit of a problem, because that's how note off gets selected or not? Maybe two different kinds of value in the table. Something?]

Other than that, I'd say finish your exit strategy thing and move on to the "After" script. (It occurs to me that it might be useful to point out which item the script exited on to the user. Maybe even analyze all, then offer to select them or something. Well, whatever...)

Last edited by FnA; 08-17-2018 at 09:19 PM.
FnA is offline   Reply With Quote
Old 08-18-2018, 03:08 AM   #74
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

Okay, found a way for the overlap detection. I'm now using 2 variables inside the pitch table: tag (for selection) and od (overlap detection).
However, this way, I had to create the full pitch table again. Otherwise, the variables tag and od can't be created.
Otherwise, I get the error: attempt to index a nil value (field '?')

So, for now, I have initialzed the whole shebang again... Unless you have a more elegant way.

Code:
local cursor_position = reaper.GetCursorPosition()  -- get edit cursor position 

-- create table for note-ons
note_on_selection = {}
for c=0,15 do -- channel table
	note_on_selection[c] = {}
	for m=0,2,2 do -- mute table
		note_on_selection[c][m] = {}
		-- for p=0, 127 do -- pitch table
		-- 	note_on_selection[c][m][p] = {tag, od}
		-- end
	end
end


if reaper.CountSelectedMediaItems(0) == 0 then
	reaper.ShowMessageBox("Please select at least one item", "Error", 0)
	return false
else 
	for i = 0, reaper.CountSelectedMediaItems(0)-1 do -- loop through all selected items
		local item = reaper.GetSelectedMediaItem(0, i) -- get current selected item
		local item_start = reaper.GetMediaItemInfo_Value(item, "D_POSITION")
		take = reaper.GetActiveTake(item)
		local cursor_position_ppq = reaper.MIDI_GetPPQPosFromProjTime(take, cursor_position) -- convert cursor_position to PPQ
		local item_start_ppq = reaper.MIDI_GetPPQPosFromProjTime(take, item_start) -- convert item_start to PPQ
			
		if reaper.TakeIsMIDI(take) then
			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 -- get string length
			tableEvents = {} -- initialize table, MIDI events will temporarily be stored in this table until they are concatenated into a string again
			local stringPos = 1 -- position in MIDIstring while parsing through events 
			local sum_offset = 0 -- initialize sum_offset (adds all offsets to get the position of every event in ticks)

			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
				sum_offset = sum_offset+offset -- add all event offsets to get next start position of event on each iteration
				local event_start = item_start_ppq+sum_offset -- calculate event start position based on item start position
				local event_type = msg:byte(1)>>4 -- save 1st nibble of status byte (contains info about the data type) to event_type, >>4 shifts the channel nibble into oblivion
				
				if event_type == 9 and msg:byte(3) ~= 0 then -- if note-on and velocity is not 0
					local channel = msg:byte(1)&0x0F
					local pitch = msg:byte(2)

					if note_on_selection[channel][flags&2][pitch].od then
						reaper.ShowMessageBox("Can't select, because overlapping notes in selected item #" .. i+1 .. " were found", "Error", 0)
						return false

					-- note-on before cursor position? select	
					elseif event_start < cursor_position_ppq then
						note_on_selection[channel][flags&2][pitch].tag = true -- tag for selection
						note_on_selection[channel][flags&2][pitch].od = true -- overlap-detection 
						flags = flags|1 -- select
					
						-- note-on after cursor position? unselect 
					elseif event_start >= cursor_position_ppq then 
						note_on_selection[channel][flags&2][pitch].tag = nil -- untag for selection
						note_on_selection[channel][flags&2][pitch].od = true -- overlap-detection 
						flags = flags &~ 1 -- unselect
					end
				
				elseif event_type == 8 or (event_type == 9 and msg:byte(3) == 0) then -- if note-off
						
					local channel = msg:byte(1)&0x0F
					local pitch = msg:byte(2)

					-- note-off anywhere and note-on before cursor? select
					if note_on_selection[channel][flags&2][pitch].tag then -- matching note-on tagged for selection?
						note_on_selection[channel][flags&2][pitch].tag = nil -- untag for selection
						note_on_selection[channel][flags&2][pitch].od = nil -- clear overlap-detection 
						flags = flags|1 -- select
					
					-- note-off and note-on after cursor? unselect
					else
						note_on_selection[channel][flags&2][pitch].od = nil -- clear overlap-detection 
						flags = flags &~ 1 -- unselect
					end
				end
				table.insert(tableEvents, string.pack("i4Bs4", offset, flags, msg)) -- re-pack MIDI string and write to table
			end
		end
		reaper.MIDI_SetAllEvts(take, table.concat(tableEvents) .. MIDIstring:sub(-12))
	end
end
reaper.MIDI_Sort(take)
reaper.UpdateArrange()

reaper.Undo_OnStateChange2(proj, "Select all notes before edit cursor")
__________________
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 08-18-2018, 06:48 AM   #75
FnA
Human being with feelings
 
FnA's Avatar
 
Join Date: Jun 2012
Posts: 2,173
Default

Again, just guessing, but I was thinking instead of using true/nil, you could use 0 or 1/nil. Use, say, 0, before cursor, and 1 after, in part where note on is found and/or selected. Anything but false or nil evaluates as true so error checking note on part should still work. Then note off part can have little nested if/else sort of like:
Code:
 
X = noteonselection[channel][fags&2][pitch]
If x then
  If x == 0 then
    Select, and nil pitch
  Else
    Unselect, and nil pitch
  End
End
Not sure if that covers everything completely.


So, I was thinking before that it might be ok to reuse the table in a following item. Now I think you should use a new or cleaned out table each time, especially if you continue evaluating other items after finding an error. Which is something I mentioned as possibility in previous post. But it might be a good idea anyway because I have run into an item or two in my projects that had an endless note/missing note off. If you reuse the table, that pitch might be lingering in it, ready to cause mischief. We can talk about this more later as it gets into optimization things which I only partly understand. I think error prevention tends to dominate that however.
FnA is offline   Reply With Quote
Old 08-18-2018, 07:52 AM   #76
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

Ah again, I was overthinking it. I absolutely didn't abstract, that I can use 0,1 and nil to get the job done.

Anyway, got it working (I hope).
And I also put the table beneath the take part, so that it gets cleared/re-initialized on every iteration.

Code:
local cursor_position = reaper.GetCursorPosition()  -- get edit cursor position 

if reaper.CountSelectedMediaItems(0) == 0 then
	reaper.ShowMessageBox("Please select at least one item", "Error", 0)
	return false
else 
	for i = 0, reaper.CountSelectedMediaItems(0)-1 do -- loop through all selected items
		local item = reaper.GetSelectedMediaItem(0, i) -- get current selected item
		local item_start = reaper.GetMediaItemInfo_Value(item, "D_POSITION")
		take = reaper.GetActiveTake(item)
		local cursor_position_ppq = reaper.MIDI_GetPPQPosFromProjTime(take, cursor_position) -- convert cursor_position to PPQ
		local item_start_ppq = reaper.MIDI_GetPPQPosFromProjTime(take, item_start) -- convert item_start to PPQ
			
		if reaper.TakeIsMIDI(take) then

			local c, m

			-- create table for note-ons
			note_on_selection = {}
			for c = 0, 15 do -- channel table
				note_on_selection[c] = {}
				for m = 0, 2, 2 do -- mute table
					note_on_selection[c][m] = {}
				end
			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 -- get string length
			tableEvents = {} -- initialize table, MIDI events will temporarily be stored in this table until they are concatenated into a string again
			local stringPos = 1 -- position in MIDIstring while parsing through events 
			local sum_offset = 0 -- initialize sum_offset (adds all offsets to get the position of every event in ticks)

			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
				sum_offset = sum_offset+offset -- add all event offsets to get next start position of event on each iteration
				local event_start = item_start_ppq+sum_offset -- calculate event start position based on item start position
				local event_type = msg:byte(1)>>4 -- save 1st nibble of status byte (contains info about the data type) to event_type, >>4 shifts the channel nibble into oblivion
				
				if event_type == 9 and msg:byte(3) ~= 0 then -- if note-on and velocity is not 0
					local channel = msg:byte(1)&0x0F
					local pitch = msg:byte(2)
					
					if note_on_selection[channel][flags&2][pitch] then
						reaper.ShowMessageBox("Can't select, because overlapping notes in selected item #" .. i+1 .. " were found", "Error", 0)
						return false

					-- note-on before cursor position? select	
					elseif event_start < cursor_position_ppq then
						flags = flags|1 -- select
						note_on_selection[channel][flags&2][pitch] = 1 -- tag note-on for selection

					-- note-on after cursor position? unselect 
					elseif event_start >= cursor_position_ppq then 
						flags = flags&~1 -- unselect
						note_on_selection[channel][flags&2][pitch] = 0 -- untag note-on
					end
				
				elseif event_type == 8 or (event_type == 9 and msg:byte(3) == 0) then -- if note-off
						
					local channel = msg:byte(1)&0x0F
					local pitch = msg:byte(2)

					-- note-off anywhere and note-on before cursor? select
					if note_on_selection[channel][flags&2][pitch] == 1 then -- matching note-on tagged for selection?
						flags = flags|1 -- select
						note_on_selection[channel][flags&2][pitch] = nil -- clear note-on

					-- note-off and note-on after cursor? unselect
					elseif note_on_selection[channel][flags&2][pitch] == 0 then
						flags = flags&~1 -- unselect
						note_on_selection[channel][flags&2][pitch] = nil -- clear note-on
					end
				end
				table.insert(tableEvents, string.pack("i4Bs4", offset, flags, msg)) -- re-pack MIDI string and write to table
			end
		end
		reaper.MIDI_SetAllEvts(take, table.concat(tableEvents) .. MIDIstring:sub(-12))
	end
end
reaper.MIDI_Sort(take)
reaper.UpdateArrange()

reaper.Undo_OnStateChange2(proj, "Select all notes before edit cursor")
__________________
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 08-18-2018, 08:33 AM   #77
FnA
Human being with feelings
 
FnA's Avatar
 
Join Date: Jun 2012
Posts: 2,173
Default

Looks good. I'll try it in Reaper this afternoon, hopefully(USA).

edit. Tried it out on several selected items that were open in Inline Editors. Noticed a screen update issue when error message was operative. Had to click in arrange or ruler to get selected notes in error free items to show up. Moved reaper.midisort(take) to be like so:
Code:
    reaper.MIDI_SetAllEvts(take, table.concat(tableEvents) .. MIDIstring:sub(-12))
    reaper.MIDI_Sort(take)
and seemed to cure it. Other than that, seems to be working right.

Maybe I'll show my little optimization experiment. It only takes a few thousandths of a second to build 1000 of these tables, but replacing only the pairs of mutestatus tables, and reusing main and channel tables is twice as fast.

Meh. Here's a little guide I read instead. I don't grasp everything in it, mind you. https://www.lua.org/gems/sample.pdf

Basically I just did a similar loop to the one that builds the table, only putting in new tables in the for m=0,2,2 loop. That was in a small function. I tried a further loop using pairs to nil out individual elements in those tables instead of replacing them with {}, but it was a loss compared to that.

The main table had to be far enough back so that all code blocks that use it can access it. I don't know what the number of items this script can be expected to encounter, if it's worth worrying about. I guess I'd build the table after the first else, then clean it when necessary.

Localizing stuff makes quite a speed difference in some cases too. In addition to containing things where they belong. But it might be a little tricky to set things up where they will be accessible every place they are needed.

I'll edit this post, unless thread evolves more or something.

Last edited by FnA; 08-18-2018 at 05:02 PM.
FnA is offline   Reply With Quote
Old 08-18-2018, 09:11 AM   #78
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

Ah great, looking forward to it (replying anyway, so that I get a notifcation, when you reply ).

In the meantime, I'm trying to find a way to select notes UNDER (= crossing) the edit cursor. This is way more complex, though.

From what I can see, I need to check if the note-on starts before the edit cursor AND if the note-off lies behind the edit cursor (= note crossing).

This is what I came up with so far:

- get note-on before the cursor
- find the matching note-off and check if it lies behind the edit cursor
- if true = select matching note-on and note-off
- else, go to next note-on

The problem is, I will have to jump back and forth in the events. Since, when I reached the matching note-off, I'm already past the note-on stringPos.

I could store the current note-on-pos.
When a matching note-off has been found, store the note-off-pos,
restore the note-on-pos to stringPos, set note-on flag to select, restore note-off-pos and set note-off flag to select. Set a skip flag for the last note-on and note-off. stringPos = note-on-pos + 1 and same procedure.

Again, maybe, I'm thinking too complicated...

Or should I rather use a table and put all events in there? Just thinking out loud...
__________________
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 08-18-2018, 10:01 AM   #79
FnA
Human being with feelings
 
FnA's Avatar
 
Join Date: Jun 2012
Posts: 2,173
Default

Idea to turn over in your mind:
You do have a table of events. I think that it, by default treatment, starts with 1, and is sequential. If that's incorrect the operations used there could be replaced with operations that do put things in 1,2,3 order. [edit. It does, tried it. tableEvents[1] gets the first event's string, tableEvents[2] the second, etc] The three variables are re packed into short, usually 12 byte (text events, etc might not be 12 bytes) strings. Which are concatenated in SetAllEvents line in parenthesis. You could store i of while loop iteration index instead of 1 as value for pitch key of mutestatus table for notes starting before cursor, and something else (true, 0, whatever) for after cursor, similar to how "before cursor" script works. You would have to unpack the string, alter, repack/replace. Or skip re packing until after the while loop entirely. Since only a few notes per item will be under cursor, probably best to use former in this script.

Last edited by FnA; 08-18-2018 at 04:58 PM.
FnA is offline   Reply With Quote
Old 03-30-2019, 03:52 AM   #80
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,067
Default

Hey FnA, sorry for never having gotten back to this! Select notes under edit cursor is still on my list, though!

At the moment, I'm doing variations of the script. While working with these scripts, I noticed that
instead of the edit cursor, the mouse cursor would be a way better condition for selecting the notes.
Changing the script accordingly was no problem at all.

Additionally, I want to mute all notes that are located before or after the mouse cursor.
However, I (again) get some unexpected behavior and I can't find the reason for it.
This is the huge issue with algorithms, once you put them aside, it's such a pain to get into it again after a while.

That's my code to mute notes that are in the take under the mouse and located after the mouse cursor. I basically only changed the flags (select) from flags = flags|1 to (mute) flags = flags|2
And from (unselect) flags = flags &~ 1 to (unmute) flags = flags &~ 2
But I must overlook something...

Code:
-- create table for note-ons
note_on_selection = {}
for c = 0, 15 do -- channel table
	note_on_selection[c] = {}
  for f = 0, 2, 2 do -- flag table
    note_on_selection[c][f] = {}
  end
end

sumOffset = 0 -- initialize sumOffset (adds all offsets to get the position of every event in ticks)
_, _, _ = reaper.BR_GetMouseCursorContext()
mouse_pos = reaper.BR_PositionAtMouseCursor(false)
take = reaper.BR_GetMouseCursorContext_Take()
mouse_position_ppq = reaper.MIDI_GetPPQPosFromProjTime(take, mouse_pos) -- convert to PPQ
item = reaper.BR_GetMouseCursorContext_Item()
item_start = reaper.GetMediaItemInfo_Value(item, "D_POSITION") -- get start of item
item_start_ppq = reaper.MIDI_GetPPQPosFromProjTime(take, item_start) -- convert itemStart to PPQ

if reaper.TakeIsMIDI(take) then
    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 -- get string length
    tableEvents = {} -- initialize table, MIDI events will temporarily be stored in this table until they are concatenated into a string again
    local stringPos = 1 -- position in MIDIstring while parsing through events 
    local sum_offset = 0 -- initialize sum_offset (adds all offsets to get the position of every event in ticks)

    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
        sum_offset = sum_offset+offset -- add all event offsets to get next start position of event on each iteration
        local event_start = item_start_ppq+sum_offset -- calculate event start position based on item start position
        local event_type = msg:byte(1)>>4 -- save 1st nibble of status byte (contains info about the data type) to event_type, >>4 shifts the channel nibble into oblivion
        
        if event_type == 9 and msg:byte(3) ~= 0 then -- if note-on and velocity is not 0
            local channel = msg:byte(1)&0x0F
            local pitch = msg:byte(2)
            
            if note_on_selection[channel][flags&2][pitch] then
                reaper.ShowMessageBox("Can't mute, because overlapping notes were found", "Error", 0)
                return false

            -- note-on after mouse position? mute	
            elseif event_start > mouse_position_ppq then
                flags = flags|2 -- mute
                note_on_selection[channel][flags&2][pitch] = true -- tag note-on

            -- note-on before mouse position? unmute 
            elseif event_start <= mouse_position_ppq then 
                flags = flags &~ 2 -- unmute
                note_on_selection[channel][flags&2][pitch] = nil -- untag note-on
            end
        
        elseif event_type == 8 or (event_type == 9 and msg:byte(3) == 0) then -- if note-off
                
            local channel = msg:byte(1)&0x0F
            local pitch = msg:byte(2)

            -- note-off anywhere and note-on after mouse? mute
            if note_on_selection[channel][flags&2][pitch] then -- matching note-on tagged for mute?
                flags = flags|2 -- mute
                note_on_selection[channel][flags&2][pitch] = nil -- untag note-on
            
            -- note-off and note-on before mouse? unmute
            else
                flags = flags &~ 2 -- unmute
            end
        end
        table.insert(tableEvents, string.pack("i4Bs4", offset, flags, msg)) -- re-pack MIDI string and write to table
    end
end

reaper.MIDI_SetAllEvts(take, table.concat(tableEvents) .. MIDIstring:sub(-12))
reaper.MIDI_Sort(take)
reaper.UpdateArrange()

reaper.Undo_OnStateChange2(proj, "Mute notes after mouse in take under mouse")
__________________
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
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 08:22 PM.


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