MIDI to System Events: Can I force a single instance of a Script?

Here’s a message from the developer:

Run out of time for today so we’ll pick this up again tomorrow.
I’ll have to go through the folder of examples (included with MidiPipe) to see what he means.

. which is exactly what my script does. Unfortunately, the smallest timing interval with ‘current date’ is one second ” although the accuracy depends on whereabouts in the adjacent seconds the time’s sampled! Finer timings can be obtained with a third-party OSAX like LapTime OSAX, which is accurate to with a few milliseconds.

With LapTime OSAX installed in one of your Scripting Additions folders, and a required non-execution period of half a second, the script might look like this:

-- This script requires LapTime OSAX by Tetsuro KURITA.
-- <http://www.script-factory.net/software/OSAX/LapTime/en/index.html>

property interval : 500 -- 500 ms (half a second).
property delayEnd : 0

on runme(message)
	if (message begins with {176, 17, 65}) then
		set now to (get msec) -- LapTime OSAX.
		if (now ≥ delayEnd) then -- Only execute if this is the first call or the non-execution period's expired.
			tell application "System Events" to key code 126 -- (up arrow)
			set delayEnd to now + interval -- Set the expiry time for a non-execution period which begins now.
		end if
	end if
end runme

Hello.

I just couldn’t help it: :slight_smile: I made my own version, without the timeLapse osaxen, but created a slightly more inaccurate higher granularity timer, which may prove to be just as good, after some initial adjustments.

property interval : 12
-- hundreds of a sec
property delay_end : 0
property firing_per_sec : 100
property sofar : 0
property this_day : 0
# Two ways to scale this:
# if it is firing to seldom, that is, the keypresses are coming to slow
# then lower the number of firings per sec. Otherwise, increase.

on runme(message)
	if (message begins with {176, 17, 65}) then
		set sofar to sofar + 1
		set {now, cur_day} to {time, day} of (current date)
		if cur_day ≠ this_day then
			set this_day to cur_day
			set sofar to 0
		end if
		if (now ≥ delayEnd) then -- Only execute if this is the first call or the non-execution period's expired.
			tell application "System Events" to key code 126 -- (up arrow)
			set delayEnd to now + interval -- Set the expiry time for a non-execution period which begins now.
		end if
	end if
end runme



This is the proof of concept, you can view the result in a log window: The timings won’t be accurate, but what matters is really the size of the interval. The size of the intervals, shouldn’t deviate to much during run.

Below uses a different value for firing_per_sec and this script can be used for giving you an idea about calibration.

property interval : 12
-- hundreds of a sec
property delay_end : 0
property firing_per_sec : 50
property sofar : 0
property this_day : 0
# Two ways to scale this:
# if it is firing to seldom, that is, the keypresses are coming to slow
# then lower the number of firings per sec. Otherwise, increase.

repeat while true
	
	set sofar to sofar + (100 / firing_per_sec)
	log sofar
	set {now, cur_day} to {time, day} of (current date) # ) * 10 + sofar -- high granularity time
	if cur_day ≠ this_day then
		set sofar to 0
		set this_day to cur_day
	end if
	set now to now * 10 + sofar
	log now as integer
	if (now ≥ delay_end) then -- Only execute if this is the first call or the non-execution period's expired.
		log "Firing at " & (now as integer)
		set delay_end to now + interval -- Set the expiry time for a non-execution period which begins now.
	end if
end repeat

Edit
I have corrected the code/made it easier to adjust/use by removing the reset of the variable sofar.

Edit++
I have reset the sofar when the day number changes, as to not get integer overflow by the continuous increment of sofar variable, (if that can happen).

An integer in Applescript can hold values up to 536870911, which will hold for a day, as long as you don’t increase the counter with 1 more than 6214 times per second. (Which I find unlikely to happen in the near foreseeable future. :slight_smile:


set MIDImessage to {176, 16, 1} -- for testing in Applescript Editor
runme(MIDImessage) -- for testing in Applescript Editor

property IamBusy : 0
on runme(message)
	if (message begins with {176, 16, 1}) then
		if not IamBusy is 1 then
			set IamBusy to 1
			tell application "System Events" to key code 125
			delay 1
			set IamBusy to 0
		end if
	end if
end runme

Hmmm. So this works as expected OUTSIDE of MidiPipe. I set the script to launch on a key command in Key Maestro and it does indeed ignore any key commands I press while it’s executing.

Gonna look at the solutions now Nigel and McUsrII put up this morning.

property interval : 12
-- hundreds of a sec
property delay_end : 0
property firing_per_sec : 100
property sofar : 0
property this_day : 0
# Two ways to scale this:
# if it is firing to seldom, that is, the keypresses are coming to slow
# then lower the number of firings per sec. Otherwise, increase.

on runme(message)
   if (message begins with {176, 17, 65}) then
       set sofar to sofar + 1
       set {now, cur_day} to {time, day} of (current date)
       if cur_day ≠ this_day then
           set this_day to cur_day
           set sofar to 0
       end if
       if (now ≥ delayEnd) then -- Only execute if this is the first call or the non-execution period's expired.
           tell application "System Events" to key code 126 -- (up arrow)
           set delayEnd to now + interval -- Set the expiry time for a non-execution period which begins now.
       end if
   end if
end runme

I pasted this in MidiPipe and it compiles but no keystrokes are sent at all. I tried different intervals and firings per second, but no way to get this to trigger in MidiPipe.

And I’m sorry but I don’t understand the second version. I pasted it in the Script Editor and it runs but nothing happens. What am I missing?

Hello.

I’m sorry, a line got lost during edit:set now to now * 10 + sofar Here is a version that should be correct. (It made it to the proof of concept, I guess it made it to the script below as well) but got lost by mistake in a later edition. :confused:

property interval : 12
-- hundreds of a sec
property delay_end : 0
property firing_per_sec : 100
property sofar : 0
property this_day : 0
# Two ways to scale this:
# if it is firing to seldom, that is, the keypresses are coming to slow
# then lower the number of firings per sec. Otherwise, increase.

on runme(message)
	if (message begins with {176, 17, 65}) then
		set sofar to sofar + 1
		set {now, cur_day} to {time, day} of (current date)
		if cur_day ≠ this_day then
			set this_day to cur_day
			set sofar to 0
		end if
		set now to now * 10 + sofar
		if (now ≥ delayEnd) then -- Only execute if this is the first call or the non-execution period's expired.
			tell application "System Events" to key code 126 -- (up arrow)
			set delayEnd to now + interval -- Set the expiry time for a non-execution period which begins now.
		end if
	end if
end runme

property interval : 500 -- 500 ms (half a second).
property delayEnd : 0

on runme(message)
   if (message begins with {176, 16, 1}) then
       set now to (get msec) -- LapTime OSAX.
       if (now ≥ delayEnd) then -- Only execute if this is the first call or the non-execution period's expired.
           tell application "System Events" to key code 125 -- (down arrow)
           set delayEnd to now + interval -- Set the expiry time for a non-execution period which begins now.
       end if
   end if
end runme

Thank you Nigel! Installed Lap Time and this works. I can tweak in milliseconds and there’s no buffer of strokes when I stop turning.

(though I don’t understand why this method blocks/ignores messages and mine doesn’t)

property IamBusy : 0
on runme(message)
   if (message begins with {176, 16, 1}) then
       if not IamBusy is 1 then
           set IamBusy to 1
           tell application "System Events" to key code 125
           delay 0.5
           set IamBusy to 0
       end if
   end if
end runme

Aren’t I telling the next Midi message to end if the property is set to 1?
It delays but obviously every message tries to tell “system events”.

I don’t get it.

Hi Bobby.

The ‘delay’ command only makes the script pause for the specified time. It doesn’t stop codes from being sent to its input and processed. So the trigger codes will still all cause keystrokes, but there’ll be a minimum of the specified time between the keystrokes being added to the buffer.

My method ignores triggers which occur within the specified time after the generation of a keystroke.

It is not the delay command that I’m expecting to shunt the script.

It’s the line “if not IamBusy is 1 then” as I read it, should be ignoring messages.

If the property in this script is temporarily set to 1 (while it’s running, and consequently delaying) I don’t see why it’s not just jumping to “end if” and “end run me”.

Hello.

Try this, with a slightly simpler logic expression instead. I can’t give you any reason for why it should work better, but please do try it.

Edit

The reason this may work, is that your logic clause may be ambiguous; I am not sure whether the compiler would interpret it as (not IamBusy) is 1 or if it would interpret it as not (IamBusy is 1), I removed the double negation all together, as it is difficult for humans to sort out, as the compiler.

A good alternative would be to just state: if not IamBusy.

And by that, I’ll return to my occupation. :slight_smile:

property IamBusy : 0
on runme(message)
	if (message begins with {176, 16, 1}) then
		if IamBusy is 0 then
			set IamBusy to 1
			tell application "System Events" to key code 125
			delay 0.5
			set IamBusy to 0
		end if
	end if
end runme

Ps. I am coming back to this thread with a little shell script tool, that returns the number of tenth’s of seconds since midnight, since that is the resolution of seconds that a human can notice.

There’s only one instance of the script, which contains just one call to the handler. The script receives each message in turn from the pipe and calls the handler to deal with it. Your handler, when a message has a certain value, sets IamBusy to 1, outputs a key code, pauses for half a second, then sets IamBusy to 0 and exits. Only then is the script ready to receive the next message from the queue in the pipe. So IamBusy is never 1 when the handler’s called and trigger messages are never ignored.

Nope, same effect. Delayed keystrokes, piled in a buffer. Though I read it the same way. It should not try to talk to “System Events” while this Property is set to 1.

But all the messages wait in line.

How is Nigel’s “if (now ≥ delayEnd) then” different than “if IamBusy is 0” or “if NOT IamBusy is 1”?

The only reason I can think of is the Property is not being set. Nigel did say it’s not a proper “script” although Nico, the developer, suggests I can use a Property.

It has to be like Nigel describes, I guess if my and Wilcofan’s faulty understanding was correct, we would have come no further, for if because a new event with the same script had been triggered, then no property would probably have been set anyway.

Edit
As it is, the events are executed sequentially on a single thread, and no next event can necessarily be processed before the former is finished, and hence the queueing up of events to be processed, all starting with IamBusy set to 0.

Hello.

Having understood the problem fully, that we are acting on synchronous events I think this should work: at least after some adjustments of the ToDrop property, which signifies the number of events we are to chew up before we find a new to act on.

property IamBusy : 0
property Ctr : 0
property ToDrop : 10
# you may need to adjust this event  ToDrop count.
on runme(message)
	if (message begins with {176, 16, 1}) then
		if IamBusy is 0 then
			# We'll act on this event
			set IamBusy to 1
			tell application "System Events" to key code 125
		else if Ctr = ToDrop then
			# We are done chewing up events, we reset the properties.
			set IamBusy to 0
			set Ctr to 0
		else
			set Ctr to Ctr + 1
			# we have acted on one event, and are chewing up
			# the others that we are not acting upon
		end if
	end if
end runme

I still thinks that Nigel’s solution may be the best, since if the events are generated from some analog device, then the accelleration of the knob, may inflict upon the number of events sent. (Faster turning, → more events.) So time may prove more stable than the mere number of events.

I’ll be back later as I said earlier, with a commandline alternative, to the osax.

Hello I created a command line utility to return the seconds in tenths since midnight. It can be downloaded fromhere

Implemented in the applescript it will look something like this:
EditPlease observe that the test is expanded, so that we also take care of the case of passing midnight, because then now will become lesser than delay end for a long long time, possibly perpetually.

property interval_in_10s : 1
property delay_end : 0
on runme(message)
	if (message begins with {176, 17, 65}) then
		set now to do shell script "tenths"
		
		if (now > delay_end or (delay_end - now > interval_in_10s)) then
			-- Only execute if this is the first call or the non-execution period's expired.
			-- or we have passed midnight, then we have to start afresh!
			tell application "System Events" to key code 126 -- (up arrow)
			set delay_end to now + interval_in_10s -- Set the expiry time for a non-execution period which begins now.
		else if {} then
		end if
	end if
end runme


Edit

This will work exactly the same way, as if you had a shell script with just echo in it if much else doesn’t happen in your script and the do shell script is within a tight loop; it will make your machine hang.
There is no way to fix this, if the utility is to be precise by tenths of a second, and that is its purpose.

Nigel,

Just throwin’ it out there… is there a way to make the delay happen before the strokes, not after?

(Meaning the first stroke will be delayed by the same amount)

It’s sorta doable:

-- This script requires LapTime OSAX by Tetsuro KURITA.
-- <http://www.script-factory.net/software/OSAX/LapTime/en/index.html>

property triggerMessage : {176, 17, 65}
property bypassInterval : 500 -- 500 ms (half a second).
property bypassEndTime : 0
property bypassOn : false

on runme(message)
	set isTriggerMessage to (message begins with triggerMessage) -- '(message is triggerMessage)' is probably OK.
	if ((isTriggerMessage) or (bypassOn)) then
		set now to (get msec) -- LapTime OSAX.
		if (now ≥ bypassEndTime) then
			if (bypassOn) then
				tell application "System Events" to key code 126 -- (up arrow)
				set bypassOn to false
			end if
			if (isTriggerMessage) then
				set bypassEndTime to now + bypassInterval
				set bypassOn to true
			end if
		end if
	end if
end runme

There’s a weakness here in that, if the message stream ends during a bypass period, the last triggered key code won’t be sent and ‘bypassOn’ will be left at ‘true’ ” which will cause a key code to be sent on the very first message in the next MIDI stream. If you know of another MIDI message guaranteed to be sent uniquely at either the beginning or the end of each stream, this could be used to trigger the resetting of ‘bypassOn’.

I’ve settled on a workflow with Nigel’s LapTime OSAX thing and it has really done wonders. Thanks so much for the time and efforts put in here. I’ve got a new tool and a better understanding of the scripting.

See you around the forums.

Bobby