Skip to content

Commit

Permalink
Code cleanup for set middle C (#47)
Browse files Browse the repository at this point in the history
* Code cleanup for set middle C

- Tweak examples
- Add minor example for transpose
- Update README
- Code cleanup
- Bump version

* Rename file to suit a broader purpose in time

* Update eslintrc
  • Loading branch information
walmik committed Aug 25, 2017
1 parent 7366450 commit 4d8255e
Show file tree
Hide file tree
Showing 13 changed files with 140 additions and 144 deletions.
8 changes: 3 additions & 5 deletions .eslintrc
@@ -1,14 +1,12 @@
{
"rules": {
"quotes": [2, "single", "avoid-escape"],
"eqeqeq": 0,
"eqeqeq": 2,
"eol-last": 0,
"no-nested-ternary": 1,
"padded-blocks": [1, "never"],
"space-before-blocks": [1, "always"]
},
"ecmaFeatures": {
"modules": true
"space-before-blocks": [1, "always"],
"spaced-comment": 2
},
env: {
"es6": true
Expand Down
24 changes: 8 additions & 16 deletions README.md
Expand Up @@ -135,36 +135,28 @@ Import the 3 MIDI files thus generated into your favorite music creation softwar

[Simple kick drum with bass and some hats generated by Scribbletune](https://soundcloud.com/walmik/loop)

### Changing Middle C
Sometimes Middle C is not C4, but a C in a different octave. To set the octave for middle C use
### Middle C
MIDI specifications define middle C as the number 60. It does not state if this is on the third or the fourth octave. It just says, if you want a middle C, it will numerically be 60 from MIDI’s point of view.

Scribbletune equates C4 as the middle C. This is because the primary (and only) dependency of Scribbletune, [jsdmidgen](https://github.com/dingram/jsmidgen), sets 4 as middle C. But sometimes music creation software will associate middle C with another octave. To set the octave for middle C use
```js
scribble.setMiddleC(octave)
```
This automatically transposes every note to the new key determined by the new middle C.
So, let's say we're using a music editing software that uses C5 as it's middle C. If we were to use this,
This automatically transposes every note to the new key determined by the new middle C. Ableton Live, Propellerhead Reason, Steinberg Cubase, Logic Audio & Garage Band use C3 as their middle C. MIDI clips created via Scribbletune will end up creating the notes as per C4 as the middle C. And on importing, the software will display them as if they were an octave lower. Since the software **lowers** an octave for what you create with Scribbletune, you can **bump up** the middle C to an octave higher than the default to fix this.

```js
const scribble = require("scribbletune");

let clip = scribble.clip({
notes: ['c4', 'd4', 'e4', 'f4'],
pattern: 'x'.repeat(8)
});
scribble.setMiddleC(5); // Ask Scribbletune to export clips on an octave higher than the default

scribble.midi(clip, "octave.midi");
```
then the melody would seem to be one octave higher than it should be.
If we initialize the middle C octave to 5 like,
```js
const scribble = require("scribbletune");
scribble.setMiddleC(5)
let clip = scribble.clip({
notes: ['c4', 'd4', 'e4', 'f4'],
pattern: 'x'.repeat(8)
});

scribble.midi(clip, "octave.midi");
```
then the clip would sound like it should!
Now the imported clip will show the notes as they were added.

### Tranposing notes
Sometimes a clip might sound better if it was just a couple octaves higher or lower, but how could we transpose it that way?
Expand Down
5 changes: 4 additions & 1 deletion examples/bassline.js
Expand Up @@ -2,8 +2,11 @@

const scribble = require('../src/');

// Most music software consider C3 as middle C, so lets bump it by 1 to ensure our notes are imported as expected
scribble.setMiddleC(5);

// Get alternate notes from the C Phrygian mode
var notes = scribble.mode('c', 'phrygian').filter((x, i) => i % 2 === 0);
var notes = scribble.mode('c phrygian 2').filter((x, i) => i % 2 === 0);

// Generate 4 clips (one for each note) and concat them together
var clip = notes.reduce((accumulator, note) => {
Expand Down
2 changes: 1 addition & 1 deletion examples/kick.js
Expand Up @@ -3,7 +3,7 @@
const scribble = require('../src/');

let clip = scribble.clip({
notes: ['c3'],
notes: 'c4',
pattern: 'x---'.repeat(4)
});

Expand Down
2 changes: 1 addition & 1 deletion examples/scale.js
Expand Up @@ -3,7 +3,7 @@
const scribble = require('../src/');

let clip = scribble.clip({
notes: scribble.mode('c|major|4'),
notes: scribble.mode('c major 4'),
pattern: 'x_'.repeat(8)
});

Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "scribbletune",
"version": "0.10.1",
"version": "0.11.0",
"description": "Create music with JavaScript and Node.js!",
"main": "./src/index.js",
"scripts": {
Expand Down
13 changes: 2 additions & 11 deletions src/chord.js
@@ -1,9 +1,6 @@
'use strict';

const mode = require('./mode');
const setMiddleC = require('./setMiddleC');
// Regex for identifying chords

const chordPtn = /^([a-g][#|b]?)(d[io]m7{0,1}|[67]th|maj7{0,1}|min7{0,1}|m7{0,1}|sus[24]|aug|sixth)\-?([0-8])?/;

/**
Expand Down Expand Up @@ -89,10 +86,7 @@ modeMap['6th'] = modeMap.sixth;
* @param {String} str [examples: CMaj Cmaj cmaj Cm cmin f#maj7 etc]
* @return {Boolean}
*/
const isChord = (str) => {
let compStr = str.toLocaleLowerCase();
return compStr.match(chordPtn);
};
const isChord = str => str.toLowerCase().match(chordPtn);

/**
* Derive a chord from the given string. Exposed as simply `chord` in Scribbletune
Expand All @@ -107,10 +101,7 @@ const getChord = str => {
octave = octave || 4;
let m = mode(root.toLowerCase(), modeMap[scale].mode, octave);
modeMap[scale].int.forEach(i => {
const noteObj = {
note: m[i]
};
arr.push(setMiddleC.transposeNote(noteObj.note));
arr.push(m[i]);
});
});
return arr;
Expand Down
4 changes: 2 additions & 2 deletions src/index.js
Expand Up @@ -5,8 +5,8 @@ const clip = require('./clip');
const pattern = require('./pattern');
const midi = require('./midi');
const scales = require('./modes');
const setMiddleC = require('./setMiddleC');
const transpose = require('./transpose');
let modes = Object.keys(scales);

// Allow scale to be denoted by mode as well
module.exports = {mode: scale, scale, chord: chord.getChord, listChords: chord.listChords, modes, scales: modes, clip, pattern, midi, setMiddleC: setMiddleC.setMiddleC, transposeNote: setMiddleC.transposeNote};
module.exports = {mode: scale, scale, chord: chord.getChord, listChords: chord.listChords, modes, scales: modes, clip, pattern, midi, setMiddleC: transpose.setMiddleC, transposeNote: transpose.transposeNote};
8 changes: 4 additions & 4 deletions src/midi.js
Expand Up @@ -3,15 +3,15 @@
const fs = require('fs');
const assert = require('assert');
const jsmidgen = require('jsmidgen');
const setMiddleC = require('./setMiddleC');
const transpose = require('./transpose');

/**
* Take an array of note objects to generate a MIDI file in the same location as this method is called
* @param {Array} notes Notes are in the format: {note: ['c3'], level: 127, length: 64}
* @param {String} fileName If a filename is not provided, then `music.mid` is used by default
*/
const midi = (notes, fileName) => {
assert(notes !== undefined && typeof notes !== 'string', 'You must provide an array of notes to write!');
assert(Array.isArray(notes), 'You must provide an array of notes to write!');
fileName = fileName || 'music.mid';
let file = new jsmidgen.File();
let track = new jsmidgen.Track();
Expand All @@ -23,8 +23,8 @@ const midi = (notes, fileName) => {
// only the first noteOn (or noteOff) needs the complete arity of the function call
// subsequent calls need only the first 2 args (channel and note)
if (noteObj.note) {
noteObj.note = setMiddleC.transposeNote(noteObj.note);
//Transpose the note to the correct middle C
// Transpose the note to the correct middle C (in case middle C was changed)
noteObj.note = transpose.transposeNote(noteObj.note);
if (typeof noteObj.note === 'string') {
track.noteOn(0, noteObj.note, noteObj.length, level); // channel, pitch(note), length, velocity
track.noteOff(0, noteObj.note, noteObj.length, level);
Expand Down
7 changes: 4 additions & 3 deletions src/mode.js
Expand Up @@ -2,7 +2,7 @@

const assert = require('assert');
const modes = require('./modes');
const setMiddleC = require('./setMiddleC');
const transpose = require('./transpose');
const chromaticNotes = ['c', 'c#', 'd', 'd#', 'e', 'f', 'f#', 'g', 'g#', 'a', 'a#', 'b'];

/**
Expand Down Expand Up @@ -51,8 +51,9 @@ const mode = (root, mode, octave, addRootFromNextOctave) => {

root = root || 'c';
mode = mode || 'ionian';
octave = octave ? setMiddleC.transposeOctave(Number(octave)) : setMiddleC.transposeOctave(4);
//Transpose octave into correct octave determined by middle C
octave = +octave || transpose.defaultMiddleC;

// Transpose octave into correct octave determined by middle C
addRootFromNextOctave = addRootFromNextOctave !== false;

// Append octave to chromatic notes
Expand Down
81 changes: 0 additions & 81 deletions src/setMiddleC.js

This file was deleted.

91 changes: 91 additions & 0 deletions src/transpose.js
@@ -0,0 +1,91 @@
'use strict';
const assert = require('assert');
const defaultMiddleC = 4;

/**
* Transposition is a global that subtracts the provided value for middle C from the default middle C
* For e.g. if you set the middle C to 5, the transposition will be be set to defaultMiddleC - 5 = -1.
* While writing to MIDI, this "transposition" will be considered and a note entered as C4
* will appear as C4 in Ableton Live or Propellerhead Reason which consider C3 as the middle C.
* Without this adjustment it will look like C3 in most modern music creation software!
*/
let transposition = 0;

/**
* startOctave is a global to be able to transpose an array of notes relative to the octave of the first note
* in the array. (TODO: Ideally we need to come up with a better way than have this global var here)
*/
let startOctave;

/**
* Takes an integer and transposes all notes to a different middle C octave.
* @param {Integer} octave The new octave for middle C.
*/
function setMiddleC(octave) {
octave = Number(octave);
assert(Number.isInteger(octave), 'Octave must be an integer to set middle C.');
transposition = octave - defaultMiddleC;
}

/**
* Takes an octave and transposes it to the octave determined by transposition
* @param {Integer/String} initialOctave The initial octave
* @return {Integer} The correctly transposed octave
*/
function transposeOctave(initialOctave) {
initialOctave = Number(initialOctave); // Not using parseInt as it will convert invalid input such as 3.3 to 3
assert(Number.isInteger(initialOctave), 'Initial Octave must be an integer.');
return initialOctave += transposition;
}

/**
* Takes a single note or array of notes and transposes into the octave given by transposition or the octave param
* @param {String/Array} noteArg The Array/String contaning the note(s)
* @param {Integer} octave The octave to transpose to
* @return {String(s)} The correctly transposed note(s)
*/
const transposeNote = (noteArg, octave) => {
assert(typeof noteArg === 'string' || Array.isArray(noteArg));
assert(Number.isInteger(octave) || octave === undefined, 'Octave, if defined, must be an integer');
if(typeof noteArg === 'string') {
// If a single note was passed, transpose the single note
return _transposeSingle(noteArg, 0, octave);
} else {
// If an array of notes were passed, transpose every note in the array relative to the octave of the first note
return noteArg.map((n, i) => _transposeSingle(n, i, octave));
}
}
/**
* Private method to transpose a single note to the correct octave determined by transposition or the octave argument
* @param {String} note Note to be transposed
* @param {Integer} noteIndex Index in note array (if noteIndex is 0, we will use the octave of that note as a ref)
* @param {Integer} octave Optional octave to transpose to
* @return {String} Transposed note
*/
const _transposeSingle = (note, noteIndex, octave) => {
assert(typeof note === 'string', 'Note must be a string.');

// Get the root from the note, for e.g. get C from C4
let root = note.replace(/\d/g, '');

// Get the octave from the note, for e.g. get 4 from C4
let oct = +note.replace(/[^\d]/g, '');

// In case of an Array of notes, consider the first note's octave as the relative octave
// For e.g. If the input was ['c4', 'd5', 'e6'] with octave set to 6, dont convert it to ['c6', 'd6', 'e6']
// Instead, convert it to ['c6', 'd7', 'e8']. Basically bump octave relative to the first note in the array
// It took the first note 2 octaves to get to 6 from 4, hence move the rest of the notes up by 2 octaves only
// This is helpful for transposing chords & melodies.
if (noteIndex === 0) {
startOctave = oct;
}

if (octave) {
oct = octave + (oct - startOctave);
} else {
oct += transposition;
}
// Transpose the octave
return root + oct;
}
module.exports = {setMiddleC, transposeNote, transposeOctave, defaultMiddleC};

0 comments on commit 4d8255e

Please sign in to comment.