Introduction
For a long time I’ve disliked the traditional way of simulating a synth/piano keyboard on a computer. Using the bottom row of letters for white keys and the ones above them as black wastes a lot of potential space for additional notes, leading to a very limited octave range. Not to mention that usually if a song is played in a specific scale, not even all the notes will be used.
I want to build a more modular system that utilizes the buttons on the computer keyboard more effectively. I’ve recently played a lot of kalimba, and I feel like much design inspiration can be taken from that instrument.
The traditional layout problems
The traditional approach of mapping a piano keyboard to a computer one consists of approximating the note positions from the instrument.
The main benefit of this approach is that it’s very intuitive. If you’ve played a piano / synth before, the notes are where you expect them to be.
There are however, multiple shortfalls in this approach:
- The note range is very limited, at just about 2 and a half octaves. This is a huge problem for playing pieces or experimenting with ideas that cover a wider spectrum of pitches.
- The layout is inefficient. Some notes are repeated twice, while at the same time, multiple keys aren’t used for any notes.
- The selection of scales is limited, as the starting note is C. This is especially annoying when playing music where the root note is one of the notes behind C, leading to an even further reduced playing range.
Exploring new layouts
The app
To explore new options, I developed a small app where the user can build dynamic layouts for playing piano notes. The main idea is to focus on the computer keyboard for what it is and not try to replicate any physical instrument.
Main interface
The app looks like this and is accessible at https://piano.dimitars.dev/. It consists of multiple input rows and a button for adding a new input. In the example above, there are 3 input rows.
Individual input
Each input row has 5 drop down boxes, allowing the player to configure the following parameters:
- The input keys determine which keys are used to play notes. In the example above, these are all of the keys in the first row of letters, just above the space bar.
- The root note determines which is the tonic of the scale.
- The octave determines what is the octave of the root note.
- The scale type determines the intervals through which consequent notes are played.
- The step determines which is the first note of the scale being played, considering 1 is the tonic.
Diatonic rows approach
Let’s say you want to play something in G major. A very intuitive way would be to have each row of keys acting as steps in the scale. In the images above, the app is configured to play this way.
The immediate gain from this approach is having almost 5 octaves of range! There are also no unused keys, however there are some redundant notes that can be further optimized.
Having only scale notes comes with a drawback however: there’s no comfortable way to play outside the scale.
Optimized diatonic rows
Building off the previous idea, moving the starting note off the tonic allows for each consecutive row to start from the note next to the one where the previous row ended. This allows for the keyboard to be used optimally, in a way such that every key has a purpose.
Quick chords
The same row of keys can be used for multiple inputs. Having 3 inputs and shifting each by a third from the last will lead to producing a triad when pressing any key from that row.
Implementation details
Tech stack
Due to this being a small prototype project, I opted for using vanilla HTML, CSS and JavaScript. If I want to expand my ideas further, I’ll build a new project from scratch, using a more scalable stack.
Cycle generator
Inspired by Python’s itertools.cycle, this is a very convenient utility for cycling through a list of items. In the example below, I use it to repeat a sequence of note distances when assigning notes to keyboard keys:
function* cycle(arr) {
while (true) {
for (const item of arr) {
yield item;
}
}
}
const scale_steps_map = {}
scale_steps_map["Chromatic"] = [1]
scale_steps_map["Diatonic"] = [2,2,1,2,2,2,1]
scale_steps_map["Harmonic Minor"] = [2,1,2,2,1,3,1]
scale_steps_map["Pentatonic"] = [2,2,3,2,3]
let keys = Array.from("zxcvbnm,./")
let scale_steps = scale_steps_map["Diatonic"]
const steps = cycle(scale_steps)
let temp_note = 42
keys.forEach(key => {
const current_note = temp_note
// Do some logic with the current note
// In this app, I added button press event handlers
temp_note += steps.next().value
})
External Resources
- https://gitlab.com/mitk0100/web-piano project repository
- https://commons.wikimedia.org/wiki/File:An_unexploded_75%25_keyboard_layout,_April_2024.png computer keyboard layout image
- https://commons.wikimedia.org/wiki/File:KEYS.TIF piano layout image
- https://freesound.org/people/TEDAgame/packs/25405/ piano sound samples