- Java 100%
| .idea | ||
| build-logic | ||
| gradle | ||
| narae-core | ||
| platform-android | ||
| platform-api | ||
| platform-desktop | ||
| synthia | ||
| .gitattributes | ||
| .gitignore | ||
| AGENTS.md | ||
| build.gradle.kts | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| LICENSE | ||
| README.md | ||
| settings.gradle.kts | ||
| SYNTHESIS.md | ||
Narae DSP
Java 21+ 기반의 오디오 신호 처리 및 합성 라이브러리입니다. float planar 오디오 버퍼를 중심으로 분석기, 필터, 이펙터, wavetable·샘플링 합성기와 데스크톱 Java Sound/WAV 입출력을 제공합니다.
현재 API는 개발 중입니다. 일부 알고리즘의 파라미터와 포맷 지원 범위는 버전별로 변경될 수 있습니다.
현재 저장소는 교육·프로토타이핑과 오프라인 DSP 처리에 적합한 초기 구현 단계입니다. 모든 알고리즘이 방송/측정 표준 또는 상용 플러그인 수준으로 검증된 것은 아니므로, 제품 배포 전에는 golden reference, 장치별 latency, 수치 안정성 검증을 별도로 수행하십시오.
특징
- 외부 DSP 의존성이 거의 없는 Java 라이브러리
- Java 21 Vector API를 사용하는 배열 연산 유틸리티
- 실시간 처리 루프를 고려한 버퍼 재사용 및 SPSC lock-free ring buffer
- pitch, onset, spectrum, tempo, loudness 분석
- 필터, dynamics, delay, reverb, modulation, pitch/time processing
- sine, saw, square, triangle wavetable 합성
- 실제 악기 녹음 샘플의 어택 재생 및 sustain loop 기반 pitch 합성
- 데스크톱 Java Sound 입출력 및 WAV 스트리밍/RF64 출력
모듈
| 모듈 | 역할 |
|---|---|
narae-core |
DSP 알고리즘, 버퍼, 분석, 합성, 필터, 이펙터 |
platform-api |
플랫폼 계층이 의존하는 API 모듈 |
platform-desktop |
Java Sound, WAV reader/writer, offline renderer |
platform-android |
reflection 기반 Android AudioRecord/AudioTrack I/O |
시작하기
요구 사항:
- Java 21 이상
- Gradle wrapper 9.7.x
./gradlew :narae-core:test
./gradlew :platform-desktop:test
./gradlew :platform-android:test
./gradlew test
Java Sound를 사용하는 데스크톱 기능은 운영체제에 사용 가능한 오디오 장치가 있어야 합니다.
기본 처리 모델
AudioProcessor는 AudioBuffer를 in-place로 처리합니다. AudioBuffer는 interleaved가 아닌 채널별 planar 구조입니다.
AudioContext context = new AudioContext(48_000.0f, 256, 2);
AudioBuffer buffer = new AudioBuffer(2, 256);
AudioProcessor gain = (ctx, audio) -> {
for (int channel = 0; channel < audio.getChannels(); channel++) {
float[] samples = audio.getChannelData(channel);
for (int i = 0; i < audio.getSize(); i++) {
samples[i] *= 0.5f;
}
}
};
gain.process(context, buffer);
실시간 process()에서는 객체 생성, 파일 I/O, 동기화, 긴 계산을 피하고 미리 할당한 버퍼를 재사용해야 합니다.
핵심 클래스
코어 오디오 API
| 클래스 | 설명 |
|---|---|
AudioContext |
샘플레이트, 블록 크기, 채널 수를 담는 record |
AudioBuffer |
채널별 float[] planar 버퍼. clear, copyFrom, add 제공 |
AudioProcessor |
process(context, buffer)와 reset()을 정의하는 기본 처리 인터페이스 |
AudioNode |
DSP 그래프의 입력·출력 연결과 processor를 보유하는 노드 |
AudioGraphEngine |
노드 위상 정렬, 분기, 여러 입력의 합산, cycle 검출을 수행하는 그래프 엔진 |
분석(analysis)
| 클래스 | 설명 |
|---|---|
PitchDetector |
pitch detector 공통 인터페이스 |
PitchResult |
검출 주파수, probability, pitched 상태를 담는 재사용 결과 객체 |
YinPitchDetector |
YIN 기반 pitch 검출 |
MpmPitchDetector |
McLeod Pitch Method/NSDF 기반 pitch 검출 |
AutocorrelationPitchDetector |
autocorrelation 기반 pitch 검출 |
AudioLoudnessMeter |
peak, RMS, dBFS, dBSPL, dBA, momentary LUFS 측정 |
OnsetDetector |
energy 및 spectral flux 기반 onset 검출 |
SpectrumAnalyzer |
FFT 및 spectrum band 분석 |
TempoEstimator |
onset novelty를 이용한 BPM 추정 |
SpectralFeatures |
centroid, rolloff, flux, flatness, zero-crossing rate |
Mfcc |
mel filter bank 및 MFCC 추출 |
Pitch 검출 예시:
PitchDetector detector = new YinPitchDetector(48_000.0f, 2_048);
PitchResult result = new PitchResult();
detector.detectPitch(monoSamples, monoSamples.length, result);
if (result.isPitched()) {
System.out.println(result.getPitchHz() + " Hz");
}
필터(filter)
| 클래스 | 설명 |
|---|---|
BiQuadFilter |
biquad low/high/band-pass 필터 |
FirFilter |
상태형 mono FIR 필터 |
FilterDesign |
windowed-sinc FIR 및 low-pass biquad 계수 설계 |
ParametricEQ |
low/high cut, shelf, peaking을 제공하는 5-band EQ |
LinkwitzRileyFilter |
crossover용 Linkwitz-Riley filter |
BiQuadFilter lowPass = new BiQuadFilter();
lowPass.configureLowPass(48_000.0f, 2_000.0f, 0.707f);
lowPass.process(context, buffer);
dynamics 및 effects(effects)
| 클래스 | 설명 |
|---|---|
AudioCompressor |
threshold, ratio, knee, attack, release 기반 compressor |
MultiBandCompressor |
low/mid/high 대역별 compressor |
PeakLimiter |
ceiling과 lookahead를 사용하는 peak limiter |
WaveShaperSaturation |
tube, soft/hard 계열 saturation |
ReverbEffect |
algorithmic reverb |
ConvolutionReverb |
impulse response 기반 convolution reverb |
PingPongDelay |
좌우 delay가 교차하는 ping-pong delay |
ModulationEffect |
chorus/flanger 계열 modulation delay |
ChorusEffect |
chorus preset/modulation |
FlangerEffect |
flanger preset/modulation |
PhaseVocoderPitchShifter |
phase vocoder 기반 pitch shifting |
WsolaTimeStretcher |
WSOLA 기반 time stretching |
여러 processor는 직접 순서대로 연결할 수 있습니다.
AudioCompressor compressor = new AudioCompressor();
PeakLimiter limiter = new PeakLimiter();
ReverbEffect reverb = new ReverbEffect();
compressor.process(context, buffer);
reverb.process(context, buffer);
limiter.process(context, buffer);
합성(synthesis)
| 클래스 | 설명 |
|---|---|
WavetableOscillator |
Hermite interpolation 기반 sine/saw/square/triangle oscillator |
MultiOscillatorProcessor |
여러 wavetable oscillator를 snapshot 기반으로 합산하는 processor |
SampleAsset |
mono 녹음 샘플, 기준 주파수, sustain loop 메타데이터 |
SampleLoopOscillator |
어택 1회 재생, loop 반복, Hermite 보간, pitch 변환 |
PolyphonicSampleSynthProcessor |
고정 voice pool 기반 sample polyphony 및 voice stealing |
WavetableSynthProcessor |
wavetable 또는 sample loop와 ADSR를 결합한 단일 voice processor |
AdsrEnvelope |
attack, decay, sustain, release 상태 머신 |
LFO |
sine, triangle, saw, square 저주파 oscillator |
EnvelopeFollower |
attack/release 기반 envelope 추적기 |
AutomatedFilterProcessor |
LFO와 envelope follower로 cutoff를 자동화하는 필터 |
단일 oscillator:
WavetableOscillator oscillator = WavetableOscillator.createSine(2_048);
oscillator.setFrequency(440.0f);
for (int i = 0; i < buffer.getSize(); i++) {
float sample = oscillator.processNextSample(context.sampleRate());
for (int channel = 0; channel < buffer.getChannels(); channel++) {
buffer.getChannelData(channel)[i] = sample * 0.2f;
}
}
여러 oscillator를 합산하고 ADSR을 적용하는 예시:
WavetableOscillator sine = WavetableOscillator.createSine(2_048);
WavetableOscillator saw = WavetableOscillator.createSaw(2_048);
sine.setFrequency(440.0f);
saw.setFrequency(442.0f); // 약간의 detune
MultiOscillatorProcessor mixer = new MultiOscillatorProcessor(
java.util.List.of(sine, saw),
java.util.List.of(0.35f, 0.20f));
mixer.setMasterGain(0.8f);
mixer.process(context, buffer);
MultiOscillatorProcessor의 ADD 모드는 기존 버퍼에 합산합니다.
mixer.setMixMode(MultiOscillatorProcessor.MixMode.ADD);
MultiOscillatorProcessor는 다음 규칙으로 블록을 처리합니다.
REPLACE가 기본 모드이며, 생성된 oscillator 합을 출력 버퍼에 기록합니다.ADD모드에서는 기존 버퍼와 합산한 뒤 각 샘플을[-1.0, 1.0]로 클램핑합니다.setMasterGain은 전체 oscillator 합에 적용되며 음수 gain은 위상 반전으로 사용할 수 있습니다.addOscillator의 구성 snapshot은 블록 경계에서 적용됩니다. 배열 복사가 발생하므로 오디오 render loop 외부에서 호출하십시오.- oscillator가 비어 있는 경우
REPLACE모드는 0을 출력하고,ADD모드는 기존 신호를 유지합니다.
여러 oscillator의 합은 쉽게 clipping에 도달할 수 있으므로 oscillator별 gain과 master gain을 함께 조절하십시오. 프로세서는 모든 출력 채널에 동일한 mono 합성 결과를 복사합니다.
실제 악기 녹음본을 사용하는 샘플링 합성:
// samples는 mono 녹음 PCM이며, loopEnd는 exclusive입니다.
SampleAsset asset = new SampleAsset(
samples,
48_000.0f, // 원본 sample rate
261.6256f, // 녹음 기준음: C4
loopStart,
loopEnd);
SampleLoopOscillator sampleOscillator = new SampleLoopOscillator(asset);
sampleOscillator.setCrossfadeSamples(256);
WavetableSynthProcessor sampler = WavetableSynthProcessor.forSample(
sampleOscillator,
new AdsrEnvelope());
sampler.setMasterGain(0.8f);
sampler.triggerMidiNote(69); // A4
sampler.process(context, buffer);
sampler.releaseNote();
샘플은 loopStart까지의 어택을 한 번 재생한 뒤 loopStart부터 loopEnd 직전까지 반복합니다. 루프 경계의 클릭을 줄이려면 zero crossing 부근을 선택하고 setCrossfadeSamples를 설정하십시오. 현재 샘플러는 mono asset을 모든 출력 채널에 복사하는 단일 voice 구조이며, 여러 음을 동시에 재생하려면 voice별 SampleLoopOscillator와 WavetableSynthProcessor를 구성해 그래프에 연결할 수 있습니다.
공간 음향 및 유틸리티
| 클래스 | 설명 |
|---|---|
SpatialPanner |
위치, 거리 감쇠, ITD/IID 기반 stereo spatial processing |
Beamformer |
microphone array delay-and-sum beamforming |
Doppler |
상대 속도 기반 Doppler pitch ratio |
HrtfSpatializer |
기본 IID/ITD binaural spatializer |
SpscAudioRingBuffer |
single-producer/single-consumer lock-free float ring buffer |
VectorMath |
Vector API 기반 gain, mix, add 배열 연산 |
FastFourierTransform |
radix-2 FFT/IFFT |
Stft |
STFT 및 overlap-add ISTFT |
Transforms |
DCT/IDCT, Haar DWT/IDWT, Hilbert transform |
AudioResampler |
windowed-sinc/polyphase offline resampling |
Dither |
TPDF dither 및 1차 noise shaping |
SignalMath |
scale 및 선형 convolution |
SpscAudioRingBuffer ring = new SpscAudioRingBuffer(1_024);
ring.write(samples, 0, samples.length);
int frames = ring.read(output, 0, output.length);
그래프와 여러 oscillator
AudioGraphEngine은 여러 oscillator branch를 합산한 뒤 이펙터에 전달할 수 있습니다.
AudioGraphEngine graph = new AudioGraphEngine();
WavetableSynthProcessor voice1 = new WavetableSynthProcessor(
WavetableOscillator.createSine(2_048), new AdsrEnvelope());
WavetableSynthProcessor voice2 = new WavetableSynthProcessor(
WavetableOscillator.createSaw(2_048), new AdsrEnvelope());
AudioNode node1 = new AudioNode("voice-1", voice1);
AudioNode node2 = new AudioNode("voice-2", voice2);
graph.addNode(node1);
graph.addNode(node2);
graph.connect("voice-1", "GRAPH_OUTPUT");
graph.connect("voice-2", "GRAPH_OUTPUT");
voice1.triggerNote(440.0f);
voice2.triggerNote(660.0f);
graph.process(context, buffer);
여러 branch의 출력은 GRAPH_OUTPUT에서 합산됩니다. 합산 후에는 limiter나 master gain을 사용해 clipping을 관리하십시오.
데스크톱 입출력
platform-desktop은 Java Sound와 WAV 파일 처리를 제공합니다.
| 클래스 | 설명 |
|---|---|
AudioIO |
Java Sound 기반 실시간 입력·출력 엔진 |
OfflineAudioRenderer |
WAV를 블록 단위로 처리하는 offline renderer |
WavAudioIO |
WAV metadata, streaming reader/writer, 16/24/32-bit 및 float 출력 |
PcmCodec |
PCM signed/unsigned/float와 planar float 버퍼 사이의 변환기 |
WAV 저장
WavAudioIO.writeWavFile(outputFile, buffer, 48_000.0f);
WavAudioIO.writeWavFile(outputFile, buffer, 48_000.0f, 24, true);
WavAudioIO.writeFloatWavFile(floatWavFile, buffer, 48_000.0f);
큰 파일은 streaming API를 사용합니다.
try (WavAudioIO.Reader reader = WavAudioIO.openReader(inputFile);
WavAudioIO.Writer writer = WavAudioIO.openWriter(
outputFile, reader.sampleRate(), reader.channels(),
reader.totalFrames(), 16, false)) {
long remaining = reader.totalFrames();
while (remaining > 0) {
int requested = (int) Math.min(1_024, remaining);
AudioBuffer block = new AudioBuffer(reader.channels(), requested);
int frames = reader.read(block, 0, requested);
if (frames == 0) break;
processor.process(
new AudioContext(reader.sampleRate(), frames, reader.channels()),
block);
writer.write(block, 0, frames);
remaining -= frames;
}
}
Offline rendering
OfflineAudioRenderer renderer = new OfflineAudioRenderer(processor, 1_024);
renderer.render(inputFile, outputFile,
progress -> System.out.println((progress * 100.0f) + "%"));
취소 가능한 렌더링도 지원합니다.
renderer.render(inputFile, outputFile, null, cancellationToken::isCancelled);
실시간 Java Sound
AudioIO audio = new AudioIO(
processor,
48_000.0f,
2,
256,
false, // microphone input disabled
true // speaker output enabled
);
audio.start();
// ...
audio.stop();
지원 장치 목록은 포맷으로 조회할 수 있습니다.
AudioFormat format = new AudioFormat(48_000.0f, 16, 2, true, false);
Mixer.Info[] outputs = AudioIO.listOutputDevices(format);
Android 오디오
platform-android의 AndroidAudioIO는 Android AudioRecord와 AudioTrack을 사용한 16-bit PCM mono/stereo 실시간 처리를 제공합니다.
AndroidAudioIO audio = new AndroidAudioIO(
processor, 48_000.0f, 1, 256,
true, // AudioRecord input
true // AudioTrack output
);
audio.start();
// ...
audio.close();
Android 앱에서는 RECORD_AUDIO 권한을 manifest에 선언하고 runtime permission을 승인해야 합니다. 현재 모듈은 일반 JVM 빌드를 유지하기 위해 Android SDK를 직접 컴파일 의존성으로 연결하지 않고 reflection으로 API를 찾습니다. 따라서 실제 Android 기기/에뮬레이터에서 오디오 권한, 장치 선택, underrun/overrun, audio focus를 별도로 검증해야 합니다.
패키지 구조
narae-core/src/main/java/com/elex_project/narae/
├── analysis/ # pitch, onset, spectrum, tempo, loudness
├── effects/ # dynamics, delay, reverb, modulation, time/pitch effects
├── filter/ # biquad, EQ, crossover
├── spatial/ # spatial panner
├── synthesis/ # wavetable/sample oscillator, envelope, LFO, synth processor
└── util/ # vector math, SPSC ring buffer
platform-desktop/src/main/java/com/elex_project/narae/jvm/
├── AudioIO.java
├── OfflineAudioRenderer.java
├── PcmCodec.java
└── WavAudioIO.java
platform-android/src/main/java/com/elex_project/narae/android/
├── AndroidAudioIO.java
└── package-info.java
리뷰 결과와 고도화 로드맵
우선순위 높음
AudioGraphEngine의 그래프 변경과process()동시 실행을 분리하고, render loop에서 발생하는 버퍼/컬렉션 할당을 제거합니다.AudioLoudnessMeter를 ITU-R BS.1770-4의 정확한 K-weighting, 400ms momentary window, short-term/integrated gating과 다채널 weighting으로 교체하고 reference vector로 검증합니다.Mfcc의 mel band를 선형 Hz 간격이 아니라 mel scale 삼각 filter bank로 계산하고, window/energy normalization 및 DCT convention을 고정합니다.PhaseVocoderPitchShifter와WsolaTimeStretcher에 latency, flush/end-of-stream, overlap normalization, transient 보존 테스트를 추가합니다.AudioResampler를 streaming state API로 확장하고, polyphase coefficient bank, anti-alias cutoff, end-padding 정책을 고정한 뒤 주파수 응답을 측정합니다.
우선순위 중간
SpatialPanner,Beamformer,HrtfSpatializer에 listener pose, multichannel microphone geometry, 실제 HRTF/SOFA impulse response, 거리 변화에 따른 smooth parameter interpolation을 추가합니다.- FIR/IIR 설계에 Kaiser/equiripple, Butterworth/Chebyshev, SOS cascade, coefficient stability 검사를 추가합니다.
PcmCodec에 64-bit 정수 PCM, unsigned WAV writer, endian/format chunk 검증, deterministic dither seed, noise-shaping reference test를 추가합니다.- Android 구현을 reflection 기반 호환 계층과 실제 Android library 모듈로 분리하고, modern
AudioFormat/audio focus/performance mode API를 지원합니다.
API와 품질
- 모든 public type/member에 일관된 Javadoc,
@since, 단위와 범위 계약을 추가합니다. - 모든 DSP 클래스에 NaN/Infinity, zero-length, sample-rate 변경, reset, block-boundary 테스트를 적용합니다.
- reference WAV/impulse/sine fixtures와 성능 벤치마크를 추가하고, CI에서 core·desktop·Android JVM 테스트를 분리 실행합니다.
- 현재 테스트는 기능 회귀 중심이며, aliasing, THD+N, passband ripple, LUFS 오차, 실시간 GC, 실제 Android latency를 보증하지 않습니다.
설계 및 사용 시 주의 사항
AudioBuffer의 샘플은[-1.0, 1.0]범위를 기준으로 합니다.process()는 버퍼를 in-place로 변경합니다.- 실시간 루프에서 배열, 리스트, 파일, 로그, blocking lock을 새로 만들지 마십시오.
- 여러 oscillator를 합산하면 clipping이 발생할 수 있으므로 gain staging을 적용하십시오.
AudioProcessor.reset()은 샘플레이트나 재생 상태가 바뀔 때 호출하십시오.AudioIO의 출력 장치 사용 가능 여부는 운영체제와 Java Sound provider에 따라 달라집니다.platform-android는 AndroidAudioRecord/AudioTrack기반 실시간 PCM I/O를 제공합니다. 앱에서는RECORD_AUDIO권한을 런타임에 승인해야 하며, 실제 Android 런타임에서만 오디오 장치를 열 수 있습니다.
라이선스
프로젝트 라이선스 정책은 저장소의 라이선스 파일을 확인하십시오.
Copyright © 2026 Elex. All rights reserved.