Compose a Cue from Small Segments

CBeginner
Practice Now

Introduction

A useful notification can contain two different tones separated by a short pause. You will compose that pattern from small C functions, then make the beginning and end of each tone gradual with a linear fade. Your result will be a real WAV that you can inspect, play and download.

Complete Turn a Loop into a Tone first, or be comfortable with its sine sampling loop, 16 kHz mono PCM, integer conversion, compilation and output redirection. This environment is independent: its writer, caller and browser are prepared again, and it needs no files or processes from the earlier lab. The 50-minute duration is an author estimate, including reading and ordinary errors.

Put Three Segments in Order

In this step, you will compose a working two-tone pattern before adding fades. Start in Terminal. Use the absolute path to recover from a mistaken working directory:

cd /home/labex/project/sound-studio

Our first cue has a 440 Hz tone for 300 milliseconds, zero-valued silence for 150 milliseconds, then a 660 Hz tone for 400 milliseconds. The order is part of the result. At 16,000 samples per second, those segments contain 4,800, 2,400 and 6,400 samples: 13,600 in total, or 0.850 seconds.

A pause must contain actual zeros. Omitting the pause's writes would make the WAV shorter, and changing a duration field in the header would not add the missing samples. The provided main.c writes one header for the whole cue, then calls write_cue. Your functions append only PCM samples to that same data stream. Do not write a new header for each segment.

Read the function declarations and starter:

cat cue.h cue.c

write_tone contains the plain sine loop from the previous lab. Its additional fade_ms parameter will control the fade in Step 2. For this first version, (void)fade_ms explicitly marks that parameter as unused so the compiler does not warn about it; it does not generate any samples. The two other functions are incomplete. First hear the working primitive before changing those functions.

The independent example examples/tone-main.c writes a header for 4,800 frames and calls write_tone(440, 300, 20, 10000). It does not call the unfinished composition or silence functions and does not synthesize samples itself. Its requested fade is the supported value 20 ms. The current plain function ignores that argument; after you add gain, the same caller will request a faded tone. Read it:

cat examples/tone-main.c

Compile this small caller with the existing source and writer. The new -I. option tells GCC to look for headers in the current directory: the dot means here, so the file under examples/ can include cue.h and pcm.h. -I. has uppercase I; it is a compiler option, separate from nano's -I. The other options and the final -lm have the same meanings as in the first lab. A successful build should be quiet.

gcc -std=c11 -Wall -Wextra -I. examples/tone-main.c cue.c pcm.c -lm -o tone-example

The new executable is only a single-tone example, separate from the required cue program. Save its output through the familiar binary redirection:

./tone-example > outputs/single-tone.wav
python3 inspect_wav.py outputs/single-tone.wav

Expect 4,800 frames, 0.300 seconds and peak 10,000. Switch to Sound Studio, click Refresh files, select single-tone.wav and click Load sound. Confirm that the selection and Ready to play both name this file. Press Play, then Pause, move Playback position and download it with Download WAV. This normal 440 Hz sound proves the existing tone primitive works. It does not complete the two-tone cue. Return to Terminal.

Every call to write_tone has its own local i, starting at zero. Its time seconds = (double)i / rate and sine angle therefore restart for that segment. A 660 Hz second tone gets its own local clock rather than accidentally using the first tone's index. The silence loop must also start at zero and call wav_sample(0) once per sample.

As before, sin takes radians, one cycle is 2 * pi, and its result lies between -1 and 1. Multiplying by amplitude before (int) converts it to a bounded integer sample. The provided pcm.c packs those integers as signed 16-bit mono PCM; it never supplies a missing tone or pause. Keep main.c, cue.h, pcm.c, pcm.h, inspect_wav.py, audio.py and examples/tone-main.c unchanged. Only cue.c needs implementation.

Save your own unfinished starting source. cp copies the first file to the second. Do not overwrite this checkpoint with a later stage:

cp cue.c cue-start.c

Use nano to change only the two incomplete functions. -I ignores personal editor settings and -l shows line numbers. Use the ordinary ASCII C text shown below.

nano -I -l cue.c

Press Ctrl+W, type void write_silence, then Enter. This finds the existing function declaration. Leave that declaration and its braces in place. Move to the (void)milliseconds; line below it, use Home then Ctrl+K twice to cut that line and the following placeholder comment. Insert only this body, ending each line with Enter:

    int count = 16000 * milliseconds / 1000;
    for (int i = 0; i < count; i++) {
        wav_sample(0);
    }

The count converts milliseconds into samples; the loop appends an actual zero for each position. Now search for void write_cue with Ctrl+W, Enter. Its declaration spans two lines, so leave both lines and the opening brace. Move down to (void)first_frequency;, press Home, then Ctrl+K eight times: remove the seven unused-parameter lines and the one placeholder comment. Insert these three calls inside the existing braces:

    write_tone(first_frequency, first_ms, fade_ms, amplitude);
    write_silence(gap_ms);
    write_tone(second_frequency, second_ms, fade_ms, amplitude);

Do not edit write_tone in this stage. Its existing plain samples are already correct. The new function body expresses the three-segment order; it does not write another header. Press Ctrl+O, Enter to save to cue.c, then Ctrl+X to return to the shell.

cat cue.c

Check the saved stage: the tone function is still the original plain loop, silence writes zeros, and composition contains exactly the three calls above. For an unsaved mistake, Ctrl+X, N discards the buffer; reopen the saved file. For a saved mistake before completing this stage, cp cue-start.c cue.c restores your own unfinished source, after which you must repeat both local edits. This is recovery, not an extra normal step. Nano's Ctrl+W can find a function or a distinctive line without replacing the entire file.

Compile with the provided caller and writer. C11, warning flags, the output name and the final mathematics library option have the same roles as in the first guided lab. A successful build should print no warning or error.

gcc -std=c11 -Wall -Wextra main.c cue.c pcm.c -lm -o cue

After this build succeeds, save your completed plain-stage source. Keep it separate from both the unfinished checkpoint and the later faded checkpoint:

cp cue.c cue-plain.c

The executable takes seven arguments in this order:

Argument Unit and inclusive range
First frequency Whole Hz, 200–1200
First tone length Whole milliseconds, 100–1000
Pause length Whole milliseconds, 50–500
Second frequency Whole Hz, 200–1200
Second tone length Whole milliseconds, 100–1000
Fade length at each end Whole milliseconds, 5–50
Amplitude before fading Integer sample units, 1000–12000

All parameters are parsed before any header bytes are written. The first version passes but deliberately ignores the fade argument; Step 2 will implement it. Generate the normal pattern with a requested 20 ms fade and amplitude 10,000, retaining this initial unfaded result separately:

./cue 440 300 150 660 400 20 10000 > outputs/plain-cue.wav
python3 inspect_wav.py outputs/plain-cue.wav

Look for Frames: 13600, Duration: 0.850 s and Peak: 10000. A complete header alone does not prove those samples have the correct tone order or silence; the check also analyzes the real data.

Switch to Sound Studio. Click Refresh files, choose plain-cue.wav and click Load sound. Confirm that the selection and Ready to play message identify that file. Play it, pause, and use Playback position to move through it. With Tab, focus the position slider and use Home, End and the arrow keys. The first sound should be lower than the second, with a clear pause between them. A sharp onset or ending is still expected in this unfaded version. Use Download WAV to save the actual file.

Return to Terminal. Keep plain-cue.wav, cue.c and the current cue executable. The first-step check accepts either this complete plain implementation or the later complete faded implementation, so advancing to Step 2 does not invalidate an earlier recheck. Your delivered executable and rebuilt source must implement the same permitted behavior across the recipes; a stale plain executable paired with faded source is insufficient. The saved plain-cue.wav always remains the unfaded comparison. Later checks strictly require fades.

The check requires the saved file, tests the delivered executable and rebuilds your unchanged source in a disposable directory with other supported parameters. It does not create missing work. When a compilation fails, use the reported line and nearby preceding statement to locate the error in nano, save the correction and rebuild before rerunning. For example, a missing semicolon after a tone call can be reported at the next statement. An older executable does not contain your newly saved edit. Never generate a new WAV with that executable after a failed build.

Fade Both Ends of Each Tone

In this step, you will scale the sine samples with a gradually changing gain. Begin in Terminal, in /home/labex/project/sound-studio.

Gain is a multiplier: 0.0 makes a sample silent, 0.5 halves its amplitude and 1.0 leaves it unchanged. A linear fade changes gain by the same amount per sample. At our rate, 20 milliseconds is 16000 * 20 / 1000 = 320 fade samples. During the first ramp, the gains at local indices 0, 80, 160, 240 and 320 are 0, 0.25, 0.5, 0.75 and 1.

The fade must be local to each tone. Begin at zero again for the second tone; do not stretch one fade across the entire cue. For a tone with count samples, the last index is count - 1. Therefore count - 1 - i measures how many sample positions remain before that end. Dividing that distance by fade gives the descending ramp, reaching exactly zero at the final sample.

Use decimal division: (double)i / fade preserves fractions. Integer division would turn almost the entire first ramp into zeros. The range limits guarantee a positive fade length and room for both ramps: even a shortest 100 ms tone with the longest 50 ms fade is valid. That case has no full-volume plateau; the two discrete ramps meet near the middle. Amplitude is a ceiling before fading, not a promise that every segment must contain a sample exactly at that peak.

This stage changes only write_tone: add a fade-sample count, add the two local ramps inside its loop and include gain in the output expression. Keep write_silence, write_cue, the loop's time calculation and the prepared caller unchanged. Before editing, predict that the total 13,600 frames and zero-valued pause will stay the same; only the tone values near their ends will change.

nano -I -l cue.c

Use Ctrl+W, Enter to find each named line and Home before changing a whole line:

First, search for (void)fade_ms;. Cut only that line with Ctrl+K and replace it with the following line. This uses the already validated positive fade duration:

    int fade = rate * fade_ms / 1000;

Next, search for double seconds. From Home, insert these lines before it, inside the tone's loop. Retain the existing seconds and phase lines afterward:

        double gain = 1.0;
        if (i < fade) {
            gain = (double)i / fade;
        }
        if (count - 1 - i < fade) {
            gain = (double)(count - 1 - i) / fade;
        }

Then search for wav_sample((int)(amplitude * sin(phase)));. From Home, cut only that one line, then replace it with:

        wav_sample((int)(amplitude * gain * sin(phase)));

The two if statements select the starting and ending ramps; positions between them retain gain 1. The gain multiplies the sine before integer conversion. Press Ctrl+O, Enter, Ctrl+X to save and exit.

cat cue.c

Your stage checkpoint is structural: write_tone now computes a positive fade before the loop, computes gain inside it and writes amplitude * gain * sin(phase). The two composition functions must still match the plain stage. If you have not saved a mistaken buffer, Ctrl+X, N discards it. If a saved edit needs a restart during this fade stage, cp cue-plain.c cue.c restores your own complete plain composition; repeat only the fade edits above. Restoring cue-start.c would also remove the already completed pause and order.

Rebuild the saved source. If GCC reports an error, open cue.c in nano, inspect the named line and its preceding punctuation, save the correction and repeat this build. Do not run an older binary after a failed compile. Proceed only when the build is quiet:

gcc -std=c11 -Wall -Wextra main.c cue.c pcm.c -lm -o cue

Save the successfully built faded source as its own checkpoint. This is the recovery point for later stages; do not use the old plain checkpoint after advancing unless you deliberately want to remove fades.

cp cue.c cue-faded.c

Generate the new result while preserving plain-cue.wav:

./cue 440 300 150 660 400 20 10000 > outputs/cue.wav
python3 inspect_wav.py outputs/cue.wav

The result still has 13,600 frames and lasts 0.850 seconds. Fading changes sample values, not the segment lengths. The pause is still exactly 2,400 zero samples. For this recipe, the first tone occupies indices 0–4,799, the pause 4,800–7,199 and the second tone 7,200–13,599. The two tone functions still use their own local index starting at zero, independently of those whole-file positions.

Switch to Sound Studio, refresh, load cue.wav and play it. Compare it with plain-cue.wav: the order, pitch and pause are the same, while the faded tones begin and end gradually. Use Pause, the position slider and Download WAV on the new result. The player reads this saved WAV directly; it cannot add a missing pause or fade.

The faded cue loaded in Sound Studio

The selection and Ready message both identify cue.wav. Its 13,600 samples at 16,000 Hz last 0.850 seconds; Download WAV saves those actual bytes.

Return to Terminal and rerun the independent single-tone caller with your completed function. The example still asks for the valid 20 ms fade, so it now produces a faded 440 Hz tone. It must remain usable after this edit:

gcc -std=c11 -Wall -Wextra -I. examples/tone-main.c cue.c pcm.c -lm -o tone-example
./tone-example > outputs/single-tone.wav
python3 inspect_wav.py outputs/single-tone.wav

This deliberately replaces only the earlier single-tone example: it still has 4,800 frames and lasts 0.300 seconds, but its ends now fade. It does not overwrite either comparison cue. Switch to Sound Studio, refresh, load single-tone.wav again and play it; refresh and reload are necessary to read the new saved bytes. Then return to Terminal. The two-tone target and checks remain separate from this example.

Keep the faded cue.c, the rebuilt cue executable and both saved files. Checks now require the current program to generate faded cues; the older plain file remains a comparison artifact and does not have to be regenerated by the faded program.

As in the first guided lab, mathematical and rounding equivalents are allowed. For each tone, the checker fits the requested sine carrier with the expected linear gain. Fitted amplitude may differ by at most 2% of the requested amplitude plus 2 sample units; RMS residual, an average-sized sample error, may be at most 0.5% of amplitude plus 2 units. Peaks may exceed the requested amplitude by at most 2 units, and each faded tone's first and last samples must be zero within 2 units. These tolerances allow integer rounding and equivalent discrete ramp expressions, including division by fade - 1 when the same endpoints and overall envelope meet the limits. The exact frame count, segment order and zero-valued pause still apply. There is no separate permission to change the requested frequency. You do not need to implement the fitting calculation.

Keep Timing Correct at the Edges

In this step, you will reuse the same program where there is no plateau and where tone durations contain a fractional number of cycles. Begin in Terminal.

Try the shortest supported tones with the longest fade. A 100 ms segment contains 1,600 samples; a 50 ms ramp contains 800. Both ramps fit, with no plateau. This is valid input, not an error to reject.

./cue 200 100 50 1200 100 50 1000 > outputs/edge-cue.wav
python3 inspect_wav.py outputs/edge-cue.wav

Look for 4,000 frames and 0.250 seconds: 1,600 for each tone plus an 800-sample pause. The peak can be below 1,000 because the ramps and the sampled sine peaks do not necessarily align. The amplitude bound still holds.

Now use 733 Hz for 101 ms, a 51 ms pause and 997 Hz for 103 ms, with 7 ms fades:

./cue 733 101 51 997 103 7 7373 > outputs/odd-cue.wav
python3 inspect_wav.py outputs/odd-cue.wav

Expect 4,080 frames and 0.255 seconds. The first tone contains 1,616 samples, the pause 816 and the second 1,648. Frequency times duration need not be a whole number of cycles. Counting samples defines the duration, and the local fade makes each tone return to zero at its end. Do not trim an older recording or relabel its header to imitate the new recipe.

Switch to Sound Studio, refresh and play edge-cue.wav and odd-cue.wav. Check their different durations, then use the actual controls to pause and seek. Tab shows keyboard focus; Enter activates Play; Home, End and arrows operate Playback position. Download odd-cue.wav. Browser playback and numerical checks provide evidence about the files; they do not verify a physical speaker or general audio-production quality.

Return to Terminal. The next trial intentionally requests an invalid 51 ms fade. Use a new disposable output path. As before, shell redirection opens that file before validation, so a refusal may leave it empty; it does not preserve an old file at that same path.

./cue 440 300 150 660 400 51 10000 > outputs/rejected-fade.wav

Read the original native status immediately, with no command between the trial and this read:

echo $?

Expect the published range explanation and status 2. The invalid argument is the fade, not the 150 ms pause. Switch to Sound Studio, refresh, select rejected-fade.wav and click Load sound. It reports Cannot load sound with no playable resource or download; a .wav extension cannot repair an empty file. Return to Terminal and remove only that disposable output:

rm -- outputs/rejected-fade.wav

Recover using a supported fade and a new output name:

./cue 440 300 150 660 400 20 10000 > outputs/recovered-cue.wav
python3 inspect_wav.py outputs/recovered-cue.wav

Again expect 13,600 frames and 0.850 seconds. If a relative path is mistyped, use the absolute cd from Step 1 and the correct filename. A compiler error means open cue.c in nano, fix the reported area, save and rebuild; a parameter refusal means correct that argument; Cannot load sound concerns the saved file's presence or format. None requires resetting the environment.

Keep cue.c, the working cue executable and all five successful cue WAVs. If a later saved source mistake needs rollback, cp cue-faded.c cue.c restores your own completed faded implementation; then rebuild cue successfully before generating another result. Do not restore cue-plain.c and assume its unfaded behavior still satisfies the final check. Your local checkpoint files are optional recovery aids, not scoring artifacts. The final check reads those actual files and tests the delivered executable and unmodified source in disposable fixtures, including the published endpoints and non-round recipe. Scoring does not overwrite your outputs, rebuild your live executable, repair a missing file or synthesize a replacement. You have made a reusable three-segment sound generator rather than one hard-coded recording.

Summary

You composed a WAV from two independent tone calls and a real zero-sample pause. You applied a linear gain at both ends of each tone, kept exact integer sample boundaries and reused the program for short, long-fade and non-round recipes. Your source, executable and five actual files preserve both the initial comparison and the completed faded behavior.