diff --git a/HISTORY.md b/HISTORY.md index 3c64f4d4..fc00575b 100755 --- a/HISTORY.md +++ b/HISTORY.md @@ -17,6 +17,7 @@ * new sound + music "volume" feature + blocks * new sound + music stereo "panning" feature + blocks * new sound attribute getter reporter + * new "play sound at sample rate" command * new "play frequency" commands in the Sounds category * added "neg" selector to monadic function reporter in "Operators" category * added "log2" selector to monadic function reporter in "Operators" category @@ -60,6 +61,7 @@ ### 2019-04-08 * Blocks, Objects, Threads: new "getSoundAttribute" reporter primitive +* Blocks, Objects, Threads: new "play sound at sample rate" command primitive ### 2019-04-05 * Objects: eliminated "clicks" when playing music notes diff --git a/src/blocks.js b/src/blocks.js index e809727c..12286768 100644 --- a/src/blocks.js +++ b/src/blocks.js @@ -1010,6 +1010,19 @@ SyntaxElementMorph.prototype.labelPart = function (spec) { true // read-only ); break; + case '%rate': + part = new InputSlotMorph( + null, + true, + { + '22.05 kHz' : 22050, + '44.1 kHz' : 44100, + '88.2 kHz' : 88200, + '96 kHz' : 96000 + } + ); + part.setContents(1); + break; case '%month': part = new InputSlotMorph( null, // text diff --git a/src/objects.js b/src/objects.js index 07e6f80a..121ce5b6 100644 --- a/src/objects.js +++ b/src/objects.js @@ -434,6 +434,12 @@ SpriteMorph.prototype.initBlocks = function () { category: 'sound', spec: 'play sound %snd until done' }, + doPlaySoundAtRate: { + type: 'command', + category: 'sound', + spec: 'play sound %snd at %rate hz', + defaults: ['', 44100] + }, doStopAllSounds: { type: 'command', category: 'sound', @@ -2035,6 +2041,7 @@ SpriteMorph.prototype.blockTemplates = function (category) { blocks.push(block('doPlaySoundUntilDone')); blocks.push(block('doStopAllSounds')); blocks.push('-'); + blocks.push(block('doPlaySoundAtRate')); blocks.push(block('reportGetSoundAttribute')); blocks.push('-'); blocks.push(block('doRest')); @@ -7517,6 +7524,7 @@ StageMorph.prototype.blockTemplates = function (category) { blocks.push(block('doPlaySoundUntilDone')); blocks.push(block('doStopAllSounds')); blocks.push('-'); + blocks.push(block('doPlaySoundAtRate')); blocks.push(block('reportGetSoundAttribute')); blocks.push('-'); blocks.push(block('doRest')); diff --git a/src/threads.js b/src/threads.js index 93556da9..b1fd6bc4 100644 --- a/src/threads.js +++ b/src/threads.js @@ -2249,6 +2249,71 @@ Process.prototype.doStopAllSounds = function () { } }; +Process.prototype.doPlaySoundAtRate = function (name, rate) { + var sound, samples, ctx, gain, pan, channels, frameCount, arrayBuffer, + i, source, rcvr; + + if (!(name instanceof List)) { + sound = name instanceof Sound ? name : detect( + this.blockReceiver().sounds.asArray(), + function (s) {return s.name === name.toString(); } + ); + if (!sound.audioBuffer) { + this.decodeSound(sound); + return; + } + samples = this.reportGetSoundAttribute('samples', sound); + } else { + samples = name; + } + + rcvr = this.blockReceiver(); + ctx = rcvr.audioContext(); + gain = rcvr.getGainNode(); + pan = rcvr.getPannerNode(); + channels = (samples.at(1) instanceof List) ? samples.length() : 1; + frameCount = (channels === 1) ? samples.length() : samples.at(1).length(); + arrayBuffer = ctx.createBuffer(channels, frameCount, +rate || 44100); + + 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]; + } + }; + } + if (channels === 1) { + arrayBuffer.copyToChannel( + Float32Array.from(samples.asArray()), + 0, + 0 + ); + } else { + for (i = 0; i < channels; i += 1) { + arrayBuffer.copyToChannel( + Float32Array.from(samples.at(i + 1).asArray()), + i, + 0 + ); + } + } + source = ctx.createBufferSource(); + source.buffer = arrayBuffer; + rcvr.setVolume(rcvr.volume); + source.connect(gain); + if (pan) { + gain.connect(pan); + pan.connect(ctx.destination); + rcvr.setPan(rcvr.pan); + } else { + gain.connect(ctx.destination); + } + source.start(); + source.pause = source.stop; + rcvr.parentThatIsA(StageMorph).activeSounds.push(source); +}; + Process.prototype.reportGetSoundAttribute = function (choice, soundName) { var sound = soundName instanceof Sound ? soundName : detect( this.blockReceiver().sounds.asArray(),