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.
The chord colors basically follow this scheme:
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.
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
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/
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
.
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.
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.
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.
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.
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.
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.
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
.
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.
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.