Skip to content

Commit

Permalink
Merge 2c8732d into bae66eb
Browse files Browse the repository at this point in the history
  • Loading branch information
no-chris committed Nov 28, 2023
2 parents bae66eb + 2c8732d commit 3c741da
Show file tree
Hide file tree
Showing 12 changed files with 315 additions and 74 deletions.
12 changes: 7 additions & 5 deletions packages/chord-mark/src/parser/getAllKeysInSong.js
Expand Up @@ -14,16 +14,18 @@ export default function getAllKeysInSong(allLines, allChords) {
explicit: [],
};

const autoDetectedKey = guessKey(allChords);
if (autoDetectedKey) {
allKeys.auto = autoDetectedKey;
}

allLines.forEach((line) => {
if (line.type === lineTypes.KEY_DECLARATION) {
allKeys.explicit.push(_cloneDeep(line.model));
}
});

if (allKeys.explicit.length === 0) {
const autoDetectedKey = guessKey(allChords);
if (autoDetectedKey) {
allKeys.auto = autoDetectedKey;
}
}

return allKeys;
}
29 changes: 29 additions & 0 deletions packages/chord-mark/src/parser/helper/keyHelpers.js
Expand Up @@ -226,3 +226,32 @@ function chord2Key(chord) {

return keyString;
}

/**
* Return the number of semitones between two keys notes
* @param {string} key1
* @param {string} key2
* @returns {Number}
*/
export function getSemitonesBetweenKeys(key1, key2) {
if (!key1 || !key2) return 0;

return getSemitonesBetweenNotes(
key1.replace('m', ''),
key2.replace('m', '')
);
}

function getSemitonesBetweenNotes(note1, note2) {
const noteSharp1 = flatsToSharps[note1] || note1;
const noteSharp2 = flatsToSharps[note2] || note2;

const indexNote1 = allNotesSharp.indexOf(noteSharp1);
const indexNote2 = allNotesSharp.indexOf(noteSharp2);

if (indexNote1 === -1 || indexNote2 === -1) return 0;

const semitones = indexNote2 - indexNote1;

return semitones < 0 ? semitones + 12 : semitones;
}
64 changes: 8 additions & 56 deletions packages/chord-mark/src/renderer/components/renderSong.js
Expand Up @@ -13,11 +13,8 @@ import renderSectionLabelLine from './renderSectionLabel';
import renderTimeSignature from './renderTimeSignature';

import songTpl from './tpl/song.js';
import getChordSymbol from '../helpers/getChordSymbol';
import renderAllSectionsLabels from '../helpers/renderAllSectionLabels';
import { transposeKey } from '../../parser/helper/keyHelpers';

import { chordRendererFactory } from 'chord-symbol';
import renderAllChords from '../helpers/renderAllChords';

import lineTypes from '../../parser/lineTypes';
import replaceRepeatedBars from '../replaceRepeatedBars';
Expand Down Expand Up @@ -78,19 +75,13 @@ export default function renderSong(
let contextTimeSignature = defaultTimeSignature.string;
let previousBarTimeSignature;

let currentKey;

if (allKeys.auto) {
currentKey = transposeKey(
allKeys.auto,
transposeValue,
accidentalsType
);
}
let renderChord = getChordSymbolRenderer();

allLines = allLines
.map(renderChords)
allLines = renderAllChords(allLines, allKeys.auto, {
transposeValue,
accidentalsType,
chordSymbolRenderer,
simplifyChords,
useShortNamings,
})
.map(addPrintChordsDurationsFlag)
.map(addPrintBarTimeSignatureFlag)
.filter(shouldRenderLine)
Expand Down Expand Up @@ -120,45 +111,6 @@ export default function renderSong(
return songTpl({ song: allRenderedLines.join('') });
}

function renderChords(line) {
if (line.type === lineTypes.KEY_DECLARATION) {
currentKey = transposeKey(
line.model,
transposeValue,
accidentalsType
);

renderChord = getChordSymbolRenderer();
line.symbol = currentKey.string;
} else if (line.type === lineTypes.CHORD) {
line.model.allBars.forEach((bar) => {
bar.allChords.forEach((chord) => {
chord.symbol = getChordSymbol(chord.model, renderChord);
});
});
}
return line;
}

function getChordSymbolRenderer() {
if (typeof chordSymbolRenderer === 'function') {
return chordSymbolRenderer;
}
const accidental =
accidentalsType === 'auto'
? currentKey
? currentKey.accidental
: 'sharp'
: accidentalsType;

return chordRendererFactory({
simplify: simplifyChords,
useShortNamings,
transposeValue,
accidental,
});
}

function getSectionWrapperClasses(line) {
return [
'cmSection',
Expand Down
90 changes: 90 additions & 0 deletions packages/chord-mark/src/renderer/helpers/renderAllChords.js
@@ -0,0 +1,90 @@
import { chordRendererFactory } from 'chord-symbol';
import getChordSymbol from '../helpers/getChordSymbol';

import lineTypes from '../../parser/lineTypes';
import {
transposeKey,
getSemitonesBetweenKeys,
} from '../../parser/helper/keyHelpers';

// eslint-disable-next-line max-lines-per-function
export default function renderAllChords(
allLines,
detectedKey,
{
transposeValue,
accidentalsType,
chordSymbolRenderer,
simplifyChords,
useShortNamings,
}
) {
let currentKey;
let baseKey;

if (detectedKey) {
currentKey = transposeKey(detectedKey, transposeValue, accidentalsType);
}

let renderChord = getChordSymbolRenderer();

function renderChords(line) {
if (line.type === lineTypes.KEY_DECLARATION) {
currentKey = transposeKey(
line.model,
transposeValue,
accidentalsType
);
line.symbol = currentKey.string;

if (!baseKey) {
baseKey = currentKey;
}
} else if (line.type === lineTypes.CHORD) {
let transposeOffSet = 0;
if (shouldTransposeRepeatedChords(line)) {
transposeOffSet = getSemitonesBetweenKeys(
baseKey && baseKey.string,
currentKey && currentKey.string
);
}
renderChord = getChordSymbolRenderer(transposeOffSet);

line.model.allBars.forEach((bar) => {
bar.allChords.forEach((chord) => {
chord.symbol = getChordSymbol(chord.model, renderChord);
});
});
}
return line;
}

function shouldTransposeRepeatedChords(line) {
return (
line.isFromAutoRepeatChords ||
line.isFromSectionCopy ||
line.isFromChordLineRepeater
);
}

function getChordSymbolRenderer(transposeOffSet) {
if (typeof chordSymbolRenderer === 'function') {
return chordSymbolRenderer;
}
const accidental =
accidentalsType === 'auto'
? currentKey
? currentKey.accidental
: 'sharp'
: accidentalsType;

return chordRendererFactory({
simplify: simplifyChords,
useShortNamings,
transposeValue: transposeValue + transposeOffSet,
accidental,
});
}

return allLines.map(renderChords);
}
Expand Up @@ -321,10 +321,7 @@ key Am`;
},
],
allKeys: {
auto: {
string: 'C',
accidental: 'flat',
},
auto: undefined,
explicit: [
{
string: 'G',
Expand Down
@@ -0,0 +1,33 @@
key C

#v
C % F G
my 1st verse

#c
Dm G7 C %
my 1st chorus

key D

#v
my 2nd verse

key E

#v
my 3rd verse

key Bb

#v

#o
key G

#c
my second chorus

#b
%
%%
@@ -0,0 +1,38 @@
key: C

Verse 1
|C |% |F |G |
my 1st verse

Chorus 1
|Dm |G7 |C |% |
my 1st chorus

key: D

Verse 2
|D |% |G |A |
my 2nd verse

key: E

Verse 3
|E |% |A |B |
my 3rd verse

key: Bb

Verse 4
|Bb |% |Eb |F |
my 1st verse

Outro
key: G

Chorus 2
|Am |D7 |G |% |
my second chorus

Bridge
|Am |D7 |G |% |
|G |% |C |D |
Expand Up @@ -124,6 +124,11 @@ describe.each([
'song4-output-no-inline-time-signatures.txt',
{ printInlineTimeSignatures: false },
],
[
'chord transposition based on key declaration',
'song5-input.txt',
'song5-output-key-transposition.txt',
],
])('Render components: %s', (title, inputFile, outputFile, options) => {
test('produces expected rendering', () => {
const input = removeLastLine(
Expand Down
Expand Up @@ -46,10 +46,7 @@ describe('getAllKeysInSong()', () => {
const allKeys = getAllKeysInSong(allLines, allChords);

const expectedAllKeys = {
auto: {
string: 'C',
accidental: 'flat',
},
auto: undefined, // detection is disabled as soon as an explicit key is declared
explicit: [
{
string: 'C',
Expand Down
31 changes: 31 additions & 0 deletions packages/chord-mark/tests/unit/parser/helpers/keyHelpers.spec.js
Expand Up @@ -2,6 +2,7 @@ import {
getKeyAccidental,
transposeKey,
guessKey,
getSemitonesBetweenKeys,
} from '../../../../src/parser/helper/keyHelpers';
import parseSong from '../../../../src/parser/parseSong';

Expand All @@ -10,6 +11,7 @@ describe('keyHelpers', () => {
expect(typeof getKeyAccidental).toBe('function');
expect(typeof transposeKey).toBe('function');
expect(typeof guessKey).toBe('function');
expect(typeof getSemitonesBetweenKeys).toBe('function');
});
});

Expand Down Expand Up @@ -220,3 +222,32 @@ describe.each([
expect(guessKey(parsed.allChords)).toBeUndefined();
});
});

describe('getSemitonesBetweenKeys', () => {
describe.each([
[undefined, undefined, 0],
['C', undefined, 0],
[undefined, 'D', 0],

['C', 'C', 0],
['C', 'C#', 1],
['C', 'Db', 1],
['C', 'G#', 8],
['C', 'Ab', 8],
['C', 'A', 9],
['C', 'B', 11],

['C', 'Cm', 0],
['Cm', 'C', 0],
['C', 'Dbm', 1],
['C', 'G#m', 8],

['C', 'B#', 0],
['B#', 'A', 0],
['B#', 'E#', 0],
])('getSemitonesBetweenKeys(%s, %s) => %s', (key1, key2, semitones) => {
test('correctly returns semitones between keys', () => {
expect(getSemitonesBetweenKeys(key1, key2)).toBe(semitones);
});
});
});

0 comments on commit 3c741da

Please sign in to comment.