Realtime playback

Methods that provide support for realtime playback as opposed to static file based rendering. The commonly used method is play. To work at a higher level with sample accurate scheduling of sound events, see Gen and the associated gens track, loop and the example ping. For interactivity, see control (for UI to signal) and probe (for signal to UI).

Synth.startaudioFunction
startaudio(callback)

callback is a function that will be called like callback(sample_rate, readqueue, writequeue). sample_rate indicates the output sampling rate. readqueue and writequeue are Channels that accept either an audio buffer as a Vector{Float32} or Val(:done) to indicate completion of the audio process. The callback is expected to take! a buffer from the readqueue, fill it up and submit it to the writequeue using put!.

When the audio generation is done, callback should put!(writequeue, Val(:done)) and break out of its generation loop and return.

It returns a function that can be called (without any arguments) to stop the audio processing thread. Make sure to start julia with a sufficient number of threads for this to work.

source
Synth.playFunction
play(signal, duration_secs=Inf; blocksize=64)

Plays the given signal on the default audio output in realtime for the given duration or till the signal ends, whichever happens earlier. Be careful about the signal level since in this case the signal can't be globally normalized.

Returns a stop function (like with startaudio and synthesizer) which when called with no arguments will stop playback.

source
play(m::Gen, duration_secs = Inf)

To play a Gen, it needs to be scheduled on a bus. This overloaded version of play automatically does that for you. Similar to the overload for playing a signal, it returns a stop function that can be called to terminate playback at any time.

source
Synth.micFunction
mic(dev :: AbstractString = "mic"; blocksize :: Int = 128, samplingrate :: Float64 = 48000.0)

Opens input from the default mono mic source. You shouldn't need to fiddle with the blocksize and samplingrate, but they're available for configuration.

A Mic supports intrinsic fanout and therefore you shouldn't need to create multiple mic instances on the same audio interface.

Todo: At some point, support selecting named audio sources and also stereo sources.

source
Synth.controlFunction
control(chan :: Channel{Float32}, dezipper_interval = 0.04; initial = 0.0f0, samplingrate=48000) :: Control
control(dezipper_interval = 0.04; initial = 0.0f0, samplingrate = 48000) :: Control

A "control" is a signal that is driven by values received on a given or created channel. The control will dezipper the value using a first order LPF and send it out as its value. The intention is to be able to bind a UI element that produces a numerical value as a signal that can be patched into the graph.

If c is a Control struct, you can set the value of the control using c[] = 0.5f0.

Close the channel to mark the control signal as "done".

Channels and memory

A control signal uses a channel to receive its values. This raises a question about the amount of memory that'll be consumed by using what looks like a system resource. Julia's channels cost about 416 bytes each, meaning a 1000 channels, which would be a pretty complex scenario to put it mildly, will be well under 1MB. Even if you have 1000 voices with 10 channels controlling each voice, the memory won't be significant (under 5MB) by 2025 standards.

source
Synth.stopFunction
stop(c :: Control)

Stops the control signal. From the renderer's perspective, the control signal will switch to the "done" state. The control channel will close, causing any further put! calls to raise an exception. If you control the sustain of an adsr using a control signal, then stopping the control will basically end the ADSR envelope by switching it into "release" phase.

source
Synth.probeFunction
probe(s :: Signal, chan :: Channel{Float32}, interval :: Float64 = 0.04; samplingrate = 48000) :: Probe
probe(s :: Signal, interval :: Float64 = 0.04; samplingrate = 48000) :: Probe

A probe is a "pass through" signal transformer which periodically reports a reading of the signal to a channel. The channel may either be given or a new one can be created by the second variant. Since it is a pass-through, you can insert a probe at any signal point. A probe low-pass-filters the signal before sending it out the channel, so it won't be useful for signals that vary very fast. The default value has it sampling the signal every 40ms.

The channel can then be connected to a UI display widget that shows values as they come in on the channel.

source
Synth.waveprobeFunction
waveprobe(s :: Signal, chan :: Channel{TimedSamples}, duration :: Float64 = 0.25, interval :: Float64 = 0.04; samplingrate = 48000) :: Probe
waveprobe(s :: Signal, duration :: Float64 = 0.25, interval :: Float64 = 0.04; samplingrate = 48000) :: Probe

A probe is a "pass through" signal transformer which periodically reports a reading of the signal to a channel. The channel may either be given or a new one can be created by the second variant. Since it is a pass-through, you can insert a probe at any signal point. A probe low-pass-filters the signal before sending it out the channel, so it won't be useful for signals that vary very fast. The default value has it sampling the signal every 40ms.

The channel can then be connected to a UI display widget that shows values as they come in on the channel.

STATUS: Needs testing. Also needs a corresponding UI element to test its utility.

source
Synth.levelFunction
level(s::Signal; interval=0.015, refmin=1/(32767*32767), samplingrate=48000)

Computes the smoothed dB level of a signal. The range is from 0 to about 90dB.

  • refmin is the tiniest sliver of sound intensity that can be registered. The default is set to a value appropriate for 16-bit sampled sound.
  • interval is the smoothing interval - i.e. the time constant of the first order filter that's applied on the square of the signal.

refmin determines the range since it is the floor of the signal that is 0dB. For the default value, the max ends up around $20\log_{10}(32767) \approx 90.309\text{dB}$. A factor of two change in amplitude is a change in level of around 6.021 dB.

source
Synth.busFunction
bus(clk :: Signal) :: Bus

Creates a "bus" which is itself a signal that can be composed with other signals and processors. The bus runs on its own clock and sending Tuple{Float64,Signal} values on the .gchan property will trigger those signals at the given times according to the clock. The scheduling is sample accurate.

A "bus" supports fanout.

source
bus(tempo_bpm::Real)

Simpler constructor for a fixed tempo bus.

source
Synth.schedFunction
sched(sch :: Bus{Clk}, t::Real, s::Signal) where {Clk <: Signal}
sched(sch::Bus{Clk}, s::Signal) where {Clk<:Signal}
sched(sch::Bus{Clk}, t::Real, g::Gen) where {Clk<:Signal}
sched(sch::Bus{Clk}, g::Gen) where {Clk<:Signal}

Schedules a signal to start at time t according to the clock of the given bus. In the third variant without a t, the scheduling happens at an ill-specified "now" - which basically means "asap".

source
Synth.nowFunction
now(s::Bus{<:Signal})::Float64
now(s::Bus{<:Signal}, b::Type{<:Integer})::Float64
now(s::Bus{<:Signal}, b::Rational)::Float64
now(s::Bus{<:Signal}, b::Integer)::Float64

Returns the bus' "current time". If a beat is passed or asked for, the time is quantized to beats according to the clock's tempo.

source
Synth.laterFunction
later(delay_secs :: Float64, s :: Signal)

Postpones the signal by the given delaysecs. Note that this is not the same as a delay line where there is memory allocated to store some of the samples. The signal is not touched until `delaysecs` has passed, and the time value that the signal ends up seeing also does not span the period up to the delay.

source