Shepard tone timbre is now a harmonic series (buzz).

Fixed exception from custom tablet destructor. May need to check for a memory leak now.
This commit is contained in:
yaw-man 2022-09-16 11:41:12 -04:00
parent 1a6edd85a0
commit ef0c96f72a
5 changed files with 70 additions and 63 deletions

View File

@ -83,10 +83,10 @@ protected:
fParameters[idx] = val; fParameters[idx] = val;
switch (idx) { switch (idx) {
case ktpax: case ktpax:
synth.setFrequencyShift(val); synth.setPhaseVelocity(val);
break; break;
case ktpay: case ktpay:
synth.setTargetRatio(val); synth.setTimbre(val);
break; break;
case ktpaz: case ktpaz:
break; break;

View File

@ -7,12 +7,20 @@ Synth::Synth(double sampleRate){
#define frac(x) ((x) - ((long)x)) #define frac(x) ((x) - ((long)x))
constexpr double MIN_VOLUME = 0.00001; constexpr double MIN_VOLUME = 0.00001;
static constexpr double amin = 50;
static constexpr double apiq = 500;
static constexpr double amax = 10000;
//Volume of voice as a function of sample rate independent frequency. //Volume of voice as a function of sample rate independent frequency.
static constexpr double getAmplitude( double hz ) static constexpr double getAmplitude( double hz )
{ {
if( hz < 20.0 ) return 0.0; if( hz < amin ) return 0.0;
if( hz < 440.0 ) return hz / 440.0; if( hz < apiq ) {
if( hz < 10000.0 ) return (10000.0 - hz) / 9560.0; double a = (hz - amin) / (apiq - amin);
return a * a;}
if( hz < amax ) {
double a = 1.0 - (hz - apiq) / (amax - apiq);
return a * a;
}
return 0.0; return 0.0;
} }
@ -21,6 +29,26 @@ static constexpr double getAmplitude( double hz )
static_assert( MIN_VOLUME > getAmplitude(10.0) ); static_assert( MIN_VOLUME > getAmplitude(10.0) );
static_assert( MIN_VOLUME > getAmplitude(20000.0)); static_assert( MIN_VOLUME > getAmplitude(20000.0));
//New fundamental is twice as high as old fundamental.
//Even overtones become plain overtones.
void Synth::shiftUp()
{
spectrumPhase += 1.0;
for(uint voice = 0; voice < NUM_VOICES; ++voice){
phases[voice] = phases[(2 * voice + 1) & VOICE_MASK];
}
}
//New fundamental is half as high as old fundamental.
//Overtones become even overtones.
void Synth::shiftDown()
{
spectrumPhase -= 1.0;
for(uint voice = NUM_VOICES / 2 - 1; voice < NUM_VOICES; --voice){
phases[voice * 2 + 1] = phases[voice];
}
}
void Synth::process(float* output, const uint32_t frames) void Synth::process(float* output, const uint32_t frames)
{ {
double hz; double hz;
@ -29,76 +57,53 @@ void Synth::process(float* output, const uint32_t frames)
for(uint32_t i = 0; i < frames; i++){ for(uint32_t i = 0; i < frames; i++){
//Set pitch. //Set pitch.
hz = hzFund *= hzShift; hz = hzFund = spectrumPhase * fMin + fMin;
//Set timbre. bool isOddHarmonic = true;
ratio = ratioSlewRate * ratio + (1.0 - ratioSlewRate) * targetRatio;
for(uint voice = 0; voice < NUM_VOICES; ++voice){ for(uint voice = 0; voice < NUM_VOICES; ++voice){
//Get new phase. //Get new phase.
double phase = phases[(voice + idxFund) & VOICE_MASK]; double phase = phases[voice];
phase += hz * sampleInterval; phase += hz * sampleInterval;
phase = frac(phase); phase = frac(phase);
//Anti-aliasing: don't bother rendering anything over the Nyquist rate. //Anti-aliasing: don't bother rendering anything over the Nyquist rate.
if( hz > hzNyq ) break; if( hz > hzNyq ) break;
output[i] += getAmplitude(hz) * sin(2.0 * M_PI * phase);
output[i] += (!isOddHarmonic + isOddHarmonic * spectrumPhase * spectrumPhase) //Fade in odd harmonics.
* getAmplitude(hz) //Frequency response.
* sin(2.0 * M_PI * phase); //Additives.
//Remember phase, move to higher overtone. //Remember phase, move to higher overtone.
phases[(voice + idxFund) & VOICE_MASK] = phase; phases[voice] = phase;
hz *= ratio * (voice + 1.0) / (voice + 2.0); hz *= (voice + 2.0) / (voice + 1.0);
isOddHarmonic = !isOddHarmonic;
} }
output[i] *= volume / static_cast<double>(NUM_VOICES); output[i] *= volume / static_cast<double>(NUM_VOICES);
//Wrapping.
spectrumPhase += spectrumPhaseVelocity * sampleInterval;
if( spectrumPhase > 1.0) shiftDown();
if( spectrumPhase < 0.0) shiftUp();
} }
//Make timbre cyclical.
if( hzShift < 1.0 )
{
//Pitch low and decreasing, shift bottom voices to top.
while( getAmplitude(hz) > MIN_VOLUME
&& getAmplitude(hzFund) < MIN_VOLUME)
{
++idxFund;
hz *= 2.0 * ratio;
hzFund *= 2.0 * ratio;
}
}
if( (hzShift > 1.0) || (targetRatio > ratio))
{
//Pitch high and increasing. Shift top voices to bottom.
while( getAmplitude(hz) < MIN_VOLUME
&& getAmplitude(hzFund) > MIN_VOLUME)
{
--idxFund;
hz /= 2.0 * ratio;
hzFund /= 2.0 * ratio;
}
}
} }
void Synth::setSampleRate(double oldRate, double newRate){ void Synth::setSampleRate(double oldRate, double newRate){
sampleInterval = 1.0 / newRate; sampleInterval = 1.0 / newRate;
hzNyq = newRate / 2; hzNyq = newRate / 2;
ratioSlewRate = pow(ratioSlewRate, oldRate / newRate);
hzShift = pow(hzShift, newRate / oldRate);
} }
//Takes value from 0 to 1 representing frequency shift factor. //Takes value from 0 to 1 representing frequency shift factor.
//0 : lower one octave per second //0 : lower one octave per second
//1 : raise one octave per second //1 : raise one octave per second
void Synth::setFrequencyShift(double in){ void Synth::setPhaseVelocity(double in){
hzShift = exp2(2.0 * (in - 0.5) * sampleInterval); spectrumPhaseVelocity = 2.0 * (in - 0.5);
} }
static constexpr double minRatio = 1.0; void Synth::setTimbre(double in){
static constexpr double maxRatio = 1.05;
//Slew to given inharmonicity ratio.
//0 : next voice is exactly harmonic.
//1 : next voice is 5% inharmonic.
void Synth::setTargetRatio(double in){
targetRatio = minRatio + in * (maxRatio - minRatio);
} }
void Synth::setVolume(double in){ void Synth::setVolume(double in){

View File

@ -7,7 +7,7 @@ struct Voice
double phase; double phase;
}; };
constexpr unsigned int NUM_VOICES = 64; constexpr unsigned int NUM_VOICES = 256;
constexpr unsigned int VOICE_MASK = NUM_VOICES - 1; constexpr unsigned int VOICE_MASK = NUM_VOICES - 1;
class Synth class Synth
@ -15,23 +15,26 @@ class Synth
public: public:
explicit Synth(double sampleRate); explicit Synth(double sampleRate);
void process(float *output, uint32_t frames); void process(float *output, uint32_t frames);
void setFrequencyShift(double in); void setPhaseVelocity(double in);
void setTargetRatio(double in); void setTimbre(double in);
void setSampleRate(double oldRate, double newRate); void setSampleRate(double oldRate, double newRate);
void setVolume(double in); void setVolume(double in);
void shiftUp();
void shiftDown();
private: private:
//Phase must persist between run invocation. //Phase must persist between run invocation.
//Phase of each voice, considered as a period function on the unit interval. //Phase of each voice, considered a function on the unit circle.
std::array<double, NUM_VOICES> phases = {0}; std::array<double, NUM_VOICES> phases = {0};
//Index of lowest voice in phase array.
unsigned int idxFund = 0;
float volume = 0.0f; float volume = 0.0f;
double hzFund = 32.7; //Parameter in unit circle controlling pitch (varies by one octave).
double hzShift = 1.0; double spectrumPhase = 1.0f;
//Revolutions per second of spectrum phase.
double spectrumPhaseVelocity = 0.0;
//Lowest fundamental frequency of blit.
static inline constexpr double fMin = 55.0;
//Current fundamental frequency of blit.
double hzFund = fMin;
double hzNyq = 24000.0; double hzNyq = 24000.0;
double ratio = 2.0;
double targetRatio = 2.0;
double ratioSlewRate = 0.99999;
double sampleInterval = 1.0 / 48000.0; double sampleInterval = 1.0 / 48000.0;
}; };

View File

@ -30,7 +30,6 @@ struct Packet {
class Tablet { class Tablet {
public: public:
Tablet(uintptr_t handle); Tablet(uintptr_t handle);
~Tablet();
bool GetPacket( Packet &pkt ); bool GetPacket( Packet &pkt );
bool initialized; bool initialized;
std::string errormsg = ""; std::string errormsg = "";

View File

@ -18,10 +18,10 @@ Tablet::Tablet(uintptr_t handle)
NewContext(hwnd); NewContext(hwnd);
} }
Tablet::~Tablet() { /*Tablet::~Tablet() {
if (hctx) { gpWTClose(hctx); } // if (hctx) { gpWTClose(hctx); }
UnloadWintab(); UnloadWintab();
} }*/
bool Tablet::GetPacket( Packet& packet ) { bool Tablet::GetPacket( Packet& packet ) {
//Serial number of newest packet. //Serial number of newest packet.
@ -54,7 +54,7 @@ void Tablet::NewContext(HWND hwnd) {
AXIS TabletZ = { 0 }; AXIS TabletZ = { 0 };
AXIS TabletPressure = { 0 }; AXIS TabletPressure = { 0 };
gpWTInfoA(WTI_DEFCONTEXT, 0, &ctx); gpWTInfoA(WTI_DEFCONTEXT, 0, &ctx);
ctx.lcOptions |= CXO_MESSAGES; //TODO: checker çela ctx.lcOptions |= CXO_MESSAGES; //TODO: checker <EFBFBD>ela
ctx.lcPktData = PACKETDATA; ctx.lcPktData = PACKETDATA;
ctx.lcPktMode = 0; ctx.lcPktMode = 0;