DIY-friendly MIDI interface for SparkSDR

289 views
Skip to first unread message

Louis Mamakos

unread,
Nov 1, 2023, 12:19:40 AM11/1/23
to SparkSDR
I'm a new user of SparkSDR and a Hermes Lite 2 radio.  I'm having great fun with the combination of the two, and thanks to all who've invested time and effort making these things possible.

I started investigating adding a rotary encoder knob to use as a tuner and poked around for something easily available.  Rather than getting some off-the-shelf DJ MIDI controller thing (because that would just work, and what's the fun in that?), I went a different way.  

I wanted something a bit more compact and that could be customized.  After some investigation, I discovered the QMK open-source project that's intended to be used as firmware in a custom keyboard.  And looking at the github repo, I discovered quite a large number of custom keyboards that could support this firmware.   The code targets wither Atmel ATMEGA microcontrollers as well as STM32 ARM microcontrollers found in more recent hardware designs.

QMK also supports MIDI over USB as a configuration option!

So searching around on Amazon for QMK compatible macro keypads, I discover a "KEEBMONKEY Magalodon Triple Knob Macro Pad Programmable Designer Mini Keyboard 16 Keys" which is available in all sort of colors to suite anyone's taste.  Out of the box, this supports the "VIA"  capability, which is an option in the QMK firmware and an interactive way of programming the keys and macros, without having to reprogram the microcontroller.  However, the default code doesn't support MIDI.  (And then later I found that VIA doesn't seem to generally support setting MIDI commands on keys.)  

So I set upon building a new version of the QMK firmware for this keyboard with the MIDI capability enabled.   After some searching around, I found the keyboard configuration in the QMK github repo, listed as a "DIO KB16 rev 2" keyboard.  Good news is that the rev2 version has a (clone) STM32F103 microcontroller onboard, so plenty of code space and no worries about turning on the MIDI feature and still fitting. 

Enabling MIDI is done by creating a new "keymap" for this keyboard.  This manifests as 3 files of some C code which is really data structure definitions of key bindings, and a header file and makefile fragment where additional options can be specified (to turn on MIDI).  I was able to build and install the firmware after cloning the github repo locally on a Linux box, installing a "qmk" tool to do the build, and then downloading the firmware image over USB.  

Just to same someone some time, the download over USB is done when the microcontroller is in "DFU" mode -- this is accomplished by disconnecting the keypad's USB cable, holding down the uppermost left key and plugging it back in.  Hold it for a few seconds and let go.   The small OLED display should remain off, and you're in DFU mode.

I've made it work, at least so far as emitting MIDI events and configuring SparkSDR so that the large knob tunes the VFO.  There's still some work to do to figure out how to most effectively use it, but now all the buttons in one of the 4 "layers" of mappings on the keypad all emit unique MIDI events.  

For more detail if you want to try this yourself, follow the instructions in the QMK documentation to get that environment set up.  Then what I did was create 3 new files in a new directory in the checked-out repo that are reproduced below (since they're fairly small.)  In my configuration, the second layer has the MIDI events defined, so you'd need to press down on the small encoder at the upper right once to switch from keymap layer 0 to keymap layer 1, as also shown on the OLED display.

Perhaps this is helpful for someone who wants to experiment with the rotary encoder capability or an array of buttons?  I believe the firmware can receive MIDI messages as well; perhaps some future version of SparkSDR could emit, e.g., the current VFO frequency in some custom MIDI message so it could be show on the OLED display?  Or other information?  The display also has an array of individual RGB LEDs under each key that presently don't do anything.  Maybe have them all turn red when the PTT button is depressed?  Lots of opportunity to expends hours of time with this thing :-)

-louie
WA3YMH

qmk_firmware/keyboards/doio/kb16/rev2/keymaps/viamidi/config.h -
/* enable basic and advanced MIDI capability */
#define MIDI_BASIC
#define MIDI_ADVANCED

qmk_firmware/keyboards/doio/kb16/rev2/keymaps/viamidi/rules.mk -
VIA_ENABLE = yes

# Encoder enabled
ENCODER_MAP_ENABLE = yes

# enable MIDI
MIDI_ENABLE = yes

qmk_firmware/keyboards/doio/kb16/rev2/keymaps/viamidi/keymap.c -
/* Copyright 2022 DOIO
 * Copyright 2022 HorrorTroll <https://github.com/HorrorTroll>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include QMK_KEYBOARD_H

// OLED animation
#include "lib/layer_status/layer_status.h"

// Each layer gets a name for readability, which is then used in the keymap matrix below.
// The underscores don't mean anything - you can have a layer called STUFF or any other name.
// Layer names don't all need to be of the same length, obviously, and you can also skip them
// entirely and just use numbers.

enum layer_names {
    _BASE,
    _FN,
    _FN1,
    _FN2
};

// enum layer_keycodes { };

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {

/*
       ┌───┬───┬───┬───┐   ┌───┐ ┌───┐
       │ 1 │ 2 │ 3 │ 4 │   │Ply│ │TO1│
       ├───┼───┼───┼───┤   └───┘ └───┘
       │ 5 │ 6 │ 7 │ 8 │
       ├───┼───┼───┼───┤
       │ 9 │ 0 │ ↑ │Ent│      ┌───┐
       ├───┼───┼───┼───┤      │Mut│
       │Fn2│ ← │ ↓ │ → │      └───┘
       └───┴───┴───┴───┘
       ┌───┬───┬───┬───┐   ┌───┐ ┌───┐
       │ ! │ @ │ # │ $ │   │   │ │   │
       ├───┼───┼───┼───┤   └───┘ └───┘
       │ % │ ^ │ & │ * │
       ├───┼───┼───┼───┤
       │ ( │ ) │   │   │      ┌───┐
       ├───┼───┼───┼───┤      │   │
       │   │   │   │   │      └───┘
       └───┴───┴───┴───┘
*/
    /*  Row:    0         1        2        3         4      */
    [_BASE] = LAYOUT(
                KC_1,     KC_2,    KC_3,    KC_4,     KC_MPLY,
                KC_5,     KC_6,    KC_7,    KC_8,     TO(_FN),
                KC_9,     KC_0,    KC_UP,   KC_ENT,   KC_MUTE,
                MO(_FN2), KC_LEFT, KC_DOWN, KC_RIGHT
            ),

/*
       ┌───┬───┬───┬───┐   ┌───┐ ┌───┐
       │   │   │   │   │   │   │ │   │
       ├───┼───┼───┼───┤   └───┘ └───┘
       │   │   │   │   │
       ├───┼───┼───┼───┤
       │   │   │   │   │      ┌───┐
       ├───┼───┼───┼───┤      │   │
       │   │   │   │   │      └───┘
       └───┴───┴───┴───┘
*/
    /*  Row:    0        1        2        3        4 (knob press)  */
    [_FN] = LAYOUT(
MI_A1,    MI_B1,   MI_C1,   MI_D1,       KC_MUTE,
                MI_E1,    MI_F1,   MI_G1,   MI_A2,       TO(_FN1),
                MI_B2,    MI_C2,   MI_D2,   MI_E2,       MI_F2,
                MI_F2,    MI_G2,   MI_A3,   MI_B3
            ),

/*
       ┌───┬───┬───┬───┐   ┌───┐ ┌───┐
       │   │   │   │   │   │   │ │   │
       ├───┼───┼───┼───┤   └───┘ └───┘
       │   │   │   │   │
       ├───┼───┼───┼───┤
       │   │   │   │   │      ┌───┐
       ├───┼───┼───┼───┤      │   │
       │   │   │   │   │      └───┘
       └───┴───┴───┴───┘
*/
    /*  Row:    0        1        2        3        4       */
    [_FN1] = LAYOUT(
                _______, _______, _______, _______, _______,
                _______, _______, _______, _______, TO(_FN2),
                _______, _______, _______, _______, _______,
                _______, _______, _______, _______
            ),

/*
       ┌───┬───┬───┬───┐   ┌───┐ ┌───┐
       │Spi│Spd│   │   │   │   │ │TO0│
       ├───┼───┼───┼───┤   └───┘ └───┘
       │Sai│Sad│   │   │
       ├───┼───┼───┼───┤
       │Tog│Mod│Hui│   │      ┌───┐
       ├───┼───┼───┼───┤      │   │
       │   │Vai│Hud│Vad│      └───┘
       └───┴───┴───┴───┘
*/
    /*  Row:    0        1        2        3        4        */
    [_FN2] = LAYOUT(
                RGB_SPI, RGB_SPD, _______, QK_BOOT, _______,
                RGB_SAI, RGB_SAD, _______, _______, TO(_BASE),
                RGB_TOG, RGB_MOD, RGB_HUI, _______, _______,
                _______, RGB_VAI, RGB_HUD, RGB_VAD
            ),
};

#ifdef OLED_ENABLE
    bool oled_task_user(void) {
        render_layer_status();

        return true;
    }
#endif

#ifdef ENCODER_MAP_ENABLE
const uint16_t PROGMEM encoder_map[][NUM_ENCODERS][NUM_DIRECTIONS] = {
    [_BASE] = { ENCODER_CCW_CW(KC_MPRV, KC_MNXT), ENCODER_CCW_CW(KC_PGDN, KC_PGUP), ENCODER_CCW_CW(KC_VOLD, KC_VOLU) },
    [_FN]   = { ENCODER_CCW_CW(MI_C,    MI_D   ), ENCODER_CCW_CW(MI_E,    MI_F   ), ENCODER_CCW_CW(MI_A,    MI_B   ) },
    [_FN1]  = { ENCODER_CCW_CW(KC_TRNS, KC_TRNS), ENCODER_CCW_CW(KC_TRNS, KC_TRNS), ENCODER_CCW_CW(KC_TRNS, KC_TRNS) },
    [_FN2]  = { ENCODER_CCW_CW(KC_TRNS, KC_TRNS), ENCODER_CCW_CW(KC_TRNS, KC_TRNS), ENCODER_CCW_CW(KC_TRNS, KC_TRNS) },
};
#endif
Reply all
Reply to author
Forward
0 new messages