From 0a50d2559e7dc09f3c05c3786ade61d6636bb1d1 Mon Sep 17 00:00:00 2001 From: Will Hedgecock Date: Fri, 11 Aug 2023 15:33:20 -0500 Subject: [PATCH] Add framework to deal with audio analyses --- .eslintrc.json | 6 + Makefile | 2 +- README.md | 1 + demos/analysis/analysisdemo.css | 5 + demos/analysis/analysisdemo.js | 163 ++++++++++++++++++ demos/analysis/index.html | 76 ++++++++ demos/index.html | 1 + library/webaudioapi/analyses/AnalysisBase.mjs | 19 ++ .../webaudioapi/analyses/PowerSpectrum.mjs | 32 ++++ library/webaudioapi/analyses/TotalPower.mjs | 31 ++++ library/webaudioapi/modules/Analysis.mjs | 27 +++ library/webaudioapi/modules/Constants.mjs | 8 + library/webaudioapi/modules/Encoder.mjs | 6 - library/webaudioapi/webAudioAPI.src.js | 60 ++++++- package-lock.json | 154 ++++++++--------- package.json | 2 +- 16 files changed, 502 insertions(+), 91 deletions(-) create mode 100644 demos/analysis/analysisdemo.css create mode 100644 demos/analysis/analysisdemo.js create mode 100644 demos/analysis/index.html create mode 100644 library/webaudioapi/analyses/AnalysisBase.mjs create mode 100644 library/webaudioapi/analyses/PowerSpectrum.mjs create mode 100644 library/webaudioapi/analyses/TotalPower.mjs create mode 100644 library/webaudioapi/modules/Analysis.mjs diff --git a/.eslintrc.json b/.eslintrc.json index 89622cc..c1ce24c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -37,6 +37,12 @@ "no-inner-declarations": "off" } }, + { + "files": ["AnalysisBase.mjs"], + "rules": { + "no-unused-vars": "off" + } + }, { "files": ["EffectBase.mjs"], "rules": { diff --git a/Makefile b/Makefile index ad9db3c..bf9d3b8 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ DEMO_DIR := $(BUILD_DIR)/demos DOCS_DIR := $(BUILD_DIR)/docs LIB_DIR := $(BUILD_DIR)/lib -DEMO_TARGETS := netsblox piano score external effects +DEMO_TARGETS := netsblox piano score external effects analysis TOOL_TARGETS := instrumentcreator .PHONY : all clean lib assets docs demos $(DEMO_TARGETS) $(TOOL_TARGETS) run diff --git a/README.md b/README.md index dfe1281..32938e0 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Javascript library to generate music using the Web Audio API - NetsBlox emulation demo: https://hedgecrw.github.io/WebAudioAPI/netsblox - Live external device demo: https://hedgecrw.github.io/WebAudioAPI/external - Effects manipulation demo: https://hedgecrw.github.io/WebAudioAPI/effects + - Live analysis demo: https://hedgecrw.github.io/WebAudioAPI/analysis ## Tools - New instrument creator: https://hedgecrw.github.io/WebAudioAPI/instrumentcreator diff --git a/demos/analysis/analysisdemo.css b/demos/analysis/analysisdemo.css new file mode 100644 index 0000000..3879786 --- /dev/null +++ b/demos/analysis/analysisdemo.css @@ -0,0 +1,5 @@ +.choice { + display: flex; + justify-content: center; + margin: 12px auto 12px auto; +} diff --git a/demos/analysis/analysisdemo.js b/demos/analysis/analysisdemo.js new file mode 100644 index 0000000..f432ed8 --- /dev/null +++ b/demos/analysis/analysisdemo.js @@ -0,0 +1,163 @@ +function changeMidiDevice() { + const deviceSelector = document.getElementById('device'); + const deviceSelection = deviceSelector.options[deviceSelector.selectedIndex].value; + if (deviceSelector.selectedIndex > 0) + window.audioAPI.connectMidiDeviceToTrack('defaultTrack', deviceSelection).then(() => { + document.getElementById("status").textContent = 'Connected'; + document.getElementById("status").style.color = 'black'; + console.log('Connected to MIDI device!'); + }); +} + +function changeAudioInputDevice() { + const deviceSelector = document.getElementById('input'); + const deviceSelection = deviceSelector.options[deviceSelector.selectedIndex].value; + if (deviceSelector.selectedIndex > 0) + window.audioAPI.connectAudioInputDeviceToTrack('defaultTrack', deviceSelection).then(() => { + document.getElementById("status").textContent = 'Connected'; + document.getElementById("status").style.color = 'black'; + console.log('Connected to audio input device!'); + }); + else + window.audioAPI.disconnectAudioInputDeviceFromTrack('defaultTrack'); +} + +function changeAudioOutputDevice() { + const deviceSelector = document.getElementById('output'); + const deviceSelection = deviceSelector.options[deviceSelector.selectedIndex].value; + window.audioAPI.selectAudioOutputDevice(deviceSelection).then(() => { + document.getElementById("status").textContent = 'Connected'; + document.getElementById("status").style.color = 'black'; + console.log('Connected to audio output device!'); + }); +} + +function changeInstrument() { + const instrumentSelector = document.getElementById('instrument'); + const instrumentSelection = instrumentSelector.options[instrumentSelector.selectedIndex].value; + if (instrumentSelector.selectedIndex > 0) { + window.audioAPI.start(); + document.getElementById("status").textContent = 'Loading...'; + document.getElementById("status").style.color = 'black'; + window.audioAPI.updateInstrument('defaultTrack', instrumentSelection).then(() => { + document.getElementById("status").textContent = 'Ready'; + document.getElementById("status").style.color = 'black'; + console.log('Instrument loading complete!'); + }); + } +} + +function startRecordMidi() { + document.getElementById('startMidiButton').setAttribute('disabled', 'disabled'); + document.getElementById('stopMidiButton').removeAttribute('disabled'); + document.getElementById('playMidiButton').setAttribute('disabled', 'disabled'); + document.getElementById('exportMidiButton').setAttribute('disabled', 'disabled'); + try { + if (document.getElementById('midiDuration').value > 0) { + window.midiClip = window.audioAPI.recordMidiClip('defaultTrack', window.audioAPI.getCurrentTime() + Number(document.getElementById('midiStartOffset').value), document.getElementById('midiDuration').value); + window.midiClip.notifyWhenComplete(stopRecordMidi); + } + else + window.midiClip = window.audioAPI.recordMidiClip('defaultTrack', window.audioAPI.getCurrentTime() + Number(document.getElementById('midiStartOffset').value)); + } + catch (err) { + document.getElementById('startMidiButton').removeAttribute('disabled'); + document.getElementById('stopMidiButton').setAttribute('disabled', 'disabled'); + document.getElementById('playMidiButton').setAttribute('disabled', 'disabled'); + document.getElementById('exportMidiButton').setAttribute('disabled', 'disabled'); + document.getElementById("status").textContent = err.message; + document.getElementById("status").style.color = 'red'; + } +} + +function stopRecordMidi() { + document.getElementById('startMidiButton').removeAttribute('disabled'); + document.getElementById('stopMidiButton').setAttribute('disabled', 'disabled'); + document.getElementById('playMidiButton').removeAttribute('disabled'); + document.getElementById('exportMidiButton').removeAttribute('disabled'); + if (window.midiClip) + window.midiClip.finalize(); +} + +function playMidi() { + if (window.midiClip) + window.audioAPI.playClip('defaultTrack', window.midiClip, window.audioAPI.getCurrentTime()) +} + +async function exportMidi() { + const encodingTypes = window.audioAPI.getAvailableEncoders(); + if (window.midiClip) { + const encodedBlob = await window.midiClip.getEncodedData(encodingTypes.WAV); + const link = document.createElement('a'); + link.download = 'RecordedMidiClip.wav'; + link.href = URL.createObjectURL(encodedBlob); + link.click(); + URL.revokeObjectURL(link.href); + } +} + +function startRecordAudio() { + document.getElementById('startAudioButton').setAttribute('disabled', 'disabled'); + document.getElementById('stopAudioButton').removeAttribute('disabled'); + document.getElementById('playAudioButton').setAttribute('disabled', 'disabled'); + document.getElementById('exportAudioButton').setAttribute('disabled', 'disabled'); + try { + if (document.getElementById('audioDuration').value > 0) { + window.audioClip = window.audioAPI.recordAudioClip('defaultTrack', window.audioAPI.getCurrentTime() + Number(document.getElementById('audioStartOffset').value), document.getElementById('audioDuration').value); + window.audioClip.notifyWhenComplete(stopRecordAudio); + } + else + window.audioClip = window.audioAPI.recordAudioClip('defaultTrack', window.audioAPI.getCurrentTime() + Number(document.getElementById('audioStartOffset').value)); + } + catch (err) { + document.getElementById('startAudioButton').removeAttribute('disabled'); + document.getElementById('stopAudioButton').setAttribute('disabled', 'disabled'); + document.getElementById('playAudioButton').setAttribute('disabled', 'disabled'); + document.getElementById('exportAudioButton').setAttribute('disabled', 'disabled'); + document.getElementById("status").textContent = err.message; + document.getElementById("status").style.color = 'red'; + } +} + +function stopRecordAudio() { + document.getElementById('startAudioButton').removeAttribute('disabled'); + document.getElementById('stopAudioButton').setAttribute('disabled', 'disabled'); + document.getElementById('playAudioButton').removeAttribute('disabled'); + document.getElementById('exportAudioButton').removeAttribute('disabled'); + if (window.audioClip) + window.audioClip.finalize(); +} + +function playAudio() { + if (window.audioClip) + window.audioAPI.playClip('defaultTrack', window.audioClip, window.audioAPI.getCurrentTime()) +} + +async function exportAudio() { + const encodingTypes = window.audioAPI.getAvailableEncoders(); + if (window.audioClip) { + const encodedBlob = await window.audioClip.getEncodedData(encodingTypes.WAV); + const link = document.createElement('a'); + link.download = 'RecordedAudioClip.wav'; + link.href = URL.createObjectURL(encodedBlob); + link.click(); + URL.revokeObjectURL(link.href); + } +} + +window.onload = () => { + window.midiClip = window.audioClip = null; + window.audioAPI = new WebAudioAPI(); + window.audioAPI.createTrack('defaultTrack'); + const midiDeviceSelector = document.getElementById('device'); + midiDeviceSelector.add(new Option('Choose a MIDI device')); + const instrumentSelector = document.getElementById('instrument'); + instrumentSelector.add(new Option('Choose an instrument')); + const inputSelector = document.getElementById('input'); + inputSelector.add(new Option('Choose an audio input device')); + const outputSelector = document.getElementById('output'); + window.audioAPI.getAvailableInstruments('../instruments').then(instruments => instruments.forEach(instrument => instrumentSelector.add(new Option(instrument)))); + window.audioAPI.getAvailableMidiDevices().then(devices => devices.forEach(device => midiDeviceSelector.add(new Option(device)))); + window.audioAPI.getAvailableAudioInputDevices().then(devices => devices.forEach(device => inputSelector.add(new Option(device)))); + window.audioAPI.getAvailableAudioOutputDevices().then(devices => devices.forEach(device => outputSelector.add(new Option(device)))); +}; diff --git a/demos/analysis/index.html b/demos/analysis/index.html new file mode 100644 index 0000000..1f8a05a --- /dev/null +++ b/demos/analysis/index.html @@ -0,0 +1,76 @@ + + + + + + Live Audio Analysis Demo + + + + + + + +
+ Status: Not Ready +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +

 

+ +
+ + + + +       + + + + +
+ +
+ + + + +       + + + + +
+ + + + diff --git a/demos/index.html b/demos/index.html index 6ea8a4f..c584341 100644 --- a/demos/index.html +++ b/demos/index.html @@ -14,6 +14,7 @@

Demos:

  • NetsBlox emulation demo
  • Live external device demo
  • Effects manipulation demo
  • +
  • Live analysis demo
  • Tools: