Zum Inhalt springen
Vorschau auf das nächste Semester, noch nicht veröffentlicht.
Zur aktuellen Seite

Lautstärke

Wellenform der WAV-Datei

Aufgabe

So ist eine WAV-Datei aufgebaut

WAV ist ein gängiges Dateiformat für Audiodaten.

  1. Header: Die ersten 44 Byte sind der Header (bei den Dateien dieser Aufgabe immer genau 44). Er beschreibt die Datei, etwa ihre Größe, die Zahl der Samples pro Sekunde und die Größe eines Samples. Er wird unverändert in die Ausgabedatei übernommen.
  2. Samples: Danach folgen bis zum Dateiende nur noch Samples. Ein Sample ist der Wert des Audiosignals zu einem Zeitpunkt – im Bild oben ein Punkt der Welle.
  3. Größe eines Samples: Jedes Sample ist eine ganze Zahl mit Vorzeichen aus 2 Byte (16 Bit), also zwischen −32768 und 32767. In C passt dazu der Typ int16_t.
  4. Lautstärke: Multipliziert man jedes Sample mit demselben Faktor, ändert sich die Lautstärke: Ein Faktor über 1 macht die Datei lauter, einer unter 1 leiser.
  5. Nachkommastellen: Das Produkt ist eine Kommazahl. Beim Speichern in einem int16_t fallen die Nachkommastellen weg (es wird nicht gerundet, sondern in Richtung 0 abgeschnitten).
  6. Wertebereich: Ein Produkt außerhalb von −32768 bis 32767 passt nicht in ein int16_t. Solche Faktoren müssen Sie nicht abfangen.

Beispiel mit drei Samples:

Samplemal 0.5mal 2.0
10005002000
−3−1.5 → −1−6
73.5 → 314

Was genau im Header steht und warum Faktor 2.0 nicht „doppelt so laut“ klingt, steht am Ende der Seite unter „Zum Weiterlesen“.

Demo

Aufgabenmaterial

Für diese Aufgabe werden Sie ein von uns zur Verfügung gestelltes Codegerüst vervollständigen.

Aufgabenmaterial herunterladen

Öffnen Sie VS CodeVisual Studio Code, der kostenlose Code-Editor von Microsoft, in dem Sie im Kurs programmieren. Glossar → entsprechend Ihrem Setup.

Öffnen Sie Ihr TerminalfensterFenster, in dem Sie dem Computer Befehle als Text eintippen statt zu klicken. In VS Code liegt es im unteren Bereich des Fensters. Glossar → und führen Sie dann cdWechselt das Verzeichnis: cd me geht in den Ordner me, cd .. eine Ebene nach oben, cd allein ins Homeverzeichnis. Glossar → aus. Die EingabeaufforderungDas Zeichen am Anfang der Zeile im Terminal, etwa $ oder me/ $. Es zeigt, dass das Terminal auf Ihren nächsten Befehl wartet. Steht davor ein Ordnername wie me/, befinden Sie sich gerade in diesem Ordner. Glossar → Ihres Terminalfensters sollte wie folgt aussehen:

$

Zur Erinnerung: Mit Strg+Shift+V (bei Windows, ansonsten rechter Mausklick in das Terminalfenster) fügen Sie kopierten Text in die Kommandozeile ein. Frühere Befehle holen Sie mit den Pfeiltasten (hoch/runter) oder Strg+R zurück.

Geben Sie dann

wget https://dev.inf.zone/download/exercises/07/volume.zip

ein und führen Sie den Befehl mit der Eingabetaste aus, um eine ZIP-Datei namens volume.zip in den aktuellen Ordner herunterzuladen. Achten Sie darauf, dass Sie das Leerzeichen zwischen wgetLädt eine Datei aus dem Internet in das aktuelle Verzeichnis herunter, im Kurs zum Beispiel das Aufgabenmaterial einer Übung. Glossar → und der folgenden URL nicht übersehen, und auch kein anderes Zeichen!

Führen Sie jetzt

unzip volume.zip

aus, um das ZIP-Archiv in einen Ordner namens volume zu extrahieren. Sie brauchen die ZIP-Datei nicht mehr, also können Sie

rm volume.zip

ausführen. Antworten Sie mit “y”, gefolgt von der Eingabetaste, um die heruntergeladene ZIP-Datei zu entfernen.

Führen Sie dann

cd volume

aus, um in dieses Verzeichnis zu wechseln. Ihre Eingabeaufforderung sollte nun wie folgt aussehen:

volume/ $

Wenn alles funktioniert hat, sollten Sie nach dem Ausführen von

ls

die Dateien volume.c und input.wav sehen. Wenn Sie code volume.c ausführen, sollte sich die Datei öffnen. In diese Datei werden Sie Ihren Code für diese Aufgabe einfügen. Wenn diese Datei nicht angezeigt wird, verfolgen Sie Ihre Schritte zurück und schauen Sie, ob Sie herausfinden können, wo Sie einen Fehler gemacht haben!

Überblick über volume.c

volume.c erledigt schon alles außer den beiden TODOs:

  • Argumente: Ist argc ungleich 4 (Programmname plus drei Argumente), gibt das Programm Usage: ./volume input.wav output.wav factor aus und beendet sich mit Statuscode 1.
  • Dateien öffnen: argv[1] wird mit fopen zum Lesen geöffnet (input), argv[2] zum Schreiben (output). Liefert fopen NULL, erscheint Could not open file. und das Programm endet mit 1.
  • Faktor: float factor = atof(argv[3]); wandelt das dritte Argument in eine Kommazahl um.
  • Header-Größe: Die Konstante HEADER_SIZE ist 44.
  • Schließen: Am Ende schließt fclose beide Dateien. Wann immer Sie fopen aufrufen, gehört später ein fclose dazu.

Dazwischen stehen Ihre beiden Aufgaben:

// TODO: Copy header from input file to output file

// TODO: Read samples from input file and write updated data to output file

Spezifikation

Vervollständigen Sie volume.c so, dass es die Lautstärke von input um factor ändert und das Ergebnis in output speichert. Bei factor gleich 2.0 werden zum Beispiel alle Samples verdoppelt.

  • Lesen Sie zuerst die 44 Byte des Headers aus der Eingabedatei und schreiben Sie sie unverändert in die Ausgabedatei.
    • Jedes Byte des Headers können Sie als uint8_t behandeln: eine vorzeichenlose (u für unsigned), also nicht negative, ganze Zahl aus 8 Bit (daher die 8), genau ein Byte. uint8_t ist in <stdint.h> deklariert.
  • Lesen Sie danach die restlichen Daten ein Sample nach dem anderen, multiplizieren Sie jedes mit factor und schreiben Sie das neue Sample in die Ausgabedatei.
    • Gehen Sie davon aus, dass die WAV-Datei 16-Bit-Samples mit Vorzeichen enthält. (WAV-Dateien können auch andere Sample-Größen haben, in dieser Aufgabe nicht.)
    • Jedes Sample können Sie als int16_t behandeln: eine ganze Zahl mit Vorzeichen, also positiv oder negativ, aus 16 Bit. int16_t ist ebenfalls in <stdint.h> deklariert.
  • Ihr Programm darf, falls es malloc verwendet, keine Memory-Leaks haben. malloc ist für diese Aufgabe aber nicht nötig.

Hilfestellung

Klicken Sie auf die folgenden Tipps, um einige Ratschläge zu erhalten. Beide Tipps zeigen Schritt für Schritt den Code einer möglichen Lösung. Schauen Sie sie deshalb erst an, wenn Sie die Aufgabe ernsthaft selbst versucht haben. Nicht jede Aufgabe hat einen so ausführlichen Lösungsweg; meist ist eine solche Aufgabe eine Aufwärmübung für eine schwierigere, die später folgt.

WAV-Header von Eingabedatei in Ausgabedatei kopieren

Ihr erstes TODO: den Header aus input lesen und in output schreiben.

Bisher kennen Sie Typen wie int, bool, char, double, float und long. In <stdint.h> gibt es weitere Typen, bei denen Größe (in Bit) und Vorzeichen genau festgelegt sind. Für WAV-Dateien brauchen Sie zwei davon (siehe Spezifikation): uint8_t für ein Byte des Headers und int16_t für ein Sample.

Für den Header brauchen Sie ein Array von Bytes. Ein Array aus n Bytes legen Sie so an:

uint8_t header[n];

Für n setzen Sie die Größe des Headers ein, also HEADER_SIZE. Dann können Sie header als Argument für fread und fwrite verwenden: erst den Header aus der Eingabedatei in header einlesen, dann von header in die Ausgabedatei schreiben.

Achtung, Spoiler! Mit diesem Code ist das erste TODO erledigt:

// Copy header from input file to output file
uint8_t header[HEADER_SIZE];
fread(header, HEADER_SIZE, 1, input);
fwrite(header, HEADER_SIZE, 1, output);
Aktualisierte Daten in die Ausgabedatei schreiben

Ihr zweites TODO: die Samples aus input lesen, ändern und in output schreiben. Beim Lesen von Dateien legt man üblicherweise einen „Puffer“ an, der die Daten vorübergehend aufnimmt. Dort ändern Sie die Daten und schreiben sie dann in die neue Datei.

Für ein einzelnes Sample genügt eine Variable vom Typ int16_t:

// Create a buffer for a single sample
int16_t buffer;

In diesen Puffer lesen Sie mit fread ein Sample nach dem anderen. Als Argument übergeben Sie &buffer, die Adresse der Variablen:

// Create a buffer for a single sample
int16_t buffer;

// Read single sample into buffer
fread(&buffer, sizeof(int16_t), 1, input);

Um die Lautstärke des Samples zu ändern, multiplizieren Sie es mit factor:

// Create a buffer for a single sample
int16_t buffer;

// Read single sample into buffer
fread(&buffer, sizeof(int16_t), 1, input);

// Update volume of sample
buffer *= factor;

Und schließlich schreiben Sie das neue Sample in output:

// Create a buffer for a single sample
int16_t buffer;

// Read single sample from input into buffer
fread(&buffer, sizeof(int16_t), 1, input);

// Update volume of sample
buffer *= factor;

// Write updated sample to new file
fwrite(&buffer, sizeof(int16_t), 1, output);

Das muss sich wiederholen, solange noch Samples zu lesen sind:

  • Jeder Aufruf von fread liest dort weiter, wo der vorige aufgehört hat. Die geöffnete Datei merkt sich ihre aktuelle Position.
  • fread gibt laut Dokumentation zurück, wie viele Elemente (hier: Samples) es vollständig gelesen hat. Am Dateiende ist das 0.
  • Sie können fread also direkt in die Bedingung einer while-Schleife schreiben:
while (fread(...))
{

}

Achtung, Spoiler! So ist das zweite TODO erledigt:

// Create a buffer for a single sample
int16_t buffer;

// Read single sample from input into buffer while there are samples left to read
while (fread(&buffer, sizeof(int16_t), 1, input) != 0)
{
    // Update volume of sample
    buffer *= factor;

    // Write updated sample to new file
    fwrite(&buffer, sizeof(int16_t), 1, output);
}

Da C Werte ungleich 0 als true und 0 als false behandelt, geht es noch kürzer:

// Create a buffer for a single sample
int16_t buffer;

// Read single sample from input into buffer while there are samples left to read
while (fread(&buffer, sizeof(int16_t), 1, input))
{
    // Update volume of sample
    buffer *= factor;

    // Write updated sample to new file
    fwrite(&buffer, sizeof(int16_t), 1, output);
}

Testen

Ihr Programm sollte sich wie in den folgenden Beispielen verhalten:

./volume input.wav output.wav 2.0

Wenn Sie output.wav anhören, sollte die Datei deutlich lauter sein als input.wav.

./volume input.wav output.wav 0.5

Jetzt sollte output.wav deutlich leiser sein als input.wav.

Zum Anhören machen Sie einen Rechtsklick auf output.wav im Dateibrowser, wählen Herunterladen... und öffnen die Datei in einem Audio-Player auf Ihrem Computer. Bei einer lokalen Installation mit Docker können Sie Audio-Dateien auch direkt in VS Code abspielen.

Korrektheit

Führen Sie in Ihrem Terminal den folgenden Befehl aus, um die Korrektheit Ihrer Arbeit zu überprüfen:

check50 -l inf-zone/exercises/2026/volume

Style

Führen Sie den folgenden Befehl aus, um den Stil Ihres Codes mit style50Prüfprogramm von CS50, das zeigt, wo Ihr Code von den Formatierungsregeln abweicht. In Grün zeigt es, was Sie ergänzen sollten, in Rot, was wegfallen soll. Glossar → zu analysieren:

style50 volume.c

Abgeben

Geben Sie im Ordner volume ab:

inf upload volume

Danach sehen Sie, welche Tests Ihr Programm besteht und ob die Übung für die Bonuspunkte zählt. Wie Sie inf installieren und sich anmelden, steht unter Abgeben mit inf.

Zum Weiterlesen: Header und Lautstärke

Für die Lösung der Aufgabe nicht nötig.

Was im Header von input.wav steht:

ByteInhaltWert in input.wav
0–3KennungRIFF
8–11FormatWAVE
22–23Zahl der Kanäle2 (Stereo)
24–27Samples pro Sekunde und Kanal44100
34–35Bit pro Sample16
40–43Größe der Audiodaten in Byte352800

Bei Stereo wechseln sich die Samples für den linken und den rechten Kanal ab. Für Ihr Programm spielt das keine Rolle, weil alle Samples mit demselben Faktor multipliziert werden. Viele WAV-Dateien in freier Wildbahn haben übrigens längere Header mit zusätzlichen Abschnitten; die 44 Byte gelten nur für den einfachsten Aufbau, wie ihn die Dateien dieser Aufgabe haben.

Warum Faktor 2.0 nicht doppelt so laut klingt: Der Faktor verdoppelt die Ausschläge der Welle (die Amplitude). Das entspricht etwa 6 Dezibel mehr. Als „doppelt so laut“ empfinden Menschen nach einer gängigen Faustregel erst etwa 10 Dezibel mehr, also ungefähr die dreifache Amplitude.

Was bei zu großen Faktoren passiert: Die Samples in input.wav liegen zwischen −8185 und 8117; bis Faktor 4 passt also jedes Ergebnis in ein int16_t. Darüber sprengen die lautesten Stellen den Wertebereich. Der C-Standard legt für diesen Fall kein Ergebnis fest, in der Praxis entstehen falsche Werte, die als Knacken und Verzerrung zu hören sind. Ein Programm, das das vermeiden will, müsste zu große Ergebnisse auf 32767 bzw. −32768 begrenzen.

Diese Seite als Markdown: ansehen herunterladen