diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 0b681ea9531..aff1a30f7aa 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -33,6 +33,7 @@ target_sources(app PRIVATE src/events/activity_state_changed.c) target_sources(app PRIVATE src/events/position_state_changed.c) target_sources(app PRIVATE src/events/sensor_event.c) target_sources(app PRIVATE src/events/mouse_button_state_changed.c) +target_sources(app PRIVATE src/events/midi_key_state_changed.c) target_sources_ifdef(CONFIG_ZMK_WPM app PRIVATE src/events/wpm_state_changed.c) target_sources_ifdef(CONFIG_USB_DEVICE_STACK app PRIVATE src/events/usb_conn_state_changed.c) target_sources(app PRIVATE src/behaviors/behavior_reset.c) @@ -72,6 +73,14 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources(app PRIVATE src/events/keycode_state_changed.c) target_sources_ifdef(CONFIG_ZMK_HID_INDICATORS app PRIVATE src/hid_indicators.c) + if (CONFIG_ZMK_MIDI) + target_sources(app PRIVATE src/midi.c) + target_sources(app PRIVATE src/midi_listener.c) + target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_MIDI_KEY_PRESS app PRIVATE src/behaviors/behavior_midi_key_press.c) + target_sources_ifdef(CONFIG_ZMK_USB app PRIVATE src/usb_midi.c) + target_sources_ifdef(CONFIG_ZMK_USB app PRIVATE src/usb_midi_packet.c) + endif() + if (CONFIG_ZMK_BLE) target_sources(app PRIVATE src/events/ble_active_profile_changed.c) target_sources(app PRIVATE src/behaviors/behavior_bt.c) diff --git a/app/Kconfig b/app/Kconfig index 5aedd9d9030..f5093fd55bb 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -375,6 +375,14 @@ config ZMK_MOUSE #Mouse Options endmenu +menu "USB MIDI Options" + +config ZMK_MIDI + bool "Enable ZMK midi emulation" + +#MIDI Options +endmenu + menu "Power Management" config ZMK_BATTERY_REPORTING diff --git a/app/Kconfig.behaviors b/app/Kconfig.behaviors index c9754bf7d83..3ddd1941359 100644 --- a/app/Kconfig.behaviors +++ b/app/Kconfig.behaviors @@ -17,6 +17,12 @@ config ZMK_BEHAVIOR_SOFT_OFF default y depends on DT_HAS_ZMK_BEHAVIOR_SOFT_OFF_ENABLED && ZMK_PM_SOFT_OFF +config ZMK_BEHAVIOR_MIDI_KEY_PRESS + bool + default y + depends on DT_HAS_ZMK_BEHAVIOR_MIDI_KEY_PRESS_ENABLED + imply ZMK_MIDI + config ZMK_BEHAVIOR_SENSOR_ROTATE_COMMON bool @@ -35,4 +41,4 @@ config ZMK_BEHAVIOR_SENSOR_ROTATE_VAR config ZMK_BEHAVIOR_MACRO bool default y - depends on DT_HAS_ZMK_BEHAVIOR_MACRO_ENABLED || DT_HAS_ZMK_BEHAVIOR_MACRO_ONE_PARAM_ENABLED || DT_HAS_ZMK_BEHAVIOR_MACRO_TWO_PARAM_ENABLED \ No newline at end of file + depends on DT_HAS_ZMK_BEHAVIOR_MACRO_ENABLED || DT_HAS_ZMK_BEHAVIOR_MACRO_ONE_PARAM_ENABLED || DT_HAS_ZMK_BEHAVIOR_MACRO_TWO_PARAM_ENABLED diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index fde75271891..661cf2937fa 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -21,3 +21,5 @@ #include #include #include +#include + diff --git a/app/dts/behaviors/midi_key_press.dtsi b/app/dts/behaviors/midi_key_press.dtsi new file mode 100644 index 00000000000..39013dd87b1 --- /dev/null +++ b/app/dts/behaviors/midi_key_press.dtsi @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +/ { + behaviors { + /omit-if-no-ref/ midi: midi_key_press { + compatible = "zmk,behavior-midi-key-press"; + #binding-cells = <1>; + }; + }; +}; \ No newline at end of file diff --git a/app/dts/bindings/behaviors/zmk,behavior-midi-key-press.yaml b/app/dts/bindings/behaviors/zmk,behavior-midi-key-press.yaml new file mode 100644 index 00000000000..c89363c092f --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-midi-key-press.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2024 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Midi key press/release behavior + +compatible: "zmk,behavior-midi-key-press" + +include: one_param.yaml diff --git a/app/include/dt-bindings/zmk/midi.h b/app/include/dt-bindings/zmk/midi.h new file mode 100644 index 00000000000..3266911c3f6 --- /dev/null +++ b/app/include/dt-bindings/zmk/midi.h @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include + +// NOTE MIDI octave index is +2 vs "normal" octave index +// so standard C3 is MIDI C5 here + +// All NOTE_* have the identical keycode and midi code +#define NOTE_C 0x0 +#define NOTE_Cs 0x1 +#define NOTE_Db NOTE_Cs +#define NOTE_D 0x2 +#define NOTE_Ds 0x3 +#define NOTE_Eb NOTE_Ds +#define NOTE_E 0x4 +#define NOTE_F 0x5 +#define NOTE_Fs 0x6 +#define NOTE_Gb NOTE_Fs +#define NOTE_G 0x7 +#define NOTE_Gs 0x8 +#define NOTE_Ab NOTE_Gs +#define NOTE_A 0x9 +#define NOTE_As 0xa +#define NOTE_Bb NOTE_As +#define NOTE_B 0xb + +#define NOTE_C_1 0xc +#define NOTE_Cs_1 0xd +#define NOTE_Db_1 NOTE_Cs_1 +#define NOTE_D_1 0xe +#define NOTE_Ds_1 0xf +#define NOTE_Eb_1 NOTE_Ds_1 +#define NOTE_E_1 0x10 +#define NOTE_F_1 0x11 +#define NOTE_Fs_1 0x12 +#define NOTE_Gb_1 NOTE_Fs_1 +#define NOTE_G_1 0x13 +#define NOTE_Gs_1 0x14 +#define NOTE_Ab_1 NOTE_Gs_1 +#define NOTE_A_1 0x15 +#define NOTE_As_1 0x16 +#define NOTE_Bb_1 NOTE_As_1 +#define NOTE_B_1 0x17 + +#define NOTE_C_2 0x18 +#define NOTE_Cs_2 0x19 +#define NOTE_Db_2 NOTE_Cs_2 +#define NOTE_D_2 0x1a +#define NOTE_Ds_2 0x1b +#define NOTE_Eb_2 NOTE_Ds_2 +#define NOTE_E_2 0x1c +#define NOTE_F_2 0x1d +#define NOTE_Fs_2 0x1e +#define NOTE_Gb_2 NOTE_Fs_2 +#define NOTE_G_2 0x1f +#define NOTE_Gs_2 0x20 +#define NOTE_Ab_2 NOTE_Gs_2 +#define NOTE_A_2 0x21 +#define NOTE_As_2 0x22 +#define NOTE_Bb_2 NOTE_As_2 +#define NOTE_B_2 0x23 + +#define NOTE_C_3 0x24 +#define NOTE_Cs_3 0x25 +#define NOTE_Db_3 NOTE_Cs_3 +#define NOTE_D_3 0x26 +#define NOTE_Ds_3 0x27 +#define NOTE_Eb_3 NOTE_Ds_3 +#define NOTE_E_3 0x28 +#define NOTE_F_3 0x29 +#define NOTE_Fs_3 0x2a +#define NOTE_Gb_3 NOTE_Fs_3 +#define NOTE_G_3 0x2b +#define NOTE_Gs_3 0x2c +#define NOTE_Ab_3 NOTE_Gs_3 +#define NOTE_A_3 0x2d +#define NOTE_As_3 0x2e +#define NOTE_Bb_3 NOTE_As_3 +#define NOTE_B_3 0x2f + +#define NOTE_C_4 0x30 +#define NOTE_Cs_4 0x31 +#define NOTE_Db_4 NOTE_Cs_4 +#define NOTE_D_4 0x32 +#define NOTE_Ds_4 0x33 +#define NOTE_Eb_4 NOTE_Ds_4 +#define NOTE_E_4 0x34 +#define NOTE_F_4 0x35 +#define NOTE_Fs_4 0x36 +#define NOTE_Gb_4 NOTE_Fs_4 +#define NOTE_G_4 0x37 +#define NOTE_Gs_4 0x38 +#define NOTE_Ab_4 NOTE_Gs_4 +#define NOTE_A_4 0x39 +#define NOTE_As_4 0x3a +#define NOTE_Bb_4 NOTE_As_4 +#define NOTE_B_4 0x3b + +#define NOTE_C_5 0x3c +#define NOTE_Cs_5 0x3d +#define NOTE_Db_5 NOTE_Cs_5 +#define NOTE_D_5 0x3e +#define NOTE_Ds_5 0x3f +#define NOTE_Eb_5 NOTE_Ds_5 +#define NOTE_E_5 0x40 +#define NOTE_F_5 0x41 +#define NOTE_Fs_5 0x42 +#define NOTE_Gb_5 NOTE_Fs_5 +#define NOTE_G_5 0x43 +#define NOTE_Gs_5 0x44 +#define NOTE_Ab_5 NOTE_Gs_5 +#define NOTE_A_5 0x45 +#define NOTE_As_5 0x46 +#define NOTE_Bb_5 NOTE_As_5 +#define NOTE_B_5 0x47 + +#define NOTE_C_6 0x48 +#define NOTE_Cs_6 0x49 +#define NOTE_Db_6 NOTE_Cs_6 +#define NOTE_D_6 0x4a +#define NOTE_Ds_6 0x4b +#define NOTE_Eb_6 NOTE_Ds_6 +#define NOTE_E_6 0x4c +#define NOTE_F_6 0x4d +#define NOTE_Fs_6 0x4e +#define NOTE_Gb_6 NOTE_Fs_6 +#define NOTE_G_6 0x4f +#define NOTE_Gs_6 0x50 +#define NOTE_Ab_6 NOTE_Gs_6 +#define NOTE_A_6 0x51 +#define NOTE_As_6 0x52 +#define NOTE_Bb_6 NOTE_As_6 +#define NOTE_B_6 0x53 + +#define NOTE_C_7 0x54 +#define NOTE_Cs_7 0x55 +#define NOTE_Db_7 NOTE_Cs_7 +#define NOTE_D_7 0x56 +#define NOTE_Ds_7 0x57 +#define NOTE_Eb_7 NOTE_Ds_7 +#define NOTE_E_7 0x58 +#define NOTE_F_7 0x59 +#define NOTE_Fs_7 0x5a +#define NOTE_Gb_7 NOTE_Fs_7 +#define NOTE_G_7 0x5b +#define NOTE_Gs_7 0x5c +#define NOTE_Ab_7 NOTE_Gs_7 +#define NOTE_A_7 0x5d +#define NOTE_As_7 0x5e +#define NOTE_Bb_7 NOTE_As_7 +#define NOTE_B_7 0x5f + +#define NOTE_C_8 0x60 +#define NOTE_Cs_8 0x61 +#define NOTE_Db_8 NOTE_Cs_8 +#define NOTE_D_8 0x62 +#define NOTE_Ds_8 0x63 +#define NOTE_Eb_8 NOTE_Ds_8 +#define NOTE_E_8 0x64 +#define NOTE_F_8 0x65 +#define NOTE_Fs_8 0x66 +#define NOTE_Gb_8 NOTE_Fs_8 +#define NOTE_G_8 0x67 +#define NOTE_Gs_8 0x68 +#define NOTE_Ab_8 NOTE_Gs_8 +#define NOTE_A_8 0x69 +#define NOTE_As_8 0x6a +#define NOTE_Bb_8 NOTE_As_8 +#define NOTE_B_8 0x6b + +#define NOTE_C_9 0x6c +#define NOTE_Cs_9 0x6d +#define NOTE_Db_9 NOTE_Cs_9 +#define NOTE_D_9 0x6e +#define NOTE_Ds_9 0x6f +#define NOTE_Eb_9 NOTE_Ds_9 +#define NOTE_E_9 0x70 +#define NOTE_F_9 0x71 +#define NOTE_Fs_9 0x72 +#define NOTE_Gb_9 NOTE_Fs_9 +#define NOTE_G_9 0x73 +#define NOTE_Gs_9 0x74 +#define NOTE_Ab_9 NOTE_Gs_9 +#define NOTE_A_9 0x75 +#define NOTE_As_9 0x76 +#define NOTE_Bb_9 NOTE_As_9 +#define NOTE_B_9 0x77 + +#define NOTE_C_10 0x78 +#define NOTE_Cs_10 0x79 +#define NOTE_Db_10 NOTE_Cs_10 +#define NOTE_D_10 0x7a +#define NOTE_Ds_10 0x7b +#define NOTE_Eb_10 NOTE_Ds_10 +#define NOTE_E_10 0x7c +#define NOTE_F_10 0x7d +#define NOTE_Fs_10 0x7e +#define NOTE_Gb_10 NOTE_Fs_10 +#define NOTE_G_10 0x7f +// 0x7f aka 127 is the max value + +// NOTE sentinals +#define MIDI_MIN_NOTE NOTE_C +#define MIDI_MAX_NOTE NOTE_G_10 +#define MIDI_INVALID 0xFF + +// Midi control change keycodes +// appended with 0xB0 +#define SUSTAIN 0xB040 +#define PORTAMENTO 0xB041 +#define SOSTENUTO 0xB042 +#define OCT_UP 0xB081 +#define OCT_DOWN 0xB082 + +// midi control sentinals +#define MIDI_MIN_CONTROL SUSTAIN +#define MIDI_MAX_CONTROL OCT_DOWN diff --git a/app/include/zmk/endpoints.h b/app/include/zmk/endpoints.h index f2aff2bcc2d..1e5b372e565 100644 --- a/app/include/zmk/endpoints.h +++ b/app/include/zmk/endpoints.h @@ -75,3 +75,7 @@ int zmk_endpoints_send_mouse_report(); #endif // IS_ENABLE(CONFIG_ZMK_MOUSE) void zmk_endpoints_clear_current(void); + +#if IS_ENABLED(CONFIG_ZMK_MIDI) +int zmk_endpoints_send_midi_report(); +#endif // IS_ENABLE(CONFIG_ZMK_MIDI) diff --git a/app/include/zmk/events/midi_key_state_changed.h b/app/include/zmk/events/midi_key_state_changed.h new file mode 100644 index 00000000000..134be33cb96 --- /dev/null +++ b/app/include/zmk/events/midi_key_state_changed.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include +#include + +struct zmk_midi_key_state_changed { + zmk_midi_key_t key; + bool state; + int64_t timestamp; +}; + +ZMK_EVENT_DECLARE(zmk_midi_key_state_changed); + +static inline struct zmk_midi_key_state_changed +zmk_midi_key_state_changed_from_encoded(uint32_t encoded, bool pressed, int64_t timestamp) { + + // no decoding necessary + zmk_midi_key_t id = encoded; + + return (struct zmk_midi_key_state_changed){.key = id, .state = pressed, .timestamp = timestamp}; +} + +static inline int raise_zmk_midi_key_state_changed_from_encoded(uint32_t encoded, bool pressed, + int64_t timestamp) { + return raise_zmk_midi_key_state_changed( + zmk_midi_key_state_changed_from_encoded(encoded, pressed, timestamp)); +} \ No newline at end of file diff --git a/app/include/zmk/midi.h b/app/include/zmk/midi.h new file mode 100644 index 00000000000..f0f78cbcf19 --- /dev/null +++ b/app/include/zmk/midi.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include + +// in hid.c +#define ZMK_MIDI_NUM_KEYS 0x100 + +// should come after the last ZMK_HID_REPORT_ID in hid.h +#define ZMK_REPORT_ID_MIDI 0x04 + +#define ZMK_MIDI_CIN_NOTE_ON 0x90 +#define ZMK_MIDI_CIN_NOTE_OFF 0x80 +#define ZMK_MIDI_CIN_CONTROL_CHANGE 0xB0 +#define ZMK_MIDI_CIN_PITCH_BEND_CHANGE 0xE0 + +#define ZMK_MIDI_MAX_VELOCITY 0x7F +#define ZMK_MIDI_ON_VELOCITY 0x3F +#define ZMK_MIDI_OFF_VELOCITY 0x64 + +#define ZMK_MIDI_TOGGLE_ON 0x7F +#define ZMK_MIDI_TOGGLE_OFF 0x0 + +// Analogous to zmk_hid_mouse_report_body in hid.h +struct zmk_midi_key_report_body { + zmk_midi_cin_t cin; + zmk_midi_key_t key; + zmk_midi_value_t key_value; +} __packed; + +// Analogous to zmk_hid_mouse_report in hid.h +struct zmk_midi_report { + uint8_t report_id; + struct zmk_midi_key_report_body body; +} __packed; + +// Analogous to zmk_hid_mouse* in hid.h +int zmk_midi_key_press(zmk_midi_key_t key); +int zmk_midi_key_release(zmk_midi_key_t key); +void zmk_midi_clear(void); + +// Analogous to zmk_hid_get_mouse_report in hid.h +struct zmk_midi_report *zmk_get_midi_report(); diff --git a/app/include/zmk/midi_keys.h b/app/include/zmk/midi_keys.h new file mode 100644 index 00000000000..499523b19a8 --- /dev/null +++ b/app/include/zmk/midi_keys.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +typedef uint8_t zmk_midi_cin_t; +typedef uint16_t zmk_midi_key_t; +typedef uint8_t zmk_midi_value_t; + +// used for bitmaps +typedef uint64_t zmk_midi_keys_t; diff --git a/app/include/zmk/usb_hid.h b/app/include/zmk/usb_hid.h index c0cbc08a7ad..b73d0456a29 100644 --- a/app/include/zmk/usb_hid.h +++ b/app/include/zmk/usb_hid.h @@ -13,4 +13,7 @@ int zmk_usb_hid_send_consumer_report(void); #if IS_ENABLED(CONFIG_ZMK_MOUSE) int zmk_usb_hid_send_mouse_report(void); #endif // IS_ENABLED(CONFIG_ZMK_MOUSE) +#if IS_ENABLED(CONFIG_ZMK_MIDI) +int zmk_usb_hid_send_midi_report(void); +#endif // IS_ENABLED(CONFIG_ZMK_MIDI) void zmk_usb_hid_set_protocol(uint8_t protocol); diff --git a/app/include/zmk/usb_midi.h b/app/include/zmk/usb_midi.h new file mode 100644 index 00000000000..a83768f178e --- /dev/null +++ b/app/include/zmk/usb_midi.h @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include +#include + +// TODO can add this back as a config option, but for now hardcode it +#define USB_MIDI_NUM_INPUTS 1 +#define USB_MIDI_NUM_OUTPUTS 1 + +#define USB_MIDI_DEFAULT_CABLE_NUM 0 +#define USB_MIDI_MAX_NUM_BYTES 3 + +// TODO: these are hardcoded here for usb_write, but bEndpointAddress actually get assigned +// automatically in the usb configs hard coding them in the usb configs doesn't seem to help, so we +// don't have a good way of ensuring that the endpoint addresses defined here actually match what +// zephyr gives our endpoints you can see what the endpoint addresses are by doing "cat +// /sys/kernel/debug/usb/devices" when the device is plugged in eventually we should find a way to +// get this information back out of the usb device configuration +#define ZMK_USB_MIDI_EP_IN 0x81 +#define ZMK_USB_MIDI_EP_OUT 0x01 + +/* Require at least one jack */ +BUILD_ASSERT((USB_MIDI_NUM_INPUTS + USB_MIDI_NUM_OUTPUTS > 0), + "USB MIDI device must have more than 0 jacks"); + +/** + * MS (MIDI streaming) Class-Specific Interface Descriptor Subtypes. + * See table A.1 in the spec. + */ +enum usb_midi_if_desc_subtype { + USB_MIDI_IF_DESC_UNDEFINED = 0x00, + USB_MIDI_IF_DESC_MS_HEADER = 0x01, + USB_MIDI_IF_DESC_MIDI_IN_JACK = 0x02, + USB_MIDI_IF_DESC_MIDI_OUT_JACK = 0x03, + USB_MIDI_IF_DESC_ELEMENT = 0x04 +}; + +/** + * MS Class-Specific Endpoint Descriptor Subtypes. + * See table A.2 in the spec. + */ +enum usb_midi_ep_desc_subtype { + USB_MIDI_EP_DESC_UNDEFINED = 0x00, + USB_MIDI_EP_DESC_MS_GENERAL = 0x01 +}; + +/** + * MS MIDI IN and OUT Jack types. + * See table A.3 in the spec. + */ +enum usb_midi_jack_type { + USB_MIDI_JACK_TYPE_UNDEFINED = 0x00, + USB_MIDI_JACK_TYPE_EMBEDDED = 0x01, + USB_MIDI_JACK_TYPE_EXTERNAL = 0x02 +}; + +#define USB_MIDI_AUDIO_INTERFACE_CLASS 0x01 +#define USB_MIDI_MIDISTREAMING_INTERFACE_SUBCLASS 0x03 +#define USB_MIDI_AUDIOCONTROL_INTERFACE_SUBCLASS 0x01 + +/** + * USB MIDI input pin. + */ +struct usb_midi_input_pin { + uint8_t baSourceID; + uint8_t baSourcePin; +} __packed; + +/** + * Class-specific AC (audio control) Interface Descriptor. + */ +struct usb_midi_ac_if_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint16_t bcdADC; + uint16_t wTotalLength; + uint8_t bInCollection; + uint8_t baInterfaceNr; +} __packed; + +/** + * Class-Specific MS Interface Header Descriptor. + * See table 6.2 in the spec. + */ +struct usb_midi_ms_if_descriptor { + /** Size of this descriptor, in bytes */ + uint8_t bLength; + /** CS_INTERFACE descriptor type */ + uint8_t bDescriptorType; + /** MS_HEADER descriptor subtype. */ + uint8_t bDescriptorSubtype; + /** + * MIDIStreaming SubClass Specification Release Number in + * Binary-Coded Decimal. Currently 01.00. + */ + uint16_t BcdADC; + /** + * Total number of bytes returned for the class-specific + * MIDIStreaming interface descriptor. Includes the combined + * length of this descriptor header and all Jack and Element descriptors. + */ + uint16_t wTotalLength; +} __packed; + +/** + * MIDI IN Jack Descriptor. See table 6.3 in the spec. + */ +struct usb_midi_in_jack_descriptor { + /** Size of this descriptor, in bytes. */ + uint8_t bLength; + /** CS_INTERFACE descriptor type. */ + uint8_t bDescriptorType; + /** MIDI_IN_JACK descriptor subtype. */ + uint8_t bDescriptorSubtype; + /** EMBEDDED or EXTERNAL */ + uint8_t bJackType; + /** + * Constant uniquely identifying the MIDI IN Jack within + * the USB-MIDI function. + */ + uint8_t bJackID; + /** Index of a string descriptor, describing the MIDI IN Jack. */ + uint8_t iJack; +} __packed; + +/** + * MIDI OUT Jack Descriptor. See table 6.4 in the spec. + */ +struct usb_midi_out_jack_descriptor { + /** Size of this descriptor, in bytes: */ + uint8_t bLength; + /** CS_INTERFACE descriptor type. */ + uint8_t bDescriptorType; + /** MIDI_OUT_JACK descriptor subtype. */ + uint8_t bDescriptorSubtype; + /** EMBEDDED or EXTERNAL */ + uint8_t bJackType; + /** + * Constant uniquely identifying the MIDI OUT Jack + * within the USB-MIDI function. + */ + uint8_t bJackID; + /** + * Number of Input Pins of this MIDI OUT Jack + * (assumed to be 1 in this implementation). + */ + uint8_t bNrInputPins; + /** ID and source pin of the entity to which this jack is connected. */ + struct usb_midi_input_pin input_pin; + /** Index of a string descriptor, describing the MIDI OUT Jack. */ + uint8_t iJack; +} __packed; + +/** + * The same as Zephyr's usb_ep_descriptor but with two additional fields + * to match the USB MIDI spec. + */ +struct usb_ep_descriptor_padded { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bEndpointAddress; + uint8_t bmAttributes; + uint16_t wMaxPacketSize; + uint8_t bInterval; + /* The following two attributes were added to match the USB MIDI spec. */ + uint8_t bRefresh; + uint8_t bSynchAddress; +} __packed; + +/** + * Class-Specific MS Bulk Data Endpoint Descriptor + * corresponding to a MIDI output. See table 6-7 in the spec. + */ +struct usb_midi_bulk_out_ep_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bNumEmbMIDIJack; + uint8_t BaAssocJackID[USB_MIDI_NUM_INPUTS]; +} __packed; + +/** + * Class-Specific MS Bulk Data Endpoint Descriptor + * corresponding to a MIDI input. See table 6-7 in the spec. + */ +struct usb_midi_bulk_in_ep_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bNumEmbMIDIJack; + uint8_t BaAssocJackID[USB_MIDI_NUM_OUTPUTS]; +} __packed; + +#define USB_MIDI_ELEMENT_CAPS_COUNT 1 + +/** + * Element descriptor. See table 6-5 in the spec. + */ +struct usb_midi_element_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubtype; + uint8_t bElementID; + uint8_t bNrInputPins; + + struct usb_midi_input_pin input_pins[USB_MIDI_NUM_INPUTS]; + uint8_t bNrOutputPins; + uint8_t bInTerminalLink; + uint8_t bOutTerminalLink; + uint8_t bElCapsSize; + uint8_t bmElementCaps[USB_MIDI_ELEMENT_CAPS_COUNT]; + uint8_t iElement; +} __packed; + +/** + * A complete set of descriptors for a USB MIDI device without physical jacks. + */ +struct usb_midi_config { + struct usb_if_descriptor ac_if; + struct usb_midi_ac_if_descriptor ac_cs_if; + struct usb_if_descriptor ms_if; + struct usb_midi_ms_if_descriptor ms_cs_if; + struct usb_midi_in_jack_descriptor in_jacks_emb[USB_MIDI_NUM_INPUTS]; + struct usb_midi_out_jack_descriptor out_jacks_emb[USB_MIDI_NUM_OUTPUTS]; + struct usb_midi_element_descriptor element; + struct usb_ep_descriptor_padded out_ep; + struct usb_midi_bulk_out_ep_descriptor out_cs_ep; + struct usb_ep_descriptor_padded in_ep; + struct usb_midi_bulk_in_ep_descriptor in_cs_ep; +} __packed; + +/* No jack string descriptors by default */ +#define INPUT_JACK_STRING_DESCR_IDX(jack_idx) 0 +#define OUTPUT_JACK_STRING_DESCR_IDX(jack_idx) 0 + +/* Audio control interface descriptor */ +#define INIT_AC_IF \ + { \ + .bLength = sizeof(struct usb_if_descriptor), .bDescriptorType = USB_DESC_INTERFACE, \ + .bInterfaceNumber = 0, .bAlternateSetting = 0, .bNumEndpoints = 0, \ + .bInterfaceClass = USB_MIDI_AUDIO_INTERFACE_CLASS, \ + .bInterfaceSubClass = USB_MIDI_AUDIOCONTROL_INTERFACE_SUBCLASS, \ + .bInterfaceProtocol = 0x00, .iInterface = 0x00 \ + } + +/* Class specific audio control interface descriptor */ +#define INIT_AC_CS_IF \ + { \ + .bLength = sizeof(struct usb_midi_ac_if_descriptor), \ + .bDescriptorType = USB_DESC_CS_INTERFACE, .bDescriptorSubtype = 0x01, .bcdADC = 0x0100, \ + .wTotalLength = sizeof(struct usb_midi_ac_if_descriptor), .bInCollection = 0x01, \ + .baInterfaceNr = 0x01 \ + } + +/* MIDI streaming interface descriptor */ +#define INIT_MS_IF \ + { \ + .bLength = sizeof(struct usb_if_descriptor), .bDescriptorType = USB_DESC_INTERFACE, \ + .bInterfaceNumber = 0x01, .bAlternateSetting = 0x00, .bNumEndpoints = 2, \ + .bInterfaceClass = USB_MIDI_AUDIO_INTERFACE_CLASS, \ + .bInterfaceSubClass = USB_MIDI_MIDISTREAMING_INTERFACE_SUBCLASS, \ + .bInterfaceProtocol = 0x00, .iInterface = 0x00 \ + } + +/* Class specific MIDI streaming interface descriptor */ +#define INIT_MS_CS_IF \ + { \ + .bLength = sizeof(struct usb_midi_ms_if_descriptor), \ + .bDescriptorType = USB_DESC_CS_INTERFACE, .bDescriptorSubtype = 0x01, .BcdADC = 0x0100, \ + .wTotalLength = MIDI_MS_IF_DESC_TOTAL_SIZE \ + } + +/* Embedded MIDI input jack */ +#define INIT_IN_JACK(idx, idx_offset) \ + { \ + .bLength = sizeof(struct usb_midi_in_jack_descriptor), \ + .bDescriptorType = USB_DESC_CS_INTERFACE, \ + .bDescriptorSubtype = USB_MIDI_IF_DESC_MIDI_IN_JACK, \ + .bJackType = USB_MIDI_JACK_TYPE_EMBEDDED, .bJackID = 1 + idx + idx_offset, \ + .iJack = INPUT_JACK_STRING_DESCR_IDX(idx), \ + } + +/* Embedded MIDI output jack */ +#define INIT_OUT_JACK(idx, jack_id_idx_offset) \ + { \ + .bLength = sizeof(struct usb_midi_out_jack_descriptor), \ + .bDescriptorType = USB_DESC_CS_INTERFACE, \ + .bDescriptorSubtype = USB_MIDI_IF_DESC_MIDI_OUT_JACK, \ + .bJackType = USB_MIDI_JACK_TYPE_EMBEDDED, .bJackID = 1 + idx + jack_id_idx_offset, \ + .bNrInputPins = 0x01, \ + .input_pin = \ + { \ + .baSourceID = ELEMENT_ID, \ + .baSourcePin = 1 + idx, \ + }, \ + .iJack = OUTPUT_JACK_STRING_DESCR_IDX(idx) \ + } + +/* Out endpoint */ +#define INIT_OUT_EP \ + { \ + .bLength = sizeof(struct usb_ep_descriptor_padded), .bDescriptorType = USB_DESC_ENDPOINT, \ + .bEndpointAddress = ZMK_USB_MIDI_EP_OUT, .bmAttributes = 0x02, .wMaxPacketSize = 0x0040, \ + .bInterval = 0x00, .bRefresh = 0x00, .bSynchAddress = 0x00, \ + } + +/* In endpoint */ +#define INIT_IN_EP \ + { \ + .bLength = sizeof(struct usb_ep_descriptor_padded), .bDescriptorType = USB_DESC_ENDPOINT, \ + .bEndpointAddress = ZMK_USB_MIDI_EP_IN, .bmAttributes = 0x02, .wMaxPacketSize = 0x0040, \ + .bInterval = 0x00, .bRefresh = 0x00, .bSynchAddress = 0x00, \ + } + +#define ELEMENT_ID 0xf0 +#define IDX_WITH_OFFSET(index, offset) (index + offset) +#define INIT_INPUT_PIN(index, offset) \ + { .baSourceID = (index + offset), .baSourcePin = 1 } + +#define INIT_ELEMENT \ + { \ + .bLength = sizeof(struct usb_midi_element_descriptor), \ + .bDescriptorType = USB_DESC_CS_INTERFACE, .bDescriptorSubtype = USB_MIDI_IF_DESC_ELEMENT, \ + .bElementID = ELEMENT_ID, .bNrInputPins = USB_MIDI_NUM_INPUTS, \ + .input_pins = {LISTIFY(USB_MIDI_NUM_INPUTS, INIT_INPUT_PIN, (, ), 1)}, \ + .bNrOutputPins = USB_MIDI_NUM_OUTPUTS, .bInTerminalLink = 0, .bOutTerminalLink = 0, \ + .bElCapsSize = 1, .bmElementCaps = 1, .iElement = 0 \ + } + +/* Value for the wTotalLength field of the class-specific MS Interface Descriptor, + i.e the total number of bytes following that descriptor. */ +#define MIDI_MS_IF_DESC_TOTAL_SIZE \ + (sizeof(struct usb_midi_in_jack_descriptor) * USB_MIDI_NUM_INPUTS + \ + sizeof(struct usb_midi_out_jack_descriptor) * USB_MIDI_NUM_OUTPUTS + \ + sizeof(struct usb_midi_element_descriptor) + sizeof(struct usb_ep_descriptor_padded) + \ + sizeof(struct usb_midi_bulk_out_ep_descriptor) + sizeof(struct usb_ep_descriptor_padded) + \ + sizeof(struct usb_midi_bulk_in_ep_descriptor)) + +int zmk_usb_send_midi_report(struct zmk_midi_key_report_body *body); diff --git a/app/include/zmk/usb_midi_packet.h b/app/include/zmk/usb_midi_packet.h new file mode 100644 index 00000000000..d5c020e7c10 --- /dev/null +++ b/app/include/zmk/usb_midi_packet.h @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include + +enum usb_midi_error_t { + USB_MIDI_SUCCESS = 0, + USB_MIDI_ERROR_INVALID_CIN = -1, + USB_MIDI_ERROR_INVALID_CABLE_NUM = -2, + USB_MIDI_ERROR_INVALID_MIDI_MSG = -3 +}; + +/* Code Index Numbers. See table 4-1 in the spec. */ +enum usb_midi_cin_t { + /* Miscellaneous function codes. Reserved for future extensions. */ + USB_MIDI_CIN_MISC = 0x0, + /* Cable events. Reserved for future expansion. */ + USB_MIDI_CIN_CABLE_EVENT = 0x1, + /* Two-byte System Common messages like MTC, SongSelect, etc. */ + USB_MIDI_CIN_SYSCOM_2BYTE = 0x2, + /* Three-byte System Common messages like SPP, etc. */ + USB_MIDI_CIN_SYSCOM_3BYTE = 0x3, + /* SysEx starts or continues */ + USB_MIDI_CIN_SYSEX_START_OR_CONTINUE = 0x4, + /* Single-byte System Common Message or SysEx ends with following single byte. */ + USB_MIDI_CIN_SYS_COMMON_OR_SYSEX_END_1BYTE = 0x5, + /* SysEx ends with following two bytes. */ + USB_MIDI_CIN_SYSEX_END_2BYTE = 0x6, + /* SysEx ends with following three bytes. */ + USB_MIDI_CIN_SYSEX_END_3BYTE = 0x7, + /* Note-off */ + USB_MIDI_CIN_NOTE_ON = 0x8, + /* Note-on */ + USB_MIDI_CIN_NOTE_OFF = 0x9, + /* Poly-KeyPress */ + USB_MIDI_CIN_POLY_KEYPRESS = 0xA, + /* Control Change */ + USB_MIDI_CIN_CONTROL_CHANGE = 0xB, + /* Program Change */ + USB_MIDI_CIN_PROGRAM_CHANGE = 0xC, + /* Channel Pressure */ + USB_MIDI_CIN_CHANNEL_PRESSURE = 0xD, + /* PitchBend Change */ + USB_MIDI_CIN_PITCH_BEND_CHANGE = 0xE, + /* Single Byte */ + USB_MIDI_CIN_1BYTE_DATA = 0xF +}; + +/** Called when a non-sysex message has been parsed */ +typedef void (*usb_midi_message_cb_t)(uint8_t *bytes, uint8_t num_bytes, uint8_t cable_num); +/** Called when a sysex message starts */ +typedef void (*usb_midi_sysex_start_cb_t)(uint8_t cable_num); +/** Called when sysex data bytes have been received */ +typedef void (*usb_midi_sysex_data_cb_t)(uint8_t *data_bytes, uint8_t num_data_bytes, + uint8_t cable_num); +/** Called when a sysex message ends */ +typedef void (*usb_midi_sysex_end_cb_t)(uint8_t cable_num); + +struct usb_midi_parse_cb_t { + usb_midi_message_cb_t message_cb; + usb_midi_sysex_start_cb_t sysex_start_cb; + usb_midi_sysex_data_cb_t sysex_data_cb; + usb_midi_sysex_end_cb_t sysex_end_cb; +}; + +/* A USB MIDI event packet. See chapter 4 in the spec. */ +struct usb_midi_packet_t { + uint8_t cable_num; + uint8_t cin; + uint8_t bytes[4]; + uint8_t num_midi_bytes; +}; + +enum usb_midi_error_t usb_midi_packet_from_midi_bytes(uint8_t *midi_bytes, uint8_t cable_num, + struct usb_midi_packet_t *packet); diff --git a/app/src/behaviors/behavior_midi_key_press.c b/app/src/behaviors/behavior_midi_key_press.c new file mode 100644 index 00000000000..3c58989c797 --- /dev/null +++ b/app/src/behaviors/behavior_midi_key_press.c @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_midi_key_press + +#include +#include +#include + +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +static int behavior_midi_key_press_init(const struct device *dev) { return 0; }; + +static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1); + + return raise_zmk_midi_key_state_changed_from_encoded(binding->param1, true, event.timestamp); +} + +static int on_keymap_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1); + return raise_zmk_midi_key_state_changed_from_encoded(binding->param1, false, event.timestamp); +} + +static const struct behavior_driver_api behavior_midi_key_press_driver_api = { + .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released}; + +#define MIDI_INST(n) \ + BEHAVIOR_DT_INST_DEFINE(n, behavior_midi_key_press_init, NULL, NULL, NULL, POST_KERNEL, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \ + &behavior_midi_key_press_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(MIDI_INST) + +#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ \ No newline at end of file diff --git a/app/src/endpoints.c b/app/src/endpoints.c index 7c9d15a31fe..35b31a87f2d 100644 --- a/app/src/endpoints.c +++ b/app/src/endpoints.c @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include #include #include @@ -239,6 +241,40 @@ int zmk_endpoints_send_mouse_report() { } #endif // IS_ENABLED(CONFIG_ZMK_MOUSE) +#if IS_ENABLED(CONFIG_ZMK_MIDI) +int zmk_endpoints_send_midi_report() { + + struct zmk_midi_report *midi_report = zmk_get_midi_report(); + switch (current_instance.transport) { + case ZMK_TRANSPORT_USB: { +#if IS_ENABLED(CONFIG_ZMK_USB) + int err = zmk_usb_send_midi_report(&midi_report->body); + if (err) { + LOG_ERR("FAILED TO SEND OVER USB: %d", err); + } + return err; +#else + LOG_ERR("USB endpoint is not supported"); + return -ENOTSUP; +#endif /* IS_ENABLED(CONFIG_ZMK_USB) */ + } + + case ZMK_TRANSPORT_BLE: { +#if IS_ENABLED(CONFIG_ZMK_BLE) + LOG_ERR("BLE midi endpoint is not supported"); + return -ENOTSUP; +#else + LOG_ERR("BLE midi endpoint is not supported"); + return -ENOTSUP; +#endif /* IS_ENABLED(CONFIG_ZMK_BLE) */ + } + } + + LOG_ERR("Unhandled endpoint transport %d", current_instance.transport); + return -ENOTSUP; +} +#endif // IS_ENABLED(CONFIG_ZMK_MIDI) + #if IS_ENABLED(CONFIG_SETTINGS) static int endpoints_handle_set(const char *name, size_t len, settings_read_cb read_cb, diff --git a/app/src/events/midi_key_state_changed.c b/app/src/events/midi_key_state_changed.c new file mode 100644 index 00000000000..532a298e5b7 --- /dev/null +++ b/app/src/events/midi_key_state_changed.c @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +ZMK_EVENT_IMPL(zmk_midi_key_state_changed); \ No newline at end of file diff --git a/app/src/midi.c b/app/src/midi.c new file mode 100644 index 00000000000..d5391d4b6e0 --- /dev/null +++ b/app/src/midi.c @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include "zmk/midi.h" +#include +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); +#include + +static struct zmk_midi_report midi_report = { + .report_id = ZMK_REPORT_ID_MIDI, + .body = {.cin = MIDI_INVALID, .key = MIDI_INVALID, .key_value = MIDI_INVALID}}; + +static bool sustain_toggle_on = false; +static bool sostenuto_toggle_on = false; + +void set_bitmap(uint64_t map, uint32_t bit_num, bool value) { + // do this in a function as WRITE_BIT + // dirties the value in bitnum + WRITE_BIT(map, bit_num, value); +} + +bool bit_is_set(uint64_t map, uint32_t bit_num) { + // The BIT macro modifies the value, so using it outside of a function + // can dirty the bit_num variable + return (map & BIT(bit_num)); +} + +void zmk_midi_report_clear() { + midi_report.body.cin = MIDI_INVALID; + midi_report.body.key = MIDI_INVALID; + midi_report.body.key_value = MIDI_INVALID; +} + +int zmk_midi_key_press(const zmk_midi_key_t key) { + LOG_INF("zmk_midi_key_press received: 0x%04x aka %d", key, key); + + switch (key) { + case MIDI_MIN_NOTE ... MIDI_MAX_NOTE: + // and write and updated report + zmk_midi_report_clear(); + midi_report.body.cin = ZMK_MIDI_CIN_NOTE_ON; + midi_report.body.key = key; + midi_report.body.key_value = ZMK_MIDI_ON_VELOCITY; + break; + case MIDI_MIN_CONTROL ... MIDI_MAX_CONTROL: + zmk_midi_key_t control_key_transformed = (uint8_t)key; + if (SUSTAIN == key) { + if (!sustain_toggle_on) { + // we set the toggle on in the release + // since there will be 2 releases before we want + // to turn off the toggle + // dont set the toggle on here! + zmk_midi_report_clear(); + midi_report.body.cin = ZMK_MIDI_CIN_CONTROL_CHANGE; + midi_report.body.key = control_key_transformed; + midi_report.body.key_value = ZMK_MIDI_TOGGLE_ON; + } else { + zmk_midi_report_clear(); + return -EINPROGRESS; + } + } else if (SOSTENUTO == key) { + if (!sostenuto_toggle_on) { + // we set the toggle on in the release + // since there will be 2 releases before we want + // to turn off the toggle + // dont set the toggle on here! + zmk_midi_report_clear(); + midi_report.body.cin = ZMK_MIDI_CIN_CONTROL_CHANGE; + midi_report.body.key = control_key_transformed; + midi_report.body.key_value = ZMK_MIDI_TOGGLE_ON; + } else { + zmk_midi_report_clear(); + return -EINPROGRESS; + } + } else { + // not implemented + zmk_midi_report_clear(); + LOG_INF("midi control handling not implemented"); + } + return 0; + break; + default: + LOG_ERR("Unsupported midi key %d", key); + return -EINVAL; + break; + } + + return 0; +} + +int zmk_midi_key_release(const zmk_midi_key_t key) { + LOG_INF("zmk_midi_key_release received: 0x%04x aka %d", key, key); + + switch (key) { + case MIDI_MIN_NOTE ... MIDI_MAX_NOTE: + // write an updated report + zmk_midi_report_clear(); + midi_report.body.cin = ZMK_MIDI_CIN_NOTE_OFF; + midi_report.body.key = key; + midi_report.body.key_value = ZMK_MIDI_OFF_VELOCITY; + return 0; + break; + case MIDI_MIN_CONTROL ... MIDI_MAX_CONTROL: + zmk_midi_key_t control_key_transformed = (uint8_t)key; + if (SUSTAIN == key) { + if (!sustain_toggle_on) { + // the first release we see of a toggle we should ignore + // otherwise it doesn't behave as a toggle! + // just set the toggle variable + sustain_toggle_on = true; + zmk_midi_report_clear(); + return -EINPROGRESS; + } else if (sustain_toggle_on) { + sustain_toggle_on = false; + zmk_midi_report_clear(); + midi_report.body.cin = ZMK_MIDI_CIN_CONTROL_CHANGE; + midi_report.body.key = control_key_transformed; + midi_report.body.key_value = ZMK_MIDI_TOGGLE_OFF; + } + } else if (SOSTENUTO == key) { + if (!sostenuto_toggle_on) { + // the first release we see of a toggle we should ignore + // otherwise it doesn't behave as a toggle! + // just set the toggle variable + sostenuto_toggle_on = true; + zmk_midi_report_clear(); + return -EINPROGRESS; + } else if (sostenuto_toggle_on) { + sostenuto_toggle_on = false; + zmk_midi_report_clear(); + midi_report.body.cin = ZMK_MIDI_CIN_CONTROL_CHANGE; + midi_report.body.key = control_key_transformed; + midi_report.body.key_value = ZMK_MIDI_TOGGLE_OFF; + } + } else { + // not implemented + zmk_midi_report_clear(); + LOG_INF("midi control handling not implemented"); + } + return 0; + break; + default: + LOG_ERR("Unsupported midi key %d", key); + return -EINVAL; + } + + return 0; +} +void zmk_midi_clear(void) { memset(&midi_report.body, 0, sizeof(midi_report.body)); } + +struct zmk_midi_report *zmk_get_midi_report(void) { + return &midi_report; +} diff --git a/app/src/midi_listener.c b/app/src/midi_listener.c new file mode 100644 index 00000000000..51ba97560c1 --- /dev/null +++ b/app/src/midi_listener.c @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include +#include +#include + +static void listener_midi_key_pressed(const struct zmk_midi_key_state_changed *ev) { + LOG_DBG("midi key: 0x%04X", ev->key); + int ret = zmk_midi_key_press(ev->key); + if (ret < 0) { + LOG_DBG("listener_midi_key_pressed received error, ignoring"); + return; + } + zmk_endpoints_send_midi_report(); +} + +static void listener_midi_key_released(const struct zmk_midi_key_state_changed *ev) { + LOG_DBG("midi key: 0x%04X", ev->key); + int ret = zmk_midi_key_release(ev->key); + if (ret < 0) { + LOG_DBG("listener_midi_key_released received error, ignoring"); + return; + } + zmk_endpoints_send_midi_report(); +} + +int midi_listener(const zmk_event_t *eh) { + const struct zmk_midi_key_state_changed *midi_key_ev = as_zmk_midi_key_state_changed(eh); + if (midi_key_ev) { + if (midi_key_ev->state) { + listener_midi_key_pressed(midi_key_ev); + } else { + listener_midi_key_released(midi_key_ev); + } + return 0; + } + return 0; +} + +ZMK_LISTENER(midi_listener, midi_listener); +ZMK_SUBSCRIPTION(midi_listener, zmk_midi_key_state_changed); \ No newline at end of file diff --git a/app/src/usb_midi.c b/app/src/usb_midi.c new file mode 100644 index 00000000000..da5d0dc5777 --- /dev/null +++ b/app/src/usb_midi.c @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +#include +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +static K_SEM_DEFINE(midi_sem, 1, 1); + +static int usb_midi_is_available = false; + +// This macros should be used to place the USB descriptors +// in predetermined order in the RAM. +USBD_CLASS_DESCR_DEFINE(primary, 0) +struct usb_midi_config usb_midi_config_data = { + .ac_if = INIT_AC_IF, + .ac_cs_if = INIT_AC_CS_IF, + .ms_if = INIT_MS_IF, + .ms_cs_if = INIT_MS_CS_IF, + .out_jacks_emb = {LISTIFY(USB_MIDI_NUM_OUTPUTS, INIT_OUT_JACK, (, ), 0)}, + .in_jacks_emb = {LISTIFY(USB_MIDI_NUM_INPUTS, INIT_IN_JACK, (, ), USB_MIDI_NUM_OUTPUTS)}, + .element = INIT_ELEMENT, + .in_ep = INIT_IN_EP, + .in_cs_ep = {.bLength = sizeof(struct usb_midi_bulk_in_ep_descriptor), + .bDescriptorType = USB_DESC_CS_ENDPOINT, + .bDescriptorSubtype = 0x01, + .bNumEmbMIDIJack = USB_MIDI_NUM_OUTPUTS, + .BaAssocJackID = {LISTIFY(USB_MIDI_NUM_OUTPUTS, IDX_WITH_OFFSET, (, ), 1)}}, + .out_ep = INIT_OUT_EP, + .out_cs_ep = {.bLength = sizeof(struct usb_midi_bulk_out_ep_descriptor), + .bDescriptorType = USB_DESC_CS_ENDPOINT, + .bDescriptorSubtype = 0x01, + .bNumEmbMIDIJack = USB_MIDI_NUM_INPUTS, + .BaAssocJackID = {LISTIFY(USB_MIDI_NUM_INPUTS, IDX_WITH_OFFSET, (, ), + 1 + USB_MIDI_NUM_OUTPUTS)}}}; + +void usb_status_callback(struct usb_cfg_data *cfg, enum usb_dc_status_code cb_status, + const uint8_t *param) { + switch (cb_status) { + /** USB error reported by the controller */ + case USB_DC_ERROR: + LOG_DBG("USB_DC_ERROR"); + break; + /** USB reset */ + case USB_DC_RESET: + LOG_DBG("USB_DC_RESET"); + break; + /** USB connection established, hardware enumeration is completed */ + case USB_DC_CONNECTED: + LOG_DBG("USB_DC_CONNECTED"); + break; + /** USB configuration done */ + case USB_DC_CONFIGURED: + LOG_DBG("USB_DC_CONFIGURED"); + LOG_INF("USB MIDI device is available"); + usb_midi_is_available = true; + break; + /** USB connection lost */ + case USB_DC_DISCONNECTED: + LOG_DBG("USB_DC_DISCONNECTED"); + break; + /** USB connection suspended by the HOST */ + case USB_DC_SUSPEND: + LOG_DBG("USB_DC_SUSPEND"); + LOG_INF("USB MIDI device is unavailable"); + usb_midi_is_available = false; + break; + /** USB connection resumed by the HOST */ + case USB_DC_RESUME: + LOG_DBG("USB_DC_RESUME"); + break; + /** USB interface selected */ + case USB_DC_INTERFACE: + LOG_DBG("USB_DC_INTERFACE"); + break; + /** Set Feature ENDPOINT_HALT received */ + case USB_DC_SET_HALT: + LOG_DBG("USB_DC_SET_HALT"); + break; + /** Clear Feature ENDPOINT_HALT received */ + case USB_DC_CLEAR_HALT: + LOG_DBG("USB_DC_CLEAR_HALT"); + break; + /** Start of Frame received */ + case USB_DC_SOF: + LOG_DBG("USB_DC_SOF"); + break; + /** Initial USB connection status */ + case USB_DC_UNKNOWN: + LOG_DBG("USB_DC_UNKNOWN"); + break; + } +} + +static void midi_out_ep_cb(uint8_t ep, enum usb_dc_ep_cb_status_code ep_status) { + LOG_DBG("midi_out_ep_cb is not implemented"); +} + +static void midi_in_ep_cb(uint8_t ep, enum usb_dc_ep_cb_status_code ep_status) { + LOG_DBG("midi_in_ep_cb is not implemented"); +} + +static struct usb_ep_cfg_data midi_ep_cfg[] = {{ + .ep_cb = midi_in_ep_cb, + .ep_addr = ZMK_USB_MIDI_EP_IN, + }, + { + .ep_cb = midi_out_ep_cb, + .ep_addr = ZMK_USB_MIDI_EP_OUT, + }}; + +static void midi_interface_config(struct usb_desc_header *head, uint8_t bInterfaceNumber) { + struct usb_if_descriptor *if_desc = (struct usb_if_descriptor *)head; + struct usb_midi_config *desc = CONTAINER_OF(if_desc, struct usb_midi_config, ac_if); + + desc->ac_if.bInterfaceNumber = bInterfaceNumber; + desc->ms_if.bInterfaceNumber = bInterfaceNumber + 1; +} + +// this is the macro that sets up the usb device for midi +USBD_DEFINE_CFG_DATA(usb_midi_config) = { + .usb_device_description = NULL, + .interface_config = midi_interface_config, + .interface_descriptor = &usb_midi_config_data.ac_if, + .cb_usb_status = usb_status_callback, + .interface = + { + .class_handler = NULL, + .custom_handler = NULL, + .vendor_handler = NULL, + }, + .num_endpoints = ARRAY_SIZE(midi_ep_cfg), + .endpoint = midi_ep_cfg, +}; + +static int zmk_usb_midi_send(uint8_t cable_number, uint8_t *midi_bytes, size_t len) { + + LOG_INF("Sending midi bytes %02x %02x %02x", midi_bytes[0], midi_bytes[1], midi_bytes[2]); + // prepare the packet + struct usb_midi_packet_t packet; + enum usb_midi_error_t error = + usb_midi_packet_from_midi_bytes(midi_bytes, cable_number, &packet); + if (error != USB_MIDI_SUCCESS) { + LOG_ERR("Building packet from MIDI bytes %02x %02x %02x failed with error %d", + midi_bytes[0], midi_bytes[1], midi_bytes[2], error); + return -EINVAL; + } + + LOG_INF("Sending midi packet %02x %02x %02x %02x to endpoint %02x", packet.bytes[0], + packet.bytes[1], packet.bytes[2], packet.bytes[3], ZMK_USB_MIDI_EP_IN); + + // ensure usb is ready + switch (zmk_usb_get_status()) { + case USB_DC_SUSPEND: + return usb_wakeup_request(); + case USB_DC_ERROR: + case USB_DC_RESET: + case USB_DC_DISCONNECTED: + case USB_DC_UNKNOWN: + return -ENODEV; + default: + k_sem_take(&midi_sem, K_MSEC(30)); + LOG_INF("doing midi usb_write"); + uint32_t num_written_bytes = 0; + int ret = usb_write(ZMK_USB_MIDI_EP_IN, packet.bytes, 4, &num_written_bytes); + if (ret < 0) { + LOG_INF("usb_midi usb write error %d", ret); + } + LOG_INF("completed midi usb write %d", ret); + + // TODO error if num_written_bytes != 4, make sure to release sem on error like usb_hid.c + + // TODO usb_hid.c holds the sem until its in_ready_cb is hit. do we have something like + // this? usb status seems to be different, perhaps that is using hid status? anyway, for now + // just release the sem right after we transmit + + k_sem_give(&midi_sem); + + return 0; + } +} + +int zmk_usb_send_midi_report(struct zmk_midi_key_report_body *body) { + uint8_t midi_bytes[USB_MIDI_MAX_NUM_BYTES]; + + LOG_INF("body cin = %d, key = %d, key_value = %d", body->cin, body->key, body->key_value); + + if (body->key > 0 && body->key < MIDI_INVALID && body->key_value < MIDI_INVALID) { + + midi_bytes[0] = body->cin; // note on, note off, control change, etc + midi_bytes[1] = body->key; // the note, control change code, etc + midi_bytes[2] = body->key_value; // the velocity, or control change value, etc + } else { + LOG_ERR("No valid midi key!"); + return -1; + } + + return zmk_usb_midi_send(USB_MIDI_DEFAULT_CABLE_NUM, midi_bytes, USB_MIDI_MAX_NUM_BYTES); +} \ No newline at end of file diff --git a/app/src/usb_midi_packet.c b/app/src/usb_midi_packet.c new file mode 100644 index 00000000000..ead73d226de --- /dev/null +++ b/app/src/usb_midi_packet.c @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +#define SYSEX_START_BYTE 0xF0 +#define SYSEX_END_BYTE 0xF7 + +static enum usb_midi_error_t channel_msg_cin(uint8_t first_byte, uint8_t *cin) { + uint8_t high_nibble = first_byte >> 4; + + switch (high_nibble) { + case 0x8: /* Note off */ + case 0x9: /* Note on */ + case 0xa: /* Poly KeyPress */ + case 0xb: /* Control Change */ + case 0xe: /* PitchBend Change */ + /* Three byte channel Voice Message */ + *cin = high_nibble; + break; + case 0xc: /* Program Change */ + case 0xd: /* Channel Pressure */ + /* Two byte channel Voice Message */ + *cin = high_nibble; + break; + default: + /* Invalid status byte */ + return USB_MIDI_ERROR_INVALID_MIDI_MSG; + } + + /* Valid status byte */ + return USB_MIDI_SUCCESS; +} + +static enum usb_midi_error_t non_sysex_system_msg_cin(uint8_t first_byte, uint8_t *cin) { + switch (first_byte) { + case 0xf1: /* MIDI Time Code Quarter Frame */ + case 0xf3: /* Song Select */ + /* 2 byte System Common message */ + *cin = USB_MIDI_CIN_SYSCOM_2BYTE; + break; + case 0xf2: /* Song Position Pointer */ + /* 3 byte System Common message */ + *cin = USB_MIDI_CIN_SYSCOM_3BYTE; + break; + case 0xf6: /* Tune request */ + /* Single-byte System Common Message */ + *cin = USB_MIDI_CIN_SYS_COMMON_OR_SYSEX_END_1BYTE; + break; + case 0xf8: /* Timing Clock */ + case 0xfa: /* Start */ + case 0xfb: /* Continue */ + case 0xfc: /* Stop */ + case 0xfe: /* Active Sensing */ + case 0xff: /* System Reset */ + /* 1 byte system real time */ + *cin = USB_MIDI_CIN_1BYTE_DATA; + break; + default: + /* Invalid status byte */ + return USB_MIDI_ERROR_INVALID_MIDI_MSG; + } + + /* Valid status byte */ + return USB_MIDI_SUCCESS; +} + +static enum usb_midi_error_t sysex_msg_cin(uint8_t *midi_bytes, uint8_t *cin) { + int is_data_byte[3] = {midi_bytes[0] < 0x80, midi_bytes[1] < 0x80, midi_bytes[2] < 0x80}; + + if (midi_bytes[0] == SYSEX_START_BYTE) { + if (midi_bytes[1] == SYSEX_END_BYTE) { + /* Sysex case 1: F0 F7 */ + *cin = USB_MIDI_CIN_SYSEX_END_2BYTE; + } else if (is_data_byte[1]) { + if (midi_bytes[2] == SYSEX_END_BYTE) { + /* Sysex case 2: F0 d F7 */ + *cin = USB_MIDI_CIN_SYSEX_END_3BYTE; + } else if (is_data_byte[2]) { + /* Sysex case 3: F0 d d */ + *cin = USB_MIDI_CIN_SYSEX_START_OR_CONTINUE; + } + } + } else if (is_data_byte[0]) { + if (is_data_byte[1]) { + if (is_data_byte[2]) { + /* Sysex case 4: d d d */ + *cin = USB_MIDI_CIN_SYSEX_START_OR_CONTINUE; + } else if (midi_bytes[2] == SYSEX_END_BYTE) { + /* Sysex case 5: d d F7 */ + *cin = USB_MIDI_CIN_SYSEX_END_3BYTE; + } + } else if (midi_bytes[1] == SYSEX_END_BYTE) { + /* Sysex case 6: d F7 */ + *cin = USB_MIDI_CIN_SYSEX_END_2BYTE; + } + } else if (midi_bytes[0] == SYSEX_END_BYTE) { + /* Sysex case 7: F7 */ + *cin = USB_MIDI_CIN_SYS_COMMON_OR_SYSEX_END_1BYTE; + } else { + /* Invalid sysex sequence */ + return USB_MIDI_ERROR_INVALID_MIDI_MSG; + } + + /* Valid sysex sequence */ + return USB_MIDI_SUCCESS; +} + +static uint8_t num_midi_bytes_for_cin(uint8_t cin) { + switch (cin) { + case USB_MIDI_CIN_MISC: + case USB_MIDI_CIN_CABLE_EVENT: + /* Reserved for future expansion. Ignore. */ + return 0; + case USB_MIDI_CIN_SYS_COMMON_OR_SYSEX_END_1BYTE: + case USB_MIDI_CIN_1BYTE_DATA: + return 1; + case USB_MIDI_CIN_SYSCOM_2BYTE: + case USB_MIDI_CIN_SYSEX_END_2BYTE: + case USB_MIDI_CIN_PROGRAM_CHANGE: + case USB_MIDI_CIN_CHANNEL_PRESSURE: + return 2; + default: + return 3; + } +} + +enum usb_midi_error_t usb_midi_packet_from_midi_bytes(uint8_t *midi_bytes, uint8_t cable_num, + struct usb_midi_packet_t *packet) { + /* Building a USB MIDI packet from a MIDI message amounts to determining the code + * index number (CIN) corresponding to the message. This in turn determines the + * size of the MIDI message. + * + * The MIDI message is assumed to not contain interleaved system real time bytes. + * + * A MIDI message contained in a USB MIDI packet is 1, 2 or 3 bytes long. It either + * + * 1. is a channel message starting with one of the follwing status bytes + * followed by data bytes: (n is the MIDI channel) + * + * 8n - note off, cin 0x8 + * 9n - note on, cin 0x9 + * An - Polyphonic aftertouch, cin 0xa + * Bn - Control change, cin 0xb + * Cn - Program change, cin 0xc + * Dn - Channel aftertouch, cin 0xd + * En - Pitch bend change, cin 0xe + * + * 2. is a non-sysex system message starting with one of the following + * status bytes followed by zero or more data bytes + * (F4, F5, F9 and FD are undefined. F0, F7 are sysex) + * + * F1 - MIDI Time Code Qtr. Frame, cin 0x2 + * F2 - Song Position Pointer, cin 0x3 + * F3 - Song Select, cin 0x2 + * F6 - Tune request, cin 0x5 + * F8 - Timing clock, cin 0xf + * FA - Start, cin 0xf + * FB - Continue, cin 0xf + * FC - Stop, cin 0xf + * FE - Active Sensing, cin 0xf + * FF - System reset, cin 0xf + * + * 3. is a (partial) sysex message, taking one of the following forms (d is a data byte) + * F0, F7 - sysex case 1, cin 0x6 (SysEx ends with following two bytes) + * F0, d, F7 - sysex case 2, cin 0x7 (SysEx ends with following three bytes) + * F0, d, d - sysex case 3, cin 0x4 (SysEx starts or continues) + * d, d, d - sysex case 4, cin 0x4 (SysEx starts or continues) + * d, d, F7 - sysex case 5, cin 0x7 (SysEx ends with following three bytes) + * d, F7 - sysex case 6, cin 0x6 (SysEx ends with following two bytes) + * F7 - sysex case 7, cin 0x5 (Single-byte System Common Message or + * SysEx ends with following single byte.) + */ + + if (cable_num >= 16) { + return USB_MIDI_ERROR_INVALID_CABLE_NUM; + } + + packet->cable_num = cable_num; + packet->cin = 0; + packet->num_midi_bytes = 0; + + enum usb_midi_error_t cin_error = channel_msg_cin(midi_bytes[0], &packet->cin); + if (cin_error != USB_MIDI_SUCCESS) { + cin_error = non_sysex_system_msg_cin(midi_bytes[0], &packet->cin); + } + if (cin_error != USB_MIDI_SUCCESS) { + cin_error = sysex_msg_cin(midi_bytes, &packet->cin); + } + + packet->num_midi_bytes = num_midi_bytes_for_cin(packet->cin); + + if (cin_error != USB_MIDI_SUCCESS || packet->num_midi_bytes == 0) { + /* Invalid MIDI message. */ + return USB_MIDI_ERROR_INVALID_MIDI_MSG; + } + + /* Put cable number and CIN in packet byte 0 */ + packet->bytes[0] = (packet->cable_num << 4) | packet->cin; + + /* Fill packet bytes 1,2 and 3 with zero padded midi bytes. */ + packet->bytes[1] = 0; + packet->bytes[2] = 0; + packet->bytes[3] = 0; + for (int i = 0; i < packet->num_midi_bytes; i++) { + packet->bytes[i + 1] = midi_bytes[i]; + } + + /* No errors */ + return USB_MIDI_SUCCESS; +} diff --git a/docs/docs/behaviors/index.mdx b/docs/docs/behaviors/index.mdx index bdacc209ad7..c8621a6e3fb 100644 --- a/docs/docs/behaviors/index.mdx +++ b/docs/docs/behaviors/index.mdx @@ -72,6 +72,12 @@ Below is a summary of pre-defined behavior bindings and user-definable behaviors | `&ext_power` | [Power management](power.md#behavior-binding) | Allows enabling or disabling the VCC power output to save power | | `&soft_off` | [Soft off](soft-off.md#behavior-binding) | Turns the keyboard off. | +## MIDI Behaviors + +| Binding | Behavior | Description | +| ------- | ------------------------------------------ | ----------------------------------- | +| `&midi` | [MIDI Key Press](midi.md#behavior-binding) | Sends MIDI messages to the USB host | + ## User-Defined Behaviors | Behavior | Description | diff --git a/docs/docs/behaviors/midi.md b/docs/docs/behaviors/midi.md new file mode 100644 index 00000000000..5b397f71f4d --- /dev/null +++ b/docs/docs/behaviors/midi.md @@ -0,0 +1,82 @@ +--- +title: MIDI Behavior +sidebar_label: MIDI +--- + +## Summary + +The MIDI feature allows a keyboard to send MIDI messages to host. + +Unlike other behaviors, MIDI only works over usb. Bluetooth MIDI is not supported. + +Currently, only sending MIDI messages is supported. Boards cannot receive MIDI messages. + +## Enabling MIDI support + +MIDI support has been tested on both the `bluemicro840_v1` and the `nice_nano_v2` + +1. add the config option to the boards `.conf` + +```ini +CONFIG_ZMK_MIDI=y +``` + +2. include the dt-binding header file at the top of the boards `.keymap` + +```dts +#include +``` + +enabling MIDI support adds two new USB endpoints to the board. On linux, these get picked up by the `snd-usb-audio` driver + +``` +sudo cat /sys/kernel/debug/usb/devices +... +... +T: Bus=03 Lev=01 Prnt=01 Port=00 Cnt=01 Dev#= 17 Spd=12 MxCh= 0 +D: Ver= 2.00 Cls=00(>ifc ) Sub=00 Prot=00 MxPS=64 #Cfgs= 1 +P: Vendor=1d50 ProdID=615e Rev= 3.05 +S: Manufacturer=ZMK Project +S: Product=btrfld +S: SerialNumber=DF4A1D9720CD8BD8 +C:* #Ifs= 3 Cfg#= 1 Atr=e0 MxPwr=100mA +I:* If#= 0 Alt= 0 #EPs= 0 Cls=01(audio) Sub=01 Prot=00 Driver=snd-usb-audio +I:* If#= 1 Alt= 0 #EPs= 2 Cls=01(audio) Sub=03 Prot=00 Driver=snd-usb-audio +E: Ad=01(O) Atr=02(Bulk) MxPS= 64 Ivl=0ms +E: Ad=81(I) Atr=02(Bulk) MxPS= 64 Ivl=0ms +I:* If#= 2 Alt= 0 #EPs= 1 Cls=03(HID ) Sub=00 Prot=00 Driver=usbhid +E: Ad=82(I) Atr=03(Int.) MxPS= 16 Ivl=1ms +``` + +## MIDI keycodes + +MIDI keycodes are defined in the header [`dt-bindings/zmk/midi.h`](https://github.com/zmkfirmware/zmk/blob/main/app/include/dt-bindings/zmk/midi.h) + +The majority of the keycode defines are the Note On/Off messages, which are denoted by `NOTE_*`There is one for each note in a standard octave, with 10 octaves available. + +There is also support for control change messages, with `SUSTAIN`, `PORTAMENTO`, and `SOSTENUTO` currently implemented. + +The following documents can be used to learn about all of the possible MIDI messages: +https://midi.org/summary-of-midi-1-0-messages +https://www.cs.cmu.edu/~music/cmsip/readings/MIDI%20tutorial%20for%20programmers.html + +## Behavior Binding + +- Reference: `&midi` +- Parameter #1: The midi keycode, e.g. `NOTE_C_5` or `SUSTAIN` + +### Examples + +1. while pressed, sends the E9 Note + + ```dts + &midi NOTE_E_9 + ``` + +2. while pressed, presses the sustain pedal + + ```dts + &midi SUSTAIN + ``` + +MIDI keycodes can be combined with the other zmk behaviors to create interesting instruments.