From 6306bcc27927bc51addc51210f80714fad4b1449 Mon Sep 17 00:00:00 2001 From: jmoenig Date: Sun, 20 Oct 2019 13:15:54 +0200 Subject: [PATCH] added "new sound" from list of samples primitive reporter to "sound" category --- HISTORY.md | 4 +++ snap.html | 4 +-- src/objects.js | 28 ++++++++++++++- src/threads.js | 95 +++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 127 insertions(+), 4 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 82c63ef2..d9d399de 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -3,6 +3,7 @@ ## in development: * **New Features:** * new primitive in "looks": NEW COSTUME from a list of pixels and dimensions, allowing CURRENT + * new primitige in "sound": NEW SOUND from a list of samples * added selectors for sprites' and the stage's bounding box (LEFT, RIGHT, TOP, BOTTOM) to MY dropdown * **Notable Changes:** * running STOP ALL now also toggles (pauses and resumes) all generic WHEN hat blocks (just like pressing the stop button) @@ -12,6 +13,9 @@ * **Translation Updates:** * German +### 2019-10-20 +* objects, threads: added "new sound" from list of samples primitive reporter to "sound" category + ### 2019-10-18 * objects, blocks, threads: added dimension getters for the stage * German translation update (left, right, top, bottom selectors in MY) diff --git a/snap.html b/snap.html index 6ff85d14..5ca18b0e 100755 --- a/snap.html +++ b/snap.html @@ -7,8 +7,8 @@ - - + + diff --git a/src/objects.js b/src/objects.js index f6936d01..38a18488 100644 --- a/src/objects.js +++ b/src/objects.js @@ -84,7 +84,7 @@ BlockEditorMorph, BlockDialogMorph, PrototypeHatBlockMorph, BooleanSlotMorph, localize, TableMorph, TableFrameMorph, normalizeCanvas, VectorPaintEditorMorph, HandleMorph, AlignmentMorph, Process, XML_Element, WorldMap*/ -modules.objects = '2019-October-18'; +modules.objects = '2019-October-20'; var SpriteMorph; var StageMorph; @@ -478,6 +478,11 @@ SpriteMorph.prototype.initBlocks = function () { spec: '%aa of sound %snd', defaults: [['duration']] }, + reportNewSoundFromSamples: { + type: 'reporter', + category: 'sound', + spec: 'new sound %l' + }, doRest: { type: 'command', category: 'sound', @@ -2266,6 +2271,7 @@ SpriteMorph.prototype.blockTemplates = function (category) { blocks.push('-'); blocks.push(block('doPlaySoundAtRate')); blocks.push(block('reportGetSoundAttribute')); + blocks.push(block('reportNewSoundFromSamples')); blocks.push('-'); blocks.push(block('doRest')); blocks.push(block('doPlayNote')); @@ -7227,6 +7233,25 @@ SpriteMorph.prototype.doScreenshot = function (imgSource, data) { this.addCostume(costume); }; +// SpriteMorph adding sounds + +SpriteMorph.prototype.newSoundName = function (name, ignoredSound) { + var ix = name.indexOf('('), + stem = (ix < 0) ? name : name.substring(0, ix), + count = 1, + newName = stem, + all = this.sounds.asArray().filter( + function (each) {return each !== ignoredSound; } + ).map( + function (each) {return each.name; } + ); + while (contains(all, newName)) { + count += 1; + newName = stem + '(' + count + ')'; + } + return newName; +}; + // SpriteHighlightMorph ///////////////////////////////////////////////// // SpriteHighlightMorph inherits from Morph: @@ -8317,6 +8342,7 @@ StageMorph.prototype.blockTemplates = function (category) { blocks.push('-'); blocks.push(block('doPlaySoundAtRate')); blocks.push(block('reportGetSoundAttribute')); + blocks.push(block('reportNewSoundFromSamples')); blocks.push('-'); blocks.push(block('doRest')); blocks.push(block('doPlayNote')); diff --git a/src/threads.js b/src/threads.js index 015f91b7..988a1fa0 100644 --- a/src/threads.js +++ b/src/threads.js @@ -61,7 +61,7 @@ StageMorph, SpriteMorph, StagePrompterMorph, Note, modules, isString, copy, isNil, WatcherMorph, List, ListWatcherMorph, alert, console, TableMorph, Color, TableFrameMorph, ColorSlotMorph, isSnapObject, Map, newCanvas, Symbol*/ -modules.threads = '2019-October-18'; +modules.threads = '2019-October-20'; var ThreadManager; var Process; @@ -2816,6 +2816,99 @@ Process.prototype.encodeSound = function (samples, rate) { return source; }; +// Process first-class sound creation from samples, interpolated + +Process.prototype.reportNewSoundFromSamples = function (samples) { + // interpolated + var audio, blob, reader, + myself = this; + + if (isNil(this.context.accumulator)) { + this.context.accumulator = { + audio: null + }; + audio = new Audio(); + blob = this.audioBufferToWaveBlob( + this.encodeSound(samples, 44100).audioBuffer, + samples.length() + ); + reader = new FileReader(); + reader.onload = function () { + audio.src = reader.result; + myself.context.accumulator.audio = audio; + }; + reader.readAsDataURL(blob); + } + if (this.context.accumulator.audio) { + return new Sound( + this.context.accumulator.audio, + this.blockReceiver().newSoundName(localize('sound')) + ); + } + this.pushContext('doYield'); + this.pushContext(); +}; + +Process.prototype.audioBufferToWaveBlob = function (aBuffer, len) { + // Convert AudioBuffer to a Blob using WAVE representation + // taken from: + // https://www.russellgood.com/how-to-convert-audiobuffer-to-audio-file/ + + var numOfChan = aBuffer.numberOfChannels, + length = len * numOfChan * 2 + 44, + buffer = new ArrayBuffer(length), + view = new DataView(buffer), + channels = [], i, sample, + offset = 0, + pos = 0; + + function setUint16(data) { + view.setUint16(pos, data, true); + pos += 2; + } + + function setUint32(data) { + view.setUint32(pos, data, true); + pos += 4; + } + + // write WAVE header + setUint32(0x46464952); // "RIFF" + setUint32(length - 8); // file length - 8 + setUint32(0x45564157); // "WAVE" + + setUint32(0x20746d66); // "fmt " chunk + setUint32(16); // length = 16 + setUint16(1); // PCM (uncompressed) + setUint16(numOfChan); + setUint32(aBuffer.sampleRate); + setUint32(aBuffer.sampleRate * 2 * numOfChan); // avg. bytes/sec + setUint16(numOfChan * 2); // block-align + setUint16(16); // 16-bit (hardcoded in this demo) + + setUint32(0x61746164); // "data" - chunk + setUint32(length - pos - 4); // chunk length + + // write interleaved data + for (i = 0; i < aBuffer.numberOfChannels; i += 1) { + channels.push(aBuffer.getChannelData(i)); + } + + while (pos < length) { + for (i = 0; i < numOfChan; i += 1) { // interleave channels + sample = Math.max(-1, Math.min(1, channels[i][offset])); // clamp + // scale to 16-bit signed int: + sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0; + view.setInt16(pos, sample, true); // write 16-bit sample + pos += 2; + } + offset += 1; // next source sample + } + + // create Blob + return new Blob([buffer], {type: "audio/wav"}); +}; + // Process audio input (interpolated) Process.prototype.reportAudio = function (choice) {