diff --git a/.eslintrc b/.eslintrc index 69acf19..fc4b1be 100644 --- a/.eslintrc +++ b/.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 diff --git a/README.md b/README.md index 1dd1a16..1a6aac9 100644 --- a/README.md +++ b/README.md @@ -135,28 +135,20 @@ 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) @@ -164,7 +156,7 @@ let clip = scribble.clip({ 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? diff --git a/examples/bassline.js b/examples/bassline.js index c64581f..d266300 100644 --- a/examples/bassline.js +++ b/examples/bassline.js @@ -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) => { diff --git a/examples/kick.js b/examples/kick.js index f120391..4a955ff 100644 --- a/examples/kick.js +++ b/examples/kick.js @@ -3,7 +3,7 @@ const scribble = require('../src/'); let clip = scribble.clip({ - notes: ['c3'], + notes: 'c4', pattern: 'x---'.repeat(4) }); diff --git a/examples/scale.js b/examples/scale.js index fbcc506..c07c7dc 100644 --- a/examples/scale.js +++ b/examples/scale.js @@ -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) }); diff --git a/package.json b/package.json index de1ffad..e7b00b5 100644 --- a/package.json +++ b/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": { diff --git a/src/chord.js b/src/chord.js index 9791b36..720eb0c 100644 --- a/src/chord.js +++ b/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])?/; /** @@ -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 @@ -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; diff --git a/src/index.js b/src/index.js index 0d1c6e1..fbaebf4 100644 --- a/src/index.js +++ b/src/index.js @@ -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}; diff --git a/src/midi.js b/src/midi.js index 1c50f2c..1158902 100644 --- a/src/midi.js +++ b/src/midi.js @@ -3,7 +3,7 @@ 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 @@ -11,7 +11,7 @@ const setMiddleC = require('./setMiddleC'); * @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(); @@ -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); diff --git a/src/mode.js b/src/mode.js index a6b6ba2..1df2c63 100644 --- a/src/mode.js +++ b/src/mode.js @@ -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']; /** @@ -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 diff --git a/src/setMiddleC.js b/src/setMiddleC.js deleted file mode 100644 index 9dcf0c8..0000000 --- a/src/setMiddleC.js +++ /dev/null @@ -1,81 +0,0 @@ -'use strict'; -const assert = require('assert'); -let transposition = 0; -let startOctave; -//Initialzing startOctave for later -/** - * Takes an integer and transposes all notes to a different middle C octave. - * @param {Integer} octaveIndex The new octave for middle C. - */ -function setMiddleC(octaveIndex) { - assert(Number.isInteger(octaveIndex), 'Octave Index must be an integer.'); - transposition = octaveIndex - 4; -} - -/** - * 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) { - assert(Number.isInteger(initialOctave) || (typeof initialOctave === 'string' && Number.isInteger(parseFloat(initialOctave))), 'Initial Octave must be an integer or an integer in a string.'); - if(typeof initialOctave === 'string') { - initialOctave = parseInt(initialOctave); - } - return initialOctave += transposition; -} -/** - * Takes a noteObj and transposes the note into the octave given by transposition - * @param {String/Array} noteArg The Array/String contaning the note(s) - * @param {Integer} octave Optional octave to transpose to - * @return {String(s)} The correctly transposed note(s) - */ -const transposeNote = (noteArg, octave) => { - let note; - assert(noteArg !== undefined && (typeof noteArg === 'string' || typeof noteArg === 'object') && noteArg.note === undefined && noteArg[0] !== undefined, 'NoteArg must contain a note that is either an array or a string.'); - assert(Number.isInteger(octave) || octave === undefined, 'Octave must be an integer'); - if(typeof noteArg === 'string') { - //If a single note was passed, transpose the single note - note = transposeSingle(noteArg, 0, octave); - } else { - //If an array of notes were passed, transpose every note in the array - note = []; - //Create an array for the transposed notes to be stores in - for(var i = 0; i { - assert(typeof note === 'string', 'Note must be a string.'); - assert(Number.isInteger(octave) || octave === undefined, 'If octave is passed, it must be an integer.'); - let index = 1; - if (isNaN(note[1])) { - //Test if note is a single character like 'a5' or if it is like 'ab5' - index = 2; - } - let oct = parseInt(note.slice(index,note.length)); - if (noteIndex === 0) { - startOctave = oct; - } - //Parse the octave into an integer - if (octave) { - oct = octave + (oct - startOctave); - } else { - oct += transposition; - } - //Transpose the octave - return note.slice(0,index) + oct.toString(); -} -module.exports = {setMiddleC, transposeNote, transposeOctave}; \ No newline at end of file diff --git a/src/transpose.js b/src/transpose.js new file mode 100644 index 0000000..08a42f7 --- /dev/null +++ b/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}; diff --git a/test/setMiddleC.js b/test/transpose.js similarity index 71% rename from test/setMiddleC.js rename to test/transpose.js index 3011fdf..d2ad54f 100644 --- a/test/setMiddleC.js +++ b/test/transpose.js @@ -2,7 +2,7 @@ const test = require('tape'); const scribble = require('../src/index'); -const setMiddleC = require('../src/setMiddleC'); +const transpose = require('../src/transpose'); const AssertionError = require('assert').AssertionError; test('Scribbletune::setMiddleC', t => { scribble.setMiddleC(5); @@ -27,89 +27,90 @@ test('Scribbletune::setMiddleC', t => { ); t.equal( - setMiddleC.transposeOctave(4), + transpose.transposeOctave(4), 5, 'Octave is correctly transposed for int parameter' ); t.equal( - setMiddleC.transposeOctave('3'), + transpose.transposeOctave('3'), 4, 'Octave is correctly transposed for string parameter' ); t.throws( - (() => setMiddleC.transposeOctave({octave: '4'})), + (() => transpose.transposeOctave({octave: '4'})), AssertionError, 'transposeOctave throws an error on invalid argument' ); t.throws( - (() => setMiddleC.transposeOctave(4.3)), + (() => transpose.transposeOctave(4.3)), AssertionError, 'transposeOctave throws an error for non-integer argument' ); t.throws( - (() => setMiddleC.transposeOctave('4.3')), + (() => transpose.transposeOctave('4.3')), AssertionError, 'transposeOctave throws an error for a non-integer string argument' ); t.equals( - setMiddleC.transposeNote('c4'), + transpose.transposeNote('c4'), 'c5', 'transposeNote returns the correct note for a natural' ); t.equals( - setMiddleC.transposeNote('ab2'), + transpose.transposeNote('ab2'), 'ab3', 'transposeNote returns the correct note for a flat' ); t.equals( - setMiddleC.transposeNote('e#3'), - 'e#4', + transpose.transposeNote('f#3'), + 'f#4', 'transposeNote returns the correct note for a sharp' ); t.deepEquals( - setMiddleC.transposeNote(['a2', 'c#3', 'db4']), + transpose.transposeNote(['a2', 'c#3', 'db4']), ['a3', 'c#4', 'db5'], 'transposeNote returns the correct note for an array of notes' ); t.throws( - (() => setMiddleC.transposeNote({note: 'c4'})), + (() => transpose.transposeNote({note: 'c4'})), AssertionError, 'transposeNote throws an error with an object argument' ); t.deepEquals( - setMiddleC.transposeNote(['ab2', 'b#3', 'c4', 'd1'], 4), + transpose.transposeNote(['ab2', 'b#3', 'c4', 'd1'], 4), ['ab4', 'b#5', 'c6', 'd3'], - 'transposeNote correctly transposes an array of notes with an octave given' + 'transposeNote correctly transposes an array of notes relative to the first note' ); t.equals( - setMiddleC.transposeNote('a3', 5), + transpose.transposeNote('a3', 5), 'a5', 'transposeNote correctly transposes a note with an octave given' ); t.throws( - (() => setMiddleC.transposeNote('a3', 3.3)), + (() => transpose.transposeNote('a3', 3.3)), AssertionError, 'transposeNote throws an error with a non-integer octave' ); t.throws( - (() => setMiddleC.transposeNote('d1', '2')), + (() => transpose.transposeNote('d1', '2')), AssertionError, 'transposeNote throws an error with a string octave' ) t.end(); }) + +// Revert middle C to default middle C (which is 4) scribble.setMiddleC(4); -//Revert the scribble package to normal middleC \ No newline at end of file