diff --git a/HISTORY.md b/HISTORY.md index 03f14f31..263a646f 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -9,6 +9,7 @@ ### 2020-01-28 * new dev version * gui: record sounds in mono +* gui: force stereo audio recordings to mono ## 5.4.4: * **Notable Fixes** diff --git a/snap.html b/snap.html index 3a138349..6f347640 100755 --- a/snap.html +++ b/snap.html @@ -8,7 +8,7 @@ - + diff --git a/src/gui.js b/src/gui.js index c6b1f75e..b98ee0fc 100644 --- a/src/gui.js +++ b/src/gui.js @@ -74,7 +74,7 @@ CommandBlockMorph, BooleanSlotMorph, RingReporterSlotMorph, ScriptFocusMorph, BlockLabelPlaceHolderMorph, SpeechBubbleMorph, XML_Element, WatcherMorph, BlockRemovalDialogMorph,TableMorph, isSnapObject, isRetinaEnabled, SliderMorph, disableRetinaSupport, enableRetinaSupport, isRetinaSupported, MediaRecorder, -Animation, BoxMorph, BlockEditorMorph, BlockDialogMorph*/ +Animation, BoxMorph, BlockEditorMorph, BlockDialogMorph, Note*/ // Global stuff //////////////////////////////////////////////////////// @@ -2432,12 +2432,14 @@ IDE_Morph.prototype.recordNewSound = function () { myself = this; soundRecorder = new SoundRecorderDialogMorph( - function (sound) { - if (sound) { - myself.currentSprite.addSound( - sound, + function (audio) { + var sound; + if (audio) { + sound = myself.currentSprite.addSound( + audio, myself.newSoundName('recording') ); + myself.makeSureRecordingIsMono(sound); myself.spriteBar.tabBar.tabTo('sounds'); myself.hasChangedMedia = true; } @@ -2447,6 +2449,163 @@ IDE_Morph.prototype.recordNewSound = function () { soundRecorder.popUp(this.world()); }; +IDE_Morph.prototype.makeSureRecordingIsMono = function (sound) { + // private and temporary, a horrible kludge to work around browsers' + // reluctance to implement audio recording constraints that let us + // record sound in mono only. As of January 2020 the audio channelCount + // constraint only works in Firefox, hence this terrible function to + // force convert a stereo sound to mono for Chrome. + // If this code is still here next year, something is very wrong. + // -Jens + + decodeSound(sound, makeMono); + + function decodeSound(sound, callback) { + var base64, binaryString, len, bytes, i, arrayBuffer, audioCtx; + if (sound.audioBuffer) { + return callback (sound); + } + base64 = sound.audio.src.split(',')[1]; + binaryString = window.atob(base64); + len = binaryString.length; + bytes = new Uint8Array(len); + for (i = 0; i < len; i += 1) { + bytes[i] = binaryString.charCodeAt(i); + } + arrayBuffer = bytes.buffer; + audioCtx = Note.prototype.getAudioContext(); + sound.isDecoding = true; + audioCtx.decodeAudioData( + arrayBuffer, + function(buffer) { + sound.audioBuffer = buffer; + return callback (sound); + }, + function (err) {throw err; } + ); + } + + function makeMono(sound) { + var samples, audio, blob, reader; + if (sound.audioBuffer.numberOfChannels === 1) {return; } + samples = sound.audioBuffer.getChannelData(0); + + audio = new Audio(); + blob = new Blob( + [ + audioBufferToWav( + encodeSound(samples, 44100).audioBuffer + ) + ], + {type: "audio/wav"} + ); + reader = new FileReader(); + reader.onload = function () { + audio.src = reader.result; + sound.audio = audio; // .... aaaand we're done! + sound.audioBuffer = null; + sound.cachedSamples = null; + sound.isDecoding = false; + // console.log('made mono', sound); + }; + reader.readAsDataURL(blob); + + } + + function encodeSound(samples, rate) { + var ctx = Note.prototype.getAudioContext(), + frameCount = samples.length, + arrayBuffer = ctx.createBuffer(1, frameCount, +rate || 44100), + i, + source; + + if (!arrayBuffer.copyToChannel) { + arrayBuffer.copyToChannel = function (src, channel) { + var buffer = this.getChannelData(channel); + for (i = 0; i < src.length; i += 1) { + buffer[i] = src[i]; + } + }; + } + arrayBuffer.copyToChannel( + Float32Array.from(samples), + 0, + 0 + ); + source = ctx.createBufferSource(); + source.buffer = arrayBuffer; + source.audioBuffer = source.buffer; + return source; + } + + function audioBufferToWav(buffer, opt) { + var sampleRate = buffer.sampleRate, + format = (opt || {}).float32 ? 3 : 1, + bitDepth = format === 3 ? 32 : 16, + result; + + result = buffer.getChannelData(0); + return encodeWAV(result, format, sampleRate, 1, bitDepth); + } + + function encodeWAV( + samples, + format, + sampleRate, + numChannels, + bitDepth + ) { + var bytesPerSample = bitDepth / 8, + blockAlign = numChannels * bytesPerSample, + buffer = new ArrayBuffer(44 + samples.length * bytesPerSample), + view = new DataView(buffer); + + function writeFloat32(output, offset, input) { + for (var i = 0; i < input.length; i += 1, offset += 4) { + output.setFloat32(offset, input[i], true); + } + } + + function floatTo16BitPCM(output, offset, input) { + var i, s; + for (i = 0; i < input.length; i += 1, offset += 2) { + s = Math.max(-1, Math.min(1, input[i])); + output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); + } + } + + function writeString(view, offset, string) { + for (var i = 0; i < string.length; i += 1) { + view.setUint8(offset + i, string.charCodeAt(i)); + } + } + + writeString(view, 0, 'RIFF'); // RIFF identifier + // RIFF chunk length: + view.setUint32(4, 36 + samples.length * bytesPerSample, true); + writeString(view, 8, 'WAVE'); // RIFF type + writeString(view, 12, 'fmt '); // format chunk identifier + view.setUint32(16, 16, true); // format chunk length + view.setUint16(20, format, true); // sample format (raw) + view.setUint16(22, numChannels, true); // channel count + view.setUint32(24, sampleRate, true); // sample rate + // byte rate (sample rate * block align): + view.setUint32(28, sampleRate * blockAlign, true); + // block align (channel count * bytes per sample): + view.setUint16(32, blockAlign, true); + view.setUint16(34, bitDepth, true); // bits per sample + writeString(view, 36, 'data'); // data chunk identifier + // data chunk length: + view.setUint32(40, samples.length * bytesPerSample, true); + if (format === 1) { // Raw PCM + floatTo16BitPCM(view, 44, samples); + } else { + writeFloat32(view, 44, samples); + } + return buffer; + } +}; + IDE_Morph.prototype.duplicateSprite = function (sprite) { var duplicate = sprite.fullCopy(); duplicate.isDown = false; diff --git a/src/objects.js b/src/objects.js index 73c0c772..7d294ca6 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, copyCanvas*/ -modules.objects = '2020-January-11'; +modules.objects = '2020-January-28'; var SpriteMorph; var StageMorph; @@ -3584,8 +3584,10 @@ SpriteMorph.prototype.reportCostumes = function () { // SpriteMorph sound management SpriteMorph.prototype.addSound = function (audio, name) { + var sound = new Sound(audio, name); this.shadowAttribute('sounds'); - this.sounds.add(new Sound(audio, name)); + this.sounds.add(sound); + return sound; }; SpriteMorph.prototype.doPlaySound = function (name) {