루프로 소리 만들기

ShellBeginner
지금 연습하기

소개

짧은 소리는 작업이 끝났음을 알려 줄 수 있습니다. 이 실습에서는 소리 자체를 만듭니다. C 루프가 숫자열을 계산하고, 이 숫자들이 재생 가능한 WAV 파일이 됩니다. 먼저 정상 동작하는 440 Hz 예제를 듣고 음높이만 바꾼 다음, 재사용 가능한 샘플링 함수를 완성합니다.

C 변수, 산술, 루프, 배열, 함수와 기본 터미널 탐색을 알아야 합니다. 오디오 샘플링, 라디안, 바이너리 출력은 여기서 소개하므로 포인터나 신호 처리 지식은 필요하지 않습니다. 컴파일러, 작고 원리가 드러나는 WAV 기록기, 실제 파일만 읽는 브라우저 플레이어가 준비되어 있습니다. 모든 오디오는 직접 합성하므로 녹음 장치가 필요 없습니다. 50분은 읽기와 일반적인 실수를 포함한 저자의 예상 시간입니다.

정상 동작하는 소리 듣기

이 단계에서는 정상 동작하는 0.5초 소리를 재생한 다음, 값 하나를 바꿔 더 높은 소리를 만듭니다. Terminal에서 시작하세요. cd는 디렉터리를 변경합니다. 이 절대 경로는 작업 디렉터리를 잘못 선택했을 때도 복구에 사용할 수 있습니다. 여기서 작업하세요.

cd /home/labex/project/sound-studio

샘플은 한 순간의 파형을 나타내는 부호 있는 숫자입니다. PCM은 펄스 부호 변조로, 파일이 샘플 값을 직접 저장한다는 뜻입니다. 이 실습에서는 모노라고 하는 단일 채널을 사용하며 초당 16,000개 샘플을 저장합니다. 부호 있는 16비트 샘플은 -32,768부터 32,767까지의 정수를 나타낼 수 있습니다. 양수와 음수는 파형의 양쪽이며, 0은 중심입니다. 진폭은 값이 0에서 얼마나 멀어지는지 결정합니다. 스피커나 헤드폰으로 듣는다면 편안한 음량에서 시작하세요.

준비된 sample.c는 440 Hz, 즉 초당 440주기의 사인파를 만듭니다. <math.h>에 선언된 sin은 라디안 각도를 받아 -1부터 1까지의 값을 반환합니다. 한 주기는 2 * pi 라디안입니다. 10,000을 곱하면 적당한 진폭으로 확대됩니다. 소리를 들은 뒤 시간 계산을 살펴보겠습니다.

제공된 예제를 컴파일합니다. gcc는 C를 컴파일하고, -std=c11은 C11을 선택하고, -Wall -Wextra는 유용한 경고를 켜고, -o sample은 실행 파일 이름을 지정합니다. pcm.c는 WAV 기록기를 제공합니다. 소스 파일 뒤의 -lm은 sin이 사용하는 수학 라이브러리를 링크합니다. 이 파일들은 컴파일러 경고를 내지 않아야 합니다.

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

./sample은 현재 디렉터리의 실행 파일을 실행합니다. 프로그램은 바이너리 WAV 바이트를 표준 출력에 씁니다. >는 이 바이트를 파일로 리디렉션합니다. 오디오 바이트는 읽을 수 있는 터미널 텍스트가 아니므로 리디렉션 없이 실행하지 마세요. outputs는 이미 준비된 디렉터리입니다. 다음 새 이름을 사용합니다.

./sample > outputs/sample-440.wav

성공하면 텍스트가 출력되지 않습니다. Sound Studio 탭으로 전환하세요. Refresh files를 누르고 WAV file에서 sample-440.wav를 선택한 뒤 Load sound를 누릅니다. Ready to play와 8000 samples · 16000 Hz · 0.500 s가 보여야 합니다. Play를 누르고 Pause와 Playback position도 사용해 보세요. Tab으로 위치 컨트롤에 초점을 맞춘 뒤 Home, End, 방향키를 사용할 수 있습니다. 기본 오디오 컨트롤은 1초 미만의 길이를 0:00으로 반올림해 표시할 수 있습니다. 위의 텍스트와 초 단위 위치가 정확한 길이를 보여 줍니다.

Download WAV를 눌러 실제 파일을 저장합니다. 플레이어는 만든 파일을 읽을 뿐, 없는 샘플을 생성하지 않습니다. Cannot load sound는 파일이 없거나 지원되지 않는다는 뜻입니다. 스튜디오 페이지 자체가 열리지 않는 연결 오류와는 다릅니다. 경로를 잘못 입력했다면 Terminal로 돌아가 위의 cd를 사용하고, 이름을 확인하고, 해당 명령을 다시 실행한 뒤 목록을 새로 고치세요.

Sound Studio의 첫 번째 실제 소리

선택된 sample-440.wav, 0.5초 메타데이터, Play, Playback position, Download WAV 컨트롤을 확인하세요.

Terminal로 돌아갑니다. cat은 텍스트를 표시합니다. 짧은 예제를 읽어 보세요.

cat sample.c

루프는 샘플 인덱스 0부터 7999까지 방문합니다. seconds = (double)i / rate는 인덱스를 시간으로 변환합니다. 인덱스 0은 0초, 160은 0.010초입니다. (double)은 정수 나눗셈 대신 소수 나눗셈을 사용하게 합니다. 위상 2.0 * pi * frequency * seconds는 주기를 따라 진행하는 각도입니다. (int)는 확대된 사인 값의 소수 부분을 없애 정수로 변환합니다. 제공된 wav_sample은 그 숫자의 PCM 두 바이트를 기록할 뿐 숫자를 대신 선택하지 않습니다.

이제 주파수만 바꿉니다. 다음 명령은 특정 치환을 수행하는 텍스트 편집기 sed를 사용합니다. -i는 지정 파일을 그 자리에서 편집합니다. 따옴표 안의 s/old/new/는 첫 번째 슬래시 쌍 사이의 리터럴 텍스트를 두 번째 쌍 사이의 텍스트로 바꿉니다. sample.c만 편집합니다.

sed -i 's/frequency = 440/frequency = 660/' sample.c

변경이 실행 파일에 포함되도록 다시 빌드하고 다른 파일로 저장합니다. 첫 WAV도 남겨 두세요.

gcc -std=c11 -Wall -Wextra sample.c pcm.c -lm -o sample
./sample > outputs/sample-660.wav

Sound Studio로 전환해 새로 고치고 sample-660.wav를 불러와 재생합니다. 같은 8,000개 샘플과 0.5초 길이지만 소리가 더 높습니다. 바뀐 것은 주파수뿐입니다. 편집한 sample.c, sample 실행 파일, 두 WAV를 보관하세요. 이 비교는 준비 과정이며, 다음에는 자신의 재사용 가능한 프로그램과 별도 결과를 만듭니다.

모든 샘플 직접 계산하기

이 단계에서는 다양한 음높이, 길이, 진폭을 하나의 C 함수로 처리하도록 write_tone을 완성합니다. Terminal의 /home/labex/project/sound-studio에서 시작하세요.

초당 16,000개 샘플에서 300밀리초에는 16000 * 300 / 1000 = 4800개 샘플이 필요합니다. 1밀리초는 1초의 1,000분의 1입니다. 이 샘플링 속도에서는 정수 밀리초 길이가 정확한 정수 샘플 수에 대응합니다. i = 0에서 시작해 i < count인 동안 반복하면 정확히 count개 값을 출력합니다. <=를 쓰면 불필요한 샘플이 하나 추가됩니다.

훨씬 작은 예를 생각해 보세요. 초당 여덟 번 샘플링하는 초당 한 주기의 사인파는 첫 여덟 인덱스에서 대략 0, 0.71, 1, 0.71, 0, -0.71, -1, -0.71 값을 갖습니다. 인덱스가 시간을 전진시키고 sin이 세로 위치를 제공합니다. 실제 16,000 Hz에서는 한 주기에 훨씬 많은 값이 들어갑니다.

제공된 인터페이스와 시작 함수를 읽습니다.

cat tone.h tone.c

write_tone(frequency, milliseconds, amplitude)가 모든 샘플을 생성해야 합니다. 시작 코드는 0만 쓰므로 컴파일되어도 구조상 유효하지만 무음인 WAV가 나올 수 있습니다. 파일 이름, 크기, 성공 종료만으로 소리가 있다고 증명할 수 없습니다.

main.c, tone.h, pcm.h, pcm.c, inspect_wav.py, audio.py는 변경하지 마세요. 준비된 main.c가 세 인수를 읽고 검증하고 헤더를 쓰고 함수를 호출합니다. 포인터 기반 인수 파서는 수정할 필요가 없습니다. 자신의 구현은 tone.c이며 브라우저는 준비되어 있습니다.

기록기는 포장과 합성을 분리합니다. wav_begin(count)는 헤더를 한 번 쓰고, wav_sample(value)는 전달된 정수 하나를 씁니다. 둘 다 파형을 만들지 않습니다. 작은 pcm.c는 putchar로 표준 출력에 바이트를 쓰고, fputs로 고정 청크 이름을 씁니다. C 구조체를 그대로 덤프하지 않고 하위 바이트, 상위 바이트 순으로 출력합니다. 음수 샘플은 16비트 표현으로 인코딩하며, 기록기는 부호 범위 밖의 값을 거부하고 클리핑하거나 대체하지 않습니다.

기록되는 필드는 다음과 같습니다. fmt 이름에는 끝 공백이 포함됩니다.

필드 이 실습에서의 의미
RIFF, size, WAVE WAV 컨테이너 식별. size는 파일 바이트 수에서 처음 8바이트를 뺀 값
fmt, size 16 이어지는 16개 형식 바이트 설명
Format 1, channels 1 비압축 PCM, 모노
Sample rate 16000 초당 샘플 수
Byte rate 32000 초당 16,000프레임 × 프레임당 2바이트
Block alignment 2, sample bits 16 프레임 하나는 두 바이트 샘플 하나
data, size 2 * count 뒤따르는 실제 PCM 바이트 식별 및 개수 표시

cat pcm.c로 이 제공된 래퍼를 읽을 수 있습니다. 필드 값은 출력을 설명할 뿐, 헤더만 바꾼다고 무음 샘플이 소리가 되지는 않습니다.

아래의 짧은 함수를 구현합니다. <math.h>는 sin을 선언합니다. pi는 지역 소수 상수이므로 비표준 M_PI 매크로에 의존하지 않습니다. amplitude는 정수 변환 전에 적용하는 배율입니다. 파서의 최댓값 12,000 덕분에 사인 샘플이 안전하게 16비트 범위에 들어갑니다. 각 호출의 시간 인덱스는 0부터 시작합니다.

cat > tone.c는 이 소스 파일만 덮어씁니다. here-document <<'C'는 C만 있는 마지막 줄까지 다음 줄들을 cat에 전달합니다. 구분자를 따옴표로 감싸 C 텍스트를 그대로 보존합니다. 마지막 줄까지 전체 블록을 붙여 넣으세요.

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

함수를 준비된 호출자 및 기록기와 함께 컴파일합니다. 줄 오류가 나오면 tone.c를 수정하거나 전체 교체 블록을 다시 입력하고 빌드하세요. 컴파일 실패 후 오래된 실행 파일을 실행하면 오래된 코드를 시험하게 됩니다.

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

실행 파일은 Hz 단위 주파수, 정수 밀리초 길이, 진폭 순으로 받습니다. 양 끝을 포함하는 범위는 주파수 200–1200, 길이 100–1000, 진폭 1000–12000입니다. 프로그램으로 두 새 출력을 만듭니다.

./tone 440 300 8000 > outputs/tone-440.wav
./tone 660 500 10000 > outputs/tone-660.wav

준비된 읽기 도구로 실제 저장 바이트를 검사합니다. python3는 지정한 읽기 전용 Python 도구를 실행하며 Python을 작성할 필요는 없습니다. 파일이 없거나 불완전하거나 지원되지 않으면 오류를 보고하고 절대로 재생성하지 않습니다.

python3 inspect_wav.py outputs/tone-440.wav

예상 출력:

Format: signed 16-bit mono PCM
Rate: 16000 Hz
Frames: 4800
Duration: 0.300 s
Peak: 8000

Sound Studio로 전환합니다. 새로 고치고 tone-440.wav를 불러 재생한 뒤 tone-660.wav도 재생합니다. 표시 길이는 0.300초와 0.500초입니다. 새 출력 하나도 다운로드하세요. 이들은 함수의 결과이며 sample-440.wav, sample-660.wav와 별개입니다.

플레이어에 표시된 직접 생성한 소리

선택 파일과 Ready to play는 모두 tone-660.wav를 가리키며, 메타데이터는 실제 저장된 샘플을 설명합니다.

tone.c, 동작하는 tone 실행 파일, 두 새 출력을 보관하세요. 검사는 저장 WAV를 읽고 제출 실행 파일을 시험하며, 변경하지 않은 소스를 임시 디렉터리에서 다른 유효 매개변수로 재컴파일합니다. 세 가지 모두 동작해야 합니다. 소스만 있거나, 오래된 실행 파일이거나, 예제를 복사한 것만으로는 부족합니다. 동등한 수학과 정수 반올림은 허용됩니다. 맞춤 계산된 진폭의 오차는 요청 진폭의 2%에 2샘플 단위를 더한 값 이내이고, 샘플 피크는 요청값을 최대 2단위 초과할 수 있습니다. 요청 주파수의 사인파에 대한 잔차의 제곱평균제곱근(RMS)은 진폭의 0.5%에 2단위를 더한 값 이내입니다. RMS 잔차는 평균적인 크기의 샘플 오차를 나타냅니다. 이 수치 허용차는 반올림을 위한 것이며 요청 음높이를 바꿔도 된다는 뜻은 아닙니다. 정확한 프레임 수와 PCM 형식도 필요합니다. 제공된 식은 이 한도를 충분히 만족합니다.

길이를 확인하고 거부에서 복구하기

이 단계에서는 짧고 딱 떨어지지 않는 길이에도 같은 함수를 사용하고, 잘못된 입력과 손상된 소리 파일을 구분합니다. Terminal에서 시작하세요.

733 Hz, 101밀리초, 진폭 7,373을 생성합니다. 주기가 구간 길이를 나누어떨어지게 할 필요는 없으며, count가 여전히 샘플 수를 결정합니다.

./tone 733 101 7373 > outputs/short-tone.wav
python3 inspect_wav.py outputs/short-tone.wav

Frames: 1616과 Duration: 0.101 s를 찾습니다. 16000 * 101 / 1000에서 직접 나오는 결과입니다. 마지막 샘플은 구간 끝 바로 전에 있으며 끝점에 샘플을 추가하지 않습니다. 이 첫 실습에서는 아직 진폭을 페이드하지 않으므로 급한 시작이나 종료에서 클릭 소리가 날 수 있습니다. 다음 가이드 실습에서 점진적인 게인 변화와 소리 사이의 휴지를 배웁니다.

다음 명령은 최솟값보다 작은 99밀리초를 의도적으로 요청합니다. 새 임시 이름을 쓰세요. 셸은 프로그램이 인수를 검증하기 전에 리디렉션 파일을 열므로 거부되면 빈 파일이 남을 수 있습니다. 같은 이름의 기존 파일 보존을 보장하지 않습니다.

./tone 440 99 10000 > outputs/rejected.wav

echo $?는 바로 앞 명령의 원래 종료 상태를 즉시 보여 줍니다. 0은 성공, 0이 아닌 값은 거부나 실패입니다. 시험과 상태 읽기 사이에 다른 명령을 실행하지 마세요.

echo $?

공개 범위 설명과 상태 2가 나와야 합니다. 정상적인 입력 거부이며 환경 초기화의 이유가 아닙니다. 99가 잘못된 길이 인수입니다. 기존의 정상 WAV는 별도 파일로 남아 계속 사용할 수 있습니다.

Sound Studio로 전환해 새로 고치고 rejected.wav를 선택해 불러옵니다. Cannot load sound와 함께 완전한 지원 PCM WAV가 아니라는 설명이 나와야 합니다. 재생과 다운로드는 사용할 수 없습니다. 빈 파일은 이름이 .wav로 끝난다고 소리가 되지 않습니다.

Terminal로 돌아갑니다. rm --는 옵션 끝을 나타내는 -- 뒤의 정확한 파일을 삭제합니다. 이 실패한 임시 출력만 지웁니다.

rm -- outputs/rejected.wav

지원하는 최솟값과 다른 새 출력 이름을 사용해 복구합니다.

./tone 440 100 10000 > outputs/minimum-tone.wav
python3 inspect_wav.py outputs/minimum-tone.wav

이제 1,600프레임과 0.100초가 나와야 합니다. Sound Studio로 돌아가 새로 고치고 minimum-tone.wav를 재생한 뒤 short-tone.wav를 불러 Download WAV를 사용합니다. 키보드를 확인하려면 Tab으로 보이는 컨트롤을 이동하고, Play에서 Enter를 누르며, 위치 슬라이더에서 Home, End, 방향키를 사용합니다. 다른 파일로 이동하기 전에는 일시 정지하세요.

재사용 가능한 소스, 일치하는 실행 파일, 실제 재생 가능한 출력이 준비됐습니다. 예제 비교와 직접 만든 결과를 보관하세요. 검사는 임시 디렉터리에서 실행되고 현재 파일을 읽을 뿐, 프로그램을 고치거나 빠진 결과를 만들지 않습니다. 브라우저 디코딩과 재생은 물리 스피커가 연결됐음을 증명하지 않으며, 이 한정된 WAV 부분집합은 범용 오디오 인코더가 아닙니다.

요약

C 루프의 시간 인덱스를 실제 소리와 연결했습니다. 예제의 음높이를 바꾸고 재사용 가능한 사인파 샘플링을 구현했으며, 진폭을 부호 있는 16비트 범위 안에 유지하고 정확한 프레임 수의 WAV를 저장했습니다. 실제 파일을 검사하고 재생하고 다운로드했으며, 의도적인 매개변수 거부에서 복구했습니다. 다음에는 소리, 무음, 점진적인 페이드를 조합할 수 있습니다.