|
1 | | -//============================================================================== |
2 | | -#if 0 && ! DOXYGEN |
3 | | - //These are some bits of code that could be useful someday... |
4 | | - |
5 | | - /** Obtain the maximum value, pre-negative a number of bits to the */ |
6 | | - inline float getMaxValueForNumBits (int numBits) noexcept |
7 | | - { |
8 | | - BigInteger biggy; |
9 | | - biggy.setRange (0, numBits - 1, true); |
10 | | - |
11 | | - return (float) biggy.toInteger() * 0.5f; //Halved to offset, making the middle-most number 0 dBFS |
12 | | - } |
13 | | - |
14 | | - /** Discrete sample bit-conversion algorithm */ |
15 | | - inline float reduceSample (float sample, int bitDepth) noexcept |
| 1 | +BitCrusherProcessor::BitCrusherProcessor() : |
| 2 | + InternalProcessor (false) |
| 3 | +{ |
| 4 | + presets = |
16 | 5 | { |
17 | | - return (float) std::clamp (getMaxValueForNumBits (bitDepth) * sample, -maxValue, maxValue); |
18 | | - } |
19 | | - |
20 | | - inline constexpr float compressSample (float sample, float scale) noexcept |
| 6 | + { NEEDS_TRANS ("CleanPass"), 16.0f, 1.0f, 1.0f }, |
| 7 | + { NEEDS_TRANS ("PixelPulse"), 8.0f, 12.0f, 2.5f }, |
| 8 | + { NEEDS_TRANS ("PocketConsole"), 5.0f, 16.0f, 3.0f }, |
| 9 | + { NEEDS_TRANS ("Lofi"), 10.0f, 4.0f, 6.0f }, |
| 10 | + { NEEDS_TRANS ("WoodgrainFury"), 4.0f, 32.0f, 6.0f }, |
| 11 | + { NEEDS_TRANS ("BrokenDAC"), 3.0f, 64.0f, 10.0f }, |
| 12 | + { NEEDS_TRANS ("AliasBoi"), 4.0f, 72.0f, 10.0f } |
| 13 | + }; |
| 14 | + |
| 15 | + auto layout = createDefaultParameterLayout(); |
| 16 | + |
| 17 | + auto addFloatParam = [&] (StringRef id, StringRef name, |
| 18 | + float start, float end, float defaultValue, |
| 19 | + float interval, float skew) |
21 | 20 | { |
22 | | - return sample * scale; |
23 | | - } |
| 21 | + auto newParam = std::make_unique<AudioParameterFloat> (id, name, |
| 22 | + NormalisableRange<float> (start, end, interval, skew), |
| 23 | + defaultValue); |
| 24 | + auto* np = newParam.get(); |
| 25 | + layout.add (std::move (newParam)); |
| 26 | + return np; |
| 27 | + }; |
24 | 28 |
|
25 | | - // Ye olde... |
26 | | - m = (float) (1 << (localBitDepth - 1)); |
27 | | - M = 1.0f / (float) m; |
| 29 | + bitDepthParam = addFloatParam ("depth", NEEDS_TRANS ("Depth"), 4.0f, 16.0f, 8.0f, 1.0f, 0.4f); |
| 30 | + downsampleFactorParam = addFloatParam ("downsampleFactor", NEEDS_TRANS ("Downsample Factor"), 1.0f, 128.0f, 8.0f, 1.0f, 0.4f); |
| 31 | + driveParam = addFloatParam ("drive", NEEDS_TRANS ("Drive"), 1.0f, 20.0f, 2.5f, 0.001f, 0.5f); |
28 | 32 |
|
29 | | - for (int i = numChannels; --i >= 0;) |
30 | | - for (int f = numSamples; --f >= 0;) |
31 | | - dest[i][f] = (float) ((float) src[i][f] * m * M); |
| 33 | + resetAPVTSWithLayout (std::move (layout)); |
32 | 34 |
|
33 | | -template<typename FloatType> |
34 | | -inline FloatType decimate (FloatType input, int keepBits) |
35 | | -{ |
36 | | - const auto quantum = std::pow (static_cast<FloatType> (2), (FloatType) keepBits); |
37 | | - return std::floor (input * quantum) / quantum; |
| 35 | + setCurrentProgramDirectly (1); |
38 | 36 | } |
39 | | -#endif |
40 | 37 |
|
41 | 38 | //============================================================================== |
42 | | -inline double crushToNBit (double sample, int bitDepth) |
43 | | -{ |
44 | | - constexpr auto one = 1.0; |
45 | | - constexpr auto two = 2.0; |
46 | | - |
47 | | - const auto bd = bitDepth; |
48 | | - auto s = 0.0; |
49 | | - |
50 | | - if (sample >= 1.0) |
51 | | - s = std::pow (two, bd - 1) - one; |
52 | | - else if (sample <= -1.0) |
53 | | - s = std::pow (-two, bd - 1); |
54 | | - else |
55 | | - s = std::floor (sample * -std::pow (-two, bd - one)); |
| 39 | +int BitCrusherProcessor::getNumPrograms() { return std::max (1, (int) presets.size()); } |
| 40 | +int BitCrusherProcessor::getCurrentProgram() { return programIndex; } |
56 | 41 |
|
57 | | - // NB: Deliberately quantising with casts here! |
58 | | - return (double) (int) s; |
| 42 | +void BitCrusherProcessor::setCurrentProgramDirectly (int index) |
| 43 | +{ |
| 44 | + programIndex = index; |
| 45 | + const auto& p = presets[index]; |
| 46 | + setBitDepth ((int) p.bitDepth); |
| 47 | + setDownsampleFactor ((int) p.downsample); |
| 48 | + setDrive ((float) p.drive); |
59 | 49 | } |
60 | 50 |
|
61 | | -inline double crushBit (double sample, int bitDepth) |
| 51 | +void BitCrusherProcessor::setCurrentProgram (int index) |
62 | 52 | { |
63 | | -/* |
64 | | - auto mixAmount = std::lerp (1.0, 32.0, static_cast<double> (bitDepth) / 32.0) / 32.0; |
65 | | -
|
66 | | - mixAmount *= 0.08; |
67 | | -
|
68 | | - mixAmount = 0.0; |
69 | | -
|
70 | | - sample = (sample * (1.0 - mixAmount)) |
71 | | - + (DistortionFunctions::hyperbolicTangentSoftClipping (sample) * mixAmount); |
72 | | -*/ |
73 | | - sample = crushToNBit (sample, bitDepth); |
74 | | - sample /= -std::pow (-2.0, (double) bitDepth - 1.0); |
75 | | - return sample; |
| 53 | + // Don't check if programIndex is already set; |
| 54 | + // doing this allows "resetting" to the program. |
| 55 | + if (isPositiveAndBelow (index, (int) presets.size())) |
| 56 | + setCurrentProgramDirectly (index); |
76 | 57 | } |
77 | 58 |
|
78 | | -//============================================================================== |
79 | | -BitCrusherProcessor::BitCrusherProcessor() |
| 59 | +const String BitCrusherProcessor::getProgramName (int index) |
80 | 60 | { |
81 | | - AudioProcessor::addParameter (bitDepth); |
| 61 | + if (isPositiveAndBelow (index, (int) presets.size())) |
| 62 | + return TRANS (presets[index].name); |
| 63 | + |
| 64 | + return {}; |
82 | 65 | } |
83 | 66 |
|
84 | 67 | //============================================================================== |
85 | | -void BitCrusherProcessor::setBitDepth (int newBitDepth) |
| 68 | +void BitCrusherProcessor::prepareToPlay (const double sampleRate, const int estimatedSamplesPerBlock) |
86 | 69 | { |
87 | | - bitDepth->operator= (newBitDepth); |
88 | | -} |
| 70 | + InternalProcessor::prepareToPlay (sampleRate, estimatedSamplesPerBlock); |
89 | 71 |
|
90 | | -int BitCrusherProcessor::getBitDepth() const noexcept |
91 | | -{ |
92 | | - return bitDepth->get(); |
| 72 | + const auto tc = jmax (2, getTotalNumInputChannels(), getTotalNumOutputChannels()); |
| 73 | + floatStates.resize (tc); |
| 74 | + doubleStates.resize (tc); |
93 | 75 | } |
94 | 76 |
|
| 77 | +void BitCrusherProcessor::setBitDepth (int v) { bitDepthParam->operator= ((float) v); } |
| 78 | +int BitCrusherProcessor::getBitDepth() const noexcept { return (int) bitDepthParam->get(); } |
| 79 | +void BitCrusherProcessor::setDownsampleFactor (int v) { downsampleFactorParam->operator= ((float) v); } |
| 80 | +int BitCrusherProcessor::getDownsampleFactor() const noexcept { return (int) downsampleFactorParam->get(); } |
| 81 | +void BitCrusherProcessor::setDrive (float v) { driveParam->operator= (v); } |
| 82 | +float BitCrusherProcessor::getDrive() const noexcept { return driveParam->get(); } |
| 83 | + |
95 | 84 | //============================================================================== |
96 | | -void BitCrusherProcessor::processBlock (juce::AudioBuffer<float>& buffer, MidiBuffer&) { process (buffer); } |
97 | | -void BitCrusherProcessor::processBlock (juce::AudioBuffer<double>& buffer, MidiBuffer&) { process (buffer); } |
| 85 | +void BitCrusherProcessor::processBlock (juce::AudioBuffer<float>& buffer, MidiBuffer&) { process<float> (buffer, floatStates); } |
| 86 | +void BitCrusherProcessor::processBlock (juce::AudioBuffer<double>& buffer, MidiBuffer&) { process<double> (buffer, doubleStates); } |
98 | 87 |
|
99 | 88 | template<typename FloatType> |
100 | | -void BitCrusherProcessor::process (juce::AudioBuffer<FloatType>& buffer) |
| 89 | +FloatType BitCrusherProcessor::crush (FloatType sample, ChannelState<FloatType>& state, |
| 90 | + FloatType levels, FloatType makeup, |
| 91 | + FloatType drive, int dsFactor) |
101 | 92 | { |
102 | | - if (isBypassed()) |
103 | | - return; |
| 93 | + // Pre-gain + saturation |
| 94 | + sample = juce::dsp::FastMathApproximations::tanh (sample * drive); |
104 | 95 |
|
105 | | - const auto localBitDepth = bitDepth->get(); |
| 96 | + // Sample-rate reduction (ZOH) |
| 97 | + if (--state.holdCounter <= 0) |
| 98 | + { |
| 99 | + state.heldSample = sample; |
| 100 | + state.holdCounter = dsFactor; // precomputed int >= 1 |
| 101 | + } |
| 102 | + |
| 103 | + // Bit depth reduction |
| 104 | + // levels = 1 << bits |
| 105 | + sample = std::round (state.heldSample * levels) / levels; |
106 | 106 |
|
107 | | - if (buffer.hasBeenCleared() || localBitDepth >= 16) |
| 107 | + // Makeup gain |
| 108 | + sample *= makeup; // 1 / sqrt(drive) |
| 109 | + |
| 110 | + return sample; |
| 111 | +} |
| 112 | + |
| 113 | +template<typename FloatType> |
| 114 | +void BitCrusherProcessor::process (juce::AudioBuffer<FloatType>& buffer, Array<ChannelState<FloatType>>& states) |
| 115 | +{ |
| 116 | + const auto numChannels = buffer.getNumChannels(); |
| 117 | + const auto numSamples = buffer.getNumSamples(); |
| 118 | + if (isBypassed() |
| 119 | + || numChannels <= 0 |
| 120 | + || numSamples <= 0 |
| 121 | + || buffer.hasBeenCleared()) |
108 | 122 | return; // Nothing to do here. |
109 | 123 |
|
110 | | - for (auto channel : AudioBufferView (buffer)) |
111 | | - for (auto& sample : channel) |
112 | | - sample = (FloatType) crushBit ((double) sample, localBitDepth); |
| 124 | + const auto localBitDepth = getBitDepth(); |
| 125 | + const auto localDrive = (FloatType) getDrive(); |
| 126 | + const auto localDownsampleFactor = getDownsampleFactor(); |
| 127 | + const auto levels = FloatType (1 << localBitDepth); |
| 128 | + const auto makeup = FloatType (1) / std::sqrt (localDrive); |
| 129 | + |
| 130 | + auto chans = buffer.getArrayOfWritePointers(); |
| 131 | + |
| 132 | + for (int i = 0; i < numChannels; ++i) |
| 133 | + { |
| 134 | + if (auto chan = chans[i]) |
| 135 | + { |
| 136 | + auto& state = states.getReference (i); |
| 137 | + for (int f = 0; f < numSamples; ++f) |
| 138 | + chan[f] = crush (chan[f], state, levels, makeup, localDrive, localDownsampleFactor); |
| 139 | + } |
| 140 | + } |
113 | 141 | } |
| 142 | + |
0 commit comments