私たちの新しい翻訳には少し懐かしさがあります-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/でデモバージョンを試すこともでき ます。
ミューズがあなたを去り、音楽を書く気がまったくない場合は、いくつかの既存の曲を試して、おなじみのサウンドを楽しんでください。
- リングトーンノキア
- 現代の音楽がもっと好きならiPhoneのリングトーン
- 火をつけてください
- 没頭する
- 良い、悪い、そして醜い
- Rondo Alla Turca(モーツァルト)
ちなみに、本当に何か書いた場合は、URLを共有してください(すべての曲とBPMはURLのハッシュ部分に保存されるので、リンクをコピーしたりブックマークしたりするのと同じくらい簡単に曲の保存/共有ができます。
楽しんでいただけたでしょうか。 この記事を参照してくださいGithub、Twitterでニュースをフォローするか、 rssを介してサブスクライブ できます 。