💾 Archived View for compudanzas.net › uxn_tutorial_day_7.gmi captured on 2023-07-22 at 16:26:25. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-06-14)
-=-=-=-=-=-=-
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 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.
their ports are normally defined as follows:
|a0 @File0 [ &vector $2 &success $2 &stat $2 &delete $1 &append $1 &name $2 &length $2 &read $2 &write $2 ] |b0 @File1 [ &vector $2 &success $2 &stat $2 &delete $1 &append $1 &name $2 &length $2 &read $2 &write $2 ]
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!
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:
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 .File0/name DEO2 ( set address of file path ) #00ff .File0/length DEO2 ( will attempt to read 255 bytes ) ( set address for the data to read, and do read ) ;file/data .File0/read DEO2 ( check the success byte and jump accordingly ) .File0/success DEI2 #0000 EQU2 ,&failed JCN &success LIT "Y .Console/write DEO RTN &failed LIT "N .Console/write DEO RTN @file &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.
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!
in order to write a file, we need:
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 .File0/name DEO2 ( set file name ) #0006 .File0/length DEO2 ( will attempt to write 6 bytes ) ( set data starting address, and do write ) ;file/data .File0/write DEO2 ( read and evaluate success byte ) .File/success DEI2 #0006 NEQ2 ,&failed JCN &success LIT "Y .Console/write DEO RTN &failed LIT "N .Console/write DEO RTN @file &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/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.
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.
we could adapt our previous subroutine in order to load the theme file and apply its data as system colors:
@load-theme ( -- ) ;theme/name .File0/name DEO2 ( set address of file path ) #0006 .File0/length DEO2 ( will attempt to read 6 bytes ) ( set address for the data to read, and do read ) ;theme/data .File0/read DEO2 ( check the success byte and jump accordingly ) .File0/success DEI2 #0006 NEQ2 ,&failed JCN &success ( 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 RTN &failed RTN @theme &name ".theme 00 &data ( reserving 6 bytes for the data: ) &r $2 &g $2 &b $2
note how the &data and &r labels are pointing to the same location: it's not a problem! :)
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 DEI2 ;theme/r STA2 .System/g DEI2 ;theme/g STA2 .System/b DEI2 ;theme/b STA2 ;theme/name .File0/name DEO2 ( set address of file path ) #0006 .File0/length DEO2 ( will attempt to write 6 bytes ) ( set address for the data and do write ) ;theme/data .File0/write DEO2 ( check the success byte and jump accordingly ) .File0/success DEI2 #0006 NEQ2 ,&failed JCN &success ( report success? ) RTN &failed RTN
i invite you to compare these subroutines with the ones present in the 100r programs like nasu!
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:
|c0 @DateTime [ &year $2 &month $1 &day $1 &hour $1 &minute $1 &second $1 &dotw $1 &doty $2 &isdst $1 ]
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? :)
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.
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 assume that you might not be familiar with these concepts, so we'll briefly discuss them.
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:
@piano-pcm 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 @violin-pcm 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 @sin-pcm 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 @tri-pcm 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 @saw-pcm 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
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:
;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.
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.
in theory, it would appear that 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 ) |0100 ;saw-pcm .Audio0/addr DEO2 ( set sample address ) #0100 .Audio0/length DEO2 ( set sample length ) #3c .Audio0/pitch DEO ( set pitch as middle C ) BRK
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!
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!
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:
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:
#ffff .Audio0/adsr
ok, now we are ready to play the sound!
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 ) |0100 ;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 ) BRK
note (!) that it will play the sound only once, and it does it when the program starts.
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?
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:
%PLAY-NOTE { .Audio0/pitch DEO } ( pitch -- )
when a specific event happened, you could call it:
#3c PLAY-NOTE ( play middle C )
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.
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? :)
the audio device provides us with two ways of checking during runtime the state of the playback:
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.
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! :)
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!
if you enjoyed this tutorial and found it helpful, consider sharing it and giving it your support :)
text, images, and code are shared with the peer production license