This commit is contained in:
Evalyn Emmerich 2024-08-15 12:14:34 +02:00 committed by GitHub
commit 3998209f5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1608 additions and 0 deletions

View file

@ -37,6 +37,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)
@ -76,6 +77,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)

View file

@ -381,6 +381,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

View file

@ -93,6 +93,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

View file

@ -21,3 +21,5 @@
#include <behaviors/macros.dtsi>
#include <behaviors/mouse_key_press.dtsi>
#include <behaviors/soft_off.dtsi>
#include <behaviors/midi_key_press.dtsi>

View file

@ -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>;
};
};
};

View file

@ -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

View file

@ -0,0 +1,223 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zephyr/dt-bindings/dt-util.h>
// 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

View file

@ -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)

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zephyr/kernel.h>
#include <zmk/event_manager.h>
#include <zmk/midi.h>
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));
}

48
app/include/zmk/midi.h Normal file
View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zmk/midi_keys.h>
// 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();

View file

@ -0,0 +1,17 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zephyr/kernel.h>
#include <dt-bindings/zmk/midi.h>
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;

View file

@ -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);

347
app/include/zmk/usb_midi.h Normal file
View file

@ -0,0 +1,347 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zephyr/usb/usb_device.h>
#include <zmk/midi.h>
#include <usb_descriptor.h>
// 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);

View file

@ -0,0 +1,80 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <stdint.h>
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);

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#define DT_DRV_COMPAT zmk_behavior_midi_key_press
#include <zephyr/device.h>
#include <drivers/behavior.h>
#include <zephyr/logging/log.h>
#include <zmk/behavior.h>
#include <zmk/event_manager.h>
#include <zmk/events/midi_key_state_changed.h>
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) */

View file

@ -14,6 +14,8 @@
#include <zmk/hid.h>
#include <dt-bindings/zmk/hid_usage_pages.h>
#include <zmk/usb_hid.h>
#include <zmk/usb_midi.h>
#include <zmk/midi.h>
#include <zmk/hog.h>
#include <zmk/event_manager.h>
#include <zmk/events/ble_active_profile_changed.h>
@ -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,

View file

@ -0,0 +1,9 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zmk/events/midi_key_state_changed.h>
ZMK_EVENT_IMPL(zmk_midi_key_state_changed);

156
app/src/midi.c Normal file
View file

@ -0,0 +1,156 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include "zmk/midi.h"
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <dt-bindings/zmk/modifiers.h>
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;
}

50
app/src/midi_listener.c Normal file
View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <drivers/behavior.h>
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zmk/events/midi_key_state_changed.h>
#include <zmk/endpoints.h>
#include <zmk/midi.h>
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);

206
app/src/usb_midi.c Normal file
View file

@ -0,0 +1,206 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/usb/usb_device.h>
#include <zmk/usb_midi.h>
#include <zmk/usb_midi_packet.h>
#include <zmk/usb.h>
#include <zephyr/logging/log.h>
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);
}

214
app/src/usb_midi_packet.c Normal file
View file

@ -0,0 +1,214 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zmk/usb_midi_packet.h>
#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;
}

View file

@ -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 |

View file

@ -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 <dt-bindings/zmk/midi.h>
```
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.