Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions doc/12_roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ I have a lot of ideas for a '1.2' version of six sines. Not 2.0. Still compatibl
- LFO -> M etc... depth as an additive and attenuation target

## Noise Upgrades
- Pink, and Tilt (with N for tilt)
- Scale by 0.1 for finer control with modulation
- Pink, White, Tilt (with N for tilt) **DONE**
- Old BaconPaul Chip LFSR 'semi-tuned' (with N for 'sequence')

## Smaller Things from the crew
- Can we smooth MPE pitch bend with a lagger? (Or do we)
- Unison retuning slider could cubic rescale
- Mod target sliders scale matches display rescaling based on target (could be tricky)
- Now we have DES move MPE to the instance
Expand Down
109 changes: 109 additions & 0 deletions src/dsp/noise_helper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Six Sines
*
* A synth with audio rate modulation.
*
* Copyright 2024-2025, Paul Walker and Various authors, as described in the github
* transaction log.
*
* This source repo is released under the MIT license, but has
* GPL3 dependencies, as such the combined work will be
* released under GPL3.
*
* The source code and license are at https://github.com/baconpaul/six-sines
*/

#ifndef BACONPAUL_SIX_SINES_DSP_NOISE_HELPER_H
#define BACONPAUL_SIX_SINES_DSP_NOISE_HELPER_H

#include <algorithm>
#include <cmath>
#include <cstdint>

#include "sst/basic-blocks/dsp/PinkNoise.h"
#include "sst/basic-blocks/dsp/RNG.h"
#include "sst/basic-blocks/tables/DbToLinearProvider.h"
#include "sst/filters/FastTiltNoiseFilter.h"

#include "synth/patch.h"

namespace baconpaul::six_sines
{
struct NoiseHelper
{
using NoiseType = Patch::SourceNode::NoiseType;

// Tilt filter takes ±tiltMaxDb across the bipolar N range.
static constexpr float tiltMaxDb{6.f};

struct Host
{
const sst::basic_blocks::tables::DbToLinearProvider *db{nullptr};
double sri{1.0 / 48000.0};
float dbToLinear(float dbVal) { return db->dbToLinear(dbVal); }
float getSampleRateInv() { return static_cast<float>(sri); }
} host;

sst::basic_blocks::dsp::PinkNoise pinkNoise;
sst::filters::FastTiltNoiseFilter<Host> tiltFilter;
sst::basic_blocks::dsp::RNG &rng;

NoiseHelper(sst::basic_blocks::dsp::RNG &r,
const sst::basic_blocks::tables::DbToLinearProvider &dbProv)
: pinkNoise(r.unifU32()), tiltFilter(host), rng(r)
{
host.db = &dbProv;
}

void setSampleRate(double sr) { host.sri = 1.0 / sr; }

// Map the patch's unipolar [0,1] N value onto a bipolar tilt gain in dB.
static float nToTiltDb(float n) { return (n * 2.f - 1.f) * tiltMaxDb; }

// Push 11 white samples through the tilt filter to prime its history.
void warmup()
{
float w[11];
for (int i = 0; i < 11; ++i)
w[i] = rng.unifPM1();
tiltFilter.init(w, nToTiltDb(0.5f));
}

// Refill 16 samples of the chosen noise color into buf, normalized to ~±1.
// CHIP_LFSR is silent for now until the LFSR generator lands.
void fill16(float buf[16], NoiseType type, float nValue)
{
switch (type)
{
case NoiseType::WHITE:
for (int i = 0; i < 16; ++i)
buf[i] = rng.unifPM1();
break;
case NoiseType::PINK:
pinkNoise.generate16(buf);
break;
case NoiseType::TILT:
{
// Slope is half the user-visible tilt; positive tilt also gets a linear
// -4 dB/tiltDb attenuation to compensate for the high-shelf gain stack.
auto tiltDb = std::clamp(nToTiltDb(nValue), -tiltMaxDb, tiltMaxDb);
tiltFilter.setCoeff(tiltDb * 0.5f);
auto atten = (tiltDb > 0.f) ? host.dbToLinear(-4.f * tiltDb) : 1.f;
for (int i = 0; i < 16; ++i)
{
buf[i] = rng.unifPM1();
sst::filters::FastTiltNoiseFilter<Host>::step(tiltFilter, buf[i]);
buf[i] *= atten;
}
break;
}
case NoiseType::CHIP_LFSR:
for (int i = 0; i < 16; ++i)
buf[i] = 0.f;
break;
}
}
};
} // namespace baconpaul::six_sines

#endif
51 changes: 41 additions & 10 deletions src/dsp/op_source.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,14 @@

#include "configuration.h"

#include "sst/basic-blocks/dsp/PinkNoise.h"

#include "dsp/sintable.h"
#include "dsp/node_support.h"
#include "synth/patch.h"
#include "synth/mono_values.h"
#include "synth/voice_values.h"
#include "remap_functions.h"
#include "resonant_window.h"
#include "noise_helper.h"

namespace baconpaul::six_sines
{
Expand Down Expand Up @@ -73,7 +72,7 @@ struct alignas(16) OpSource : public EnvelopeSupport<Patch::SourceNode>,
ktlo(sn.keyTrackValueIsLow), ktlov(sn.keyTrackLowFrequencyValue),
startPhase(sn.startingPhase), octTranspose(sn.octTranspose),
lfoToRatioFine(sn.lfoToRatioFine), envToRatioFine(sn.envToRatioFine),
pinkNoise(mv.rng.unifU32())
noiseHelper(mv.rng, mv.dbToLinear)
{
reset();
}
Expand Down Expand Up @@ -122,10 +121,16 @@ struct alignas(16) OpSource : public EnvelopeSupport<Patch::SourceNode>,
{
extendedLagM.setRateInMilliseconds(10, monoValues.sr.samplerate, blockSizeInv);
extendedLagM.snapTo(sourceNode.extendedModeM.value);
// N is unused in all current extended modes — leave its lag uninitialized
// until a future mode that consumes N is added.
}
if (em == EM::NOISE)
{
extendedLagN.setRateInMilliseconds(10, monoValues.sr.samplerate, blockSizeInv);
extendedLagN.snapTo(sourceNode.extendedModeN.value);
}
}
noiseHelper.setSampleRate(monoValues.sr.sampleRate);
noiseHelper.warmup();
noisePos = 16;
zeroInputs();
snapActive();

Expand Down Expand Up @@ -196,6 +201,8 @@ struct alignas(16) OpSource : public EnvelopeSupport<Patch::SourceNode>,
static_cast<EM>(static_cast<uint32_t>(std::round(sourceNode.extendedModeMode.value)));
if (em == EM::PHASE_REMAP || em == EM::RESONANT_SWEEP || em == EM::NOISE)
used = used || (sourceNode.lfoToExtendedModeM.value != 0);
if (em == EM::NOISE)
used = used || (sourceNode.lfoToExtendedModeN.value != 0);

return used;
}
Expand Down Expand Up @@ -465,6 +472,10 @@ struct alignas(16) OpSource : public EnvelopeSupport<Patch::SourceNode>,
case NM::MUL_BY_SIGNAL:
innerLoopImpl<EM::NOISE, PMSAW, RWSAW, NM::MUL_BY_SIGNAL>(onto, fbv, rf, dRF, phs);
break;
case NM::MUL_BY_UNI_SIGNAL:
innerLoopImpl<EM::NOISE, PMSAW, RWSAW, NM::MUL_BY_UNI_SIGNAL>(onto, fbv, rf, dRF,
phs);
break;
}
break;
}
Expand All @@ -481,7 +492,10 @@ struct alignas(16) OpSource : public EnvelopeSupport<Patch::SourceNode>,
{
using EM = Patch::SourceNode::ExtendedMode;
using NMode = Patch::SourceNode::NoiseMode;
using NT = Patch::SourceNode::NoiseType;
float nextM{0.f}, dM{0.f};
float nextN{0.f};
NT noiseType{NT::PINK};
if constexpr (ET == EM::PHASE_REMAP || ET == EM::RESONANT_SWEEP || ET == EM::NOISE)
{
// Raw target m for this block: patch value + external mod + env / lfo contributions.
Expand All @@ -498,6 +512,18 @@ struct alignas(16) OpSource : public EnvelopeSupport<Patch::SourceNode>,
dM = (nextM - extendedMPrior) / blockSize;
std::swap(nextM, extendedMPrior);
}
if constexpr (ET == EM::NOISE)
{
auto lfoFac = *lfoFacP;
float targetN = sourceNode.extendedModeN.value + extendedNMod +
sourceNode.envToExtendedModeN.value * env.outputCache[blockSize - 1] +
lfoFac * sourceNode.lfoToExtendedModeN.value * lfo.outputBlock[0];
extendedLagN.setTarget(targetN);
extendedLagN.process();
nextN = extendedLagN.v;
noiseType =
static_cast<NT>(static_cast<uint32_t>(std::round(sourceNode.noiseType.value)));
}

for (int i = 0; i < blockSize; ++i)
{
Expand Down Expand Up @@ -561,10 +587,10 @@ struct alignas(16) OpSource : public EnvelopeSupport<Patch::SourceNode>,
{
if (noisePos >= 16)
{
pinkNoise.generate16(noiseBuf);
noiseHelper.fill16(noiseBuf, noiseType, nextN);
noisePos = 0;
}
float noise = noiseBuf[noisePos++] * Patch::SourceNode::noiseScale;
float noise = noiseBuf[noisePos++];
float m = nextM;
nextM += dM;
if constexpr (NM == NMode::ADD_TO_PHASE)
Expand All @@ -581,6 +607,11 @@ struct alignas(16) OpSource : public EnvelopeSupport<Patch::SourceNode>,
{
out = st.at(ph) * (1.f + m * noise);
}
else if constexpr (NM == NMode::MUL_BY_UNI_SIGNAL)
{
auto u = (st.at(ph) + 1.f) * 0.5f;
out = u * (1.f + m * noise) * 2.f - 1.f;
}
else // MIX_WITH_SIGNAL
{
auto base = st.at(ph);
Expand Down Expand Up @@ -671,9 +702,9 @@ struct alignas(16) OpSource : public EnvelopeSupport<Patch::SourceNode>,
SinTable stWindow;
float fbVal[2]{0.f, 0.f};

// PinkNoise generates 16 samples at a time; blockSize is 8 so we drain the buffer
// across two blocks. Seeded once at construction; not re-seeded on note retrigger.
sst::basic_blocks::dsp::PinkNoise pinkNoise;
// 16-sample noise buffer drained across two blocks (blockSize = 8). NoiseHelper
// is seeded once at construction; not re-seeded on note retrigger.
NoiseHelper noiseHelper;
float noiseBuf alignas(16)[16]{};
int noisePos{16};
};
Expand Down
3 changes: 3 additions & 0 deletions src/synth/mono_values.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#ifndef BACONPAUL_SIX_SINES_SYNTH_MONO_VALUES_H
#define BACONPAUL_SIX_SINES_SYNTH_MONO_VALUES_H

#include <sst/basic-blocks/tables/DbToLinearProvider.h>
#include <sst/basic-blocks/tables/EqualTuningProvider.h>
#include <sst/basic-blocks/tables/TwoToTheXProvider.h>
#include <sst/basic-blocks/dsp/RNG.h>
Expand Down Expand Up @@ -53,6 +54,7 @@ struct MonoValues
{
tuningProvider.init();
twoToTheX.init();
dbToLinear.init();
std::fill(macroPtr.begin(), macroPtr.end(), nullptr);
}

Expand All @@ -71,6 +73,7 @@ struct MonoValues

sst::basic_blocks::tables::EqualTuningProvider tuningProvider;
sst::basic_blocks::tables::TwoToTheXProvider twoToTheX;
sst::basic_blocks::tables::DbToLinearProvider dbToLinear;

sst::basic_blocks::dsp::RNG rng;

Expand Down
12 changes: 6 additions & 6 deletions src/synth/patch.h
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ struct Patch : pats::PatchBase<Patch, Param>
ADD_TO_SIGNAL = 1,
MIX_WITH_SIGNAL = 2,
MUL_BY_SIGNAL = 3,
MUL_BY_UNI_SIGNAL = 4,
};

enum struct NoiseType : uint32_t
Expand All @@ -543,11 +544,9 @@ struct Patch : pats::PatchBase<Patch, Param>
CHIP_LFSR = 3,
};

// Noise from PinkNoise.generate16 has roughly ±0.25 typical amplitude;
// scale up so M=1 reaches a useful range. ADD_TO_PHASE additionally needs
// attenuation since one full cycle of phase jitter is too much.
static constexpr float noiseScale = 3.0f;
static constexpr float noisePhaseScale = 0.1f;
// Noise samples come out of NoiseHelper normalized to ~±1. ADD_TO_PHASE
// attenuates further since one full cycle of phase jitter is too much.
static constexpr float noisePhaseScale = 0.33f;

enum struct PhaseMapShape : uint32_t
{
Expand Down Expand Up @@ -832,7 +831,7 @@ struct Patch : pats::PatchBase<Patch, Param>
{(int)ResonantSweepFrequencyDepth::TEN, "10 oct"},
})),
noiseMode(intMd(version_120f)
.withRange(0, 3)
.withRange(0, 4)
.withDefault(0)
.withID(id(185, idx))
.withName(name(idx) + " Noise Mode")
Expand All @@ -842,6 +841,7 @@ struct Patch : pats::PatchBase<Patch, Param>
{(int)NoiseMode::ADD_TO_SIGNAL, "Add to Signal"},
{(int)NoiseMode::MIX_WITH_SIGNAL, "Mix"},
{(int)NoiseMode::MUL_BY_SIGNAL, "Mul by Signal"},
{(int)NoiseMode::MUL_BY_UNI_SIGNAL, "Mul by Uni Signal"},
})),
noiseType(intMd(version_120f)
.withRange(0, 3)
Expand Down
Loading
Loading