Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

Chording keyboard: strings sent at key combinations


  • Please log in to reply
28 replies to this topic
Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
Sending strings at pressing two or more keyboard keys simultaneously would greatly increase the number of characters/text blocks one can type. Diacritical letters or longer strings, like addresses or names could be assigned to key combinations. The following script recognizes multiple keys (letters only) pressed in the same time (like s,d,f) and sends a special character or an arbitrary string to the active window.

In the variable "keys" we store the key names to be recognized. Only lower case letters can be listed. A parse loop generates hotkeys for each key in "keys", and its shifted (capital) version, and also hotkeys activated at the release of these keys. The "down" hotkeys record the key name in a variable named after the key (like a = A) and also the current time in "KeyTick", as the tick count since the PC was switched on. The "up" hotkeys clear the corresponding variables. This way modified keys (Ctrl-, Alt-...) are not affected.

The values of these variables are used in the timer subroutine "Action", activated in every 15 ms.
- After a key is pressed, the subroutine waits a little (30 ms), to allow further keystrokes to be registered as key combinations.
- When the key combination pressed does not change for 30 ms, the substitute is sent. These are selected in a long else-if sequence. If a key is pressed alone, we send it unchanged, so the normal keyboard behavior is not altered.
- After a substitute is sent, there is a blackout interval (360 ms) for the same key combination, to prevent too fast auto-repeating.
- After an already auto-repeated key the substitute is sent repeatedly in shorter period (50 ms).

This is the basic function, the keys and the substitutes for key combinations need to be defined according to your needs. A little more code is needed for non-letter keys, like ";". Because of the physical design of keyboards, some key combinations are not recognized, or the keys have to be pressed in a certain order to be seen as key combinations, so some experimenting is necessary.
StringCaseSense On
Process Priority,,High
#MaxHotkeysPerInterval 999
#UseHook
SetKeyDelay -1

SentTick = 0
keys = asdfghjkl
Loop Parse, keys
{
   AllKeys = %AllKeys%`%%A_LoopField%`%
   HotKey  %A_LoopField%,  KeyDown
   HotKey +%A_LoopField%, CKeyDown
   HotKey  %A_LoopField% up, KeyUp
   HotKey +%A_LoopField% up,CKeyUp
}
SetTimer Action, 15
Return

KeyDown:
   KeyTick = %A_TickCount%
   %A_ThisHotKey% = %A_ThisHotKey%
Return

CKeyDown:
   KeyTick = %A_TickCount%
   StringRight key, A_ThisHotKey, 1
   StringUpper key, Key
   %key% = %key%
Return

KeyUp:
   StringLeft key, A_ThisHotKey, 1
   %key% =
Return

CKeyUp:
   StringMid key, A_ThisHotKey, 2, 1
   %key% =
Return

Action:
   Transform keys, Deref, %AllKeys%
   If (A_TickCount - KeyTick  < 30                    ; wait for 2nd key
    or A_TickCount - SentTick < 360 and key1 <> keys  ; first auto-repeat
    or A_TickCount - SentTick < 50  and key1 == keys) ; auto-repeat
       Return
   key1 = %key0%
   key0 = %keys%
   SentTick = %A_TickCount%
   If StrLen(keys) = 1
      Send %keys%
   Else IfEqual keys,as, Send z
   Else IfEqual keys,AS, Send As I have said
   Else IfEqual keys,sd, Send x
   Else IfEqual keys,df, Send c
   Else IfEqual keys,sdf,Send self defense
   Else SetEnv SentTick, 0    ; when no key sent
Return
Below is a more complicated version of the chording keyboard script, which can handle numbers and special characters, like `, %, ;. To avoid naming problems, each key has an associated variable, x, where stands for the decimal ANSI code of the key. We have to extend the upper case conversion to handle special shifted characters, which are now taken from a character list from the same position as the un-shifted variant is in its own list. With two constants
Low = ``1234567890-=qwertyuiop[]\asdfghjkl`;'zxcvbnm,./
Shft= ~!@#$`%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?
a single instruction does the conversion of key "k" to its shifted variant, put in "U".
StringMid U,Shft,InStr(Low,k,1),1

To improve responsiveness the hotkey subroutines below do not return, and wait for the timer to activate Action, but directly jump to Action. This way practically no keys are lost, even if they were pressed briefly. (Very rarely they still can be lost, if the system was busy and could not trigger the corresponding hotkey.)

Pressing more keys never happens in exactly the same time, so we have to leave a small time-window, where different keystrokes are considered simultaneous. If two keys are pressed quickly one after the other, the script cannot tell if they were meant as a key combination or two separate keystrokes. To minimize this problem, if there is no substitution defined for a key combination, the last key is sent unchanged, assuming the preceding key was hit accidentally. In addition, if one key is not yet released, while the next is stricken, we see an unwanted key combination for a short time. This event is caught, and the same action is triggered as when the first key was released before the next press.

Design goals:
- Key combinations send replacement strings (or trigger other actions, like launch programs)
- Keys have to be pressed together, within a short tolerance, in any order
- Auto repeat should function normally even with substitutions (needed for special characters)
- No other timing requirements (e.g. hold down or wait between keys for activation)
- Modified (Ctrl, Alt, Win) keys should not be disturbed
- As short script as possible

Rules:
- Any key combination physically distinguished is registered, a change is time stamped
- No replacement is sent for T0 time (20..40ms)
- A single key released within T0 (short press), still triggers a replacement (at key up)
- A key combination sends its replacement after T0 time
- After T1 (150..400ms) a second replacement is sent
- After further T2 time periods (40..100ms) further replacements are periodically sent, until one of the keys of the combination is released
- If there is no replacement defined, the last pressed key is sent (accidental key press)
- At slow releasing keys of combinations (decreasing key-combination lengths) no action is taken
- Event sequences key1-key2-key1up-key2up in rapid successions are treated as key1-key1up, key2-key2up

Limitations:
- If there are larger time differences between key presses, they might not be recognized as combinations, but as separate keystrokes (that is, the script cannot read you mind).
- Characters are recognized and sent by the interpreted script, not by low-level drivers, therefore, more system resources are used and the reaction time is dependent on other activities in the system.
- In rare occasions, at very fast typing, key-up events could be registered earlier than the key-down. If this happens, a key may remain registered as pressed down and repeat indefinitely. Pressing the key again stops the rapid fire.

Self-healing:
To prevent these repeat cycles we check each key in a lower priority loop if it is physically pressed down. If not, we clear the record.

ToDo:
- Replacement rules are read from an ini file
- On-line additions, changing of replacement rules, save/load rules
- Arbitrary actions triggered by key combinations

Other approaches:
There are other approaches to the problem, too. For example, key combinations could be activated with one of their keys pressed down for a certain period of time, followed by the other keys of the combination, or certain other patterns in key timings. In practice, these turned out to be unusable without long training, because normal typing has varying speed and key press times: some letters we type fast after each other, others have a short lag, enough to activate unwanted replacements. Furthermore, for auto-repeated keys we keep some keys down for a while, which have to be distinguished from the replacement triggers. This makes the activation timing sensitive, and our typing full of surprises. After experimenting with different schemes, the method described here proved to be the least obtrusive.
#MaxThreadsPerHotkey 10
#MaxThreadsBuffer ON
#MaxHotkeysPerInterval 999
#UseHook
StringCaseSense On
Process Priority,,Realtime
SetKeyDelay -1
BlockInput Send

Low = ``1234567890-=qwertyuiop[]\asdfghjkl`;'zxcvbnm,./
Shft= ~!@#$`%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?

KeySet = ``1234567890-=qwertyuiop[]\asdfghjkl`;'zxcvbnm,./
Loop Parse, KeySet
{
   AllKeys := AllKeys "%x" Asc(A_LoopField) "%"
   HotKey  %A_LoopField%,  KeyDown, B
   HotKey  %A_LoopField% up, KeyUp, B
   HotKey +%A_LoopField%, CKeyDown, B
   HotKey +%A_LoopField% up, KeyUp, B
}
SentTick = 0
SetTimer Action, 10                    ; handle key repeat
RI = 0
Loop                                   ; self healing missed key releases
{                                      ; to prevent infinite repeat
   Sleep 10
   RI := Mod(RI, StrLen(KeySet)) + 1
   StringMid r, KeySet, RI, 1
   If GetKeyState(r,"P")
      Continue
   r := "x" Asc(r)
   %r% =
}
Return

KeyDown:                               ; register keys pressed
   key := "x" Asc(A_ThisHotKey)
   %key% = %A_ThisHotKey%
GoTo Tick

CKeyDown:                              ; register shifted keys pressed
   StringRight k, A_ThisHotKey, 1      ; remove "+"
   StringMid U,Shft,InStr(Low,k,1),1   ; convert k to Shift-%k%
   key := "x" Asc(k)
   %key% = %U%
GoTo Tick

KeyUp:                                 ; register key release
   StringReplace k, A_ThisHotKey, +    ; remove "+"
   key := "x" Asc(k)
   %key% =

Tick:                                  ; register time of key event
   KeyTick = %A_TickCount%
Action:
   Transform keys, Deref, %AllKeys%
   IfNotEqual keys, %key0%, {          ; KEYS CHANGED keys <> key0
      If (keyn = 0 and len0 = 1 and len1 = 0 and keys = "")
         GoSub SENDX                   ; short single key press
      Else If (keyn = 1 and len0 = 1 and len1 = 2 and keys = "" and SentKeys <> key0 and StrLen(SentKeys) = 1)
         GoSub SENDX                   ; overlapping keys
      len1:= StrLen(key0)
      len0:= StrLen(keys)
      key0 = %keys%                    ; previous key combination
      keyn = 0                         ; the number of repetitions
   }
   Else {                              ; NO KEY CHANGE keys == key0
      if (keys = ""
       or A_TickCount - KeyTick  < 40  and keyn = 0
       or A_TickCount - SentTick < 360 and keyn = 1
       or A_TickCount - SentTick < 60  and keyn > 1)
         Return
      keyn++
      GoTo SEND
   }
Return

SEND:
   IfLess len0,%len1%, Return          ; no send at releasing keys
SENDX:
   SentTick = %A_TickCount%            ; remember time of send
   SentKeys = %key0%
   If StrLen(key0) = 1
      Send {%key0%}                    ; single keys unchanged
   Else IfEqual key0,as, Send z        ; examples ... edit here
   Else IfEqual key0,AS, Send As I have said
   Else IfEqual key0,sd, Send x
   Else IfEqual key0,df, Send c
   Else IfEqual key0,sdf,Send self defense
   Else IfEqual key0,wo, Send without
   Else IfEqual key0,[], Send in the box
   Else IfEqual key0,tc, Send [TC]
   Else {
      L := %key%
      SendRaw %L%                      ; send last pressed key
     ;SentTick = 0xFFFFFFFF            ; uncomment for no auto repeat undefined combos
   }
Return

Edit 2005.12.18: more complex variant added.
Edit 2005.12.24: New version with practically no skip, and with self-healing

Chordful
  • Guests
  • Last active:
  • Joined: --
Thanks Laszlo. I'm testing this code and it is working. When doing regular typing, sometimes the asdfghjkl keys skip. This happens when I'm typing rather fast. I think we need to find tune the timing numbers, so I'll be fiddling with those numbers.

The code you wrote basically helps what I'm trying to do.

Thanks,

Something you want for Xmas?

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005

Something you want for Xmas?

Yes. A long outstanding wish: a way to get samples to an AHK script from the microphone or a webcam, without running a third party program, or at worst, just running a very simple one. Anyone?

Harrie
  • Members
  • 41 posts
  • Last active: Dec 13 2007 04:25 PM
  • Joined: 04 Aug 2005
That's quite amazing! This deserves a big, big workout! I will give it one and report back. That's a solid promise.

What a boon for all the pain in the neck, small phrases, like "and a" - "who was" - "when she".

Harrie
  • Members
  • 41 posts
  • Last active: Dec 13 2007 04:25 PM
  • Joined: 04 Aug 2005
Why does this alteration mess it all up, or, should all letter keys be on the same line?

StringCaseSense On 
Process Priority,,High 
#MaxHotkeysPerInterval 999 
#UseHook 
SetKeyDelay -1 

SentTick = 0 
keys = asdfghjkl 
keys = zxcvbnm
keys = qwertyuiop
keys = 1234567890
Loop Parse, keys 
{ 
   AllKeys = %AllKeys%`%%A_LoopField%`% 
   HotKey  %A_LoopField%,  KeyDown 
   HotKey +%A_LoopField%, CKeyDown 
   HotKey  %A_LoopField% up, KeyUp 
   HotKey +%A_LoopField% up,CKeyUp 
} 
SetTimer Action, 15 
Return 

KeyDown: 
   KeyTick = %A_TickCount% 
   %A_ThisHotKey% = %A_ThisHotKey% 
Return 

CKeyDown: 
   KeyTick = %A_TickCount% 
   StringRight key, A_ThisHotKey, 1 
   StringUpper key, Key 
   %key% = %key% 
Return
 

KeyUp: 
   StringLeft key, A_ThisHotKey, 1 
   %key% = 
Return 

CKeyUp: 
   StringMid key, A_ThisHotKey, 2, 1 
   %key% = 
Return 

Action: 
   Transform keys, Deref, %AllKeys% 
   If (A_TickCount - KeyTick  < 30                    ; wait for 2nd key 
    or A_TickCount - SentTick < 360 and key1 <> keys  ; first auto-repeat 
    or A_TickCount - SentTick < 50  and key1 == keys) ; auto-repeat 
       Return 
   key1 = %key0% 
   key0 = %keys% 
   SentTick = %A_TickCount% 
   If StrLen(keys) = 1 
      Send %keys% 
   Else IfEqual keys,as, Send the last 
   Else IfEqual keys,sd, Send when he 
   Else IfEqual keys,df, Send and a
   Else IfEqual keys,sdf, Send she did find 
   Else IfEqual keys,tpi, Send the patient is
   Else IfEqual keys,tpw, Send the patient was	
   Else IfEqual keys,wo, Send without 
   Else IfEqual keys,tc, Send the child
   Else SetEnv SentTick, 0    ; when no key sent 
Return

(Edited to put the code in proper forum style)

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
You are overwriting the previous values of "keys". Try
keys = asdfghjkl 

keys = zxcvbnm%keys% 

keys = qwertyuiop%keys% 

keys = 1234567890%keys%


Harrie
  • Members
  • 41 posts
  • Last active: Dec 13 2007 04:25 PM
  • Joined: 04 Aug 2005
Thanks, Laszlo. No, now I am getting just a continuous string of "zero" numbers sent in Word. I did reload, etc. Perhaps it's because of the Send phrases I put in at the end. It is obvious and plain to see that I don't understand AHK language, of course! Thank you very much for the reply.

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
The problem is in the script: it does not handle numbers well. I fix it an post it in ah hour. In the mean time, you could play with the script with only letters in "keys".

Harrie
  • Members
  • 41 posts
  • Last active: Dec 13 2007 04:25 PM
  • Joined: 04 Aug 2005
Cheers, Laszlo. No rush, either.

For now, I've gone back to the original and I'm going to try working for an hour with just using four I have set up. If you give a two-key combination a three or four word phrase (or longer), that's going to beat all else that I've ever tried, no question about it.

Harrie
  • Members
  • 41 posts
  • Last active: Dec 13 2007 04:25 PM
  • Joined: 04 Aug 2005

I'm testing this code and it is working. When doing regular typing, sometimes the asdfghjkl keys skip. This happens when I'm typing rather fast. I think we need to find tune the timing numbers, so I'll be fiddling with those numbers.


@Chordful and Laszlo: I agree with that assessment. I noticed it particularly when typing a word that ended in "al." As in axial, or lumbosacral. Got slow on those two and wanted to skip the L.

I will watch for the fine-tuning by you two! :D

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
I posted a version in the original place, which is more complex (handles non-letter characters), but also slower, so there could be more keys lost if you type too fast. Please experiment, and post any improvement you find!

Harrie
  • Members
  • 41 posts
  • Last active: Dec 13 2007 04:25 PM
  • Joined: 04 Aug 2005
Experimentation coming up, all day tomorrow. Thanks for the opportunity!

chordfull
  • Guests
  • Last active:
  • Joined: --
Laszlo, I think this coding has amazing possibilities!. You could start typing very nice phrases with simple key chord patterns.

I'm also trying to remap some of the keys in addition to this chording application. But I'm having difficulty. If say I want to remap a,b,c for 1,2,3 ; I tried coding a::send1, b::send2, c::send3. If I place it at the top of my coding lists, your chroding code doesn't kick in. If I place it at the bottom, it doesn't take. So the remapping must somehow be embedded in your chord coding. How would you do it.

I played a little bit with the timer. If I bring the blackout interval down to 125 from 360, and keypress subroutine down to 20 from 30, already autorepeat down to 20 from 50; I get virtually no skipping keys. But my keyboard is more sensitive to mistyping if I leave my finger on a single key for a wee bit too long. I still need to tweak a bit more to find the right balance between a trigger happy keyboard and a misfiring keyboard.

You sure you don't want something for Xmas?
Your request for a mike or cam enabled AHK sampler makes no sense to me. You mean you say a "hotkey" word in the mike and a script would execute on your PC? Or you wink your right eye into the webcam, and your PC would kick into gear, turn on the light, start the coffee, run the hot water. Maybe I don't want to ask what if you wink your left eye.

Harrie
  • Members
  • 41 posts
  • Last active: Dec 13 2007 04:25 PM
  • Joined: 04 Aug 2005
The amazing possibilities are there, all right.

Just wanted to "report." I tried the newer one, Laszlo, that you said would slow it down more, and I found that to be the case. In fact, it was a lot slower and I found I couldn't work with it. This was in my word processor (Word) and in dialog boxes. It hampered using my regular expander with it because of the increased slowness, which I didn't notice with the first script. I can see from yours and chordfull's comments that tweaking is definitely in order. I didn't have time to do that today, but look forward to it, especially noting chordfull's remark about getting virtually no skipping keys with changing the blackout interval.

Other findings for interpretation (yours, not mine): :twisted: I made several combinations but certain ones would not work. For instance, tc, wo. But if I make sure the keys are all right next to each other, from right to left, they do. Just like in your original examples. So, gh will work, and so will kl, fg, iop, etc. Is this the intention, or not necessarily?

The possibilities are killing me, too! I can picture a big increase in speed with this, if fiddling is successful.

Edit: One more thing. Just tried changing the numbers to the same values mentioned by you, chordfull. That makes my typing go awry, LOL. Crazy mistypings, extra letters. I guess I will fool around with those numbers, too, and see what is best.

MarshaG
  • Guests
  • Last active:
  • Joined: --
This sounds similar to stenography - pressing several keys simultaneously (except there are more keys on a regular keyboard then a stenography keyboard). If the timing gets tweaked, is there a possibility to add a way to add in the abbreviations in a quick and easy fashion? This could be so powerful!!!

MarshaG.