Busted and Blue

Als im letzten Jahr Gorillaz’ Humanz erschien und ich darauf hingewiesen wurde, dass die Domain busted.blue noch nicht vergeben war, wusste ich noch nicht was ich damit tun sollte. Seit ein paar Monaten liegt dort nun eine relativ Dynamische Seite, die die Lyrics des Liedes Busted and Blue beim laden verstreut1. Das allein wird schnell langweilig. Die nächste Idee war es, die Zeilen einzeln zu animieren, das Lied im Hintergrund zu spielen und die aktuelle Zeile hervorzuheben. Mit u.A. Musixmatch gibt es Sammlungen von Lyrics mit Timestamps, aber dort ist Busted and Blue noch nicht eingetragen.

Die Zeiten selbst aufzuschreiben ist zu fehleranfällig und langweilig. Ein ein wenig automatischerer Weg könnte sein, die Instrumente heruaszufiltern und die übrigen Ausschläge zu gruppieren. Nach zwei Wiederholungen von Audacitys Noise Reduction blieb eine sehr kaputte die Stimme mit wenigen Instrumenten übrig.

Als nächstes mussten die Audiodatei selbst gelesen werden. Dazu kann das bis hierher verwendete FLAC nach WAVE konvertiert werden, wofurch das lesen relativ einfach wird2.

WAVE

Das Format ist alt und gut dokumentiert. Es existieren Blogposts und gute Beschreibungen darüber. Vor allem The Sonic Spots Wave File Format war hilfreich und bietet eine genauere Erklärung der einzelnen Teile.

WAVEs liegen in RIFF-Dateien. RIFF-Dateien wiederum sind in mehrere (mindestens ein) Chunks aufgeteilt. Ein Chunk kann bspw. Metadaten über das Lied, das Lied selbst oder mehrere Teile dessen beinhalten. Chunks sind in einen acht Byte großen Header und im Chunk enthaltene Daten aufgeteilt. Die ersten vier Byte des Headers halten die Chunk ID (in ASCII), die letzten vier die Größe des Chunks exkl. dessen Header.

Das RIFF-Chunk, in dessen Payload alle weiteren Chunks liegen, hat eine ID “RIFF” und als Größe entsprechen die Dateigröße exkl. dem RIFF Header selbst. Darauf folgend, im Chunk selbst, steht die Type ID (hier fix “WAVE”) gefolgt von einem Format-Chunk und beliebig vielen weiteren.

Das folgende ist ein Beispiel für einen RIFF Header sowie die ersten vier darauffolgenden Bytes3.

// R,    I,    F,    F,                   2084,    W,    A,    V,    E,
0x52, 0x49, 0x46, 0x46, 0x24, 0x08, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45,
ID: RIFF
Size: 2084
Type: WAVE

Nötig in WAVE sind lediglich ein RIFF-, ein fmt- und null, ein oder mehrere data-Chunks, die wie folgt aufgeteilt sind:

+----------------------+
| ID: RIFF             |
| Size of File         |
+----------------------+
| +------------------+ |
| | ID: fmt          | |
| | Size of chunk    | |
| +------------------+ |
| | ...              | |
| +------------------+ |
| +------------------+ |
| | ID: data         | |
| | Size of chunk    | |
| +------------------+ |
| | 0x00, 0x00, 0x00 | |
| | 0x00, 0x24, 0x17 | |
| | 0x1e, 0xf3, ...  | |
| +------------------+ |
| ...                  |
+----------------------+

Format-Chunk

Das Format Chunk hat eine ID fmt (f, m, t, Space) und ist i.d.R. 8+16 Byte groß. Es beinhaltet Metadata wie die Anzahl der Channels oder die größe der einzelnen Samples. Diese sind in das Format (2 Byte), die Anzahl der Channels (1 = Mono, 2 = Stereo, 2 Byte), die Sample Rate (Abtastungen in Hz, 4 Byte), die Byte Rate (4 Byte), sowie Block Align (Bytes pro Sample auf allen Channels) und Bits Per Sample (je 2 Byte) aufgeteilt. Alle folgenden Chunks sind optional.

Als Beispiel die 8+16 Byte des Format-Chunks sowie dessen Übersetzung:

// F,    M,    T,    ␣,                     16,          1,          2,
0x66, 0x6d, 0x74, 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00,
//               22050,                  88200,          4,         16,
0x22, 0x56, 0x00, 0x00, 0x88, 0x58, 0x01, 0x00, 0x04, 0x00, 0x10, 0x00,
ID: fmt 
Size: 16
AudioFormat: 1
Chans: 2
SampleRate: 22050
ByteRate: 88200
BlockAlign: 4
BitsPerSample: 16

Data-Chunks

Data-Chunks haben eine ID data und size Bytes an Samples.

// d,    a,    t,    a,                     26,          0,          0,
0x64, 0x61, 0x74, 0x61, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
//    5924,      -3298,       4924,       5180,      -1770,      -1768,
0x24, 0x17, 0x1e, 0xf3, 0x3c, 0x13, 0x3c, 0x14, 0x16, 0xf9, 0x18, 0xf9,
//   -6348,     -23005,      -3524,      -3548,     -12783,       3354,
0x34, 0xe7, 0x23, 0xa6, 0x3c, 0xf2, 0x24, 0xf2, 0x11, 0xce, 0x1a, 0x0d,
ID: data
Size: 26

Wie im Format-Chunk beschrieben, verwendet dieses Beispiel 16 Bits pro Sample auf zwei Channels.

Channels folgen aufeinander. Das erste Sample beider Channels ist 0, das zweite links ist 5924 und rechts -3298.

Weitere Chunks

Neben Format- und Data-Chunks existieren noch weitere, die u.a. im Sonic Spot beschrieben werden. Es ist möglich mehrere Data-Chunks zu verwenden, allerdings bestehen Lieder i.d.R. aus einem.

Plotten

Nachdem die einzelnen Samples nun mit bake/wave gelesen und geschrieben werden können, können sie auch geplottet werden. Das oben verwendete Beispiel Busted and Blue kann bspw. mit einer Version des Liedes, der die Instrumente größtenteils entfernt wurden verglichen werden:

Waveform von Gorillaz - Busted and Blue.

Waveform ohne Instrumente.

Vergrößterte Wave ohne Instrumente.

Das Beispiel liegt in Originalgröße hier4.

Weiter

Zeitmarken, der Grund für diesen Post, fehlen noch. Das Format wurde auf Github unter bake/wave implementiert und die Dokumentation findet sich im entsprechenden GoDoc. Auf git.192k.pw unter bake/wave liegt ein CLI Tool das zum plotten verwendet wurde. Dieses hat ein reencode-Command, mit dem aus einer WAVE gelesen und in eine andere geschrieben werden kann.

Zu einem späteren Zeitpunkt möchte ich es verwenden, um die in der Einleitung beschriebenen Änderungen (Animationen und das Hervorheben der aktuell gespielten Zeile) zu realisieren.


  1. Ähnlich mebious.co.uk und mebious.mobi aus Serial Experiments Lain. [return]
  2. Ich habe mir lediglich WAV angeschaut. [return]
  3. Dieses Beispiel stammt aus der Doku von soundfile++. Es ist außerdem im GoDoc des Projekts vorhanden. [return]
  4. In der Vergrößerung ist erkennbar, dass die Samples nur als Pixel eingezeichnet werden, nicht als Graph. [return]