TOOLS GAMES DEVLOG DOCS ABOUT CONTACT
HOME > DOCS > C / raylib > 외부 사운드 파일 없는 C 언어 절차적 파형 합성 및 44바이트 WAV 헤더 직렬화Procedural Audio Waveform Synthesis and 44-Byte WAV Header Serialization in C
C / raylib Advanced C99 / raylib Audio

외부 사운드 파일 없는 C 언어 절차적 파형 합성 및 44바이트 WAV 헤더 직렬화Procedural Audio Waveform Synthesis and 44-Byte WAV Header Serialization in C

외부 .wav/.ogg 에셋 파일에 의존하지 않고 C 언어 삼각함수(sinf), 지수 감쇠 엔벨로프, LFSR 의사 난수 노이즈로 16비트 PCM 샘플을 합성하여 RIFF/WAVE 메모리 버퍼로 직렬화하는 기법입니다.Synthesizing 16-bit PCM audio samples using C math functions (sinf, exponential decay envelopes, LFSR noise) and serializing a 44-byte RIFF/WAVE header in memory without external sound files.

#C #Audio #DSP #WAV #Procedural
핵심 개념단일 실행 파일(Single-Binary) 아케이드 게임이나 임베디드 환경에서는 외부 사운드 파일을 디스크에서 로드하는 과정이 배포를 번거롭게 만듭니다. 사운드의 본질은 시간($t$)에 따른 압력 배열이므로, C 언어의 sinf()와 선형 피드백 시프트 레지스터(LFSR)로 16비트 부호 있는 정수(int16_t) PCM 파형을 계산하고 앞에 44바이트 표준 RIFF/WAVE 헤더를 붙이면 메모리 상에서 즉시 재생 가능한 사운드 에셋을 생성할 수 있습니다.
Key ConceptIn single-binary arcade games or embedded systems, bundling and loading external .wav or .ogg files adds deployment complexity. Because digital audio is simply a time-series array of air pressure values, you can compute 16-bit signed integer (int16_t) PCM waveforms using sinf() and Linear Feedback Shift Registers (LFSR), prepend a 44-byte standard RIFF/WAVE header, and play the resulting memory buffer directly.

1. 44바이트 RIFF/WAVE 헤더 구조체 및 주파수 스윕 PCM 합성기 구현1. Implementing the 44-Byte RIFF/WAVE Header and Frequency-Sweep PCM Synthesizer

샘플 레이트 44,100Hz, 단일 채널(Mono), 16비트 PCM 규격에 맞춰 44바이트 헤더를 패딩 없이 기록하고, 위상 누적기(phase)를 적분하여 주파수가 급격히 변할 때도 파형이 끊기지 않는 연속 사인파/구형파 샘플을 생성합니다.

Pack the 44-byte header for 44,100Hz mono 16-bit PCM without struct padding, and integrate a phase accumulator (phase) so frequency sweeps produce smooth waveforms without phase discontinuities.

procedural_wav_synth.c
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#define SAMPLE_RATE 44100
#define TWO_PI      6.28318530718f

#pragma pack(push, 1)
typedef struct {
    char     riff_id[4];      // "RIFF"
    uint32_t file_size;       // 36 + data_size
    char     wave_id[4];      // "WAVE"
    char     fmt_id[4];       // "fmt "
    uint32_t fmt_size;        // 16 (PCM)
    uint16_t audio_format;    // 1 (Linear Quantization)
    uint16_t num_channels;    // 1 (Mono)
    uint32_t sample_rate;     // 44100
    uint32_t byte_rate;       // sample_rate * num_channels * 2
    uint16_t block_align;     // num_channels * 2
    uint16_t bits_per_sample; // 16
    char     data_id[4];      // "data"
    uint32_t data_size;       // num_samples * 2
} WavHeader44;
#pragma pack(pop)

// 메모리 버퍼에 44바이트 WAV 헤더 + 16비트 PCM 레이저/점프 효과음 생성
uint8_t* BuildProceduralSfxWav(float duration_sec, float start_hz, float end_hz, size_t *out_bytes) {
    uint32_t num_samples = (uint32_t)(SAMPLE_RATE * duration_sec);
    uint32_t data_size   = num_samples * sizeof(int16_t);
    size_t   total_bytes = sizeof(WavHeader44) + data_size;

    uint8_t *buffer = (uint8_t *)malloc(total_bytes);
    if (!buffer) return NULL;

    WavHeader44 hdr = {
        .riff_id = {'R','I','F','F'}, .file_size = 36 + data_size,
        .wave_id = {'W','A','V','E'}, .fmt_id = {'f','m','t',' '},
        .fmt_size = 16, .audio_format = 1, .num_channels = 1,
        .sample_rate = SAMPLE_RATE, .byte_rate = SAMPLE_RATE * 2,
        .block_align = 2, .bits_per_sample = 16,
        .data_id = {'d','a','t','a'}, .data_size = data_size
    };
    memcpy(buffer, &hdr, sizeof(WavHeader44));

    int16_t *pcm = (int16_t *)(buffer + sizeof(WavHeader44));
    float phase = 0.0f;

    for (uint32_t i = 0; i < num_samples; ++i) {
        float t = (float)i / (float)num_samples; // 정규화 진행률 (0.0 ~ 1.0)
        float freq = start_hz + (end_hz - start_hz) * t;
        float env  = expf(-5.0f * t);            // 지수 감쇠 볼륨 엔벨로프

        phase += (TWO_PI * freq) / (float)SAMPLE_RATE;
        if (phase >= TWO_PI) phase -= TWO_PI;

        float sample_f = sinf(phase) * env;
        pcm[i] = (int16_t)(sample_f * 28000.0f); // 16비트 클리핑 방지 여유폭 확보
    }

    if (out_bytes) *out_bytes = total_bytes;
    return buffer; // raylib LoadWaveFromMemory(".wav", buffer, total_bytes)에 즉시 전달 가능
}
procedural_wav_synth.c
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#define SAMPLE_RATE 44100
#define TWO_PI      6.28318530718f

#pragma pack(push, 1)
typedef struct {
    char     riff_id[4];      // "RIFF"
    uint32_t file_size;       // 36 + data_size
    char     wave_id[4];      // "WAVE"
    char     fmt_id[4];       // "fmt "
    uint32_t fmt_size;        // 16 (PCM)
    uint16_t audio_format;    // 1 (Linear Quantization)
    uint16_t num_channels;    // 1 (Mono)
    uint32_t sample_rate;     // 44100
    uint32_t byte_rate;       // sample_rate * num_channels * 2
    uint16_t block_align;     // num_channels * 2
    uint16_t bits_per_sample; // 16
    char     data_id[4];      // "data"
    uint32_t data_size;       // num_samples * 2
} WavHeader44;
#pragma pack(pop)

// Generate a 44-byte WAV header + 16-bit PCM laser/jump sound effect in memory
uint8_t* BuildProceduralSfxWav(float duration_sec, float start_hz, float end_hz, size_t *out_bytes) {
    uint32_t num_samples = (uint32_t)(SAMPLE_RATE * duration_sec);
    uint32_t data_size   = num_samples * sizeof(int16_t);
    size_t   total_bytes = sizeof(WavHeader44) + data_size;

    uint8_t *buffer = (uint8_t *)malloc(total_bytes);
    if (!buffer) return NULL;

    WavHeader44 hdr = {
        .riff_id = {'R','I','F','F'}, .file_size = 36 + data_size,
        .wave_id = {'W','A','V','E'}, .fmt_id = {'f','m','t',' '},
        .fmt_size = 16, .audio_format = 1, .num_channels = 1,
        .sample_rate = SAMPLE_RATE, .byte_rate = SAMPLE_RATE * 2,
        .block_align = 2, .bits_per_sample = 16,
        .data_id = {'d','a','t','a'}, .data_size = data_size
    };
    memcpy(buffer, &hdr, sizeof(WavHeader44));

    int16_t *pcm = (int16_t *)(buffer + sizeof(WavHeader44));
    float phase = 0.0f;

    for (uint32_t i = 0; i < num_samples; ++i) {
        float t = (float)i / (float)num_samples; // Normalized progress (0.0 ~ 1.0)
        float freq = start_hz + (end_hz - start_hz) * t;
        float env  = expf(-5.0f * t);            // Exponential decay volume envelope

        phase += (TWO_PI * freq) / (float)SAMPLE_RATE;
        if (phase >= TWO_PI) phase -= TWO_PI;

        float sample_f = sinf(phase) * env;
        pcm[i] = (int16_t)(sample_f * 28000.0f); // Headroom to prevent 16-bit clipping
    }

    if (out_bytes) *out_bytes = total_bytes;
    return buffer; // Can be passed directly to raylib LoadWaveFromMemory(".wav", buffer, total_bytes)
}

2. 파형(Waveform) 수식 및 게임 효과음 매핑2. Waveform Formulas and Game Sound Effect Mapping

좌우로 밀어서 표 전체 내용을 확인하세요
파형 종류시간/위상 수식 ($\phi \in [0, 2\pi)$)음색 특징 및 주요 효과음 용도
Sine (사인파)$y = \sin(\phi)$배음이 없는 순음; 코인 획득, UI 알림음, 서브 베이스 타격음
Square (구형파)$y = \sin(\phi) \ge 0 \; ? \; 1.0 : -1.0$홀수 배음이 풍부한 8비트 음색; 점프, 레이저 발사, 레트로 메뉴 이동
Sawtooth (톱니파)$y = \frac{\phi}{\pi} - 1.0$짝수·홀수 배음이 모두 포함된 날카로운 음색; 엔진 배기음, 경고 알람
LFSR White Noiselfsr ^= (lfsr >> 7); lfsr ^= (lfsr << 9);비주기적 광대역 스펙트럼; 폭발음, 충돌 파열음, 하이햇 타악기
Swipe horizontally to view the full table
Waveform TypeTime / Phase Formula ($\phi \in [0, 2\pi)$)Timbre Characteristics and SFX Use Cases
Sine Wave$y = \sin(\phi)$Pure fundamental tone without harmonics; coin pickups, UI chimes, sub-bass impacts
Square Wave$y = \sin(\phi) \ge 0 \; ? \; 1.0 : -1.0$Rich odd harmonics with 8-bit chiptune character; jumps, laser shots, retro menu blips
Sawtooth Wave$y = \frac{\phi}{\pi} - 1.0$Contains both even and odd harmonics for a sharp buzz; engine rumble, warning alarms
LFSR White Noiselfsr ^= (lfsr >> 7); lfsr ^= (lfsr << 9);Aperiodic broadband spectrum; explosions, impact bursts, hi-hat percussion

3. 실무 적용 및 주의사항3. Practical Considerations and Caveats

  • 위상 연속성(Phase Continuity) 보장: 주파수가 시간에 따라 변하는 스윕(Sweep) 효과음을 만들 때 sinf(TWO_PI * freq * t)처럼 절대 시간 t를 직접 곱하면 위상이 급격히 튀면서 찢어지는 잡음이 납니다. 반드시 샘플마다 phase += (TWO_PI * freq) / SAMPLE_RATE로 위상을 누적해야 합니다.
  • 구조체 패딩 방지(#pragma pack): 44바이트 WAV 헤더는 바이트 오프셋이 엄격히 고정된 바이너리 포맷이므로, 컴파일러가 임의로 정렬 패딩을 넣지 않도록 #pragma pack(push, 1)을 지정하고 sizeof(WavHeader44) == 44인지 확인해야 합니다.
  • 16비트 정수 오버플로우(Clipping) 방지: int16_t의 표현 범위는 -32768 ~ 32767입니다. 여러 파형을 합성할 때 합산 진폭이 32767을 넘으면 정수 오버플로우로 인해 극심한 디지털 파열음이 발생하므로 스케일 상수를 28000.0f 수준으로 여유 있게 제한하거나 클램핑을 거칩니다.
  • Preserve Phase Continuity: Evaluating sinf(TWO_PI * freq * t) directly when freq changes over time causes sudden phase jumps and audible clicking. Always integrate phase per sample via phase += (TWO_PI * freq) / SAMPLE_RATE.
  • Prevent Struct Padding (#pragma pack): Because the 44-byte WAV header is a fixed binary layout, wrap the struct definition in #pragma pack(push, 1) to prevent compiler alignment padding and verify sizeof(WavHeader44) == 44.
  • Avoid 16-Bit Integer Overflow (Clipping): The range of int16_t is -32768 to 32767. Exceeding 32767 wraps around and produces harsh digital distortion, so scale normalized samples by around 28000.0f or clamp the output.

C 언어 수준에서 직접 PCM 파형을 합성하고 메모리 내 WAV 바이너리로 직렬화하면, 외부 에셋 파일 누락 걱정 없이 수 킬로바이트의 코드만으로 다채로운 아케이드 사운드 시스템을 구축할 수 있습니다.

Synthesizing PCM waveforms in C and serializing them into in-memory WAV buffers enables self-contained arcade audio systems in a few kilobytes of code without external asset files.