Skip to content

Commit

Permalink
Wave export (#17)
Browse files Browse the repository at this point in the history
* add function to convert to wave

* refactor for offline rendering

* remove unused buffer
fix render to resolve oncomplete

* add references and update readme
add missing await
  • Loading branch information
chris--jones authored Oct 9, 2020
1 parent 9da7b85 commit 0ea056d
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 38 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ const characters = morsify.characters(); // {'1': {'A': '.-', ...}, ..., '11': {
const audio = morsify.audio('SOS');
audio.play(); // play audio
audio.stop(); // stop audio
audio.exportWave(); // download audio wave file (promise)
const url = await audio.getWaveUrl(); // get audio wave url (promise)
const blob = await audio.getWaveBlob(); // get audio wave blob (promise)
```

Alternatively, you can use the library directly with including the source file.
Expand All @@ -45,6 +48,9 @@ Alternatively, you can use the library directly with including the source file.
var gainNode = audio.gainNode; // GainNode
audio.play(); // play audio
audio.stop(); // stop audio
audio.exportWave(); // download audio wave file (promise)
var url = await audio.getWaveUrl(); // get audio wave url (promise)
var blob = await audio.getWaveBlob(); // get audio wave blob (promise)
</script>
```

Expand Down
179 changes: 141 additions & 38 deletions src/morsify.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,79 +178,182 @@
}).join('');
};

let AudioContext = null;
let context = null;
const getGainTimings = (morse, opts, currentTime = 0) => {
let timings = [];
let time = 0;

const audio = (text, opts, morseString) => {

if (AudioContext === null && typeof window !== 'undefined') {
AudioContext = window.AudioContext || window.webkitAudioContext;
context = new AudioContext();
}

const options = getOptions(opts);
const morse = morseString || encode(text, opts);
const oscillator = context.createOscillator();
const gainNode = context.createGain();

let timeout;
let t = context.currentTime;

oscillator.type = options.oscillator.type;
oscillator.frequency.value = options.oscillator.frequency;
oscillator.onended = options.oscillator.onended;

gainNode.gain.setValueAtTime(0, t);
timings.push([0, time]);

const tone = (i) => {
gainNode.gain.setValueAtTime(1, t);
t += i * options.unit;
timings.push([1, currentTime + time]);
time += i * opts.unit;
};

const silence = (i) => {
gainNode.gain.setValueAtTime(0, t);
t += i * options.unit;
timings.push([0, currentTime + time]);
time += i * opts.unit;
};

const gap = (i) => {
gainNode.gain.setValueAtTime(0, t);
t += i * options.fwUnit;
timings.push([0, currentTime + time]);
time += i * opts.fwUnit;
};

for (let i = 0; i <= morse.length; i++) {
if (morse[i] === options.space) {
if (morse[i] === opts.space) {
gap(7);
} else if (morse[i] === options.dot) {
} else if (morse[i] === opts.dot) {
tone(1);
silence(1);
} else if (morse[i] === options.dash) {
} else if (morse[i] === opts.dash) {
tone(3);
silence(1);
} else if (
(typeof morse[i + 1] !== 'undefined' && morse[i + 1] !== options.space) &&
(typeof morse[i - 1] !== 'undefined' && morse[i - 1] !== options.space)
(typeof morse[i + 1] !== 'undefined' && morse[i + 1] !== opts.space) &&
(typeof morse[i - 1] !== 'undefined' && morse[i - 1] !== opts.space)
) {
gap(3);
}
}

return [timings, time];
}

const floatTo16BitPCM = (output, offset, input) => {
for (let i = 0; i < input.length; i++, offset += 2) {
let s = Math.max(-1, Math.min(1, input[i]));
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
}

const writeString = (view, offset, string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}

// graciously borrowed from: https://github.com/mattdiamond/Recorderjs/blob/master/src/recorder.js
const encodeWAV = (sampleRate, samples) => {
let buffer = new ArrayBuffer(44 + samples.length * 2);
let view = new DataView(buffer);

/* RIFF identifier */
writeString(view, 0, 'RIFF');
/* RIFF chunk length */
view.setUint32(4, 36 + samples.length * 2, true);
/* RIFF type */
writeString(view, 8, 'WAVE');
/* format chunk identifier */
writeString(view, 12, 'fmt ');
/* format chunk length */
view.setUint32(16, 16, true);
/* sample format (raw) */
view.setUint16(20, 1, true);
/* channel count */
view.setUint16(22, 1, true);
/* sample rate */
view.setUint32(24, sampleRate, true);
/* byte rate (sample rate * block align) */
view.setUint32(28, sampleRate * 4, true);
/* block align (channel count * bytes per sample) */
view.setUint16(32, 2, true);
/* bits per sample */
view.setUint16(34, 16, true);
/* data chunk identifier */
writeString(view, 36, 'data');
/* data chunk length */
view.setUint32(40, samples.length * 2, true);

floatTo16BitPCM(view, 44, samples);

return view;
}

const audio = (text, opts, morseString) => {
let AudioContext = null;
let OfflineAudioContext = null;
let context = null;
let offlineContext = null;
let source;
const options = getOptions(opts);
const morse = morseString || encode(text, opts);
const [gainValues, totalTime] = getGainTimings(morse, options);

if (AudioContext === null && typeof window !== 'undefined') {
AudioContext = window.AudioContext || window.webkitAudioContext;
context = new AudioContext();
source = context.createBufferSource();
source.connect(context.destination);
}

if (OfflineAudioContext === null && typeof window !== 'undefined') {
OfflineAudioContext = window.OfflineAudioContext;
offlineContext = new OfflineAudioContext(1, 22050 * totalTime, 22050);
}

const oscillator = offlineContext.createOscillator();
const gainNode = offlineContext.createGain();

oscillator.type = options.oscillator.type;
oscillator.frequency.value = options.oscillator.frequency;

gainValues.forEach(([value, time]) => gainNode.gain.setValueAtTime(value, time));

oscillator.connect(gainNode);
gainNode.connect(context.destination);
gainNode.connect(offlineContext.destination);
source.onended = options.oscillator.onended;

// offline rendering as a promise inspired by: http://joesul.li/van/tale-of-no-clocks/
let render = new Promise(resolve => {
oscillator.start(0);
offlineContext.startRendering();
offlineContext.oncomplete = (e) => {
source.buffer = e.renderedBuffer;
resolve();
}
});

let timeout;

const play = () => {
oscillator.start(context.currentTime);
timeout = setTimeout(() => stop(), (t - context.currentTime) * 1000);
const play = async () => {
await render;
source.start(context.currentTime);
timeout = setTimeout(() => stop(), totalTime * 1000);
};

const stop = () => {
clearTimeout(timeout);
timeout = 0;
oscillator.stop(0);
source.stop(0);
};

const getWaveBlob = async () => {
await render;
const waveData = encodeWAV(offlineContext.sampleRate, source.buffer.getChannelData(0));
const audioBlob = new Blob([waveData], { 'type': 'audio/wav' });
return audioBlob;
}

const getWaveUrl = async () => {
const audioBlob = await getWaveBlob();
return URL.createObjectURL(audioBlob);
};

const exportWave = async () => {
let waveUrl = await getWaveUrl();
var anchor = document.createElement('a');
anchor.href = waveUrl;
anchor.target = '_blank';
anchor.download = 'morsify.wav';
anchor.click();
};

return {
play,
stop,
getWaveBlob,
getWaveUrl,
exportWave,
context,
oscillator,
gainNode
Expand Down

0 comments on commit 0ea056d

Please sign in to comment.