DEV Community

red blue
red blue

Posted on • Originally published at keystrum.app

How keystrum turns a QWERTY keyboard into a 6-chord strum machine

The QWERTY keyboard already has the right shape: rows of keys, columns of keys. What if each column became a chord, and each row became a strum lane?

That's keystrum — a browser-based instrument that maps your keyboard to six chords. No external instrument, no samples, no install. Pure Web Audio.

The layout

         Am       C       Em       G       Dm       F
       ┌──────┬──────┬──────┬──────┬──────┬──────┐
  E4   │  1   │  2   │  3   │  4   │  5   │  6   │  ← highest pitch
       ├──────┼──────┼──────┼──────┼──────┼──────┤
  B3   │  Q   │  W   │  E   │  R   │  T   │  Y   │
       ├──────┼──────┼──────┼──────┼──────┼──────┤
  G3   │  A   │  S   │  D   │  F   │  G   │  H   │
       ├──────┼──────┼──────┼──────┼──────┼──────┤
  D3   │  Z   │  X   │  C   │  V   │  B   │  N   │  ← lowest pitch
       └──────┴──────┴──────┴──────┴──────┴──────┘
Enter fullscreen mode Exit fullscreen mode

Each column is one chord. Sweeping a column top-to-bottom — say 2 W S X for C — fires four notes in fast succession. That's a strum.

Strum detection

The trick is detecting intent. A user pressing 2 W S X simultaneously is one event; pressing them spread across 200ms is another. keystrum's rule:

Three or more keys in the same column within 90ms = a strum. Otherwise, individual notes.

// Simplified strum detector
const STRUM_WINDOW_MS = 90;
const buffer: KeyEvent[] = [];

function onKeyDown(key: string, time: number) {
  buffer.push({ key, time });
  buffer.splice(0, buffer.findIndex((e) => time - e.time < STRUM_WINDOW_MS));

  const sameColumn = buffer.filter((e) => columnOf(e.key) === columnOf(key));
  if (sameColumn.length >= 3) {
    triggerStrum(columnOf(key), buffer);
  } else {
    triggerNote(key, time);
  }
}
Enter fullscreen mode Exit fullscreen mode

The 90ms window is the sweet spot from playtests — short enough that intentional taps stay individual, long enough that real strums get caught.

Karplus-Strong, in 30 lines

The sound comes from Karplus-Strong synthesis, a physical-modeling technique from 1983 that simulates plucked strings with a delay line and a low-pass filter. No samples. The whole thing runs in AudioWorklet.

// AudioWorklet processor — pluck a string at frequency f
class PluckProcessor extends AudioWorkletProcessor {
  delay: Float32Array;
  index = 0;

  constructor(opts: AudioWorkletNodeOptions) {
    super();
    const len = Math.floor(sampleRate / opts.processorOptions.frequency);
    this.delay = new Float32Array(len);
    for (let i = 0; i < len; i++) this.delay[i] = Math.random() * 2 - 1; // noise burst
  }

  process(_inputs: Float32Array[][], outputs: Float32Array[][]) {
    const out = outputs[0][0];
    const len = this.delay.length;
    for (let i = 0; i < out.length; i++) {
      const next = (this.index + 1) % len;
      this.delay[this.index] = (this.delay[this.index] + this.delay[next]) * 0.5 * 0.996;
      out[i] = this.delay[this.index];
      this.index = next;
    }
    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode

The decay factor 0.996 controls sustain. The 0.5 averaging is the low-pass filter. That's the entire string model.

Why no samples

Samples are 5-50MB per instrument. They lock the timbre. They introduce attack-time latency from disk decode. Karplus-Strong is stateless math — sampleRate / frequency floats per voice, total latency under one buffer (about 6ms at 48kHz). The whole bundle is under 100KB.

What's next

  • Mobile gestures (sweep on touchscreen)
  • More chord sets (jazz voicings, modal scales)
  • Sustain pedal mapping to spacebar

Try it live → — works in any modern browser. Source: github.com/kimhinton/keystrum.

Top comments (0)