Ein bisschen Nostalgie in unserer neuen Übersetzung - versuchen Sie, Nokia Composer zu schreiben und unsere eigene Melodie zu komponieren.
Hat einer Ihrer Leser ein altes Nokia verwendet, zum Beispiel die Modelle 3310 oder 3210? Sie sollten sich an die großartige Funktion erinnern - die Möglichkeit, Ihre eigenen Klingeltöne direkt auf der Telefontastatur zu erstellen. Indem Sie Notizen und Pausen in der gewünschten Reihenfolge anordnen, können Sie eine beliebte Melodie über den Telefonlautsprecher abspielen und die Kreation sogar mit Freunden teilen! Wenn Sie diese Ära verpasst haben, sah es so aus:

Nicht beeindruckt? Vertrau mir einfach, es klang damals wirklich cool, besonders für diejenigen, die sich für Musik interessierten.
Die in Nokia Composer verwendete Musiknotation (Musiknotation) und das Format werden als RTTTL (Ring Tone Text Transfer Language) bezeichnet. RTTL wird von Amateuren immer noch häufig verwendet, um monophone Melodien auf Arduino usw. zu spielen. Mit
RTTTL können Sie Musik für nur eine Stimme schreiben. Noten können nur nacheinander ohne Akkorde und Polyphonie gespielt werden. Diese Einschränkung stellte sich jedoch als Killer-Feature heraus, da ein solches Format leicht zu schreiben und zu lesen, leicht zu analysieren und zu reproduzieren ist.
In diesem Artikel werden wir versuchen, einen RTTTL-Player in JavaScript zu erstellen, indem wir zum Spaß ein bisschen Code Golf und Mathematik hinzufügen, um den Code so kurz wie möglich zu halten.
Analysieren von RTTTL
Für RTTTL wird eine formale Grammatik verwendet. Das RTTL-Format ist eine Zeichenfolge, die aus drei Teilen besteht: dem Namen der Melodie, ihren Eigenschaften wie Tempo (BPM - Beats pro Minute, dh Anzahl der Beats pro Minute), Oktave und Dauer der Note sowie dem Melodiecode selbst. Wir werden jedoch das Verhalten von Nokia Composer selbst simulieren, nur einen Teil der Melodie analysieren und das BPM-Tempo als separaten Eingabeparameter betrachten. Der Name der Melodie und ihre Serviceeigenschaften bleiben außerhalb des Geltungsbereichs dieses Artikels.
Eine Melodie ist einfach eine Folge von Noten / Pausen, die durch Kommas mit zusätzlichen Leerzeichen getrennt sind. Jede Note besteht aus einer Länge (2/4/8/16/32/64), einer Tonhöhe (c / d / e / f / g / a / b), optional einer scharfen (#) und der Anzahl der Oktaven (von 1) bis 3, da nur drei Oktaven unterstützt werden).
Am einfachsten ist es, reguläre Ausdrücke zu verwenden . Neuere Browser verfügen über eine sehr praktische matchAll- Funktion , die eine Reihe aller Übereinstimmungen in einer Zeichenfolge zurückgibt:
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
}
};
Das erste, was Sie über jede Note herausfinden müssen, ist, wie Sie sie in die Frequenz der Schallwellen umwandeln. Natürlich können wir eine HashMap für alle sieben Notenbuchstaben erstellen. Da diese Buchstaben jedoch nacheinander angeordnet sind, sollte es einfacher sein, sie als Zahlen zu betrachten. Für jede Buchstabennote finden wir den entsprechenden numerischen Zeichencode ( ASCII- Code ). Für "A" ist dies 0x41 und für "a" ist es 0x61. Für "B / b" ist es 0x42 / 0x62, für "C / c" ist es 0x43 / 0x63 und so weiter:
// 'k' is an ASCII code of the note:
// A..G = 0x41..0x47
// a..g = 0x61..0x67
let k = m[4].charCodeAt();
Wir sollten wahrscheinlich die höchstwertigen Bits überspringen, wir werden nur k & 7 als Notenindex verwenden (a = 1, c = 2,…, g = 7). Was kommt als nächstes? Die nächste Stufe ist nicht sehr angenehm, da sie mit der Musiktheorie zusammenhängt. Wenn wir nur 7 Noten haben, zählen wir sie als alle 12. Dies liegt daran, dass die scharfen / flachen Noten zwischen den üblichen Noten ungleichmäßig verborgen sind:
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
Wie Sie sehen können, steigt der Notenindex in Oktave schneller an als der Notencode (k & 7). Außerdem nimmt sie nicht linear zu: Der Abstand zwischen E und F oder zwischen B und C beträgt 1 Halbton, nicht 2, wie zwischen den übrigen Noten.
Intuitiv können wir versuchen, (k & 7) mit 12/7 (12 Halbtöne und 7 Noten) zu multiplizieren:
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
Wenn wir diese Zahlen ohne Dezimalstellen betrachten, werden wir sofort feststellen, dass sie nicht linear sind, wie wir erwartet hatten:
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 -------
Aber nicht wirklich ... Der "Halbton" -Abstand sollte zwischen B / C und E / F liegen, nicht zwischen C / D. Versuchen wir es mit anderen Verhältnissen (die Unterstriche geben Halbtöne an):
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 ------- ------- -------
Es ist klar, dass die Werte 1.8 und 1.5 nicht geeignet sind: Der erste hat nur einen Halbton und der zweite hat zu viele. Die anderen beiden, 1.6 und 1.7, scheinen gut zu uns zu passen: 1.7 gibt die Hauptskala GA-BC-D-EF an und 1.6 gibt die Hauptskala AB-CD-EFG an. Genau das, was wir brauchen!
Jetzt müssen wir die Werte ein wenig ändern, so dass C 0 ist, D 2 ist, E 4 ist, F 5 ist und so weiter. Wir sollten um 4 Halbtöne versetzt sein, aber wenn wir 4 subtrahieren, wird die A-Note unter der C-Note erzeugt. Stattdessen addieren wir 8 und berechnen Modulo 12, wenn der Wert außerhalb einer Oktave liegt:
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 ...
Wir müssen auch den "scharfen" Charakter berücksichtigen, der von der m [3] -Gruppe des regulären Ausdrucks erfasst wird. Wenn vorhanden, erhöhen Sie den Notenwert um 1 Halbton:
// 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];
Schließlich müssen wir die richtige Oktave verwenden. Die Oktaven sind bereits als Zahlen in der regulären Ausdrucksgruppe m [5] gespeichert. Laut Musiktheorie besteht jede Oktave aus 12 Seminoten, sodass wir die Oktavzahl mit 12 multiplizieren und zum Notenwert addieren können:
// 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
Klemmung
Was passiert, wenn jemand die Anzahl der Oktaven als 10 oder 1000 angibt? Dies kann zu Ultraschall führen! Wir sollten nur den richtigen Wertesatz für solche Parameter zulassen. Das Begrenzen der Anzahl zwischen den beiden anderen wird üblicherweise als "Klemmen" bezeichnet. Modernes JS verfügt über eine spezielle Funktion Math.clamp (x, niedrig, hoch) , die jedoch in den meisten Browsern noch nicht verfügbar ist. Die einfachste Alternative ist:
clamp = (x, a, b) => Math.max(Math.min(x, b), a);
Da wir jedoch versuchen, unseren Code so kurz wie möglich zu halten, können wir das Rad neu erfinden und die Verwendung von mathematischen Funktionen einstellen. Wir verwenden den Standardwert x = 0 , damit die Klemmung auch mit undefinierten Werten funktioniert :
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
Beachten Sie Tempo und Dauer
Wir erwarten, dass BPM als Parameter an die Funktion out play () übergeben wird . Wir müssen es nur validieren:
bpm = clamp(bpm, 40, 400);
Um zu berechnen, wie lange eine Note in Sekunden dauern soll, können wir ihre musikalische Dauer (ganz / halb / viertel /…) ermitteln, die in der Regex-Gruppe m [1] gespeichert ist. Wir verwenden die folgende Formel:
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);
Wenn wir diese Formeln zu einer kombinieren und die Dauer der Note begrenzen, erhalten wir:
// Assuming that default note duration is 4: duration = 240 / bpm / clamp(m[1] || 4, 1, 64);
Vergessen Sie auch nicht, Noten mit Punkten anzugeben, die die Länge der aktuellen Note um 50% erhöhen. Wir haben eine Gruppe m [2], deren Wert ein Punkt sein kann . oder undefiniert . Wenn wir dieselbe Methode anwenden, die wir zuvor für das scharfe Zeichen verwendet haben, erhalten wir:
// !!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);
Jetzt können wir die Anzahl und Dauer für jede Note berechnen. Es ist Zeit, die WebAudio-API zu verwenden, um eine Melodie abzuspielen.
WEBAUDIO
Wir benötigen nur 3 Teile aus der gesamten WebAudio-API : Audiokontext, einen Oszillator zum Verarbeiten der Schallwelle und einen Verstärkungsknoten zum Ein- und Ausschalten des Klangs. Ich werde einen rechteckigen Oszillator verwenden, damit die Melodie wie das schreckliche Klingeln eines alten Telefons klingt:
// 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();
Dieser Code selbst erstellt noch keine Musik, aber da wir unsere RTTTL-Melodie analysiert haben, können wir WebAudio mitteilen, welche Note wann, mit welcher Frequenz und wie lange gespielt werden soll.
Alle WebAudio-Knoten verfügen über eine spezielle setValueAtTime- Methode , die ein Wertänderungsereignis (Frequenz oder Knotenverstärkung) plant.
Wenn Sie sich erinnern, hatten wir früher in diesem Artikel bereits den ASCII-Code für die Notiz als k gespeichert, den Notenindex als n und wir hatten die Dauer der Notiz in Sekunden. Jetzt können wir für jede Note Folgendes tun:
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);
}
Das ist alles. Unser play () -Programm kann jetzt ganze Melodien in RTTTL-Notation abspielen. Hier ist der vollständige Code mit einigen geringfügigen Erläuterungen wie der Verwendung von v als Verknüpfung für setValueAtTime oder der Verwendung von Ein-Buchstaben-Variablen (C = Kontext, z = Oszillator, da ein ähnlicher Klang erzeugt wird, g = Verstärkung, q = BPM, c = Klemme):
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);
Bei Minimierung mit Terser beträgt dieser Code nur 417 Byte. Dies liegt immer noch unter dem 512-Byte-Schwellenwert. Warum fügen wir keine stop () -Funktion hinzu, um die Wiedergabe zu unterbrechen:
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 :)
Dies sind immer noch rund 445 Bytes. Wenn Sie diesen Code in die Entwicklerkonsole einfügen, können Sie die RTTTL abspielen und die Wiedergabe beenden, indem Sie die JS-Funktionen play () und stop () aufrufen .
Benutzeroberfläche
Ich denke, das Hinzufügen einer kleinen Benutzeroberfläche zu unserem Synthesizer wird den Moment des Musikmachens noch angenehmer machen. An dieser Stelle würde ich vorschlagen, Code Golf zu vergessen. Es ist möglich, einen winzigen Editor für RTTTL-Klingeltöne zu erstellen, ohne Bytes mit normalem HTML und CSS zu speichern und ein minimiertes Skript nur zur Wiedergabe einzuschließen.
Ich habe beschlossen, den Code hier nicht zu posten, da er ziemlich langweilig ist. Sie finden es auf Github . Sie können die Demoversion auch hier ausprobieren: https://zserge.com/nokia-composer/ .

Wenn die Muse Sie verlassen hat und Sie überhaupt keine Lust haben, Musik zu schreiben, probieren Sie einige vorhandene Songs aus und genießen Sie den vertrauten Klang:
- Klingelton Nokia
- iPhone Klingelton, wenn Sie moderne Musik mehr mögen
- Zünde mein Feuer an
- Verliere dich
- Der gute der böse und der Hässliche
- Rondo Alla Turca (Mozart)
Übrigens, wenn Sie tatsächlich etwas komponiert haben, teilen Sie die URL (alle Songs und BPMs werden im Hash-Teil der URL gespeichert, sodass das Speichern / Teilen Ihrer Songs so einfach ist wie das Kopieren oder Lesezeichen des Links.
Ich hoffe, es hat Ihnen gefallen. Siehe diesen Artikel Sie können die Nachrichten auf Github , Twitter verfolgen oder über RSS abonnieren .