Skip to content

Commit

Permalink
Add framework to deal with audio analyses
Browse files Browse the repository at this point in the history
  • Loading branch information
hedgecrw committed Aug 11, 2023
1 parent 2c44d5b commit 0a50d25
Show file tree
Hide file tree
Showing 16 changed files with 502 additions and 91 deletions.
6 changes: 6 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
"no-inner-declarations": "off"
}
},
{
"files": ["AnalysisBase.mjs"],
"rules": {
"no-unused-vars": "off"
}
},
{
"files": ["EffectBase.mjs"],
"rules": {
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions demos/analysis/analysisdemo.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.choice {
display: flex;
justify-content: center;
margin: 12px auto 12px auto;
}
163 changes: 163 additions & 0 deletions demos/analysis/analysisdemo.js
Original file line number Diff line number Diff line change
@@ -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))));
};
76 changes: 76 additions & 0 deletions demos/analysis/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">
<title>Live Audio Analysis Demo</title>
<link rel="stylesheet" href="analysisdemo.css">
<script type="module" src="js/webAudioAPI.js"></script>
<script src="js/analysisdemo.js"></script>
</head>

<body>

<div class="choice">
Status:&nbsp;<span id="status">Not Ready</span>
</div>

<div class="choice">
<label for="device">MIDI Device:&nbsp;</label>
<select name="device" id="device" onchange="changeMidiDevice()"></select>
</div>

<div class="choice">
<label for="instrument">Instrument:&nbsp;</label>
<select name="instrument" id="instrument" onchange="changeInstrument()"></select>
</div>

<div class="choice">
<label for="input">Audio Input Device:&nbsp;</label>
<select name="input" id="input" onchange="changeAudioInputDevice()"></select>
</div>

<div class="choice">
<label for="output">Audio Output Device:&nbsp;</label>
<select name="output" id="output" onchange="changeAudioOutputDevice()"></select>
</div>

<p>&nbsp;</p>

<div class="choice">
<label for="midiStartOffset">Record MIDI Clip:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Start Time Offset:&nbsp;</label>
<input type="number" name="midiStartOffset" id="midiStartOffset" min="0" max="100" value="0">
<label for="midiDuration">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Duration:&nbsp;</label>
<select name="midiDuration" id="midiDuration">
<option value="0">Until Stopped</option>
<option value="1">1 second</option>
<option value="2">2 seconds</option>
<option value="3">3 seconds</option>
</select>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<button type="button" id="startMidiButton" onclick="startRecordMidi()">Start Recording</button>
<button type="button" id="stopMidiButton" onclick="stopRecordMidi()" disabled="true">Stop Recording</button>
<button type="button" id="playMidiButton" onclick="playMidi()" disabled="true">Play Back</button>
<button type="button" id="exportMidiButton" onclick="exportMidi()" disabled="true">Export</button>
</div>

<div class="choice">
<label for="audioStartOffset">Record Audio Clip:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Start Time Offset:&nbsp;</label>
<input type="number" name="audioStartOffset" id="audioStartOffset" min="0" max="100" value="0">
<label for="audioDuration">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Duration:&nbsp;</label>
<select name="audioDuration" id="audioDuration">
<option value="0">Until Stopped</option>
<option value="1">1 second</option>
<option value="2">2 seconds</option>
<option value="3">3 seconds</option>
</select>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<button type="button" id="startAudioButton" onclick="startRecordAudio()">Start Recording</button>
<button type="button" id="stopAudioButton" onclick="stopRecordAudio()" disabled="true">Stop Recording</button>
<button type="button" id="playAudioButton" onclick="playAudio()" disabled="true">Play Back</button>
<button type="button" id="exportAudioButton" onclick="exportAudio()" disabled="true">Export</button>
</div>

</body>

</html>
1 change: 1 addition & 0 deletions demos/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ <h1>Demos:</h1>
<li><a href="/netsblox">NetsBlox emulation demo</a></li>
<li><a href="/external">Live external device demo</a></li>
<li><a href="/effects">Effects manipulation demo</a></li>
<li><a href="/analysis">Live analysis demo</a></li>
</ul>
<h1>Tools:</h1>
<ul>
Expand Down
19 changes: 19 additions & 0 deletions library/webaudioapi/analyses/AnalysisBase.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/** Class representing all base-level {@link WebAudioAPI} audio analysis functions */
export class AnalysisBase {

/**
* Called by a concrete analysis instance to initialize the inherited {@link AnalysisBase} data
* structure.
*/
constructor() { /* Empty constructor */ }

/**
* Performs a spectral analysis corresponding to an underlying concrete class on the passed-in
* buffer containing audio frequency content.
*
* @param {Uint8Array} frequencyContent - {@link https://developer.mozilla.org/en-US/docs/Web/API/Uint8Array Uint8Array} containing audio frequency data
* @returns {Object} Object containing the results of the specified acoustic analysis
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Uint8Array Uint8Array}
*/
static analyze(frequencyContent) { return undefined; }
}
32 changes: 32 additions & 0 deletions library/webaudioapi/analyses/PowerSpectrum.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { AnalysisBase } from './AnalysisBase.mjs';

/**
* Class representing an acoustic "power spectrum" analysis algorithm.
*
* A Power Spectrum is an array in which each bin contains the power attributed to a discrete
* range of frequencies within an audio signal.
*
* @extends AnalysisBase
*/
export class PowerSpectrum extends AnalysisBase {

/**
* Constructs a new {@link PowerSpectrum} analysis object.
*/
constructor() {
super();
}

/**
* Performs a power spectrum analysis on the passed-in buffer containing audio
* frequency content.
*
* @param {Uint8Array} frequencyContent - {@link https://developer.mozilla.org/en-US/docs/Web/API/Uint8Array Uint8Array} containing audio frequency data
* @returns {Float32Array} Array containing the power spectrum corresponding to the specified frequency data
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Float32Array Float32Array}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Uint8Array Uint8Array}
*/
static analyze(frequencyContent) {
return undefined;
}
}
31 changes: 31 additions & 0 deletions library/webaudioapi/analyses/TotalPower.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { AnalysisBase } from './AnalysisBase.mjs';

/**
* Class representing an acoustic "total power" analysis algorithm.
*
* Total power analysis determines the total cumulative spectral power present across all
* frequencies within an audio signal.
*
* @extends AnalysisBase
*/
export class TotalPower extends AnalysisBase {

/**
* Constructs a new {@link TotalPower} analysis object.
*/
constructor() {
super();
}

/**
* Performs a total power spectral analysis on the passed-in buffer containing audio
* frequency content.
*
* @param {Uint8Array} frequencyContent - {@link https://developer.mozilla.org/en-US/docs/Web/API/Uint8Array Uint8Array} containing audio frequency data
* @returns {number} Total power content across all frequencies in the specified frequency data
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Uint8Array Uint8Array}
*/
static analyze(frequencyContent) {
return undefined;
}
}
27 changes: 27 additions & 0 deletions library/webaudioapi/modules/Analysis.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Module containing functionality to create and utilize {@link WebAudioAPI} audio analyzers.
* @module Analysis
*/

import { AnalysisType } from './Constants.mjs';
import { PowerSpectrum } from '../analyses/PowerSpectrum.mjs';
import { TotalPower } from '../analyses/TotalPower.mjs';

const AnalysisClasses = {
[AnalysisType.PowerSpectrum]: PowerSpectrum,
[AnalysisType.TotalPower]: TotalPower
};

/**
* Returns a concrete analyzer implementation for the specified analysis type. The value passed
* to the `analysisType` parameter must be the **numeric value** associated with a certain
* {@link module:Constants.AnalysisType AnalysisType}, not a string-based key.
*
* @param {number} analysisType - Numeric value corresponding to the desired {@link module:Constants.AnalysisType AnalysisType}
* @returns {AnalysisBase} Concrete analyzer implementation for the specified {@link module:Constants.AnalysisType AnalysisType}
* @see {@link module:Constants.AnalysisType AnalysisType}
* @see {@link AnalysisBase}
*/
export function getAnalyzerFor(analysisType) {
return new AnalysisClasses[analysisType];
}
Loading

0 comments on commit 0a50d25

Please sign in to comment.