├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── main.go └── sounds └── default ├── down1.mp3 ├── down2.mp3 ├── down3.mp3 ├── down4.mp3 ├── down5.mp3 ├── down6.mp3 ├── down7.mp3 ├── down_enter.mp3 ├── down_mouse.mp3 ├── down_space.mp3 ├── enter.mp3 ├── key1.mp3 ├── key2.mp3 ├── key3.mp3 ├── key4.mp3 ├── space.mp3 ├── up1.mp3 ├── up2.mp3 ├── up3.mp3 ├── up4.mp3 ├── up5.mp3 ├── up6.mp3 ├── up7.mp3 ├── up_enter.mp3 ├── up_mouse.mp3 └── up_space.mp3 /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Web Dev Cody 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a work in progress 2 | 3 | # Virtualized Keyboard Switches 4 | 5 | ## How to Run 6 | 7 | 1. `go mod tidy` 8 | 2. `go run main.go` 9 | 10 | ## Planned Features 11 | 12 | - add more keyboard sounds 13 | - add more mouse sounds 14 | - ability to customize click sounds 15 | 16 | ## Adding Sounds 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/webdevcody/key-party 2 | 3 | go 1.22.5 4 | 5 | require ( 6 | github.com/hajimehoshi/go-mp3 v0.3.4 7 | github.com/hajimehoshi/oto v1.0.1 8 | github.com/robotn/gohook v0.41.0 9 | ) 10 | 11 | require ( 12 | github.com/vcaesar/keycode v0.10.1 // indirect 13 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect 14 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 // indirect 15 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect 16 | golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68= 2 | github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo= 3 | github.com/hajimehoshi/oto v1.0.1 h1:8AMnq0Yr2YmzaiqTg/k1Yzd6IygUGk2we9nmjgbgPn4= 4 | github.com/hajimehoshi/oto v1.0.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos= 5 | github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo= 6 | github.com/robotn/gohook v0.41.0 h1:h1vK3w/UQpq0YkIiGnxm9Awv85W54esL0/NUYGueggo= 7 | github.com/robotn/gohook v0.41.0/go.mod h1:FedpuAkVqzM5t67L5fcf3hSSCUDO9cM5YkWCw1U+nuc= 8 | github.com/vcaesar/keycode v0.10.1 h1:0DesGmMAPWpYTCYddOFiCMKCDKgNnwiQa2QXindVUHw= 9 | github.com/vcaesar/keycode v0.10.1/go.mod h1:JNlY7xbKsh+LAGfY2j4M3znVrGEm5W1R8s/Uv6BJcfQ= 10 | github.com/vcaesar/tt v0.20.0 h1:9t2Ycb9RNHcP0WgQgIaRKJBB+FrRdejuaL6uWIHuoBA= 11 | github.com/vcaesar/tt v0.20.0/go.mod h1:GHPxQYhn+7OgKakRusH7KJ0M5MhywoeLb8Fcffs/Gtg= 12 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg= 13 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 14 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0= 15 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 16 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc= 17 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 18 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 19 | golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 20 | golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e h1:NHvCuwuS43lGnYhten69ZWqi2QOj/CiDNcKbVqwVoew= 21 | golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 22 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 23 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "math/rand" 9 | "os" 10 | "sync" 11 | 12 | "github.com/hajimehoshi/go-mp3" 13 | "github.com/hajimehoshi/oto" 14 | hook "github.com/robotn/gohook" 15 | ) 16 | 17 | var keyboard string = "default" 18 | var ENTER uint16 = 36 19 | var SPACE uint16 = 49 20 | 21 | var sounds map[string][]byte = make(map[string][]byte) 22 | 23 | var keyMap = make(map[uint16]bool) 24 | 25 | func loadSoundsForKeyboard(keyboard string) { 26 | keys := []string{"down1", "up1", "down2", "up2", "down3", "up3", "down4", "up4", "down5", "up5", "down6", "up6", "down7", "up7", "down_space", "up_space", "down_enter", "up_enter", "up_mouse", "down_mouse"} 27 | for _, key := range keys { 28 | loadSound(keyboard, key) 29 | } 30 | } 31 | 32 | func loadSound(keyboard string, soundName string) { 33 | soundFile, err := os.Open(fmt.Sprintf("sounds/%s/%s.mp3", keyboard, soundName)) 34 | 35 | if err != nil { 36 | log.Fatalf("failed to open sound file: %v", err) 37 | } 38 | defer soundFile.Close() 39 | 40 | sound, err := io.ReadAll(soundFile) 41 | if err != nil { 42 | log.Fatalf("failed to read sound file: %v", err) 43 | } 44 | 45 | sounds[soundName] = sound 46 | } 47 | 48 | func getRandomUpKey() string { 49 | keys := []string{"down1", "down2", "down3", "down4", "down5", "down6", "down7"} 50 | key := keys[rand.Intn(len(keys))] 51 | return key 52 | } 53 | 54 | func getRandomDownKey() string { 55 | keys := []string{"up1", "up2", "up3", "up4", "up5", "up6", "up7"} 56 | key := keys[rand.Intn(len(keys))] 57 | return key 58 | } 59 | 60 | func main() { 61 | 62 | if len(os.Args) > 1 { 63 | keyboard = os.Args[1] 64 | } 65 | 66 | if _, err := os.Stat(fmt.Sprintf("sounds/%s", keyboard)); os.IsNotExist(err) { 67 | log.Fatalf("Keyboard sounds not found: %s", keyboard) 68 | } 69 | 70 | fmt.Printf("Using keyboard: %s\n", keyboard) 71 | 72 | loadSoundsForKeyboard(keyboard) 73 | 74 | // Create an Oto context (for audio playback) 75 | context, err := oto.NewContext(48000, 2, 2, 8192) 76 | if err != nil { 77 | log.Fatalf("failed to create Oto context: %v", err) 78 | } 79 | defer context.Close() 80 | 81 | // Function to play sound in a goroutine 82 | playSound := func(key string) { 83 | // Create an MP3 decoder 84 | decoder, err := mp3.NewDecoder(bytes.NewReader(sounds[key])) 85 | if err != nil { 86 | log.Fatalf("failed to create MP3 decoder: %v", err) 87 | } 88 | 89 | player := context.NewPlayer() 90 | defer player.Close() 91 | 92 | // Reset the decoder (so it plays from the beginning) 93 | decoder.Seek(0, 0) 94 | 95 | // Create a buffer to read the decoded audio 96 | buf := make([]byte, 8192) 97 | for { 98 | n, err := decoder.Read(buf) 99 | if err != nil && err != io.EOF { 100 | log.Printf("failed to read decoded audio: %v", err) 101 | break 102 | } 103 | if n == 0 { 104 | break 105 | } 106 | player.Write(buf[:n]) 107 | } 108 | } 109 | 110 | var wg sync.WaitGroup 111 | 112 | scheduleSound := func(key string) { 113 | // useful to help debug sounds 114 | // fmt.Printf("Playing sound: %s\n", key) 115 | wg.Add(1) 116 | go func() { 117 | defer wg.Done() 118 | playSound(key) 119 | }() 120 | } 121 | 122 | hook.Register(hook.KeyDown, []string{"A-Z a-z 0-9"}, func(e hook.Event) { 123 | 124 | if keyMap[e.Rawcode] { 125 | return 126 | } 127 | keyMap[e.Rawcode] = true 128 | 129 | if e.Rawcode == ENTER { 130 | scheduleSound("down_enter") 131 | } else if e.Rawcode == SPACE { 132 | scheduleSound("down_space") 133 | } else { 134 | scheduleSound(getRandomDownKey()) 135 | } 136 | }) 137 | 138 | hook.Register(hook.KeyUp, []string{"A-Z a-z 0-9"}, func(e hook.Event) { 139 | if !keyMap[e.Rawcode] { 140 | return 141 | } 142 | keyMap[e.Rawcode] = false 143 | 144 | if e.Rawcode == ENTER { 145 | scheduleSound("up_enter") 146 | } else if e.Rawcode == SPACE { 147 | scheduleSound("up_space") 148 | } else { 149 | scheduleSound(getRandomUpKey()) 150 | } 151 | }) 152 | 153 | // TODO: figure out why this doesn't trigger until the first key is pressed 154 | hook.Register(hook.MouseHold, []string{"mleft"}, func(e hook.Event) { 155 | if keyMap[e.Button] { 156 | return 157 | } 158 | keyMap[e.Button] = true 159 | scheduleSound("down_mouse") 160 | }) 161 | 162 | hook.Register(hook.MouseDown, []string{"mleft"}, func(e hook.Event) { 163 | if keyMap[e.Button] { 164 | scheduleSound("up_mouse") 165 | keyMap[e.Button] = false 166 | return 167 | } 168 | }) 169 | 170 | hook.Register(hook.MouseUp, []string{"mleft"}, func(e hook.Event) { 171 | if !keyMap[e.Button] { 172 | return 173 | } 174 | keyMap[e.Button] = false 175 | scheduleSound("up_mouse") 176 | }) 177 | 178 | s := hook.Start() 179 | <-hook.Process(s) 180 | wg.Wait() 181 | } 182 | -------------------------------------------------------------------------------- /sounds/default/down1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/down1.mp3 -------------------------------------------------------------------------------- /sounds/default/down2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/down2.mp3 -------------------------------------------------------------------------------- /sounds/default/down3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/down3.mp3 -------------------------------------------------------------------------------- /sounds/default/down4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/down4.mp3 -------------------------------------------------------------------------------- /sounds/default/down5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/down5.mp3 -------------------------------------------------------------------------------- /sounds/default/down6.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/down6.mp3 -------------------------------------------------------------------------------- /sounds/default/down7.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/down7.mp3 -------------------------------------------------------------------------------- /sounds/default/down_enter.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/down_enter.mp3 -------------------------------------------------------------------------------- /sounds/default/down_mouse.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/down_mouse.mp3 -------------------------------------------------------------------------------- /sounds/default/down_space.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/down_space.mp3 -------------------------------------------------------------------------------- /sounds/default/enter.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/enter.mp3 -------------------------------------------------------------------------------- /sounds/default/key1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/key1.mp3 -------------------------------------------------------------------------------- /sounds/default/key2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/key2.mp3 -------------------------------------------------------------------------------- /sounds/default/key3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/key3.mp3 -------------------------------------------------------------------------------- /sounds/default/key4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/key4.mp3 -------------------------------------------------------------------------------- /sounds/default/space.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/space.mp3 -------------------------------------------------------------------------------- /sounds/default/up1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/up1.mp3 -------------------------------------------------------------------------------- /sounds/default/up2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/up2.mp3 -------------------------------------------------------------------------------- /sounds/default/up3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/up3.mp3 -------------------------------------------------------------------------------- /sounds/default/up4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/up4.mp3 -------------------------------------------------------------------------------- /sounds/default/up5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/up5.mp3 -------------------------------------------------------------------------------- /sounds/default/up6.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/up6.mp3 -------------------------------------------------------------------------------- /sounds/default/up7.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/up7.mp3 -------------------------------------------------------------------------------- /sounds/default/up_enter.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/up_enter.mp3 -------------------------------------------------------------------------------- /sounds/default/up_mouse.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/up_mouse.mp3 -------------------------------------------------------------------------------- /sounds/default/up_space.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/type-joy/e52e829916b7e1505441e0a6b904152d3368659d/sounds/default/up_space.mp3 --------------------------------------------------------------------------------