Skip to content

Commit

Permalink
Rework ABI to allow export of wave buffers
Browse files Browse the repository at this point in the history
As requested in discordier#1, the wave buffer generating routines should be
exported.
However, the suggested way of adding multiple exports to the module then
broke the common-js builds as there may only be one default export.
Therefore we now have multiple methods being exported by the main class.
  • Loading branch information
discordier committed Oct 2, 2019
1 parent 561ccb8 commit 80b1808
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 70 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,27 @@ For further details, refer to

## Usage

Require the module via npm: `npm install sam-js`
Require the module via yarn: `yarn add sam-js`

Use it in your program:

```javascript
import SamJs from 'sam-js';

let sam = new SamJs();

// Play "Hello world" over the speaker.
// This returns a Promise resolving after playback has finished.
sam.speak('Hello world');

// Generate a wave file containing "Hello world" and download it.
sam.download('Hello world');

// Render the passed text as 8bit wave buffer array (Uint8Array).
const buf8 = sam.buf8('Hello world');

// Render the passed text as 32bit wave buffer array (Float32Array).
const buf32 = sam.buf32('Hello world');
```

### Typical voice values
Expand Down
2 changes: 1 addition & 1 deletion src/guessnum.es6
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ function GuessNum(e) {
if (raw) {
output.innerText += "\n" + raw;
}
Renderer(phonemes, {phonetic: true});
Renderer(phonemes);
}
input.onkeydown = (e) => {
if (e.keyCode === 13) {
Expand Down
96 changes: 70 additions & 26 deletions src/index.es6
Original file line number Diff line number Diff line change
@@ -1,40 +1,84 @@
import {PlayBuffer, RenderBuffer} from './util/player.es6'
import TextToPhonemes from './reciter/reciter.es6';
import {SamSpeak} from './sam/sam.es6';
import {SamProcess, SamBuffer} from './sam/sam.es6';

const convert = TextToPhonemes;
const buf8 = SamProcess;
const buf32 = SamBuffer;

/**
* @param {object} [options]
* @param {Boolean} [options.phonetic] Default false.
* @param {Boolean} [options.singmode] Default false.
* @param {Boolean} [options.debug] Default false.
* @param {Number} [options.pitch] Default 64.
* @param {Number} [options.speed] Default 72.
* @param {Number} [options.mouth] Default 128.
* @param {Number} [options.throat] Default 128.
*
* @constructor
*/
function SamJs (options) {
const opts = options || {};

const convert = this.convert = (text) => {
let input = TextToPhonemes(text);
if (!input) {
if (process.env.DEBUG_SAM === true) {
throw new Error(`phonetic input: "${text}" could not be converted`);
}
throw new Error();
const ensurePhonetic = (text, phonetic) => {
if (!(phonetic || opts.phonetic)) {
return convert(text);
}
return text.toUpperCase();
}

if (process.env.DEBUG_SAM === true) {
console.log('phonetic data: "%s"', input);
}
/**
* Render the passed text as 8bit wave buffer array.
*
* @param {string} text The text to render or phoneme string.
* @param {boolean} [phonetic] Flag if the input text is already phonetic data.
*
* @return {Uint8Array|Boolean}
*/
this.buf8 = (text, phonetic) => {
return buf8(ensurePhonetic(text, phonetic), opts);
}

return input;
};
/**
* Render the passed text as 32bit wave buffer array.
*
* @param {string} text The text to render or phoneme string.
* @param {boolean} [phonetic] Flag if the input text is already phonetic data.
*
* @return {Float32Array|Boolean}
*/
this.buf32 = (text, phonetic) => {
return buf32(ensurePhonetic(text, phonetic), opts);
}

/**
* Render the passed text as wave buffer and play it over the speakers.
*
* @param {string} text The text to render or phoneme string.
* @param {boolean} [phonetic] Flag if the input text is already phonetic data.
*
* @return {Promise}
*/
this.speak = (text, phonetic) => {
if (process.env.DEBUG_SAM === true) {
console.log('text input: ', text);
}

let input;
return PlayBuffer(this.buf32(text, phonetic));
}

if (!(phonetic || opts.phonetic)) {
input = convert(text);
} else {
input = text.toUpperCase();
}

return SamSpeak(input, opts);
};
/**
* Render the passed text as wave buffer and download it via URL API.
*
* @param {string} text The text to render or phoneme string.
* @param {boolean} [phonetic] Flag if the input text is already phonetic data.
*
* @return void
*/
this.download = (text, phonetic) => {
RenderBuffer(this.buf8(text, phonetic));
}
}

SamJs.buf8 = buf8;
SamJs.buf32 = buf32;
SamJs.convert = convert;

export default SamJs;
54 changes: 12 additions & 42 deletions src/sam/sam.es6
Original file line number Diff line number Diff line change
@@ -1,60 +1,55 @@
import {PlayBuffer, RenderBuffer} from '../util/player.es6';
import {PlayBuffer, UInt8ArrayToFloat32Array} from '../util/player.es6';

import Parser from '../parser/parser.es6';
import Renderer from '../renderer/renderer.es6';

/**
* Process the input and return the audiobuffer.
* Process the input and play the audio buffer.
*
* @param {String} input
*
* @param {object} [options]
* @param {Boolean} [options.phonetic] Default false.
* @param {Boolean} [options.singmode] Default false.
* @param {Boolean} [options.debug] Default false.
* @param {Number} [options.pitch] Default 64.
* @param {Number} [options.speed] Default 72.
* @param {Number} [options.mouth] Default 128.
* @param {Number} [options.throat] Default 128.
*
* @return {Uint8Array|Boolean}
* @return {Promise}
*/
export function SamSpeak (input, options) {
const buffer = SamProcess(input, options);
const buffer = SamBuffer(input, options);
if (false === buffer) {
return false;
}
const audio = new Float32Array(buffer.length);
for(let i=0; i < buffer.length; i++) {
audio[i] = (buffer[i] - 128) / 256;
return Promise.reject();
}

// Now push buffer to wave player.
return PlayBuffer(audio);
return PlayBuffer(buffer);
}

/**
* Process the input and return the audiobuffer.
* Process the input and return the audio buffer.
*
* @param {String} input
*
* @param {object} [options]
* @param {Boolean} [options.phonetic] Default false.
* @param {Boolean} [options.singmode] Default false.
* @param {Boolean} [options.debug] Default false.
* @param {Number} [options.pitch] Default 64.
* @param {Number} [options.speed] Default 72.
* @param {Number} [options.mouth] Default 128.
* @param {Number} [options.throat] Default 128.
*
* @return {Uint8Array|Boolean}
* @return {Float32Array|Boolean}
*/
export function SamWave (input, options) {
export function SamBuffer (input, options) {
const buffer = SamProcess(input, options);
if (false === buffer) {
return false;
}
RenderBuffer(buffer);

return UInt8ArrayToFloat32Array(buffer);
}

/**
Expand All @@ -63,7 +58,6 @@ export function SamWave (input, options) {
* @param {String} input
*
* @param {object} [options]
* @param {Boolean} [options.phonetic] Default false.
* @param {Boolean} [options.singmode] Default false.
* @param {Boolean} [options.debug] Default false.
* @param {Number} [options.pitch] Default 64.
Expand All @@ -73,35 +67,11 @@ export function SamWave (input, options) {
*
* @return {Uint8Array|Boolean}
*/
function SamProcess (input, options) {
export function SamProcess (input, options = {}) {
const parsed = Parser(input);
if (false === parsed) {
return false;
}

return Renderer(parsed, options.pitch, options.mouth, options.throat, options.speed, options.singmode);
}

/**
*
* @param {object} [options]
* @param {Boolean} [options.phonetic] Default false.
* @param {Boolean} [options.singmode] Default false.
* @param {Boolean} [options.debug] Default false.
* @param {Number} [options.pitch] Default 64.
* @param {Number} [options.speed] Default 72.
* @param {Number} [options.mouth] Default 128.
* @param {Number} [options.throat] Default 128.
*
* @returns {Function}
* @constructor
*/
export default function Sam (options) {
this.speak = (_input) => {
SamSpeak(_input, options);
};

this.wave = (_input) => {
SamWave(_input, options);
};
}
22 changes: 22 additions & 0 deletions src/util/player.es6
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {text2Uint8Array, Uint32ToUint8Array, Uint16ToUint8Array} from '../util/u
*
* @param {AudioContext} context
* @param audiobuffer
*
* @return {Promise}
*/
function Play(context, audiobuffer) {
return new Promise((resolve) => {
Expand All @@ -28,6 +30,8 @@ let context = null;
* Play an audio buffer.
*
* @param {Float32Array} audiobuffer
*
* @return {Promise}
*/
export function PlayBuffer(audiobuffer) {
if (null === context) {
Expand All @@ -44,9 +48,27 @@ export function PlayBuffer(audiobuffer) {
return Play(context, audiobuffer);
}

/**
* Convert a Uint8Array wave buffer to a Float32Array WaveBuffer
*
* @param {Uint8Array} buffer
*
* @return {Float32Array}
*/
export function UInt8ArrayToFloat32Array (buffer) {
const audio = new Float32Array(buffer.length);
for(let i=0; i < buffer.length; i++) {
audio[i] = (buffer[i] - 128) / 256;
}

return audio
}

/**
*
* @param {Uint8Array} audiobuffer
*
* @return void
*/
export function RenderBuffer (audiobuffer) {
let filename = 'sam.wav';
Expand Down
77 changes: 77 additions & 0 deletions test/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { assert } from 'chai'

import SamJs from '../src/index.es6';

describe('index.es6', () => {
describe('SamJs', () => {
it('should have method buf8', () => {
const sam = new SamJs({});
assert.isDefined(sam.buf8);
})
it('should have method buf32', () => {
const sam = new SamJs({});
assert.isDefined(sam.buf32);
})
it('should have method speak', () => {
const sam = new SamJs({});
assert.isDefined(sam.speak);
})
it('should speak', () => {
let bufferLength, setBuffer
const source = {
buffer: {
set (buffer) {
assert.strictEqual(buffer, soundBuffer)
}
},
connect (destination) {
assert.strictEqual(destination, context.destination)
},
start (when) {
assert.strictEqual(when, 0)
assert.notEqual(setBuffer, undefined)
assert.strictEqual(setBuffer.length, bufferLength)
assert.notEqual(this.onended, undefined)
this.onended()
}
};
const context = {
createBufferSource () {
return source
},
createBuffer(numberOfChannels, length, sampleRate) {
bufferLength = length
assert.strictEqual(numberOfChannels, 1)
assert.notEqual(length, 0)
assert.strictEqual(sampleRate, 22050)
return soundBuffer
},
destination: {}
};
const soundBuffer = {
getChannelData (channel) {
assert.strictEqual(channel, 0)
return setBuffer = []
}
};
global.AudioContext = function () {
return context
}

const sam = new SamJs({});
return sam.speak('/HEHLOW').then(
() => {
delete global.AudioContext
},
(e) => {
delete global.AudioContext
console.log(e)
assert.fail('Failed to play.');
});
})
});
it('should have method download', () => {
const sam = new SamJs({});
assert.isDefined(sam.download);
})
});
1 change: 1 addition & 0 deletions test/minimum-tests.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ require('./parser/parser-vs-parser-c.spec.js');
require('./renderer/renderer-c.spec');
require('./renderer/renderer.spec');
require('./renderer/renderer-vs-renderer-c.spec.js');
require('./index.spec.js');

0 comments on commit 80b1808

Please sign in to comment.