Skip to content

Commit 374ba4c

Browse files
authored
Merge pull request #145 from ml5js/removingNew
Removing class initialization for all methods
2 parents 2546bbb + af1ada0 commit 374ba4c

File tree

10 files changed

+197
-190
lines changed

10 files changed

+197
-190
lines changed

src/FeatureExtractor/index.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ General Feature Extractor Manager
1010
import Mobilenet from './Mobilenet';
1111

1212
/* eslint max-len: ["error", { "code": 180 }] */
13-
const FeatureExtractor = (model, optionsOrCallback, cb = () => {}) => {
13+
const featureExtractor = (model, optionsOrCallback, cb = () => {}) => {
1414
let modelName;
1515
if (typeof model !== 'string') {
16-
console.error('Please specify a model to use. E.g: "Mobilenet"');
16+
throw new Error('Please specify a model to use. E.g: "MobileNet"');
1717
} else {
1818
modelName = model.toLowerCase();
1919
}
@@ -30,8 +30,7 @@ const FeatureExtractor = (model, optionsOrCallback, cb = () => {}) => {
3030
if (modelName === 'mobilenet') {
3131
return new Mobilenet(options, callback);
3232
}
33-
console.error(`${modelName} is not a valid model.`);
34-
return null;
33+
throw new Error(`${modelName} is not a valid model.`);
3534
};
3635

37-
export default FeatureExtractor;
36+
export default featureExtractor;

src/ImageClassifier/index.js

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,44 +19,21 @@ const DEFAULTS = {
1919
};
2020

2121
class ImageClassifier {
22-
constructor(modelName, videoOrOptionsOrCallback, optionsOrCallback, cb = null) {
23-
let options = {};
24-
let callback = cb;
25-
26-
if (videoOrOptionsOrCallback instanceof HTMLVideoElement) {
27-
this.video = videoOrOptionsOrCallback;
28-
} else if (typeof videoOrOptionsOrCallback === 'object' && videoOrOptionsOrCallback.elt instanceof HTMLVideoElement) {
29-
this.video = videoOrOptionsOrCallback.elt; // Handle a p5.js video element
30-
} else if (videoOrOptionsOrCallback === 'object') {
31-
options = videoOrOptionsOrCallback;
32-
} else if (videoOrOptionsOrCallback === 'function') {
33-
callback = videoOrOptionsOrCallback;
34-
}
35-
36-
if (typeof optionsOrCallback === 'object') {
37-
options = optionsOrCallback;
38-
} else if (typeof optionsOrCallback === 'function') {
39-
callback = optionsOrCallback;
40-
}
41-
42-
if (typeof modelName === 'string') {
43-
this.modelName = modelName.toLowerCase();
44-
this.version = options.version || DEFAULTS[this.modelName].version;
45-
this.alpha = options.alpha || DEFAULTS[this.modelName].alpha;
46-
this.topk = options.topk || DEFAULTS[this.modelName].topk;
47-
this.modelLoaded = false;
48-
this.model = null;
49-
if (this.modelName === 'mobilenet') {
50-
this.modelToUse = mobilenet;
51-
} else {
52-
this.modelToUse = null;
53-
}
54-
55-
// Load the model
56-
this.modelLoaded = this.loadModel(callback);
22+
constructor(modelName, video, options, callback) {
23+
this.modelName = modelName;
24+
this.video = video;
25+
this.version = options.version || DEFAULTS[this.modelName].version;
26+
this.alpha = options.alpha || DEFAULTS[this.modelName].alpha;
27+
this.topk = options.topk || DEFAULTS[this.modelName].topk;
28+
this.modelLoaded = false;
29+
this.model = null;
30+
if (this.modelName === 'mobilenet') {
31+
this.modelToUse = mobilenet;
5732
} else {
58-
console.error('Please specify a model to use. E.g: "Mobilenet"');
33+
this.modelToUse = null;
5934
}
35+
// Load the model
36+
this.modelLoaded = this.loadModel(callback);
6037
}
6138

6239
async loadModel(callback) {
@@ -121,4 +98,35 @@ class ImageClassifier {
12198
}
12299
}
123100

124-
export default ImageClassifier;
101+
const imageClassifier = (modelName, videoOrOptionsOrCallback, optionsOrCallback, cb = null) => {
102+
let model;
103+
let video;
104+
let options = {};
105+
let callback = cb;
106+
107+
if (typeof modelName === 'string') {
108+
model = modelName.toLowerCase();
109+
} else {
110+
throw new Error('Please specify a model to use. E.g: "MobileNet"');
111+
}
112+
113+
if (videoOrOptionsOrCallback instanceof HTMLVideoElement) {
114+
video = videoOrOptionsOrCallback;
115+
} else if (typeof videoOrOptionsOrCallback === 'object' && videoOrOptionsOrCallback.elt instanceof HTMLVideoElement) {
116+
video = videoOrOptionsOrCallback.elt; // Handle a p5.js video element
117+
} else if (videoOrOptionsOrCallback === 'object') {
118+
options = videoOrOptionsOrCallback;
119+
} else if (videoOrOptionsOrCallback === 'function') {
120+
callback = videoOrOptionsOrCallback;
121+
}
122+
123+
if (typeof optionsOrCallback === 'object') {
124+
options = optionsOrCallback;
125+
} else if (typeof optionsOrCallback === 'function') {
126+
callback = optionsOrCallback;
127+
}
128+
129+
return new ImageClassifier(model, video, options, callback);
130+
};
131+
132+
export default imageClassifier;

src/ImageClassifier/index.test.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ const DEFAULTS = {
1111
};
1212

1313
describe('Create an image classifier', () => {
14-
//let classifier;
14+
// let classifier;
1515

16-
1716
it('true', () => {
1817
expect(true).toBe(true);
1918
});

src/LSTM/index.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ const DEFAULTS = {
1818
temperature: 0.5,
1919
};
2020

21-
class LSTMGenerator {
22-
constructor(modelPath = './', callback = () => {}) {
21+
class LSTM {
22+
constructor(modelPath, callback) {
2323
this.modelPath = modelPath;
2424
this.ready = false;
2525
this.indices_char = {};
@@ -58,7 +58,7 @@ class LSTMGenerator {
5858
const indexTensor = tf.tidy(() => {
5959
const input = this.convert(seed);
6060
const prediction = this.model.predict(input).squeeze();
61-
return LSTMGenerator.sample(prediction, this.temperature);
61+
return LSTM.sample(prediction, this.temperature);
6262
});
6363
const index = await indexTensor.data();
6464
indexTensor.dispose();
@@ -99,4 +99,6 @@ class LSTMGenerator {
9999
}
100100
}
101101

102+
const LSTMGenerator = (modelPath = './', callback = () => {}) => new LSTM(modelPath, callback);
103+
102104
export default LSTMGenerator;

src/Crepe/index.js renamed to src/PitchDetection/index.js

Lines changed: 56 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,59 @@
33
// This software is released under the MIT License.
44
// https://opensource.org/licenses/MIT
55

6-
// Crepe Pitch Detection model
7-
// https://github.com/marl/crepe/tree/gh-pages
8-
// https://marl.github.io/crepe/crepe.js
6+
/*
7+
Crepe Pitch Detection model
8+
https://github.com/marl/crepe/tree/gh-pages
9+
https://marl.github.io/crepe/crepe.js
10+
*/
911

1012
import * as tf from '@tensorflow/tfjs';
1113

12-
class Crepe {
13-
// in here are the functions to make exposed
14-
constructor(audioContext, stream) {
14+
class PitchDetection {
15+
constructor(modelName, audioContext, stream) {
16+
this.modelName = modelName;
1517
this.audioContext = audioContext;
1618
this.stream = stream;
17-
this.initTF();
19+
this.loadModel();
1820
}
1921

20-
async initTF() {
21-
try {
22-
console.log('Loading Keras model...');
23-
this.model = await tf.loadModel('model/model.json');
24-
console.log('Model loading complete');
25-
} catch (e) {
26-
console.error(e);
27-
}
22+
async loadModel() {
23+
this.model = await tf.loadModel('model/model.json');
2824
this.initAudio();
2925
}
3026

31-
// perform resampling the audio to 16000 Hz, on which the model is trained.
32-
// setting a sample rate in AudioContext is not supported by most browsers at the moment.
27+
initAudio() {
28+
if (this.audioContext) {
29+
try {
30+
this.processStream(this.stream);
31+
} catch (e) {
32+
throw new Error(`Error: Could not access microphone - ${e}`);
33+
}
34+
} else {
35+
throw new Error('Could not access microphone - getUserMedia not available');
36+
}
37+
}
38+
39+
processStream(stream) {
40+
const mic = this.audioContext.createMediaStreamSource(stream);
41+
const minBufferSize = (this.audioContext.sampleRate / 16000) * 1024;
42+
let bufferSize = 4;
43+
while (bufferSize < minBufferSize) bufferSize *= 2;
44+
45+
const scriptNode = this.audioContext.createScriptProcessor(bufferSize, 1, 1);
46+
scriptNode.onaudioprocess = this.processMicrophoneBuffer.bind(this);
47+
const gain = this.audioContext.createGain();
48+
gain.gain.setValueAtTime(0, this.audioContext.currentTime);
49+
50+
mic.connect(scriptNode);
51+
scriptNode.connect(gain);
52+
gain.connect(this.audioContext.destination);
53+
54+
if (this.audioContext.state !== 'running') {
55+
console.warn('User gesture needed to start AudioContext, please click');
56+
}
57+
}
58+
3359
static resample(audioBuffer, onComplete) {
3460
const interpolate = (audioBuffer.sampleRate % 16000 !== 0);
3561
const multiplier = audioBuffer.sampleRate / 16000;
@@ -39,7 +65,6 @@ class Crepe {
3965
if (!interpolate) {
4066
subsamples[i] = original[i * multiplier];
4167
} else {
42-
// simplistic, linear resampling
4368
const left = Math.floor(i * multiplier);
4469
const right = left + 1;
4570
const p = (i * multiplier) - left;
@@ -51,42 +76,32 @@ class Crepe {
5176

5277
processMicrophoneBuffer(event) {
5378
this.results = {};
54-
// bin number -> cent value mapping
5579
const centMapping = tf.add(tf.linspace(0, 7180, 360), tf.tensor(1997.3794084376191));
56-
Crepe.resample(event.inputBuffer, (resampled) => {
80+
PitchDetection.resample(event.inputBuffer, (resampled) => {
5781
tf.tidy(() => {
5882
this.running = true;
59-
60-
// run the prediction on the model
6183
const frame = tf.tensor(resampled.slice(0, 1024));
6284
const zeromean = tf.sub(frame, tf.mean(frame));
6385
const framestd = tf.tensor(tf.norm(zeromean).dataSync() / Math.sqrt(1024));
6486
const normalized = tf.div(zeromean, framestd);
6587
const input = normalized.reshape([1, 1024]);
6688
const activation = this.model.predict([input]).reshape([360]);
67-
68-
// the confidence of voicing activity and the argmax bin
6989
const confidence = activation.max().dataSync()[0];
7090
const center = activation.argMax().dataSync()[0];
7191
this.results.confidence = confidence.toFixed(3);
7292

73-
// slice the local neighborhood around the argmax bin
7493
const start = Math.max(0, center - 4);
7594
const end = Math.min(360, center + 5);
7695
const weights = activation.slice([start], [end - start]);
7796
const cents = centMapping.slice([start], [end - start]);
7897

79-
// take the local weighted average to get the predicted pitch
8098
const products = tf.mul(weights, cents);
8199
const productSum = products.dataSync().reduce((a, b) => a + b, 0);
82100
const weightSum = weights.dataSync().reduce((a, b) => a + b, 0);
83101
const predictedCent = productSum / weightSum;
84102
const predictedHz = 10 * ((predictedCent / 1200.0) ** 2);
85103

86-
// update
87104
const result = (confidence > 0.5) ? `${predictedHz.toFixed(3)} + Hz` : 'no voice';
88-
// const strlen = result.length;
89-
// for (let i = 0; i < 11 - strlen; i += 1) result = result;
90105
this.results.result = result;
91106
});
92107
});
@@ -95,55 +110,20 @@ class Crepe {
95110
getResults() {
96111
return this.results;
97112
}
113+
}
98114

99-
processStream(stream) {
100-
console.log('Setting up AudioContext ...');
101-
console.log(`Audio context sample rate = + ${this.audioContext.sampleRate}`);
102-
const mic = this.audioContext.createMediaStreamSource(stream);
103-
104-
// We need the buffer size that is a power of two
105-
// and is longer than 1024 samples when resampled to 16000 Hz.
106-
// In most platforms where the sample rate is 44.1 kHz or 48 kHz,
107-
// this will be 4096, giving 10-12 updates/sec.
108-
const minBufferSize = (this.audioContext.sampleRate / 16000) * 1024;
109-
let bufferSize = 4;
110-
while (bufferSize < minBufferSize) bufferSize *= 2;
111-
console.log(`Buffer size = ${bufferSize}`);
112-
const scriptNode = this.audioContext.createScriptProcessor(bufferSize, 1, 1);
113-
scriptNode.onaudioprocess = this.processMicrophoneBuffer.bind(this);
114-
// It seems necessary to connect the stream to a sink
115-
// for the pipeline to work, contrary to documentataions.
116-
// As a workaround, here we create a gain node with zero gain,
117-
// and connect temp to the system audio output.
118-
const gain = this.audioContext.createGain();
119-
gain.gain.setValueAtTime(0, this.audioContext.currentTime);
120-
121-
mic.connect(scriptNode);
122-
scriptNode.connect(gain);
123-
gain.connect(this.audioContext.destination);
124-
125-
if (this.audioContext.state === 'running') {
126-
console.log('Running ...');
127-
} else {
128-
console.error('User gesture needed to start AudioContext, please click');
129-
// user gesture (like click) is required to start AudioContext, in some browser versions
130-
// status('<a href="javascript:crepe.resume();" style="color:red;">*
131-
// Click here to start the demo *</a>')
132-
}
115+
const pitchDetection = (modelName, context, stream) => {
116+
let model;
117+
if (typeof modelName === 'string') {
118+
model = modelName.toLowerCase();
119+
} else {
120+
throw new Error('Please specify a model to use. E.g: "Crepe"');
133121
}
134122

135-
initAudio() {
136-
if (this.audioContext) {
137-
console.log('Initializing audio');
138-
try {
139-
this.processStream(this.stream);
140-
} catch (e) {
141-
console.error('Error: Could not access microphone - ', e);
142-
}
143-
} else {
144-
console.error('Could not access microphone - getUserMedia not available');
145-
}
123+
if (model === 'crepe') {
124+
return new PitchDetection(model, context, stream);
146125
}
147-
}
126+
throw new Error(`${model} is not a valid model to use in pitchDetection()`);
127+
};
148128

149-
export default Crepe;
129+
export default pitchDetection;

0 commit comments

Comments
 (0)