何かあれば GitHub のリポジトリに issue を作るか ryukau@gmail.com までお気軽にどうぞ。
Update: 2025-01-07
TrapezoidSynth のオシレータのコードがあまりにも場当たり的で自分でも読めなくなったので整理しました。
TrapezoidSynth の PTR 台形オシレータは 4 つの PTR ランプ関数をつぎはぎすることで台形を合成しています。このつぎはぎが複雑さの原因です。
ランプ関数の定義です。
PTR ランプ関数の導出は次のリンクにまとめています。
次の図はランプ関数のつぎはぎの様子を表しています。下側の矢印はランプ関数の
まずは台形の各部に名前をつけます。
PTR の次数を
図では
Maxima で解きます。
A2: 1 - A1 - 1 / K;
B1: A1 + 2 * (y - h) / K;
B2: A2 + 2 * h / K;
eq: (A1 + B1) * (y - h) / 2 = (A2 + B2) * h / 2;
expand(solve(eq, h));
出力です。
これで直流成分の大きさ
記号の定義です。
PTR 台形オシレータの次数
オーバーサンプリングを極力避けるためには次の式のように PTR
の次数を切り替えることが考えられます。例として
分岐処理がオーバーサンプリングよりも速いかどうかはベンチマークを取ってみないとわかりません。また
TrapezoidSynth では何を思ったのか
C++ です。
定数をキャストしているのはテンプレートで float
と
double
を切り替えることを想定していたからです。結局オーバーサンプリングしてごまかしたのでテンプレート化は見送りました。
#include <algorithm>
class PTRTrapezoidOsc {
public:
(float sampleRate, float frequencyHz) : sampleRate(sampleRate)
PTRTrapezoidOsc{
(frequencyHz);
setFreq}
void setFreq(float hz)
{
if (hz >= 0) tick = hz / sampleRate;
}
void setPhase(float phase) { this->phase = phase; }
void addPhase(float phase) { this->phase += phase; }
void setSlope(float slope) { this->slope = slope; }
void setPulseWidth(float pw) { this->pw = pw; }
void reset() { phase = 0; }
float process()
{
if (tick <= 0) return 0;
+= tick;
phase -= somefloor<float>(phase);
phase return ptrTpz5(phase, tick, slope, pw);
}
float sampleRate;
float phase = 0;
float tick = 0;
float slope = 8; // 台形の斜辺の傾き。この文章の K に相当。
float pw = float(0.5); // Pulse width. A1 に相当。
protected:
// tick must be greater than 0.
static float ptrTpz5(float phase, float tick, float slope, float pw)
{
uint32_t order = 5; // PTR の次数。 N に相当。
if (order > float(0.25) / tick) order = int(float(0.25) / tick);
const float ptrLen = order * tick; // NT に相当。
// A1 の範囲を制限。
const float maxSlope = float(0.25) / ptrLen;
if (slope > maxSlope) {
= maxSlope;
slope } else {
const float minSlope = float(1);
if (slope < minSlope) slope = minSlope;
}
// K の範囲を制限。
const float maxPw = float(1) - float(1) / slope;
if (pw > maxPw) pw = std::max<float>(float(0), maxPw);
// 直流の計算。 y は適当に見つけた上手く動く値。
const float y = float(1) - float(2) * slope * ptrLen;
const float dc = (y * y + pw * slope * y) / (float(2) * y + slope - float(1));
// N による分岐。
if (order == 5)
return branch<PTRTrapezoidOsc::ptrRamp5>(slope, pw, y, phase, tick) - dc;
else if (order == 4)
return branch<PTRTrapezoidOsc::ptrRamp4>(slope, pw, y, phase, tick) - dc;
else if (order == 3)
return branch<PTRTrapezoidOsc::ptrRamp3>(slope, pw, y, phase, tick) - dc;
return branch<PTRTrapezoidOsc::ptrRamp2>(slope, pw, y, phase, tick) - dc;
}
template<float (*ptrfunc)(float, float)>
static float branch(float slope, float pw, float y, float phase, float tick)
{
// ランプ関数のつぎはぎを行う分岐。
if (phase <= float(0.25) / slope) // Ramp 1
return slope * ptrfunc(phase, tick);
else if (phase <= float(0.5) / slope) // Ramp 2
return y - slope * ptrfunc(float(0.5) / slope - phase, tick);
else if (phase <= float(0.5) / slope + pw) // 台形の上辺。
return y;
else if (phase <= float(0.75) / slope + pw) // Ramp 3
return y - slope * ptrfunc(phase - float(0.5) / slope - pw, tick);
else if (phase <= float(1) / slope + pw) // Ramp 4
return slope * ptrfunc(float(1) / slope + pw + -phase, tick);
return float(0); // 残りを 0 埋め。台形の各部の名前の図の A2 にあたる領域。
}
// 以降は PTR ランプ関数。
static float ptrRamp2(float phi, float T)
{
float n = phi / T;
if (n >= float(1)) return float(2) * T * n - float(2) * T;
if (n < float(1)) return (T * n * n * n) / float(3);
if (n < float(2))
return -(T * n * n * n) / float(3) + float(2) * T * n * n - float(2) * T * n
+ (float(2) * T) / float(3);
return 0.0; // Just in case.
}
static float ptrRamp3(float phi, float T)
{
float n = phi / T;
if (n >= float(2)) return float(2) * T * n - float(3) * T;
if (n < float(1)) return (T * n * n * n * n) / float(12);
if (n < float(2))
return -(T * n * n * n * n) / float(6) + T * n * n * n
- (float(3) * T * n * n) / float(2) + T * n - T / float(4);
if (n < float(3))
return (T * n * n * n * n) / float(12) - T * n * n * n
+ (float(9) * T * n * n) / float(2) - float(7) * T * n
+ (float(15) * T) / float(4);
return 0.0; // Just in case.
}
static float ptrRamp4(float phi, float T)
{
float n = phi / T;
if (n >= float(3)) return float(2) * T * n - float(4) * T;
if (n < float(1)) return (T * n * n * n * n * n) / float(60);
if (n < float(2))
return -(T * n * n * n * n * n) / float(20) + (T * n * n * n * n) / float(3)
- (float(2) * T * n * n * n) / float(3) + (float(2) * T * n * n) / float(3)
- (T * n) / float(3) + T / float(15);
if (n < float(3))
return (T * n * n * n * n * n) / float(20)
- (float(2) * T * n * n * n * n) / float(3)
+ (float(10) * T * n * n * n) / float(3) - (float(22) * T * n * n) / float(3)
+ (float(23) * T * n) / float(3) - (float(47) * T) / float(15);
if (n < float(4))
return -(T * n * n * n * n * n) / float(60) + (T * n * n * n * n) / float(3)
- (float(8) * T * n * n * n) / float(3) + (float(32) * T * n * n) / float(3)
- (float(58) * T * n) / float(3) + (float(196) * T) / float(15);
return 0.0; // Just in case.
}
static float ptrRamp5(float phi, float T)
{
float n = phi / T;
if (n >= float(4)) return float(2) * T * n - float(5) * T;
if (n < float(1)) return (T * n * n * n * n * n * n) / float(360);
if (n < float(2))
return -(T * n * n * n * n * n * n) / float(90)
+ (T * n * n * n * n * n) / float(12) - (float(5) * T * n * n * n * n) / float(24)
+ (float(5) * T * n * n * n) / float(18) - (float(5) * T * n * n) / float(24)
+ (T * n) / float(12) - T / float(72);
if (n < float(3))
return (T * n * n * n * n * n * n) / float(60) - (T * n * n * n * n * n) / float(4)
+ (float(35) * T * n * n * n * n) / float(24)
- (float(25) * T * n * n * n) / float(6) + (float(155) * T * n * n) / float(24)
- (float(21) * T * n) / float(4) + (float(127) * T) / float(72);
if (n < float(4))
return -(T * n * n * n * n * n * n) / float(90) + (T * n * n * n * n * n) / float(4)
- (float(55) * T * n * n * n * n) / float(24)
+ (float(65) * T * n * n * n) / float(6) - (float(655) * T * n * n) / float(24)
+ (float(141) * T * n) / float(4) - (float(1331) * T) / float(72);
if (n < float(5))
return (T * n * n * n * n * n * n) / float(360)
- (T * n * n * n * n * n) / float(12)
+ (float(25) * T * n * n * n * n) / float(24)
- (float(125) * T * n * n * n) / float(18) + (float(625) * T * n * n) / float(24)
- (float(601) * T * n) / float(12) + (float(2765) * T) / float(72);
return 0.0; // Just in case.
}
};