Compile Time Computation

Kronos produces runtime code that is both deterministic and simple. Therefore, most of the abstraction you can use to generate complex patches takes place at compile time.

Sinusoid Approximation

As an example, we are going to write a sinusoidal oscillator that uses polynomial approximation. This technique is not as efficient as the various recursive methods of sinusoid generation, but it has the benefit of adjustable precision and inexpensive modulation. That is why it is a good choice for frequency modulation synthesis.

MacLaurin Series

Our plan is to develop a polynomial approximation for a sinusoidal slope and utilize the symmetry properties to generate a full waveform period. The first step is the MacLaurin series.

Import Algorithm Fact(n) { When(n > #1 n * Fact(n - #1) Otherwise #1) } is = Algorithm:Expand(#10 (+ #1) #0) Algorithm:Map(Fact is)#1x2 #2 #6 #24 #120 #720 #5040 #40320 #362880 nil Sin-Coef(n) { Math:Pow(#-1 n) / Fact(#2 * n + #1) } coefs = Algorithm:Map(Sin-Coef is) coefs#1 #-0.16666666666666666666666666666666666667 #0.008333333333333333333333333333333333333 #-0.0001984126984126984126984126984126984127 #0.000002755731922398589065255731922398589065 #-0.000000025052108385441718775052108385441718775 #0.00000000016059043836821614599392377170154947933 #-0.0000000000007647163731819816475901131985788070444 #0.000000000000002811457254345520763198945583010320016 #-8.220635246624329716955981236872280749e-18 nil

Please note that as we are using invariant numbers (#10), the entire computation happens at compile time during the specialization pass.

Horner-form polynomial

Armed with polynomial coefficients, we can use Horner's scheme to evaluate the polynomial efficiently. Since our series only has coefficients for odd powers, we use x^2 as the polynomial variable.

Horner(x coefs...) { Algorithm:Fold((c p) => c + x * p coefs...) } SinA(w) { w * Horner(w * w coefs) } Import Math Algorithm:Map(SinA Math:Pi / 4 Math:Pi / 3 Math:Pi / 2)0.70710677 0.86602545 0.99999988

Periodicity

Approximating sin makes sense, because zero is in the middle of the slope. We can further shift the phase angle by -Pi/2 to translate sine into cosine, which is beneficial because cosine is symmetrical around zero.

CosA(w) { SinA(Abs(w) - Math:Pi / #2) } Algorithm:Map(CosA Math:Pi Math:Pi / -2 0 Math:Pi / 2 Math:Pi )#0.9999999999999998484472428494967738426 0 -0.99999988 0 #0.9999999999999998484472428494967738426

We now have an approximation that's decent in the interval [-Pi, Pi]. We can plug in a phasor:

Import Gen snd = CosA((Gen:Phasor(440) * 2 - 1) * Math:Pi) * 0.2

Cleaning Up

For completeness, we may want to warp the whole thing in a function.

SinOsc(freq) { ; Refer to Map, Expand and Fold from Algorithm ; with implicit package prefix Use Algorithm[Map Expand Fold] N = #10 ; approximation order ; compute polynomial coefficients coefs = Map(Sin-Coef Expand(N (+ #1) #0)) ; phasor phase = Gen:Phasor(freq) ; map phasor to [-Pi, Pi] x = ( Abs(phase * 2 - 1) - 0.5 ) * Math:Pi x * Horner(x * x coefs) } snd = SinOsc(440 + 10 * SinOsc(5.5)) * 0.2 ; Frequency Modulation Synthesis f0 = 110 mod = SinOsc(0.1) * 32 op1 = SinOsc(f0 * 3) op2 = SinOsc((1 + op1 * mod) * f0) snd = op2 * 0.2

Compile Time Power

To recap, we have an oscillator definition that relies on a MacLaurin series that looks pretty close to that from a mathematical textbook. However, the generated code avoids all the tricky stuff. If we take a peek at the inner loop of our signal processor, this is what we see in LLVMish:

%15 = fmul float %10, 2.000000e+00 %16 = fadd float %15, -1.000000e+00 %17 = fmul float %16, 0x400921FB60000000 %abs.i.i = tail call float @llvm.fabs.f32(float %17) #1 %18 = fadd float %abs.i.i, 0xBFF921FB60000000 %19 = fmul float %18, %18 %20 = fmul float %19, 0x3C62F49B40000000 %21 = fsub float 0x3CE952C780000000, %20 %22 = fmul float %19, %21 %23 = fadd float %22, 0xBD6AE7F3E0000000 %24 = fmul float %19, %23 %25 = fadd float %24, 0x3DE6124620000000 %26 = fmul float %19, %25 %27 = fadd float %26, 0xBE5AE64560000000 %28 = fmul float %19, %27 %29 = fadd float %28, 0x3EC71DE3A0000000 %30 = fmul float %19, %29 %31 = fadd float %30, 0xBF2A01A020000000 %32 = fmul float %19, %31 %33 = fadd float %32, 0x3F81111120000000 %34 = fmul float %19, %33 %35 = fadd float %34, 0xBFC5555560000000 %36 = fmul float %19, %35 %37 = fadd float %36, 1.000000e+00 %38 = fmul float %18, %37 %39 = bitcast i8* %2 to float* store float %38, float* %39, align 4, !alias.scope !9, !noalias !10

In other words, the compiler has generated an inlined Horner form polynomial with constant multipliers: pretty much what we would write by hand in C. Second-order code generation has some additional benefits - this oscillator could easily have parametric precision. SinOsc could simply receive the approximation order, N as a compile time constant argument.

To look at Kronos code output, please take a look at the command line workflow and kc, the static compiler.