Discuss Scratch

MegaApuTurkUltra
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

Mandatory credit statement: @comp09's project inspired me to make this

I've complained about Scratch's lack of sound quality before, so I made this extension!

It plays sounds of any quality you want, either from Scratch or from an external URL. (I tried to write javascript to upload sounds to the scratch assets CDN, but it's non-trivial/impossible since the API endpoint isn't CORS-friendly). Since I was confused about how exactly sound buffering works / the audio api apparently doesn't fire the onload event, I made this ajax all sounds, so URLs either need to go through a cors proxy, or need to be on the assets CDN (it sets the cors header on sound downloads, but not on uploads -_-).

Uploading to the assets CDN looks like this:
POST http://assets.scratch.mit.edu/internalapi/asset/<YOUR FILE'S MD5 HASH>.wav/set/?v=v432&_rnd=0.5114612365141511 HTTP/1.1
Host: assets.scratch.mit.edu
Connection: keep-alive
content-type: audio/wav
x-csrftoken: <YOUR CSRF>
Referer: http://cdn.scratch.mit.edu/scratchr2/static/__27442983cb3602c2816db35c44be906e__/Scratch.swf
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.8,hu;q=0.6
Cookie: scratchsessionsid=<YOUR SESSION ID>; scratchcsrftoken=<YOUR CSRF>

<RAW FILE HERE>

Anyways, here's how to use it (it provides more controls than the default sound blocks!)
if<Is running HQS?::extension>
Load sound [Pick a name to access this sound with][asset md5 here]::extension
// blocks until loaded. Use the percent loaded reporter on another thread if you want
Load sound [360noscope][af946297a5e07fefbaa785ade11bf1f0]::extension
// example. This is actually an MP3 which chrome correctly identifies despite content-type: audio/wav
Load sound [SwagMaster] from url [http://swag.com/swag.mp3]::extension
// same as above, just loads from URL
Play or resume sound [SwagMaster]::extension
// Plays or resumes the sound
Pause sound [SwagMaster]::extension
// pauses so you can resume with the play/resume block
Stop sound [SwagMaster]::extension
// stops, so future plays will start from the beginning
Set volume for sound [SwagMaster] to [0 to 100]::extension
say (Volume of sound [SwagMaster]::extension)
// reports current volume. Volume for individual sounds is independent
Stop all sounds::extension
Pause all sounds::extension
Set volume of all sounds to (100)::extension
// self-explanatory ^^
for each [sound name v] in (Get all loaded sounds::extension)::cstart
// use the hacked for loop to see all currently loaded sounds
end
else
play sound [Swag v] // fall back
end

Beware that stopping the project will NOT stop currently playing sounds!
Workaround:
when gf clicked
create clone of [myself v]
play sounds::grey

when I start as a clone
forever
reset timer
end

when [timer v] > (0.1)
stop all sounds::extension
reset timer
wait (0.2) secs

How to install:
Either run this code in the console:
ScratchExtensions.loadExternalJS("https://cdn.rawgit.com/MegaApuTurkUltra/Scratch-HQ-Sound/c9bc589fd176ba6f466b1d3141eb67370dfc0a0f/scratch-hqs.ext.js")

Or run the extension code in console:
(function (ext) {
    ext._shutdown = function () {};
    ext._getStatus = function () {
        return {
            status: 2,
            msg: 'Ready to No$c0pe'
        };
    };
    var sounds = {};
    var findSound = function (md5) {
        for (x in sounds) {
            if (sounds[x].md5 == md5) return sounds[x];
        }
        return null;
    };
    ext.load_sound = function (name, md5, callback) {
        if (sounds.hasOwnProperty(name)) {
            callback();
            return;
        }
        var s = findSound(md5);
        if (s != null) {
            sounds[name] = s;
            callback();
            return;
        }
        var obj = {
            md5: md5,
            percent: 0,
            audio: null
        };
        sounds[name] = obj;
        var audio = new window.Audio();
        audio.onended = function () {
            this.currentTime = 0;
        };
        var xhr = new XMLHttpRequest();
        xhr.onprogress = function (e) {
            if (e.lengthComputable) {
                var percent = (e.loaded / e.total) * 100;
                console.log("Loaded", percent);
                obj.percent = percent;
            }
        };
        xhr.onreadystatechange = function () {
            if (this.readyState == 4 && this.status == 200) {
                audio.src = window.URL.createObjectURL(xhr.response);
                obj.audio = audio;
                callback();
            }
        };
        xhr.open('GET', "http://cdn.assets.scratch.mit.edu/internalapi/asset/" + md5 + ".wav/get/");
        xhr.responseType = 'blob';
        xhr.send();
    };
	
	ext.load_sound_url = function (name, url, callback) {
        if (sounds.hasOwnProperty(name)) {
            callback();
            return;
        }
        var s = findSound(md5);
        if (s != null) {
            sounds[name] = s;
            callback();
            return;
        }
        var obj = {
            md5: md5,
            percent: 0,
            audio: null
        };
        sounds[name] = obj;
        var audio = new window.Audio();
        audio.onended = function () {
            this.currentTime = 0;
        };
        var xhr = new XMLHttpRequest();
        xhr.onprogress = function (e) {
            if (e.lengthComputable) {
                var percent = (e.loaded / e.total) * 100;
                console.log("Loaded", percent);
                obj.percent = percent;
            }
        };
        xhr.onreadystatechange = function () {
            if (this.readyState == 4 && this.status == 200) {
                audio.src = window.URL.createObjectURL(xhr.response);
                obj.audio = audio;
                callback();
            }
        };
        xhr.open('GET', url);
        xhr.responseType = 'blob';
        xhr.send();
    };
    
    ext.percent_loaded = function(name){
        if(sounds.hasOwnProperty(name)) return sounds[name].percent;
        else return 0;
    };
    ext.play = function (name) {
        if (sounds.hasOwnProperty(name)) sounds[name].audio.play();
    };
    ext.pause = function (name) {
        if (sounds.hasOwnProperty(name)) sounds[name].audio.pause();
    };
    ext.stop = function (name) {
        if (sounds.hasOwnProperty(name)) {
            sounds[name].audio.pause();
            sounds[name].audio.currentTime = 0;
        }
    };
    ext.set_volume = function (name, volume) {
        if (sounds.hasOwnProperty(name)) {
            sounds[name].audio.volume = volume / 100;
        }
    };
    ext.get_volume = function (name) {
        if (sounds.hasOwnProperty(name)) {
            return sounds[name].audio.volume * 100;
        } else return 0;
    };
    ext.global_stop = function () {
        for (x in sounds) ext.stop(x);
    };
    ext.global_pause = function () {
        for (x in sounds) ext.pause(x);
    };
    ext.global_set_volume = function (volume) {
        for (x in sounds) ext.set_volume(x, volume);
    };
    ext.get_all_sounds = function () {
        var s = [];
        for (x in sounds) {
            s.push(x);
        }
        return s;
    };
    ext.is_enabled = function () {
        return true;
    };
    
    ext.is_playing = function(name){
    	if (sounds.hasOwnProperty(name)) {
            return !(sounds[name].audio.paused);
        } else return false;
    }
    
     ext.get_length = function(name){
    	if (sounds.hasOwnProperty(name)) {
            return sounds[name].audio.duration;
        } else return 0;
    }
    
     ext.get_time = function(name){
    	if (sounds.hasOwnProperty(name)) {
            return sounds[name].audio.currentTime;
        } else return 0;
    }
    
     ext.set_time = function(name, time){
    	if (sounds.hasOwnProperty(name)) {
            sounds[name].audio.currentTime = time;
        }
    }
    var descriptor = {
        blocks: [
            ['b', 'Is Running HQS?', 'is_enabled'],
            ['w', 'Load sound %s %s', 'load_sound', 'sound name', 'md5'],
            ['w', 'Load sound %s from url %s', 'load_sound_url', 'sound name', 'url'],
            ['r', 'Percentage of %s loaded', 'percent_loaded', 'sound name'],
            [' ', 'Play or resume sound %s', 'play', 'sound name'],
            [' ', 'Pause sound %s', 'pause', 'sound name'],
            [' ', 'Stop sound %s', 'stop', 'sound name'],
            [' ', 'Set volume for sound %s to %n', 'set_volume', 'sound name', 100],
            ['r', 'Volume of sound %s', 'get_volume', 'sound name'],
            ['b', 'Is sound %s playing?', 'is_playing', 'sound name'],
            ['r', 'Length of sound %s in seconds', 'get_length', 'sound name'],
            ['r', 'Current position in sound %s in seconds', 'get_time', 'sound name'],
            [' ', 'Set position in sound %s to %n seconds', 'set_time', 'sound name', '5'],
            [' ', 'Stop all sounds', 'global_stop'],
            [' ', 'Pause all sounds', 'global_pause'],
            [' ', 'Set volume of all sounds to %n', 'global_set_volume', 100],
            ['r', 'Get all loaded sounds', 'get_all_sounds']
        ],
        url: "http://scratch.mit.edu/discuss/youtube/dQw4w9WgXcQ/" // yes
    };
    ScratchExtensions.register('High Quality Sounds', descriptor, ext);
})({});

Last edited by MegaApuTurkUltra (Feb. 19, 2015 17:56:48)

comp09
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

Hey, at least give credit to me for inspiring you to do this!
Thepuzzlegame
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

Very nice!
bobbybee
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

Panning blocks might be cool <3
Projector14
Scratcher
13 posts

Another amazing extension! ~ High quality sounds

I like your idea, bobbybee

Can you check out my scratch project as it took me hours and I would be really grateful if you could look at it. Thanks!

http://scratch.mit.edu/projects/48151432/
PullJosh
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

Very cool!
MegaApuTurkUltra
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

bobbybee wrote:

Panning blocks might be cool <3
Eh I don't want to fool with webaudio.

I could add
is sound [sound name] playing?::boolean extension
length of sound [sound name]::reporter extension
current position in sound [sound name]::reporter extension
set position in sound [sound name] to (10) seconds::extension
Want them?
Edit: added. It was trivial

Last edited by MegaApuTurkUltra (Feb. 19, 2015 17:56:06)

QuillzToxic
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

Add an upscaler, Make it an userscript
MegaApuTurkUltra
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

QuillzToxic wrote:

Add an upscaler, Make it an userscript
It sucks because using ScratchExtensions.loadExternalJs puts the extension info in the project JSON yet Scratch refuses to load it. A userscript is overkill, especially since you can't tell when exactly flash and the project are loaded.

A bookmarklet, maybe?

Edit: you know what might be a good idea? A userscript that reads project JSON and automatically loads extensions from info in there. Problem is, I don't know when flash is loaded. Any solutions?

Last edited by MegaApuTurkUltra (Feb. 20, 2015 00:09:29)

djdolphin
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

MegaApuTurkUltra wrote:

Edit: you know what might be a good idea? A userscript that reads project JSON and automatically loads extensions from info in there. Problem is, I don't know when flash is loaded. Any solutions?
This is the best I could do:
// ==UserScript==
// @name        Test
// @namespace   IDK
// @include     http://scratch.mit.edu/projects/*
// @version     1
// @grant       none
// @run-at      document-start
// ==/UserScript==
var consoleLog = console.log;
console.log = function() {
  consoleLog.apply(console, arguments);
  if (arguments[0] == 'Play recorded') ScratchExtensions.loadExternalJS('https://cdn.rawgit.com/MegaApuTurkUltra/Scratch-HQ-Sound/c9bc589fd176ba6f466b1d3141eb67370dfc0a0f/scratch-hqs.ext.js');
};
It loads the extension when the green flag is first pressed.
MegaApuTurkUltra
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

djdolphin wrote:

MegaApuTurkUltra wrote:

Edit: you know what might be a good idea? A userscript that reads project JSON and automatically loads extensions from info in there. Problem is, I don't know when flash is loaded. Any solutions?
This is the best I could do:
// ==UserScript==
// @name        Test
// @namespace   IDK
// @include     http://scratch.mit.edu/projects/*
// @version     1
// @grant       none
// @run-at      document-start
// ==/UserScript==
var consoleLog = console.log;
console.log = function() {
  consoleLog.apply(console, arguments);
  if (arguments[0] == 'Play recorded') ScratchExtensions.loadExternalJS('https://cdn.rawgit.com/MegaApuTurkUltra/Scratch-HQ-Sound/c9bc589fd176ba6f466b1d3141eb67370dfc0a0f/scratch-hqs.ext.js');
};
It loads the extension when the green flag is first pressed.
Hax. Hax everywhere
nXIII
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

// ==UserScript==
// @match http://scratch.mit.edu/*
// @run-at document-end
// ==/UserScript==
 
var script = document.createElement('script')
script.textContent = '('+function() {
  var old = JSsetProjectStats
  if (old) {
    var times = 0
    window.JSsetProjectStats = function() {
      old.apply(this, arguments)
      if (times++) console.log('project loaded')
    }
  }
}+')()'
document.body.appendChild(script)
This is in no way fragile
QuillzToxic
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

Any examples? Wanna test them on my Martin Logan system :3
MegaApuTurkUltra
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

QuillzToxic wrote:

Any examples? Wanna test them on my Martin Logan system :3
Load sound [swag] [af946297a5e07fefbaa785ade11bf1f0] ::extension // test sound I hacked onto the assets CDN
Play or resume sound [swag]::extension

nXIII wrote:

// ==UserScript==
// @match http://scratch.mit.edu/*
// @run-at document-end
// ==/UserScript==
 
var script = document.createElement('script')
script.textContent = '('+function() {
  var old = JSsetProjectStats
  if (old) {
    var times = 0
    window.JSsetProjectStats = function() {
      old.apply(this, arguments)
      if (times++) console.log('project loaded')
    }
  }
}+')()'
document.body.appendChild(script)
This is in no way fragile
Awesome! How does that work?

Last edited by MegaApuTurkUltra (Feb. 20, 2015 17:22:09)

comp09
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

MegaApuTurkUltra wrote:

nXIII wrote:

// ==UserScript==
// @match http://scratch.mit.edu/*
// @run-at document-end
// ==/UserScript==
 
var script = document.createElement('script')
script.textContent = '('+function() {
  var old = JSsetProjectStats
  if (old) {
    var times = 0
    window.JSsetProjectStats = function() {
      old.apply(this, arguments)
      if (times++) console.log('project loaded')
    }
  }
}+')()'
document.body.appendChild(script)
This is in no way fragile
Awesome! How does that work?

It redefines the browser function console.log as a function that examines each console log entry and passes it back to the original console.log. The Scratch project player calls console.log when the project is finished loading and when the user clicks the green flag.
comp09
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

Also, Scratch uses MD5 sums to identify assets. At the rate people are uploading to Scratch, there's bound to be a collision some time…

Maybe they should consider switching to SHA3 (Keccak)…
MegaApuTurkUltra
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

comp09 wrote:

Also, Scratch uses MD5 sums to identify assets. At the rate people are uploading to Scratch, there's bound to be a collision some time…

Maybe they should consider switching to SHA3 (Keccak)…
Yeah, and I hope they have a backup plan in case of a collision. I mean just imagine if someone found a collision for the hash of the default cat sprite. That would screw up 50% of all Scratch projects

comp09 wrote:

It redefines the browser function console.log as a function that examines each console log entry and passes it back to the original console.log. The Scratch project player calls console.log when the project is finished loading and when the user clicks the green flag.
No no, I know how yours works, just wondering how @nxIII's works.
nXIII
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

comp09 wrote:

MegaApuTurkUltra wrote:

Awesome! How does that work?
It redefines the browser function console.log as a function that examines each console log entry and passes it back to the original console.log. The Scratch project player calls console.log when the project is finished loading and when the user clicks the green flag.
Mine doesn't touch console.log, except to demonstrate that it knows the project has loaded.

It redefines the function JSsetProjectStats, which Scratch calls (via Flash's ExternalInterface.call) after it finishes loading the project. I happen to know this because I have access to the source code, but you can also figure it out by decompiling Scratch.swf. For various reasons, Flash actually calls JSsetProjectStats twice; the first call is too early, so it ignores it by counting (i.e., if (times++) …).

The DOM/Function::toString wrapper around the actual code is to get it to run in the same Window as the script that sets JSsetProjectStats. I realize now that the script should actually be:
// ==UserScript==
// @match http://scratch.mit.edu/*
// @run-at document-end
// ==/UserScript==
 
var script = document.createElement('script')
script.textContent = '('+function() {
  var old = window.JSsetProjectStats
  if (old) {
    var times = 0
    window.JSsetProjectStats = function() {
      old.apply(this, arguments)
      if (times++) console.log('project loaded')
    }
  }
}+')()'
document.body.appendChild(script)
…which should avoid throwing errors on non-project pages.
MegaApuTurkUltra
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

nXIII wrote:

comp09 wrote:

MegaApuTurkUltra wrote:

Awesome! How does that work?
It redefines the browser function console.log as a function that examines each console log entry and passes it back to the original console.log. The Scratch project player calls console.log when the project is finished loading and when the user clicks the green flag.
Mine doesn't touch console.log, except to demonstrate that it knows the project has loaded.

It redefines the function JSsetProjectStats, which Scratch calls (via Flash's ExternalInterface.call) after it finishes loading the project. I happen to know this because I have access to the source code, but you can also figure it out by decompiling Scratch.swf. For various reasons, Flash actually calls JSsetProjectStats twice; the first call is too early, so it ignores it by counting (i.e., if (times++) …).

The DOM/Function::toString wrapper around the actual code is to get it to run in the same Window as the script that sets JSsetProjectStats. I realize now that the script should actually be:
// ==UserScript==
// @match http://scratch.mit.edu/*
// @run-at document-end
// ==/UserScript==
 
var script = document.createElement('script')
script.textContent = '('+function() {
  var old = window.JSsetProjectStats
  if (old) {
    var times = 0
    window.JSsetProjectStats = function() {
      old.apply(this, arguments)
      if (times++) console.log('project loaded')
    }
  }
}+')()'
document.body.appendChild(script)
…which should avoid throwing errors on non-project pages.
You do know about unsafeWindow right?

// ==UserScript==
// @match http://scratch.mit.edu/*
// @run-at document-end
// @grant unsafeWindow
// ==/UserScript==
 
(function() {
  var old = unsafeWindow.JSsetProjectStats
  if (old) {
    var times = 0
    unsafeWindow.JSsetProjectStats = function() {
      old.apply(this, arguments)
      if (times++) console.log('project loaded')
    };
  }
})();
Why wouldn't this work?
comp09
Scratcher
1000+ posts

Another amazing extension! ~ High quality sounds

MegaApuTurkUltra wrote:

-snip-
You do know about unsafeWindow right?

// ==UserScript==
// @match http://scratch.mit.edu/*
// @run-at document-end
// @grant unsafeWindow
// ==/UserScript==
 
(function() {
  var old = unsafeWindow.JSsetProjectStats
  if (old) {
    var times = 0
    unsafeWindow.JSsetProjectStats = function() {
      old.apply(this, arguments)
      if (times++) console.log('project loaded')
    };
  }
})();
Why wouldn't this work?

unsafeWindow does not work in native Chrome userscripts, and TamperMonkey is disgustingly horrible. It's a slow and memory-leaky extension that makes tab processes consume more and more memory every page load.

Correct me if I'm wrong.

Powered by DjangoBB