Wednesday, January 08, 2020

Auto Ring Tone Generation - Making Some Noise

A while back I heard not one, but two different iPhones use a morse-code sequence as a ringtone. I'm not sure if the tapped out message was static or changed with the caller, but I knew it was a concept I wanted to play with. If I could setup the morse code to match the in-coming caller ID, I'd have a custom ringtone for every user in my address book with little effort. Plus, I'd earn serious geek points!

While I had a vague idea of how I could use Tasker to make this idea go, there were still countless unanswered questions. Do I pre-generate the sounds files or create them on the fly? If Tasker can't associate a ringtone with a user, what app can? Is Tasker reliable and efficient enough to run a sound generation task as a call comes in and then kill that task when the call is answered? And on, and on.

However, before I started sorting all of this out I figured I'd start simpler: generate morse code tones from text. This would confirm that morse-code based ringtones are even worth pursuing and would let me get some important momentum with the idea.

When it comes to generating sound, it's hard to beat JavaScript's AudioContext for getting the job done. Because this code runs in any modern-day web browser, it's easy to write and test anywhere.

I've experimented with browser based audio before, yet that was a while ago. I found this tutorial helped get me back up to speed.

My approach to morse code generation was first to support playing a tone of 'units' length:

  // Inspired by:
  // https://modernweb.com/audio-synthesis-in-javascript/
  function tone(units) {
    var ctx = new (window.AudioContext || window.webkitAudioContext)();
    var osc = ctx.createOscillator();  
    var duration = units * tick;
    var attack = 1;
    var gain = ctx.createGain();
    
    gain.connect(ctx.destination);
    gain.gain.setValueAtTime(0, ctx.currentTime);
    gain.gain.linearRampToValueAtTime(1, ctx.currentTime + (attack / 1000));
    gain.gain.linearRampToValueAtTime(0, ctx.currentTime + (duration / 1000));

    osc.type = "sine";
    osc.frequency.value = units == 1 ? 440.000 : 880.0000;
    osc.connect(gain);

    osc.start();
    setTimeout(() => {
      osc.stop();
      osc.disconnect(gain);
      gain.disconnect(ctx.destination);
    }, duration);
  }

I then convert the text to a series of dots and dashes, and loop through each letter playing either a 1 or 3 unit tone (1 for dot, 3 for dash). This happens more or less in real-time, with each letter being played followed by a callback being registered to play the rest of the text 'snooze' seconds later.

  function play(code) {
    if(code.length > 0) {
      var c = code.shift();
      if(c == ' ') {
        var snooze = tick;
      } else {
        var units = (c == '.' ? 1 : 3)
        var snooze = tick * units;
        tone(units);
      }
      snooze += tick;
      setTimeout(() => {
        play(code);
      }, snooze);
    }
  }

You can experiment with result here: Auto-Ringtone. You can see complete code here.

While I'm sure I could get the morse code to sound better, I'm happy with the approach I've taken so far. Dots and dashes are not only different durations of sound, but also different frequencies. Next up: figuring out a ring tone strategy on my phone.

No comments:

Post a Comment