Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Further chord picker functionality and story #27

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions packages/react-guitar-fretter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@
"lodash.flatten": "^4.4.0",
"lodash.max": "^4.0.1",
"lodash.min": "^4.0.1",
"lodash.range": "^3.2.0"
"lodash.range": "^3.2.0",
"lodash.reverse": "^4.0.1"
},
"devDependencies": {
"@types/lodash.flatmap": "^4.5.7",
"@types/lodash.flatten": "^4.4.7",
"@types/lodash.max": "^4.0.7",
"@types/lodash.min": "^4.0.7",
"@types/lodash.range": "^3.2.7"
"@types/lodash.flatmap": "^4.5.6",
"@types/lodash.flatten": "^4.4.6",
"@types/lodash.max": "^4.0.6",
"@types/lodash.min": "^4.0.6",
"@types/lodash.range": "^3.2.6",
"@types/lodash.reverse": "^4.0.7"
}
}
54 changes: 52 additions & 2 deletions packages/react-guitar-fretter/src/__test__/fretter.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,65 @@
import fretter from '../'
import fretter, { toSemitones } from '../'

const toSemitones = (text: string) => text.split('').map((c) => c === '1')
describe('toSemitones()', () => {
it(`maps correctly`, () =>
// prettier-ignore
expect(
toSemitones('01010101010')
).toEqual([false, true, false, true, false, true, false, true, false, true, false]))
})

describe('fretter()', () => {
it(`frets C major`, () =>
expect(
fretter({ root: 0, semitones: toSemitones('00010010000') })[0]
).toEqual([0, 1, 0, 2, 3, -1]))

it(`frets C major barre`, () =>
expect(
fretter(
{
root: 0,
semitones: toSemitones('00010010000'),
},
{ frettingType: 'barre' }
)[0]
).toEqual([3, 5, 5, 5, 3, -1]))

it(`frets C major open`, () =>
expect(
fretter(
{
root: 0,
semitones: toSemitones('00010010000'),
},
{ frettingType: 'open' }
)[0]
).toEqual([0, 1, 0, 2, 3, -1]))

it(`frets A minor`, () =>
expect(
fretter({ root: 9, semitones: toSemitones('00100010000') })[0]
).toEqual([0, 1, 2, 2, 0, -1]))

it(`frets A minor barre`, () =>
expect(
fretter(
{
root: 9,
semitones: toSemitones('00100010000'),
},
{ frettingType: 'barre' }
)[0]
).toEqual([5, 5, 5, 7, 7, 5]))

it(`frets A minor open`, () =>
expect(
fretter(
{
root: 9,
semitones: toSemitones('00100010000'),
},
{ frettingType: 'open' }
)[0]
).toEqual([0, 1, 2, 2, 0, -1]))
})
105 changes: 101 additions & 4 deletions packages/react-guitar-fretter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,51 @@ import flatten from 'lodash.flatten'
import flatMap from 'lodash.flatmap'
import max from 'lodash.max'
import min from 'lodash.min'
import reverse from 'lodash.reverse'
import mod from './util/mod'
import search from './util/search'
import findDuplicates from './util/find-duplicates'

/**
* Returns an array of string frettings.
*
* @param chord
* `root` is starting position in the chromatic scale, starting at 0 which is C for standard tuning.
*
* `semitones` is an array of booleans that represent the notes played in the chord on the chromatic scale.
*
* For example, to describe a c-major chord, true must be set for the 1st, 5th, and 8th note of the chromatic scale i.e:
* ```javascript
* const chord = {
root: 0,
semitones: [
true,
false,
false,
false,
true,
false,
false,
true,
false,
false,
false,
false,
],
}
* ```
* @param options
* Optional configuration of `tuning`, `frets`, and `frettingType`.
*
* `frettingType` accepts either `open` or `barre` as input
*/
export default function fretter(
chord: { root: number; semitones: boolean[] },
options?: { tuning?: number[]; frets?: number }
options?: {
tuning?: number[]
frets?: number
frettingType?: string
}
): number[][] {
const { root, semitones } = chord
const { tuning = [64, 59, 55, 50, 45, 40], frets = 22 } = options ?? {}
Expand Down Expand Up @@ -41,13 +80,34 @@ export default function fretter(
)
)

return search<number[]>(
const isOpenChord = (fretting: number[]) => {
return fretting.includes(0) && !fretting.some((fret) => fret > 4)
}

const isBarreChord = (fretting: number[]) => {
const strippedFretting = fretting.filter((fret) => fret > 0)
const duplicates = findDuplicates(strippedFretting)
return (
duplicates.length !== 0 &&
duplicates.some(
(duplicateFret) =>
duplicateFret === min(fretting.filter((fret) => fret >= 0))
)
)
}

const playableChords = (fretting: number[]) => {
const strippedFretting = fretting.filter((fret) => fret > 0)
const duplicates = findDuplicates(strippedFretting)
return strippedFretting.length < 5 || (duplicates && isBarreChord(fretting))
}

const frettings = search<number[]>(
[],
(fretting) =>
fretting.length === 0
? flatten(
range(tuning.length)
.map((string) => tuning.length - 1 - string)
reverse(range(tuning.length))
.map((string) => getFrets(string, [root]))
.map((frets, i) =>
frets.map((fret) =>
Expand All @@ -62,9 +122,46 @@ export default function fretter(
)
.map((fretting) => [...fretting].reverse())
.filter(containsAllNotes)
.filter(playableChords)
.sort(
(f1, f2) =>
(min(f1.filter((n) => n > 0)) ?? 0) -
(min(f2.filter((n) => n > 0)) ?? 0)
)

if (options?.frettingType?.toLowerCase() === 'open')
return frettings.filter(isOpenChord)
if (options?.frettingType?.toLowerCase() === 'barre')
return frettings.filter(isBarreChord)
return frettings
}

/**
* Converts a string of form '00010010000' to array of booleans where 0 -> false and 1 -> true.
*
* @param text
* String of the form '00010010000'
*/
export const toSemitones = (text: string) =>
text.split('').map((c) => c === '1')

/**
* Returns semitone boolean array for use in guitar fretter package.
*
* @param chordType
* 'major' | 'minor' | 'diminished triad'
*/
export const getChordSemitones = (
chordType: 'major' | 'minor' | 'diminished triad'
) => {
switch (chordType.toLowerCase()) {
case 'major':
return toSemitones('00010010000')
case 'minor':
return toSemitones('00100010000')
case 'diminished triad':
return toSemitones('00100100000')
default:
return toSemitones('0000000000')
}
}
13 changes: 13 additions & 0 deletions packages/react-guitar-fretter/src/util/find-duplicates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const findDuplicates = (arr: number[]) => {
let sorted_arr = arr.slice().sort()

let results = []
for (let i = 0; i < sorted_arr.length - 1; i++) {
if (sorted_arr[i + 1] == sorted_arr[i]) {
results.push(sorted_arr[i])
}
}
return results
}

export default findDuplicates
23 changes: 22 additions & 1 deletion site/components/ChordSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ function ChordSelectorModal(props: {
tuning: number[]
frets: number
lefty: boolean
frettingType: string
theme?: Theme
instrument?: StringInstrument
onChange: (chord: TChord, fretting: number[]) => void
Expand All @@ -73,6 +74,7 @@ function ChordSelectorModal(props: {
const initialChord = props.chord
const [root, setRoot] = useState(initialChord?.tonic || 'C')
const [notes, setNotes] = useState(() => getNotes(initialChord))
const [frettingType, setFrettingType] = useState(props.frettingType)
const pressed = 0
const types = useMemo(
() =>
Expand All @@ -87,8 +89,9 @@ function ChordSelectorModal(props: {
fretter(getFretterChord(root, notes), {
frets: props.frets,
tuning: props.tuning,
frettingType: frettingType,
}),
[props.tuning, props.frets, notes, root]
[props.tuning, props.frets, notes, root, frettingType]
)
const [frettingIndex, setFrettingIndex] = useState(0)
const fretting = frettings[frettingIndex] ?? props.tuning.map(() => 0)
Expand Down Expand Up @@ -157,6 +160,23 @@ function ChordSelectorModal(props: {
onChange={setRoot}
/>
</Label>
<Label
name={
<div>
Fretting
<div className="mt-1">
<strong>{frettingType}</strong>
</div>
</div>
}
lowercase
>
<Select
value={frettingType}
values={['All', 'Open', 'Barre']}
onChange={setFrettingType}
/>
</Label>
<div className="inline-flex overflow-auto">
{range(1, 12).map((i) => (
<Label
Expand Down Expand Up @@ -297,6 +317,7 @@ export default function ChordSelector(props: {
frets: number
lefty: boolean
theme?: Theme
frettingType: string
instrument?: StringInstrument
onChange: (strings: number[]) => void
onRequestOpenChange: (open: boolean) => void
Expand Down
1 change: 1 addition & 0 deletions site/components/Demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ function Demo() {
frets={isNaN(frets) ? 0 : frets}
lefty={lefty}
theme={theme}
frettingType={'all'}
instrument={instrument}
onRequestOpenChange={setChordSelectorOpen}
onChange={setStrings}
Expand Down
59 changes: 48 additions & 11 deletions storybook/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,28 @@ import range from 'lodash.range'
import { useState } from '@storybook/addons'
import coco from 'react-guitar-theme-coco'
import dark from 'react-guitar-theme-dark'
import fretter, { getChordSemitones } from 'react-guitar-fretter'

const themes = { spanish: spanishTheme, dark, coco }

const getNotes = () => {
return range(12)
.map((note) => note + 12)
.reduce(
(acc, note) => ({
...acc,
[midiToNoteName(note, { pitchClass: true, sharps: true })]: note,
}),
{} as {
[K: string]: number
}
)
}

storiesOf('Guitar', module)
.addDecorator(withKnobs)
.add('advanced', () => {
const notes = range(12)
.map((note) => note + 12)
.reduce(
(acc, note) => ({
...acc,
[midiToNoteName(note, { pitchClass: true, sharps: true })]: note,
}),
{} as {
[K: string]: number
}
)
const notes = getNotes()
const root = select('Root', notes, notes['C'])
const renderFingerFunctions = {
'Scientific Pitch Notation': getRenderFingerSpn(standard),
Expand Down Expand Up @@ -143,3 +148,35 @@ storiesOf('Guitar', module)
<Guitar strings={[0, 1, 2, 2, 0, -1]} />
</div>
))
.add('fretter', () => {
const notes = getNotes()
const chords = fretter(
{
root: select('Root', notes, notes['C']),
semitones: getChordSemitones(
select('Type', ['major', 'minor', 'diminished triad'], 'major')
),
},
{
restrictChordTypeTo: select(
'Fretting type',
['all', 'open', 'barre'],
'all'
),
}
)
return (
<div style={{ fontSize: '.5em' }}>
{chords.map((chord) => (
<Guitar
strings={chord}
renderFinger={getRenderFingerSpn(standard)}
center={true}
/>
))}
{chords.length === 0 && (
<div style={{ fontSize: '3em' }}>No chords found!</div>
)}
</div>
)
})
Loading