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 Synth.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::Signal, duration_secs=Inf; blocksize=64)
play(soundfile::AbstractString)

Other named arguments -

  • mididevice::AbstractString="" a name pattern to match against the MIDI output device to select. Leave as default to pick a suitable one.

  • audiodevice::AbstractString="" a case insensitive name pattern to match against the set of available audio output devices. If you leave as default, then only the availability of sufficient output channels will be considered as a criterion for selecting a device. For example, if you pass "speaker", it will match against "MacBook Air Speakers".

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.

The version that takes a string is a short hand for loading the file and playing it. The file is cached in memory, so it won't load it again if you call it again.

Returns a stop function (like with startaudio 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(obs::Observable{Float32}, range::Tuple{Real,Real}; ...) :: Control
control(obs::Observable{Float32}, range::StepRangeLen; ...) :: Control
control(range::ControlRange; ...) :: Control

Named parameters for all variants -

  • dezipper_interval::Real with a default value of 0.0075 in seconds,
  • initial::Real with a default value of 0.0f0,
  • samplingrate::Real with a default value of 48000 in Hz,
  • label::AbstractString with a default value of empty string. Optional label that may be shown when visualizing the 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 you use a tuple as range, then the value may be any value within that range and the range is taken to be continuous. If you use a StepRangeLen like 0.5f0:0.1f0:1.5f0 then the range is taken to be discrete and reading the control value will produce only discrete (quantized) values.

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
Missing docstring.

Missing docstring for Synth.stop. Check Documenter's build log for details.

Synth.probeFunction
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, duration :: Float64 = 0.25, interval :: Float64 = 0.04; samplingrate = 48000) :: Probe

A waveprobe, like probe is a "pass through" signal transformer which periodically reports a reading of the signal to an Observable. Since it is a pass-through, you can insert a probe at any signal point. The default value has it sampling the signal every 40ms.

connectui can be used to connect a Probe{TimedSamples} to a WaveProbe.

source
Synth.levelFunction
 level(s::Signal)

Essentially performs the same computation as monitor, but gives the value as a signal, while monitor passes through the signal while observing it.

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::AudioGenBus{Clk}, t::Real, s::Signal) where {Clk <: Signal}
sched(sch::AudioGenBus{Clk}, s::Signal) where {Clk<:Signal}
sched(sch::AudioGenBus{Clk}, t::Real, g::Gen) where {Clk<:Signal}
sched(sch::AudioGenBus{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".

Note: When scheduling signals and gens within a proc() call, a maximum of 16 is supported. You usually won't need so many since any co-scheduled gens and signals usually have a temporal relationship that is better captured using available composition operators.

source
sched(dev::MIDIDest, t::Real, msg::MIDIMsg)
sched(t::Real, msg::MIDIMsg)
sched(dev::MIDIDest, msg::MIDIMsg)
sched(msg::MIDIMsg)

The first two schedule a MIDI message to be sent at the designated time. The last two will send it out immediately. If the MIDI output device is omitted, then the current midi output device active will be used.

source
Synth.nowFunction
now(s::AudioGenBus{<:Signal})::Float64
now(s::AudioGenBus{<:Signal}, b::Type{<:Integer})::Float64
now(s::AudioGenBus{<:Signal}, b::Rational)::Float64
now(s::AudioGenBus{<: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.afterFunction
after(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