Having covered the necessary basics, we can get around to spitting out some audio.

To understand audio, you need a basic idea of how reactivity works in Kronos.

A data path that has reactivity has an implicit clock or an update rate. Signals like audio are such data paths; an audio signal is meaningful only when the samples are streamed at a regular rate.

Any functions that take a reactive argument automatically become reactive themselves. The simplest example of a reactive streaming function is probably the following;

	audiofile = Audio:File("snare.wav")

Audio:File is a function provided in IO.k that defines some hooks that tell k2cli to stream an audio file with the given file name. Kronos itself knows nothing about where the data is coming from; it merely relays the path along with a tag to k2cli, and is happy to receive the audio samples.

‘audiofile()’ is a reactive function; therefore its output needs to be refreshed at the audio rate. To enjoy the results, it must be connected to a reactive sink, an output capable of absorping the data as it becomes available. Several such reactive sinks are defined by k2cli. The one most relevant here is ‘–audio-out’. This tells k2cli to stream the reactive function ‘audiofile()’ to the appropriate reactive sink.

PS > .\k2cli.exe --audio-out "audiofile()" .\part6-audiobasics.k
K2CLI 0.1
(c) 2011 Vesa Norilo

Reactive instance size 4 bytes
Reactive processing active... Press any key to quit
CPU: 1.1%

You should hear a sound. This is the part where you might want to do some device setup. The command line switch –setup tells k2cli to enumerate the available hardware. Subsequently you can note the device ids you want to use and use the –audio-dev <out-id> <in-id> to select the preferred devices for playback.

Stateless processing

Stateless processes are easy to implement for reactive audio streams. As a trivial example, let’s change the gain of the audio file by -6dB:

	audiofile-6dB = Audio:File("snare.wav") * 0.5

As a result the audio plays softer. Let’s take a look at the multiplication. The left hand side is the reactive audio file source familiar from the first example. The right hand side is a constant — it is not reactive, as the value doesn’t vary in time.

The multiplication becomes reactive as well, being connected to a reactive source. The non-reactive constant doesn’t contest this. Therefore the entire function ‘audiofile-6dB’ becomes reactive as well, propagating reactivity to any computations dependant on its result. In practice, the system knows whether a signal path is audio or not.

As long as a program has only constants and audio inputs, the output will necessarily be audio, due to the dominance of reactivity over constants shown above. The situtation becomes slightly more complicated when we introduce user interaction and different signal clocks.

Stateful processing

Let’s implement a simple echo-delay with feedback. We can use the built-in ring buffer operator, demonstrated below:

     file = Audio:File("snare.wav")
     delay = rbuf('0 #4000 file + delay * 0.5)
     audiofile-delay = file + delay 

#4000′ here defines the ring buffer size. But what is the unit? It is actually the number of reactive ticks of the signal input. In this case, the signal input comes from Audio:File, inheriting its reactivity and thus the audio sample rate of the system.

Please note that if the input to ‘rbuf’ is not reactive, the ring buffer never ticks and only outputs the initial value.

This becomes important once we would like to synthesize audio out of thin air.

Something out of Nothing

To synthesize sound, a self-evolving signal is required. In the absence of an external audio stream, this always means recursion in Kronos. Let’s start by synthesizing a ramp signal that increases monotonically with time.

    ramp = z-1('0 ramp + 1)
    monotonic-ramp = ramp

‘z-1′ is a unit delay, much like a ring buffer with a size of 1. The first parameter is the initializer function, called to set the contents of the  unit delay at the start of the audio stream. From part 5 you may recognize it as an anonymous function that returns zero.

The input to the unit delay is ‘ramp + 1′. Incidentally ‘ramp’ is defined as the output of the unit delay. This creates a recursion, where upon every reactive tick of the signal ramp is incremented by one. Please note that the only permitted definitions with recursion are those with delay, as they are finitely computable.

Well and good, except for the fact that the example doesn’t work. The reason for this is that the recursive loop is not reactive; therefore there’s no tick. We can clock the loop by using a function provided that turns an arbitray signal into audio by clocking it:

    ramp = z-1('0 ramp + Audio:Clock(increment)))
    monotonic-ramp = ramp

‘Audio:Clock’ can be placed anywhere inside or upstream of the recursive loop, but the example shown above is recommended. The increment value itself is turned into an audio signal, activating the recursive loop that depends on it. This is handy when the argument to ‘monotonic-ramp’ is reactive in itself. It could well be adjustable from the user interface, subsequently reacting to slider movements. This placement makes sure that the audio clock overrides any reactivity supplied from outside the loop.

Having created a ramp, a simple sinusoidal oscillator can be constructed;

    ramp = z-1('0 ramp + Audio:Clock(increment)))
    monotonic-ramp = ramp

    sine-osc = Crt:sin(monotonic-ramp(freq * Pi))

Note the unit of ‘freq’. Being radians / sample, it is dependant on the sample rate, so that 0 corresponds to 0Hz and 1 corresponds to the sampling rate. The highest valid frequency, Nyquist frequency, is always at 0.5.

This simple function plays a pure tone, at least until the numerical precision of the ramp becomes too low. This is evident by undesired pitch changes as the oscillator plays on. This might happen sooner than you expect, therefore, in part 7, we present a better way to synthesize sines.

No Comment.

Add Your Comment

− 1 = six