0

I am trying to write a morse code trainer that produces a random two letter pattern every 5 seconds with the audiocontext recreated each loop, but I cannot figure out how to add code which will call for a repeated loop. I've tried setTimeout() setInterval(), but they both eliminate the audio.

Also, after pressing the button five times on the following code. I get the error

" TypeError: null is not an object (evaluating 'ctx.currentTime')"

 <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
    <button onclick = "startIt()">Play</button>
    <button onclick = "stopIt()">Stop</button>
    <h2>Morse Code</h2>

    <h1 id="demo"></h1>
    <h1 id="demo2"></h1>

    <script>
    var codeStream = '';
    var dot = 1.2 / 15;
    var text = "";
    var display = "";
    var k = 0;
    var alphabet = [["A",".-"],["B","-..."],["C","-.-."],["D","-.."],["E","."],["F","..-."],["G","--."],["H","...."],["I",".."],["J",".---"],
        ["K","-.-"],["L",".-.."],["M","--"],["N","-."],["O","---"],["P",".--."],["Q","--.-"],["R",".-."],["S","..."],["T","-"],["U","..-"],
        ["V","...-"],["W",".--"],["X","-..-"],["Y","-.--"],["Z","--.."],["1",".----"],["2","..---"],["3","...--"],["4","....-"],["5","....."],
        ["6","-...."],["7","--..."],["8","---.."],["9","----."],["0","-----"],[".",".-.-.-"],[",","--..--"],["?","..--.."],["'",".----."],["!","-.-.--"],
        ["/","-..-."],[":","---..."],[";","-.-.-."],["=","-...-"],["-","-....-"],["_","..--.-"],["\"",".-..-."],["@",".--.-."],["(","-.--.-"],[" ",""]];

    stopIt = function(){
                ctx.close();
                location.reload();
            }

    function nextGroup() {
            for (i = 0; i < 2; i++){                
                var randomLetter = Math.floor(Math.random() * 26);
                var code = alphabet[randomLetter][1] + " ";
                var character = alphabet[randomLetter][0];      
                display += code;                    
                text += character;                  
            }
        codeStream = display;       
    }

    function startIt(){     
            var AudioContext = window.AudioContext || window.webkitAudioContext;
            var ctx = new AudioContext();
            var t = ctx.currentTime;
            var oscillator = ctx.createOscillator();        
            oscillator.type = "sine";
            oscillator.frequency.value = 600;
            oscillator.start();
            var gainNode = ctx.createGain();

            nextGroup();
            console.log(codeStream);
            document.getElementById("demo").innerHTML = text;
            document.getElementById("demo2").innerHTML = codeStream;
            display = "";
            text = "";                      
            gainNode.gain.setValueAtTime(0, t);

            for (var i = 0; i < codeStream.length; i++) {
                switch(codeStream.charAt(i)) {
                    case ".":
                        gainNode.gain.setValueAtTime(1, t);
                        t += dot;
                        gainNode.gain.setValueAtTime(0, t);
                        t += dot;
                        break;
                    case "-":
                        gainNode.gain.setValueAtTime(1, t);
                        t += 3 * dot;
                        gainNode.gain.setValueAtTime(0, t);
                        t += dot;
                        break;
                    case " ":
                        t += 7 * dot;
                        break;
                }           
            }

                gainNode.gain.setValueAtTime(0, t);
                t += 50 * dot;          

            oscillator.connect(gainNode);
            gainNode.connect(ctx.destination);          
            codeStream = '';                    
        oscillator.stop(t);         
        }                   
    </script>   
    </body>
</html>
Vikash Kumar
  • 642
  • 1
  • 11
  • 25
djl
  • 15
  • 4

1 Answers1

0

It looks like some of the issues are to do with scoping and state management of the oscillator. I wasn't able to reproduce the error you were seeing but the stopIt function certainly doesn't have access to ctx created in startIt.

An alternative might be to, rather than recreate the context, oscillator and gain node on each run, create them once and reuse them instead. Demo here: http://jsfiddle.net/kts74g0x/

The code:

const ALPHABET = [
  ["A", ".-"],
  ...
  [" ",""]
];
const DOT = 1;
const DASH = 3;
const NEXT = DOT;
const SPACE = 7;
const SPEED = 1.2 / 15;

const AudioContext = window.AudioContext || window.webkitAudioContext;

/**
 * Create a single audio context, oscillator and gain node and repeatedly
 * use them instead of creating a new one each time. The gain is just
 * silent most of the time.
 */
const ctx = new AudioContext();
const oscillator = ctx.createOscillator();
const gainNode = ctx.createGain();
oscillator.type = "sine";
oscillator.frequency.value = 600;
oscillator.connect(gainNode);
oscillator.start();
gainNode.connect(ctx.destination);
gainNode.gain.value = 0;

function playCodeStream(stream) {
  let t = ctx.currentTime;
  gainNode.gain.setValueAtTime(0, t);
  for (var i = 0; i < stream.length; i++) {
    switch(stream.charAt(i)) {
      case ".":
        gainNode.gain.setValueAtTime(1, t);
        t += DOT * SPEED;
        gainNode.gain.setValueAtTime(0, t);
        t += NEXT * SPEED;
        break;
      case "-":
        gainNode.gain.setValueAtTime(1, t);
        t += DASH * SPEED;
        gainNode.gain.setValueAtTime(0, t);
        t += NEXT * SPEED;
        break;
      case " ":
        t += SPACE * SPEED;
        break;
    }  
  }
}

/**
 * Set interval will wait initially for the period of
 * time before first triggering the function.
 */
setInterval(() => { playCodeStream([
  ALPHABET.filter(v => v[0] === "H"),
  ALPHABET.filter(v => v[0] === "E"),
  ALPHABET.filter(v => v[0] === "L"),
  ALPHABET.filter(v => v[0] === "L"),
  ALPHABET.filter(v => v[0] === "O")
].join(" ")); }, 10000);

Set interval returns an ID that can be passed to clearInterval to prevent future runs, the play button might start the interval and the stop button could clear it, for example.

For iOS there are restrictions so that an AudioContext cannot play sound unless it is in response to a user interaction (https://hackernoon.com/unlocking-web-audio-the-smarter-way-8858218c0e09). We can get around the problem by adding a button.

<button id="go">Go</button>

And checking the state of the audio context / starting the interval in response to clicking this button (demo: http://jsfiddle.net/7gfnrubc/). The updated code:

function next() {
  playCodeStream([
    ALPHABET.filter(v => v[0] === "H"),
    ALPHABET.filter(v => v[0] === "E"),
    ALPHABET.filter(v => v[0] === "L"),
    ALPHABET.filter(v => v[0] === "L"),
    ALPHABET.filter(v => v[0] === "O")
  ].join(" "));
}

function go() {
  if (ctx.state === 'suspended') {
    ctx.resume();
  }
  /**
   * Set interval will wait initially for the period of
   * time before first triggering the function. Can call
   * the function initially to start off.
   */
  next();
  setInterval(next, 10000);
}

const button = document.getElementById("go");
button.addEventListener("click", go);
Stuart Wakefield
  • 6,294
  • 24
  • 35
  • That seems like a very elegant solution. Thanks. – djl Jan 08 '19 at 22:19
  • I revised the code as you suggested, Stuart, and it works like a charm on my Mac. Unfortunately, like all of the versions which I devised on my own, there is no sound on my ios device. The original code I submitted did produce sound on the ios device at the first pass, but never when I looped it with setInterval(). Any ideas on how to get around this problem? – djl Jan 09 '19 at 05:32
  • iOS requires explicit user interaction to unsuspend the AudioContext, take a look at https://hackernoon.com/unlocking-web-audio-the-smarter-way-8858218c0e09. You could wrap the section of code with the set interval (and add in the context suspend / resume check in the article) with a function to be called by the play button. – Stuart Wakefield Jan 09 '19 at 08:53
  • I had already added a button, but adding the code to check the ctx state was the key missing element. Now the code works exactly as hoped. – djl Jan 10 '19 at 02:02