Awesome
Chords
Text-based chord progression editor
Type chord names in a textbox to create a sequence of play buttons that you can use like an instrument.
Try it in your browser: https://evashort.com/chords/
chordsite.slack.com is the Slack channel for discussing this project.
Follow this link for a Slack invite.
Current features
- Chords in textbox are colored to match play buttons
- Play chords as arpeggio, strum pattern, continuous pad, or basic strum
- Queue up a second chord to play in the next measure
- Select a key signature to transpose the entire chord progression
- Save progressions in URL to bookmark or share them
- Adjustable tempo
- Playable circle of 5ths
- 7th, 9th, and 13th chords
- Table showing all chords in key
- View recently played chords to capture improvised sequences
- On-screen piano keyboard that displays the most recently played chord
- Search for chords by selecting piano keys
- Option to save settings in local storage
- Tutorial for new users
Planned features
- Option to input chords as roman numerals
- Tap to set tempo
- Syntax to set duration of each chord
- Chord duration saved in "Recently played" view
- Export progression as MIDI
- Buttons to play entire line of progression
- View different Greek modes in chord table
- 7th chords displayed within circle of 5ths, with show/hide option
Possible features
- More arpeggio styles
- Mobile-friendly interface
- Suggestions for next chord in progression
- Support for selected slash chords
- Show chords on musical staff
- Show guitar chord diagrams
- Somehow indicate which notes are in the current key signature
- Allow focusing chords using arrow keys
Color choices
The chord colors basically follow this scheme:
- blue = tonic
- red = dominant
- green = subdominant
Chords are arranged in rainbow order as you move up the major scale by thirds. This way, triads that share two notes have neighboring colors.
Major and minor 7ths are colored by mixing the two triads they contain.
Diminished triads and chords that contain them are dark with white text.
The colors were chosen to be maximally saturated and close to the same lightness in the CIELAB color space. Since the red-green axis is missing for most people with color vision deficiency, the lightness varies to compensate, with green being lightest and red being darkest. The lightness was chosen to contrast with the text as required by WCAG 2.0.
Build steps
-
Install Elm from http://elm-lang.org/
-
From the project folder, run
elm make src/Main.elm --output main.js
-
Run
elm reactor
-
Go to the URL shown, most likely http://localhost:8000
-
Click on
index.html
IDE setup
-
Install VS Code from https://code.visualstudio.com/
-
Within VS Code, install the
elm
extension -
Install Node.js from https://nodejs.org/en/
-
Use npm to install elm-live: https://github.com/architectcodes/elm-live
In .vscode/tasks.json
, there is a build command to start elm-live from within VS Code. To run it, click Run Build Task... in the Tasks menu. You only have to do this once at the beginning of each session. Once elm-live is running, it will recompile the project every time you save a file. Your changes should appear immediately at the URL displayed by elm-live, most likely http://localhost:8000/
Implementation details
On the JS side, I use the Web Audio API to create a new chain of audio nodes for each note that is played. A sawtooth oscillator feeds into a gain node and then a lowpass filter, both of which decrease over time. There is a loop that disconnects the filter nodes so they can be garbage-collected.
On the Elm side, when you click a chord play button, I generate a list of
AudioChange
objects, such as AddNote { t : Float, f : Float }
or
MuteAllNotes
. Then I encode them with a custom JSON encoder and send them
through a port to JS. So the entire arpeggio is sent to the Web Audio API as
soon as you click the button. In order for Elm to schedule the notes at the
right time, I had to write a native module called
AudioTime
to access the audio clock.
Elm also sets "alarms", which are times when the UI needs to be updated. The
loop in audio.js
notifies Elm when an alarm has passed.
In order to programmatically replace text in the textarea without erasing the
undo history, we secretly create a new textarea for every replacement. If the
textarea receives a ctrl+z key press and the text doesn't change within 5ms,
we assume that the user is trying to undo the programmatic change and we
restore the old textarea. Same goes for redo. In Chrome and Edge, pressing
ctrl+z can also affect the hidden textareas so we have to detect and
counteract that. This is all handled on the JS side in
theater.js
,
with an Elm interface in
Theater.elm
.
Design principles
Keep it flat flat flat
Refactor code when it becomes hard to manage, not when it becomes repetitive. Refactoring for the sake of code re-use tends to introduce extra abstraction layers which can hide opportunities for more meaningful refactors. The same is true of refactors that attempt to increase encapsulation.
Simple is better than pure
It's usually a bad idea to write extra code that smooths over an ugly hack for the sake of "correctness". Expect new features to introduce rough edges to the code base. If you let ugly code sit for a while, you'll often find a better solution while working on a different feature.
Always be open to removing features
The most important refactors start by questioning the utility of a feature you always thought was essential. This project was essentially dead for two months because the code got too complicated to maintain. I was able to bring it back by removing the ability to invert chords, a feature that had been baked into the design from the beginning.
Accept no glitches
As users, we don't always notice how much mental energy we spend compensating for interfaces that are slightly unpredictable. We develop a lot of unconscious behaviors like double-checking that a certain element has focus. This program is meant to act like an extension of the user's brain as they reason about chords, so it's very important to fix any glitches that might sap their mental energy.
The UI should be normal
Use standard controls like checkboxes, buttons, and drop-down menus. Don't style them with custom CSS. Yes, I know broke this rule by making fancy Mac-inspired radio buttons. Each UI element should have an explicit text description that's always visible so users know what it does at a glance. Don't hide things to save space unless absolutely necessary.
Strong opinions loosely held
Keep the UI simple by not overwhelming the user with choices. This means having an opinion about everything. If a user wants something to be customizable, see if you can accommodate their use case by changing your opinion instead.
Variable names
Prefer single-word variable names that refer to concrete objects in the real
world. Use a thesaurus to spark your creativity but forego the long words.
Take advantage of metaphors to name multiple related variables. For example, a
1D list containing multiple rows of data separated with a delimiter is called
a train
, and each row of data is called a car
.
Fail loudly
It's not always practical to rely on Elm's guarantee of no runtime errors. For
example, you might let y = modBy 12 x
and then assume that y < 12
. Or you
add an item to a list and then assume that the list is not empty. Make these
assumptions explicit by calling Debug.todo
if they fail and printing some
useful information.
Code style
I don't have strong opinions about code style and formatting; I'm just doing what works for me. I'd like to start using elm-format but I'm afraid it will ruin the commit history.