Introduction
A short sound can tell someone that a task has finished. In this lab, you will make the sound itself: a C loop calculates a sequence of numbers, and those numbers become a playable WAV file. First hear a working 440 Hz example, change only its pitch, then complete your own reusable sampling function.
You need C variables, arithmetic, loops, arrays and functions, plus basic terminal navigation. Audio sampling, radians and binary output are introduced here; pointers and signal processing are not prerequisites. The compiler, a small transparent WAV writer and a file-only browser player are prepared. All audio is original synthesis; no recording device is needed. The 50-minute duration is an author estimate, including reading and ordinary mistakes.
Hear One Working Tone
In this step, you will play a working half-second tone, then change one value to make it higher. Start in Terminal. cd changes directory; this absolute path also recovers from a mistaken working directory. Keep your work here.
cd /home/labex/project/sound-studio
A sample is one signed number describing the waveform at one instant. PCM means pulse-code modulation: the file stores these sample values directly. This lab uses one channel, called mono, and 16,000 samples per second. Its signed 16-bit samples can represent integers from -32,768 through 32,767. Positive and negative values are two sides of the waveform; zero is the center. Amplitude controls how far the values move from zero. Begin with a comfortable device volume if you are listening through speakers or headphones.
The prepared sample.c makes a sine wave at 440 Hz: 440 cycles per second. sin, declared in <math.h>, accepts an angle in radians and returns a value between -1 and 1. One full cycle is 2 * pi radians. Multiplying by 10,000 scales that shape to a modest amplitude. We will examine the time calculation after hearing the result.
Compile the supplied example. gcc compiles C; -std=c11 selects C11, -Wall -Wextra enable useful warnings, and -o sample names the executable. pcm.c supplies the WAV writer. -lm, placed after the source files, links the mathematics library used by sin. These files do not need to produce compiler warnings.
gcc -std=c11 -Wall -Wextra sample.c pcm.c -lm -o sample
./sample runs the executable in this directory. It writes binary WAV bytes to standard output. > redirects those bytes into a file; do not run this program without redirection, because audio bytes are not readable terminal text. outputs is an already prepared directory. Use this new filename:
./sample > outputs/sample-440.wav
Success prints no text. Switch to the Sound Studio Tab. Click Refresh files, choose sample-440.wav under WAV file, then click Load sound. The page should say Ready to play and show 8000 samples · 16000 Hz · 0.500 s. Click Play, then try Pause and Playback position. You can focus the position control with Tab and use Home, End and the arrow keys. Native audio controls may round a subsecond duration to 0:00; the text above and the position in seconds give the precise duration.
Click Download WAV to save the actual file. The player reads the file you created; it does not generate missing samples. Cannot load sound reports a missing or unsupported file. That is different from a connection error where the studio page itself does not open. For a path mistake, return to Terminal, use the cd command above, check the filename, rerun the matching command and refresh the file list.

Look at the selected sample-440.wav, the half-second metadata and the Play, Playback position and Download WAV controls.
Switch back to Terminal. cat displays text. Read the short example:
cat sample.c
The loop visits sample indices 0 through 7999. seconds = (double)i / rate converts an index into time: index 0 is at 0 seconds; index 160 is at 0.010 seconds. (double) requests decimal division instead of integer division. The phase, 2.0 * pi * frequency * seconds, counts the angle through the cycles. (int) converts the scaled sine value into an integer by dropping its fractional part. The provided wav_sample writes that number's two PCM bytes; it does not choose the number for you.
Now change only the frequency. The next command uses sed, a text editor for a specific substitution. -i edits the named file in place; the quoted s/old/new/ expression replaces the literal text between the first pair of slashes with the text between the second pair. It edits only sample.c.
sed -i 's/frequency = 440/frequency = 660/' sample.c
Rebuild so the executable contains the edit, then save a different file. Keep the first WAV too.
gcc -std=c11 -Wall -Wextra sample.c pcm.c -lm -o sample
./sample > outputs/sample-660.wav
Switch to Sound Studio, refresh and load sample-660.wav, then play it. It has the same 8,000 samples and half-second duration, with a higher pitch. Only frequency changed. Keep the edited sample.c, the sample executable and both files. This sample comparison is preparation; your reusable program and its separate results come next.
Calculate Every Sample Yourself
In this step, you will complete write_tone so one C function works with different pitches, durations and amplitudes. Begin in Terminal, in /home/labex/project/sound-studio.
At 16,000 samples per second, 300 milliseconds needs 16000 * 300 / 1000 = 4800 samples. A millisecond is one thousandth of a second. Whole millisecond durations have exact integer sample counts at this rate. A loop over i = 0 while i < count emits exactly count values; using <= would add an unwanted sample.
Think of a much smaller illustration: at eight samples per second, a one-cycle-per-second sine wave has approximate values 0, 0.71, 1, 0.71, 0, -0.71, -1, -0.71 over its first eight indices. The index moves time forward, and sin supplies the vertical position. Our real 16,000 Hz rate gives many more values per cycle.
Read the provided interface and starter function:
cat tone.h tone.c
write_tone(frequency, milliseconds, amplitude) must generate all the samples. The starter writes zeros, so compiling it can produce a structurally valid but silent WAV. A filename, file size or successful exit cannot prove a tone exists.
Keep main.c, tone.h, pcm.h, pcm.c, inspect_wav.py and audio.py unchanged. The prepared main.c reads three arguments, validates them, writes the header and calls your function. You do not need to change its pointer-based argument parser. tone.c is your implementation; the browser is already prepared.
The writer separates packaging from synthesis. wav_begin(count) writes the header once; wav_sample(value) writes one supplied integer. Neither creates a waveform. The small pcm.c uses putchar to write a byte to standard output and fputs to write fixed chunk names. It emits low byte then high byte, rather than dumping a C structure. Negative samples have their 16-bit representation encoded; the writer refuses values outside the signed range and never clips or substitutes them.
These are the fields it writes. The fmt name includes its trailing space.
| Field | Meaning in this lab |
|---|---|
RIFF, size, WAVE |
Identify a WAV container; size is file bytes minus the first 8 bytes |
fmt, size 16 |
Describe the following 16 format bytes |
Format 1, channels 1 |
Uncompressed PCM, mono |
Sample rate 16000 |
Samples per second |
Byte rate 32000 |
16,000 frames per second × 2 bytes per frame |
Block alignment 2, sample bits 16 |
One frame contains one two-byte sample |
data, size 2 * count |
Identify and count the actual PCM bytes that follow |
You can read this provided wrapper with cat pcm.c. Its field values describe the output; changing only a header cannot turn silent samples into sound.
Implement the short function below. <math.h> declares sin. pi is a local decimal constant, so the program does not rely on a nonstandard M_PI macro. amplitude is a scale factor applied before conversion to an integer; the parser's maximum of 12,000 keeps these sine samples safely inside the 16-bit range. Each call starts its time index at zero.
cat > tone.c replaces only this source file. The here-document <<'C' sends all following lines to cat until the final C on its own line. Quoting the delimiter preserves the C text literally. Paste the complete block, including that last line.
cat > tone.c <<'C'
#include <math.h>
#include "tone.h"
#include "pcm.h"
void write_tone(int frequency, int milliseconds, int amplitude) {
const int rate = 16000;
const double pi = 3.14159265358979323846;
int count = rate * milliseconds / 1000;
for (int i = 0; i < count; i++) {
double seconds = (double)i / rate;
double phase = 2.0 * pi * frequency * seconds;
int sample = (int)(amplitude * sin(phase));
wav_sample(sample);
}
}
C
Compile your function with the prepared caller and writer. If compilation reports a line error, correct tone.c or repeat its complete replacement block, then rebuild. Running an old executable after a failed compilation would test old code.
gcc -std=c11 -Wall -Wextra main.c tone.c pcm.c -lm -o tone
The executable takes frequency in Hz, duration in whole milliseconds, then amplitude. Supported inclusive ranges are frequency 200–1200, duration 100–1000, and amplitude 1000–12000. Create two new outputs using your program:
./tone 440 300 8000 > outputs/tone-440.wav
./tone 660 500 10000 > outputs/tone-660.wav
Use the prepared reader to inspect the actual saved bytes. python3 runs the named read-only Python tool; you do not need to write Python. It reports an error for missing, incomplete or unsupported files and never regenerates them.
python3 inspect_wav.py outputs/tone-440.wav
Expect:
Format: signed 16-bit mono PCM
Rate: 16000 Hz
Frames: 4800
Duration: 0.300 s
Peak: 8000
Switch to Sound Studio. Refresh, load tone-440.wav and play it, then load and play tone-660.wav. Their displayed durations are 0.300 and 0.500 seconds. Download one of these new outputs too. These are your function's results, separate from sample-440.wav and sample-660.wav.

The selected file and Ready to play message both identify tone-660.wav; the metadata describes the actual saved samples.
Keep tone.c, the working tone executable and both new outputs. The check reads the saved WAVs, tests your delivered executable and recompiles your unchanged source in a disposable directory with other valid parameters. All three must work; source alone, an old executable or a copied sample is insufficient. Equivalent mathematics and integer rounding are accepted: fitted tone amplitude may differ by at most 2% of the requested amplitude plus 2 sample units, sample peaks may exceed the request by at most 2 units, and root-mean-square residual from a sine at the requested frequency may be at most 0.5% of amplitude plus 2 units. Root-mean-square residual is an average-sized sample error. This numerical tolerance accommodates rounding; it is not a separate permission to change the requested pitch. Exact frame counts and the PCM format still apply. The provided formula comfortably meets these limits.
Check Timing and Recover a Refusal
In this step, you will use the same function for a short non-round duration and distinguish invalid input from a broken sound file. Begin in Terminal.
Generate 101 milliseconds at 733 Hz with amplitude 7,373. A cycle need not divide the segment duration exactly; count still defines the number of samples.
./tone 733 101 7373 > outputs/short-tone.wav
python3 inspect_wav.py outputs/short-tone.wav
Look for Frames: 1616 and Duration: 0.101 s. That follows directly from 16000 * 101 / 1000. The last sample occurs just before the end of the segment; you do not add a sample at the endpoint. A sharp start or stop may click because this first lab does not yet fade the amplitude. The next guided lab teaches a gradual gain change and pauses between tones.
The next command intentionally asks for 99 milliseconds, below the supported minimum. Use a new disposable filename. The shell opens a redirected file before the program validates its arguments, so a refusal can leave an empty file; it does not promise to preserve an old file with that name.
./tone 440 99 10000 > outputs/rejected.wav
echo $? immediately displays the preceding command's native exit status: zero means success; nonzero means refusal or failure. Do not run another command between the trial and this status read.
echo $?
Expect the published range explanation and status 2. This is a normal input refusal, not a reason to reset the environment. 99 is the invalid duration argument. The existing good WAVs are separate files and remain available.
Switch to Sound Studio, refresh, select rejected.wav and load it. The page should show Cannot load sound and explain that it is not a complete supported PCM WAV. Play and download stay unavailable. An empty file cannot become a tone just because its name ends in .wav.
Return to Terminal. rm -- removes the exact file after --, which ends command options. Remove only this disposable failed output:
rm -- outputs/rejected.wav
Recover by using the supported minimum and a different new output name:
./tone 440 100 10000 > outputs/minimum-tone.wav
python3 inspect_wav.py outputs/minimum-tone.wav
Now expect 1,600 frames and 0.100 seconds. Return to Sound Studio, refresh and play minimum-tone.wav, then load short-tone.wav and use Download WAV. To check keyboard operation, press Tab through the visible controls, use Enter on Play, and use the position slider's Home, End and arrow keys. Pause before moving to another file.
You now have a reusable source file, an executable that matches it and actual playable outputs. Keep the example comparison and your own outputs. Checks run in disposable directories and read your live files; they do not repair the program or create missing work. Browser decoding and playback do not prove that a physical speaker is connected, and this bounded WAV subset is not a general audio encoder.
Summary
You connected a C loop's time index to a real sound. You changed a sample's pitch, implemented reusable sine sampling, kept amplitudes within a signed 16-bit range and saved WAVs with exact frame counts. You inspected, played and downloaded actual files and recovered from an intentional parameter refusal. Next, you can combine tones with silence and gradual fades.



