UNPKG

@chordbook/tuner

Version:

The tuner used by ChordBook.app

1 lines 5.56 kB
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { PitchDetector } from 'pitchy'\n\nconst semitone = 69\nconst noteStrings = ['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B']\n\nexport interface TunerConfig {\n a4?: number\n clarityThreshold?: number\n minVolumeDecibels?: number\n bufferSize?: number\n smoothingTimeConstant?: number\n minFrequency?: number\n maxFrequency?: number\n updateInterval?: number\n sampleRate?: number\n onNote?: (note: Note) => void\n}\n\nexport const TunerDefaults: TunerConfig = {\n a4: 440,\n clarityThreshold: 0.9,\n minVolumeDecibels: -1000,\n bufferSize: 8192,\n smoothingTimeConstant: 0.8,\n minFrequency: 27.5, // A0, Lowest note on a piano\n maxFrequency: 4186.01, // C8, Highest note on a piano\n updateInterval: 50,\n sampleRate: 44100, // Seems to work better than 48000 for some reason\n}\n\nexport interface Note {\n name: string\n value: number\n cents: number\n octave: number\n frequency: number\n clarity: number\n}\n\nexport function createTuner(config: TunerConfig = {}) {\n config = { ...TunerDefaults, ...config }\n\n const context = new AudioContext({ sampleRate: config.sampleRate })\n const highpass = new BiquadFilterNode(context, { type: \"highpass\", frequency: config.minFrequency })\n const lowpass = new BiquadFilterNode(context, { type: \"lowpass\", frequency: config.maxFrequency })\n const analyser = new AnalyserNode(context, { fftSize: config.bufferSize, smoothingTimeConstant: config.smoothingTimeConstant })\n lowpass.connect(highpass).connect(analyser)\n\n const detector = PitchDetector.forFloat32Array(analyser.fftSize)\n detector.minVolumeDecibels = config.minVolumeDecibels!;\n const inputBuffer = new Float32Array(detector.inputLength)\n\n let stream: MediaStream\n let source: MediaStreamAudioSourceNode\n let interval = 0;\n\n async function start () {\n stream = await navigator.mediaDevices.getUserMedia({ audio: true })\n source = context.createMediaStreamSource(stream)\n source.connect(lowpass)\n\n interval = setInterval(process, config.updateInterval!)\n }\n\n function process() {\n const { clarityThreshold, minFrequency, maxFrequency, onNote } = config\n analyser.getFloatTimeDomainData(inputBuffer);\n const [frequency, clarity] = detector.findPitch(inputBuffer, context.sampleRate)\n\n if (clarity > clarityThreshold! && frequency > minFrequency! && frequency < maxFrequency!) {\n onNote?.(getNote(frequency, clarity))\n }\n }\n\n function getNote (frequency: number, clarity?: number) {\n const value = Math.round(12 * (Math.log(frequency / config.a4!) / Math.log(2))) + semitone\n const standardFrequency = config.a4! * Math.pow(2, (value - semitone) / 12)\n const cents = Math.floor((1200 * Math.log(frequency / standardFrequency)) / Math.log(2))\n const name = noteStrings[value % 12]\n const octave: number = Math.floor(value / 12) - 1\n return { frequency, name, value, cents, octave, clarity } as Note\n }\n\n async function stop() {\n clearInterval(interval)\n stream.getTracks().forEach(track => track.stop())\n stream.removeTrack(stream.getAudioTracks()[0])\n }\n\n return { start, stop, context, analyser, detector, config, getNote }\n}\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,mBAAAE,EAAA,gBAAAC,IAAA,eAAAC,EAAAJ,GAAA,IAAAK,EAA8B,kBAExBC,EAAW,GACXC,EAAc,CAAC,IAAK,UAAM,IAAK,UAAM,IAAK,IAAK,UAAM,IAAK,UAAM,IAAK,UAAM,GAAG,EAevEL,EAA6B,CACxC,GAAI,IACJ,iBAAkB,GAClB,kBAAmB,KACnB,WAAY,KACZ,sBAAuB,GACvB,aAAc,KACd,aAAc,QACd,eAAgB,GAChB,WAAY,KACd,EAWO,SAASC,EAAYK,EAAsB,CAAC,EAAG,CACpDA,EAAS,CAAE,GAAGN,EAAe,GAAGM,CAAO,EAEvC,IAAMC,EAAU,IAAI,aAAa,CAAE,WAAYD,EAAO,UAAW,CAAC,EAC5DE,EAAW,IAAI,iBAAiBD,EAAS,CAAE,KAAM,WAAY,UAAWD,EAAO,YAAa,CAAC,EAC7FG,EAAU,IAAI,iBAAiBF,EAAS,CAAE,KAAM,UAAW,UAAWD,EAAO,YAAa,CAAC,EAC3FI,EAAW,IAAI,aAAaH,EAAS,CAAE,QAASD,EAAO,WAAY,sBAAuBA,EAAO,qBAAsB,CAAC,EAC9HG,EAAQ,QAAQD,CAAQ,EAAE,QAAQE,CAAQ,EAE1C,IAAMC,EAAW,gBAAc,gBAAgBD,EAAS,OAAO,EAC/DC,EAAS,kBAAoBL,EAAO,kBACpC,IAAMM,EAAc,IAAI,aAAaD,EAAS,WAAW,EAErDE,EACAC,EACAC,EAAW,EAEf,eAAeC,GAAS,CACtBH,EAAS,MAAM,UAAU,aAAa,aAAa,CAAE,MAAO,EAAK,CAAC,EAClEC,EAASP,EAAQ,wBAAwBM,CAAM,EAC/CC,EAAO,QAAQL,CAAO,EAEtBM,EAAW,YAAYE,EAASX,EAAO,cAAe,CACxD,CAEA,SAASW,GAAU,CACjB,GAAM,CAAE,iBAAAC,EAAkB,aAAAC,EAAc,aAAAC,EAAc,OAAAC,CAAO,EAAIf,EACjEI,EAAS,uBAAuBE,CAAW,EAC3C,GAAM,CAACU,EAAWC,CAAO,EAAIZ,EAAS,UAAUC,EAAaL,EAAQ,UAAU,EAE3EgB,EAAUL,GAAqBI,EAAYH,GAAiBG,EAAYF,GAC1EC,IAASG,EAAQF,EAAWC,CAAO,CAAC,CAExC,CAEA,SAASC,EAASF,EAAmBC,EAAkB,CACrD,IAAME,EAAQ,KAAK,MAAM,IAAM,KAAK,IAAIH,EAAYhB,EAAO,EAAG,EAAI,KAAK,IAAI,CAAC,EAAE,EAAIF,EAC5EsB,EAAoBpB,EAAO,GAAM,KAAK,IAAI,GAAImB,EAAQrB,GAAY,EAAE,EACpEuB,EAAQ,KAAK,MAAO,KAAO,KAAK,IAAIL,EAAYI,CAAiB,EAAK,KAAK,IAAI,CAAC,CAAC,EACjFE,EAAOvB,EAAYoB,EAAQ,EAAE,EAC7BI,EAAiB,KAAK,MAAMJ,EAAQ,EAAE,EAAI,EAChD,MAAO,CAAE,UAAAH,EAAW,KAAAM,EAAM,MAAAH,EAAO,MAAAE,EAAO,OAAAE,EAAQ,QAAAN,CAAQ,CAC1D,CAEA,eAAeO,GAAO,CACpB,cAAcf,CAAQ,EACtBF,EAAO,UAAU,EAAE,QAAQkB,GAASA,EAAM,KAAK,CAAC,EAChDlB,EAAO,YAAYA,EAAO,eAAe,EAAE,CAAC,CAAC,CAC/C,CAEA,MAAO,CAAE,MAAAG,EAAO,KAAAc,EAAM,QAAAvB,EAAS,SAAAG,EAAU,SAAAC,EAAU,OAAAL,EAAQ,QAAAkB,CAAQ,CACrE","names":["src_exports","__export","TunerDefaults","createTuner","__toCommonJS","import_pitchy","semitone","noteStrings","config","context","highpass","lowpass","analyser","detector","inputBuffer","stream","source","interval","start","process","clarityThreshold","minFrequency","maxFrequency","onNote","frequency","clarity","getNote","value","standardFrequency","cents","name","octave","stop","track"]}