Add /deps.sh, in /linapple remove full-screen mode to make it run properly on recent...
[applesoft_basic.git] / apple_io.py
1 import alsaaudio
2 import atexit
3 import fcntl
4 import math
5 import os
6 import select
7 import struct
8 import sys
9 import termios
10 import time
11 import tty
12
13 PCM_RATE = 44100
14 PCM_PERIODSIZE = 441
15
16 # target will yield this much time whenever it polls HW_IOADR
17 POLL_TIMEOUT_MS = 1
18
19 INVFLG_NORMAL = 0xff
20 INVFLG_INVERSE = 0x3f
21 INVFLG_FLASH = 0x7f
22
23 # see https://www.tinaja.com/ebooks/tearing_rework.pdf
24 ZP_WNDLFT = 0x20 # Left side of scroll window
25 ZP_WNDWTH = 0x21 # Width of scroll window
26 ZP_WNDTOP = 0x22 # Top of scroll window
27 ZP_WNDBTM = 0x23 # Bottom of scroll window
28 ZP_CH = 0x24 # Cursor horizontal position
29 ZP_CV = 0x25 # Cursor vertical position
30 ZP_GBASL = 0x21 # LORES graphics base low
31 ZP_GBASH = 0x27 # LORES graphics base high
32 ZP_BASL = 0x28 # TEXT base address low
33 ZP_BASH = 0x29 # TEXT base address high
34 ZP_BAS2L = 0x2A # Scroll temporary base low
35 ZP_BAS2H = 0x2B # Scroll temporary base high
36 ZP_COLOR = 0x30 # Holds the LORES color value
37 ZP_INVFLG = 0x32 # Normal/Inverse/Flash mask
38 ZP_PROMPT = 0x33 # Holds prompt symbol
39 ZP_YSAV = 0x34 # Temporary Y register hold
40 ZP_CSWL = 0x36 # Output character hook low
41 ZP_CSWH = 0x37 # Output character hook high
42 ZP_KSWL = 0x38 # Input character hook low
43 ZP_KSWH = 0x39 # Input character hook high
44 ZP_ACC = 0x45 # Accumulator save
45 ZP_XREG = 0x46 # X register save
46 ZP_YREG = 0x47 # Y register save
47 ZP_STATUS = 0x48 # Flag register save
48 ZP_SPNT = 0x49 # Stack pointer save
49 ZP_RNDL = 0x4E # Keybounce random number low
50 ZP_RNDH = 0x4F # Keybounce random number high
51 ZP_LOMEM = 0x69 # current lomem (word variable)
52 ZP_HIMEM = 0x73 # current himem (word variable)
53 VP_REENTER = 0x03D0 # Re-enter DOS
54 VP_RECONNECT = 0x03EA # Reconnect DOS I/O hooks
55 VP_BRKV = 0x03F0 # Break vector address
56 VP_SOFTEV = 0x03F2 # Warm start vector address
57 VP_PWRDUP = 0x03F4 # Warm start EOR A5 checksum
58 VP_AMPERV = 0x03F6 # Applesoft "&" Jump Code
59 VP_USRADR = 0x03F8 # Control Y vector Jump Code
60 VP_NMI = 0x03FB # NMI vector Jump Code
61 VP_IRQLOC = 0x03FE # Interrupt vector address
62 HW_IOADR = 0xC000 # Keyboard input location
63 HW_KBDSTRB = 0xC010 # Keyboard strobe reset
64 HW_TAPEOUT = 0xC020 # Cassette data output
65 HW_SPKR = 0xC030 # Speaker click output
66 HW_STROBE = 0xC040 # Game I/O connector strobe
67 HW_TXTCLR = 0xC050 # Graphics ON soft switch
68 HW_TXTSET = 0xC051 # Text ON soft switch
69 HW_MIXCLR = 0xC052 # Full screen ON soft switch
70 HW_MIXSET = 0xC053 # Split screen ON soft switch
71 HW_LOWSCR = 0xC054 # Page ONE display soft switch
72 HW_HISCR = 0xC055 # Page TWO display soft switch
73 HW_LORES = 0xC056 # LORES ON soft switch
74 HW_HIRES = 0xC057 # HIRES ON soft switch
75 HW_AN0CLR = 0xC058 # Annunciator 0 OFF soft switch
76 HW_AN0SET = 0xC059 # Annunciator 0 ON soft switch
77 HW_AN1CLR = 0xC05A # Annunciator 1 OFF soft switch
78 HW_AN1SET = 0xC05B # Annunciator 1 ON soft switch
79 HW_AN2CLR = 0xC05C # Annunciator 2 OFF soft switch
80 HW_AN2SET = 0xC05D # Annunciator 2 ON soft switch
81 HW_AN3CLR = 0xC05E # Annunciator 3 OFF soft switch
82 HW_AN3SET = 0xC05F # Annunciator 3 ON soft switch
83 HW_TAPEIN = 0xC060 # Cassette tape read input
84 HW_PB0 = 0xC061 # Push button 0 input
85 HW_PB1 = 0xC062 # Push button 1 input
86 HW_PB2 = 0xC063 # Push button 2 input
87 HW_PDL0 = 0xC064 # Game Paddle 0 analog input
88 HW_PDL1 = 0xC065 # Game Paddle 1 analog input
89 HW_PDL2 = 0xC066 # Game Paddle 2 analog input
90 HW_PDL3 = 0xC067 # Game Paddle 3 analog input
91 ROM_PLOT = 0xF800 # Plot a block on LORES screen
92 ROM_HLINE = 0xF819 # Draw a horizontal LORES line
93 ROM_VLINE = 0xF828 # Draw a vertical LORES line
94 ROM_CLRSCR = 0xF832 # Clear full LORES screen
95 ROM_CLRTOP = 0xF836 # Clear top of LORES screen
96 ROM_GBASCALC = 0xF847 # Calculate LORES base address
97 ROM_NEXTCOL = 0xF85F # Increase LORES color by three
98 ROM_SETCOL = 0xF864 # Set color for LORES plotting
99 ROM_SCRN = 0xF871 # Read color of LORES screen
100 ROM_PRNTAX = 0xF941 # Output A then X as hex
101 ROM_PRBLNK = 0xF948 # Output three spaces via hooks
102 ROM_PRBL2 = 0xF94A # Output X spaces via hooks
103 ROM_STEP = 0xFA43 # Single step (old ROM only!)
104 ROM_REGDSP = 0xFAD7 # Display working registers
105 ROM_PREAD = 0xFB1E # Read a game paddle
106 ROM_INIT = 0xFB2F # Initialize text screen
107 ROM_SETTXT = 0xFB39 # Set up text screen
108 ROM_SETGR = 0xFB40 # Setup LORES screen
109 ROM_SETWND = 0xFB4B # Set text window to normal
110 ROM_BASCALC = 0xFBC1 # Calculate text base address
111 ROM_BELL = 0xFBD9 # 1 Beep speaker if ctrl G
112 ROM_BELL2 = 0xFBE4 # Beep speaker once
113 ROM_ADVANCE = 0xFBF4 # Move text cursor right by one
114 ROM_VIDOUT = 0xFBFD # Output ASCII to screen only
115 ROM_BS = 0xFC10 # Backspace screen
116 ROM_UP = 0xFC1A # Move screen cursor up one
117 ROM_VTAB = 0xFC22 # Vertical screen tab using CV
118 ROM_VTABZ = 0xFC24 # Vertical screen tab using A
119 ROM_ESC1 = 0xFC2C # Process escape movements A-G
120 ROM_CLREOP = 0xFC42 # Clear text to end of screen
121 ROM_HOME = 0xFC58 # Clear screen and home cursor
122 ROM_CR = 0xFC62 # Carriage return to screen
123 ROM_LF = 0xFC66 # Line feed to screen onIy
124 ROM_SCROLL = 0xFC70 # Scroll text screen up one
125 ROM_CLEOL = 0xFC9C # Clear text to end of line
126 ROM_WAIT = 0xFCA8 # Time delay set by accumulator
127 ROM_RDKEY = 0xFD0C # Get input character via hooks
128 ROM_KEYIN = 0xFD1B # Read the Apple keyboard
129 ROM_RDCHAR = 0xFD35 # Get key and process ESC A-F
130 ROM_CANCEL = 0xFD62 # Cancel keyboard line entry
131 ROM_GETLNZ = 0xFD67 # CR, then get kbrl input line
132 ROM_GETLN = 0xFD6A # Get input line from keyboard
133 ROM_GETLN1 = 0xFD6F # Get kbd input, no prompt
134 ROM_CROUT1 = 0xFD8B # Clear EOL then CR via hooks
135 ROM_CROUT = 0xFD8E # Output return via hooks
136 ROM_PRBYTE = 0xFDDA # Output full A in hpxto hooks
137 ROM_PRHEX = 0xFDE3 # Output low A in hex to hooks
138 ROM_COUT = 0xFDED # Output character via hooks
139 ROM_COUT1 = 0xFDF0 # Output character to screen
140 ROM_MOVE = 0xFE2C # Move block of memory
141 ROM_VERIFY = 0xFE36 # Verify block of memory
142 ROM_LIST = 0xFE5E # Disassemble 20 instructions
143 ROM_L1ST2 = 0xFE63 # Disassemble A instructions
144 ROM_SETINV = 0xFE80 # Print inverse text on screen
145 ROM_SETNORM = 0xFE84 # Print normal text on screen
146 ROM_SETVID = 0xFE93 # Grab output hooks for screen
147 ROM_XBASIC = 0xFEB0 # Goto BASIC, destroying old
148 ROM_BASCON = 0xFEB3 # Goto BASIC continuing old
149 ROM_TRACE = 0xFEC2 # Start tracing (old ROM only!)
150 ROM_WRITE = 0xFECD # Save to cassette tape
151 ROM_READ = 0xFEFD # Read from cassette tape
152 ROM_PRERR = 0xFF2D # Print "ERR" to output hook
153 ROM_BELL = 0xFF3A # Output bell via hooks
154 ROM_IORESR = 0xFF3F # Restore all working register
155 ROM_IOSAVE = 0xFF4A # Save all working registers
156 ROM_OLDRST = 0xFF59 # Old reset entry, no autostart
157 ROM_MON = 0xFF65 # Enter monitor and beep spkr
158
159 # see https://imgur.com/h0NNOc3
160 # colors (map 15 apple colors to 16 VT100 colours, some duplicate or unused)
161 colors = [
162   0x0, # black -> black
163   0x1, # red -> red
164   0x4, # d.blue -> blue
165   0x5, # purple -> magenta
166   0x2, # d.green -> green
167   0x7, # gray 1 -> white
168   0xc, # m.blue -> intense blue
169   0xd, # l.blue -> intense magenta
170   0x3, # brown -> yellow
171   0xb, # orange -> intense yellow
172   0x7, # grey 2 -> white
173   0xd, # pink -> intense magenta
174   0xa, # l.green -> intense green
175   0xb, # yellow -> intense yellow
176   0xe, # aqua -> intense cyan
177   0xf, # white -> intense white
178 ]
179
180 # see /ribbit/ribbit.bas
181 # hrcg is loaded at DOS addr - $801 = $9600 - $801 = $8dff
182 RBOOT_ENTRY = 0x208
183 HRCG_START = 0x8dff # initialization with banner
184 HRCG_ENTRY = HRCG_START + 3 # initialization without banner
185
186 # see /lemonade/lemonade_tone.lst
187 LEMONADE_TONE_PERIOD = 0x300
188 LEMONADE_TONE_DUR = 0x301
189 LEMONADE_TONE_START = 0x302
190 LEMONADE_TONE_END = 0x317
191 LEMONADE_TONE_DATA = [
192   0xAD, 0x30, 0xC0,
193   0x88,
194   0xD0, 0x05,
195   0xCE, 0x01, 0x03,
196   0xF0, 0x09,
197   0xCA,
198   0xD0, 0xF5,
199   0xAE, 0x00, 0x03,
200   0x4C, 0x02, 0x03,
201   0x60,
202 ]
203
204 # see /ribbit/ribbit_tone.lst
205 # it is the same as lemonade_tone except for some context save/restore
206 LEMONADE_TONE_END2 = 0x31d
207 LEMONADE_TONE_DATA2 = [
208   0x20, 0x4A, 0xFF,
209   0xAD, 0x30, 0xC0,
210   0x88,
211   0xD0, 0x05,
212   0xCE, 0x01, 0x03,
213   0xF0, 0x09,
214   0xCA,
215   0xD0, 0xF5,
216   0xAE, 0x00, 0x03,
217   0x4C, 0x05, 0x03,
218   0x20, 0x3F, 0xFF,
219   0x60,
220 ]
221
222 # see /little_brick_out/little_brick_out_tone.lst
223 LITTLE_BRICK_OUT_TONE_PERIOD = 6
224 LITTLE_BRICK_OUT_TONE_DUR = 7
225 LITTLE_BRICK_OUT_TONE_START = 0x300
226 LITTLE_BRICK_OUT_TONE_END = 0x313
227 LITTLE_BRICK_OUT_TONE_DATA = [
228   0xAD, 0x30, 0xC0,
229   0x88,
230   0xD0, 0x04,
231   0xC6, 0x07,
232   0xF0, 0x08,
233   0xCA,
234   0xD0, 0xF6,
235   0xA6, 0x06,
236   0x4C, 0x00, 0x03,
237   0x60,
238 ]
239
240 # see /util/tone.lst
241 TONE_REST = 0x300
242 TONE_TONE = 0x308
243 TONE_DURL = 0x309
244 TONE_DURH = 0x30b
245 TONE_FREQL = 0x30d
246 TONE_FREQH = 0x314
247
248 # see /lemonade/lemonade_flash_patched.lst
249 LEMONADE_FLASH_INIT = 0x3600
250 LEMONADE_FLASH_INIT_PATCHED = 0x95ef
251 LEMONADE_FLASH_EXECUTE = 0x3603
252 LEMONADE_FLASH_EXECUTE_PATCHED = 0x9586
253 lemonade_flash_color0 = 0
254
255 # command line
256 BEEP_STYLE_ALSA = 0
257 BEEP_STYLE_VT100 = 1
258
259 beep_style = BEEP_STYLE_ALSA
260
261 # global state
262 termios_attr = None
263 fd_in = sys.stdin.fileno()
264 fd_out = sys.stdout.fileno()
265 ch_in = ''
266 poll_in = select.poll()
267 poll_in.register(fd_in, select.POLLIN)
268
269 # following values were taken from a 48K Apple after normal boot and NEW
270 low_mem = [0] * 0x800
271 #low_mem[ZP_WNDLFT] = 0
272 low_mem[ZP_WNDWTH] = 40
273 #low_mem[ZP_WNDTOP] = 0
274 low_mem[ZP_WNDBTM] = 24
275 #low_mem[ZP_CH] = 0
276 low_mem[ZP_CV] = 23 # likely value (depends on previous terminal activity)
277 #low_mem[ZP_COLOR] = 0
278 low_mem[ZP_INVFLG] = INVFLG_NORMAL
279 low_mem[ZP_LOMEM] = 4
280 low_mem[ZP_LOMEM + 1] = 8
281 #low_mem[ZP_HIMEM] = 0
282 low_mem[ZP_HIMEM + 1] = 150
283
284 mem = {
285   HW_IOADR: 0,
286   HW_PB0: 0,
287   HW_PB1: 0,
288   HW_PB2: 0,
289 }
290 pdl_value = [255 for i in range(4)]
291 current_gr = False
292 current_speed = 255
293 hrcg = False
294
295 COUT_STATE_NORMAL = 0
296 COUT_STATE_CTRL_A = 1
297 cout_state = COUT_STATE_NORMAL
298
299 # see https://github.com/python/cpython/blob/3.10/Lib/tty.py
300 TERMIOS_IFLAG = 0
301 TERMIOS_OFLAG = 1
302 TERMIOS_CFLAG = 2
303 TERMIOS_LFLAG = 3
304 TERMIOS_ISPEED = 4
305 TERMIOS_OSPEED = 5
306 TERMIOS_CC = 6
307
308 def init():
309   global termios_attr, pcm
310
311   if termios_attr is None and os.isatty(fd_in):
312     termios_attr = termios.tcgetattr(fd_in)
313     atexit.register(deinit)
314
315     # deep copy the termios structure
316     attr = list(termios_attr)
317     attr[TERMIOS_CC] = list(attr[TERMIOS_CC])
318
319     # see cfmakeraw() in termios man page
320     attr[TERMIOS_IFLAG] &= ~(
321       termios.IGNBRK |
322         termios.BRKINT |
323         termios.PARMRK |
324         termios.ISTRIP |
325         termios.INLCR |
326         termios.IGNCR |
327         termios.ICRNL |
328         termios.IXON
329     )
330     attr[TERMIOS_OFLAG] &= ~termios.OPOST
331     attr[TERMIOS_LFLAG] &= ~(
332       termios.ECHO |
333         termios.ECHONL |
334         termios.ICANON |
335         #termios.ISIG |
336         termios.IEXTEN
337     )
338     attr[TERMIOS_CFLAG] &= ~(termios.CSIZE | termios.PARENB);
339     attr[TERMIOS_CFLAG] |= termios.CS8;
340     # now customize
341     attr[TERMIOS_LFLAG] |= termios.ISIG
342     attr[TERMIOS_CC][termios.VINTR] = 3 # ctrl-c
343     termios.tcsetattr(fd_in, termios.TCSAFLUSH, attr)
344
345     # auto wrap off, hide cursor, reset attributes
346     write('\x1b[?7l\x1b[?25l\x1b[0m')
347
348 def deinit():
349   global termios_attr
350
351   if termios_attr is not None:
352     # reset to initial state, auto wrap on, show cursor, reset attributes
353     # note: reset to initial state clears the screen which I do not like,
354     # but it may be the only way to clear our change to the beep settings
355     #write('\x1bc\x1b[?7h\x1b[?25h\x1b[0m')
356
357     # without full reset: reset scrolling region (or user must run "reset") 
358     s = struct.pack('HHHH', 0, 0, 0, 0)
359     t = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, s)
360     h, w, _, _ = struct.unpack('HHHH', t)
361     set_scrolling_region(0, h)
362
363     # auto wrap on, show cursor, reset attributes
364     write('\x1b[?7h\x1b[?25h\x1b[0m')
365
366     termios.tcsetattr(fd_in, termios.TCSADRAIN, termios_attr)
367     atexit.unregister(deinit)
368     termios_attr = None
369
370 def pr_hash(n):
371   pass
372
373 def in_hash(n):
374   pass
375
376 # don't use Python buffered I/O facility for stdin, because we don't know of
377 # a documented way to check the input buffer level, and without this we can't
378 # meaningfully use poll() to check whether an input character is available
379 def read(n):
380   out = []
381   while len(out) < n:
382     # decode utf-8 character
383     _in = os.read(fd_in, 1)
384     if _in[0] & 0xe0 == 0xc0:
385       _in += os.read(fd__in, 1)
386     elif _in[0] & 0xf0 == 0xe0:
387       _in += os.read(fd_in, 2)
388     elif _in[0] & 0xf8 == 0xf0:
389       _in += os.read(fd_in, 3)
390     out.append(str(_in, 'utf-8'))
391   return ''.join(out)
392
393 # don't use Python buffered I/O facility for stdout, because applesoft code
394 # uses plot/print for animation, so we'd need to flush it every time anyway
395 def write(data):
396   os.write(fd_out, bytes(data, 'utf-8'))
397
398 def crlf():
399   if not hrcg:
400     write(
401       '\r\n' + (f'\x1b[{low_mem[ZP_WNDLFT]:d}C' if low_mem[ZP_WNDLFT] else '')
402     )
403   low_mem[ZP_CV] += 1
404   if low_mem[ZP_CV] >= low_mem[ZP_WNDBTM]:
405     low_mem[ZP_CV] = low_mem[ZP_WNDBTM] - 1
406   low_mem[ZP_CH] = low_mem[ZP_WNDLFT]
407
408 def _print(data):
409   global cout_state
410
411   for ch in data:
412     if cout_state == COUT_STATE_NORMAL:
413       if ch == '\a':
414         tone(1000., .1) # 1 kHz for .1 sec
415       elif ch == '\b':
416         if low_mem[ZP_CH] > low_mem[ZP_WNDLFT]:
417           write('\b')
418           low_mem[ZP_CH] -= 1
419       elif ch == '\n':
420         write('\n')
421         low_mem[ZP_CV] += 1
422         if low_mem[ZP_CV] >= low_mem[ZP_WNDBTM]:
423           low_mem[ZP_CV] = low_mem[ZP_WNDBTM] - 1
424       elif ch == '\r':
425         # apple treats \r as \r\n, so if you write e.g. \r\n you'll get \r\n\n
426         if hrcg:
427           write('\r')
428         crlf()
429       elif ord(ch) >= 0x20:
430         # some applications expect BASL, BASH = base address of current line
431         addr = bascalc(low_mem[ZP_CV])
432         low_mem[ZP_BASL] = addr & 0xff
433         low_mem[ZP_BASH] = addr >> 8
434         i = ord(ch)
435         data = (
436           (i & 0x3f) ^ 0x60
437         if invflg == INVFLG_FLASH else
438           (i & 0x3f) ^ 0x20
439         if invflg == INVFLG_INVERSE else
440           i | 0x80
441         )
442         low_mem[addr + low_mem[ZP_CH]] = data
443
444         if current_gr and low_mem[ZP_CV] < 20:
445           write(gr_encode([data])[0] + invflg())
446         else:
447           write(ch)
448         low_mem[ZP_CH] += 1
449         if low_mem[ZP_CH] >= low_mem[ZP_WNDLFT] + low_mem[ZP_WNDWTH]:
450           crlf()
451       elif hrcg:
452         write(ch)
453         if ord(ch) == 1: # ctrl-a
454           cout_state = COUT_STATE_CTRL_A
455     elif cout_state == COUT_STATE_CTRL_A:
456       # doesn't advance cursor
457       write(ch)
458       COUT_STATE = COUT_STATE_NORMAL
459     else:
460       assert False
461
462     if current_speed < 255:
463       time.sleep(5.12 / (current_speed + 1))
464
465 def get_internal():
466   global ch_in
467
468   ch_in = read(1)
469   if len(ch_in) == 0:
470     raise Exception('end of input') # due to piping or input redirection
471   if ch_in == '\x7f':
472     ch_in = '\b'
473   mem[HW_IOADR] = (ord(ch_in) & 0x7f) | 0x80
474   return
475
476 def get():
477   global ch_in
478
479   if len(ch_in) == 0:
480     write('\x1b[?25h') # show cursor
481     get_internal()
482     write('\x1b[?25l') # hide cursor
483   ch = ch_in
484   ch_in = ''
485   mem[HW_IOADR] &= 0x7f
486   return ch
487
488 def input():
489   global ch_in
490
491   write('\x1b[?25h') # show cursor
492   line = ''
493   while True:
494     # taken from get(), but doesn't do cursor
495     if len(ch_in) == 0:
496       get_internal()
497     ch = ch_in
498     ch_in = ''
499     mem[HW_IOADR] &= 0x7f
500
501     if ch == '\b':
502       if len(line):
503         _print('\b')
504         line = line[:-1]
505       elif low_mem[ZP_CH] > low_mem[ZP_WNDLFT]:
506         _print('\r')
507     else:
508       _print(ch)
509       if ch == '\r':
510         break
511       if len(line) >= 247:
512         _print('\a')
513         if len(line) >= 255:
514           _print('\r')
515           line = ''
516           continue
517       line += ch
518   write('\x1b[?25l') # hide cursor
519   return line
520
521 def htab(x):
522   low_mem[ZP_CH] = (x - 1) & 0xff
523   write(ch())
524
525 def vtab(y):
526   low_mem[ZP_CV] = (y - 1) & 0xff
527   write(cv())
528
529 def cleol():
530   write('\x1b[K')
531
532 def clreop():
533   write('\x1b[J')
534
535 def set_scrolling_region(y0, y1):
536   # save cursor, set scrolling region, restore cursor
537   write(f'\x1b[s\x1b[{y0 + 1:d};{y1:d}r\x1b[u')
538
539 def home():
540   # move cursor home, clear to end of screen
541   # just approximates proper behaviour of clearing the window rectangle
542   write(
543     f'\x1b[{low_mem[ZP_WNDTOP] + 1:d};{low_mem[ZP_WNDLFT] + 1:d}H\x1b[J'
544   )
545   low_mem[ZP_CH] = low_mem[ZP_WNDLFT]
546   low_mem[ZP_CV] = low_mem[ZP_WNDTOP]
547   for i in range(low_mem[ZP_WNDTOP], 24):
548     addr = bascalc(i)
549     low_mem[addr:addr + 40] = [0xa0] * 40
550
551 def ch():
552   return f'\x1b[{low_mem[ZP_CH] + 1:d}G'
553
554 def cv():
555   return f'\x1b[{low_mem[ZP_CV] + 1:d}d'
556
557 def invflg():
558   return (
559     '\x1b[0;7;5m'
560   if low_mem[ZP_INVFLG] == INVFLG_FLASH else
561     '\x1b[0;7m'
562   if low_mem[ZP_INVFLG] == INVFLG_INVERSE else
563     '\x1b[0m'
564   )
565
566 def normal():
567   if low_mem[ZP_INVFLG] != INVFLG_NORMAL:
568     low_mem[ZP_INVFLG] = INVFLG_NORMAL
569     write(invflg())
570
571 def inverse():
572   if low_mem[ZP_INVFLG] != INVFLG_INVERSE:
573     low_mem[ZP_INVFLG] = INVFLG_INVERSE
574     write(invflg())
575
576 def flash():
577   if low_mem[ZP_INVFLG] != INVFLG_FLASH:
578     low_mem[ZP_INVFLG] = INVFLG_FLASH
579     write(invflg())
580
581 def tone(freq, dur): # Hz, s
582   if beep_style == BEEP_STYLE_VT100:
583     write(f'\x1b[10;{round(freq):d}]\x1b[11;{round(dur * 1e3):d}]\a')
584   elif beep_style == BEEP_STYLE_ALSA:
585     samples = math.floor(dur * PCM_RATE)
586     samples_last = (samples + PCM_PERIODSIZE - 1) % PCM_PERIODSIZE + 1
587     buf = bytes(
588       [
589         0xff if math.floor(2 * freq * i / PCM_RATE) & 1 else 0
590         for i in range(samples)
591       ] +
592       [0] * (PCM_PERIODSIZE - samples_last)
593     )
594
595     pcm = alsaaudio.PCM(
596       device = 'default',
597       channels = 1,
598       rate = PCM_RATE,
599       format = alsaaudio.PCM_FORMAT_U8,
600       periodsize = PCM_PERIODSIZE
601     )
602     try:
603       i = 0
604       while i < len(buf):
605         i += pcm.write(buf[i:])
606     finally:
607       pcm.close()
608   else:
609     assert False
610
611 def himem(addr):
612   addr &= 0xffff
613   low_mem[ZP_HIMEM] = addr & 0xff
614   low_mem[ZP_HIMEM + 1] = addr >> 8
615
616 def lomem(addr):
617   addr &= 0xffff
618   low_mem[ZP_LOMEM] = addr & 0xff
619   low_mem[ZP_LOMEM + 1] = addr >> 8
620
621 def peek(addr):
622   addr &= 0xffff
623   if addr == HW_IOADR:
624     # sometimes the application ignores an invalid key and waits for a new one
625     #if len(ch_in) == 0 and len(poll_in.poll(POLL_TIMEOUT_MS)):
626     if len(poll_in.poll(POLL_TIMEOUT_MS)):
627       get_internal()
628   return low_mem[addr] if addr < 0x800 else mem.get(addr, 0)
629
630 def poke(addr, data):
631   global ch_in
632
633   addr &= 0xffff
634   if addr == HW_KBDSTRB:
635     ch_in = ''
636     mem[HW_IOADR] &= 0x7f
637   else:
638     data &= 0xff
639     if addr < 0x800:
640       low_mem[addr] = data
641       if addr >= 0x400:
642         y = ((addr >> 7) & 7) | (((addr & 0x7f) // 40) << 3)
643         if y < 24:
644           x = (addr & 0x7f) % 40
645           gr_update(x, y, x + 1, y + 1)
646       elif addr == ZP_CH:
647         write(ch())
648       elif addr == ZP_CV:
649         write(cv())
650       elif addr == ZP_WNDTOP or addr == ZP_WNDBTM:
651         set_scrolling_region(low_mem[ZP_WNDTOP], low_mem[ZP_WNDBTM])
652       elif addr == ZP_INVFLG:
653         write(invflg())
654     else:
655       mem[addr] = data
656
657 # often-used tone routines that are installed by POKEing to addresses 768+:
658 def lemonade_tone():
659   # for lemonade or ribbit, see /test/lemonade_tone.py
660   period_count = ((low_mem[LEMONADE_TONE_PERIOD] - 1) & 0xff) + 1
661   duration_count = ((low_mem[LEMONADE_TONE_DUR] - 1) & 0xff) + 1
662   cycles = 1.37788799e-02 + duration_count * (
663     -4.21513128e-06 + 1.27999925e+02 / period_count
664   )
665   duration = 1.27361219e-05 + duration_count * (
666     2.50702246e-03 + 2.50310997e-03 / period_count
667   )
668   tone(cycles / duration, duration)
669
670 def little_brick_out_tone():
671   # for little brick out, see /test/little_brick_out_tone.py
672   period_count = ((low_mem[LITTLE_BRICK_OUT_TONE_PERIOD] - 1) & 0xff) + 1
673   duration_count = ((low_mem[LITTLE_BRICK_OUT_TONE_DUR] - 1) & 0xff) + 1
674   cycles = 1.37788799e-02 + duration_count * (
675     -4.21513128e-06 + 1.27999925e+02 / period_count
676   )
677   duration = 1.27091766e-05 + duration_count * (
678     2.50897802e-03 + 2.25279897e-03 / period_count
679   )
680   tone(cycles / duration, duration)
681
682 # my tone routine
683 # - uses 16-bit duration, for longer maximum duration
684 # - uses 16-bit frequency, for improved tuning accuracy
685 # - allows zero frequency, for a "rest" or accurate delay
686 # however, duration and frequency constants in the application must be
687 # pre-computed, as code is simplified by not equalizing execution paths
688 def tone_tone():
689   frequency_incr = low_mem[TONE_FREQL] + (low_mem[TONE_FREQH] << 8)
690   duration_count = -(low_mem[TONE_DURL] + (low_mem[TONE_DURH] << 8))
691   duration_count = ((duration_count - 1) & 0xffff) + 1
692   duration = 3.58309497e-10 + duration_count * (
693     2.34821712e-05 + frequency_incr * 4.47591203e-11
694   )
695   if frequency_incr:
696     period = 0x1fffe * (2.34821712e-05 / frequency_incr + 4.47591203e-11)
697     tone(1. / period, duration)
698   else:
699     time.sleep(duration)
700
701 # the following pair of machine language subroutines for lemonade allow
702 # application to quickly replace one colour on the GR screen with another
703 def lemonade_flash_init():
704   global lemonade_flash_color0
705   lemonade_flash_color0 = low_mem[ZP_COLOR] & 0xf
706
707 def lemonade_flash_execute():
708   global lemonade_flash_color0
709
710   time.sleep(.02)
711   color1 = low_mem[ZP_COLOR] & 0xf
712   for i in range(20):
713     x0 = 40
714     x1 = 0
715     addr = bascalc(i)
716     for j in range(40):
717       if (low_mem[addr + j] & 0xf) == lemonade_flash_color0:
718         low_mem[addr + j] = (low_mem[addr + j] & 0xf0) | color1
719         if j < x0:
720           x0 = j
721         x1 = j + 1
722       if (low_mem[addr + j] >> 4) == lemonade_flash_color0:
723         low_mem[addr + j] = (low_mem[addr + j] & 0xf) | (color1 << 4)
724         if j < x0:
725           x0 = j
726         x1 = j + 1
727     if x1 > x0:
728       gr_update(x0, i, x1, i + 1)
729   lemonade_flash_color0 = color1
730
731 def call(addr):
732   addr &= 0xffff
733   if addr == RBOOT_ENTRY or addr == HRCG_START or addr == HRCG_ENTRY:
734     # these do not do anything locally, as they run in the terminal instead
735     pass
736   elif (
737     addr == LEMONADE_TONE_START and (
738       low_mem[LEMONADE_TONE_START:LEMONADE_TONE_END] == LEMONADE_TONE_DATA or
739         low_mem[LEMONADE_TONE_START:LEMONADE_TONE_END2] == LEMONADE_TONE_DATA2
740     )
741   ):
742     lemonade_tone()
743   elif (
744     addr == LITTLE_BRICK_OUT_TONE_START and
745       low_mem[LITTLE_BRICK_OUT_TONE_START:LITTLE_BRICK_OUT_TONE_END] ==
746         LITTLE_BRICK_OUT_TONE_DATA
747   ):
748     little_brick_out_tone()
749   elif addr == TONE_REST:
750     low_mem[TONE_FREQL] = 0
751     low_mem[TONE_FREQH] = 0
752     tone_tone()
753   elif addr == TONE_REST or addr == TONE_TONE:
754     # note: will be loaded with PRINT CHR$(4);"BLOAD TONE.OBJ" which we do not
755     # emulate and therefore it does not appear in low_mem[], it has to go last
756     # (after the above checks for specific tone routines) to avoid a conflict
757     tone_tone()
758   elif (
759     addr == LEMONADE_FLASH_INIT or
760       addr == LEMONADE_FLASH_INIT_PATCHED
761   ):
762     lemonade_flash_init()
763   elif (
764     addr == LEMONADE_FLASH_EXECUTE or
765       addr == LEMONADE_FLASH_EXECUTE_PATCHED
766   ):
767     lemonade_flash_execute()
768   elif addr == ROM_CLREOP:
769     clreop()
770   elif addr == ROM_BELL2:
771     tone(1000, 100) # 1 kHz for .1 sec
772   elif addr == ROM_HOME:
773     # note: was not in Applesoft 1 (cassette version) so can appear as CALL
774     home()
775   elif addr == ROM_CLEOL:
776     cleol()
777   else:
778     raise Exception(f'call {addr:04x}')
779
780 def bascalc(y):
781   return 0x400 | ((y & 7) << 7) | ((y >> 3) * 40)
782
783 def text():
784   global current_gr
785
786   # save cursor, set scrolling region, restore cursor
787   write('\x1b[s\x1b[1;24r\x1b[u')
788   low_mem[ZP_WNDLFT] = 0
789   low_mem[ZP_WNDWTH] = 40
790   low_mem[ZP_WNDTOP] = 0
791   low_mem[ZP_WNDBTM] = 24
792   current_gr = False
793
794 def gr():
795   global current_gr
796
797   # clear screen, set scrolling region (homes cursor)
798   #write(f'\x1b[2J\x1b[21;24r\x1b[1;21H')
799   write(f'\x1b[2J\x1b[21;24r')
800   low_mem[ZP_WNDLFT] = 0
801   low_mem[ZP_WNDWTH] = 40
802   low_mem[ZP_WNDTOP] = 20
803   low_mem[ZP_WNDBTM] = 24
804   low_mem[ZP_CH] = 0
805   low_mem[ZP_CV] = 20
806   for i in range(20):
807     addr = bascalc(i)
808     low_mem[addr:addr + 40] = [0] * 40
809   current_gr = True
810
811 # takes memory encoded with upper pixel in bits 0-3, lower in bits 4-7
812 # returns a stream of unicode characters with attribute-setting escapes
813 # attribute-setting can be optimized by passing in a previous attribute
814 def gr_encode(_in, attr = (-1, -1, -1, -1, -1)):
815   out = []
816   for i in _in:
817     background = colors[i & 0xf]
818     foreground = colors[i >> 4]
819     # we cannot draw two different intense colours in same block,
820     # so just do the best we can (the greater colour gets priority)
821     if background < foreground:
822       ch = '▄'
823     elif background > foreground:
824       background, foreground = foreground, background
825       ch = '▀'
826     elif background < 8:
827       # we try to draw not-intense pixels using background, it
828       # might play nicer with terminals that use black-on-white
829       foreground = 0
830       ch = ' '
831     else:
832       background = 0
833       ch = '█'
834     new_attr = (background & 7, foreground & 7, foreground >> 3, -1, -1)
835     if new_attr != attr:
836       out.append(
837         '\x1b[0;{0:d};{1:d}{2:s}m'.format(
838           new_attr[0] + 40, # foreground
839           new_attr[1] + 30, # background
840           ';1' if new_attr[2] else '' # intense
841         )
842       )
843       attr = new_attr
844     out.append(ch)
845   return ''.join(out), attr
846
847 # similar to gr_encode(), but for the text portion of screen (lines 21-24)
848 # decodes hi bits of characters into attributes interleaved with characters
849 text_hibits = [
850   (0x40, (-1, -1, -1, 1, 0)), # inverse @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
851   (0x20, (-1, -1, -1, 1, 0)), # inverse  !"#$%&'()*+,-./0123456789:;<=>?
852   (0x40, (-1, -1, -1, 1, 1)), # blink   @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
853   (0x20, (-1, -1, -1, 1, 1)), # blink    !"#$%&'()*+,-./0123456789:;<=>?
854   (0x40, (-1, -1, -1, 0, 0)), # normal  @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
855   (0x20, (-1, -1, -1, 0, 0)), # normal   !"#$%&'()*+,-./0123456789:;<=>?
856   (0x40, (-1, -1, -1, 0, 0)), # normal  @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
857   (0x60, (-1, -1, -1, 0, 0)), # normal  `abcdefghijklmnopqrstuvwxyz{|}~del
858 ]
859 def text_encode(_in, attr = (-1, -1, -1, -1, -1)):
860   out = []
861   for i in _in:
862     base, new_attr = text_hibits[i >> 5]
863     if new_attr != attr:
864       out.append(
865         '\x1b[0{0:s}{1:s}m'.format(
866           ';7' if new_attr[3] else '', # reverse video
867           ';5' if new_attr[4] else '' # blink
868         )
869       )
870       attr = new_attr
871     out.append(chr(base | (i & 0x1f)))
872   return ''.join(out), attr
873
874 def gr_update(x0, y0, x1, y1):
875   write('\x1b[s') # save cursor
876   attr = (-1, -1, -1, -1, -1)
877   for i in range(y0, y1):
878     addr = bascalc(i)
879     data, attr = (
880       gr_encode(low_mem[addr + x0:addr + x1], attr)
881     if current_gr and i < 20 else
882       text_encode(low_mem[addr + x0:addr + x1], attr)
883     )
884     write(f'\x1b[{i + 1:d};{x0 + 1:d}H{data:s}')
885   write('\x1b[u' + invflg()) # restore cursor and normal/inverse/flash
886
887 def color(n):
888   low_mem[ZP_COLOR] = n
889
890 def plot(x, y):
891   color = low_mem[ZP_COLOR] & 0xf
892   i = y & 1
893   j = y >> 1
894   addr = bascalc(j)
895   if i == 0:
896     low_mem[addr + x] = (low_mem[addr + x] & 0xf0) | color
897   else:
898     low_mem[addr + x] = (low_mem[addr + x] & 0xf) | (color << 4)
899   gr_update(x, j, x + 1, j + 1)
900
901 def hlin(x0, x1, y):
902   color = low_mem[ZP_COLOR] & 0xf
903   if x1 < x0:
904     x0, x1 = x1, x0
905   i = y & 1
906   j = y >> 1
907   addr = bascalc(j)
908   if i == 0:
909     for x in range(x0, x1 + 1):
910       low_mem[addr + x] = (low_mem[addr + x] & 0xf0) | color
911   else:
912     for x in range(x0, x1 + 1):
913       low_mem[addr + x] = (low_mem[addr + x] & 0xf) | (color << 4)
914   gr_update(x0, j, x1 + 1, j + 1)
915
916 def vlin(y0, y1, x):
917   color = low_mem[ZP_COLOR] & 0xf
918   if y1 < y0:
919     y0, y1 = y1, y0
920   for y in range(y0, y1 + 1):
921     i = y & 1
922     j = y >> 1
923     addr = bascalc(j)
924     if i == 0:
925       low_mem[addr + x] = (low_mem[addr + x] & 0xf0) | color
926     else:
927       low_mem[addr + x] = (low_mem[addr + x] & 0xf) | (color << 4)
928   gr_update(x, y0 >> 1, x + 1, (y1 >> 1) + 1)
929
930 def usr(n):
931   # for ribbit (HRCG)
932   return HRCG_START
933
934 def scrn(x, y):
935   i = y & 1
936   j = y >> 1
937   addr = bascalc(j)
938   return low_mem[addr + x] & 0xf if i == 0 else low_mem[addr + x] >> 4
939
940 def pdl(n):
941   return pdl_value[n]
942
943 def pos():
944   return low_mem[ZP_CH]
945
946 def speed(n):
947   global current_speed
948   current_speed = n & 0xff