├── traktor_s4_mk1_midify ├── __init__.py ├── midi-alsa-control-map.csv ├── evcode-type-map.csv ├── midi-evcode-map-mixer-effect.csv ├── midi-evcode-map-deck.csv └── midify.py ├── .flake8 ├── .gitignore ├── doc ├── mappings.ods ├── Traktor_S4_default_mappings.png └── amixer_controls.tsv ├── pyproject.toml ├── LICENSE └── README.md /traktor_s4_mk1_midify/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | traktor_s4_mk1_midify.egg-info/ 4 | env/ 5 | -------------------------------------------------------------------------------- /doc/mappings.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blaxpot/traktor-s4-mk1-midify/HEAD/doc/mappings.ods -------------------------------------------------------------------------------- /doc/Traktor_S4_default_mappings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blaxpot/traktor-s4-mk1-midify/HEAD/doc/Traktor_S4_default_mappings.png -------------------------------------------------------------------------------- /traktor_s4_mk1_midify/midi-alsa-control-map.csv: -------------------------------------------------------------------------------- 1 | 0x01,74,118,74,118,- 2 | 0x05,83,127,83,127,9 3 | 0x06,82,126,82,126,13 4 | 0x07,78,122,78,122,8 5 | 0x08,79,123,79,123,12 6 | 0x09,80,124,80,124,11 7 | 0x0A,81,125,81,125,- 8 | 0x0B,66,110,66,110,- 9 | 0x0C,68,112,68,112,- 10 | 0x0D,70,114,70,114,- 11 | 0x0E,72,116,72,116,- 12 | 0x0F,90,134,90,134,- 13 | 0x10,91,135,91,135,- 14 | 0x11,92,136,92,136,- 15 | 0x12,93,137,93,137,- 16 | 0x17,76,120,76,120,- 17 | 0x18,77,121,77,121,154 18 | 0x19,-,-,-,-,155 19 | 0x1A,-,-,-,-,156 20 | 0x1B,-,-,-,-,157 21 | 0x1C,-,-,-,-,158 22 | 0x22,-,-,-,-,159 23 | 0x23,-,-,-,-,160 24 | 0x24,-,-,-,-,161 25 | 0x25,-,-,-,-,162 26 | 0x26,-,-,-,-,163 27 | 0x3E,25,38,51,64,- 28 | 0x3F,26,39,52,65,- 29 | 0x44,24,37,50,63,- 30 | 0x46,"16,17,18,19,20,21","29,30,31,32,33,34","42,43,44,45,46,47","55,56,57,58,59,60",- 31 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "flit_core.buildapi" 3 | requires = ["flit_core >=3.7.1,<4"] 4 | 5 | [project] 6 | name = "traktor_s4_mk1_midify" 7 | version = "0.4.1" 8 | authors = [ 9 | { name="Conal Moloney", email="blaxpot@blax.site" }, 10 | ] 11 | description = "Get MIDI signals from a Traktor S4 mk1 DJ controller under Linux" 12 | readme = "README.md" 13 | requires-python = ">=3.10, <3.11" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: POSIX :: Linux", 18 | ] 19 | dependencies = [ 20 | "evdev==1.4.0", 21 | "python-rtmidi==1.5.8", 22 | ] 23 | 24 | [project.scripts] 25 | traktor-s4-mk1-midify = "traktor_s4_mk1_midify.midify:midify" 26 | traktor-s4-mk1-print-events= "traktor_s4_mk1_midify.midify:print_events" 27 | 28 | [project.urls] 29 | "Homepaage" = "https://github.com/blaxpot/traktor-s4-mk1-midify" 30 | "Bug Tracker" = "https://github.com/blaxpot/traktor-s4-mk1-midify/issues" 31 | 32 | [tool.black] 33 | line-length = 120 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Conal Moloney 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 | -------------------------------------------------------------------------------- /traktor_s4_mk1_midify/evcode-type-map.csv: -------------------------------------------------------------------------------- 1 | 16,POT 2 | 17,POT 3 | 18,POT 4 | 19,POT 5 | 21,POT 6 | 22,POT 7 | 23,POT 8 | 24,POT 9 | 25,POT 10 | 26,JOG_TOUCH 11 | 27,JOG_TOUCH 12 | 28,POT 13 | 29,POT 14 | 30,POT 15 | 31,POT 16 | 32,POT 17 | 33,POT 18 | 34,POT 19 | 35,POT 20 | 36,POT 21 | 37,POT 22 | 38,POT 23 | 39,POT 24 | 40,POT 25 | 41,POT 26 | 42,POT 27 | 43,POT 28 | 44,POT 29 | 45,POT 30 | 46,POT 31 | 47,POT 32 | 48,POT 33 | 49,POT 34 | 50,POT 35 | 51,POT 36 | 52,JOG_ROT 37 | 53,JOG_ROT 38 | 54,BROWSE_ROT 39 | 55,ROT 40 | 56,ROT 41 | 57,ROT 42 | 58,ROT 43 | 59,GAIN_ROT 44 | 60,GAIN_ROT 45 | 61,GAIN_ROT 46 | 62,GAIN_ROT 47 | 256,BTN 48 | 258,BTN 49 | 259,BTN 50 | 260,BTN 51 | 261,BTN 52 | 262,BTN 53 | 263,BTN 54 | 265,BTN 55 | 266,BTN 56 | 267,BTN 57 | 268,BTN 58 | 269,BTN 59 | 270,BTN 60 | 271,BTN 61 | 272,BTN 62 | 273,BTN 63 | 274,BTN 64 | 275,BTN 65 | 280,BTN 66 | 281,BTN 67 | 282,BTN 68 | 283,BTN 69 | 284,BTN 70 | 288,BTN 71 | 289,BTN 72 | 290,BTN 73 | 291,BTN 74 | 292,BTN 75 | 296,BTN 76 | 297,BTN 77 | 298,BTN 78 | 299,BTN 79 | 305,BTN 80 | 306,BTN 81 | 307,BTN 82 | 308,BTN 83 | 309,BTN 84 | 310,BTN 85 | 311,BTN 86 | 312,BTN 87 | 314,BTN 88 | 315,BTN 89 | 316,BTN 90 | 317,BTN 91 | 318,BTN 92 | 319,BTN 93 | 321,BTN 94 | 322,BTN 95 | 323,BTN 96 | 324,BTN 97 | 325,BTN 98 | 328,BTN 99 | 329,BTN 100 | 330,BTN 101 | 331,BTN 102 | 332,BTN 103 | 333,BTN 104 | 334,BTN 105 | 335,BTN 106 | 345,BTN 107 | 346,BTN 108 | 347,BTN 109 | 348,BTN 110 | 349,BTN 111 | -------------------------------------------------------------------------------- /traktor_s4_mk1_midify/midi-evcode-map-mixer-effect.csv: -------------------------------------------------------------------------------- 1 | 16,0x45,0xb3,0x45,0xb3 2 | 17,0x45,0xb1,0x45,0xb1 3 | 18,0x45,0xb0,0x45,0xb0 4 | 19,0x45,0xb2,0x45,0xb2 5 | 23,0x0A,0xb4,0x0A,0xb4 6 | 24,0x0C,0xB4,0x0C,0xB4 7 | 25,0x0B,0xB4,0x0B,0xB4 8 | 28,0x43,0xb3,0x43,0xb3 9 | 29,0x42,0xb3,0x42,0xb3 10 | 30,0x41,0xb3,0x41,0xb3 11 | 31,0x40,0xb3,0x40,0xb3 12 | 32,0x21,0xb4,0x21,0xb4 13 | 33,0x20,0xb4,0x20,0xb4 14 | 34,0x1f,0xb4,0x1f,0xb4 15 | 35,0x1e,0xb4,0x1e,0xb4 16 | 36,0x43,0xb1,0x43,0xb1 17 | 37,0x42,0xb1,0x42,0xb1 18 | 38,0x41,0xb1,0x41,0xb1 19 | 39,0x40,0xb1,0x40,0xb1 20 | 40,0x43,0xb0,0x43,0xb0 21 | 41,0x42,0xb0,0x42,0xb0 22 | 42,0x41,0xb0,0x41,0xb0 23 | 43,0x40,0xb0,0x40,0xb0 24 | 44,0x43,0xb2,0x43,0xb2 25 | 45,0x42,0xb2,0x42,0xb2 26 | 46,0x41,0xb2,0x41,0xb2 27 | 47,0x40,0xb2,0x40,0xb2 28 | 48,0x17,0xb4,0x17,0xb4 29 | 49,0x16,0xb4,0x16,0xb4 30 | 50,0x15,0xb4,0x15,0xb4 31 | 51,0x14,0xb4,0x14,0xb4 32 | 54,0x02,0xb4,0x02,0xb4 33 | 59,0x3c,0xb0,0x3c,0xb0 34 | 60,0x3c,0xb1,0x3c,0xb1 35 | 61,0x3c,0xb2,0x3c,0xb2 36 | 62,0x3c,0xb3,0x3c,0xb3 37 | 280,0x05,0xb4,0x05,0xb4 38 | 281,0x07,0xb4,0x07,0xb4 39 | 282,0x06,0xb4,0x06,0xb4 40 | 283,0x08,0xb4,0x08,0xb4 41 | 284,0x09,0xb4,0x09,0xb4 42 | 288,0x44,0xb2,0x44,0xb2 43 | 289,0x44,0xb0,0x44,0xb0 44 | 290,0x44,0xb1,0x44,0xb1 45 | 291,0x44,0xb3,0x44,0xb3 46 | 292,0x03,0xb4,0x03,0xb4 47 | 321,0x18,0xb4,0x18,0xb4 48 | 322,0x19,0xb4,0x19,0xb4 49 | 323,0x1a,0xb4,0x1a,0xb4 50 | 324,0x1b,0xb4,0x1b,0xb4 51 | 325,0x1c,0xb4,0x1c,0xb4 52 | 328,0x3e,0xb2,0x3e,0xb2 53 | 329,0x3f,0xb2,0x3f,0xb2 54 | 330,0x3e,0xb0,0x3e,0xb0 55 | 331,0x3f,0xb0,0x3f,0xb0 56 | 332,0x3e,0xb1,0x3e,0xb1 57 | 333,0x3f,0xb1,0x3f,0xb1 58 | 334,0x3e,0xb3,0x3e,0xb3 59 | 335,0x3f,0xb3,0x3f,0xb3 60 | 345,0x22,0xb4,0x22,0xb4 61 | 346,0x23,0xb4,0x23,0xb4 62 | 347,0x24,0xb4,0x24,0xb4 63 | 348,0x25,0xb4,0x25,0xb4 64 | 349,0x26,0xb4,0x26,0xb4 65 | -------------------------------------------------------------------------------- /traktor_s4_mk1_midify/midi-evcode-map-deck.csv: -------------------------------------------------------------------------------- 1 | 21,0x4,0xb0,0x22,0xb0,0x4,0xb2,0x22,0xb2 2 | 22,0x4,0xb1,0x22,0xb1,0x4,0xb3,0x22,0xb3 3 | 26,0x3,0xb0,0x21,0xb0,0x3,0xb2,0x21,0xb2 4 | 27,0x3,0xb1,0x21,0xb1,0x3,0xb3,0x21,0xb3 5 | 52,0x2,0xb0,0x20,0xb0,0x2,0xb2,0x20,0xb2 6 | 53,0x2,0xb1,0x20,0xb1,0x2,0xb3,0x20,0xb3 7 | 55,0x13,0xb0,0x31,0xb0,0x13,0xb2,0x31,0xb2 8 | 56,0x15,0xb0,0x33,0xb0,0x15,0xb2,0x33,0xb2 9 | 57,0x13,0xb1,0x31,0xb1,0x13,0xb3,0x31,0xb3 10 | 58,0x15,0xb1,0x33,0xb1,0x15,0xb3,0x33,0xb3 11 | 256,0xb,0xb0,0x29,0xb0,0xb,0xb2,0x29,0xb2 12 | 258,0xc,0xb0,0x2a,0xb0,0xc,0xb2,0x2a,0xb2 13 | 259,0x8,0xb0,0x26,0xb0,0x8,0xb2,0x26,0xb2 14 | 260,0xd,0xb0,0x2b,0xb0,0xd,0xb2,0x2b,0xb2 15 | 261,0x9,0xb0,0x27,0xb0,0x9,0xb2,0x27,0xb2 16 | 262,0xe,0xb0,0x2c,0xb0,0xe,0xb2,0x2c,0xb2 17 | 263,0xa,0xb0,0x28,0xb0,0xa,0xb2,0x28,0xb2 18 | 265,0xf,0xb0,0x2d,0xb0,0xf,0xb2,0x2d,0xb2 19 | 266,0x17,0xb0,0x35,0xb0,0x17,0xb2,0x35,0xb2 20 | 267,0x10,0xb0,0x2e,0xb0,0x10,0xb2,0x2e,0xb2 21 | 268,0x18,0xb0,0x36,0xb0,0x18,0xb2,0x36,0xb2 22 | 269,0x11,0xb0,0x2f,0xb0,0x11,0xb2,0x2f,0xb2 23 | 270,0x1,0xb0,0x1f,0xb0,0x1,0xb2,0x1f,0xb2 24 | 271,0x12,0xb0,0x30,0xb0,0x12,0xb2,0x30,0xb2 25 | 272,0x6,0xb0,0x24,0xb0,0x6,0xb2,0x24,0xb2 26 | 273,0x5,0xb0,0x23,0xb0,0x5,0xb2,0x23,0xb2 27 | 274,0x16,0xb0,0x34,0xb0,0x16,0xb2,0x34,0xb2 28 | 275,0x14,0xb0,0x32,0xb0,0x14,0xb2,0x32,0xb2 29 | 296,0x6,0xb1,0x24,0xb1,0x6,0xb3,0x24,0xb3 30 | 297,0x5,0xb1,0x23,0xb1,0x5,0xb3,0x23,0xb3 31 | 298,0x16,0xb1,0x34,0xb1,0x16,0xb3,0x34,0xb3 32 | 299,0x14,0xb1,0x32,0xb1,0x13,0xb3,0x32,0xb3 33 | 305,0xf,0xb1,0x2d,0xb1,0xf,0xb3,0x2d,0xb3 34 | 306,0x17,0xb1,0x35,0xb1,0x17,0xb3,0x35,0xb3 35 | 307,0x10,0xb1,0x2e,0xb1,0x10,0xb3,0x2e,0xb3 36 | 308,0x18,0xb1,0x36,0xb1,0x18,0xb3,0x36,0xb3 37 | 309,0x11,0xb1,0x2f,0xb1,0x11,0xb3,0x2f,0xb3 38 | 310,0x1,0xb1,0x1f,0xb1,0x1,0xb3,0x1f,0xb3 39 | 311,0x12,0xb1,0x30,0xb1,0x12,0xb3,0x30,0xb3 40 | 312,0xb,0xb1,0x29,0xb1,0xb,0xb3,0x29,0xb3 41 | 314,0xc,0xb1,0x2a,0xb1,0xc,0xb3,0x2a,0xb3 42 | 315,0x8,0xb1,0x26,0xb1,0x8,0xb3,0x26,0xb3 43 | 316,0xd,0xb1,0x2b,0xb1,0xd,0xb3,0x2b,0xb3 44 | 317,0x9,0xb1,0x27,0xb1,0x9,0xb3,0x27,0xb3 45 | 318,0xe,0xb1,0x2c,0xb1,0xe,0xb3,0x2c,0xb3 46 | 319,0xa,0xb1,0x28,0xb1,0xa,0xb3,0x28,0xb3 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # traktor-s4-mk1-midify 2 | 3 | Translate events from a Traktor S4 mk1 DJ controller generated by the linux `snd-usb-caiaq` kernel module to MIDI signals which can be used by [Mixxx](https://mixxx.org/download/) (virtual DJ software). 4 | 5 | ## Setup 6 | 1. Check that you have the `snd-usb-caiaq` kernel module enabled: 7 | ```bash 8 | # lsmod | grep snd_usb_caiaq 9 | snd_usb_caiaq 57344 0 10 | snd_rawmidi 36864 3 snd_seq_midi,snd_usbmidi_lib,snd_usb_caiaq 11 | snd_pcm 106496 14 snd_hda_codec_hdmi,snd_hda_intel,snd_usb_audio,snd_hda_codec,snd_sof,snd_sof_intel_hda_common,snd_soc_core,snd_hda_core,snd_usb_caiaq,snd_pcm_dmaengine 12 | snd 90112 33 snd_hda_codec_generic,snd_seq,snd_seq_device,snd_hda_codec_hdmi,snd_hwdep,snd_hda_intel,snd_usb_audio,snd_usbmidi_lib,snd_hda_codec,snd_hda_codec_realtek,snd_timer,snd_compress,thinkpad_acpi,snd_soc_core,snd_pcm,snd_usb_caiaq,snd_rawmidi 13 | ``` 14 | 2. Check that you have `amixer`/`aplay` - on Ubuntu they're in the `alsa-utils` package: 15 | ```bash 16 | # amixer controls 17 | numid=43,iface=CARD,name='HDMI/DP,pcm=10 Jack' 18 | numid=19,iface=CARD,name='HDMI/DP,pcm=3 Jack' 19 | numid=25,iface=CARD,name='HDMI/DP,pcm=7 Jack' 20 | ... 21 | # aplay -l |grep Traktor 22 | card 1: TraktorKontrolS [Traktor Kontrol S4], device 0: Traktor Kontrol S4 [Traktor Kontrol S4] 23 | ``` 24 | 3. You will need the following packages in order to build and install: `libasound2-dev` and `libjack-dev` (on Ubuntu these can be install using apt) 25 | 4. Install this package using `pip install .` in the project root dir ([Python 3](https://www.python.org/downloads/) / [pip](https://pypi.org/project/pip/#files) required) 26 | 5. Run `traktor-s4-mk1-midify` and check that the controller is detected: 27 | ```bash 28 | # traktor-s4-mk1-midify 29 | Jog sensitivity must be between 1 and 100. Using default value (5). 30 | Detected ALSA device: 31 | card 1: TraktorKontrolS [Traktor Kontrol S4], device 0: Traktor Kontrol S4 [Traktor Kontrol S4] 32 | Detected evdev: 33 | Traktor Kontrol S4 /dev/input/event6 usb-0000:07:00.0-2.2/input0 34 | ``` 35 | 6. Launch [Mixxx](https://mixxx.org/download/) and configure it to use the MIDI device named `traktor-s4-mk1-midify` 36 | 37 | # Usage 38 | You should ensure `traktor-s4-mk1-midify` is running before starting Mixxx. You could run it in the background using `screen` or similar. 39 | 40 | For example, this command would run `traktor-s4-mk1-midify` the background and then launch Mixxx: 41 | 42 | `screen -dmS ts4m traktor-s4-mk1-midify && mixxx` 43 | 44 | CLI options: 45 | ```bash 46 | # traktor-s4-mk1-midify -h 47 | usage: traktor-s4-mk1-midify [-h] [-j JOG_SENSITIVITY] [-d] 48 | 49 | Convert events generated by the snd-usb-caiaq kernel module to MIDI signals 50 | 51 | options: 52 | -h, --help show this help message and exit 53 | -j JOG_SENSITIVITY, --jog_sensitivity JOG_SENSITIVITY 54 | Adjust jog wheel sensitivity (min: 1, max: 100, default: 5) 55 | -d, --debug Show debug log messages 56 | ``` 57 | 58 | # Notes 59 | * LEDs are controlled by shelling out to ALSA. See the output below for an idea of what can be controlled this way. (Note: the controller seems to need to be communicating with Mixxx and/or this program to work properly): 60 | ```bash 61 | # amixer -c TraktorKontrolS controls |grep -i sync 62 | numid=79,iface=HWDEP,name='LED: Deck A: Sync' 63 | numid=123,iface=HWDEP,name='LED: Deck B: Sync' 64 | 65 | [13] blaxpot@whydah-navi ~ 66 | # amixer -c TraktorKontrolS cset numid=79 31 67 | numid=79,iface=HWDEP,name='LED: Deck A: Sync' 68 | ; type=INTEGER,access=rw------,values=1,min=0,max=31,step=0 69 | : values=31 70 | ``` 71 | * Get debug logs from Mixxx: `mixxx --controllerDebug --developer` 72 | * Configure inputs / outputs properly in `~/.asoundrc`: 73 | ``` 74 | pcm.TraktorS4InputCOutputMain { type plug; slave.pcm "hw:TraktorKontrolS,0,0"; } 75 | pcm.TraktorS4InputDOutputHeadphones { type plug; slave.pcm "hw:TraktorKontrolS,0,1"; } 76 | ``` 77 | -------------------------------------------------------------------------------- /doc/amixer_controls.tsv: -------------------------------------------------------------------------------- 1 | numid LED name 2 | 15 LED: Channel A: < 3 | 14 LED: Channel A: > 4 | 23 LED: Channel A: Active 5 | 24 LED: Channel A: Cue 6 | 25 LED: Channel A: FX1 7 | 26 LED: Channel A: FX2 8 | 16 LED: Channel A: Meter 1 9 | 17 LED: Channel A: Meter 2 10 | 18 LED: Channel A: Meter 3 11 | 19 LED: Channel A: Meter 4 12 | 20 LED: Channel A: Meter 5 13 | 21 LED: Channel A: Meter 6 14 | 22 LED: Channel A: Meter clip 15 | 28 LED: Channel B: < 16 | 27 LED: Channel B: > 17 | 36 LED: Channel B: Active 18 | 37 LED: Channel B: Cue 19 | 38 LED: Channel B: FX1 20 | 39 LED: Channel B: FX2 21 | 29 LED: Channel B: Meter 1 22 | 30 LED: Channel B: Meter 2 23 | 31 LED: Channel B: Meter 3 24 | 32 LED: Channel B: Meter 4 25 | 33 LED: Channel B: Meter 5 26 | 34 LED: Channel B: Meter 6 27 | 35 LED: Channel B: Meter clip 28 | 41 LED: Channel C: < 29 | 40 LED: Channel C: > 30 | 49 LED: Channel C: Active 31 | 50 LED: Channel C: Cue 32 | 51 LED: Channel C: FX1 33 | 52 LED: Channel C: FX2 34 | 42 LED: Channel C: Meter 1 35 | 43 LED: Channel C: Meter 2 36 | 44 LED: Channel C: Meter 3 37 | 45 LED: Channel C: Meter 4 38 | 46 LED: Channel C: Meter 5 39 | 47 LED: Channel C: Meter 6 40 | 48 LED: Channel C: Meter clip 41 | 54 LED: Channel D: < 42 | 53 LED: Channel D: > 43 | 62 LED: Channel D: Active 44 | 63 LED: Channel D: Cue 45 | 64 LED: Channel D: FX1 46 | 65 LED: Channel D: FX2 47 | 55 LED: Channel D: Meter 1 48 | 56 LED: Channel D: Meter 2 49 | 57 LED: Channel D: Meter 3 50 | 58 LED: Channel D: Meter 4 51 | 59 LED: Channel D: Meter 5 52 | 60 LED: Channel D: Meter 6 53 | 61 LED: Channel D: Meter clip 54 | 66 LED: Deck A: 1 (blue) 55 | 67 LED: Deck A: 1 (green) 56 | 68 LED: Deck A: 2 (blue) 57 | 69 LED: Deck A: 2 (green) 58 | 70 LED: Deck A: 3 (blue) 59 | 71 LED: Deck A: 3 (green) 60 | 72 LED: Deck A: 4 (blue) 61 | 73 LED: Deck A: 4 (green) 62 | 80 LED: Deck A: Cue 63 | 86 LED: Deck A: Deck A 64 | 87 LED: Deck A: Deck C 65 | 75 LED: Deck A: Deck C button 66 | 94 LED: Deck A: Digit 1 - A 67 | 95 LED: Deck A: Digit 1 - B 68 | 96 LED: Deck A: Digit 1 - C 69 | 97 LED: Deck A: Digit 1 - D 70 | 98 LED: Deck A: Digit 1 - E 71 | 99 LED: Deck A: Digit 1 - F 72 | 100 LED: Deck A: Digit 1 - G 73 | 101 LED: Deck A: Digit 1 - dot 74 | 102 LED: Deck A: Digit 2 - A 75 | 103 LED: Deck A: Digit 2 - B 76 | 104 LED: Deck A: Digit 2 - C 77 | 105 LED: Deck A: Digit 2 - D 78 | 106 LED: Deck A: Digit 2 - E 79 | 107 LED: Deck A: Digit 2 - F 80 | 108 LED: Deck A: Digit 2 - G 81 | 109 LED: Deck A: Digit 2 - dot 82 | 76 LED: Deck A: In 83 | 85 LED: Deck A: Keylock 84 | 74 LED: Deck A: Load 85 | 84 LED: Deck A: Master 86 | 89 LED: Deck A: On Air 87 | 77 LED: Deck A: Out 88 | 81 LED: Deck A: Play 89 | 90 LED: Deck A: Sample 1 90 | 91 LED: Deck A: Sample 2 91 | 92 LED: Deck A: Sample 3 92 | 93 LED: Deck A: Sample 4 93 | 88 LED: Deck A: Samples 94 | 78 LED: Deck A: Shift 95 | 79 LED: Deck A: Sync 96 | 83 LED: Deck A: Tempo down 97 | 82 LED: Deck A: Tempo up 98 | 110 LED: Deck B: 1 (blue) 99 | 111 LED: Deck B: 1 (green) 100 | 112 LED: Deck B: 2 (blue) 101 | 113 LED: Deck B: 2 (green) 102 | 114 LED: Deck B: 3 (blue) 103 | 115 LED: Deck B: 3 (green) 104 | 116 LED: Deck B: 4 (blue) 105 | 117 LED: Deck B: 4 (green) 106 | 124 LED: Deck B: Cue 107 | 130 LED: Deck B: Deck B 108 | 131 LED: Deck B: Deck D 109 | 119 LED: Deck B: Deck D button 110 | 138 LED: Deck B: Digit 1 - A 111 | 139 LED: Deck B: Digit 1 - B 112 | 140 LED: Deck B: Digit 1 - C 113 | 141 LED: Deck B: Digit 1 - D 114 | 142 LED: Deck B: Digit 1 - E 115 | 143 LED: Deck B: Digit 1 - F 116 | 144 LED: Deck B: Digit 1 - G 117 | 145 LED: Deck B: Digit 1 - dot 118 | 146 LED: Deck B: Digit 2 - A 119 | 147 LED: Deck B: Digit 2 - B 120 | 148 LED: Deck B: Digit 2 - C 121 | 149 LED: Deck B: Digit 2 - D 122 | 150 LED: Deck B: Digit 2 - E 123 | 151 LED: Deck B: Digit 2 - F 124 | 152 LED: Deck B: Digit 2 - G 125 | 153 LED: Deck B: Digit 2 - dot 126 | 120 LED: Deck B: In 127 | 129 LED: Deck B: Keylock 128 | 118 LED: Deck B: Load 129 | 128 LED: Deck B: Master 130 | 133 LED: Deck B: On Air 131 | 121 LED: Deck B: Out 132 | 125 LED: Deck B: Play 133 | 134 LED: Deck B: Sample 1 134 | 135 LED: Deck B: Sample 2 135 | 136 LED: Deck B: Sample 3 136 | 137 LED: Deck B: Sample 4 137 | 132 LED: Deck B: Samples 138 | 122 LED: Deck B: Shift 139 | 123 LED: Deck B: Sync 140 | 127 LED: Deck B: Tempo down 141 | 126 LED: Deck B: Tempo up 142 | 155 LED: FX1: 1 143 | 156 LED: FX1: 2 144 | 157 LED: FX1: 3 145 | 158 LED: FX1: Mode 146 | 154 LED: FX1: dry/wet 147 | 160 LED: FX2: 1 148 | 161 LED: FX2: 2 149 | 162 LED: FX2: 3 150 | 163 LED: FX2: Mode 151 | 159 LED: FX2: dry/wet 152 | 11 LED: Master: Browser button 153 | 2 LED: Master: Headphone 154 | 3 LED: Master: Master 155 | 6 LED: Master: Master button 156 | 12 LED: Master: Play button 157 | 1 LED: Master: Quant 158 | 10 LED: Master: Quant button 159 | 8 LED: Master: Rec 160 | 9 LED: Master: Size 161 | 4 LED: Master: Snap 162 | 7 LED: Master: Snap button 163 | 13 LED: Master: Undo button 164 | 5 LED: Master: Warning 165 | -------------------------------------------------------------------------------- /traktor_s4_mk1_midify/midify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # TODO: allow user specified event code mappings via CLI option 4 | 5 | import argparse 6 | import csv 7 | import evdev 8 | import os 9 | import re 10 | import rtmidi 11 | import subprocess 12 | import time 13 | 14 | 15 | # Indicies are snd-usb-caiaq event codes, values are MIDI control change (CC) codes/channels. 16 | def load_midi_map_mixer_effect(filename=os.path.join(os.path.dirname(__file__), "midi-evcode-map-mixer-effect.csv")): 17 | mapping = [None for _ in range(350)] 18 | 19 | with open(filename, newline="") as csvfile: 20 | reader = csv.reader(csvfile, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL) 21 | 22 | for line in reader: 23 | mapping[int(line[0])] = [ 24 | [int(line[1], 16), int(line[2], 16)], 25 | [int(line[3], 16), int(line[4], 16)], 26 | ] 27 | 28 | return mapping 29 | 30 | 31 | MIDI_MAP_MIXER_EFFECT = load_midi_map_mixer_effect() 32 | 33 | 34 | # Indicies are snd-usb-caiaq event codes, values are MIDI control change (CC) codes/channels. 35 | # Decks are affected by the shift modifier key and the deck toggle buttons, so we need to send different MIDI data based 36 | # on the state of these modifiers. 37 | def load_midi_map_deck(filename=os.path.join(os.path.dirname(__file__), "midi-evcode-map-deck.csv")): 38 | mapping = [None for _ in range(320)] 39 | 40 | with open(filename, newline="") as csvfile: 41 | reader = csv.reader(csvfile, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL) 42 | 43 | for line in reader: 44 | mapping[int(line[0])] = [ 45 | [int(line[1], 16), int(line[2], 16)], 46 | [int(line[3], 16), int(line[4], 16)], 47 | [int(line[5], 16), int(line[6], 16)], 48 | [int(line[7], 16), int(line[8], 16)], 49 | ] 50 | 51 | return mapping 52 | 53 | 54 | MIDI_MAP_DECK = load_midi_map_deck() 55 | 56 | 57 | def load_evcode_type_map(filename=os.path.join(os.path.dirname(__file__), "evcode-type-map.csv")): 58 | mapping = [None for _ in range(350)] 59 | 60 | with open(filename, newline="") as csvfile: 61 | reader = csv.reader(csvfile, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL) 62 | 63 | for line in reader: 64 | mapping[int(line[0])] = line[1] 65 | 66 | return mapping 67 | 68 | 69 | EVCODE_TYPE_MAP = load_evcode_type_map() 70 | 71 | 72 | def load_midi_alsa_control_map(filename=os.path.join(os.path.dirname(__file__), "midi-alsa-control-map.csv")): 73 | mapping = [None for _ in range(71)] 74 | 75 | with open(filename, newline="") as csvfile: 76 | reader = csv.reader(csvfile, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL) 77 | 78 | for line in reader: 79 | line_values = [None for _ in range(5)] 80 | 81 | for i in range(5): 82 | if line[i + 1].isdigit(): 83 | line_values[i] = int(line[i + 1]) 84 | elif re.match("[0-9]+,[0-9,]+", line[i + 1]): 85 | cell_values = line[i + 1].split(",") 86 | line_values[i] = [int(value) for value in cell_values] 87 | 88 | mapping[int(line[0], 16)] = line_values 89 | 90 | return mapping 91 | 92 | 93 | MIDI_ALSA_CONTROL_MAP = load_midi_alsa_control_map() 94 | ALSA_DEV = subprocess.getoutput('aplay -l | grep "Traktor Kontrol S4" | cut -d " " -f 2').replace(":", "") 95 | 96 | 97 | def select_controller_device(): 98 | print("List of your devices:") 99 | devices = [evdev.InputDevice(path) for path in evdev.list_devices()] 100 | 101 | for i, device in enumerate(devices): 102 | print(f"[{i}]\t{device.path}\t{device.name}\t{device.phys}") 103 | 104 | device_id = int(input("Which of these is the controller? ")) 105 | controller = devices.pop(device_id) 106 | 107 | for device in devices: 108 | device.close() 109 | 110 | return controller 111 | 112 | 113 | def detect_controller_device(): 114 | for path in evdev.list_devices(): 115 | device = evdev.InputDevice(path) 116 | 117 | if device.name.__contains__("Traktor Kontrol S4"): 118 | print("Detected evdev:") 119 | print(f"{device.name}\t{device.path}\t{device.phys}") 120 | return device 121 | else: 122 | device.close() 123 | 124 | print("Couldn't find your controller. Do you see it in the output of lsusb?") 125 | quit() 126 | 127 | 128 | def detect_alsa_device(): 129 | if ALSA_DEV: 130 | print("Detected ALSA device: ") 131 | print(subprocess.getoutput('aplay -l | grep "Traktor Kontrol S4"')) 132 | else: 133 | print("Couldn't find your controller with aplay. Do you have snd-usb-caiaq installed / enabled?") 134 | quit() 135 | 136 | 137 | def evcode_to_midi(evcode, shift_a, shift_b, toggle_ac, toggle_bd): 138 | if MIDI_MAP_MIXER_EFFECT[evcode] is not None: 139 | if shift_a or shift_b: 140 | return MIDI_MAP_MIXER_EFFECT[evcode][1] 141 | else: 142 | return MIDI_MAP_MIXER_EFFECT[evcode][0] 143 | elif MIDI_MAP_DECK[evcode] is not None: 144 | # Decks B/D use 0xB1/0xB3 145 | if MIDI_MAP_DECK[evcode][1][1] & 1 == 1: 146 | if toggle_bd: 147 | if shift_b: 148 | return MIDI_MAP_DECK[evcode][3] 149 | else: 150 | return MIDI_MAP_DECK[evcode][2] 151 | else: 152 | if shift_b: 153 | return MIDI_MAP_DECK[evcode][1] 154 | else: 155 | return MIDI_MAP_DECK[evcode][0] 156 | # Decks A/C use 0xB0/0xB2 157 | else: 158 | if toggle_ac: 159 | if shift_a: 160 | return MIDI_MAP_DECK[evcode][3] 161 | else: 162 | return MIDI_MAP_DECK[evcode][2] 163 | else: 164 | if shift_a: 165 | return MIDI_MAP_DECK[evcode][1] 166 | else: 167 | return MIDI_MAP_DECK[evcode][0] 168 | else: 169 | return None 170 | 171 | 172 | def midi_to_alsa_control(midi_bytes): 173 | cc = midi_bytes[1] 174 | 175 | if MIDI_ALSA_CONTROL_MAP[cc] is None: 176 | return None 177 | 178 | # Bitwise & to get the lower 4 bytes of the MIDI CC 179 | channel = midi_bytes[0] & 0x4F 180 | 181 | if channel < len(MIDI_ALSA_CONTROL_MAP[cc]): 182 | return MIDI_ALSA_CONTROL_MAP[cc][channel] 183 | else: 184 | return None 185 | 186 | 187 | # We have 7 volume indicators per channel, one of which indicates clipping, which can be set at 31 brightness levels. 188 | # 18 (brightness levels) * 7 (LEDs) = 126, which is one off the max value Mixxx will send in a MIDI message to indicate 189 | # Volume (0x7F). Subtract 1 from this value and use this value to set the right number of LEDs at the right brightness. 190 | # 191 | # We can skip a few brightness levels as we scale up so that we more or less linearly increase brightness. 192 | def set_vu_meter(controls, value): 193 | light = value - 1 # ensure we stay <= 126 194 | full_brightness = light // 18 195 | 196 | if value: 197 | full_brightness = light // 18 198 | partial = light % 18 199 | 200 | for i in range(full_brightness): 201 | set_led(controls[i], 31) 202 | 203 | if partial: 204 | alsa_values = [ 205 | 2, 206 | 4, 207 | 5, 208 | 7, 209 | 9, 210 | 10, 211 | 12, 212 | 14, 213 | 15, 214 | 17, 215 | 19, 216 | 21, 217 | 22, 218 | 24, 219 | 26, 220 | 28, 221 | 29, 222 | 31, 223 | ] 224 | set_led(controls[full_brightness], alsa_values[partial - 1]) 225 | 226 | for i in range(full_brightness, 6, 1): 227 | set_led(controls[i], 0) 228 | 229 | 230 | def set_led(alsa_id, brightness): 231 | subprocess.call( 232 | ["amixer", "-c", ALSA_DEV, "cset", f"numid={alsa_id}", str(brightness)], 233 | stdout=subprocess.DEVNULL, 234 | stderr=subprocess.DEVNULL, 235 | ) 236 | 237 | 238 | def handle_midi_input(msg, args): 239 | control = midi_to_alsa_control(msg[0]) 240 | 241 | # If there is no ALSA control corresponding to the MIDI CC recieved, we should handle LED control between the 242 | # snd-usb-caiaq kernel module and this program (i.e. not communicate with Mixxx), as this seems to be the behaviour 243 | # of the NI drivers on macOS / Windows. 244 | if control is None: 245 | return 246 | 247 | if args.debug: 248 | print( 249 | "[Recieved MIDI message] Channel: {}, CC: {}, Value: {}".format( 250 | hex(msg[0][0]), hex(msg[0][1]), hex(msg[0][2]) 251 | ) 252 | ) 253 | 254 | value = msg[0][2] 255 | 256 | if isinstance(control, list): # Vu meters 257 | set_vu_meter(control, value) 258 | else: # All other MIDI controlled LEDs 259 | if value != 0: 260 | set_led(control, 31) 261 | else: 262 | set_led(control, 0) 263 | 264 | 265 | def calculate_jog_midi_value_update_jog_data(event, jog_data): 266 | if jog_data["prev_control_value"] is None: 267 | jog_data["prev_control_value"] = event.value 268 | jog_data["updated"] = time.time() 269 | return None, jog_data 270 | 271 | # Get the change in the jog wheel control value since the last event 272 | if event.value <= 255 and jog_data["prev_control_value"] >= 767: # value increased past max 273 | diff = 1024 - jog_data["prev_control_value"] + event.value 274 | elif event.value >= 767 and jog_data["prev_control_value"] <= 255: # value decreased past min 275 | diff = event.value - 1024 - jog_data["prev_control_value"] 276 | else: 277 | diff = event.value - jog_data["prev_control_value"] # value stayed within range 278 | 279 | jog_data["prev_control_value"] = event.value 280 | 281 | # If it's been less than the interval specified in jog_sensitivity, store the cumulative jog wheel control value 282 | # change for later and stop processing. 283 | if time.time() - jog_data["updated"] < jog_data["sensitivity"]: 284 | jog_data["counter"] += diff 285 | return None, jog_data 286 | else: 287 | midi_value = jog_data["counter"] + diff 288 | jog_data["counter"] = 0 289 | jog_data["updated"] = time.time() 290 | 291 | # Convert signed int value to unsigned values that Mixxx expects in jog wheel MIDI messages 292 | if -64 <= midi_value < 0: 293 | midi_value = 128 + midi_value 294 | elif midi_value < -64: 295 | midi_value = 64 296 | elif midi_value >= 63: 297 | midi_value = 63 298 | 299 | return midi_value, jog_data 300 | 301 | 302 | def calculate_gain_midi_value_update_gain_data(event, gain_data): 303 | if gain_data["prev_control_value"] is None: 304 | gain_data["prev_control_value"] = event.value 305 | gain_data["counter"] = 0x3F 306 | gain_data["updated"] = time.time() 307 | return 0x3F, gain_data 308 | 309 | # Get the change in the rotary encoder value since the last event 310 | if event.value <= 3 and gain_data["prev_control_value"] >= 12: # value increased past max 311 | diff = 16 - gain_data["prev_control_value"] + event.value 312 | elif event.value >= 12 and gain_data["prev_control_value"] <= 3: # value decreased past min 313 | diff = event.value - 16 - gain_data["prev_control_value"] 314 | else: 315 | diff = event.value - gain_data["prev_control_value"] # value stayed within range 316 | 317 | gain_data["prev_control_value"] = event.value 318 | 319 | # If it has been less than 5ms since last gain rot message, store cumulative gain rot control data for later and 320 | # stop processing. 321 | if time.time() - gain_data["updated"] < 0.005: 322 | gain_data["counter"] += diff 323 | return None, gain_data 324 | else: 325 | gain_data["counter"] += diff 326 | gain_data["updated"] = time.time() 327 | 328 | if gain_data["counter"] > 0x7F: 329 | gain_data["counter"] = 0x7F 330 | elif gain_data["counter"] < 0: 331 | gain_data["counter"] = 0 332 | 333 | return gain_data["counter"], gain_data 334 | 335 | 336 | def calculate_rot_midi_value_update_rot_data(event, rot_data): 337 | if rot_data["prev_control_value"] is None: 338 | rot_data["prev_control_value"] = event.value 339 | rot_data["updated"] = time.time() 340 | return None, rot_data 341 | 342 | # Get the change in the rotary encoder value since the last event 343 | if event.value <= 3 and rot_data["prev_control_value"] >= 12: # value increased past max 344 | diff = 16 - rot_data["prev_control_value"] + event.value 345 | elif event.value >= 12 and rot_data["prev_control_value"] <= 3: # value decreased past min 346 | diff = event.value - 16 - rot_data["prev_control_value"] 347 | else: 348 | diff = event.value - rot_data["prev_control_value"] # value stayed within range 349 | 350 | rot_data["prev_control_value"] = event.value 351 | 352 | # If it has been less than 5ms since last rot message, store cumulative control data for later and stop processing. 353 | if time.time() - rot_data["updated"] < 0.005: 354 | rot_data["counter"] += diff 355 | return None, rot_data 356 | else: 357 | midi_value = 0x3F + rot_data["counter"] + diff 358 | rot_data["counter"] = 0 359 | rot_data["updated"] = time.time() 360 | 361 | if midi_value > 0x7F: 362 | midi_value = 0x7F 363 | elif midi_value < 0: 364 | midi_value = 0 365 | 366 | return midi_value, rot_data 367 | 368 | 369 | # Values ranges are translated from snd-usb-caiaq ranges to MIDI ranges based on the control type. For example, 370 | # a fader has a value range from 0-4095 in snd-usb-caiaq events, but Mixxx expects MIDI values between 0-127. 371 | # Thus, integer division by 32 converts the value for all fader CCs from snd-usb-caiaq to MIDI. 372 | def calculate_midi_value_update_controller_data(event, controller_data, toggle_ac, toggle_bd): 373 | match EVCODE_TYPE_MAP[event.code]: 374 | case "BTN": 375 | return event.value, controller_data 376 | case "POT": 377 | value = event.value // 32 378 | return value, controller_data 379 | case "JOG_ROT": 380 | # TODO: make these deck toggle aware 381 | jog_rots = ["jog_a", "jog_b"] 382 | jog_rot = jog_rots[event.code - 52] # jog rot event codes are sequential beginning at 52 383 | 384 | value, controller_data[jog_rot] = calculate_jog_midi_value_update_jog_data(event, controller_data[jog_rot]) 385 | 386 | return value, controller_data 387 | case "BROWSE_ROT": 388 | value, rot_data = calculate_rot_midi_value_update_rot_data(event, controller_data["browse_rot"]) 389 | controller_data["browse_rot"] = rot_data 390 | 391 | return value, controller_data 392 | case "ROT": 393 | rots = ["move_rot_", "size_rot_", "move_rot_", "size_rot_"] 394 | rot = rots[event.code - 55] # move / size rot event codes are sequential beginning at 55 395 | 396 | if event.code <= 56: 397 | if toggle_ac: 398 | rot = rot + "c" 399 | else: 400 | rot = rot + "a" 401 | else: 402 | if toggle_bd: 403 | rot = rot + "d" 404 | else: 405 | rot = rot + "b" 406 | 407 | value, controller_data[rot] = calculate_rot_midi_value_update_rot_data(event, controller_data[rot]) 408 | 409 | return value, controller_data 410 | case "GAIN_ROT": 411 | gain_rots = ["gain_rot_a", "gain_rot_b", "gain_rot_c", "gain_rot_d"] 412 | gain_rot = gain_rots[event.code - 59] # gain rot event codes are sequential beginning at 59 413 | 414 | value, rot_data = calculate_gain_midi_value_update_gain_data(event, controller_data[gain_rot]) 415 | controller_data[gain_rot] = rot_data 416 | 417 | return value, controller_data 418 | case "JOG_TOUCH": 419 | value = 0 420 | 421 | if event.value >= 3050: 422 | value = 0x7F 423 | 424 | return value, controller_data 425 | case _: 426 | return None, controller_data 427 | 428 | 429 | def midify(): 430 | jog_sensitivity = 0.005 431 | 432 | parser = argparse.ArgumentParser( 433 | description="Convert events generated by the snd-usb-caiaq kernel module to MIDI signals" 434 | ) 435 | 436 | parser.add_argument( 437 | "-j", 438 | "--jog_sensitivity", 439 | type=int, 440 | help="Adjust jog wheel sensitivity (min: 1, max: 100, default: 5)", 441 | ) 442 | 443 | parser.add_argument("-d", "--debug", action="store_true", help="Show debug log messages") 444 | args = parser.parse_args() 445 | 446 | if args.jog_sensitivity and 0 < int(args.jog_sensitivity) <= 100: 447 | jog_sensitivity = int(args.jog_sensitivity) * 0.001 448 | else: 449 | print("Jog sensitivity must be between 1 and 100. Using default value (5).") 450 | 451 | detect_alsa_device() 452 | traktor_s4 = detect_controller_device() 453 | midiin = rtmidi.MidiIn(name="blaxpot") 454 | inport = midiin.open_virtual_port(name="traktor-s4-mk1-midify") 455 | inport.set_callback(handle_midi_input, args) 456 | midiout = rtmidi.MidiOut(name="blaxpot") 457 | outport = midiout.open_virtual_port(name="traktor-s4-mk1-midify") 458 | shift_a = False 459 | shift_b = False 460 | toggle_ac = False 461 | toggle_bd = False 462 | control_values = [None for _ in range(350)] 463 | 464 | controller_data = { 465 | "jog_a": { 466 | "counter": 0, 467 | "prev_control_value": None, 468 | "sensitivity": jog_sensitivity, 469 | "updated": time.time(), 470 | }, 471 | "jog_b": { 472 | "counter": 0, 473 | "prev_control_value": None, 474 | "sensitivity": jog_sensitivity, 475 | "updated": time.time(), 476 | }, 477 | "browse_rot": { 478 | "counter": 0, 479 | "prev_control_value": None, 480 | "updated": time.time(), 481 | }, 482 | } 483 | 484 | for control in ["move", "size", "gain"]: 485 | for deck in ["a", "b", "c", "d"]: 486 | rotary_encoder = "{}_rot_{}".format(control, deck) 487 | 488 | controller_data[rotary_encoder] = { 489 | "counter": 0, 490 | "prev_control_value": None, 491 | "updated": time.time(), 492 | } 493 | 494 | for event in traktor_s4.read_loop(): 495 | # TODO: When the following controls are used, it doesn't look like any events are sent. This looks to be caused 496 | # by bugs in the snd-usb-caiaq module. Some events are recieved when the controls are used, but their evcodes 497 | # are for other controls and their values don't change. Investigate. 498 | 499 | # TODO: The footswitch doesn't send any events. It's hard to guess what evcode to expect here. 500 | 501 | # TODO: The HI eq pot on deck C doesn't seem to send any event values either. Expect evcode 47, values 0-4095. 502 | 503 | # TODO: Likewise for the loop recorder dry / wet pot. Expect evcode 20, values 0-4095. 504 | 505 | # Ignore events which don't change control values 506 | if event.value == control_values[event.code]: 507 | continue 508 | 509 | control_values[event.code] = event.value 510 | 511 | if args.debug: 512 | print( 513 | "[Processing event] Code: {}, Value: {}, Timestamp: {}".format( 514 | event.code, event.value, event.timestamp() 515 | ) 516 | ) 517 | 518 | # Handle modifier key event codes 519 | if event.code == 257: 520 | shift_a = not shift_a 521 | continue 522 | 523 | if event.code == 264 and event.value: 524 | toggle_ac = not toggle_ac 525 | continue 526 | 527 | if event.code == 313: 528 | shift_b = not shift_b 529 | continue 530 | 531 | if event.code == 304 and event.value: 532 | toggle_bd = not toggle_bd 533 | continue 534 | 535 | # TODO: Handle LED controls that don't respond to MIDI messages on macOS/Windows e.g. deck toggles, shift 536 | # buttons, deck active LEDs, etc. 537 | 538 | midi = evcode_to_midi(event.code, shift_a, shift_b, toggle_ac, toggle_bd) 539 | 540 | # Ignore events with no corresponding MIDI control defined 541 | if midi is None: 542 | continue 543 | 544 | value, controller_data = calculate_midi_value_update_controller_data( 545 | event, controller_data, toggle_ac, toggle_bd 546 | ) 547 | 548 | # Don't send a MIDI message if calculate_midi_value_update_controller_data doesn't return an appropriate value. 549 | # Not all events should trigger a MIDI message, even if they report that a control value has changed. For 550 | # instance, it's desireable to rate limit the number of messages sent when the jog wheels are moved (since 551 | # this generates a lot of events). 552 | if value is None: 553 | continue 554 | 555 | outport.send_message([midi[1], midi[0], value]) 556 | 557 | if args.debug: 558 | print("[Sent MIDI message] Channel: {}, CC: {}, Value: {}".format(hex(midi[1]), hex(midi[0]), hex(value))) 559 | 560 | inport.close_port() 561 | midiin.delete() 562 | outport.close_port() 563 | midiout.delete() 564 | traktor_s4.close() 565 | 566 | 567 | def print_events(): 568 | traktor_s4 = detect_controller_device() 569 | control_values = [None for _ in range(350)] 570 | 571 | for event in traktor_s4.read_loop(): 572 | if event.value == control_values[event.code]: 573 | continue 574 | 575 | control_values[event.code] = event.value 576 | print(event) 577 | 578 | traktor_s4.close() 579 | --------------------------------------------------------------------------------