Lautstärke
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.
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:
$
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 einenint
,argc
, als auch ein Array vonchar *
(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 istoutput
, das den Namen der neuen Audiodatei angibt, die erzeugt werden soll. Das dritte istfactor
, das den Wert angibt, um den die Lautstärke der ursprünglichen Audiodatei skaliert werden soll.- Wenn
factor
zum Beispiel2.0
ist, dann sollte Ihr Programm die Lautstärke der Audiodatei ininput
verdoppeln und die neu erzeugte Audiodatei inoutput
speichern.
- Wenn
- Ihr Programm sollte zuerst den Header aus der Eingabedatei lesen und ihn in die Ausgabedatei schreiben.
uint8_t
ist ein Typ, der eine vorzeichenlose (daheruint
!u
steht fürunsigned
), d.h. nicht negative, 8-Bit-Ganzzahl (daher8
!) speichert. Wir können jedes Byte des Headers einer WAV-Datei als einenuint8_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 einenint16_t
-Wert behandeln.int16_t
ist ebenfalls in<stdint.h>
deklariert.
- Ihr Programm darf, falls es
malloc
verwendet, keine Memory-Leaks haben. Die Verwendung vonmalloc
ist für die Bearbeitung der Aufgabe jedoch nicht zwingend erforderlich.
Hilfestellung
Klicken Sie auf die folgenden Tipps, um einige Ratschläge zu erhalten!
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 (daheruint
!u
steht fürunsigned
), d.h. nicht negative, 8-Bit-Ganzzahl (daher8
!) speichert. Wir können jedes Byte des Headers einer WAV-Datei als einenuint8_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 einenint16_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 mitfopen
geöffnete Datei die Anzahl der erfolgreich gelesenen Bytes. Bei weiteren Aufrufen der Funktionfread
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 einerwhile
-Schleife aufzurufen. Sie könnten zum Beispielfread
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
!
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