[오디오 DSP] sample부터 chain까지

Gamchan Kang·2024년 9월 8일
0

JUCE

목록 보기
1/1

우리가 듣는 소리는 연속적인(Analog) 신호이다. 소리 데이터가 컴퓨터에서 연산되기 위해서는 이산적으로(Digital) 변환되어야 한다. 이를 아날로그-디지털 변환(ADC, Analog-Digital Convert)이라고 한다. ADC 과정은 CS보다는 전자 공학에 가깝다. 그래서 다른 블로그에 정리했다. 이 글을 읽기 전에 저 글을 먼저 읽으면 도움이 될 것이다.

* JUCE 프레임워크 코드 예시로 작성된 포스팅입니다.

샘플(Sample)

샘플은 아날로그 신호를 일정한 간격으로 측정한 값이다. 즉, 디지털 오디오의 가장 작은 단위이다. 샘플의 개수는 샘플링 레이트(Sampling Rate)에 의해 결정된다. 단위는 Hz인데, 1초 동안 몇 개의 샘플을 취하는지 나타낸다.

대중 음악에서 가장 보편적인 샘플링 레이트는 44.1kHz인데, 1초에 44100개의 샘플을 취한다. 샘플링 레이트가 높을 수록 음성 데이터는 메모리를 많이 차지하고, 낮을 수록 원래 음성 데이터를 잘 나타내지 못한다.

juce::AudioBuffer<float> buffer (2, 512);  // 스테레오 버퍼 (2채널, 512 샘플)

for (int channel = 0; channel < buffer.getNumChannels(); ++channel) {
    float* channelData = buffer.getWritePointer(channel);  // 채널별 샘플에 대한 포인터 가져오기

    for (int i = 0; i < buffer.getNumSamples(); ++i) {
        // 각 샘플을 0.5로 설정 (진폭이 50%로 줄어듦)
        channelData[i] = channelData[i] * 0.5f;
    }
}

버퍼(Buffer)

버퍼는 여러 샘플을 담는 메모리 공간이다. 버퍼를 사용해 여러 개의 오디오 샘플을 한 번에 처리할 수 있다. 버퍼는 여러 개의 채널(channel) 단위로 구성된다. 이때, 채널은 오디오 신호의 경로를 의미한다. 단일 사운드로 이루어진 오디오를 모노, 좌우 다르게 들리는 오디오를 스테레오라고 한다.

juce::AudioBuffer<float> buffer (2, 512);  // 2채널 (스테레오) 버퍼, 512개의 샘플

buffer.clear();  // 버퍼를 0으로 초기화, 메모리에 남아있는 이전 데이터 혹은 불필요한 값을 제거함

// 왼쪽 채널에 샘플 값을 설정 (0번째 채널)
float* leftChannel = buffer.getWritePointer(0);  // 0번째 채널 = 왼쪽 채널
for (int i = 0; i < buffer.getNumSamples(); ++i) {
    leftChannel[i] = 0.5f;  // 모든 샘플을 0.5로 설정 (왼쪽 채널)
}

// 오른쪽 채널에 샘플 값을 설정 (1번째 채널)
float* rightChannel = buffer.getWritePointer(1);  // 1번째 채널 = 오른쪽 채널
for (int i = 0; i < buffer.getNumSamples(); ++i) {
    rightChannel[i] = -0.5f;  // 모든 샘플을 -0.5로 설정 (오른쪽 채널)
}

모노에서 사인파 생성 후 스테레오 변환(동일한 각 채널로 확장)

juce::AudioBuffer<float> monoBuffer(1, 512);  // 모노 버퍼 (1채널, 512 샘플)
juce::AudioBuffer<float> stereoBuffer(2, 512);  // 스테레오 버퍼 (2채널, 512 샘플)

// 모노 버퍼에 샘플 값을 설정 (단일 채널)
float* monoChannel = monoBuffer.getWritePointer(0);
for (int i = 0; i < monoBuffer.getNumSamples(); ++i) {
    monoChannel[i] = std::sin(2.0f * M_PI * 440.0f * i / 44100.0f);  // 사인파 생성
}

// 모노 데이터를 스테레오 버퍼로 복사 (양쪽 채널에 동일한 값)
float* leftChannel = stereoBuffer.getWritePointer(0);
float* rightChannel = stereoBuffer.getWritePointer(1);
for (int i = 0; i < stereoBuffer.getNumSamples(); ++i) {
    leftChannel[i] = monoChannel[i];  // 왼쪽 채널에 모노 데이터 복사
    rightChannel[i] = monoChannel[i];  // 오른쪽 채널에도 동일한 데이터 복사
}

블록(Block)

블록은 오디오 데이터를 작은 단위로 처리하기 위한 샘플들의 묶음이다. 버퍼와는 다른 점은 버퍼는 실제 메모리 공간, 블록은 특정 시간 동안 버퍼에서 처리하는 단위를 의미한다. 주로 오디오 프로세서가 한 번에 처리하는 샘플들의 집합을 블록이라고 한다.

Low Cut Filter를 적용하는 코드

// 2채널, 512 샘플 버퍼 생성
juce::AudioBuffer<float> buffer (2, 512);

// AudioBlock을 생성하여 버퍼를 감쌈
juce::dsp::AudioBlock<float> audioBlock(buffer);

// Low Cut 필터 생성 및 설정
juce::dsp::IIR::Filter<float> lowCutFilter;
auto coefficients = juce::dsp::IIR::Coefficients<float>::makeHighPass(44100.0, 200.0);  // 200Hz 이상만 통과시키는 필터
lowCutFilter.coefficients = coefficients;

// Low Cut 필터를 적용하기 위한 프로세서 준비
juce::dsp::ProcessSpec spec;
spec.sampleRate = 44100.0;
spec.maximumBlockSize = (juce::uint32) buffer.getNumSamples();
spec.numChannels = (juce::uint32) buffer.getNumChannels();
lowCutFilter.prepare(spec);

// Low Cut 필터를 각 채널에 적용 (AudioBlock 사용)
juce::dsp::ProcessContextReplacing<float> context(audioBlock);
lowCutFilter.process(context);

프로세서(Processor)

프로세서는 오디오 데이터를 처리하는 객체이다. 흔히 접하는 오디오 효과인 EQ, 컴프레서, 리버브 등은 모두 프로세서라고 생각하면 된다. 프로세서는 입력된 오디오 데이터를 읽어서 변형하고 출력 버퍼에 결과를 기록한다.

class SimpleGainProcessor : public juce::AudioProcessor {
public:
    void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer&) override {
        for (int channel = 0; channel < buffer.getNumChannels(); ++channel) {
            float* channelData = buffer.getWritePointer(channel);
            for (int i = 0; i < buffer.getNumSamples(); ++i)
                channelData[i] *= gain;  // 모든 샘플에 gain을 적용
        }
    }

    void setGain(float newGain) { gain = newGain; }

private:
    float gain = 1.0f;  // 기본 게인 값은 1.0 (변하지 않음)
};

컨텍스트(Context)

컨텍스트는 오디오 데이터를 처리하는 동안 프로세서가 참조해야 할 상태 정보를 포함한 객체이다. 주로 입력 버퍼, 출력 버퍼, 샘플링 레이트, 채널 수 등의 정보를 포함하며, 오디오 처리 루프 내에서 사용된다.

juce::AudioBuffer<float> buffer (2, 512);  // 스테레오 버퍼
juce::dsp::ProcessSpec spec {
	44100.0,
    (uint32) buffer.getNumSamples(),
    (uint32) buffer.getNumChannels()
};

juce::dsp::ProcessContextReplacing<float> context (buffer);

// 프로세서를 준비하고 컨텍스트를 사용해 처리하는 예시
juce::dsp::Gain<float> gainProcessor;
gainProcessor.prepare(spec);  // 프로세서 준비
gainProcessor.setGainDecibels(-6.0f);  // -6dB로 게인 설정
gainProcessor.process(context);  // 오디오 블록을 처리

체인(Chain)

체인은 여러 개의 프로세서를 순차적으로 연결하여 데이터를 처리하는 방식이다. 각 프로세서는 차례대로 오디오 데이터를 처리하며, 직렬로 연결된다.

juce::AudioBuffer<float> buffer (2, 512);
juce::dsp::AudioBlock<float> block (buffer);
juce::dsp::ProcessContextReplacing<float> context (block);

juce::dsp::ProcessorChain<juce::dsp::Gain<float>, juce::dsp::IIR::Filter<float>> processorChain;
// Gain -> Filter 순으로 체인

// 체인 준비
juce::dsp::ProcessSpec spec {
	44100.0,
    (uint32) buffer.getNumSamples(),
    (uint32) buffer.getNumChannels()
};
processorChain.prepare(spec);

// 각 프로세서에 설정을 적용
processorChain.get<0>().setGainDecibels(-6.0f);  // Gain 프로세서
processorChain.get<1>().state = juce::dsp::IIR::Coefficients<float>::makeLowPass(44100, 2000.0f);  // Low-pass 필터

// 체인을 통해 오디오 데이터 처리: -6dB 게인 -> 2000Hz Low Pass Filter
processorChain.process(context);
profile
Someday, the dream will come true

0개의 댓글