generator and source files for the site
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Warning: Missing License
We looked everywhere, but we couldn't find an OSI- or FSF-approved Free Software or Content License, which is a requirement for hosting content on Codeberg.
Learn about why no license or custom ones are a bad idea and get started on the docs.
Please consider carefully if you want to use (parts of) this project, as doing so might put you in legal trouble.

538 lines
21 KiB

# uxn tutorial: day 7, more devices
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: audio, file, and datetime.
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 device
the file device in the varvara computer allows us to read and write to external files.
its ports are normally defined as follows:
|a0 @File [ &vector $2 &success $2 &stat $2 &delete $1 &append $1 &name $2 &length $2 &load $2 &save $2 ]
* 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 load short is for the starting memory address where the read data should be stored
* the save short is for the starting memory address where the data to be written is stored
* the stat short is similar to load, 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 save append data to the end of the file, when it is the default, 00, save overwrites the contents from the start
a read operation is started when the load short is written to, and a write operation is started when the save short is written to.
these might seem like a lot of fields, but we'll see that they are not too much of a problem!
## reading a file
in order to read a file, we need to know the following:
* the path of the file, written as a string of text in program memory: terminated by a 00, and with an appropriate label - 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:
@load-file ( -- )
;file/name .File/name DEO2 ( set address of file path )
#00ff .File/length DEO2 ( will attempt to read 255 bytes )
( set address for the data to read, and do read )
;file/data .File/load DEO2
( check the success byte and jump accordingly )
.File/success DEI2 #0000 EQU2 ,&failed JCN
LIT 'Y .Console/write DEO
LIT 'N .Console/write DEO
&name "test.txt 00
&data $ff ( reserving 255 bytes for the data )
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.
int 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
* 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":
@save-file ( -- )
;file/name .File/name DEO2 ( set file name )
#0006 .File/length DEO2 ( will attempt to write 6 bytes )
( set data starting address, and do write )
;file/data .File/save DEO2
( read and evaluate success byte )
.File/success DEI2 #0006 NEQ2 ,&failed JCN
LIT 'Y .Console/write DEO
LIT 'N .Console/write DEO
&name "test.txt 00
&data "hello 0a
note how similar it is to the load-file subroutine!
the only differences, beside the use of File/save instead of File/load, 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.
this file has the name ".theme" and can be written to a local directory from nasu, using the ctrl+p shortcut.
=> uxn themes
### reading the theme file
we could adapt our previous subroutine in order to load the theme file and apply its data as system colors:
@load-theme ( -- )
;theme/name .File/name DEO2 ( set address of file path )
#0006 .File/length DEO2 ( will attempt to read 6 bytes )
( set address for the data to read, and do read )
;theme/data .File/load DEO2
( check the success byte and jump accordingly )
.File/success DEI2 #0006 NEQ2 ,&failed JCN
( set the system colors from the read data )
;theme/r LDA2 .System/r DEO2
;theme/g LDA2 .System/g DEO2
;theme/b LDA2 .System/b DEO2
&name ".theme 00
&data ( reserving 6 bytes for the data: )
&r $2 &g $2 &b $2
### 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:
@save-theme ( -- )
( read system colors into program memory )
.System/r DEO2 ;theme/r STA2
.System/g DEO2 ;theme/g STA2
.System/b DEO2 ;theme/b STA2
;theme/name .File/name DEO2 ( set address of file path )
#0006 .File/length DEO2 ( will attempt to write 6 bytes )
( set address for the data and do write )
;theme/data .File/save DEO2
( check the success byte and jump accordingly )
.File/success DEI2 #0006 NEQ2 ,&failed JCN
( report success? )
i invite you to compare these subroutines with the ones present in the 100r programs like nasu!
=> nasu source code
# 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:
|b0 @DateTime [ &year $2 &month $1 &day $1 &hour $1 &minute $1 &second $1 &dotw $1 &doty $2 &isdst $1 ]
* 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:
.DateTime/hour DEI
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? :)
remember that for timing events with a little bit more precison, 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:
|30 @Audio0 [ &vector $2 &position $2 &output $1 &pad $3 &adsr $2 &length $2 &addr $2 &volume $1 &pitch $1 ]
|40 @Audio1 [ &vector $2 &position $2 &output $1 &pad $3 &adsr $2 &length $2 &addr $2 &volume $1 &pitch $1 ]
|50 @Audio2 [ &vector $2 &position $2 &output $1 &pad $3 &adsr $2 &length $2 &addr $2 &volume $1 &pitch $1 ]
|60 @Audio3 [ &vector $2 &position $2 &output $1 &pad $3 &adsr $2 &length $2 &addr $2 &volume $1 &pitch $1 ]
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 briefly discuss these concepts in order to use them, assuming that you might not be familiar with 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 specific length, 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:
8182 8588 8d91 959b a1a6 aaad b2b5 b8bd
c1c7 cbd0 d5d9 dde1 e5e5 e4e4 e1dc d7d1
cbc5 bfb8 b2ac a6a2 9c97 928d 8884 807c
7977 7574 7372 7272 7273 7372 706d 6964
605b 5650 4d49 4643 4342 4244 4548 4a4d
5052 5556 5758 5554 5150 4c4a 4744 423f
3d3c 3a38 3835 3431 3030 2f31 3336 393e
4449 4e54 5a60 666b 7175 7b82 8990 989e
a6ab b1b6 babd bebf bfbe bbb9 b6b3 b0ae
aaa8 a6a3 a19e 9c9a 9997 9696 9798 9b9e
a1a4 a6a9 a9ac adad adae aeaf b0b0 b1b1
b3b3 b4b4 b4b3 b3b1 b0ad abab a9a9 a8a8
a7a5 a19d 9891 8b84 7e77 726e 6b6b 6b6c
6f71 7477 7776 7370 6c65 5e56 4e48 423f
3d3c 3b3a 3a39 3838 3839 393a 3c3e 4146
4a50 575b 6064 686a 6e70 7274 7677 7a7d
8186 8d94 9ba0 a3a7 acb1 b5bc c2c7 cacc
cecf d0d1 d3d5 d8db dee1 e3e5 e6e5 e5e3
dfdc d7d0 c8c2 bbb2 a99f 968c 847c 746e
675f 5851 4b43 3e3a 3533 312e 2c2b 2826
2422 2122 2327 2d34 3c44 4c57 5f68 7075
7b80 8487 8789 8a8c 8d90 9397 999c 9ea0
a2a2 a2a0 9c97 9491 8f8e 908f 918f 8e88
827a 726a 6058 5047 423f 3f40 4245 4748
4949 4746 4545 4a4f 5863 717f 8b9a a6b1
b8be c1c1 bfbd bab5 b1af acac aeb1 b7bc
c2c9 cfd3 d5d4 d3d3 d1ce cbc6 c0ba b3ab
a39a 8f85 7b72 6c67 6462 605f 5e5d 5b58
5550 4d49 4848 4949 4a4d 5052 5558 5b5e
6164 686c 7074 7677 7979 7a7b 7b7a 7977
7473 6f6e 6b69 696b 6f72 7576 7574 716b
655d 554e 4742 3f3f 4045 4b52 5a62 6b74
8083 8689 8c8f 9295 989b 9ea1 a4a7 aaad
b0b3 b6b9 bbbe c1c3 c6c9 cbce d0d2 d5d7
d9db dee0 e2e4 e6e7 e9eb ecee f0f1 f2f4
f5f6 f7f8 f9fa fbfb fcfd fdfe fefe fefe
fffe fefe fefe fdfd fcfb fbfa f9f8 f7f6
f5f4 f2f1 f0ee eceb e9e7 e6e4 e2e0 dedb
d9d7 d5d2 d0ce cbc9 c6c3 c1be bbb9 b6b3
b0ad aaa7 a4a1 9e9b 9895 928f 8c89 8683
807d 7a77 7471 6e6b 6865 625f 5c59 5653
504d 4a47 4542 3f3d 3a37 3532 302e 2b29
2725 2220 1e1c 1a19 1715 1412 100f 0e0c
0b0a 0908 0706 0505 0403 0302 0202 0202
0102 0202 0202 0303 0405 0506 0708 090a
0b0c 0e0f 1012 1415 1719 1a1c 1e20 2225
2729 2b2e 3032 3537 3a3d 3f42 4547 4a4d
5053 5659 5c5f 6265 686b 6e71 7477 7a7d
8082 8486 888a 8c8e 9092 9496 989a 9c9e
a0a2 a4a6 a8aa acae b0b2 b4b6 b8ba bcbe
c0c2 c4c6 c8ca ccce d0d2 d4d6 d8da dcde
e0e2 e4e6 e8ea ecee f0f2 f4f6 f8fa fcfe
fffd fbf9 f7f5 f3f1 efed ebe9 e7e5 e3e1
dfdd dbd9 d7d5 d3d1 cfcd cbc9 c7c5 c3c1
bfbd bbb9 b7b5 b3b1 afad aba9 a7a5 a3a1
9f9d 9b99 9795 9391 8f8d 8b89 8785 8381
7f7d 7b79 7775 7371 6f6d 6b69 6765 6361
5f5d 5b59 5755 5351 4f4d 4b49 4745 4341
3f3d 3b39 3735 3331 2f2d 2b29 2725 2321
1f1d 1b19 1715 1311 0f0d 0b09 0705 0301
0103 0507 090b 0d0f 1113 1517 191b 1d1f
2123 2527 292b 2d2f 3133 3537 393b 3d3f
4143 4547 494b 4d4f 5153 5557 595b 5d5f
6163 6567 696b 6d6f 7173 7577 797b 7d7f
8282 8183 8384 8685 8888 8889 8a8b 8c8c
8e8e 8f90 9092 9193 9494 9596 9699 9899
9b9a 9c9c 9c9d 9ea0 a1a0 a2a2 a3a5 a4a6
a7a7 a9a8 a9aa aaac adad aeae b0b0 b1b3
b2b4 b5b5 b6b7 b9b8 b9bb babc bdbc bdbe
bfc1 bfc1 c3c1 c4c5 c5c6 c6c7 c9c7 cbca
cbcc cdcd cfcf d2d0 d2d2 d2d5 d4d5 d6d7
d8d8 d9dc d9df dadf dce1 dde5 dce6 dceb
cb1f 1b1e 1c21 1c21 1f23 2025 2127 2329
2529 2829 2a2b 2b2e 2d2f 302f 3231 3234
3334 3536 3836 3939 3a3b 3b3d 3e3d 3f40
4042 4242 4444 4646 4748 474a 4a4b 4d4c
4e4e 4f50 5052 5252 5554 5557 5759 5959
5b5b 5c5d 5d5f 5e60 6160 6264 6365 6566
6867 6969 6a6c 6c6d 6d6e 706f 7071 7174
7475 7576 7777 797a 7a7c 7b7c 7e7d 7f7f
=> piano.tal source code
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 them we get a tone based on that shape!
=> ./img/screenshot_uxn-waveform_piano.png piano sample waveform
=> ./img/screenshot_uxn-waveform_violin.png violin sample waveform
=> ./img/screenshot_uxn-waveform_sin.png sin sample waveform
=> ./img/screenshot_uxn-waveform_tri.png tri sample waveform
=> ./img/screenshot_uxn-waveform_saw.png saw sample waveform
similar to how we have dealt with sprites and with the file device above, in order to set a sample in the audio device, we just have to write its address and its length:
;saw-pcm .Audio0/addr DEO2 ( set sample address )
#0100 .Audio0/length DEO2 ( set sample 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), and 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 midi note bits, it's a good idea to have a midi table around to see the hexadecimal values corresponding to different notes.
=> midi table
middle C (C4, or 3c in midi) is assumed to be the default pitch of the samples.
### a "sample" program
in theory, the following program should play our sample at that frequency, or not?
( hello-sound.tal )
( devices )
|30 @Audio0 [ &vector $2 &position $2 &output $1 &pad $3 &adsr $2 &length $2 &addr $2 &volume $1 &pitch $1 ]
( main program )
;saw-pcm .Audio0/addr DEO2 ( set sample address )
#0100 .Audio0/length DEO2 ( set sample length )
#3c .Audio0/pitch DEO ( set pitch as middle C )
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:
#ff .Audio0/volume DEO ( set maximum volume in left and right )
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 second, in decimal).
the following will set the maximum duration on each of the components, making the sound last 4 seconds:
#ffff .Audio0/adsr
ok, so 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!
( hello-sound.tal )
( devices )
|30 @Audio0 [ &vector $2 &position $2 &output $1 &pad $3 &adsr $2 &length $2 &addr $2 &volume $1 &pitch $1 ]
( main program )
;saw-pcm .Audio0/addr DEO2 ( set sample address )
#0100 .Audio0/length DEO2 ( set sample length )
#ffff .Audio0/adsr DEO2 ( set envelope )
#ff .Audio0/volume DEO ( set maximum volume )
#3c .Audio0/pitch DEO ( set pitch as middle C )
note (!) that it will play the sound only once, and it does it when the program starts.
i invite you to experiment with 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 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.
keep in mind that every time you write a pitch, the playback of the sample and the shape of the envelope starts over.
### 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 play 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 in the irc channel (or even via e-mail!)
=> uxn in lines forum
=> ./contact.gmi {contact}
irc channel: #uxn on
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} :)