Skip to content

Commit

Permalink
Improve changing song key (#558)
Browse files Browse the repository at this point in the history
* Improve test for `Song#changeKey`

* Add test for `Song#setKey`

* Implement transposing for `Song`
  • Loading branch information
martijnversluis authored Jun 4, 2022
1 parent 12c7bf0 commit 2b5b5d8
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 20 deletions.
54 changes: 49 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,9 @@ If not, it returns [INDETERMINATE](#INDETERMINATE)</p>
* [.clone()](#Song+clone)[<code>Song</code>](#Song)
* [.setKey(key)](#Song+setKey)[<code>Song</code>](#Song)
* [.setCapo(capo)](#Song+setCapo)[<code>Song</code>](#Song)
* [.transpose(delta)](#Song+transpose)[<code>Song</code>](#Song)
* [.transposeUp()](#Song+transposeUp)[<code>Song</code>](#Song)
* [.transposeDown()](#Song+transposeDown)[<code>Song</code>](#Song)
* [.changeKey(newKey)](#Song+changeKey)[<code>Song</code>](#Song)
* [.changeMetadata(name, value)](#Song+changeMetadata)
* [.mapItems(func)](#Song+mapItems)[<code>Song</code>](#Song)
Expand Down Expand Up @@ -697,7 +700,7 @@ if you want to skip the &quot;header lines&quot;: the lines that only contain me
### song.setKey(key) ⇒ [<code>Song</code>](#Song)
<p>Returns a copy of the song with the key value set to the specified key. It changes:</p>
<ul>
<li>the value for <code>key</code> in the <code>metadata</code> set</li>
<li>the value for <code>key</code> in the [metadata](metadata) set</li>
<li>any existing <code>key</code> directive</li>
</ul>

Expand All @@ -706,14 +709,14 @@ if you want to skip the &quot;header lines&quot;: the lines that only contain me

| Param | Type | Description |
| --- | --- | --- |
| key | <code>number</code> \| <code>null</code> | <p>the key. Passing <code>null</code> will:</p> <ul> <li>remove the current key from <code>metadata</code></li> <li>remove any <code>key</code> directive</li> </ul> |
| key | <code>number</code> \| <code>null</code> | <p>the key. Passing <code>null</code> will:</p> <ul> <li>remove the current key from [metadata](metadata)</li> <li>remove any <code>key</code> directive</li> </ul> |

<a name="Song+setCapo"></a>

### song.setCapo(capo) ⇒ [<code>Song</code>](#Song)
<p>Returns a copy of the song with the key value set to the specified capo. It changes:</p>
<ul>
<li>the value for <code>capo</code> in the <code>metadata</code> set</li>
<li>the value for <code>capo</code> in the [metadata](metadata) set</li>
<li>any existing <code>capo</code> directive</li>
</ul>

Expand All @@ -722,14 +725,55 @@ if you want to skip the &quot;header lines&quot;: the lines that only contain me

| Param | Type | Description |
| --- | --- | --- |
| capo | <code>number</code> \| <code>null</code> | <p>the capo. Passing <code>null</code> will:</p> <ul> <li>remove the current key from <code>metadata</code></li> <li>remove any <code>capo</code> directive</li> </ul> |
| capo | <code>number</code> \| <code>null</code> | <p>the capo. Passing <code>null</code> will:</p> <ul> <li>remove the current key from [metadata](metadata)</li> <li>remove any <code>capo</code> directive</li> </ul> |

<a name="Song+transpose"></a>

### song.transpose(delta) ⇒ [<code>Song</code>](#Song)
<p>Transposes the song by the specified delta. It will:</p>
<ul>
<li>transpose all chords, see: [transpose](#Chord+transpose)</li>
<li>transpose the song key in [metadata](metadata)</li>
<li>update any existing <code>key</code> directive</li>
</ul>

**Kind**: instance method of [<code>Song</code>](#Song)
**Returns**: [<code>Song</code>](#Song) - <p>The transposed song</p>

| Param | Type | Description |
| --- | --- | --- |
| delta | <code>number</code> | <p>The number of semitones (positive or negative) to transpose with</p> |

<a name="Song+transposeUp"></a>

### song.transposeUp() ⇒ [<code>Song</code>](#Song)
<p>Transposes the song up by one semitone. It will:</p>
<ul>
<li>transpose all chords, see: [transpose](#Chord+transpose)</li>
<li>transpose the song key in [metadata](metadata)</li>
<li>update any existing <code>key</code> directive</li>
</ul>

**Kind**: instance method of [<code>Song</code>](#Song)
**Returns**: [<code>Song</code>](#Song) - <p>The transposed song</p>
<a name="Song+transposeDown"></a>

### song.transposeDown() ⇒ [<code>Song</code>](#Song)
<p>Transposes the song down by one semitone. It will:</p>
<ul>
<li>transpose all chords, see: [transpose](#Chord+transpose)</li>
<li>transpose the song key in [metadata](metadata)</li>
<li>update any existing <code>key</code> directive</li>
</ul>

**Kind**: instance method of [<code>Song</code>](#Song)
**Returns**: [<code>Song</code>](#Song) - <p>The transposed song</p>
<a name="Song+changeKey"></a>

### song.changeKey(newKey) ⇒ [<code>Song</code>](#Song)
<p>Returns a copy of the song with the key set to the specified key. It changes:</p>
<ul>
<li>the value for <code>key</code> in the <code>metadata</code> set</li>
<li>the value for <code>key</code> in the [metadata](metadata) set</li>
<li>any existing <code>key</code> directive</li>
<li>all chords, those are transposed according to the distance between the current and the new key</li>
</ul>
Expand Down
14 changes: 9 additions & 5 deletions src/chord_sheet/chord_lyrics_pair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class ChordLyricsPair {
return `ChordLyricsPair(chords=${this.chords}, lyrics=${this.lyrics})`;
}

set(properties) {
set(properties): ChordLyricsPair {
return new ChordLyricsPair(
properties.chords || this.chords,
properties.lyrics || this.lyrics,
Expand All @@ -61,13 +61,17 @@ class ChordLyricsPair {
return this.set({ lyrics });
}

transpose(delta: number, key: string | Key) {
transpose(delta: number, key: string | Key | null = null): ChordLyricsPair {
const chordObj = Chord.parse(this.chords);

if (chordObj) {
return this.set({
chords: chordObj.transpose(delta).normalize(key).toString(),
});
let transposedChord = chordObj.transpose(delta);

if (key) {
transposedChord = transposedChord.normalize(key);
}

return this.set({ chords: transposedChord.toString() });
}

return this.clone();
Expand Down
65 changes: 57 additions & 8 deletions src/chord_sheet/song.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,23 +267,23 @@ class Song extends MetadataAccessors {

/**
* Returns a copy of the song with the key value set to the specified key. It changes:
* - the value for `key` in the `metadata` set
* - the value for `key` in the {@link metadata} set
* - any existing `key` directive
* @param {number|null} key the key. Passing `null` will:
* - remove the current key from `metadata`
* - remove the current key from {@link metadata}
* - remove any `key` directive
* @returns {Song} The changed song
*/
setKey(key) {
return this.changeMetadata(KEY, key);
setKey(key): Song {
return this.changeMetadata(KEY, key ? key.toString() : key);
}

/**
* Returns a copy of the song with the key value set to the specified capo. It changes:
* - the value for `capo` in the `metadata` set
* - the value for `capo` in the {@link metadata} set
* - any existing `capo` directive
* @param {number|null} capo the capo. Passing `null` will:
* - remove the current key from `metadata`
* - remove the current key from {@link metadata}
* - remove any `capo` directive
* @returns {Song} The changed song
*/
Expand All @@ -303,9 +303,58 @@ class Song extends MetadataAccessors {
);
}

/**
* Transposes the song by the specified delta. It will:
* - transpose all chords, see: {@link Chord#transpose}
* - transpose the song key in {@link metadata}
* - update any existing `key` directive
* @param {number} delta The number of semitones (positive or negative) to transpose with
* @returns {Song} The transposed song
*/
transpose(delta: number): Song {
const wrappedKey = Key.wrap(this.key);
let transposedKey = null;
let song = (this as Song);

if (wrappedKey) {
transposedKey = wrappedKey.transpose(delta);
song = song.setKey(transposedKey);
}

return song.mapItems((item) => {
if (item instanceof ChordLyricsPair) {
return (item as ChordLyricsPair).transpose(delta, transposedKey);
}

return item;
});
}

/**
* Transposes the song up by one semitone. It will:
* - transpose all chords, see: {@link Chord#transpose}
* - transpose the song key in {@link metadata}
* - update any existing `key` directive
* @returns {Song} The transposed song
*/
transposeUp(): Song {
return this.transpose(1);
}

/**
* Transposes the song down by one semitone. It will:
* - transpose all chords, see: {@link Chord#transpose}
* - transpose the song key in {@link metadata}
* - update any existing `key` directive
* @returns {Song} The transposed song
*/
transposeDown(): Song {
return this.transpose(-1);
}

/**
* Returns a copy of the song with the key set to the specified key. It changes:
* - the value for `key` in the `metadata` set
* - the value for `key` in the {@link metadata} set
* - any existing `key` directive
* - all chords, those are transposed according to the distance between the current and the new key
* @param {string} newKey The new key.
Expand All @@ -326,7 +375,7 @@ class Song extends MetadataAccessors {
return item;
});

updatedSong.metadata.set('key', newKey);
this.setKey(newKey);
return updatedSong;
}

Expand Down
7 changes: 7 additions & 0 deletions test/chord_sheet/chord_lyrics_pair.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,12 @@ describe('ChordLyricsPair', () => {

expect(transposedPair.chords).toEqual('Gb');
});

it('can transpose without key', () => {
const chordLyricsPair = new ChordLyricsPair('F', 'Let it');
const transposedPair = chordLyricsPair.transpose(1);

expect(transposedPair.chords).toEqual('F#');
});
});
});
5 changes: 3 additions & 2 deletions test/integration/changing_key.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChordProFormatter, ChordProParser } from '../../src';

describe('changing the key of an existing song', () => {
it('updates the key directive', () => {
it('updates the key directive and transposes the chords', () => {
const chordpro = `
{key: C}
Let it [Am]be, let it [C/G]be, let it [F]be, let it [C]be`.substring(1);
Expand All @@ -13,6 +13,7 @@ Let it [Bm]be, let it [D/A]be, let it [G]be, let it [D]be`.substring(1);
const song = new ChordProParser().parse(chordpro);
const updatedSong = song.changeKey('D');

expect(updatedSong.key).toEqual('D');
expect(new ChordProFormatter().format(updatedSong)).toEqual(changedSheet);
});

Expand All @@ -34,7 +35,7 @@ Let it [Am]be, let it [C/G]be, let it [F]be, let it [C]be`.substring(1);
Let it [Bm]be, let it [D/A]be, let it [G]be, let it [D]be`.substring(1);

const song = new ChordProParser().parse(chordSheet);
const updatedSong = song.changeMetadata('key', 'C').changeKey('D');
const updatedSong = song.setKey('C').changeKey('D');

expect(new ChordProFormatter().format(updatedSong)).toEqual(changedSheet);
});
Expand Down
49 changes: 49 additions & 0 deletions test/integration/setting_key.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ChordProFormatter, ChordProParser } from '../../src';

describe('setting the key of an existing song', () => {
it('updates the key directive', () => {
const chordpro = `
{key: C}
Let it [Am]be, let it [C/G]be, let it [F]be, let it [C]be`.substring(1);

const changedSheet = `
{key: D}
Let it [Am]be, let it [C/G]be, let it [F]be, let it [C]be`.substring(1);

const song = new ChordProParser().parse(chordpro);
const updatedSong = song.setKey('D');

expect(updatedSong.key).toEqual('D');
expect(new ChordProFormatter().format(updatedSong)).toEqual(changedSheet);
});

it('adds the key directive', () => {
const chordpro = `
Let it [Am]be, let it [C/G]be, let it [F]be, let it [C]be`.substring(1);

const changedSheet = `
{key: D}
Let it [Am]be, let it [C/G]be, let it [F]be, let it [C]be`.substring(1);

const song = new ChordProParser().parse(chordpro);
const updatedSong = song.setKey('D');

expect(updatedSong.key).toEqual('D');
expect(new ChordProFormatter().format(updatedSong)).toEqual(changedSheet);
});

it('removes the key directive when passing null', () => {
const chordpro = `
{key: C}
Let it [Am]be, let it [C/G]be, let it [F]be, let it [C]be`.substring(1);

const changedSheet = `
Let it [Am]be, let it [C/G]be, let it [F]be, let it [C]be`.substring(1);

const song = new ChordProParser().parse(chordpro);
const updatedSong = song.setKey(null);

expect(updatedSong.key).toEqual(null);
expect(new ChordProFormatter().format(updatedSong)).toEqual(changedSheet);
});
});
51 changes: 51 additions & 0 deletions test/integration/transpose_song.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ChordProFormatter, ChordProParser } from '../../src';

describe('transposing a song', () => {
it('transposes with a delta', () => {
const chordpro = `
{key: C}
Let it [Am]be, let it [C/G]be, let it [F]be, let it [C]be`.substring(1);

const changedSheet = `
{key: D}
Let it [Bm]be, let it [D/A]be, let it [G]be, let it [D]be`.substring(1);

const song = new ChordProParser().parse(chordpro);
const updatedSong = song.transpose(2);

expect(updatedSong.key).toEqual('D');
expect(new ChordProFormatter().format(updatedSong)).toEqual(changedSheet);
});

it('transposes up', () => {
const chordpro = `
{key: C}
Let it [Am]be, let it [C/G]be, let it [F]be, let it [C]be`.substring(1);

const changedSheet = `
{key: C#}
Let it [A#m]be, let it [C#/G#]be, let it [F#]be, let it [C#]be`.substring(1);

const song = new ChordProParser().parse(chordpro);
const updatedSong = song.transposeUp();

expect(updatedSong.key).toEqual('C#');
expect(new ChordProFormatter().format(updatedSong)).toEqual(changedSheet);
});

it('transposes down', () => {
const chordpro = `
{key: D}
Let it [Bm]be, let it [D/A]be, let it [G]be, let it [D]be`.substring(1);

const changedSheet = `
{key: Db}
Let it [Bbm]be, let it [Db/Ab]be, let it [Gb]be, let it [Db]be`.substring(1);

const song = new ChordProParser().parse(chordpro);
const updatedSong = song.transposeDown();

expect(updatedSong.key).toEqual('Db');
expect(new ChordProFormatter().format(updatedSong)).toEqual(changedSheet);
});
});

0 comments on commit 2b5b5d8

Please sign in to comment.