Skip to content

Commit f6db4ce

Browse files
authored
Add DiodeLadderFilter.swift (#30)
* Add `DiodeLadderFilter.swift` Based on the Will Pirkle and CCRMA Chugins emulation already present in Soundpipe -- `diode.c` * Add tests for `DiodeLadderFilter` * Add wait and fix sample rate Needed to change the sample rate back to normal for the other tests to work and also added a `wait()` for the parameter ramping test to record the correct values * Use MD5's from Rosetta I forgot that I'm using an M1 Mac and the workrunners use Intel Macs
1 parent b301947 commit f6db4ce

File tree

4 files changed

+350
-0
lines changed

4 files changed

+350
-0
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright AudioKit. All Rights Reserved.
2+
3+
#include "SoundpipeDSPBase.h"
4+
#include "ParameterRamper.h"
5+
#include "Soundpipe.h"
6+
7+
enum DiodeLadderFilterParameter : AUParameterAddress {
8+
DiodeLadderFilterParameterCutoffFrequency,
9+
DiodeLadderFilterParameterResonance,
10+
};
11+
12+
class DiodeLadderFilterDSP : public SoundpipeDSPBase {
13+
private:
14+
sp_diode *diode0;
15+
sp_diode *diode1;
16+
ParameterRamper cutoffFrequencyRamp;
17+
ParameterRamper resonanceRamp;
18+
19+
public:
20+
DiodeLadderFilterDSP() {
21+
parameters[DiodeLadderFilterParameterCutoffFrequency] = &cutoffFrequencyRamp;
22+
parameters[DiodeLadderFilterParameterResonance] = &resonanceRamp;
23+
}
24+
25+
void init(int channelCount, double sampleRate) override {
26+
SoundpipeDSPBase::init(channelCount, sampleRate);
27+
sp_diode_create(&diode0);
28+
sp_diode_init(sp, diode0);
29+
sp_diode_create(&diode1);
30+
sp_diode_init(sp, diode1);
31+
}
32+
33+
void deinit() override {
34+
SoundpipeDSPBase::deinit();
35+
sp_diode_destroy(&diode0);
36+
sp_diode_destroy(&diode1);
37+
}
38+
39+
void reset() override {
40+
SoundpipeDSPBase::reset();
41+
if (!isInitialized) return;
42+
sp_diode_init(sp, diode0);
43+
sp_diode_init(sp, diode1);
44+
}
45+
46+
void process(FrameRange range) override {
47+
for (int i : range) {
48+
49+
diode0->freq = diode1->freq = cutoffFrequencyRamp.getAndStep();
50+
diode0->res = diode1->res = resonanceRamp.getAndStep();
51+
52+
float leftIn = inputSample(0, i);
53+
float rightIn = inputSample(1, i);
54+
55+
float &leftOut = outputSample(0, i);
56+
float &rightOut = outputSample(1, i);
57+
58+
sp_diode_compute(sp, diode0, &leftIn, &leftOut);
59+
sp_diode_compute(sp, diode1, &rightIn, &rightOut);
60+
}
61+
}
62+
};
63+
64+
AK_REGISTER_DSP(DiodeLadderFilterDSP, "diod")
65+
AK_REGISTER_PARAMETER(DiodeLadderFilterParameterCutoffFrequency)
66+
AK_REGISTER_PARAMETER(DiodeLadderFilterParameterResonance)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/SoundpipeAudioKit/
2+
3+
import AudioKit
4+
import AudioKitEX
5+
import AVFoundation
6+
import CAudioKitEX
7+
8+
/// Diode Ladder Filter is an new digital implementation of the VCS3 ladder filter
9+
/// based on the work of Will Pirkle
10+
/// "Virtual Analog (VA) Diode Ladder Filter"
11+
///
12+
public class DiodeLadderFilter: Node {
13+
let input: Node
14+
15+
/// Connected nodes
16+
public var connections: [Node] { [input] }
17+
18+
/// Underlying AVAudioNode
19+
public var avAudioNode = instantiate(effect: "diod")
20+
21+
// MARK: - Parameters
22+
23+
/// Specification for cutoffFrequency (still determining best ranges)
24+
public static let cutoffFrequencyDef = NodeParameterDef(
25+
identifier: "cutoffFrequency",
26+
name: "Cutoff Frequency",
27+
address: akGetParameterAddress("DiodeLadderFilterParameterCutoffFrequency"),
28+
defaultValue: 1000.0,
29+
range: 12.0 ... 20000.0,
30+
unit: .hertz
31+
)
32+
33+
/// Filter cutoff frequency
34+
@Parameter(cutoffFrequencyDef) public var cutoffFrequency: AUValue
35+
36+
/// Specification for resonance (still determining best ranges)
37+
public static let resonanceDef = NodeParameterDef(
38+
identifier: "resonance",
39+
name: "Resonance",
40+
address: akGetParameterAddress("DiodeLadderFilterParameterResonance"),
41+
defaultValue: 0.5,
42+
range: 0.0 ... 1.0,
43+
unit: .generic
44+
)
45+
46+
/// Resonance
47+
@Parameter(resonanceDef) public var resonance: AUValue
48+
49+
// MARK: - Initialization
50+
51+
/// Initialize this filter node
52+
///
53+
/// - Parameters:
54+
/// - input: Input node to process
55+
/// - cutoffFrequency: Filter cutoff frequency
56+
/// - resonance: Resonance
57+
///
58+
public init(
59+
_ input: Node,
60+
cutoffFrequency: AUValue = cutoffFrequencyDef.defaultValue,
61+
resonance: AUValue = resonanceDef.defaultValue
62+
) {
63+
self.input = input
64+
65+
setupParameters()
66+
67+
self.cutoffFrequency = cutoffFrequency
68+
self.resonance = resonance
69+
}
70+
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/SoundpipeAudioKit/
2+
3+
import AudioKit
4+
import SoundpipeAudioKit
5+
import XCTest
6+
7+
class DiodeLadderFilterTests: XCTestCase {
8+
func testBypass() {
9+
let engine = AudioEngine()
10+
let input = Oscillator(waveform: Table(.sine),
11+
frequency: 440,
12+
amplitude: 1.0,
13+
detuningOffset: 0.0,
14+
detuningMultiplier: 1.0)
15+
let filter = DiodeLadderFilter(input)
16+
17+
engine.output = filter
18+
19+
input.start()
20+
21+
let audio = engine.startTest(totalDuration: 2.0)
22+
23+
let originalAudio = engine.render(duration: 1.0)
24+
audio.append(originalAudio)
25+
26+
filter.bypass()
27+
28+
let bypassedAudio = engine.render(duration: 1.0)
29+
audio.append(bypassedAudio)
30+
31+
guard let originalAudioPeak = originalAudio.peak()?.amplitude else {
32+
XCTFail("The audio data appears to have no peak")
33+
return
34+
}
35+
guard let bypassedAudioPeak = bypassedAudio.peak()?.amplitude else {
36+
XCTFail("The audio data appears to have no peak")
37+
return
38+
}
39+
XCTAssertNotEqual(originalAudioPeak, bypassedAudioPeak, accuracy: 0.01)
40+
41+
testMD5(audio)
42+
}
43+
func testInitialParameterValues() {
44+
let input = Oscillator(waveform: Table(.sine),
45+
frequency: 440,
46+
amplitude: 1.0,
47+
detuningOffset: 0.0,
48+
detuningMultiplier: 1.0)
49+
let filter = DiodeLadderFilter(input)
50+
51+
guard let filterParamTree = filter.au.parameterTree else {
52+
XCTFail("No parameter tree found")
53+
return
54+
}
55+
56+
let cutoffFrequencyAddress = DiodeLadderFilter.cutoffFrequencyDef.address
57+
let resonanceAddress = DiodeLadderFilter.resonanceDef.address
58+
guard let cutoffFrequencyParam = filterParamTree.parameter(withAddress: cutoffFrequencyAddress) else {
59+
XCTFail("Parameter address not found for: \(DiodeLadderFilter.cutoffFrequencyDef.name)")
60+
return
61+
}
62+
guard let resonanceParam = filterParamTree.parameter(withAddress: resonanceAddress) else {
63+
XCTFail("Parameter address not found for: \(DiodeLadderFilter.resonanceDef.name)")
64+
return
65+
}
66+
67+
XCTAssertEqual(cutoffFrequencyParam.value, DiodeLadderFilter.cutoffFrequencyDef.defaultValue)
68+
XCTAssertEqual(filter.cutoffFrequency, DiodeLadderFilter.cutoffFrequencyDef.defaultValue)
69+
XCTAssertEqual(resonanceParam.value, DiodeLadderFilter.resonanceDef.defaultValue)
70+
XCTAssertEqual(filter.resonance, DiodeLadderFilter.resonanceDef.defaultValue)
71+
}
72+
func testParameterRamping() {
73+
let engine = AudioEngine()
74+
let input = Oscillator(waveform: Table(.sine),
75+
frequency: 440,
76+
amplitude: 1.0,
77+
detuningOffset: 0.0,
78+
detuningMultiplier: 1.0)
79+
let filter = DiodeLadderFilter(input)
80+
81+
guard let filterParamTree = filter.au.parameterTree else {
82+
XCTFail("No parameter tree found")
83+
return
84+
}
85+
86+
let cutoffFrequencyAddress = DiodeLadderFilter.cutoffFrequencyDef.address
87+
let resonanceAddress = DiodeLadderFilter.resonanceDef.address
88+
guard let cutoffFrequencyParam = filterParamTree.parameter(withAddress: cutoffFrequencyAddress) else {
89+
XCTFail("Parameter address not found for: \(DiodeLadderFilter.cutoffFrequencyDef.name)")
90+
return
91+
}
92+
guard let resonanceParam = filterParamTree.parameter(withAddress: resonanceAddress) else {
93+
XCTFail("Parameter address not found for: \(DiodeLadderFilter.resonanceDef.name)")
94+
return
95+
}
96+
97+
engine.output = filter
98+
99+
input.start()
100+
101+
let audio = engine.startTest(totalDuration: 1.0)
102+
103+
let initialFreq: AUValue = 12.0
104+
let finalFreq: AUValue = 500.0
105+
let initialRes: AUValue = 0.2
106+
let finalRes: AUValue = 1.0
107+
let duration: Float = 1.0
108+
109+
filter.$cutoffFrequency.ramp(from: initialFreq, to: finalFreq, duration: duration)
110+
filter.$resonance.ramp(from: initialRes, to: finalRes, duration: duration)
111+
112+
audio.append(engine.render(duration: 0.02))
113+
wait(for: 0.02)
114+
115+
XCTAssertEqual(filter.cutoffFrequency, initialFreq)
116+
XCTAssertEqual(cutoffFrequencyParam.value, initialFreq)
117+
XCTAssertEqual(filter.resonance, initialRes)
118+
XCTAssertEqual(resonanceParam.value, initialRes)
119+
120+
audio.append(engine.render(duration: 0.98))
121+
122+
XCTAssertEqual(filter.cutoffFrequency, finalFreq)
123+
XCTAssertEqual(cutoffFrequencyParam.value, finalFreq)
124+
XCTAssertEqual(filter.resonance, finalRes)
125+
XCTAssertEqual(resonanceParam.value, finalRes)
126+
127+
testMD5(audio)
128+
}
129+
func testReset() {
130+
let engine = AudioEngine()
131+
let input = Oscillator(waveform: Table(.sine),
132+
frequency: 440,
133+
amplitude: 1.0,
134+
detuningOffset: 0.0,
135+
detuningMultiplier: 1.0)
136+
let filter = DiodeLadderFilter(input)
137+
138+
engine.output = filter
139+
140+
input.start()
141+
142+
let audio = engine.startTest(totalDuration: 2.0)
143+
guard let sampleRate = engine.mainMixerNode?.outputFormat.sampleRate else {
144+
XCTFail("No sample rate to render audio")
145+
return
146+
}
147+
if sampleRate == 0 {
148+
XCTFail("Can't render audio with 0 Hz sample rate")
149+
}
150+
151+
audio.append(engine.render(duration: 1.0))
152+
XCTAssertEqual(audio.toFloatChannelData()?[0].count, Int(sampleRate))
153+
154+
filter.reset()
155+
156+
audio.append(engine.render(duration: 1.0))
157+
XCTAssertEqual(audio.toFloatChannelData()?[0].count, Int(sampleRate) * 2)
158+
159+
testMD5(audio)
160+
}
161+
func testSampleRateChange() {
162+
let engine = AudioEngine()
163+
let input = Oscillator(waveform: Table(.sine),
164+
frequency: 440,
165+
amplitude: 1.0,
166+
detuningOffset: 0.0,
167+
detuningMultiplier: 1.0)
168+
let filter = DiodeLadderFilter(input)
169+
170+
engine.output = filter
171+
172+
input.start()
173+
174+
let audio = engine.startTest(totalDuration: 2.090909090909091)
175+
guard let sampleRate = engine.mainMixerNode?.outputFormat.sampleRate else {
176+
XCTFail("No sample rate to render audio")
177+
return
178+
}
179+
if sampleRate == 0 {
180+
XCTFail("Can't render audio with 0 Hz sample rate")
181+
}
182+
183+
XCTAssertEqual(sampleRate, Settings.sampleRate)
184+
185+
audio.append(engine.render(duration: 1.0))
186+
XCTAssertEqual(audio.toFloatChannelData()?[0].count, Int(sampleRate))
187+
188+
Settings.sampleRate = 48000
189+
190+
guard let newSampleRate = engine.mainMixerNode?.outputFormat.sampleRate else {
191+
XCTFail("No sample rate to render audio")
192+
return
193+
}
194+
XCTAssertEqual(Settings.sampleRate, newSampleRate)
195+
196+
audio.append(engine.render(duration: 1.0))
197+
XCTAssertEqual(audio.toFloatChannelData()?[0].count, Int(sampleRate) + Int(newSampleRate))
198+
199+
testMD5(audio)
200+
Settings.sampleRate = 44100 // Put this back to default for other tests
201+
}
202+
// for waiting in the background for realtime testing
203+
func wait(for interval: TimeInterval) {
204+
let delayExpectation = XCTestExpectation(description: "delayExpectation")
205+
DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
206+
delayExpectation.fulfill()
207+
}
208+
wait(for: [delayExpectation], timeout: interval + 1)
209+
}
210+
}

Tests/SoundpipeAudioKitTests/ValidatedMD5s.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ let validatedMD5s: [String: String] = [
2424
"-[BalancerTests testDefault]": "26e2c62078ee266c120677b7386ab292",
2525
"-[ConvolutionTests testConvolution]": "d585f94eba7aedafd7987c68af78ff75",
2626
"-[ConvolutionTests testStereoConvolution]": "4b7904aebc448fc6d5fbcdefba320b2a",
27+
"-[DiodeLadderFilterTests testBypass]": "2aeb94f662f38dde83d9bb70c5ceeac0",
28+
"-[DiodeLadderFilterTests testParameterRamping]": "b82608eee1e82b421d0ecc3425764c1e",
29+
"-[DiodeLadderFilterTests testReset]": "72e56cdab82e8f23ea0473bf261861ec",
30+
"-[DiodeLadderFilterTests testSampleRateChange]": "bf552ba8542b20f9c229c0d510cd443c",
2731
"-[DrumSynthTests testSynthKick]": "bc0323f2529a42eb1c30245cae4662cf",
2832
"-[DrumSynthTests testSynthSnare]": "8642993a77e40ce3d3d505b8e0782205",
2933
"-[DryWetMixerTests testBalance0]": "54fb40c15242198d45b31b6a79187d07",

0 commit comments

Comments
 (0)