512バイトのNokiaComposer Ringtone Synthesizer

私たちの新しい翻訳には少し懐かしさがあります-NokiaComposerを書いて、私たち自身のメロディーを作ろうとしています。


読者の中に、モデル3310や3210などの古いNokiaを使用した人はいますか?その優れた機能、つまり電話のキーボードで独自の着信音を作成する機能を覚えておく必要があります。ノートやポーズを好きな順番に並べることで、電話のスピーカーから人気のメロディーを演奏したり、友達と共有したりすることもできます!あなたがその時代を逃したならば、これはそれがどのように見えたかです:







感動しませんでしたか?私を信じてください、それは当時、特に音楽に興味のある人にとっては本当にクールに聞こえました。



Nokia Composerで使用される音楽表記(音楽表記)および形式は、RTTTL(リングトーンテキスト転送言語)として知られています。 RTTLは、Arduinoなどでモノフォニックなメロディーを演奏するためにアマチュアによって今でも広く使用されてい



ます。RTTTLを使用すると、1つの声だけで音楽を書くことができ、音符はコードやポリフォニーなしで順番に演奏できます。ただし、このような形式は書き込みと読み取りが簡単で、分析と再現が簡単であるため、この制限はキラー機能であることが判明しました。



この記事では、JavaScriptでRTTTLプレーヤーを作成し、ゴルフのコードと数学を少し追加して、コードをできるだけ短くして楽しみます。



RTTTLの解析



RTTTLの場合、正式な文法が使用されます。 RTTL形式は、メロディーの名前、テンポ(BPM-1分あたりの拍数、つまり1分あたりの拍数)などの特性、音符のオクターブと長さ、およびメロディーコード自体の3つの部分で構成される文字列です。ただし、Nokia Composer自体の動作をシミュレートし、メロディの一部のみを解析し、BPMテンポを個別の入力パラメーターと見なします。メロディーの名前とそのサービス特性は、この記事の範囲外です。



メロディーは、追加のスペースを含むコンマで区切られた、単なる一連のメモ/休符です。各ノートは、長さ(2/4/8/16/32/64)、ピッチ(c / d / e / f / g / a / b)、オプションでシャープ(#)、オクターブ数(1から)で構成されます。 3オクターブしかサポートされていないため、3になります)。



最も簡単な方法は、正規の式を使用 することです。新しいブラウザには、文字列内のすべての一致のセットを返す非常に便利なmatchAll関数 付属しています。



const play = s => {
  for (m of s.matchAll(/(\d*)?(\.?)(#?)([a-g-])(\d*)/g)) {
    // m[1] is optional note duration
    // m[2] is optional dot in note duration
    // m[3] is optional sharp sign, yes, it goes before the note
    // m[4] is note itself
    // m[5] is optional octave number
  }
};
      
      





各音符について最初に理解することは、それを音波の周波数に変換する方法です。もちろん、7つのノート文字すべてに対してHashMapを作成できます。しかし、これらの文字は順番に並んでいるので、数字として考える方が簡単なはずです。文字メモごとに、対応する数字の文字コード(ASCIIコード )があります。「A」の場合、これは0x41になり、「a」の場合、これは0x61になります。「B / b」の場合は0x42 / 0x62になり、「C / c」の場合は0x43 / 0x63になります。



// 'k' is an ASCII code of the note:
// A..G = 0x41..0x47
// a..g = 0x61..0x67
let k = m[4].charCodeAt();
      
      





おそらく最上位ビットをスキップする必要があります。ノートインデックスとしてkと7のみを使用し ます(a = 1、c = 2、…、g = 7)。次は何ですか?次の段階は音楽理論に関連しているので、あまり楽しいものではありません。ノートが7つしかない場合は、12個すべてとしてカウントします。これは、シャープ/フラットノートが通常のノートの間に不均一に隠れているためです。



         A#        C#    D#       F#    G#    A#         <- black keys
      A     B | C     D     E  F     G     A     B | C   <- white keys
      --------+------------------------------------+---
k&7:  1     2 | 3     4     5  6     7     1     2 | 3
      --------+------------------------------------+---
note: 9 10 11 | 0  1  2  3  4  5  6  7  8  9 10 11 | 0
      
      





ご覧のとおり、オクターブ単位のノートインデックスはノートコード(k&7)よりも速く増加します。さらに、それは非線形に増加します。EとFの間、またはBとCの間の距離は、残りの音符の間のように2ではなく、1セミトーンです。



直感的には、(k&7)に12/7(12セミトーンと7ノート)を掛けてみることができます。



note:          a     b     c     d     e      f     g
(k&7)*12/7: 1.71  3.42  5.14  6.85  8.57  10.28  12.0
      
      





小数点以下の数字のないこれらの数値を見ると、予想どおり、非線形であることがすぐにわかります。



note:                 a     b     c     d     e      f     g
(k&7)*12/7:        1.71  3.42  5.14  6.85  8.57  10.28  12.0
floor((k&7)*12/7):    1     3     5     6     8     10    12
                                  -------
      
      





しかし、実際にはそうではありません...「ハーフトーン」距離は、C / D間ではなくB / CとE / Fの間にある必要があります。他の比率を試してみましょう(下線はセミトーンを示します)。



note:              a     b     c     d     e      f     g
floor((k&7)*1.8):  1     3     5     7     9     10    12
                                           --------

floor((k&7)*1.7):  1     3     5     6     8     10    11
                               -------           --------

floor((k&7)*1.6):  1     3     4     6     8      9    11
                         -------           --------

floor((k&7)*1.5):  1     3     4     6     7      9    10
                         -------     -------      -------
      
      





1.8と1.5の値が適切でないことは明らかです:最初の値はセミトーンが1つだけで、2番目の値は多すぎます。他の2つ、1.6と1.7は、私たちに適しているようです。1.7はメジャースケールGA-BC-D-EFを示し、1.6はメジャースケールAB-CD-EFGを示します。必要なものだけ!



ここで、Cが0、Dが2、Eが4、Fが5などになるように、値を少し変更する必要があります。4セミトーンオフセットする必要がありますが、4を引くと、AノートがCノートの下になります。したがって、値が1オクターブ外の場合は、代わりに8を加算し、モジュロ12を計算します。



let n = (((k&7) * 1.6) + 8) % 12;
// A  B C D E F G A  B C ...
// 9 11 0 2 4 5 7 9 11 0 ...
      
      





また、通常の式のm [3]グループによってキャッチされる「シャープ」な文字も考慮する必要があります。存在する場合は、ノート値を1セミトーン増やします。



// we use !!m[3], if m[3] is '#' - that would evaluate to `true`
// and gets converted to `1` because of the `+` sign.
// If m[3] is undefined - it turns into `false` and, thus, into `0`:
let n = (((k&7) * 1.6) + 8)%12 + !!m[3];

      
      





最後に、正しいオクターブを使用する必要があります。オクターブは、正規表現グループm [5]に数値としてすでに格納されています。音楽理論によれば、各オクターブは12セミノットであるため、オクターブ数に12を掛けて、ノート値に追加することができます。



// n is a note index 0..35 where 0 is C of the lowest octave,
// 12 is C of the middle octave and 35 is B of the highest octave.
let n =
  (((k&7) * 1.6) + 8)%12 + // note index 0..11
  !!m[3] +                 // semitote 0/1
  m[5] * 12;               // octave number
      
      





クランプ



誰かがオクターブの数を10または1000と指定するとどうなりますか?これは超音波につながる可能性があります!そのようなパラメータには正しい値のセットのみを許可する必要があります。他の2つの間の数を制限することは、一般に「クランプ」と呼ばれます。最新のJSには特別な関数 Math.clamp(x、low、high)がありますが、ほとんどのブラウザーではまだ使用できません。最も簡単な代替方法は、次を使用することです。



clamp = (x, a, b) => Math.max(Math.min(x, b), a);
      
      





しかし、コードをできるだけ小さくしようとしているので、ホイールを作り直して、数学関数の使用をやめることができます。デフォルトのx = 0を使用して、 未定義の値でもクランプを機能させ ます:



clamp = (x=0, a, b) => (x < a && (x = a), x > b ? b : x);

clamp(0, 1, 3) // => 1
clamp(2, 1, 3) // => 2
clamp(8, 1, 3) // => 3
clamp(undefined, 1, 3) // => 1
      
      





テンポと期間に注意してください



BPMがパラメータとしてoutplay ()関数に渡されることを期待しています 検証する必要があります。



bpm = clamp(bpm, 40, 400);
      
      





ここで、ノートの持続時間を秒単位で計算するために、音楽の長さ(全体/半分/四半期/…)を取得できます。これは、regexグループm [1]に格納されています。次の式を使用します。



note_duration = m[1]; // can be 1,2,4,8,16,32,64
// since BPM is "beats per minute", or usually "quarter note beats per minute",
// BPM/4 would be "whole notes per minute" and BPM/60/4 would be "whole
// notes per second":
whole_notes_per_second = bpm / 240;
duration = 1 / (whole_notes_per_second * note_duration);
      
      





これらの式を1つに組み合わせて、メモの長さを制限すると、次のようになります。



// Assuming that default note duration is 4:
duration = 240 / bpm / clamp(m[1] || 4, 1, 64);
      
      





また、ドットでノートを指定する機能を忘れないでください。これにより、現在のノートの長さが50%長くなります。グループm [2]があり、その値はポイントになり ます。または 未定義以前にシャープサインに使用したのと同じ方法を適用すると、次のようになります。



// !!m[2] would be 1 if it's a dot, 0 otherwise
// 1+!![m2]/2 would be 1 for normal notes and 1.5 for dotted notes
duration = 240 / bpm / clamp(m[1] || 4, 1, 64) * (1+!!m[2]/2);
      
      





これで、各ノートの数と期間を計算できます。WebAudioAPIを使用して曲を再生する ときが来ました



WEBAUDIO



WebAudio API全体から必要なのは、オーディオコンテキスト、音波を処理するためのオシレーター、およびサウンドをオン/オフする ためのゲインノードの3つの部分だけです 。長方形のオシレーターを使用して、そのひどい古い電話が鳴っているようなメロディーの音を出します。



// Osc -> Gain -> AudioContext
let audio = new (AudioContext() || webkitAudioContext);
let gain = audio.createGain();
let osc = audio.createOscillator();
osc.type = 'square';
osc.connect(gain);
gain.connect(audio.destination);
osc.start();
      
      





このコード自体はまだ音楽を作成しませんが、RTTTLメロディーを解析したので、どのノートをいつ、どの周波数で、どのくらいの時間再生するかをWebAudioに伝えることができます。



すべてのWebAudioノードには、値変更イベント(周波数またはノードゲイン)をスケジュールする特別なsetValueAtTimeメソッド があります。



思い出してください。この記事の前半では、メモのASCIIコードがkとして保存され、メモのインデックスがnとして保存されており、メモの 長さは秒単位で示されていました。これで、ノートごとに次のことができます。



t = 0; // current time counter, in seconds
for (m of ......) {
  // ....we parse notes here...

  // Note frequency is calculated as (F*2^(n/12)),
  // Where n is note index, and F is the frequency of n=0
  // We can use C2=65.41, or C3=130.81. C2 is a bit shorter.
  osc.frequency.setValueAtTime(65.4 * 2 ** (n / 12), t);
  // Turn on gain to 100%. Besides notes [a-g], `k` can also be a `-`,
  // which is a rest sign. `-` is 0x2d in ASCII. So, unlike other note letters,
  // (k&8) would be 0 for notes and 8 for rest. If we invert `k`, then
  // (~k&8) would be 8 for notes and 0 for rest. Shifing it by 3 would be
  // ((~k&8)>>3) = 1 for notes and 0 for rests.
  gain.gain.setValueAtTime((~k & 8) >> 3, t);
  // Increate the time marker by note duration
  t = t + duration;
  // Turn off the note
  gain.gain.setValueAtTime(0, t);
}
      
      





それがすべてです。私たちのplay()プログラムは、RTTTL表記で書かれたメロディー全体を再生できるようになりました。これは完全なコードであり、setValueAtTimeのショートカットとしてv使用したり、1文字の変数を使用したりする などのマイナーな説明があり ます(C =コンテキスト、z =同様のサウンドを生成するため、オシレーター、g =ゲイン、q = bpm、c =クランプ):



c = (x=0,a,b) => (x<a&&(x=a),x>b?b:x); // clamping function (a<=x<=b)
play = (s, bpm) => {
  C = new AudioContext;
  (z = C.createOscillator()).connect(g = C.createGain()).connect(C.destination);
  z.type = 'square';
  z.start();
  t = 0;
  v = (x,v) => x.setValueAtTime(v, t); // setValueAtTime shorter alias
  for (m of s.matchAll(/(\d*)?(\.?)([a-g-])(#?)(\d*)/g)) {
    k = m[4].charCodeAt(); // note ASCII [0x41..0x47] or [0x61..0x67]
    n = 0|(((k&7) * 1.6)+8)%12+!!m[3]+12*c(m[5],1,3); // note index [0..35]
    v(z.frequency, 65.4 * 2 ** (n / 12));
    v(g.gain, (~k & 8) / 8);
    t = t + 240 / bpm / (c(m[1] || 4, 1, 64))*(1+!!m[2]/2);
    v(g.gain, 0);
  }
};

// Usage:
play('8c 8d 8e 8f 8g 8a 8b 8c2', 120);
      
      





terserで縮小した 場合、このコードはわずか417バイトです。これはまだ512バイトのしきい値を下回っています。再生を中断 するためにstop()関数を追加してみませんか?



C=0; // initialize audio conteext C at the beginning with zero
stop = _ => C && C.close(C=0);
// using `_` instead of `()` for zero-arg function saves us one byte :)
      
      





これはまだ約445バイトです。このコードを開発者コンソールに貼り付けると、JS関数play()および stop()を呼び出すことにより、RTTTLを再生して再生を停止できます



UI



シンセサイザーに少しUIを追加することで、音楽をさらに楽しくする瞬間が生まれると思います。この時点で、コードゴルフを忘れることをお勧めします。通常のHTMLとCSSを使用してバイトを保存せずに、再生専用の縮小スクリプトを含めずに、RTTTLリングトーン用の小さなエディターを作成することができます。



かなり退屈なので、ここにコードを投稿しないことにしました。githubで見つけることができます https://zserge.com/nokia-composer/でデモバージョンを試すこともでき ます







ミューズがあなたを去り、音楽を書く気がまったくない場合は、いくつかの既存の曲を試して、おなじみのサウンドを楽しんでください。





ちなみに、本当に何か書いた場合は、URLを共有してください(すべての曲とBPMはURLのハッシュ部分に保存されるので、リンクをコピーしたりブックマークしたりするのと同じくらい簡単に曲の保存/共有ができます。



楽しんでいただけたでしょうかこの記事を参照してくださいGithubTwitterでニュースをフォローするrssを介してサブスクライブ できます



All Articles