The user's conversion, which I reverted while I was working out whether it was half done or broken. It was half done: an sbx application wants its #Include above #Program, because services.asm ends in a #Vectors block and a #Base written after that has no segment to be the base of. So the include moves up, the bases move a page with everything else, and the test starts it from the shell instead of booting it. It plays for 9,469,987 cycles, 9,423,527 of them waiting, which is the same 567 frames of music it played as a boot image. BEING A PROGRAM MEANS ITS VECTOR IS THE SYSTEM'S TO INSTALL. It brings the screen's, so that it has a beat to play to, and CosmOS puts it in when the tune starts and takes it back out when it stops - a thing a boot image never had to have right, and the second program here to exercise the version two format at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
285 lines
8.3 KiB
NASM
285 lines
8.3 KiB
NASM
; tune.asm
|
|
; Playing a melody, which needs a sound device and a clock and has neither by halves.
|
|
; Written by Anachronaut
|
|
;
|
|
; ---- Two devices, because one is not enough ----
|
|
;
|
|
; The sound device knows how to make a note and knows nothing about when. It has no timer and
|
|
; does not interrupt, so a program that only had the sound device could play a tune at
|
|
; whatever speed the machine happened to run at, which is not a tune.
|
|
;
|
|
; The screen finishes a frame sixty times a second and will say so. That is the only regular
|
|
; beat on this machine, and it is counted in the machine's own cycles, so this plays at the
|
|
; same speed whether the emulator is running at a megahertz or as fast as it can go. Every
|
|
; duration below is in frames: 30 is half a second.
|
|
;
|
|
; A programmable timer is the device that ought to be doing this, and it does not exist yet.
|
|
; Borrowing the screen's frame costs nothing and works, which is the whole reason to notice
|
|
; that a beat is a beat wherever it comes from.
|
|
;
|
|
; ---- What borrowing it costs ----
|
|
;
|
|
; The frame is not slow. It is FIXED, and that is the different complaint. Every duration here
|
|
; is a whole number of 16.67 ms, so a note worth a third of a beat cannot be written at all -
|
|
; and the way round it is to pick a tempo whose subdivisions happen to land on whole frames,
|
|
; which is making the tune fit the machine rather than the other way round. The theme below
|
|
; was written to the frame and is a few cents of tempo away from what it wants to be.
|
|
;
|
|
; So what the timer wants is an ARBITRARY tick rather than a faster fixed one, and the reason
|
|
; the screen should not be the clock is that a display refresh and a music routine have no
|
|
; reason to share a rate.
|
|
;
|
|
; ---- What a patch costs and what a note costs ----
|
|
;
|
|
; Setting the sound up is twenty-odd writes, done once before a single note is played. After
|
|
; that the inner loop is two: the note, and letting go of it. That split is what the selector
|
|
; and value registers are for - see Making A Noise in the Programming Manual.
|
|
|
|
#Include services.asm
|
|
|
|
#Program
|
|
|
|
#Base 0x5000
|
|
|
|
start:
|
|
|
|
; ---- The instrument ----
|
|
;
|
|
; Channel 0, selected once. Every parameter write below lands on it.
|
|
RSTA
|
|
OUTA 0x41
|
|
|
|
; A saw wave, which has all the harmonics and so is the one to hear a filter on. Writing a
|
|
; port leaves A alone, so the nothing that selected the channel also selects parameter 0,
|
|
; which is oscillator 0's waveform. SplitLint will point out any attempt to put it there
|
|
; twice.
|
|
OUTA 0x42
|
|
INIA 0d2 ; Saw
|
|
OUTA 0x43
|
|
|
|
INIA 0x01 ; Oscillator 0, gain
|
|
OUTA 0x42
|
|
INIA 0xFF ; All of it. A channel arrives at full gain already, so this is
|
|
OUTA 0x43 ; saying so rather than changing it.
|
|
|
|
; A second oscillator a little out of tune with the first, which is the oldest trick there
|
|
; is for making one voice sound like more than one.
|
|
;
|
|
; SWITCHING IT ON IS A SEPARATE WRITE from setting its gain, and it is the one that matters:
|
|
; the two oscillators are averaged rather than added, so `active` is structural. Setting a
|
|
; gain on an oscillator that is off does nothing at all, silently, which is how the first
|
|
; draft of this program came to have a detune in it that could not be heard.
|
|
INIA 0x15 ; Oscillator 1, on
|
|
OUTA 0x42
|
|
INIA 0x01
|
|
OUTA 0x43
|
|
INIA 0x11 ; Oscillator 1, gain
|
|
OUTA 0x42
|
|
INIA 0xC0
|
|
OUTA 0x43
|
|
INIA 0x13 ; Oscillator 1, detune
|
|
OUTA 0x42
|
|
INIA 0d129 ; Centred on 128, and a step is about nine cents, so this is
|
|
; nine cents sharp - a shimmer rather than a wrong note.
|
|
OUTA 0x43
|
|
|
|
; Plucked: no attack to speak of, most of a second of decay, and nothing held.
|
|
;
|
|
; A sustain of nothing does NOT end the note. It goes quiet and keeps sounding, because a
|
|
; voice holding at nothing is what a held key is. Dropping the gate is the only thing that
|
|
; ends a note, which is why the loop below does it whether the sound has faded or not.
|
|
INIA 0x20 ; Amplitude envelope, attack
|
|
OUTA 0x42
|
|
INIA 0d10
|
|
OUTA 0x43
|
|
INIA 0x21 ; Decay
|
|
OUTA 0x42
|
|
INIA 0d120
|
|
OUTA 0x43
|
|
INIA 0x22 ; Sustain: nothing
|
|
OUTA 0x42
|
|
RSTA
|
|
OUTA 0x43
|
|
INIA 0x23 ; Release
|
|
OUTA 0x42
|
|
INIA 0d40
|
|
OUTA 0x43
|
|
|
|
; A low pass with the modulation envelope opening it, so each note starts bright and closes
|
|
; down. This is what the second envelope is for, and it can only be spent this way because
|
|
; the level is shaped by the first one and not by whichever happens to be wired to the
|
|
; output.
|
|
INIA 0x40 ; Filter, on
|
|
OUTA 0x42
|
|
INIA 0x01
|
|
OUTA 0x43
|
|
INIA 0x42 ; Cutoff, low to start with
|
|
OUTA 0x42
|
|
INIA 0d90
|
|
OUTA 0x43
|
|
INIA 0x43 ; A little resonance, to hear it move
|
|
OUTA 0x42
|
|
INIA 0d150
|
|
OUTA 0x43
|
|
INIA 0x44 ; What opens it: the modulation envelope
|
|
OUTA 0x42
|
|
INIA 0d2
|
|
OUTA 0x43
|
|
INIA 0x45 ; And how far, upwards from centre
|
|
OUTA 0x42
|
|
INIA 0d220
|
|
OUTA 0x43
|
|
INIA 0x31 ; That envelope's decay, which is the sweep's length
|
|
OUTA 0x42
|
|
INIA 0d70
|
|
OUTA 0x43
|
|
INIA 0x32 ; and it closes all the way
|
|
OUTA 0x42
|
|
RSTA
|
|
OUTA 0x43
|
|
|
|
INIA 0xC0 ; The device's volume, with room left over the top
|
|
OUTA 0x46
|
|
|
|
; ---- The beat ----
|
|
;
|
|
; Ask the screen to interrupt at each frame, and let interrupts in. The screen does not do
|
|
; this unless it is asked.
|
|
INIA 0x01
|
|
OUTA 0x35
|
|
SIF
|
|
|
|
; ---- The tune ----
|
|
;
|
|
; Data Pointer 0 walks the table, and nothing in this loop is a CALL, so it stays where it
|
|
; was left without being saved anywhere.
|
|
SETD.0 Theme
|
|
|
|
nextNote:
|
|
LDA.0 ; The note. Zero is the end of the tune.
|
|
BRA finished
|
|
OUTA 0x44 ; Writing the note is what starts it.
|
|
INCD.0
|
|
LDB.0 ; How many frames it lasts.
|
|
INCD.0
|
|
|
|
holdNote:
|
|
WAIT ; Nothing at all until the screen says a frame has gone by.
|
|
DECB
|
|
BNB holdNote
|
|
|
|
; Let go. The note is already fading on its own decay, but dropping the gate is what a
|
|
; keyboard does and what the release time is waiting for.
|
|
RSTA
|
|
OUTA 0x45
|
|
BRI nextNote
|
|
|
|
finished:
|
|
; Let the last note ring out rather than cutting it off, then put the screen back the way it
|
|
; was found - and stop asking to be interrupted before taking away what catches it.
|
|
INIB 0d45
|
|
lastRing:
|
|
WAIT
|
|
DECB
|
|
BNB lastRing
|
|
|
|
CIF
|
|
RSTA
|
|
OUTA 0x35
|
|
SWI osExit
|
|
|
|
; Sixty times a second, and it has nothing to do. WAIT only needs something to have happened,
|
|
; and this is the something. A handler still has to exist: an interrupt with nothing installed
|
|
; to catch it is a fault.
|
|
frame:
|
|
RETI
|
|
|
|
#Data
|
|
|
|
#Base 0x3000
|
|
|
|
; ---- Notes and how long they last ----
|
|
;
|
|
; Pairs: a MIDI note, then a count of frames. 60 is middle C and every 12 is an octave. A zero
|
|
; note ends it, which is why there are no rests in here - a rest would want a duration with no
|
|
; note, and this table has no way to say that. Adding one is a byte of flag or a note number
|
|
; nothing plays, and this program did not need it.
|
|
Tune:
|
|
0d60 0d15 ; C
|
|
0d64 0d15 ; E
|
|
0d67 0d15 ; G
|
|
0d72 0d30 ; C, an octave up, held twice as long
|
|
0d71 0d15 ; B
|
|
0d67 0d15 ; G
|
|
0d64 0d15 ; E
|
|
0d60 0d45 ; and home
|
|
0x00
|
|
|
|
Theme:
|
|
0x30 0d15 ; C
|
|
0x35 0d15 ; F
|
|
0x3C 0d15 ; C+
|
|
0x40 0d30 ; E+
|
|
0x48 0d30 ; C++
|
|
0x45 0d30 ; A+
|
|
0x47 0d30 ; B+
|
|
0x43 0d45 ; G+
|
|
; repeat four times.
|
|
0x2A 0d7 ; Gs
|
|
0x33 0d7 ; Ef
|
|
0x3C 0d7 ; C+
|
|
;
|
|
0x2A 0d7 ; Gs
|
|
0x33 0d7 ; Ef
|
|
0x3C 0d7 ; C+
|
|
;
|
|
0x2A 0d7 ; Gs
|
|
0x33 0d7 ; Ef
|
|
0x3C 0d7 ; C+
|
|
;
|
|
0x2A 0d7 ; Gs
|
|
0x33 0d7 ; Ef
|
|
0x3C 0d7 ; C+
|
|
; next chord
|
|
0x2E 0d7 ; Bf
|
|
0x35 0d7 ; F
|
|
0x3E 0d7 ; D+
|
|
; next chord
|
|
0x2E 0d7 ; Bf
|
|
0x35 0d7 ; F
|
|
0x3E 0d7 ; D+
|
|
;
|
|
; next chord
|
|
0x2E 0d7 ; Bf
|
|
0x35 0d7 ; F
|
|
0x3E 0d7 ; D+
|
|
;
|
|
; next chord
|
|
0x2E 0d7 ; Bf
|
|
0x35 0d7 ; F
|
|
0x3E 0d7 ; D+
|
|
; Finally on the C major
|
|
0x30 0d7 ; C
|
|
0x37 0d7 ; G
|
|
0x40 0d7 ; E+
|
|
;
|
|
0x30 0d7 ; C
|
|
0x37 0d7 ; G
|
|
0x40 0d7 ; E+
|
|
;
|
|
0x30 0d7 ; C
|
|
0x37 0d7 ; G
|
|
0x40 0d7 ; E+
|
|
;
|
|
0x30 0d7 ; C
|
|
0x37 0d7 ; G
|
|
0x40 0d7 ; E+
|
|
;
|
|
0x18 0d60 ; C bass
|
|
0x00
|
|
|
|
#Vectors
|
|
|
|
Boot start
|
|
Device 0x30 frame
|