evashort / chords

Text-based chord progression editor
http://evanshort.name/chords/
MIT License
28 stars 1 forks source link
chord-progression chord-visualization chords elm elm-lang elm-language music-composition music-theory web-audio web-audio-api

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

Planned features

Possible features

Color choices

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.

Build steps

  1. Install Elm from http://elm-lang.org/

  2. From the project folder, run elm make src/Main.elm --output main.js

  3. Run elm reactor

  4. Go to the URL shown, most likely http://localhost:8000

  5. Click on index.html

IDE setup

  1. Install VS Code from https://code.visualstudio.com/

  2. Within VS Code, install the elm extension

  3. Install Node.js from https://nodejs.org/en/

  4. 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.