uxn tutorial: day 7, more devices
en español:
this is the seventh and last section of the uxn tutorial! here we talk about the devices in the varvara computer that we haven't covered yet: file, datetime and audio.
this should be a light and calm end of our journey, as it has to do less with programming logic and more with the input and output conventions in these devices.
let's begin!
the file devices
the file devices in the varvara computer allow us to read and write to external files.
there are two of them and they work in exactly the same way.
device ports
their ports are normally defined as follows:
- the vector short is currently unused
- the success short stores the length of the data that was successfully read or written, or zero if there was an error
- the name short is for the memory address where the filename (null-terminated, i.e. with a 00) is stored
- the length short is the amount of bytes to read or write: don't forget that the program memory is ffff plus 1 bytes long, and that the program itself is stored there!
- the read short is for the starting memory address where the read data should be stored
- the write short is for the starting memory address where the data to be written is stored
- the stat short is similar to read, but reads the directory entry for the filename
- the delete byte deletes the file when any value is written to it
- setting the append byte to 01 makes write append data to the end of the file. when the append byte has the the default value, 00, write overwrites the contents from the start
a read operation is started when the read short is written to, and a write operation is started when the write short is written to.
these might seem like a lot of fields to handle, but we'll see that they are not too much of a problem!
reading a file
the following discussion will focus in only one file device. don't forget that for more advanced uses you can take advantage of both!
in order to read a file, we need to know the following:
- the path of the file, written as a labelled string of text in program memory and terminated by a 00 - this path would be relative to the location where uxnemu is ran.
- the amount of bytes that we want to read from the file: it's okay if this number is not equal to the size of the file; it can be smaller or even greater.
- the label for a reserved section of program memory where read data will be stored
and that's it!
we can use a structure like the following, where the filename and reserved memory are under a label, and the load-file subroutine under another one:
note that for the filename we are using the raw string rune (") that allows us to write several characters in program memory until a whitespace is found.
in this example we are writing a character to the console according to the success short being zero or not, but we could decide to take any action that we consider appropriate.
also, in this example we are not really concerned with how many bytes were actually read: keep in mind that this information is stored in File/success until another read or write happens!
it's important to remember that, as always in this context, we are dealing with raw bytes.
not only we can choose to treat these bytes as text characters, but also we can choose to use them as sprites, coordinates, dimensions, colors, etc!
writing a file
in order to write a file, we need:
- the path of the file, written as a string in program memory as the case above
- the amount of bytes that we want to write into the file
- the label for the section of program memory that we will write into the file
keep in mind that the file will be completely overwritten unless you set append to 01!
the following program will write "hello" and a newline (0a) into a file called "test.txt":
note how similar it is to the load-file subroutine!
the only differences, beside the use of File/write instead of File/read, are the file length and the comparison for the success short: in this case we know for sure how many bytes should have been written.
a brief case study: the theme file
programs for the varvara computer written by 100r tend to have the ability to read a "theme" file that contains six bytes corresponding to the three shorts for the system colors.
these six bytes are in order: the first two are for the red channel, the next two for the green channel, and the last two for the blue channel.
this file has the name ".theme" and is written to a local directory from nasu whenever a spritesheet is saved.
reading the theme file
we could adapt our previous subroutine in order to load the theme file and apply its data as system colors:
note how the &data and &r labels are pointing to the same location: it's not a problem! :)
writing the theme file
and for doing the opposite operation, we can read the system colors into our reserved space in memory, and then write them into the file:
i invite you to compare these subroutines with the ones present in the 100r programs like nasu!
the datetime device
the datetime device can be useful for low precision timing and/or for visualizations of time.
it has several fields that we can read, all of them based on the current system time and timezone:
- the year short corresponds to the number of year in the so called common era
- the month byte counts the months since january (i.e. january is 0, february 1, and so on)
- the day byte counts the days in the month starting from 1
- the hour, minute and second bytes correspond to what one would expect: their values go from 0 to 23, or 0 to 59 respectively.
- dotw (day of the week) is a byte that counts the days since sunday (i.e sunday is 0, monday is 1, tuesday is 2, and so on)
- doty (day of the year) is a byte that counts the days since january 1 (i.e. jan 1st is 0, jan 2nd is 1, and so on)
- isdst (is daylight saving time) is a flag, 01 if it's daylight saving time and 00 if it's not.
based on this, it should be straightforward for you to use them! e.g. in order to read the hour of the day into the stack, we'd do:
some time-based possibilities
i invite you to develop a creative visualization of time!
maybe you can use these values as coordinates for some sprites, or maybe you can use them as sizes or limits for shapes created with loops.
or what about conditionally drawing sprites, and/or changing the system colors depending on the time? :)
you can also use the values of date and time as seeds to generate some pseudo-randomness!
lastly, remember that for timing events with more precision than seconds, you can count the times that the screen vector has been fired.
the audio device
at last, the audio device! or i should say, the audio devices!
varvara has four identical stereo devices (or "channels"), that get mixed before going into the speakers/headphones:
similar to how in the screen device we can draw by pointing to addresses with sprite data, in the audio devices we will be able to play sounds by pointing to addresses with audio data ("samples").
stretching the analogy: similar to how we can draw sprites in different positions on the screen, we can play our samples at different rates, volume, and envelopes.
we'll assume that you might not be familiar with these concepts, so we'll briefly discuss them.
samples
as we mentioned above, we can think of the sample data as the equivalent of sprite data.
they have to be in program memory, they have a length that we have to know, and we can refer to them by labels.
the piano.tal example in the uxn repository, has several of them, all of them 256 bytes long:
and what do these numbers mean?
in the context of varvara, we can understand them as multiple unsigned bytes (u8) that correspond to amplitudes of the sound wave that compose the sample.
a "playhead" visits each of these numbers during a specific time, and uses them to set the amplitude of the sound wave.
the following images show the waveform of each one of these samples.
when we loop these waveforms, we get a tone based on their shape!
piano-pcm:
violin-pcm:
sin-pcm:
tri-pcm:
saw-pcm:
similar to how we have dealt with sprites, and similar to the file device discussed above, in order to set a sample in the audio device we just have to write its address and its length:
the frequency at which this sample is played (i.e. at which the wave amplitude takes the value from the next byte) is determined by the pitch byte.
pitch
the pitch byte makes the sample start playing whenever we write to it, similar to how the sprite byte performs the drawing of the sprite when we write to it.
the first 7 bits (from right to left) of the byte correspond to a midi note, and therefore to the frequency at which the sample will be played.
the eighth bit is a flag: when it's 0 the sample will be looped, and when it's 1 the sample will be played only once.
normally we will want to loop the sample in order to generate a tone based on it. only when the sample is long enough it will make sense to not loop it and play it once.
regarding the bits for the midi note, it's a good idea to have a midi table around to see the hexadecimal values corresponding to different notes.
middle C (C4, or 3c in midi) is assumed to be the default pitch of the samples.
a "sample" program
in theory, it would appear that the following program should play our sample at that frequency, or not?
not really!
but almost there! in order to actually hear the sound, we need two more things: to set the volume of the device, and to set the ADSR envelope!
volume
the volume byte is divided in two nibbles: the high nibble corresponds to the volume of the left channel, and the low nibble corresponds to the volume of the right channel.
therefore, each channel has 16 possible levels: 0 is the minimum, and f the maximum.
the following would set the maximum volume in the device:
although the samples are mono, we can pan them with the volume byte in order to get stereo sound!
ADSR envelope
the last component we need in order to play audio is the ADSR envelope.
ADSR stands for attack, decay, sustain, and release. it is the name of a common "envelope" that modulates the amplitude of a sound from beginning to end.
in the varvara computer, the ADSR components work as follows:
- the attack section is the time that it takes to bring the amplitude of the playing sound from 0 to 100%.
- then, the decay section is the time that it takes to bring the amplitude from 100% to 50%
- then, during the sustain section the amplitude is kept at 50%
- and finally, in the release section the amplitude goes from 50% to 0%.
each of these transitions are done linearly.
in the ADSR short of the audio device, there is one nibble for each of the components: therefore each one can have a duration from 0 to f.
the units for these durations are 15ths of a second.
as an example, if the duration of the attack component is 'f', then it will last one second (15/15 of a second, in decimal).
the following will set the maximum duration on each of the components, making the sound last 4 seconds in total:
ok, now we are ready to play the sound!
playing the sample
the following program has now the five components we need in order to play a sound: a sample address, its length, the adsr durations, the volume, and its pitch!
note (!) that it will play the sound only once, and it does it when the program starts.
some suggested experiments
i invite you to experiment modifying the ADSR values: how does the sound change when there's only one of them? or when all of them are small numbers? or with different combinations of durations?
also, try changing the pitch byte: does it correspond to your ears with the midi values as expected?
and how does the sound changes when you use a different sample? can you find or create different ones?
playing more than once
once we have set up our audio device with a sample, length, ADSR envelope and volume, we could play it again and again by (re)writing a pitch at a different moment; the other parameters can be left untouched.
for example, a macro like the following could allow us to play a note again according to the pitch given at the top of the stack:
when a specific event happened, you could call it:
keep in mind that every time you write a pitch, the playback of the sample and the shape of the envelope starts over regardless of where it was.
some ideas
what if you implement playing different pitches by pressing different keys on the keyboard? you could use our previous examples, but writing a pitch to the device instead of e.g. incrementing a coordinate :)
or what about complementing our pong program from uxn tutorial day 6 with sound effects, having the device playing a note whenever there's a bounce of the ball?
or what if you use the screen vector to time the repetitive playing of a note? or what about you have it play a melody by following a sequence of notes? could this sequence come from a text file? :)
playback information
the audio device provides us with two ways of checking during runtime the state of the playback:
- the position short
- the output byte
when we read the position short, we get the current position of the "playhead" in the sample, starting from 0 (i.e. the playhead is at the beginning of the sample) and ending at the sample length minus one.
the output byte allows us to read the amplitude of the envelope. it returns 0 when the sample is not playing, so it can be used as a way of knowing that the playback has ended.
polyphony
the idea of having four audio devices is that we can have all of them playing at once, and each one can have a different sample, ADSR envelope, volume, and pitch.
this gives us many more possibilities:
maybe in a game there could be a melody playing in the background along with incidental sounds related to the gameplay?
maybe you can build a sequencer where you can control the four devices as different tracks?
or maybe you create a livecoding platform to have a dialog with each of the four instruments?
in any case, don't hesitate to share what you create! :)
the end
hey! believe it or not, this is the end!
you made it to the end of the tutorial series! congratulations!
i hope you enjoyed it and i hope you see it as just the start of your uxn journey!
we'd love to see what you create! don't hesitate to share it in mastodon, the forum, or even via e-mail!
but before doing all this, don't forget to take a break! :)
see you around!
support
if you enjoyed this tutorial and found it helpful, consider sharing it and giving it your support :)