That's very interesting.
In case you didn't already know, there are quite a few other 'hidden' features in parameter data. Such as defining skews, labelled discrete type parameters etc.
That's very interesting.
In case you didn't already know, there are quite a few other 'hidden' features in parameter data. Such as defining skews, labelled discrete type parameters etc.
yeah that was my thinking.
But I'm not certain whether that's the case @HISEnberg was asking about or not!
@dannytaurus said in Recreating Roland Alpha Juno PWM Saw oscillator - ideas?:
I'll be doing a lot of processing after the oscillator section,
That emphasises differences rather than obscures them. Everything downstream is affected by the source.
But if accuracy to the original is not the goal, this isn't a problem whatsoever.
As for creating wavetables, use python. You can code custom tools to take your cycles and stitch them into a wavetable. You can even generate PWM in python.
(it takes a bit of setup to get python up and running though, with all the right libraries and versions, it can be a pain) After it's setup though it's great, and ChatGPT is good at writing python code, so you basically have an unlimited supply of little audio dev tools that you can generate.
Or... if you own Serum2, use that. The wavetable editor is good and it can extract cycles and stitch them using different morphs.
Can you push to that buffer from inside the c++ node though?
I think @HISEnberg is wanting to generate an audio file in c++, then push it out to the Hise script?
I might be wrong.
Just a note on this, the file player has no antialiasing! Pitching up any file will create harsh additional harmonics. Just something to be aware of
So, the easiest way to achieve it in a 'pretend' hacky manner, would be to record the original waveform output from the synth while sweeping the PWM, and extract frames to create a wavetable. You could then use the built in Hise wavetable synthesiser module to play it back.
It'll sound the same as the original thing, perceptually
That is, until you go and do other synthesis stuff with it.
The signal won't interact with the other parts of the synth in the exact same way unless you simulate it properly / with more detail. That's only an issue if you are wanting a really analog sound, or to actually get very close to the original.
And that's where things get really tricky immediately.
You'd probably need to code the oscillator in c++ from scratch. You'd need to code an antialiasing system since you can no longer rely on the Hise one from the wavetable synthesiser module. But it would open up freedom to properly implement PWM in high detail and create simulations to recreate any other quirks of the oscillator.
If you want more info I'll give it, but the method you should choose really depends on what aspects you want to simulate.
Voices in Hise are managed 'automatically'.
Take a read of Polydata.
I don't remember where it can be found. But the Hise source has all the .h and .cpp files which have the implementations for voice handling. You can see what's currently going on, and perhaps there will be some useful api that you're not yet making use of.
Christoph is the person to ask though!
Oh yeah I tried a moog. It was arguably not very good haha.
It becomes very dark in a very musty way.
Unless you're willing to stack a few to get a high order it's going to sound quite blurry. Whether or not that's a bad thing is up to you.
For refrence the real antialising filters from pedals like the memory man are steep. They are almost like low order elliptic lowpasses. That's the closest 'standard' filter response I found to the real thing. This allows them to have a high and crisp feeling cut-off (3.5k) while still killing aliasing.
It's different to a synth filter.
But that's what the hardware BBD effects do anyway.
Really fun ideas!
I took a couple of weeks to learn how to derive filters from analog because I wanted some BBD delay antialiasing filters that match hardware exactly. If you need any tips on that I'm happy to share.
But in all honesty, making up new filters is probably more fun. It's not like the analog filters are the best ever - there are probably unexplored shapes that will sound more interesting or be useful in different ways.
That's really comprehensive, more so than mine.
The only thing I noticed missing is possibly the fact that filters change shape in BBD delays depending on the delay time. They kind of warp into a lower order lower filter as time increases.
If you take IR measurements from a real device you can recreate matching filters using vector fitting!
I'd love to hear more of your delay though. BBD delay is wonderful.
Nice work!
May as well share mine : D
An incredibly distorted take on BBD haha!
I think you'll have to use the global cable system.
you can take the modulation signal out of the c++ node, into one of the global cable nodes, and then you can access the value in hise script.
Yep, if your buffer is a power of two you can use bitmasking.
The only case where I've used other strategies, is when my interpolator is very wide. My Fir convolution interpolator has 64 reads every sample, and so even bitmask wraps add up.
But in such situations I actually just use strategies to hoist the wraps. I don't do any checking at all inside the loop. I note down where the edges are and if we are nowhere near the edges we run a separate vectorized loop that avoids any checks.
Adding to David's answer,
no time at all to import samples, if your samples are named properly (already organised) or have metadata etc.
If you need to organise all the samples etc, then it will take quite a while longer.
You should be able to find youtube videos (David Healy likely has multiple) about using the stock hise sampler and how sample import works.
If you want to get it done on a tight deadline, simplify the scope.
Probably don't do the multi output routing at first.
Prototype each part of the project separately so that you can learn how each bit works, then create your actual project.
@Christoph-Hart ah, right. Yes looking at the profile it's very obvious.
Shoot you're right.
I didn't even think of that.
I wonder what the point of Ai botting a forum like this would be
@weston-donald
Yes agreed.
Make sure to match your upsampling / downsampling filters.
Your upsample stage and downsample stage need to be designed together so that they don't cause bad ripple or other artefacts. Mismatched filters will cause aliasing even (going from 2x as many samples, down to half as many may create discontinuities if not smoothed / interpolated proper).
I made this custom one at one point when I needed stronger alias rejection.
It's old though and I only used it once. just posting an alternative in case it's useful.
Something you'll need to be careful of when oversampling is that the antialising filters usually introduce latency.
So if you have things that are oversampled mixed with things that aren't, make sure to phase align them by compensating latency.
// FILE: src/Oversampler2xKaiser.h
/*
2x Kaiser-windowed FIR oversampler/downsamlper
Linear-phase, unity DC gain. Default taps=63, beta=8.0.
USAGE:
- Oversampler2xKaiser os; os.prepare(63, 8.0); os.reset();
- double u0,u1; os.upsample(x, u0, u1); // generate 2x stream
- double y = os.downsample(v0, v1); // consume pair from 2x domain
*/
#pragma once
#include <vector>
#include <cmath>
#include <cstring>
#include "Utils_General.h"
#define OS2X_HAVE_XSIMD 1
class Oversampler2xKaiser
{
public:
Oversampler2xKaiser() noexcept = default;
void prepare(int taps = 63, double beta = 8.0)
{
if ((taps & 1) == 0) ++taps;
N = taps;
B = beta;
buildCoeffs();
upHist.assign((size_t)H, 0.0);
dEven.assign((size_t)L0, 0.0);
dOdd.assign((size_t)L1, 0.0);
}
void reset() noexcept
{
std::fill(upHist.begin(), upHist.end(), 0.0);
std::fill(dEven.begin(), dEven.end(), 0.0);
std::fill(dOdd.begin(), dOdd.end(), 0.0);
}
inline void upsample(double x, double& y0, double& y1) noexcept
{
shift_in(upHist.data(), H, x);
y0 = dotSIMD(p0.data(), upHist.data(), L0);
y1 = dotSIMD(p1.data(), upHist.data(), L1);
}
inline double downsample(double v0, double v1) noexcept
{
shift_in(dEven.data(), L0, v0);
shift_in(dOdd.data(), L1, v1);
const double a = dotSIMD(p0.data(), dEven.data(), L0);
const double b = dotSIMD(p1.data(), dOdd.data(), L1);
return a + b;
}
private:
// PURPOSE: Kaiser window, halfband ideal (wc=pi/2) + normalization. Build polyphase strides p0/p1.
void buildCoeffs()
{
const int M = (N - 1) / 2;
std::vector<double> h((size_t)N);
const double kPi = gdsp::PI;
const double i0B = i0(B);
for (int n = 0; n < N; ++n)
{
const int m = n - M;
const double r = (double)m / (double)M;
const double w = i0(B * std::sqrt(std::max(0.0, 1.0 - r * r))) / i0B;
double sinc;
if (m == 0) sinc = 0.5;
else sinc = std::sin(0.5 * kPi * m) / (kPi * m);
h[(size_t)n] = w * sinc;
}
double s = 0.0; for (double v : h) s += v;
const double invS = (s != 0.0) ? (1.0 / s) : 1.0;
for (double& v : h) v *= invS;
L0 = (N + 1) / 2;
L1 = N / 2;
H = std::max(L0, L1);
p0.resize((size_t)L0);
p1.resize((size_t)L1);
for (int k = 0; k < L0; ++k) p0[(size_t)k] = h[(size_t)(2 * k)];
for (int k = 0; k < L1; ++k) p1[(size_t)k] = h[(size_t)(2 * k + 1)];
}
static inline void shift_in(double* hist, int len, double x) noexcept
{
std::memmove(hist + 1, hist, sizeof(double) * (size_t)(len - 1));
hist[0] = x;
}
// PURPOSE: SIMD dot with scalar fallback.
static inline double dotSIMD(const double* a, const double* b, int n) noexcept
{
#if OS2X_HAVE_XSIMD
using vd = xsimd::batch<double>;
constexpr int W = (int)vd::size;
int i = 0;
vd acc = vd::broadcast(0.0);
for (; i + W <= n; i += W)
acc = xsimd::fma(xsimd::load_aligned(a + i), xsimd::load_aligned(b + i), acc);
double s = xsimd::reduce_add(acc);
for (; i < n; ++i) s += a[i] * b[i];
return s;
#else
double s = 0.0;
for (int i = 0; i < n; ++i) s += a[i] * b[i];
return s;
#endif
}
// PURPOSE: Modified Bessel I0 via series. Accurate for |x| up to ~12 within ~1e-12.
static inline double i0(double x) noexcept
{
double t = 1.0;
double y = (x * x) * 0.25;
double s = 1.0;
for (int k = 1; k < 50; ++k)
{
t *= y / (double)(k * k);
s += t;
if (t < 1e-12 * s) break;
}
return s;
}
int N{ 63 }, L0{ 0 }, L1{ 0 }, H{ 0 };
double B{ 8.0 };
#if OS2X_HAVE_XSIMD
template<typename T> using AAlloc = xsimd::aligned_allocator<T, 64>;
#else
template<typename T> using AAlloc = std::allocator<T>;
#endif
std::vector<double, AAlloc<double>> p0, p1;
std::vector<double, AAlloc<double>> upHist, dEven, dOdd;
};
/*
Utils_General.h
General useful math for dsp programming
*/
#pragma once
#include <climits>
#include <cmath>
#if defined(_MSC_VER)
#pragma once
#pragma warning(4 : 4250) // "Inherits via dominance"
#endif
namespace gdsp
{
//===================| Constants |===================
// PI
static const double PI = 3.1415926535897932384626433832795;
static const double TWOPI = 2 * PI;
// FORCEINLINE
#if defined(_MSC_VER)
#define FORCEINLINE __forceinline
#else
#define FORCEINLINE inline
#endif
//===================| Fixed Point Math |===================
/*----- 16-bit signed -----*/
#if defined(_MSC_VER)
typedef __int16 Int16;
#elif (defined(__MWERKS__) || defined(__GNUC__) || defined(__BEOS__)) && SHRT_MAX == 0x7FFF
typedef short int Int16;
#else
#error No signed 16-bit integer type defined for this compiler!
#endif
/*----- 32-bit signed -----*/
#if defined(_MSC_VER)
typedef __int32 Int32;
#elif (defined(__MWERKS__) || defined(__GNUC__) || defined(__BEOS__)) && INT_MAX == 0x7FFFFFFFL
typedef int Int32;
#else
#error No signed 32-bit integer type defined for this compiler!
#endif
/*----- 64-bit signed -----*/
#if defined(_MSC_VER)
typedef __int64 Int64;
#elif defined(__MWERKS__) || defined(__GNUC__)
typedef long long Int64;
#elif defined(__BEOS__)
typedef int64 Int64;
#else
#error No 64-bit integer type defined for this compiler!
#endif
/*----- 32-bit unsigned ----*/
#if defined(_MSC_VER)
typedef unsigned __int32 UInt32;
#elif (defined(__MWERKS__) || defined(__GNUC__) || defined(__BEOS__)) && UINT_MAX == 0xFFFFFFFFUL
typedef unsigned int UInt32;
#else
#error No unsigned 32-bit integer type defined for this compiler!
#endif
// Helpers for fixed-point math
union Fixed3232
{
Int64 _all;
class
{
public:
#if defined(__POWERPC__) /* big-endian */
Int32 _msw;
UInt32 _lsw;
#else /* little-endian */
UInt32 _lsw;
Int32 _msw;
#endif
} _part;
};
// simple branch-free min/max
template <typename T> FORCEINLINE T min(T a, T b) { return (a < b) ? a : b; }
template <typename T> FORCEINLINE T max(T a, T b) { return (a > b) ? a : b; }
// IEEE-safe rounding
FORCEINLINE int round_int(double x) { return (x >= 0.0) ? int(x + 0.5) : int(x - 0.5); }
FORCEINLINE long round_long(double x) { return (x >= 0.0) ? long(x + 0.5) : long(x - 0.5); }
// bidirectional (signed) bit-shift helper
template <typename T>
FORCEINLINE T shift_bidi(T x, int s)
{
return (s >= 0) ? (x << s) : (x >> (-s));
}
} // namespace gdsp