Stimulus Generation

So far we have seen several examples of how to generate sounds and simple images (text). Here we'll cover stimulus generation in more detail.

Sounds

Weber's primary focus is on psychoacoustics, so there are many methods for generating and manipulating sounds. There are two primary ways to create sound stimuli: loading a file and composing sound primitives.

Loading a file

Using files as stimuli can be done by calling play on a file name, like so.

addtrial(moment(play,"mysound_file.wav"))
Sounds are cached

You can safely play the same file multiple times: the sound is cached, and will only load into memory once.

If you need to manipulate the sound before playing it, you can load it using sound. For example, to remove any frequencies from "mysound.wav" above 400Hz before playing the sound, you could do the following.

mysound = lowpass(sound("mysound.wav"),400Hz)
addtrial(moment(play,mysound))

Sound Primitives

There are several primitives you can use to generate simple sounds directly in Weber. They are tone (to create a pure), noise (to generate white noise), silence (for a silent period) and harmonic_complex (to create multiple pure tones with integer frequency ratios).

These primitives can then be combined and manipulated to generate more interesting sounds. You can filter sounds (bandpass, bandstop, lowpass, highpass and lowpass), mix them together (mix) and set an appropriate decibel level (attenuate). You can also manipulate the envelope of the sound (ramp, rampon, rampoff, fadeto, envelope and mult).

For instance, to play a 1 kHz tone for 1 second inside of a noise with a notch from 0.5 to 1.5 kHz, with 5 dB SNR you could call the following.

mysound = tone(1kHz,1s)
mysound = ramp(mysound)
mysound = attenuate(mysound,20)

mynoise = noise(1s)
mynoise = bandstop(mynoise,0.5kHz,1.5kHz)
mynoise = attenuate(mynoise,25)

addtrial(moment(play,mix(mysound,mynoise))

Weber exports the macro @> (from Lazy.jl) to simplify this pattern. It is easiest to understand the macro by example: the below code yields the same result as the code above.

mytone = @> tone(1kHz,1s) ramp attenuate(20)
mynoise = @> noise(1s) bandstop(0.5kHz,1.5kHz) attenuate(25)
addtrial(moment(play, mix(mytone,mynoise)))

Weber also exports @>>, and @_ (refer to Lazy.jl for details).

Sounds are arrays

Sounds are just a specific kind of array of real valued numbers. The amplitudes of a sound are represented as real numbers between -1 and 1 in sequence at a sampling rate specific to the sound's type. They can be manipulated in the same way that any array can be manipulated in Julia, with some additional support for indexing sounds using time units. For instance, to get the first 5 seconds of a sound you can do the following.

mytone = tone(1kHz,10s)
mytone[0s .. 5s]

To represent the end of a sound using this special indexing, you can use ends. For instance, to get the last 5 seconds of mysound you can do the following.

mytone[5s .. ends]

We can concatenate multiple sounds, to play them in sequence. The following code plays two tones in sequence, with a 100 ms gap between them.

interval = [tone(400Hz,50ms); silence(100ms); tone(400Hz * 2^(5/12),50ms)]
addtrial(moment(play,interval))

Stereo Sounds

You can create stereo sounds with leftright, and reference the left and right channel using :left or :right as a second index, like so.

stereo_sound = leftright(tone(1kHz,2s),tone(2kHz,2s))
addtrial(moment(play,stereo_sound[:,:left],
         moment(2s,play,stereo_sound[:,:right]))

The functions left and right can also extract the left and right chnanel, but work on both sounds and streams.

Streams

In addition to the discrete sounds that have been discussed so far, Weber also supports sound streams. Streams are arbitrarily long: you need not decide when they should stop until after they start playing. All of the primitives described so far can apply to streams (including concatenation), except that streams cannot be indexed.

Streaming operations are lazy

All manipulations of streams are lazy: they are applied just as the stream is played. The more operators you apply to a stream the more processing that has to occur during playback. If you have a particularly complicated stream you may have to increase streaming latency by changing the stream_unit parameter of setup_sound, or consider an alternative approach (e.g. audible).

To create a stream you can use one of the standard primitives, leaving out the length parameter. For example, the following will play a 1 kHz pure tone until Weber quits.

addtrial(moment(play,tone(1kHz)))

Streams always play on a specific stream channel, so if you want to stop the stream at some point you can request that the channel stop. The following plays a pure tone until the experiment participant hits spacebar.

addtrial(moment(play,tone(1kHz),channel=1),
         await_response(iskeydown(key":space:")),
         moment(stop,1))

Streams can be manipulated as they are playing as well, so if you wanted to have a ramp at the start and end of the stream to avoid clicks, you could change the example above, to the following.

ongoing_tone = @> tone(1kHz) rampon
addtrial(moment(play,ongoing_tone,channel=1),
         await_response(iskeydown(key":space:")),
         moment(play,rampoff(ongoing_tone),channel=1))
Streams are stateful

This example also demonstrates the stateful nature of streams. Once some part of a stream has been played it is forever consumed, and cannot be played again. After the stream is played, subsequent modifications only apply to unplayed frames of the stream. BEWARE: this means that you cannot play two different modifications of the same stream.

Just as with any moment, these manipulations to streams can be precisely timed. The following will turn the sound off precisely 1 second after the space key is pressed.

ongoing_tone = @> tone(1kHz) rampon
addtrial(moment(play,ongoing_tone,channel=1),
         await_response(iskeydown(key":space:")),
         moment(1s,play,rampoff(ongoing_tone),channel=1))

If you wish to turn the entirety of a finite stream into a sound, you can use sound. You can also grab the next section of an infinite stream using sound if you provide a second parameter specifying the length of the stream you want to turn into a sound.

Some manipulations of streams require that the stream be treated as a sound. You can modify individual sound segments as they play from the stream using audiofn. (Calling audiofn on a sound, rather than a stream, is the same as applying the given function to the sound directly).

Low-level Sound/Stream Generation

Finally, if none of the functions above suit your purposes for generating sounds or streams, there are two more low-level approachs. You can use the function audible to define a sound or stream using a function f(t) or f(i) defining the amplitudes for any given time or index. Alternatively you can convert any array to a sound using sound.

Images

Images can also be generated by either displaying a file or generating image primitives.

Loading a file

You can display an image file by calling display on the file name.

addtrial(moment(display,"myimage.png"))
Images are cached

You can safely display the same file multiple times: the image is cached, and will only load into memory once.

Analogous to sounds, if you need to manipulate the image before displaying it you can load it using visual. For example, the following displays the upper quarter of an image.

myimage = visual("myimage.png")
addtrial(moment(display,myimage[1:div(end,2),1:div(end,2)]))

Note that displaying a string can also result in that string being printed to the screen. Weber determines the difference between a string you want to display and a string referring to an image file by looking at the end of the string. If the string ends in a valid image file type (.bmp, .jpeg, .png, etc...), Weber assumes it is an image file you want to load, otherwise it assumes it is a string you want to print to the screen.

Image Primitives

Support for generating images in Weber comes from Images.jl. In this package, images are represented as arrays. For instance, to display a white 100x100 pixel box next to a black 100x100 pixel box, we could do the following.

addtrial(moment(display,[ones(100,100); zeros(100,100)]))

For more information about generating images please refer to the JuliaImages documentation.