Building a Custom Hammond B-3 MIDI Controller: 37 Keys, 9 Busbars, and Analog Drawbars

Converting a vintage Hammond B-3 manual into a modern MIDI controller is a masterclass in hybrid hardware design. The goal is ambitious: take a 3-octave manual (37 keys) outfitted with the original multi-contact busbar system—where each key features 9 distinct mechanical contacts corresponding to the organ’s harmonic drawbars—and translate that vintage mechanical action into a fully expressive, velocity-sensitive USB MIDI instrument.

Decoding the Busbar Architecture: Digital Scanning vs. Analog Sensing

A common point of confusion in vintage conversions is determining how to read the contacts. Because original Hammond key contacts are mechanical make/break switches that tap against physical busbars, they act strictly as binary switches rather than analog voltage sources. Consequently, dedicated Analog-to-Digital Converters (ADCs) are not required for the key contacts themselves.

Instead, managing 333 individual contact points (37 keys $\times$ 9 contacts) requires a high-density digital scanning architecture. Using multiplexers (such as the 74HC4067) or shift registers (like the 74HC165) configured in a matrix allows a microcontroller to scan every intersection efficiently without exhausting its GPIO pin footprint.

Deriving Velocity from Staggered Contacts

One of the most powerful aspects of retaining all 9 contacts per key is the ability to generate authentic velocity data without needing modern weighted hammer-action sensors. In a B-3 keybed, the 9 contacts are mechanically staggered. As a key is depressed, the contacts close sequentially. By measuring the time delta ($\Delta t$) between the initial contact closure and the final contact closure, a fast microcontroller can calculate press velocity and translate it into a standard MIDI velocity curve (0–127).

Integrating the Drawbars

While the key contacts rely on digital scanning, the physical drawbars require true analog sensing. Because slide potentiometers output a continuous variable voltage representing their physical position, they interface directly with analog multiplexers routed to the microcontroller’s ADC pins. Each of the 9 drawbars dynamically scales the gain or velocity response of its corresponding harmonic busbar channel, authentically replicating how a real tonewheel organ shapes amplitude across its harmonic spectrum.

The Recommended Hardware Stack

Bringing this project to life requires a robust processing and I/O ecosystem:

  • Microcontroller: A Teensy 4.1 is ideal, offering blistering clock speeds, abundant GPIOs, built-in ADC channels for the drawbar potentiometers, and native USB MIDI class compliance.
  • Key Scanning:74HC165 shift registers or custom matrix logic to reliably scan the 333 key-to-busbar junctions.
  • Drawbar Processing:74HC4067 analog multiplexers to multiplex the continuous resistance values from the drawbar pots into the microcontroller’s analog inputs.

By marrying vintage mechanical leaf-switch geometry with modern micro-controller scanning and analog scaling, this project bridges the gap between mid-century electromechanical design and contemporary music production.

1. Key Scanning: 74HC165 Shift Registers (37 Keys $\times$ 9 Busbars)

To handle 333 inputs efficiently, we daisy-chain 74HC165 8-bit parallel-in/serial-out shift registers. Since $333 \div 8 = 41.625$, you will need 42 shift registers chained together.

The Teensy 4.1 controls the whole chain using just 3 digital pins (SPI or bit-bang):

  • Teensy Pin 11 (MOSI / SCLK alternative or dedicated shift clock): Connected to the Clock (CP) pin of all shift registers in parallel.
  • Teensy Pin 12 (MISO): Connected to the Serial Out ($Q_7$) of the last shift register in the chain.
  • Teensy Pin 2: Connected to the Parallel Load ($\overline{PL}$ or $\overline{LD}$) pin of all shift registers in parallel.
  • Power & Ground:
    • Connect all shift register $V_{CC}$ pins to 3.3V (Teensy 4.1 logic is 3.3V tolerant; do not use 5V logic inputs directly on Teensy pins).
    • Connect all GND pins to system GND.

2. Drawbar Processing: 74HC4067 Analog Multiplexers (9 Drawbars)

A standard Hammond set has 9 drawbars. A single 74HC4067 16-channel analog multiplexer can easily handle all 9 drawbars (with 7 channels left over for future expansion, like presets or expression pedals).

  • Teensy Analog Pin (e.g., A0 / Pin 14): Connected to the SIG/COM (Common Output/Input) pin of the 74HC4067.
  • Address Control Pins (Binary Select):
    • Teensy Pin 4 $\rightarrow$ Address S0
    • Teensy Pin 5 $\rightarrow$ Address S1
    • Teensy Pin 6 $\rightarrow$ Address S2
    • Teensy Pin 7 $\rightarrow$ Address S3
  • Enable Pin ($\overline{E}$): Connected directly to GND (permanently enabled).
  • Power & Ground:
    • $V_{CC}$: 3.3V (matches Teensy ADC reference range).
    • GND: System GND.
    • VEE: System GND (since you are running on a single 3.3V supply).
  • Drawbar Connections:
    • Connect the wiper (variable pin) of Drawbar 1 to channel C0, Drawbar 2 to C1, …, up to Drawbar 9 to C8.
    • Connect one outer lug of each drawbar pot to 3.3V and the other outer lug to GND.

Master Teensy 4.1 Pin Mapping Summary

Teensy 4.1 PinFunction / ConnectionTarget Component
Pin 2Digital Output (Latch)Shift Register Parallel Load ($\overline{PL}$)
Pin 11Digital Output (Clock)Shift Register Clock (CP)
Pin 12Digital Input (Data In)Shift Register Serial Out ($Q_7$ of last chip)
Pin 4Digital Output (Address S0)74HC4067 Multiplexer S0
Pin 5Digital Output (Address S1)74HC4067 Multiplexer S1
Pin 6Digital Output (Address S2)74HC4067 Multiplexer S2
Pin 7Digital Output (Address S3)74HC4067 Multiplexer S3
Pin 14 (A0)Analog Input74HC4067 Multiplexer SIG/COM
3.3VPower OutputShift Register $V_{CC}$ & Multiplexer $V_{CC}$
GNDGroundCommon Ground Bus

Switching to an ESP32 is a fantastic choice, especially if you want built-in Wi-Fi MIDI (Apple MIDI Network) and Bluetooth LE MIDI alongside physical MIDI out.

Because the ESP32 operates at 3.3V logic (matching the Teensy requirement) and has robust processing capabilities, shifting the design over is straightforward. However, the ESP32’s pin architecture is a bit more flexible—and fragmented—than the Teensy, so assigning dedicated pins requires keeping its strappings and ADC limitations in mind.

Master ESP32 Pin Mapping Summary (Standard 38-Pin DevKit)

ESP32 PinFunction / ConnectionTarget Component
GPIO 23 (MOSI)SPI Data / Shift Register ClockShift Register Clock (CP)
GPIO 19 (MISO)SPI Data InShift Register Serial Out ($Q_7$ of last chip)
GPIO 5 (SS/CS)Digital Output (Latch)Shift Register Parallel Load ($\overline{PL}$)
GPIO 16Digital Output (Address S0)74HC4067 Multiplexer S0
GPIO 17Digital Output (Address S1)74HC4067 Multiplexer S1
GPIO 18Digital Output (Address S2)74HC4067 Multiplexer S2
GPIO 19 (Digital Output (Address S3)74HC4067 Multiplexer S3 (Wait—see note below)
GPIO 34 (ADC1_6)Analog Input74HC4067 Multiplexer SIG/COM
GPIO 1 (TX0) or CustomHardware UART MIDI Out5-Pin DIN MIDI Jack (via 220Ω resistors)
3.3VPower OutputShift Register & Multiplexer $V_{CC}$
GNDGroundCommon Ground Bus

(Note on ESP32 GPIO clashes: Standard ESP32 dev boards share pins if you aren’t careful. Let’s optimize the pinout specifically to avoid conflicts with SPI and ADC2).

Optimized ESP32 Pinout for Keybed & Drawbars

1. Key Scanning (74HC165 Shift Registers – 42 Chips)

You can use the ESP32’s hardware VSPI bus to blast through reading those 42 shift registers at high speed, keeping your loop times ultra-low for accurate velocity timing.

  • Clock (CP): GPIO 18 (VSPI SCK)
  • Serial Out ($Q_7$): GPIO 19 (VSPI MISO)
  • Parallel Load ($\overline{PL}$): GPIO 4 (General GPIO Output for latching)
  • Verification: Toggle GPIO 4 low then high, read the SPI bytes, and check that the bitstream updates instantly when a key is pressed.

2. Drawbar Processing (74HC4067 Multiplexer)

  • Analog Input (SIG/COM): GPIO 34 (Note: Use an ADC1 pin like GPIO 34, 35, 36, or 39. Avoid ADC2 pins because Wi-Fi disables ADC2 when active!).
  • Address Lines (S0–S3):
    • S0: GPIO 25
    • S1: GPIO 26
    • S2: GPIO 27
    • S3: GPIO 14
  • Verification: Sweep a drawbar from top to bottom and monitor the serial plotter or console to ensure the analog values smoothly scale from 0 to 4095 (ESP32 has 12-bit ADCs).

3. MIDI Out (Physical Jack)

  • Use Hardware UART2 (TX on GPIO 17, RX unused) connected to a standard MIDI out circuit (a 5-pin DIN port wired with a 220-ohm current-limiting resistor on pin 5, pin 4 to 3.3V through 220 ohms, and pin 2 to GND).

Moving this project to Rust on the ESP32 is a fantastic choice. Rust gives you the deterministic performance and zero-cost abstractions of C/C++ combined with memory safety—crucial when you are processing 333 digital inputs and 9 analog axes fast enough to calculate key-velocity deltas without jitter.

Because you want Wi-Fi MIDI alongside bare-metal scanning speed, the modern ecosystem utilizes esp-hal (for bare-metal peripheral control) paired with esp-wifi (for network stacks).

1. Recommended Cargo Dependencies (Cargo.toml)

To wire up your peripherals, timing, and Wi-Fi, your Cargo.toml will look something like this:

Ini, TOML

[package]
name = "hammond-esp32-midi"
version = "0.1.0"
edition = "2021"

[dependencies]
esp-hal = { version = "0.22", features = ["esp32", "embassy", "embassy-time-driver"] }
esp-wifi = { version = "0.11", features = ["esp32", "wifi"] }
embassy-executor = { version = "0.6", features = ["task-arena-size-32768", "arch-xtensa", "time", "integrated-timers"] }
embassy-time = { version = "0.3" }
embedded-hal = "1.0"
embedded-hal-async = "1.0"

2. Architecture & Rust Implementation Blueprint

Here is how the core architecture translates into idiomatic Rust using embassy for async task management or a tight polling loop for maximum velocity-timing precision.

A. Scanning the 42 Shift Registers (74HC165)

Because you have 333 contact points, reading them via hardware SPI (MISO, SCK) is lightning fast. You pulse the Parallel Load line (PL), then read 43 bytes over SPI.

Rust

use embedded_hal::digital::{OutputPin, InputPin};

pub struct KeyScanner<SPI, PL> {
    spi: SPI,
    parallel_load: PL,
}

impl<SPI, PL> KeyScanner<SPI, PL> 
where
    SPI: embedded_hal::spi::SpiDevice<u8>,
    PL: OutputPin,
{
    pub fn scan(&mut self, buffer: &mut [u8; 43]) -> Result<(), &'static str> {
        // 1. Latch the parallel inputs into the shift register chain
        self.parallel_load.set_low().map_err(|_| "Failed to set PL low")?;
        // Brief hardware delay or CPU nop if needed for pulse width
        self.parallel_load.set_high().map_err(|_| "Failed to set PL high")?;

        // 2. Shift out all 43 bytes (333 bits total)
        self.spi.read(buffer).map_err(|_| "SPI read failed")?;
        
        Ok(())
    }
}

B. Reading the Drawbars via Multiplexer (74HC4067)

To read the 9 drawbars, you cycle the address lines (S0–S3) from 0 to 8, set the multiplexer channel, and read the ADC.

Rust

pub struct DrawbarReader<ADC, S0, S1, S2, S3> {
    adc_pin: ADC,
    s0: S0,
    s1: S1,
    s2: S2,
    s3: S3,
}

impl<ADC, S0, S1, S2, S3> DrawbarReader<ADC, S0, S1, S2, S3>
where
    ADC: embedded_hal::analog::Adc<Word = u16>, // or ESP32 specific ADC trait
    S0: OutputPin,
    S1: OutputPin,
    S2: OutputPin,
    S3: OutputPin,
{
    pub fn read_drawbar(&mut self, channel: u8) -> u16 {
        // Set address lines (S3, S2, S1, S0) based on channel (0-8)
        let _ = self.s0.set_state(((channel >> 0) & 1) == 1);
        let _ = self.s1.set_state(((channel >> 1) & 1) == 1);
        let _ = self.s2.set_state(((channel >> 2) & 1) == 1);
        let _ = self.s3.set_state(((channel >> 3) & 1) == 1);

        // Allow settling time for the analog mux switch
        cortex_m::asm::delay(20); 

        // Read ADC value (0 - 4095 on ESP32 12-bit ADC)
        // self.adc_pin.read() ...
        0 // Placeholder return
    }
}

C. Velocity Calculation Logic

To derive velocity from the staggered contacts, your main scanning loop tracks state transitions across time:

  1. Contact 1 Closes: Record timestamp $t_1$.
  2. Contact 9 Closes: Record timestamp $t_9$.
  3. Calculate Delta:$\Delta t = t_9 – t_1$.
  4. Map to MIDI: Scale $\Delta t$ inversely into a velocity curve (fast transit time = high velocity like 127; slow transit time = low velocity like 20).

Wi-Fi MIDI Integration

With esp-wifi, you can initialize an async Wi-Fi station stack, connect to your local network, and spawn an RTP-MIDI server session using standard UDP packets over port 5004. This allows your custom B-3 hardware to stream wireless MIDI straight into your DAW (Logic, Ableton, Reaper) with minimal latency.