Lautstärke

Wellenform der WAV-Datei

Aufgabe

WAV-Dateien sind ein gängiges Dateiformat zur Darstellung von Audiodaten. WAV-Dateien speichern Audiodaten als eine Sequenz von “Samples”. Samples sind Zahlen, die den Wert eines Audiosignals zu einem bestimmten Zeitpunkt darstellen. WAV-Dateien beginnen mit einem 44-Byte-“Header”, der Informationen über die Datei selbst enthält, darunter die Größe der Datei, die Anzahl der Samples pro Sekunde und die Größe jedes Samples. Nach dem Header folgen die Samples, wobei jedes Sample eine einzelne 2-Byte-Ganzzahl (16 Bit) ist, die das Audiosignal zu einem bestimmten Zeitpunkt repräsentiert.

Durch Skalieren der einzelnen Sample-Werte mit einem bestimmten Faktor wird die Lautstärke der Audiodaten verändert. Wird beispielsweise jeder Sample-Wert mit 2,0 multipliziert, verdoppelt sich die Lautstärke des Ausgangsmaterials. Wird jedes Sample mit 0,5 multipliziert, halbiert sich die Lautstärke.

Wir erstellen in dieser Aufgabe ein Programm, um die Lautstärke einer Audiodatei zu ändern.

ℹ️
Beachten Sie die Anweisungen in den folgenden Abschnitten.

Demo

Aufgabenmaterial

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

Aufgabenmaterial herunterladen

Öffnen Sie VS Code entsprechend Ihrem Setup.

Öffnen Sie Ihr Terminalfenster und führen Sie dann cd aus. Die Eingabeaufforderung Ihres Terminalfensters sollte wie folgt aussehen:

$
ℹ️
Zur Erinnerung: Mit Strg+Shift+V (bei Windows, ansonsten rechter Mausklick in das Terminalfenster) kann in die Zwischenablage kopierter Text in die Kommandozeile eingefügt werden. Zuvor genutzte Befehle kann man mit den Pfeiltasten (hoch/runter) oder Strg+R erneut aufrufen.

Geben Sie dann

wget https://inf.zone/download/exercises/04/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 wget 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

eine Datei mit dem Namen volume.c 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 ist bereits so angelegt, dass es drei Kommandozeilenargumente erwartet, nämlich input, output und factor.

  • main nimmt sowohl einen int, argc, als auch ein Array von char * (Strings!), argv, entgegen.
  • Wenn argc, die Anzahl der Argumente auf der Kommandozeile einschließlich des Programms selbst, ungleich 4 ist, gibt das Programm die erwartete Verwendung aus und beendet sich mit Statuscode 1.
int main(int argc, char *argv[])
{
    // Check command-line arguments
    if (argc != 4)
    {
        printf("Usage: ./volume input.wav output.wav factor\n");
        return 1;
    }

    // ...
}

Als nächstes verwendet volume.c fopen, um die beiden als Kommandozeilenargumente angegebenen Dateien zu öffnen.

  • Es sollte geprüft werden, ob das Ergebnis des Aufrufs von fopen NULL ist. Ist dies der Fall, wurde die Datei nicht gefunden oder konnte nicht geöffnet werden.
// Open files and determine scaling factor
FILE *input = fopen(argv[1], "r");
if (input == NULL)
{
    printf("Could not open file.\n");
    return 1;
}

FILE *output = fopen(argv[2], "w");
if (output == NULL)
{
    printf("Could not open file.\n");
    return 1;
}

Später werden diese Dateien mit fclose geschlossen. Wann immer Sie fopen aufrufen, sollten Sie später fclose aufrufen!

// Close files
fclose(input);
fclose(output);

Bevor wir jedoch die Dateien schließen, sollten wir die verbleibenden TODOs erledigen.

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

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

Dafür benötigen wir noch den Faktor, mit dem die Lautstärke skaliert werden soll. volume.c wandelt hierfür bereits das dritte Kommandozeilenargument in ein float um!

float factor = atof(argv[3]);

Die beiden verbleibenden TODOs überlassen wir Ihnen. Beachten Sie die Informationen unter Details zur Umsetzung.

Details zur Umsetzung

Vervollständigen Sie die Implementierung von volume.c, um die Lautstärke einer Audiodatei um einen bestimmten Faktor zu ändern.

  • Das Programm sollte drei Kommandozeilenargumente akzeptieren. Das erste ist input, das den Namen der ursprünglichen Audiodatei angibt. Das zweite ist output, das den Namen der neuen Audiodatei angibt, die erzeugt werden soll. Das dritte ist factor, das den Wert angibt, um den die Lautstärke der ursprünglichen Audiodatei skaliert werden soll.
    • Wenn factor zum Beispiel 2.0 ist, dann sollte Ihr Programm die Lautstärke der Audiodatei in input verdoppeln und die neu erzeugte Audiodatei in output speichern.
  • Ihr Programm sollte zuerst den Header aus der Eingabedatei lesen und ihn in die Ausgabedatei schreiben.
    • uint8_t ist ein Typ, der eine vorzeichenlose (daher uint! u steht für unsigned), d.h. nicht negative, 8-Bit-Ganzzahl (daher 8!) speichert. Wir können jedes Byte des Headers einer WAV-Datei als einen uint8_t-Wert behandeln. uint8_t ist in <stdint.h> deklariert.
    • Wie viele Bytes hat noch einmal der Header einer WAV-Datei?
  • Ihr Programm sollte dann den Rest der Daten aus der WAV-Datei lesen, ein 16-Bit (2-Byte) Sample nach dem anderen. Ihr Programm sollte jedes Sample mit dem factor multiplizieren und das neue Sample in die Ausgabedatei schreiben.
    • Sie können davon ausgehen, dass die WAV-Datei 16-Bit-Werte mit Vorzeichen als Samples verwendet. In der Praxis können WAV-Dateien eine unterschiedliche Anzahl von Bits pro Sample haben, aber für diese Aufgabe gehen wir von 16-Bit-Samples aus.
    • int16_t ist ein Typ, der eine 16-Bit-Ganzzahl mit Vorzeichen (d.h. positiv oder negativ) speichert. Wir können jedes Audio-Sample in einer WAV-Datei als einen int16_t-Wert behandeln. int16_t ist ebenfalls in <stdint.h> deklariert.
  • Ihr Programm darf, falls es malloc verwendet, keine Memory-Leaks haben. Die Verwendung von malloc ist für die Bearbeitung der Aufgabe jedoch nicht zwingend erforderlich.

Hilfestellung

Klicken Sie auf die folgenden Tipps, um einige Ratschläge zu erhalten!

⚠️
Die beiden Tipps zeigen Ihnen jeweils Schritt für Schritt den Code einer möglichen Lösung. Idealerweise schauen Sie sich diese erst an, nachdem Sie die Aufgabe bearbeitet haben - oder zumindest ernsthaft versucht haben, diese zu bearbeiten. Nicht jede Aufgabe enthält einen derart umfangreichen Lösungsweg, und normalerweise ist die Aufgabe, für die in der Hilfestellung bereits eine (fast) vollständige Lösung angegeben ist, nur eine Aufwärmübung für eine schwierigere Aufgabe, die Sie später lösen sollen.
WAV-Header von Eingabedatei in Ausgabedatei kopieren

Ihr erstes TODO besteht darin, den Header der WAV-Datei aus input zu kopieren und ihn in output zu schreiben. Dazu haben Sie in den Details zur Umsetzung bereits ein paar spezielle Datentypen kennengelernt.

Noch einmal zur Wiederholung und Einordnung: Bisher haben wir eine Reihe von verschiedenen Typen in C gesehen, darunter int, bool, char, double, float und long. In einer Header-Datei namens <stdint.h> sind jedoch eine Reihe von anderen Typen deklariert, die es uns ermöglichen, die Größe (in Bits) und das Vorzeichen (mit oder ohne Vorzeichen) einer Ganzzahl sehr genau zu definieren. Vor allem zwei Typen werden uns bei der Arbeit mit WAV-Dateien nützlich sein:

  • uint8_t ist ein Typ, der eine vorzeichenlose (daher uint! u steht für unsigned), d.h. nicht negative, 8-Bit-Ganzzahl (daher 8!) speichert. Wir können jedes Byte des Headers einer WAV-Datei als einen uint8_t-Wert behandeln.
  • int16_t ist ein Typ, der eine 16-Bit-Ganzzahl mit Vorzeichen (d.h. positiv oder negativ) speichert. Wir können jedes Audio-Sample in einer WAV-Datei als einen int16_t-Wert behandeln.

Um die Daten aus dem WAV-Dateiheader, die Sie aus der Eingabedatei lesen, zu speichern, brauchen Sie ein Array von Bytes. Sie können ein solches Array mit n-Bytes für Ihren Header vom Typ uint8_t, um ein Byte zu repräsentieren, mit der folgenden Syntax erstellen:

uint8_t header[n];

Das n muss durch die tatsächliche Anzahl der Bytes ersetzt werden. Sie können dann header als Argument für fread oder fwrite verwenden, um den Header der Eingabedatei in header einzulesen oder von header in eine andere Datei zu schreiben.

Erinnern Sie sich, dass der Header einer WAV-Datei immer genau 44 Bytes lang ist. Beachten Sie, dass volume.c bereits eine Variable namens HEADER_SIZE definiert, die der Anzahl der Bytes im Header entspricht.

Abschließend noch ein ziemlich großer Tipp (Achtung, Spoiler!). Mit dem folgenden Code können Sie dieses TODO erledigen:

// 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 besteht darin, die Samples aus input zu lesen, diese Samples zu aktualisieren und die aktualisierten Samples in output zu schreiben. Beim Lesen von Dateien ist es üblich, einen “Puffer” zu erstellen, in dem die Daten vorübergehend gespeichert werden. Dort können Sie die Daten ändern und - sobald sie fertig sind - die Daten des Puffers in eine neue Datei schreiben.

Erinnern Sie sich, dass wir den Typ int16_t verwenden können, um ein Sample aus einer WAV-Datei zu repräsentieren. Um ein einzelnes Audio-Sample zu speichern, können Sie eine Puffervariable mit einer Syntax wie dieser erstellen:

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

In diesen Puffer können nun die Daten eingelesen werden - ein Sample nach dem anderen. Verwenden Sie fread für diese Aufgabe. Sie können &buffer als Argument für fread oder fwrite verwenden, um aus dem Puffer zu lesen oder in ihn zu schreiben. Erinnern Sie sich, dass der &-Operator verwendet wird, um die Adresse einer Variablen zu erhalten.

// 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 eines Samples zu vergrößern (oder zu verkleinern), müssen Sie es nur mit dem factor multiplizieren.

// 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 können Sie das aktualisierte Sample in output schreiben:

// 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);

Es gibt nur ein Problem: Sie müssen so lange damit fortfahren, ein Sample in den Puffer zu lesen, seine Lautstärke zu aktualisieren und das aktualisierte Sample in die Ausgabedatei zu schreiben, wie noch Samples zum Lesen übrig sind.

  • Gemäß der Dokumentation von fread merkt sich die mit fopen geöffnete Datei die Anzahl der erfolgreich gelesenen Bytes. Bei weiteren Aufrufen der Funktion fread auf diese Datei werden daher jeweils die Bytes nach den bereits gelesenen Bytes eingelesen.
  • Glücklicherweise gibt fread gemäß seiner Dokumentation die Anzahl der erfolgreich gelesenen Daten zurück. Das kann nützlich sein, um zu prüfen, ob Sie das Ende der Datei erreicht haben!
  • Es gibt keinen Grund, fread nicht innerhalb der Bedingung einer while-Schleife aufzurufen. Sie könnten zum Beispiel fread wie folgt aufrufen:
while (fread(...))
{

}

Achtung, Spoiler! Hier ist ein effizienter Weg, dieses TODO zu erledigen:

// 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 die von uns verwendete Version von C Nicht-Null-Werte als true und Null-Werte als false behandelt, können Sie die obige Syntax sogar noch vereinfachen:

// 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 die Datei output.wav anhören, sollte sie doppelt so laut sein wie input.wav!

ℹ️
Die Datei können Sie anhören, indem Sie einen Rechtsklick auf output.wav im Dateibrowser machen, Herunterladen... wählen und die Datei dann in einem Audio-Player auf Ihrem Computer öffnen. Bei einer lokalen Installation mit Docker können Sie Audio-Dateien auch direkt in VS Code abspielen.
./volume input.wav output.wav 0.5

Wenn Sie die Datei output.wav anhören, sollte sie nur halb so laut sein wie die Datei input.wav!

Korrektheit

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

check50 -l cs50/problems/2024/x/volume

Style

Führen Sie den folgenden Befehl aus, um den Stil Ihres Codes mit style50 zu analysieren:

style50 volume.c