Hiyve Components - v1.0.0
    Preparing search index...

    Module @hiyve/music-theory

    @hiyve/music-theory — framework-agnostic music theory drills: questions to order, as data and MusicXML, with seeded drills that can be made again.

    • generateNoteQuestion / generateIntervalQuestion / generateChordQuestion / generateKeySignatureQuestion / generateScaleQuestion — a question of each kind, with the answers to choose from
    • generateTheoryQuestion — a question of the kind a drill's settings name
    • startDrill / answerDrill / nextQuestion / finishDrill / drillSummary — a drill of questions, round by round, ended early when wanted
    • drillRecord / restoreDrill / isDrillRecord — a finished drill kept small, by its settings and seed, and brought back; a stored record that is not whole, or is of another version, is not
    • NoteSynth — a question's notes sounded, together or in order, on samples or a built-in voice
    • noteSnippetMusicXml — the one-bar score a question is drawn from
    • byEar / playAnswer — a question heard before it is seen (all but key signatures), and any question answered by playing on an instrument (question.play)
    import { generateTheoryQuestion, defaultTheoryDrillSettings, startDrill, answerDrill, nextQuestion, drillSummary } from '@hiyve/music-theory';

    const settings = { ...defaultTheoryDrillSettings, kind: 'interval' as const };
    let drill = startDrill((random) => generateTheoryQuestion(settings, random), { length: 10, seed: 42 });
    const question = drill.rounds[drill.index].question; // question.musicxml to draw, question.choices to offer
    drill = nextQuestion(answerDrill(drill, question.choices[0]));
    drillSummary(drill); // { total, answered, correct, accuracy, meanElapsedMs, missed }

    @hiyve/music-theory

    Framework-agnostic music theory drills: questions to order — a note, an interval, a chord, a key signature or a scale on a staff to name, or heard first and named by ear, with the answers to choose from — or to play on an instrument — as plain data and MusicXML, seeded drills that ask them round by round, and a small sound engine to hear them. Works in any framework; @hiyve/react-music-theory is the React layer with the staff, the answer buttons and the score card.

    npm install @hiyve/music-theory
    
    import { defaultTheoryDrillSettings, generateTheoryQuestion, startDrill, answerDrill, nextQuestion, drillSummary } from '@hiyve/music-theory';

    // Ten intervals on the treble staff, four answers to choose from each time.
    const settings = { ...defaultTheoryDrillSettings, kind: 'interval' as const };
    let drill = startDrill((random) => generateTheoryQuestion(settings, random), { length: 10, seed: 42 });

    const { question } = drill.rounds[drill.index];
    question.musicxml; // a one-bar score with the question on its staff — draw it with any MusicXML renderer
    question.choices; // ['M3', 'P5', 'm3', 'P4'] — shuffled, the right one among them
    question.notes; // the notes behind it, spelled: C4 and E4
    question.sounds; // what it sounds like, as MIDI numbers: [60, 64]

    drill = answerDrill(drill, 'M3', 1800); // judged against question.answer; the time is optional
    drill = nextQuestion(drill);

    drillSummary(drill); // { total, answered, correct, accuracy, meanElapsedMs, missed }

    The same seed makes the same drill again, so a drill can be kept, shared or retried by its seed.

    Every question has its kind, clef, answer, shuffled choices (the answer among them), the musicxml to draw, the notes it is made of, the sounds to play and whether they are heard together or inOrder (playback), whether it is asked by ear (byEar) and, if so, the reference note to name it from, and whether it is answered by playing (playAnswer) and, if so, what to play (play). Notes are always spelled by letter — a major third above E is G♯, never A♭ — and a question that would need a double sharp or flat is never asked.

    Kind Function Answer Notes
    note generateNoteQuestion(settings, random) The note's name: "F♯", "B♭", "C" The note
    interval generateIntervalQuestion(settings, random) m2 M2 m3 M3 P4 A4 d5 P5 m6 M6 m7 M7 P8 The two notes, together or one after the other
    chord generateChordQuestion(settings, random) major minor diminished augmented major7 dominant7 minor7 halfDiminished7 diminished7 The chord in root position or an inversion, with its root and inversion
    keySignature generateKeySignatureQuestion(settings, random) The key: D:major, F♯:minor The signature alone; it sounds as its tonic chord
    scale generateScaleQuestion(settings, random) major naturalMinor harmonicMinor melodicMinor dorian phrygian lydian mixolydian locrian majorPentatonic minorPentatonic The scale up to its octave, every accidental written, heard in order
    any generateTheoryQuestion(drillSettings, random) A question of the kind drillSettings.kind names, from that kind's settings

    random is the drill's source (createRandom(seed)); startDrill passes it. Each function throws when its settings leave nothing to ask — an interval too wide for the range, no mode chosen.

    With both major and minor keys asked, a key signature's relative key — the other key it could equally be — is never offered against the right answer.

    Notes, intervals, chords and scales can be asked by ear: set byEar: true in the kind's settings, and the question is meant to be heard before it is seen. Key signatures are always read — there is nothing in a signature to hear — so their settings have no byEar.

    const q = generateNoteQuestion({ byEar: true, accidentals: true }, random);
    q.byEar; // true
    q.reference; // the C at or below the note, to play first: name the note from it
    q.sounds; // then the note itself
    • A note comes with a reference: the C at or below it. Play it first, then the note, and the note is named from it. An interval, a chord or a scale is named from its own notes, so its reference is null.
    • No two answers offered sound alike, since the ear cannot tell them apart: never F♯ with G♭, or an augmented 4th with a diminished 5th. Each sound keeps one name among the choices.
    • A question asked by sight has byEar: false and no reference. Asking by ear changes nothing about drills asked by sight: the same seed asks the same questions as before.

    Any kind can be answered on an instrument instead of by name: set playAnswer: true in its settings, and the question says what to play in play (PlayTask).

    Kind Given (play.start) To play (play.notes)
    note Nothing: the note is on the staff The note, as written
    interval The lower note Both notes — one after the other, or together, as melodic says
    chord The lowest note: the root, or an inversion's bass The chord, in its inversion
    keySignature The tonic, under the signature The key's scale up from it: major, or natural minor
    scale The tonic The scale up to its octave

    play.playback says whether the notes are played together or inOrder; play.musicxml is the notes to play as a one-bar score — to follow the playing against and to show once answered — and play.promptMusicxml what the staff shows before: the note given, or the note to read. Asked by ear as well, nothing is given or shown (start and promptMusicxml are null): the question is heard and played back. Judge the playing however suits — @hiyve/react-music-performance/theory does it from a microphone or a MIDI instrument — and record it with answerDrill(drill, played, elapsedMs, correct), where correct is how it went.

    TheoryDrillSettings holds the settings of every kind and the kind asked, so a page can switch drills and find each as it was left (defaultTheoryDrillSettings).

    Kind Settings Defaults
    note (NoteIdSettings) clef, lowestMidi, highestMidi, accidentals, choiceCount, byEar, playAnswer treble staff (64–77), naturals, 4 choices
    interval (IntervalIdSettings) clef, lowestMidi, highestMidi, intervals, melodic, accidentals, choiceCount, byEar, playAnswer treble staff and ledger lines (57–84); M2, m3, M3, P4, P5, P8; together; from naturals
    chord (ChordIdSettings) clef, lowestMidi, highestMidi, qualities, inversions, accidentals, choiceCount, byEar, playAnswer 57–84; the four triads; root position; on naturals
    keySignature (KeySignatureIdSettings) clef, maxAccidentals (0–7), modes, choiceCount, playAnswer up to 4 sharps or flats; major keys
    scale (ScaleIdSettings) clef, lowestMidi, highestMidi, scales, accidentals, choiceCount, byEar, playAnswer 57–84; major and the three minors; from naturals

    Every kind is asked by sight unless byEar is set — a key signature always is — and answered by name unless playAnswer is set. The answers offered are drawn from the kind's own list — the intervals, qualities or scales chosen — so fewer are offered when fewer are chosen. accidentals lets a question start on a sharp or a flat.

    Function Description
    noteIdCandidates(settings) Every note a note drill can show
    intervalAbove(lower, interval) / intervalSizes The note an interval above another, spelled; each interval's letters and semitones
    chordTones(root, quality) / invertChord(tones, inversion) / chordShapes A chord's tones in root position, in an inversion; each quality's shape
    keySignatureTonic(fifths, mode, clef?) / keyCode(tonic, mode) The tonic of the key a signature names, on the clef's staff; the key as an answer
    scaleNotes(tonic, scale) / scaleShapes A scale's notes up to its octave, spelled; each scale's shape
    choicesWith(random, answer, pool, count, soundOf?) The answer and others from a pool, shuffled, no duplicates; with soundOf (each answer's sound), no two that sound alike
    noteSnippetMusicXml({ clef, pitches, arrangement?, fifths? }) The one-bar score a question is drawn from: whole notes stacked as a chord ('chord', the default) or in turn ('sequence'), after a key signature when given — an accidental written only where the signature does not give it; with no notes, the clef and signature alone
    Function Description
    startDrill(generate, { length?, seed? }) A drill of length questions (default 10; a whole number from 1 to MAX_DRILL_LENGTH, 200), every one drawn up front from seed (default 1)
    answerDrill(drill, choice, elapsedMs?, correct?) The drill with the current round answered and judged; a round answered once stays answered. correct takes an answer judged elsewhere — one played — as it was judged, with choice what was played
    nextQuestion(drill) The drill moved on once the round is answered; after the last round it is finished. An unanswered round is not skipped
    finishDrill(drill) The drill ended now: the rounds answered are kept and the rest are not asked, so its summary is of what was done
    isDrillFinished(drill) Whether every round has been asked, or the drill was finished early
    drillRecord(drill, settings, length?) / restoreDrill(record) A finished drill kept small, to store, and brought back as it was finished. A record (DrillRecord) holds its version, settings, length and seed, and each answer with the round it answered. Restoring draws the questions again from the seed and gives each answer back to its own round. A record read back from storage may be anything: restoreDrill gives null for one that is not whole, is of another DRILL_RECORD_VERSION (whose questions may now be drawn differently), or whose settings cannot draw its questions. isDrillRecord(value) checks one without restoring it
    drillSummary(drill) { total, answered, correct, accuracy, meanElapsedMs, missed }, so far or in the end
    checkAnswer(question, choice) Whether a choice is the question's right answer
    createRandom(seed) / newSeed() The seeded random source questions are drawn from, and a fresh seed for another drill
    import { NoteSynth } from '@hiyve/music-theory';

    // Make the context from a user gesture. Without `samples`, a built-in voice plays.
    const synth = new NoteSynth(new AudioContext(), {
    samples: { baseUrl: 'https://cdn.example.com/soundfonts/FluidR3_GM/' }, // a piano
    });
    synth.play(60); // middle C, for 900 ms
    synth.play([60, 64, 67]); // a chord
    synth.playInOrder([60, 64], 500); // a melodic interval, half a second apart

    // A question, as it is meant to be heard.
    question.playback === 'inOrder' ? synth.playInOrder(question.sounds) : synth.play(question.sounds);

    // Told as each note starts and stops sounding, by its place in the list — to light it on the staff.
    const stop = synth.playInOrder(question.sounds, 500, undefined, {
    onNoteStart: (index) => console.log('sounding', question.notes[index]),
    onNoteEnd: (index) => console.log('done', question.notes[index]),
    });
    Function Description
    new NoteSynth(ctx, { gain?, durationMs?, samples?, sampleWaitMs?, onNeedsGesture?, onError? }) Sounds notes through Web Audio. When the browser holds the sound back until the page is tapped, as iOS does with sound not started from a tap, nothing is played or announced and onNeedsGesture is told: play again from a tap. onError hears an audio context that refuses to start. Sound plays on recorded samples when samples names a host, otherwise with a small built-in voice that needs no download. play(midi | midi[], durationMs?, listener?) sounds a note or a chord at once, playInOrder(midis, stepMs?, durationMs?, listener?) one after another (500 ms apart by default); both return a function that stops them early. A NoteSynthListener (onNoteStart(index), onNoteEnd(index)) is told as each note starts and stops sounding, in step with what is heard; a note cut short by the stop function ends at once. preload(midis) downloads samples ahead of time; context is the context given
    preloadSamples(source, midis) Download samples ahead of time, with no audio context needed — each file is fetched once per page
    sampleUrl(source, midi) The file a key is played from, or null when the host has none for it
    midiHz(midi) / isSoundSupported() The frequency of a MIDI note (A4 = 440 Hz); whether this environment has Web Audio

    The SDK ships no samples: you choose the host, and that set's licence applies. samples (SampleSource) points at a host laid out like the midi-js-soundfonts collection, with one file per key:

    {baseUrl}{instrument}-{format}/{note}.{format}     e.g. …/FluidR3_GM/acoustic_grand_piano-mp3/Db4.mp3
    
    Field Default Description
    baseUrl required The soundfont's folder
    instrument 'acoustic_grand_piano' The instrument's folder name
    format 'mp3' mp3 or ogg
    lowestMidi / highestMidi 21 / 108 (A0–C8) The keys the host has files for

    Black keys are written as flats (Db4). A key outside the range, a file that cannot be fetched, or a sample still downloading after sampleWaitMs (1200 ms) is played with the built-in voice instead, so a note always sounds. The FluidR3_GM set, for one, is released under Creative Commons Attribution 3.0 and asks to be credited; the University of Iowa Electronic Music Studios piano recordings may be used for any project without restrictions, once trimmed, encoded and hosted in this layout yourself. The host must allow your page to fetch from it (CORS) and be allowed by your connect-src.

    Function Description
    pitch(step, alter, octave) / pitchMidi(step, alter, octave) A written pitch, and its MIDI number (middle C is 60)
    pitchName(pitch, { octave? }) "F♯", "B♭", "C" — with the octave when asked, "C4"
    spellMidi(midi, 'natural' | 'sharp' | 'flat') The written form of a MIDI number; null when a natural is asked for and the note has none
    spellAbove(from, letters, semitones) The note so many letters and semitones above another; null when it would need a double sharp or flat
    startingPitches(lowestMidi, highestMidi, accidentals) The notes a question may start from in a range
    noteNames(accidentals) / noteSteps Every name a note drill can ask for; the seven letters

    defaultTheoryDrillSettings and the settings of each kind, each with its merge function, which spreads the defaults first and the overrides after:

    Defaults Merge
    defaultNoteIdSettings mergeNoteIdSettings
    defaultIntervalIdSettings mergeIntervalIdSettings
    defaultChordIdSettings mergeChordIdSettings
    defaultKeySignatureIdSettings mergeKeySignatureIdSettings
    defaultScaleIdSettings mergeScaleIdSettings
    defaultDrillOptions mergeDrillOptions, which also keeps the length to a whole number from 1 to MAX_DRILL_LENGTH

    Also exported:

    • the lists intervalNames, chordQualities and scaleTypes
    • clefRanges: each clef's staff, and out to its second ledger line either side, as MIDI numbers

    At least two answers are always offered, whatever choiceCount asks for. melodicMinor is the ascending form of the melodic minor scale, with the raised sixth and seventh.

    • No runtime dependencies. ESM and CommonJS builds.
    • NoteSynth needs Web Audio (a browser); everything else runs anywhere.

    Classes

    NoteSynth

    Interfaces

    ChordIdSettings
    ChordQuestion
    DrillAnswer
    DrillOptions
    DrillRecord
    DrillRound
    DrillState
    DrillSummary
    IntervalIdSettings
    IntervalQuestion
    KeySignatureIdSettings
    KeySignatureQuestion
    NoteIdSettings
    NoteQuestion
    NoteSnippetOptions
    NoteSynthListener
    NoteSynthOptions
    Pitch
    PlayTask
    SampleSource
    ScaleIdSettings
    ScaleQuestion
    TheoryDrillSettings

    Type Aliases

    Alter
    ChordQuality
    Clef
    IntervalName
    KeyMode
    QuestionPlayback
    ScaleType
    Step
    TheoryKind
    TheoryQuestion

    Variables

    chordQualities
    chordShapes
    clefRanges
    defaultChordIdSettings
    defaultDrillOptions
    defaultIntervalIdSettings
    defaultKeySignatureIdSettings
    defaultNoteIdSettings
    defaultScaleIdSettings
    defaultTheoryDrillSettings
    DRILL_RECORD_VERSION
    intervalNames
    intervalSizes
    MAX_DRILL_LENGTH
    noteSteps
    scaleShapes
    scaleTypes

    Functions

    answerDrill
    checkAnswer
    choicesWith
    chordTones
    createRandom
    drillRecord
    drillSummary
    finishDrill
    generateChordQuestion
    generateIntervalQuestion
    generateKeySignatureQuestion
    generateNoteQuestion
    generateScaleQuestion
    generateTheoryQuestion
    intervalAbove
    invertChord
    isDrillFinished
    isDrillRecord
    isSoundSupported
    keyCode
    keySignatureTonic
    mergeChordIdSettings
    mergeDrillOptions
    mergeIntervalIdSettings
    mergeKeySignatureIdSettings
    mergeNoteIdSettings
    mergeScaleIdSettings
    midiHz
    newSeed
    nextQuestion
    noteIdCandidates
    noteNames
    noteSnippetMusicXml
    pitch
    pitchMidi
    pitchName
    preloadSamples
    restoreDrill
    sampleUrl
    scaleNotes
    spellAbove
    spellMidi
    startDrill
    startingPitches