1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
| #### RemiAudio
#### Copyright (C) 2022-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify it
#### under the terms of the GNU Affero General Public License as published by
#### the Free Software Foundation, either version 3 of the License, or (at your
#### option) any later version.
####
#### This program is distributed in the hope that it will be useful, but WITHOUT
#### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#### FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
#### License for more details.
####
#### You should have received a copy of the GNU Affero General Public License
#### along with this program. If not, see <https://www.gnu.org/licenses/>.
#### RemiAudio
#### Copyright (C) 2022-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify it
#### under the terms of the GNU Affero General Public License as published by
#### the Free Software Foundation, either version 3 of the License, or (at your
#### option) any later version.
####
#### This program is distributed in the hope that it will be useful, but WITHOUT
#### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#### FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
#### License for more details.
####
#### You should have received a copy of the GNU Affero General Public License
#### along with this program. If not, see <https://www.gnu.org/licenses/>.
require "../resampler"
module RemiAudio::DSP
# The `SoftClipper` class implements a type of limiter with a type of
# distortion that tends to be more pleasant to the ears than straight
# clipping. This will round off the peaks of the audio signal that pass above
# the maximum amplitude rather than cut them off flat.
#
# A simplified diagram can be found
# [here](https://chiselapp.com/user/MistressRemilia/repository/remiaudio/uv/soft-clipping.png).
class SoftClipper
@upsampler : RemiAudio::Resampler::Resampler?
@downsampler : RemiAudio::Resampler::Resampler?
@resampBuf : Array(Float64) = [] of Float64
@resampBuf32 : Array(Float32) = [] of Float32
getter oversampling : UInt16
getter mode : RemiAudio::Resampler::Type
# Creates a new `SoftClipper` instance that will work with the givven number
# of channels, and with the given amount of oversampling. The oversampling
# amount cannot be zero.
#
# When oversampling is `1`, then no oversampling is performed. This is the
# default to match the behavior of older versions of RemiAudio.
def initialize(*, @oversampling : UInt16 = 1u16, @channels : Int32 = 2,
@mode : RemiAudio::Resampler::Type = RemiAudio::Resampler::Type::Linear)
if @oversampling == 0
raise ArgumentError.new("Oversampling cannot be zero")
end
end
# Sets a new oversampling amount. This cannot be zero.
def oversampling=(@oversampling : UInt16) : UInt16
if @oversampling == 0
raise ArgumentError.new("Oversampling cannot be zero")
end
@oversampling
end
# Sets the resampling mode. This is not thread-safe.
def mode=(@mode : RemiAudio::Resampler::Type) : Nil
@upsampler = nil
@downsampler = nil
end
############################################################################
@[AlwaysInline]
protected def ensureResampBufSize(size : Int) : Nil
if @resampBuf.size != size * @oversampling
@resampBuf = Array(Float64).new(size * @oversampling, 0.0)
end
end
@[AlwaysInline]
protected def ensureResampBufSize32(size : Int) : Nil
if @resampBuf32.size != size * @oversampling
@resampBuf32 = Array(Float32).new(size * @oversampling, 0.0f32)
end
end
@[AlwaysInline]
private def getResampler : RemiAudio::Resampler::Resampler
case @mode
in .linear? then RemiAudio::Resampler::LinearResampler.new(@channels)
in .zero_order_hold? then RemiAudio::Resampler::LinearResampler.new(@channels)
in .sinc_fastest?
unless @channels == 2
raise RemiAudioError.new("Cannot use Sinc resampling unless channels is 2")
end
RemiAudio::Resampler::SincResamplerStereo.new(RemiAudio::Resampler::SincResampler::Quality::Fast)
in .sinc_medium?
unless @channels == 2
raise RemiAudioError.new("Cannot use Sinc resampling unless channels is 2")
end
RemiAudio::Resampler::SincResamplerStereo.new(RemiAudio::Resampler::SincResampler::Quality::Medium)
in .sinc_best?
unless @channels == 2
raise RemiAudioError.new("Cannot use Sinc resampling unless channels is 2")
end
RemiAudio::Resampler::SincResamplerStereo.new(RemiAudio::Resampler::SincResampler::Quality::Best)
end
end
@[AlwaysInline]
protected def getUpsampler : RemiAudio::Resampler::Resampler
resamp = @upsampler
if resamp.nil?
resamp = @upsampler = getResampler
end
resamp
end
@[AlwaysInline]
protected def getDownsampler : RemiAudio::Resampler::Resampler
resamp = @downsampler
if resamp.nil?
resamp = @downsampler = getResampler
end
resamp
end
# Performs upsampling on *block*, storing the results in `@resampBuf`.
@[AlwaysInline]
protected def upsample(block : Array(Float64)|Slice(Float64)) : Nil
return if @oversampling <= 1
# Ensure our resampling buf is the correct size, then resample.
ensureResampBufSize(block.size)
getUpsampler.process(block, @resampBuf, @oversampling.to_f64!)
end
# Performs upsampling on *block*, storing the results in `@resampBuf`.
@[AlwaysInline]
protected def upsample(block : Array(Float32)|Slice(Float32)) : Nil
return if @oversampling <= 1
# Ensure our resampling buf is the correct size, then resample.
ensureResampBufSize32(block.size)
getUpsampler.process(block, @resampBuf32, @oversampling.to_f64!)
end
@[AlwaysInline]
protected def downsample(block : Array(Float64)|Slice(Float64)) : Nil
return if @oversampling <= 1
# Consistency checks.
RemiLib.assert(@resampBuf.size == block.size * @oversampling)
# Downsample
getDownsampler.process(@resampBuf, block, 1 / @oversampling)
end
@[AlwaysInline]
protected def downsample(block : Array(Float32)|Slice(Float32)) : Nil
return if @oversampling <= 1
# Consistency checks.
RemiLib.assert(@resampBuf32.size == block.size * @oversampling)
# Downsample
getDownsampler.process(@resampBuf32, block, 1 / @oversampling)
end
############################################################################
# Applies soft clipping to a block of audio.
def process(maxAmplitude : Float32|Float64, block : Array(Float64)|Slice(Float64)) : Nil
if @oversampling > 1
upsample(block)
SoftClipper.process(maxAmplitude, @resampBuf)
downsample(block)
else
SoftClipper.process(maxAmplitude, block)
end
end
# Applies soft clipping to a block of audio.
def process(maxAmplitude : Float32|Float64, block : Array(Float32)|Slice(Float32)) : Nil
if @oversampling > 1
#storeAsFloat64(block)
upsample(block)
SoftClipper.process(maxAmplitude, @resampBuf32)
downsample(block)
#retrieveFloat32(block)
else
SoftClipper.process(maxAmplitude, block)
end
end
############################################################################
# Applies soft clipping to a single sample. No oversampling is performed.
@[AlwaysInline]
def self.process(maxAmplitude : Float32|Float64, sample : Float64) : Float64
maxAmp : Float64 = maxAmplitude.to_f64!
sign : Int8 = sample >= 0 ? 1i8 : -1i8
absSample : Float64 = sample.abs
if absSample > 1.0
sign * ((maxAmp + 1) * 0.5)
elsif absSample > maxAmp
sign * (maxAmp + (absSample - maxAmp) /
((((absSample - maxAmp) / (maxAmp - 1)) ** 2) + 1))
else
sample
end
end
# Applies soft clipping to a single sample. No oversampling is performed.
@[AlwaysInline]
def self.process(maxAmplitude : Float32|Float64, sample : Float32) : Float32
maxAmp : Float32 = maxAmplitude.to_f32!
sign : Int8 = sample >= 0 ? 1i8 : -1i8
absSample : Float32 = sample.abs
if absSample > 1.0f32
sign * ((maxAmp + 1) * 0.5f32)
elsif absSample > maxAmp
sign * (maxAmp + (absSample - maxAmp) /
((((absSample - maxAmp) / (maxAmp - 1)) ** 2) + 1))
else
sample
end
end
# # Applies soft clipping to a single sample. No oversampling is performed.
# @[AlwaysInline]
# def self.process(maxAmplitude : Float32|Float64, sample : Float32) : Float32
# process(maxAmplitude, sample.to_f64!).to_f32!
# end
# Applies soft clipping to a buffer of audio. No oversampling is performed.
@[AlwaysInline]
def self.process(maxAmplitude : Float32|Float64, samples : Array(Float64)|Slice(Float64)) : Nil
samples.size.times do |i|
samples.unsafe_put(i, process(maxAmplitude, samples.unsafe_fetch(i)))
end
end
# Applies soft clipping to a buffer of audio. No oversampling is performed.
@[AlwaysInline]
def self.process(maxAmplitude : Float32|Float64, samples : Array(Float32)|Slice(Float32)) : Nil
samples.size.times do |i|
samples.unsafe_put(i, process(maxAmplitude, samples.unsafe_fetch(i).to_f64!).to_f32!)
end
end
end
end
# The Soft Clipping module implements a type of limiter with a type of
# distortion that tends to be more pleasant to the ears than straight clipping.
# This will round off the peaks of the audio signal that pass above the maximum
# amplitude rather than cut them off flat.
#
# A simplified diagram can be found
# [here](https://chiselapp.com/user/MistressRemilia/repository/remiaudio/uv/soft-clipping.png).
module RemiAudio::DSP::SoftClipping
# Applies soft clipping to the two blocks of audio.
@[Deprecated("Use the new SoftClipper class")]
@[AlwaysInline]
def self.process(maxAmplitude : Float32|Float64, blockLeft : Array(Float64)|Slice(Float64),
blockRight : Array(Float64)|Slice(Float64)) : Nil
{% unless flag?(:remiaudio_wd40) %}
unless blockLeft.size == blockRight.size
raise RemiAudioError.new("Block sizes differ")
end
{% end %}
SoftClipper.process(maxAmplitude, blockLeft)
SoftClipper.process(maxAmplitude, blockRight)
end
# Applies soft clipping to a single sample.
@[Deprecated("Use the new SoftClipper class")]
@[AlwaysInline]
def self.process(maxAmplitude : Float32|Float64, sample : Float64) : Float64
SoftClipper.process(maxAmplitude, sample)
end
# Applies soft clipping to a single sample.
@[Deprecated("Use the new SoftClipper class")]
@[AlwaysInline]
def self.process(maxAmplitude : Float32|Float64, sample : Float32) : Float32
SoftClipper.process(maxAmplitude, sample)
end
end
|