Whistled Speech

I found out today about whistled languages, and I couldn't resist programming a basic Silbo Gomero reader.

Log in to post a comment.

// Very basic "Silbo Gomero" whistled speech generator
/*
This program converts a string of lowercase letters (a-z) and spaces
into an audio interpretation in Silbo Gomero whistle speech. 
Silbo Gomero is a whistled register of Spanish used by inhabitants 
of La Gomera in the Canary Islands to communicate across long distances. 

I've used a very basic mapping from Spanish phonemes into 
corresponding whistled ones, and the audio is generated using
a sine wave that is modulated in frequency and amplitude.

Some information on the phonetics of the Silbo language
can be found in:
https://en.wikipedia.org/wiki/Silbo_Gomero
http://silbo-gomero.com/Meyer-English.pdf
http://silbo-gomero.com/wlanguages.pdf
https://www.cambridge.org/core/services/aop-cambridge-core/content/view/A2829FE674349AD9A836982A23AA74B7/S0952675705000552a.pdf/phonological_and_phonetic_aspects_of_whistled_languages.pdf
*/

let sentence = "el silbo gomero es un lenguaje silbado practicado por algunos habitantes de canarias para comunicarse a traves de barrancos y valles";
//let sentence = "como estas";
//let sentence = "hola  estas alli    si  estoy aqi   esta jose alli   no  el no esta aqi";

ditty.bpm = 60; // 1 tick = 1 second

function nsin(a) {
    // Not quite a sinusoid
    let x = a - (~~a);
    //return Math.sin(2*Math.PI*x);
    return x*(x-1)*(x-0.5)*20.785;
    //x -= 0.5; x *= 2 * 3.0786; return x - x**3/6 + x**5/(1*2*3*4*5) - x**7/(1*2*3*4*5*6*7);
}

const chirpSynth = synth.def( class {
    constructor(o) {
        this.p = 0;
        this.f = o.f;
        this.g = 0; // gain
    }
    process(n,e,t,o) {
        let a0 = clamp01(ditty.dt / o.dt);
        this.f += a0 * (o.f - this.f);
        this.g = clamp01(this.g + 1.5*a0 * (2*o.g-1));
        this.p += this.f * ditty.dt;
        return nsin(this.p) * this.g;
    }
}, {f: 1400, g:1, dt:0.12, env:one, amp:0.1});




function rn(a) {
    // Random number between -a and +a
    return 2*a*(Math.random()-0.5);
}
function rrn(a) {
    // Relative random number between exp(-a) and exp(+a)
    return Math.exp(rn(a));
}


function getFreq(char) {
    // Get frequency corresponding to a given character sound
    
    // Vowels
    if(char == "u") {
        return 1450 + rn(50);
    } else if (char == "o") {
        return 1600+rn(120);
    } else if (char == "a") {
        return 1700+rn(100);
    } else if (char == "e") {
        return 2200+rn(300);
    } else if (char == "i") {
        return 2700+rn(120);
    // Consonants
    } else if (["d","j","l","n","r","s","t","y","z"].includes(char)) {
        return 3100+rn(100);
    } else if (["b","c","f","g","k","m","p","q","v","w","x"].includes(char)) {
        return 800+rn(50);
    }
    return null;
}

function getGain(char) {
    if("abdefgijlmnorsuvwyz".includes(char)) {
        return 1;
    }
    return 0;
}

function getDur(char) {
    if(char == " ") {
        return 0.5;
    } else if("aeiou".includes(char)) {
        return 0.4;
    } else if("stlckqj".includes(char)) {
        return 0.05;
    }
    return 0.12;
}

loop( () => {
    let prevdur = 1000;
    let s = chirpSynth.play({f:1400, g:0});
    for(let i=0; i<sentence.length; i++) {
        let char = sentence.charAt(i);
        let freq = getFreq(char);
        if(freq) {
            s.options.f = freq;
        }
        let gain = getGain(char);
        s.options.g = gain;
        let dur = getDur(char);
        s.options.dt = Math.min(dur, prevdur);
        sleep(dur);
        prevdur = dur;
    }
    s.options.g = 0;
    sleep(1000);
});