Shared Scripts, Data, Stimuli, Files, and Demos

Hear sight 9 Run Experiment

Hear sight 9

Jaap Murre

The goal is to develop an application for blind people so that they can hear the patterns they are focussing on through saccadic movements or through head movements. The demo includes various shapes, colors, filter options, and games to experience the system. The system is also implemented in the Open Eye Tracker by Kexxu Robotics, a wearable affordable eye tracker.

No comments yet
 




Comments

No comments yet

If you are a registered user and signed in, you can here copy this script and its stimuli to your own account, where you can edit it and change it in any way you want.

It is absolutely free to register (no credit card info asked!). You can then instantly copy this experiment with one click and edit it, change its accompanying texts, its landing page, stimuli, etc. Invite your colleagues, friends, or students to check out your experiment. There is no limit on how many people can do your experiment, even with a free account.

The catch? There is no catch! Just keep in mind that with a free account, you cannot collect data. For teaching that is usually not a problem. For research, prepaid data collection (unlimited subjects) starts as low as €10.00 for a 10-day period.

 
var resolution = 1024; // Image is converted to this resolution

var title = addblock(10,0,80,10).text("Move (or click) the mouse for shape sound",60);
//var info = addblock(10,92,80,8); // Block to some some info


var icons = [
    addblock(10,92,8,8).icon('circle').on('click',function(){draw('circle')}),
    addblock(20,92,8,8).icon('circle').style('color','green').on('click',function(){draw('circle-colored')}),
    addblock(30,92,8,8).icon('circle-blank').on('click',function(){draw('circle-blank')}),
    addblock(40,92,8,8).icon('sign-blank').on('click',function(){draw('sign-blank')}),
    addblock(50,92,8,8).icon('unchecked').on('click',function(){draw('unchecked')}),
    addblock(60,92,8,8).icon('align-justify').on('click',function(){draw('align-justify')}),
    addblock(70,92,8,8).icon('font').on('click',function(){draw('font')}),
    addblock(80,92,8,8).icon('picture').on('click',function(){draw('picture')})
];

var picture_index = -1,
    pictures = [
        // "car.png",
        // "car_edges.png",
        "away-3408119_1920.jpg",
        "portrait-3083402_1920.jpg",
        "road-220058_1280.jpg",
        "street-4942809_1920.jpg"
    ]

function draw(what) {

window.last_draw_what = what;

    var canvas = document.getElementById('canvas'),
        ctx = canvas.getContext('2d'),
        name;

    ctx.clearRect(0,0,canvas.width,canvas.height); // Clear canvas
    ctx.fillStyle = "black"; // ("rgb(0,0,0)");
    ctx.fillRect(0,0,canvas.width,canvas.height); // Clear canvas

    switch (what) {
        case 'circle':
            ctx.fillStyle = "white"; 
            ctx.beginPath();
            ctx.arc(255,255,100,0,Math.PI*2);
            ctx.fill();
            ctx.beginPath();
            ctx.arc(1024-255,255,75,0,Math.PI*2);
            ctx.fill();
            ctx.beginPath();
            ctx.arc(255,1024-255,50,0,Math.PI*2);
            ctx.fill();
            ctx.beginPath();
            ctx.arc(1024-255,1024-255,25,0,Math.PI*2);
            ctx.fill();
        break;
        case 'circle-colored':
            ctx.fillStyle = "white"; 
            ctx.beginPath();
            ctx.arc(255,255,100,0,Math.PI*2);
            ctx.fill();
            ctx.fillStyle = "red"; 
            ctx.beginPath();
            ctx.arc(1024-255,255,100,0,Math.PI*2);
            ctx.fill();
            ctx.fillStyle = "green"; 
            ctx.beginPath();
            ctx.arc(255,1024-255,100,0,Math.PI*2);
            ctx.fill();
            ctx.fillStyle = "blue"; 
            ctx.beginPath();
            ctx.arc(1024-255,1024-255,100,0,Math.PI*2);
            ctx.fill();
        break;
        case 'circle-blank':
            ctx.strokeStyle = "white"; 
            ctx.lineWidth = 5;
            ctx.beginPath();
            ctx.arc(255,255,100,0,Math.PI*2);
            ctx.stroke();
            ctx.beginPath();
            ctx.arc(1024-255,255,75,0,Math.PI*2);
            ctx.stroke();
            ctx.beginPath();
            ctx.arc(255,1024-255,50,0,Math.PI*2);
            ctx.stroke();
            ctx.beginPath();
            ctx.arc(1024-255,1024-255,25,0,Math.PI*2);
            ctx.stroke();
        break;
        case 'sign-blank':
            ctx.fillStyle = "white"; 
            ctx.fillRect(255,255,200,200);
            ctx.fillRect(1024-255,255,75,75);
            ctx.fillRect(255,1024-255,50,50);
            ctx.fillRect(1024-255,1024-255,25,25);
        break;
        case 'unchecked':
            ctx.strokeStyle = "white"; 
            ctx.lineWidth = 5;
            ctx.strokeRect(255,255,200,200);
            ctx.strokeRect(1024-255,255,75,75);
            ctx.strokeRect(255,1024-255,50,50);
            ctx.strokeRect(1024-255,1024-255,25,25);
        break;
        case 'align-justify':
            ctx.fillStyle = "white"; 
            ctx.strokeStyle = "white"; 
            ctx.lineWidth = 3;
            ctx.beginPath();
            
            ctx.moveTo(50,50);
            ctx.lineTo(800,300);
            ctx.moveTo(50,300);
            ctx.lineTo(800,50);
            
            ctx.moveTo(50,600);
            ctx.lineTo(800,600);
            
            ctx.moveTo(900,900);
            ctx.lineTo(900,50);

            ctx.stroke();
        break;
        case 'font':
            ctx.fillStyle = "white"; 
            ctx.strokeStyle = "white"; 
            ctx.font = "200px Arial";
            ctx.fillText("A    B    C",50,200);
            ctx.fillText("a cat",50,500);
            ctx.strokeText("The fox",50,800);
        break;
        case 'picture':
            __js { var img = new Image(); }
            img.crossOrigin = 'anonymous'; 
            ++picture_index;
            name = pictures[picture_index];
            picture_index = (picture_index)%pictures.length;
            img.src = image.getRemoteFileName(name);              
            img.onload = function(){
                ctx2d.drawImage(img,0,0);
            };
        break;
    }
}

var b = addblock(10,10,80,80); // Canvas block
b.style("border","thin grey solid");
b.text('');
var canvas = document.getElementById('canvas');
var ctx2d = canvas.getContext('2d');


// Show a practice circle with sx slices:
var sx = 12;
for (var a = 0; a < sx; a++) { 
    ctx2d.beginPath();
    ctx2d.moveTo(256,256);    
    ctx2d.arc(256,256,100,2*Math.PI*a/sx-0.5*Math.PI,2*Math.PI*(a+1)/sx-0.5*Math.PI,false);
    ctx2d.closePath();
    ctx2d.fillStyle = 'rgba(' + parseInt(255*a/sx) + ',120,20,1)';
    ctx2d.fill();
}
      
      
function drawPixel(x,y,r,g,b,a) {
    
    a = a === undefined ? 1 : a;
    ctx2d.fillStyle = 'rgba(' + parseInt(r*255) 
                      + ',' + parseInt(g*255) 
                      + ',' + parseInt(b*255) 
                      + ',' + parseInt(a*255) 
                      + ')';

    ctx2d.fillRect(x,y,1,1);
}


function readArea(x,y,size) {
    var p = canvas.getContext('2d')
                  .getImageData(x - Math.floor(size/2),y - Math.floor(size/2),size,size)
                  .data; 
    return p;
}


function getMousePos(evt) {
  var rect = canvas.getBoundingClientRect(), // abs. size of element
      scaleX = canvas.width / rect.width,    // relationship bitmap vs. element for X
      scaleY = canvas.height / rect.height;  // relationship bitmap vs. element for Y

  return {
    x: (evt.clientX - rect.left) * scaleX,   // scale mouse coordinates after they have
    y: (evt.clientY - rect.top) * scaleY     // been adjusted to be relative to element
  }
}


function rgbToHex(r, g, b) {
    if (r > 255 || g > 255 || b > 255)
        throw "Invalid color component";
    return ((r << 16) | (g << 8) | b).toString(16);
}


var mouse_timer;

$('#canvas').mousemove(function(e) {
    clearTimeout(mouse_timer);
    mouse_timer = setTimeout(function(){
        if ($("#onmove").is(":checked")) {
            var pos = getMousePos(e);
            emitMouseMoveEnd(pos.x,pos.y,e);
            shapeSound(pos.x,pos.y);
        }
    },200);    
});


require(["dojo/_base/window"], function(win){
    window.win = win;
});

function emitMouseMoveEnd(x,y,e) {
    on.emit(win.body(),"mousemoveend",{   // Re-emit dojo-normalized events
        bubbles: true,
        cancelable: true,
        x: x,
        y: y,
        event: e
    });                                           
}


$('#canvas').click(function(e) {
    var pos = getMousePos(e);
    shapeSound(pos.x,pos.y);
});


$(window).blur(function(e) {
    fov && fov.stopAllOcsillators();
});


// Fovea class
function Fovea(/* Canvas 2D context */ctx,diameter,segments,threshold) {
    this.ctx = ctx;
    this.diameter = diameter || 256; // fovea diameter in pixels on image
    this.segments = segments || 24; // Number of segments of the circle
    this.threshold = 0.5;     // Don't include circles below this fraction of segments
    this.preCalculateFovea(); // Pre-calculate distances and reserve memory
}

// Pre-calculate distances and reserve memory
Fovea.prototype.preCalculateFovea = function() {
    var res = this.diameter,
        x, y, d,
        cx = Math.floor(res/2), 
        cy = cx, // Assume square image
        i, dsizes = new Array(cx+1);
        // res = resolution;
        
    var mapping = this.mapping = [];
    var circles = this.circles = { r: [], g: [], b: [], x: []}; // Provides memory to fill later

    for (x = 0; x <= cx; x++) { // circle data for each color channel
        mapping.push([]);
    }
    
    dsizes.fill(0);

    for (x = cx+1; x <= res; x++) { // Right upper  quadrant
        for (y = 0; y < cy; y++) { 
            // This can be made more efficient but since it is done only once,
            // we will leave this for later (cf. drawCircle for method):
            d = Math.floor(Math.sqrt((x-cx)*(x-cx)+(y-cy)*(y-cy)));
            
            if (d > cx) {
                continue; // Follow only the largest circle, not the corners
            }
            mapping[d].push([x,y]);
            ++dsizes[d];
        }
    }
    
    for (x = res; x > cx; x--) { // Right lower quadrant
        for (y = cy; y < res; y++) { 
            d = Math.floor(Math.sqrt((x-cx)*(x-cx)+(y-cy)*(y-cy)));
            
            if (d > cx) {
                continue; // Follow only the largest circle, not the corners
            }
            mapping[d].push([x,y]);
            ++dsizes[d];
        }
    }

    for (x = cx; x >= 0; x--) { // Left lower quadrant
        for (y = res; y >= cy; y--) {
            d = Math.floor(Math.sqrt((x-cx)*(x-cx)+(y-cy)*(y-cy)));

            if (d > cx) {
                continue;
            }
            mapping[d].push([x,y]);
            ++dsizes[d];
        }
    }

    for (x = 0; x <= cx; x++) { // Left upper quadrant
        for (y = cy-1; y >= 0; y--) {
            d = Math.floor(Math.sqrt((x-cx)*(x-cx)+(y-cy)*(y-cy)));

            if (d > cx) {
                continue;
            }
            mapping[d].push([x,y]);
            ++dsizes[d];
        }
    }

    for (i = 0; i < dsizes.length; i++)
    {
        __js { // SJS construct, see https://conductance.io/reference/#sjs:%23language/syntax::__js
            circles.r.push(new Uint8Array(dsizes[i]));
            circles.g.push(new Uint8Array(dsizes[i]));
            circles.b.push(new Uint8Array(dsizes[i]));
        }
    }
}

// Make array `s` have length `size` by duplication and interpolation
Fovea.prototype.resample = function(/* array */s,size) {
    var ss = [], sss = [], newlength, t, steps, i, j, r, mult, step, a;

    // Up-sample
    if (s.length < size) {
        newlength = size;
        
        if (size/s.length > 2) {
            a = Math.floor(size/s.length);
            s2 = [];
            for (i = 0; i < s.length; i++) { // Up-sample very small arrays
                for (j = 0; j < a; j++) {
                    s2.push(s[i]);
                }
            }
            s = s2; // Continue as s
        }
        
    } else {
        newlength = (Math.floor(s.length/size) + 1)*size;
    }
    
    t = s.length/(newlength-s.length);
    steps = [];
    // Locations where to insert padding numbers
    for (i = 0; i < newlength-s.length; i++) {
        steps.push(Math.round(i*t,0));
    }
    
    // Insert padding numbers (average of left and right number)
    for (i = 0, step = 0; i < s.length; i++) {
        ss.push(s[i]);
        
        if (steps[step] === i) {
            ss.push(s[i+1] !== undefined ? (s[i]+s[i+1])/2 : s[i]);
            ++step;
        }
    }
    
    if (ss.length > s.length) {
        // Down-sample to length size
        mult = ss.length/size; // Should be an integer
        for (i = 0; i < ss.length; i += mult) {
            r = 0;
            for (j = i; j < i+mult; j++)
            r += ss[j];
            sss.push(r/mult);
        }
    }
    
    return sss;
}    

// Expand circles outside foveola exponentially
function grow(x,retina_radius,d,growth,fovea_radius) { 

    fovea_radius = fovea_radius || retina_radius/2;
    
    var retval;
    
    if (   grow._cache === undefined 
        || grow._cache.growth !== growth 
        || grow._cache.retina_radius !== retina_radius
        || grow._cache.fovea_radius !== fovea_radius
    ) {
        grow._cache = {
            retina_radius: retina_radius,
            growth: growth,
            fovea_radius: fovea_radius,
            d: {}
        }
    } 
    
    retval = grow._cache[d] && grow._cache[d][x];
    
    if (retval !== undefined) {
        return retval;
    } else {
        retval = d < fovea_radius ? x : 
            retina_radius + Math.floor((x-retina_radius)*Math.pow(1 + growth,d-fovea_radius)); 
        if (grow._cache[d] === undefined) {
            grow._cache[d] = {};
        }
        grow._cache[d][x] = retval; // Cache answer
        return retval;
    }
}


Fovea.prototype.initSegmentXs = function(growth,fovea_x) {
    var i, j, x, xx, d,
        retina_radius = this.diameter/2,
        fovea_radius = retina_radius/2;
        mapping = this.mapping,
        growth = parseFloat($("#growth").val());
        
    if (this.growth === growth) { // Used cached value until change in growth
        return;
    }
    
    // Calculated cached array
    this.segmentX = [];
    
    for (i = 0; i < mapping.length; i++) {
        this.segmentX.push([]);
        for (j = 0; j < this.segments; j++) {
            x = Math.cos((j - this.segments/4)*(1/this.segments)*2*Math.PI)*i;
            if (i > fovea_radius) {
                xx = x*Math.pow(1.0 + growth,i - fovea_radius); 
            } else {
                xx = x;
            }
            xx = fovea_x - this.diameter/2 + xx;
            this.segmentX[i].push(xx);
        }
    }
}




// Convert image of size diameter centered at (fovea_x,fovea_y) into circles
Fovea.prototype.getSpectrum = function(fovea_x,fovea_y) 
{ 
    fovea_x = Math.floor(fovea_x);
    fovea_y = Math.floor(fovea_y);
    
    var x, y, i, j, len, mp, offset, 
        circles = this.circles, 
        diameter = this.diameter,
        mapping = this.mapping,
        growth = parseFloat($("#growth").val()),
        // image = readArea(fovea_x,fovea_y,diameter),
        image = canvas.getContext('2d')
                  .getImageData(0,0,resolution,resolution)
                  .data; 
        
    for (i = 0; i < mapping.length; i++) {
        
        len = mapping[i].length;
        mp = mapping[i];
        
        for (j = 0; j < len; j++) {
            
            x = grow(mp[j][0],diameter/2,i,growth);
            y = grow(mp[j][1],diameter/2,i,growth);
            
            x = fovea_x - diameter/2 + x;
            y = fovea_y - diameter/2 + y;

            if ($("#draw_waves").is(":checked")) {
                drawPixel(x,y,0.5,0.5,0,0.2);
            }        

            if (x < 0 || x > resolution || y < 0 || y > resolution) {
                circles.r[i][j] = 0;
                circles.g[i][j] = 0;
                circles.b[i][j] = 0;
            } else {
                // We can do sharpening or edge detection here
                // circles.e[i][j] for edge octave above r
                
                offset = x*4 + y*resolution*4;
                
                circles.r[i][j] = image[offset];
                circles.g[i][j] = image[offset + 1];
                circles.b[i][j] = image[offset + 2];
               

// TODO (***): circles.edge[i][j] = // calc edge here

// if (i === 40) {
//      console.log(fovea_x,fovea_y,x,y,image[x*4 + y*resolution*4 + 1]);
// }            

            }

        }
    }

    var spectrum = {r: [], g: [], b: [], x: []}, offset = 0;

    // Resample to length `this.segments`        
    for (i = 0; i < mapping.length; i++) {

        if (circles.r[i].length >= this.segments*this.threshold) {
            spectrum.r.push(this.resample(circles.r[i],this.segments));
            spectrum.g.push(this.resample(circles.g[i],this.segments));
            spectrum.b.push(this.resample(circles.b[i],this.segments));
        } else {
            ++offset;
        }
    }

    this.blind_spot = offset;

    return spectrum;
}


// Combines `frame_size` consecutive rows and averages these
Fovea.prototype.condense = function(/*array of arrays*/s,frame_size) {
    
    var i, j, counter, current, result = [];
    
    for (i = 0, step = 0; i < s.length; i++) {

        if (!(i%frame_size)) {
            counter = 1;
            current = new Array(s[i].length).fill(0);
            result.push(current);
        } 

        for (j = 0; j < s[i].length; j++) {
            current[j] += s[i][j];
        }        
        
        if (!((i+1)%frame_size) || i === s.length-1) {
            for (j = 0; j < s[i].length; j++) {
                current[j] /= counter;
            }
        }
        
        ++counter;
    }
    
    return result;
}


Fovea.prototype.convertSpectra = function(spectra,frame_size,pitch) {
    frame_size = frame_size || 8;
    pitch = pitch || 220;
    
    return {
        r: this.condense(spectra.r,frame_size),
        g: this.condense(spectra.g,frame_size),
        b: this.condense(spectra.b,frame_size)
    };
}


__js {var ctx = new (AudioContext || webkitAudioContext)(); }

Fovea.prototype.stopAllOcsillators = function() { // Silence and stop all oscillators
        for (var i = 0; i < all_osc.length; i++) {
            all_osc[i].gain_node.gain.setTargetAtTime(0,ctx.currentTime, 0.003);
        }
        await(15);
        for (i = 0; i < all_osc.length; i++) {
            all_osc[i].osc_node.stop();
        }
    }


Fovea.prototype.playspectra = function(spectra,x,/*ms*/duration,pitch) {

    pitch = pitch || 220;

    function addOscillator() {
        var osc = ctx.createOscillator('sine'); // sine (def), sawtooth, square, triangle
        var gain = ctx.createGain();
        gain.value = 0;
        var panner = ctx.createStereoPanner();
        osc.connect(panner);
        panner.connect(gain);
        gain.connect(ctx.destination);
        return { osc_node: osc, gain_node: gain, panner_node: panner }
    }

    if (window.all_osc) {
        this.stopAllOcsillators();
    }
    window.all_osc = []; // all_osc is global in script

    var osc, f, i, 
        value, pan, xx,
        color, colors = ['r','g','b'], // maybe add 'edge' in front
        no_of_frames = spectra.r.length;
        no_of_segments = colors.length, // one for each each color (or edge)        
        segments = spectra.r[0].length,
        frame_size = parseFloat($("#frame_size").val());

    this.initSegmentXs(growth,x,no_of_frames);        


    // (***) Here I can change the pitch location on the circle

    for (i = segments*no_of_segments-1; i >= 0; i--) { 
        osc = addOscillator();
        all_osc.push(osc);
        tone = pitch * Math.pow( Math.pow(2,1/segments),i);
        osc.osc_node.frequency.value = tone;
        osc.gain_node.gain.value = 0;
        osc.osc_node.start();
    }

    osc = addOscillator(); // Reference sound at half base pitch
    all_osc.push(osc);
    tone = (pitch/2) * Math.pow( Math.pow(2,1/segments),0);
    osc.osc_node.frequency.value = tone;
    osc.gain_node.gain.value = 0.025;
    osc.osc_node.start();

    for (f = 0; f < no_of_frames; f++) {

        for (i = 0; i < segments*no_of_segments; i++) {
            color = colors[Math.floor(i/segments)];
            value = spectra[color][f][i%segments]/255/segments;
            all_osc[i].gain_node.gain.setTargetAtTime(value,ctx.currentTime,0.003);
            xx = this.segmentX[(this.blind_spot||0) + f*frame_size][i%segments];
            pan = Math.max(Math.min(2*xx/resolution - 1,1),-1);
            all_osc[i].panner_node.pan.setValueAtTime(pan,ctx.currentTime); 
        }
        
        await(duration);
    }

    this.stopAllOcsillators();
}


// Defaults Canvas 2D context ctx, diameter = 128, segments = 24, threshold = 0.5
var fov = new Fovea(ctx2d,128,24); 

function shapeSound(x,y) {
    var pitch = $("#pitch").val(),
        frame_size = $("#frame_size").val(),
        frame_length = $("#frame_length").val(),
        ground = $("#ground").val(),
        t1, t2, t3;
    
    t1 = +new Date();
    var result = fov.getSpectrum(x,y); 
    t2 = +new Date();
    var gains = fov.convertSpectra(result,frame_size,pitch); // Frame size, base pitch
    console.log("getSpectrum: ",t2-t1,"ms");

    fov.playspectra(gains,x,frame_length,pitch); // frame length in ms, ground pitch of FFT
}


function renderImage() {
    var filter = $("#filter").val();
    if (filter != 'none') {
        console.log("Start filter processing");
        var b = addblock("center","center",30,30).style("color","white")
                    .text(' Processing...');
        await(500);
        var img_data = LenaJS[filter].apply(null,[ctx2d.getImageData(0,0,canvas.width,canvas.height)]);
        b.destroy();
        console.log("Completed filter processing: ",img_data);
        ctx2d.putImageData(img_data,0,0);
    } else {
        if (window.last_draw_what) {
            if (window.last_draw_what !== 'picture') {
                draw(window.last_draw_what);
            } else {
                if (picture_index >= 0) {
                    __js { var img = new Image(); }
                    img.crossOrigin = 'anonymous'; 
                    name = pictures[picture_index];
                    img.src = image.getRemoteFileName(name);              
                    img.onload = function(){
                        ctx2d.drawImage(img,0,0);
                    };
                }
            }
        }
    }
}


var b_pitch        = addblock(91,10,9,10).align("left")
              .text('',50),
    b_frame_size   = addblock(91,20,9,10).align("left")
              .text('',50),
    b_frame_length = addblock(91,30,9,10).align("left")
              .text('',50),
    b_growth       = addblock(91,40,9,10).align("left")
              .text('',50),
    b_onmove       = addblock(91,55,9,10).align("left")
              .text('',50),
    b_draw_waves   = addblock(91,60,9,10).align("left")
              .text('',50)
;
   

LenaJS.laplacianBlend = function(pixels) {
  var operator = [ 0, -1,    0, // Blends with darkened part of the original
                  -1,  4.1, -1,
                   0, -1,    0 ]
  return LenaJS.convolution(pixels, operator)
}
   
LenaJS.outline = function(pixels) {
  var operator = [-1, -1,   -1, // outline filter (not in LenaJS)
                  -1,  8,   -1,
                  -1, -1,   -1 ]
  return LenaJS.convolution(pixels, operator)
}

LenaJS.outlineBlend = function(pixels) {
  var operator = [-1, -1,   -1, // outline blend filter (not in LenaJS)
                  -1,  8.25, -1, 
                  -1, -1,   -1 ]
  return LenaJS.convolution(pixels, operator)
}


              
var filter_select = `
`;

for (var i = 0, s = ""; i < filter_select.parts.length; i++) {
    s += filter_select.parts[i];
}

var b_filter = addblock(91,70,9,10).align("left").text(s,50);

$(document).ready(function(){
    $("#filter").change(function(){
        renderImage();            
    });
});

setup_lessons();

awaitkey("ESCAPE");


Data inspection is forthcoming!

In the mean time, authors may download their own data and make it available as an Excel file. Check out the 'Stimuli and Files' tab.

Click on a category to view the stimuli and files

You can download the files shown here by clicking on the file names or image. Note that you cannot link directly to the images, sounds, videos, etc. shown here from other web pages; the link will go stale in about one hour and will no longer work after that.

You can download the files shown here by clicking on the file names or image. Note that you cannot link directly to the images, sounds, videos, etc. shown here from other web pages; the link will go stale in about one hour and will no longer work after that.

You can download the files shown here by clicking on the file names or image. Note that you cannot link directly to the images, sounds, videos, etc. shown here from other web pages; the link will go stale in about one hour and will no longer work after that.

Related Experiments