소개
유용한 알림은 짧은 휴지를 사이에 둔 서로 다른 두 소리로 구성할 수 있습니다. 작은 C 함수로 이 패턴을 조합한 뒤 선형 페이드로 각 소리의 시작과 끝을 점진적으로 만듭니다. 결과는 검사하고 재생하고 다운로드할 수 있는 실제 WAV입니다.
먼저 Turn a Loop into a Tone을 마치거나, 그 실습의 사인 샘플링 루프, 16 kHz 모노 PCM, 정수 변환, 컴파일, 출력 리디렉션에 익숙해야 합니다. 이 환경은 독립적입니다. 기록기, 호출자, 브라우저가 새로 준비되며 이전 실습의 파일이나 프로세스는 필요하지 않습니다. 50분은 읽기와 일반적인 오류를 포함한 저자의 예상 시간입니다.
세 구간을 순서대로 배치하기
이 단계에서는 페이드를 추가하기 전에 정상 동작하는 두 소리 패턴을 조합합니다. Terminal에서 시작하세요. 작업 디렉터리를 잘못 골랐다면 절대 경로로 복구합니다.
cd /home/labex/project/sound-studio
첫 합성 신호는 440 Hz 소리 300밀리초, 0 값 무음 150밀리초, 그 뒤 660 Hz 소리 400밀리초입니다. 순서도 결과의 일부입니다. 초당 16,000개 샘플에서 각 구간은 4,800개, 2,400개, 6,400개이며 총 13,600개, 0.850초입니다.
휴지에는 실제 0이 들어가야 합니다. 휴지 기록을 생략하면 WAV가 짧아지며, 헤더의 길이 필드를 바꿔도 없는 샘플이 추가되지 않습니다. 제공된 main.c는 전체 신호의 헤더를 한 번 쓰고 write_cue를 호출합니다. 함수들은 같은 데이터 스트림에 PCM 샘플만 추가합니다. 구간마다 새 헤더를 쓰지 마세요.
함수 선언과 시작 코드를 읽습니다.
cat cue.h cue.c
write_tone에는 이전 실습의 기본 사인 루프가 있습니다. 추가된 fade_ms는 2단계에서 페이드를 제어합니다. 첫 버전의 (void)fade_ms는 미사용 매개변수임을 명시해 경고를 막을 뿐 샘플을 생성하지 않습니다. 나머지 두 함수는 미완성입니다. 빈 연산을 실제 조합과 무음으로 바꿉니다.
write_tone을 호출할 때마다 0에서 시작하는 지역 i가 있습니다. 따라서 seconds = (double)i / rate 시간과 사인 각도가 각 구간에서 다시 시작합니다. 두 번째 660 Hz 소리는 첫 소리의 인덱스를 잘못 쓰지 않고 자신의 지역 시계를 갖습니다. 무음 루프도 0에서 시작하고 샘플마다 wav_sample(0)을 한 번 호출합니다.
앞에서처럼 sin은 라디안을 받고 한 주기는 2 * pi이며 결과는 -1부터 1입니다. 진폭을 곱한 뒤 (int)로 바꾸면 범위 내 정수 샘플이 됩니다. 제공된 pcm.c는 정수를 부호 있는 16비트 모노 PCM으로 포장할 뿐 없는 소리나 휴지를 제공하지 않습니다. main.c, cue.h, pcm.c, pcm.h, inspect_wav.py, audio.py는 변경하지 마세요. 구현은 cue.c만 하면 됩니다.
이 완전한 첫 버전을 작성합니다. 인용된 here-document는 마지막 C 줄까지 C 텍스트를 보존하며, >는 cue.c만 교체합니다.
cat > cue.c <<'C'
#include <math.h>
#include "cue.h"
#include "pcm.h"
void write_tone(int frequency, int milliseconds, int fade_ms, int amplitude) {
const int rate = 16000;
const double pi = 3.14159265358979323846;
int count = rate * milliseconds / 1000;
(void)fade_ms;
for (int i = 0; i < count; i++) {
double seconds = (double)i / rate;
double phase = 2.0 * pi * frequency * seconds;
wav_sample((int)(amplitude * sin(phase)));
}
}
void write_silence(int milliseconds) {
int count = 16000 * milliseconds / 1000;
for (int i = 0; i < count; i++) {
wav_sample(0);
}
}
void write_cue(int first_frequency, int first_ms, int gap_ms,
int second_frequency, int second_ms, int fade_ms, int amplitude) {
write_tone(first_frequency, first_ms, fade_ms, amplitude);
write_silence(gap_ms);
write_tone(second_frequency, second_ms, fade_ms, amplitude);
}
C
준비된 호출자와 기록기로 컴파일합니다. C11, 경고 옵션, 출력 이름, 마지막 수학 라이브러리 옵션은 첫 가이드 실습과 같은 역할입니다. 성공하면 경고나 오류가 없어야 합니다.
gcc -std=c11 -Wall -Wextra main.c cue.c pcm.c -lm -o cue
실행 파일은 다음 순서로 일곱 인수를 받습니다.
| 인수 | 단위와 양 끝을 포함하는 범위 |
|---|---|
| 첫 주파수 | 정수 Hz, 200–1200 |
| 첫 소리 길이 | 정수 밀리초, 100–1000 |
| 휴지 길이 | 정수 밀리초, 50–500 |
| 둘째 주파수 | 정수 Hz, 200–1200 |
| 둘째 소리 길이 | 정수 밀리초, 100–1000 |
| 각 끝의 페이드 길이 | 정수 밀리초, 5–50 |
| 페이드 전 진폭 | 정수 샘플 단위, 1000–12000 |
모든 매개변수는 헤더 바이트를 쓰기 전에 파싱됩니다. 첫 버전은 페이드 인수를 받지만 의도적으로 무시하며 2단계에서 구현합니다. 20 ms 페이드와 진폭 10,000을 요청해 기본 패턴을 생성하고, 아직 페이드가 없는 첫 결과를 별도로 남깁니다.
./cue 440 300 150 660 400 20 10000 > outputs/plain-cue.wav
python3 inspect_wav.py outputs/plain-cue.wav
Frames: 13600, Duration: 0.850 s, Peak: 10000을 확인합니다. 완전한 헤더만으로 올바른 음 순서나 무음을 증명하지 못하므로 검사에서는 실제 데이터도 분석합니다.
Sound Studio로 전환합니다. Refresh files를 누르고 plain-cue.wav를 선택해 Load sound를 누릅니다. 선택과 Ready to play가 같은 파일을 가리키는지 확인합니다. 재생하고 일시 정지하고 Playback position으로 이동합니다. Tab으로 위치 슬라이더에 초점을 맞춰 Home, End, 방향키를 사용합니다. 첫 소리는 둘째보다 낮고 중간에 뚜렷한 휴지가 있어야 합니다. 페이드가 없는 이 버전의 급격한 시작과 끝은 예상된 결과입니다. Download WAV로 실제 파일을 저장합니다.
Terminal로 돌아갑니다. plain-cue.wav, cue.c, 현재 cue 실행 파일을 남기세요. 첫 단계 검사는 이 완전한 기본 구현이나 이후의 완전한 페이드 구현을 모두 받아들이므로, 2단계로 진행해도 앞 단계 재검사가 무효가 되지 않습니다. 제출 실행 파일과 다시 빌드한 소스가 모든 조건에서 같은 허용 동작을 구현해야 합니다. 오래된 페이드 없는 실행 파일과 페이드 있는 소스의 조합으로는 부족합니다. 저장한 plain-cue.wav는 항상 페이드 없는 비교입니다. 이후 검사는 페이드를 엄격하게 요구합니다.
검사는 저장 파일을 요구하고 제출 실행 파일을 시험하며, 변경하지 않은 소스를 임시 디렉터리에서 다른 지원 매개변수로 빌드합니다. 없는 작업을 만들어 주지 않습니다. 컴파일이 실패하면 보고된 줄을 수정하거나 전체 소스 블록을 반복하고 다시 빌드한 뒤 실행하세요. 오래된 실행 파일에는 편집이 반영되지 않습니다.
각 소리 양 끝에 페이드 적용하기
이 단계에서는 점진적으로 변하는 게인으로 사인 샘플을 조절합니다. Terminal의 /home/labex/project/sound-studio에서 시작하세요.
게인은 곱하는 계수입니다. 0.0은 무음으로 만들고, 0.5는 진폭을 절반으로 하며, 1.0은 그대로 둡니다. 선형 페이드는 샘플마다 같은 양만큼 게인을 바꿉니다. 이 속도에서 20밀리초는 16000 * 20 / 1000 = 320개 페이드 샘플입니다. 첫 경사 구간의 지역 인덱스 0, 80, 160, 240, 320에서 게인은 0, 0.25, 0.5, 0.75, 1입니다.
페이드는 각 소리의 지역 처리여야 합니다. 둘째 소리에서 다시 0부터 시작하며 전체 신호에 하나의 페이드를 늘여 적용하지 마세요. count개 샘플을 가진 소리의 마지막 인덱스는 count - 1입니다. 따라서 count - 1 - i는 끝까지 남은 샘플 위치 수입니다. 이 거리를 fade로 나누면 마지막 샘플에서 정확히 0이 되는 내림 경사가 됩니다.
소수 나눗셈을 사용하세요. (double)i / fade는 분수를 보존합니다. 정수 나눗셈이면 첫 경사 거의 전체가 0이 됩니다. 범위는 양수 페이드 길이와 양쪽 경사를 위한 공간을 보장합니다. 가장 짧은 100 ms 소리에 가장 긴 50 ms 페이드를 적용해도 유효합니다. 이때 최대 음량의 평탄부는 없으며 두 이산 경사가 중앙 근처에서 만납니다. 진폭은 페이드 전 상한이지, 구간마다 정확히 그 피크인 샘플이 반드시 있다는 약속이 아닙니다.
cue.c를 완전한 페이드 버전으로 바꿉니다. 순서와 무음 함수는 같고 소리 샘플에만 게인이 추가됩니다. 두 if문이 시작과 끝 경사를 선택하며 중간 위치는 게인 1을 유지합니다. 정수로 변환하기 전에 게인을 사인 값에 곱합니다.
cat > cue.c <<'C'
#include <math.h>
#include "cue.h"
#include "pcm.h"
void write_tone(int frequency, int milliseconds, int fade_ms, int amplitude) {
const int rate = 16000;
const double pi = 3.14159265358979323846;
int count = rate * milliseconds / 1000;
int fade = rate * fade_ms / 1000;
for (int i = 0; i < count; i++) {
double gain = 1.0;
if (i < fade) {
gain = (double)i / fade;
}
if (count - 1 - i < fade) {
gain = (double)(count - 1 - i) / fade;
}
double seconds = (double)i / rate;
double phase = 2.0 * pi * frequency * seconds;
wav_sample((int)(amplitude * gain * sin(phase)));
}
}
void write_silence(int milliseconds) {
int count = 16000 * milliseconds / 1000;
for (int i = 0; i < count; i++) {
wav_sample(0);
}
}
void write_cue(int first_frequency, int first_ms, int gap_ms,
int second_frequency, int second_ms, int fade_ms, int amplitude) {
write_tone(first_frequency, first_ms, fade_ms, amplitude);
write_silence(gap_ms);
write_tone(second_frequency, second_ms, fade_ms, amplitude);
}
C
다시 빌드해 새 이름으로 페이드 버전을 저장하고 plain-cue.wav는 비교용으로 보관합니다.
gcc -std=c11 -Wall -Wextra main.c cue.c pcm.c -lm -o cue
./cue 440 300 150 660 400 20 10000 > outputs/cue.wav
python3 inspect_wav.py outputs/cue.wav
여전히 13,600프레임, 0.850초입니다. 페이드는 값만 바꾸고 구간 길이는 바꾸지 않습니다. 휴지도 정확히 2,400개 0 샘플 그대로입니다. 이 조건에서 첫 소리는 인덱스 0–4,799, 휴지는 4,800–7,199, 둘째 소리는 7,200–13,599입니다. 두 소리 함수는 전체 파일 위치와 독립적으로 각각 0에서 시작하는 지역 인덱스를 씁니다.
Sound Studio로 전환해 새로 고치고 cue.wav를 불러 재생합니다. plain-cue.wav와 비교하세요. 순서, 음높이, 휴지는 같지만 페이드 소리는 점진적으로 시작하고 끝납니다. 새 결과에서 Pause, 위치 슬라이더, Download WAV를 사용합니다. 플레이어는 이 저장 WAV를 직접 읽으며 없는 휴지나 페이드를 추가할 수 없습니다.

선택과 Ready 메시지는 cue.wav를 나타냅니다. 16,000 Hz의 13,600샘플은 0.850초이며 Download WAV는 이 실제 바이트를 저장합니다.
페이드가 있는 cue.c, 다시 빌드한 cue, 두 파일을 보관합니다. 이제 검사는 현재 프로그램이 페이드 신호를 생성하도록 요구합니다. 이전 기본 파일은 비교 결과이므로 페이드 프로그램으로 다시 생성할 필요가 없습니다.
첫 실습과 마찬가지로 동등한 수학과 반올림을 허용합니다. 검사기는 각 소리에 요청 사인 반송파와 예상 선형 게인을 맞춥니다. 맞춤 진폭 오차는 요청 진폭의 2%에 2샘플 단위를 더한 값 이내입니다. 평균적인 샘플 오차 크기를 나타내는 잔차의 제곱평균제곱근(RMS)은 진폭의 0.5%에 2단위를 더한 값 이내입니다. 피크 초과는 2단위까지이며 각 페이드 소리의 첫 샘플과 마지막 샘플은 0에서 2단위 이내여야 합니다. 이 허용차는 같은 끝점과 전체 포락선이 한도를 만족할 경우 정수 반올림과 fade - 1로 나누는 등의 동등한 이산 경사를 허용합니다. 정확한 프레임 수, 구간 순서, 0 값 휴지는 계속 필요합니다. 요청 주파수를 바꿀 별도 허가는 없습니다. 맞춤 계산을 구현할 필요는 없습니다.
경계에서도 정확한 시간 유지하기
이 단계에서는 평탄부가 없거나 소리 길이가 정수가 아닌 주기 수를 포함할 때도 같은 프로그램을 사용합니다. Terminal에서 시작하세요.
가장 짧은 지원 소리와 가장 긴 페이드를 시도합니다. 100 ms 구간에는 1,600샘플, 50 ms 경사에는 800샘플이 있습니다. 두 경사가 평탄부 없이 들어갑니다. 유효한 입력이며 거부할 오류가 아닙니다.
./cue 200 100 50 1200 100 50 1000 > outputs/edge-cue.wav
python3 inspect_wav.py outputs/edge-cue.wav
4,000프레임과 0.250초를 확인합니다. 소리마다 1,600샘플과 휴지 800샘플입니다. 경사와 샘플링된 사인 피크가 반드시 정렬되지 않으므로 피크가 1,000 미만일 수 있습니다. 진폭 한도는 여전히 적용됩니다.
이제 733 Hz를 101 ms, 휴지를 51 ms, 997 Hz를 103 ms로 하고 7 ms 페이드를 사용합니다.
./cue 733 101 51 997 103 7 7373 > outputs/odd-cue.wav
python3 inspect_wav.py outputs/odd-cue.wav
4,080프레임과 0.255초가 나와야 합니다. 첫 소리는 1,616샘플, 휴지는 816샘플, 둘째는 1,648샘플입니다. 주파수와 길이의 곱이 정수 주기일 필요는 없습니다. 샘플 수가 길이를 결정하고 지역 페이드가 각 소리를 끝에서 0으로 되돌립니다. 오래된 녹음을 자르거나 헤더만 바꿔 새 조건을 흉내 내지 마세요.
Sound Studio로 전환해 새로 고치고 edge-cue.wav, odd-cue.wav를 재생합니다. 서로 다른 길이를 확인하고 실제 컨트롤로 일시 정지와 탐색을 사용합니다. Tab은 초점을 보이고 Enter는 Play를 실행하며 Home, End, 방향키는 Playback position을 조작합니다. odd-cue.wav를 다운로드합니다. 브라우저 재생과 수치 검사는 파일의 증거일 뿐, 물리 스피커나 일반적인 오디오 제작 품질을 검증하지는 않습니다.
Terminal로 돌아갑니다. 다음 시험은 잘못된 51 ms 페이드를 의도적으로 요청합니다. 새 임시 출력 경로를 사용하세요. 전처럼 셸 리디렉션은 검증 전에 파일을 열므로 거부 후 빈 파일이 남을 수 있으며, 같은 경로의 예전 파일을 보존하지 않습니다.
./cue 440 300 150 660 400 51 10000 > outputs/rejected-fade.wav
시험과 읽기 사이에 다른 명령 없이 원래 상태를 즉시 읽습니다.
echo $?
공개 범위 설명과 상태 2가 나와야 합니다. 잘못된 것은 페이드이지 150 ms 휴지가 아닙니다. Sound Studio로 전환해 새로 고치고 rejected-fade.wav를 선택한 뒤 Load sound를 누릅니다. Cannot load sound가 나오며 재생 가능한 리소스나 다운로드가 없습니다. .wav 확장자는 빈 파일을 고치지 못합니다. Terminal로 돌아와 이 임시 출력만 삭제합니다.
rm -- outputs/rejected-fade.wav
지원하는 페이드와 새 이름으로 복구합니다.
./cue 440 300 150 660 400 20 10000 > outputs/recovered-cue.wav
python3 inspect_wav.py outputs/recovered-cue.wav
다시 13,600프레임과 0.850초가 나와야 합니다. 상대 경로를 잘못 썼다면 1단계의 절대 cd와 정확한 이름을 사용하세요. 컴파일 오류는 수정과 재빌드를, 매개변수 거부는 해당 인수 수정을 뜻합니다. Cannot load sound는 저장 파일의 존재나 형식 문제입니다. 어느 경우도 환경 초기화가 필요하지 않습니다.
cue.c, 동작하는 cue, 다섯 성공 WAV를 보관하세요. 최종 검사는 실제 파일을 읽고 공개 끝점과 딱 떨어지지 않는 조건을 포함한 임시 테스트에서 제출 실행 파일과 변경되지 않은 소스를 시험합니다. 채점은 출력을 덮어쓰거나, 현재 실행 파일을 재빌드하거나, 없는 파일을 고치거나, 대체 소리를 합성하지 않습니다. 하나의 고정 녹음이 아니라 재사용 가능한 세 구간 소리 생성기를 만들었습니다.
요약
독립적인 두 소리 호출과 실제 0 샘플의 휴지로 WAV를 조합했습니다. 각 소리 양 끝에 선형 게인을 적용하고, 정확한 정수 샘플 경계를 유지하고, 짧은 소리와 긴 페이드, 딱 떨어지지 않는 조건에 같은 프로그램을 재사용했습니다. 소스, 실행 파일, 다섯 실제 파일에 초기 비교와 완성된 페이드 동작이 함께 보존됩니다.


