UNPKG

musicvis-lib

Version:

Music analysis and visualization library

377 lines (358 loc) 13.1 kB
import { randomInt, extent } from 'd3' import { randFloat } from '../utils/MathUtils.js' import { getMidiNoteByNr, getMidiNoteByLabel } from '../fileFormats/Midi.js' import GuitarNote from '../types/GuitarNote.js' import { arrayShallowEquals } from '../utils/ArrayUtils.js' /** * @module instruments/Guitar */ /** * Represents a tuning of a fretted string instrument. */ export class StringedTuning { /** * Represents a tuning of a fretted string instrument. * * @param {string} name name * @param {string[]} notes array of notes, e.g. ['E2', 'A2', 'D3', ...] */ constructor (name, notes) { this.name = name this.notes = notes this.short = notes.join(' ') this.pitches = notes.map(note => getMidiNoteByLabel(note).pitch) this.stringCount = notes.length } } /** * Maps from instrument to string number to list of tunings. * Defaults are at the top. * * @todo add more? https://en.wikipedia.org/wiki/List_of_guitar_tunings * @todo https://github.com/PirtleShell/string-tunings/blob/master/tunings.json * @todo replace arrays by maps? tuning name - tuning * @type {Map<string,Map<number,StringedTuning>>} * @example * stringedTunings.get('Guitar').get(6) for 6-string guitar tunings */ export const stringedTunings = new Map([ ['Guitar', new Map([ [6, [ new StringedTuning('E stand.', ['E2', 'A2', 'D3', 'G3', 'B3', 'E4']), new StringedTuning('Drop D', ['D2', 'A2', 'D3', 'G3', 'B3', 'E4']), new StringedTuning('Drop C', ['C2', 'G2', 'C3', 'F3', 'A3', 'D4']), new StringedTuning('1/2 down', ['D#2', 'G#2', 'C#3', 'F#3', 'A#3', 'D#4']), new StringedTuning('1 down', ['D2', 'G2', 'C3', 'F3', 'A3', 'D4']), new StringedTuning('1 1/2 down', ['C#2', 'F#2', 'B2', 'E3', 'G#3', 'C#4']), new StringedTuning('2 down', ['C2', 'F2', 'A#2', 'D#3', 'G3', 'C4']), new StringedTuning('DADGAG', ['D2', 'A2', 'D3', 'G3', 'A3', 'D4']) ]], [7, [ new StringedTuning('B stand.', ['B1', 'E2', 'A2', 'D3', 'G3', 'B3', 'E4']), new StringedTuning('Drop A', ['A1', 'E2', 'A2', 'D3', 'G3', 'B3', 'E4']), new StringedTuning('1/2 down', ['A#1', 'D#2', 'G#2', 'C#3', 'F#3', 'A#3', 'D#4']), new StringedTuning('1 down', ['A1', 'D2', 'G2', 'C3', 'F3', 'A3', 'D4']), new StringedTuning('1 1/2 down', ['G#1', 'C#2', 'F#2', 'B2', 'E3', 'G#3', 'C#4']), new StringedTuning('2 down', ['G1', 'C2', 'F2', 'A#2', 'D#3', 'G3', 'C4']) ]], [8, [ new StringedTuning('F# stand.', ['F#1', 'B1', 'E2', 'A2', 'D3', 'G3', 'B3', 'E4']), new StringedTuning('Drop E', ['E1', 'B1', 'E2', 'A2', 'D3', 'G3', 'B3', 'E4']), new StringedTuning('1/2 down', ['F1', 'A#1', 'D#2', 'G#2', 'C#3', 'F#3', 'A#3', 'D#4']), new StringedTuning('1 down', ['E1', 'A1', 'D2', 'G2', 'C3', 'F3', 'A3', 'D4']), new StringedTuning('1 1/2 down', ['D#1', 'G#1', 'C#2', 'F#2', 'B2', 'E3', 'G#3', 'C#4']), new StringedTuning('2 down', ['D1', 'G1', 'C2', 'F2', 'A#2', 'D#3', 'G3', 'C4']) ]] ])], ['Bass', new Map([ [4, [ new StringedTuning('E stand.', ['E1', 'A1', 'D2', 'G2']), new StringedTuning('Drop D', ['D1', 'A1', 'D2', 'G2']), new StringedTuning('1/2 down', ['D#1', 'G#1', 'C#2', 'F#2']), new StringedTuning('1 down', ['D1', 'G1', 'C2', 'F2']), new StringedTuning('1 1/2 down', ['C#1', 'F#1', 'B1', 'E2']), new StringedTuning('2 down', ['C1', 'F1', 'A#1', 'D#2']) ]], [5, [ new StringedTuning('B stand.', ['B0', 'E1', 'A1', 'D2', 'G2']), new StringedTuning('Drop A', ['A0', 'D1', 'A1', 'D2', 'G2']), new StringedTuning('1/2 down', ['A#0', 'D#1', 'G#1', 'C#2', 'F#2']), new StringedTuning('1 down', ['A0', 'D1', 'G1', 'C2', 'F2']), new StringedTuning('1 1/2 down', ['G#0', 'C#1', 'F#1', 'B1', 'E2']), new StringedTuning('2 down', ['G0', 'C1', 'F1', 'A#1', 'D#2']) ]], [6, [ new StringedTuning('F# stand.', ['F#0', 'B0', 'E1', 'A1', 'D2', 'G2']), new StringedTuning('Drop E', ['E0', 'A0', 'D1', 'A1', 'D2', 'G2']), new StringedTuning('1/2 down', ['F0', 'A#0', 'D#1', 'G#1', 'C#2', 'F#2']), new StringedTuning('1 down', ['E1', 'A0', 'D1', 'G1', 'C2', 'F2']), new StringedTuning('1 1/2 down', ['D#0', 'G#0', 'C#1', 'F#1', 'B1', 'E2']), new StringedTuning('2 down', ['D0', 'G0', 'C1', 'F1', 'A#1', 'D#2']) ]] ])], ['Ukulele', new Map([ [4, [ new StringedTuning('Hawaii', ['G4', 'C4', 'E4', 'A4']), new StringedTuning('Low G', ['G3', 'C4', 'E4', 'A4']), new StringedTuning('D-tuning', ['A4', 'D4', 'F#4', 'B4']), new StringedTuning('Canadian', ['A3', 'D4', 'F#4', 'B4']), new StringedTuning('Bariton', ['D3', 'G3', 'B3', 'E4']) ]] ])] ]) /** * For Notes that have a guitar string encoded in their channel, this function * allows to convert them to a GuitarNote. * * @param {Note} note a Note that has the guitar string stored in its channel * e.g. 0 to 5 for a six string * @param {StringedTuning} tuning tuning * @returns {GuitarNote} a GuitarNote */ export function guitarNoteFromNote (note, tuning) { const string = note.channel const reversedString = tuning.stringCount - string - 1 const fret = note.pitch - tuning.pitches[reversedString] return GuitarNote.fromNote(note, string, fret) } /** * Returns a tuning with the specified pitches or null if none found. * * @param {number[]} pitches pitches of the tuning, same order as in * Guitar.js' stringedTunings, i.e. low to high notes * @returns {StringedTuning|null} the found tuning or null */ export function getTuningFromPitches (pitches) { const stringCount = pitches.length for (const stringCountMap of stringedTunings.values()) { if (stringCountMap.has(stringCount)) { const tunings = stringCountMap.get(stringCount) for (const t of tunings) { if (arrayShallowEquals(t.pitches, pitches)) { return t } } } } return null } /** * Returns the pitch range of a tuning, given the number of frets. * * @param {StringedTuning} tuning tuning * @param {number} fretCount number of frets the instrument has (default: 24) * @returns {number[]} [minPitch, maxPitch] */ export function getTuningPitchRange (tuning, fretCount = 24) { // const openExtent = extent(tuning.pitches); const openMax = tuning.pitches[tuning.stringCount - 1] const openMin = tuning.pitches[0] return [openMin, openMax + fretCount] } /** * Colors for guitar strings, acces via stringColor[string] * where string in [1, 8]. * * @type {string[]} */ export const stringColors = [ '#888', '#d122e9', '#31eb1c', '#f37c14', '#10edfc', '#ffeb09', '#ff210d', 'silver', 'gold' ] /** * Returns the pitch of a note at a given fretboard position. * * @param {number} string string * @param {number} fret fret * @param {StringedTuning} tuning tuning * @returns {number} pitch */ export function getPitchFromFretboardPos (string, fret, tuning) { // Order is reversed, since tunings are named from bass to treble // but strings are numbered the other direction... const reversedString = tuning.stringCount - string + 1 const openPitch = tuning.pitches[reversedString - 1] return openPitch + fret } /** * Returns MIDI attributes of a note at a given fretboard position, e.g. C# * * @param {number} string string * @param {number} fret fret * @param {StringedTuning} tuning tuning * @returns {object} note info, e.g. { pitch: 69, name: 'A', octave: 4, label: 'A4', frequency: 440.000 } */ export function getNoteInfoFromFretboardPos (string, fret, tuning) { const pitch = getPitchFromFretboardPos(string, fret, tuning) return getMidiNoteByNr(pitch) } /** * Finds all fretboard positions with this exact pitch. * * @param {number} pitch MIDI pitch * @param {StringedTuning} tuning tuning * @param {number} fretCount number of frets the instrument has * @returns {object[]} positions */ export function getFretboardPositionsFromPitch (pitch, tuning, fretCount) { const positions = [] const stringCount = tuning.stringCount for (let string = 0; string < stringCount; string++) { const openPitch = tuning.pitches[string] // Rule out strings where pitch is lower than open string if (pitch < openPitch) { continue } // Rule out strings where pitch is higher than highest fret if (pitch > openPitch + fretCount) { continue } positions.push({ // Convert so string 1 is treble string: stringCount - string, fret: pitch - openPitch }) } return positions } /** * Finds all fretboard positions with this note in all octaves. * * @param {string} name note name, e.g. 'C#' * @param {StringedTuning} tuning tuning * @param {number} fretCount number of frets the instrument has * @returns {object[]} positions */ export function getFretboardPositionsFromNoteName (name, tuning, fretCount = 24) { const n = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] if (!n.includes(name)) { return null } const positions = [] const lowestPitch = n.indexOf(name) const stringCount = tuning.stringCount for (let string = 0; string < stringCount; string++) { // First fret where this note appears const openPitch = tuning.pitches[string] let fret = lowestPitch - (openPitch % 12) if (fret < 0) { fret += 12 } while (fret <= fretCount) { positions.push({ // Convert so string 1 is treble string: stringCount - string, fret }) // Go to next octave fret += 12 } } return positions } /** * Generates example MIDI-like data (preprocessed MIDI). * * @param {number} startTime start tick * @param {number} count number of notes to generate * @param {StringedTuning} tuning tuning * @returns {GuitarNote[]} notes */ export function generateExampleData (startTime = 0, count = 50, tuning) { let currentTime = startTime return Array.from({ length: count }) .map(() => { const start = currentTime + randFloat(0, 1) currentTime = start + randFloat(0, 1) const string = randomInt(1, 7)() const fret = randomInt(0, 25)() const pitch = getPitchFromFretboardPos(string, fret, tuning) const velocity = randomInt(15, 127)() return new GuitarNote( pitch, start, velocity, string, currentTime, string, fret ) }) } /** * Estimates the fretboard position from MIDI notes * * @todo does not work well yet * @param {Note[]} notes notes with only MIDI information * @param {StringedTuning} tuning tuning * @param {number} fretCount number of frets the instrument has * @returns {GuitarNote[]} GuitarNotes with fretboard positions */ export function fretboardPositionsFromMidi (notes, tuning, fretCount = 24) { if (!notes || notes.length === 0) { return [] } if (!tuning || !tuning.pitches) { console.warn('Invalid tuning parameter!') return [] } // Sort notes that cannot be played out in advance for better performance const [minPitch, maxPitch] = getTuningPitchRange(tuning, fretCount) const possibleNotes = [] const errorPitches = [] for (const note of notes) { if (note.pitch < minPitch || note.pitch > maxPitch) { errorPitches.push(note.pitch) } else { possibleNotes.push(note) } } const result = [] for (const note of possibleNotes) { const positions = getFretboardPositionsFromPitch(note.pitch, tuning, 24) // Choose best position // TODO: improve this to make it easier to play, take closest postion to prior one let bestPos = positions[0] for (const pos of positions) { if (pos.fret < bestPos.fret) { bestPos = pos } } const { string, fret } = bestPos result.push(GuitarNote.fromNote(note, string, fret)) } // Give advice on transposing for better results if (errorPitches.length > 0) { const [minDataPitch, maxDataPitch] = extent(notes, d => d.pitch) let advice = '' if (minDataPitch < minPitch) { advice += `Transpose by ${minPitch - minDataPitch} semitones` } if (maxPitch < maxDataPitch) { advice += `Transpose by ${maxPitch - maxDataPitch} semitones` } console.warn( `Cannot find a fretboard position for ${errorPitches.length} pitches, try another tuning instead:\n`, errorPitches, `\nCurrent tuning's pitch range is ${minPitch} - ${maxPitch}`, `\ndata pitch range is ${minDataPitch} - ${maxDataPitch}\n`, advice ) } return result } // /** // * @todo chords always? use 4-fret-blocks // * @param {GuitarNote[]} notes notes with fretboard positions // * @returns {?} fingering information // */ // export function fingeringFromFretboardPositions(notes) { // // TODO: detect chords first? // // TODO: then lookup chords' fingerings from a lookup table // // TODO: alternatively (as fallback) use heuristics // // TODO: or try to do it like humans do when playing // // TODO: consider prior and following notes/chords! // }