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.
Before editing, predict the result: changing frequency from 440 to 660 should raise the pitch, while count = 8000 and rate = 16000 keep the duration at 0.500 seconds. You will change one source line, then rebuild; the existing executable and saved WAV will not change merely because the source changes.
Save your own current source as a checkpoint. cp copies the first named file to the second. This checkpoint is a copy of the working 440 Hz example you just used, not an answer supplied by setup. Do not overwrite it with a later version.
cp sample.c sample-before-pitch.c
Open the actual source in the terminal editor nano. -I ignores personal editor configuration and -l shows line numbers; the lowercase l is a letter. These options make the instructions predictable. The C source and the text to type below use ordinary ASCII characters: use straight quotes and a normal minus sign, not typographic punctuation.
nano -I -l sample.c
Nano's bottom bar writes ^ for Ctrl. For example, ^W means hold Ctrl and press W. Press Ctrl+W, type frequency = 440, then press Enter. The search finds the frequency line. Press Home to reach its beginning, then Ctrl+K once to cut that whole line. Type the following one line and press Enter, leaving every other line untouched:
const int frequency = 660;
First practice leaving an unsaved edit. Press Ctrl+X; at Save modified buffer?, press N. You have discarded only the in-memory edit. Back at the shell, display the file again:
cat sample.c
The saved line must still say frequency = 440. The old 440 Hz WAV also remains unchanged. Now make the intended edit for real:
nano -I -l sample.c
Search for frequency = 440 with Ctrl+W, Enter, then use Home, Ctrl+K and the same single replacement line above. This time press Ctrl+O to write the file, Enter to confirm the existing filename sample.c, then Ctrl+X to exit. Do not type a new filename at the save prompt.
cat sample.c
Confirm frequency = 660 and that count, rate, amplitude and the loop are unchanged. If an unsaved buffer becomes confusing, Ctrl+X, N discards it and you can reopen the saved file. If you have already saved an unwanted edit at this stage, use cp sample-before-pitch.c sample.c to restore your own 440 Hz checkpoint; then reopen sample.c and make the one-line 660 Hz edit again. This command is only for recovery, not an extra action after the successful edit.
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.
Before implementing anything, run the existing silent starter as a diagnosis. This is separate from both normal sample WAVs and from the final tone results. Compile the current starter with its prepared caller:
gcc -std=c11 -Wall -Wextra main.c tone.c pcm.c -lm -o tone
The tone 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. These valid arguments request a 440 Hz, 300 ms tone at amplitude 8,000; the incomplete function will still write only zeros:
./tone 440 300 8000 > outputs/silent-diagnostic.wav
Use the prepared reader to inspect the actual saved bytes. python3 runs this 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/silent-diagnostic.wav
The file has 4,800 frames and lasts 0.300 seconds, but its peak is 0. Switch to Sound Studio, refresh, load silent-diagnostic.wav and press Play. The position advances through a valid file even though every sample is zero. This is a silent diagnostic, not a successful tone and not a broken connection. Compare it with sample-440.wav if needed, then return to Terminal. The reader and player have not added any missing waveform.
Save the unfinished starting point before editing. This checkpoint is useful only if you want to restart this function; restoring it later would remove your implementation.
cp tone.c tone-start.c
Now turn the zeros into samples by editing only tone.c. The time calculation comes before the sine: seconds converts index to time, phase converts time to radians, and sample scales the sine and drops its fractional part. The caller's maximum amplitude of 12,000 keeps those values inside signed 16-bit range. Each function call starts i at zero.
nano -I -l tone.c
Make these small edits in the existing file. Search with Ctrl+W, confirm with Enter, and press Home before cutting or inserting a line. Keep the existing function declaration, loop and braces.
First, search for #include "tone.h". At the beginning of that line, insert the following line and a newline before the existing include. This declares sin before use:
#include <math.h>
Next, search for int count = 16000. Use Home, Ctrl+K once to remove only that line, then type these three lines, each with its newline. pi is an ordinary local constant, avoiding a nonstandard macro:
const int rate = 16000;
const double pi = 3.14159265358979323846;
int count = rate * milliseconds / 1000;
Then search for (void)frequency;. From Home, press Ctrl+K twice to remove that line and the following (void)amplitude; line. They only marked parameters as unused; your calculation will now use both.
Finally, search for wav_sample(0);. From Home, use Ctrl+K once and insert only these four lines inside the existing loop:
double seconds = (double)i / rate;
double phase = 2.0 * pi * frequency * seconds;
int sample = (int)(amplitude * sin(phase));
wav_sample(sample);
Press Ctrl+O, Enter, Ctrl+X to save to the same file and leave nano. Inspect the source you saved:
cat tone.c
The saved function must have a loop with exactly one wav_sample(sample) call for each i < count. The three new calculations are inside the loop; constants and count are before it. No edits belong in main.c or the writer. Compile that source:
gcc -std=c11 -Wall -Wextra main.c tone.c pcm.c -lm -o tone
A successful build is quiet. Save this completed source as a different checkpoint only after compilation succeeds:
cp tone.c tone-ready.c
Next practice a normal saved source error so you can recognize it. Open the same source:
nano -I -l tone.c
Search for double seconds, press End, then Backspace once to remove only its final semicolon. Save with Ctrl+O, Enter, and leave with Ctrl+X. Intentionally compile this saved error:
gcc -std=c11 -Wall -Wextra main.c tone.c pcm.c -lm -o tone
GCC should report an error near the following declaration, because the preceding statement has no ;. Line numbers help you find that area; the first reported line can be where GCC noticed the problem rather than where the missing punctuation belongs. echo $? displays the preceding command's native exit status: 0 means success and a nonzero value means failure or refusal. Read it immediately, with no other command between the failed build and this read:
echo $?
Expect 1. Do not run ./tone now: an older executable can still exist after a failed build and does not prove the newly saved source compiled. Open the file again:
nano -I -l tone.c
Search for double seconds, use End, and type the missing ;. Save and exit. If a larger saved mistake prevents this local repair, cp tone-ready.c tone.c restores your own already-compiled implementation; tone-start.c would instead restore the unfinished zero loop. After either recovery, rebuild before generating audio:
gcc -std=c11 -Wall -Wextra main.c tone.c pcm.c -lm -o tone
Proceed only after this build succeeds without warnings or errors. A saved file, an executable and an existing WAV are separate objects; the WAV changes only when you run the rebuilt executable into that output path.
Use the same frequency, duration and amplitude argument order and ranges introduced for the silent diagnostic. Create two new outputs using your now-completed program:
./tone 440 300 8000 > outputs/tone-440.wav
./tone 660 500 10000 > outputs/tone-660.wav
Inspect the newly saved output with the same read-only reader:
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.



