Reference
Signal Generation
SignalOperators.Signal
— FunctionSignal(x,[framerate])
Coerce x
to be a signal, optionally specifying its frame rate (usually in Hz). All signal operators first call Signal(x)
for each argument. This means you only need to call Signal
when you want to pass additional arguments to it.
If you pipe Signal
and pass a frame rate, you must specify the units of the frame rate (e.g. x |> Signal(20Hz)
). A unitless number is always interpreted as a constant, infinite-length signal (see below).
If you are implementing Signal
for a custom signal, you will need to support the second argument of Signal
by specifying fs::Union{Number,Missing}=missing
, or equivalent.
The type of objects that can be coerced to signals are as follows.
Filenames
A string with a filename ending with an appropriate filetype can be read in as a signal. You will need to call import
or using
on the backend for reading the file.
Available backends include the following pacakges
Existing signals
Any existing signal just returns itself from Signal
. If a frame rate is specified it will be set if x
has an unknown frame rate. If it has a known frame rate and doesn't match framerate(x)
an error will be thrown. If you want to change the frame rate of a signal use ToFramerate
.
Numbers
Numbers can be treated as infinite length, constant signals of unknown frame rate.
Example
rand(10,2) |> Amplify(20dB) |> nframes == 10
The length of numbers are treated specially when passed to OperateOn
: if there are other types of signal passed as input, the number signals are considered to be as long as the longest signal.
nframes(Mix(1,2)) == inflen
nframes(Mix(1,rand(10,2))) == 10
Arrays
Any array can be interpreted as a signal. By default the first dimension is time, the second channels and their frame rate is a missing value. If you pass a non-missin gframerate, and the array currently has a missing frame rate a Tuple
value will be returned (see "Array & Number" below).
If you specify a non-missing frame rate to an array type with a missing frame rate the return value will be a Tuple (see Array & Number section below). Some array types change this default behavior, as follows.
Arrays of more than two dimensions are not currently supported.
AxisArrays
, if they have an axis labeledtime
and one or zero additional axes, can be treated as a signal. The time dimension must be represented using on object with thestep
function defined (e.g. anyAbstractRange
object).SampleBuf
objects are also properly interpreted as signals, as per the conventions employed for its package.DimensionalArrays
can be treated as signals if there is aTime
dimension, which must be represented using an object with thestep
function defined (e.g.AbstractRange
) and zero or one additional dimensions (treated as channels)
Array & Number
A tuple of an array and a number can be interepted as a signal. The first dimension is time, the second channels, and the number determines the frame rate (in Hertz).
Functions
Signal(fn,[framerate];[ω/frequency],[ϕ/phase])
Functions can define infinite length signals of known or unknown frame rate. The function fn
can either return a number or, for multi-channel signals, a tuple of values.
The input to fn
is either a phase value or a time value. If a frequency is specified (using either the ω or frequency keyword), the input to fn
will be a phase value in radians, ranging from 0 to 2π. If no frequency is specified the value passed to fn
is the time in seconds. Specifying phase (by the ϕ or phase keyword) will first add that value to the input before passing it to fn
. When frequency is specified, the phase is assumed to be in units of radians (but you can also pass degrees by using °
or a unit of time (e.g. s
for seconds)). When frequency is not specified the phase is assumed to be in units of seconds.
If fn == randn
no frequency or phase can be specified. Instead there is a single keyword argument, rng
, which allows you to specify the random number generator; rng
defaults to Random.GLOBAL_RNG
.
SignalOperators.sink
— Functionsink(signal,[to])
Creates a given type of object (to
) from a signal. By default the type of the resulting sink is determined by the type of the underlying data of the signal: e.g. if x
is a SampleBuf
object then sink(Mix(x,2))
is also a SampleBuf
object. If there is no underlying data (Signal(sin) |> sink
) then a Tuple of an array and the framerate is returned.
Though sink
often makes a copy of an input array, it is not guaranteed to do so. For instance sink(Until(rand(10),5frames))
will simply take a view of the first 5 frames of the input.
Values for to
Type
If to
is an array type (e.g. Array
, DimensionalArray
) the signal is written to a value of that type.
If to
is a Tuple
the result is an Array
of samples and a number indicating the sample rate in Hertz.
Filename
If to
is a string, it is assumed to describe the name of a file to which the signal will be written. You will need to call import
or using
on an appropriate backend for writing to the given file type.
Available backends include the following pacakges
SignalOperators.sink!
— Functionsink!(array,x)
Write size(array,1)
frames of signal x
to array
.
Signal Inspection
SignalOperators.inflen
— Constantinflen
Represents an infinite length. Proper overloads are defined to handle arithmetic and ordering for the infinite value.
SignalBase.duration
— Functionduration(x)
Return the duration of the signal in seconds, if known. May return missing
(e.g. for a stream).
A fallback implementation of duration
uses nframes(x) / framerate(x)
. However, if one or both of these is missing
and you want duartion
to return a non-missing value, you can define a custom method of duration
.
SignalBase.nframes
— Functionnframes(x)
Returns the number of frames in the signal, if known. May return missing
(e.g. for a file stream).
SignalBase.nchannels
— Functionnchannels(x)
Returns the number of channels in the signal.
SignalBase.framerate
— Functionframerate(x)
Returns the frame rate of the signal (in Hertz). May return missing
if the frame rate is unknown.
SignalBase.sampletype
— Functionsampletype(x)
Returns the element type of an individual channel of a signal (e.g. Float64
).
The result of sampletype
and eltype
(when defined) are often the same. They are distinct so that these two can diverge when appropriate.
Signal Operators
Basic Operators
SignalOperators.Until
— FunctionUntil(x,time)
Create a signal of all frames of x
up until and including time
.
SignalOperators.until
— FunctionSignalOperators.After
— FunctionAfter(x,time)
Create a signal of all frames of x
after time
.
If you use frames
as the unit here, keep in mind that because this returns all frames after the given index, the result is effectively zero indexed: i.e. all(sink(After(1:10,1frames)) .== 2:10)
SignalOperators.after
— FunctionSignalOperators.Window
— FunctionWindow(x;from,to)
Window(x;at,width)
Extract a window of time from a signal by specifying either the start and stop point of the window (from
and to
) or the center and width (at
and wdith
) of the window.
SignalOperators.window
— FunctionSignalOperators.Append
— FunctionAppend(x,y,...)
Append a series of signals, one after the other.
SignalOperators.append
— FunctionSignalOperators.Prepend
— FunctionPrepend(x,y,...)
Prepend the series of signals: Prepend(xs...)
is equivalent to Append(reverse(xs)...)
.
SignalOperators.prepend
— FunctionSignalOperators.Pad
— FunctionPad(x,padding)
Create a signal that appends an infinite number of values, padding
, to x
. The value padding
can be:
- a number
- a tuple or vector
- a type function: a one argument function of the
sampletype
ofx
- a value function: a one argument function of the signal
x
for whichSignalOperators.valuefunction(padding) == true
. - an indexing function: a three argument function following the same type signature as
getindex
for two dimensional arrays.
If the signal is already infinitely long (e.g. a previoulsy padded signal), Pad
has no effect.
If padding
is a number it is used as the value for all samples past the end of x
.
If padding
is a tuple or vector it is the value for all frames past the end of x
.
If padding
is a type function it is passed the sampletype
of the signal and the resulting value is used as the value for all frames past the end of x
. Examples include zero
and one
If padding
is a value function it is passed the signal x
just before padding occurs during a call to sink
; it should return a tuple of sampletype(x)
values. The return value is repeated for all remaining frames of the signal. For example, lastframe
is a value function.
If padding
is an indexing function (it accepts 3 arguments) it will be used to retrieve frames from the signal x
assuming it conforms to the AbstractArray
interface, with the first index being frames and the second channels. If the frame index goes past the bounds of the array, it should be transformed to an index within the range of that array. Note that such padding functions only work on signals that are also AbstractArray objects. You can always generate an array from a given signal by first passing it through sink
or sink!
.
A indexing function will also work on a signal represented as a tuple of an array and number; it simply passed the array (leaving off the number).
See also
SignalOperators.Extend
— FunctionExtend(x,padding)
Behaves like Pad
, except when passed directly to OperateOn
; in that case, the signal x
will only be padded up to the length of the longest signal input to OperateOn
See Also
SignalOperators.mirror
— Functionmirror(x,i,j)
An indexing function which mirrors the indices when i > size(x,1). This means that past the end of the signal x, the signal first repeats with frames in reverse order, then repeats in the original order, so on and so forth. It can be passed as the second argument to Pad
.
SignalOperators.cycle
— Functioncycle(x,i,j)
An indexing function which wraps index i using mod, thus repeating the signal when i > size(x,1). It can be passed as the second argument to Pad
.
SignalOperators.lastframe
— Functionlastframe
When passed as an argument to Pad
, allows padding using the last frame of a signal. You cannot use this function in other contexts, and it will normally throw an error. See Pad
.
SignalOperators.valuefunction
— FunctionSignalOperators.valuefunction(fn)
Returns true if fn
should be treated as a value function. See Pad
. If you wish your own function to be a value function, you can do this as follows.
SignalOperators.valuefunction(::typeof(myfun)) = true
Mapping Operators
SignalOperators.Filt
— FunctionFilt(x,::Type{<:FilterType},bounds...;method=Butterworth(order),order=5,
blocksize=4096)
Apply the given filter type (e.g. Lowpass
) using the given method to design the filter coefficients. The type is specified as per the types from DSP
Filt(x,h;[blocksize=4096])
Apply the given digital filter h
(from DSP
) to signal x
.
Blocksize
Blocksize determines the size of the buffer used when computing intermediate values of the filter. It need not normally be adjusted, though changing it can alter how efficient filter application is.
The non-lazy version of Filt
is filt
from the DSP
package. Proper methods have been defined such that it should be possible to call filt
on a signal and get a signal back.
The argument order for filt
follows a different convention, with x
coming after the filter specification. In contrast, Filt
uses the convention of keeping x
as the first argument to make piping possible.
SignalOperators.Normpower
— FunctionNormpower(x)
Return a signal with normalized power. That is, divide all frames by the root-mean-squared value of the entire signal.
SignalOperators.normpower
— FunctionSignalOperators.OperateOn
— FunctionOperateOn(fn,arguments...;padding=default_pad(fn),bychannel=false)
Apply fn
across the samples of the passed signals. The output length is the maximum length of the arguments. Shorter signals are extended using Extend(x,padding)
.
There is no piped version of OperateOn
, use Operate
to pipe. The shorter name is used to pipe because it is expected to be the more common use case.
Channel-by-channel functions (default)
When bychannel == false
the function fn
should treat each of its arguments as a single number and return a single number. This operation is broadcast across all channels of the input. It is expected to be a type stable function.
The signals are first promoted to have the same sample rate and the same number of channels using Uniform
.
Cross-channel functions
When bychannel=false
, rather than being applied to each channel seperately the function fn
is applied to each frame, containing all channels. For example, for a two channel signal, the following would swap these two channels.
x = rand(10,2)
swapped = OperateOn(x,bychannel=false) do val
val[2],val[1]
end
The signals are first promoted to have the same sample rate, but the number of channels of each input signal remains unchanged.
Padding
Padding determines how frames past the end of shorter signals are reported. If you wish to change the padding for all signals you can set the value of the keyword argument padding
. If you wish to specify distinct padding values for some of the inputs, you can first call Extend
on those arguments.
The default value for padding
is determined by the fn
passed. A fallback implementation of default_pad
returns zero
. The default value for the four basic arithmetic operators is their identity (one
for *
and zero
for +
).
To define a new default for a specific function, just create a new method of default_pad(fn)
myfun(x,y) = x + 2y
SignalOperators.default_pad(::typeof(myfun)) = one
sink(OperateOn(myfun,Until(5,2frames),Until(2,4frames))) == [9,9,5,5]
SignalOperators.Operate
— FunctionOperate(fn,rest...;padding,bychannel)
Equivalent to
julia (x) -> OperateOn(fn,x,rest...;padding=padding,bychannel=bychannel)
`
See also
SignalOperators.operate
— Functionoperate(fn,args...;padding,bychannel)
Equivalent to sink(OperateOn(fn,args...;padding,bychannel))
See also
SignalOperators.Mix
— FunctionMix(xs...)
Sum all signals together, using OperateOn
. Unlike OperateOn
, Mix
includes a piped version.
SignalOperators.mix
— FunctionSignalOperators.Amplify
— FunctionAmplify(xs...)
Find the product, on a per-frame basis, for all signals xs
using OperateOn
. Unlike OperateOn
, Amplify
includes a piped version.
SignalOperators.amplify
— FunctionSignalOperators.AddChannel
— FunctionAddChannel(xs...)
Concatenate the channels of all signals into one signal, using OperateOn
. This will result in a signal with sum(nchannels,xs)
channels. Unlike OperateOn
, AddChannels
includes a piped version.
SignalOperators.addchannel
— FunctionSignalOperators.SelectChannel
— FunctionSelectChannel(x,n)
Select channel n
of signal x
, as a single-channel signal, using OperateOn
. Unlike OperateOn
, SelectChannel
includes a piped version.
SignalOperators.selectchannel
— FunctionRamping Operators
SignalOperators.RampOn
— FunctionRampOn(x,[len=10ms],[fn=x -> sinpi(0.5x)])
Ramp the onset of a signal, smoothly transitioning from 0 to full amplitude over the course of len
seconds.
The function determines the shape of the ramp and should be non-decreasing with a range of [0,1] over the domain [0,1]. It should map over the entire range: that is fn(0) == 0
and fn(1) == 1
.
Both len
and fn
are optional arguments: either one or both can be specified, though len
must occur before fn
if present.
SignalOperators.rampon
— FunctionSignalOperators.RampOff
— FunctionRampOff(x,[len=10ms],[fn=x -> sinpi(0.5x)])
Ramp the offset of a signal, smoothly transitioning from full amplitude to 0 amplitude over the course of len
seconds.
The function determines the shape of the ramp and should be non-decreasing with a range of [0,1] over the domain [0,1]. It should map over the entire range: that is fn(0) == 0
and fn(1) == 1
.
Both len
and fn
are optional arguments: either one or both can be specified, though len
must occur before fn
if present.
SignalOperators.rampoff
— FunctionSignalOperators.Ramp
— FunctionRamp(x,[len=10ms],[fn=x -> sinpi(0.5x)])
Ramp the onset and offset of a signal, smoothly transitioning from 0 to full amplitude over the course of len
seconds at the start and from full to 0 amplitude over the course of len
seconds.
The function determines the shape of the ramp and should be non-decreasing with a range of [0,1] over the domain [0,1]. It should map over the entire range: that is fn(0) == 0
and fn(1) == 1
.
Both len
and fn
are optional arguments: either one or both can be specified, though len
must occur before fn
if present.
SignalOperators.ramp
— FunctionSignalOperators.FadeTo
— FunctionFadeTo(x,y,[len=10ms],[fn=x->sinpi(0.5x)])
Append x to y, with a smooth transition lasting len
seconds fading from x
to y
(so the total length is duration(x) + duration(y) - len
).
This fade is accomplished with a RampOff
of x
and a RampOn
for y
. fn
should be non-decreasing with a range of [0,1] over the domain [0,1]. It should map over the entire range: that is fn(0) == 0
and fn(1) == 1
.
Both len
and fn
are optional arguments: either one or both can be specified, though len
must occur before fn
if present.
SignalOperators.fadeto
— FunctionReformatting Operators
SignalOperators.ToFramerate
— FunctionToFramerate(x,fs;blocksize)
Change the frame rate of x
to the given frame rate fs
. The underlying implementation depends on whether the input is a computed or data signal, as determined by EvalTrait
.
Computed signals (e.g. Signal(sin)
) are resampled exactly: the result is simply computed for more time points or fewer time points, so as to generate the appropriate number of frames.
Data-based signals (Signal(rand(50,2))
) are resampled using filtering (akin to DSP.resample
). In this case you can use the keyword arugment blocksize
to change the analysis window used. See Filt
for more details. Setting blocksize
for a computed signal will succeed, but different blocksize
values have no effect on the underlying implementation.
Implementation
You need only implement this function for custom signals for particular scenarios, described below.
Custom Computed Signals
If you implement a new sigal type that is a computed signal, you must implement ToFramerate
with the following type signature.
function ToFramerate(x::MyCustomSignal,s::IsSignal{<:Any,<:Number},
c::ComputedSignal,framerate;blocksize)
## ...
end
The result should be a new version of the computed signal with the given frame rate.
Handling missing frame rates
If you implement a new signal type that can handle missing frame rate values, you will need to implement the following version of ToFramerate
so that a known frame rate can be applied to a signal with a missing frame rate.
function ToFramerate(x::MyCustomSignal,s::IsSignal{<:Any,Missing},
evaltrait,framerate;blocksize)
## ...
end
The result should be a new version of the signal with the specified frame rate.
SignalOperators.toframerate
— Functiontoframerate(x,fs;blocksize)
Equivalent to sink(ToFramerate(x,fs;blocksize=blocksize))
See also
SignalOperators.ToChannels
— FunctionToChannels(x,ch)
Force a signal to have ch
number of channels, by mixing channels together or broadcasting a single channel over multiple channels.
SignalOperators.tochannels
— FunctionSignalOperators.ToEltype
— FunctionToEltype(x,T)
Converts individual samples in signal x
to type T
.
SignalOperators.toeltype
— FunctionSignalOperators.Format
— FunctionFormat(x,fs,ch)
Efficiently convert both the framerate (fs
) and channels ch
of signal x
. This selects an optimal ordering for ToFramerate
and ToChannels
to avoid redundant computations.
SignalOperators.format
— FunctionSignalOperators.Uniform
— FunctionUniform(xs;channels=false)
Promote the frame rate (and optionally the number of channels) to be the highest frame rate (and optionally highest channel count) of the iterable of signals xs
.
Uniform
rarely needs to be called directly. It is called implicitly on all passed signals, within the body of operators such as OperateOn
.
Custom Signals
SignalOperators.SignalTrait
— FunctionSiganlOperators.SignalTrait(::Type{T}) where T
Returns either nothing
if the type T should not be considered a signal (the default) or IsSignal
to indicate the signal format for this signal.
SignalOperators.IsSignal
— TypeSignalOperators.IsSignal{T,Fs,L}
Represents the format of a signal type with three type parameters:
T
- Thesampletype
of the signal.Fs
- The type of the framerate. It should be eitherFloat64
orMissing
.L
- The type of the length of the signal. It should be eitherInfinity
,Missing
orInt
.
SignalOperators.EvalTrait
— FunctionSiganlOperators.EvalTrait(x)
Indicates whether the signal is a DataSignal
or ComputedSignal
. Data signals represent frames concretely as a set of frames. Examples include arrays and numbers. Data signals generally return themselves, or some wrapper type when sink
is called on them. Computed signals are any signal that invovles some intermediate computation, in which frames must be computued on the fly. Calls to sink
on a computed signal results in some new, data signal. Most signals returned by a signal operator are computed signals.
Computed signals have the extra responsibility of implementing ToFramerate
SignalOperators.nextblock
— FunctionSignalOperators.nextblock(x,maxlength,skip,[last_block])
Retrieve the next block of frames for signal x
, or nothing, if no more blocks exist. Analogous to Base.iterate
. The returned block must satisfy the interface for signal blocks as described in custom signals.
Arugments
x
: the signal to retriev blocks frommaxlength
: The resulting block must have no more thanmaxlength
frames, but may have fewer frames than that; it should not have zero frames unlessmaxlength == 0
.skip
: Ifskip == true
, it is guaranted thatframe
will never be called on the returned block. The value ofskip
istrue
when skipping blocks during a call toAfter
).last_block
The fourth argument is optional. If included, the block that occurs after this block is returned. If it is left out, nextblock returns the very first block of the signal.
SignalOperators.frame
— FunctionSignalOperators.frame(x,block,i)
Retrieves the frame at index i
of the given block of signal x
. A frame is one or more channels of sampletype(x)
values. The return value should be an indexable object (e.g. a number, tuple or array) of these channel values. This method should be implemented by blocks of custom signals.
SignalOperators.timeslice
— FunctionSignalOperators.timeslice(x::AbstractArray,indices)
Extract the slice of x with the given time indices.
Custom signals can implement this method if the signal is an AbstractArray
allowing the use of a fallback implementation of SignalOperators.nextblock
.
SignalOperators.ArrayBlock
— TypeArrayBlock{A,S}(data::A,state::S)
A straightforward implementation of blocks as an array and a custom state. The array allows a generic implementation of nframes
and SignalOperators.frame
. The fields of this struct are data
and state
.
Custom signals can return an ArrayBlock
from SignalOperators.nextblock
to allow for fallback implementations of nframes
and SignalOperators.frame
.
Custom Sinks
SignalOperators.initsink
— FunctionSignalOperators.initsink(x,::Type{T})
Initialize an object of type T so that it can store all frames of signal x
.
If you wish an object to serve as a custom sink you can implement this method. You can use nchannels
and sampletype
of x
to determine how to initialize the object for the first method, or you can just use initsink(x,Array)
and wrap the return value with your custom type.
SignalOperators.sink_helper!
— FunctionSignalOperators.sink_helper!(result,written,x,block)
Write the given block
of frames from signal x
to result
given that a total of written
frames have already been written to the result.
This method should be fast: i.e. a for loop using @simd and @inbounds. It should call nframes
and SignalOperators.frame
on the block to write the frames. Do not call frame
more than once for each index of the block.