2caf3143a7fe92c6f22e59028d84e8538a8034e8
[applesoft_basic.git] / apple_io.py
1 #!/usr/bin/env python3
2
3 import alsaaudio
4 import atexit
5 import os
6 import select
7 import sys
8 import termios
9 import time
10 import tty
11
12 PCM_RATE = 44100
13 PCM_PERIODSIZE = 44100
14
15 # see https://www.tinaja.com/ebooks/tearing_rework.pdf
16 ZP_WNDLFT = 0x20 # Left side of scroll window
17 ZP_WNDWTH = 0x21 # Width of scroll window
18 ZP_WNDTOP = 0x22 # Top of scroll window
19 ZP_WNDBTM = 0x23 # Bottom of scroll window
20 ZP_CH = 0x24 # Cursor horizontal position
21 ZP_CV = 0x25 # Cursor vertical position
22 ZP_GBASL = 0x21 # LORES graphics base low
23 ZP_GBASH = 0x27 # LORES graphics base high
24 ZP_BASL = 0x28 # TEXT base address low
25 ZP_BASH = 0x29 # TEXT base address high
26 ZP_BAS2L = 0x2A # Scroll temporary base low
27 ZP_BAS2H = 0x2B # Scroll temporary base high
28 ZP_COLOR = 0x30 # Holds the LORES color value
29 ZP_INVFLG = 0x32 # Normal/Inverse/Flash mask
30 ZP_PROMPT = 0x33 # Holds prompt symbol
31 ZP_YSAV = 0x34 # Temporary Y register hold
32 ZP_CSWL = 0x36 # Output character hook low
33 ZP_CSWH = 0x37 # Output character hook high
34 ZP_KSWL = 0x38 # Input character hook low
35 ZP_KSWH = 0x39 # Input character hook high
36 ZP_ACC = 0x45 # Accumulator save
37 ZP_XREG = 0x46 # X register save
38 ZP_YREG = 0x47 # Y register save
39 ZP_STATUS = 0x48 # Flag register save
40 ZP_SPNT = 0x49 # Stack pointer save
41 ZP_RNDL = 0x4E # Keybounce random number low
42 ZP_RNDH = 0x4F # Keybounce random number high
43 VP_REENTER = 0x03D0 # Re-enter DOS
44 VP_RECONNECT = 0x03EA # Reconnect DOS I/O hooks
45 VP_BRKV = 0x03F0 # Break vector address
46 VP_SOFTEV = 0x03F2 # Warm start vector address
47 VP_PWRDUP = 0x03F4 # Warm start EOR A5 checksum
48 VP_AMPERV = 0x03F6 # Applesoft "&" Jump Code
49 VP_USRADR = 0x03F8 # Control Y vector Jump Code
50 VP_NMI = 0x03FB # NMI vector Jump Code
51 VP_IRQLOC = 0x03FE # Interrupt vector address
52 HW_IOADR = 0xC000 # Keyboard input location
53 HW_KBDSTRB = 0xC010 # Keyboard strobe reset
54 HW_TAPEOUT = 0xC020 # Cassette data output
55 HW_SPKR = 0xC030 # Speaker click output
56 HW_STROBE = 0xC040 # Game I/O connector strobe
57 HW_TXTCLR = 0xC050 # Graphics ON soft switch
58 HW_TXTSET = 0xC051 # Text ON soft switch
59 HW_MIXCLR = 0xC052 # Full screen ON soft switch
60 HW_MIXSET = 0xC053 # Split screen ON soft switch
61 HW_LOWSCR = 0xC054 # Page ONE display soft switch
62 HW_HISCR = 0xC055 # Page TWO display soft switch
63 HW_LORES = 0xC056 # LORES ON soft switch
64 HW_HIRES = 0xC057 # HIRES ON soft switch
65 HW_AN0CLR = 0xC058 # Annunciator 0 OFF soft switch
66 HW_AN0SET = 0xC059 # Annunciator 0 ON soft switch
67 HW_AN1CLR = 0xC05A # Annunciator 1 OFF soft switch
68 HW_AN1SET = 0xC05B # Annunciator 1 ON soft switch
69 HW_AN2CLR = 0xC05C # Annunciator 2 OFF soft switch
70 HW_AN2SET = 0xC05D # Annunciator 2 ON soft switch
71 HW_AN3CLR = 0xC05E # Annunciator 3 OFF soft switch
72 HW_AN3SET = 0xC05F # Annunciator 3 ON soft switch
73 HW_TAPEIN = 0xC060 # Cassette tape read input
74 HW_PB0 = 0xC061 # Push button 0 input
75 HW_PB1 = 0xC062 # Push button 1 input
76 HW_PB2 = 0xC063 # Push button 2 input
77 HW_PDL0 = 0xC064 # Game Paddle 0 analog input
78 HW_PDL1 = 0xC065 # Game Paddle 1 analog input
79 HW_PDL2 = 0xC066 # Game Paddle 2 analog input
80 HW_PDL3 = 0xC067 # Game Paddle 3 analog input
81 ROM_PLOT = 0xF800 # Plot a block on LORES screen
82 ROM_HLINE = 0xF819 # Draw a horizontal LORES line
83 ROM_VLINE = 0xF828 # Draw a vertical LORES line
84 ROM_CLRSCR = 0xF832 # Clear full LORES screen
85 ROM_CLRTOP = 0xF836 # Clear top of LORES screen
86 ROM_GBASCALC = 0xF847 # Calculate LORES base address
87 ROM_NEXTCOL = 0xF85F # Increase LORES color by three
88 ROM_SETCOL = 0xF864 # Set color for LORES plotting
89 ROM_SCRN = 0xF871 # Read color of LORES screen
90 ROM_PRNTAX = 0xF941 # Output A then X as hex
91 ROM_PRBLNK = 0xF948 # Output three spaces via hooks
92 ROM_PRBL2 = 0xF94A # Output X spaces via hooks
93 ROM_STEP = 0xFA43 # Single step (old ROM only!)
94 ROM_REGDSP = 0xFAD7 # Display working registers
95 ROM_PREAD = 0xFB1E # Read a game paddle
96 ROM_INIT = 0xFB2F # Initial i ze text screen
97 ROM_SETTXT = 0xFB39 # Set up text screen
98 ROM_SETGR = 0xFB40 # Setup LORES screen
99 ROM_SETWND = 0xFB4B # Set text window to normal
100 ROM_BASCALC = 0xFBC1 # Calculate text base address
101 ROM_BELL = 0xFBD9 # 1 Beep speaker if ctrl G
102 ROM_BELL2 = 0xFBE4 # Beep speaker once
103 ROM_ADVANCE = 0xFBF4 # Move text cursor right by one
104 ROM_VIDOUT = 0xFBFD # Output ASCII to screen only
105 ROM_BS = 0xFC10 # Backspace screen
106 ROM_UP = 0xFC1A # Move screen cursor up one
107 ROM_VTAB = 0xFC22 # Vertical screen tab using CV
108 ROM_VTABZ = 0xFC24 # Vertical screen tab using A
109 ROM_ESCl = 0xFC2C # Process escape movements A-G
110 ROM_CLREOP = 0xFC42 # Clear text to end of screen
111 ROM_HOME = 0xFC58 # Clear screen and home cursor
112 ROM_CR = 0xFC62 # Carriage return to screen
113 ROM_LF = 0xFC66 # Line feed to screen onIy
114 ROM_SCROLL = 0xFC70 # Scroll text screen up one
115 ROM_CLEOL = 0xFC9C # Clear text to end of line
116 ROM_WAIT = 0xFCA8 # Time delay set by accumulator
117 ROM_RDKEY = 0xFD0C # Get input character via hooks
118 ROM_KEYIN = 0xFD1B # Read the Apple keyboard
119 ROM_RDCHAR = 0xFD35 # Get key and process ESC A-F
120 ROM_CANCEL = 0xFD62 # Cancel keyboard line entry
121 ROM_GETLNZ = 0xFD67 # CR, then get kbrl input line
122 ROM_GETLN = 0xFD6A # Get input line from keyboard
123 ROM_GETLN1 = 0xFD6F # Get kbd input, no prompt
124 ROM_CROUT1 = 0xFD8B # Clear EOL then CR via hooks
125 ROM_CROUT = 0xFD8E # Output return via hooks
126 ROM_PRBYTE = 0xFDDA # Output full A in hpxto hooks
127 ROM_PRHEX = 0xFDE3 # Output low A in hex to hooks
128 ROM_COUT = 0xFDED # Output character via hooks
129 ROM_COUT1 = 0xFDF0 # Output character to screen
130 ROM_MOVE = 0xFE2C # Move block of memory
131 ROM_VERIFY = 0xFE36 # Verify block of memory
132 ROM_LIST = 0xFE5E # Disassemble 20 instructions
133 ROM_L1ST2 = 0xFE63 # Disassemble A instructions
134 ROM_SETINV = 0xFE80 # Print inverse text on screen
135 ROM_SETNORM = 0xFE84 # Print normal text on screen
136 ROM_SETVID = 0xFE93 # Grab output hooks for screen
137 ROM_XBASIC = 0xFEB0 # Goto BASIC, destroying old
138 ROM_BASCON = 0xFEB3 # Goto BASIC continuing old
139 ROM_TRACE = 0xFEC2 # Start tracing (old ROM only!)
140 ROM_WRITE = 0xFECD # Save to cassette tape
141 ROM_READ = 0xFEFD # Read from cassette tape
142 ROM_PRERR = 0xFF2D # Print "ERR" to output hook
143 ROM_BELL = 0xFF3A # Output bell via hooks
144 ROM_IORESR = 0xFF3F # Restore all working register
145 ROM_IOSAVE = 0xFF4A # Save all working registers
146 ROM_OLDRST = 0xFF59 # Old reset entry, no autostart
147 ROM_MON = 0xFF65 # Enter monitor and beep spkr
148
149 # see https://imgur.com/h0NNOc3
150 # colors (map 15 apple colors to 16 VT100 colours, some duplicate or unused)
151 colors = [
152   0x0, # black -> black
153   0x1, # red -> red
154   0x4, # d.blue -> blue
155   0x5, # purple -> magenta
156   0x2, # d.green -> green
157   0x7, # gray 1 -> white
158   0xc, # m.blue -> intense blue
159   0xd, # l.blue -> intense magenta
160   0x3, # brown -> yellow
161   0xb, # orange -> intense yellow
162   0x7, # grey 2 -> white
163   0xd, # pink -> intense magenta
164   0xa, # l.green -> intense green
165   0xb, # yellow -> intense yellow
166   0xe, # aqua -> intense cyan
167   0xf, # white -> intense white
168 ]
169
170 # global state
171 attr = None
172 fd_in = sys.stdin.fileno()
173 fd_out = sys.stdout.fileno()
174 poll_in = select.poll()
175 poll_in.register(fd_in, select.POLLIN)
176 mem = {
177   ZP_WNDLFT: 0,
178   ZP_WNDWTH: 40,
179   ZP_WNDTOP: 0,
180   ZP_WNDBTM: 24,
181   ZP_CH: 0,
182   ZP_CV: 0,
183   0x300: 0, # tone period
184   0x301: 0, # tone dur
185 }
186 gr_color = 0
187 gr_mem = [[0 for j in range(40)] for i in range(40)]
188 pcm = alsaaudio.PCM(
189   device = 'default',
190   channels = 1,
191   rate = PCM_RATE,
192   format = alsaaudio.PCM_FORMAT_U8,
193   periodsize = PCM_PERIODSIZE
194 )
195
196 def init():
197   global attr
198
199   if attr is None and os.isatty(fd_in):
200     attr = termios.tcgetattr(fd_in)
201     atexit.register(deinit)
202     tty.setraw(fd_in)
203     write('\x1b[?25l\x1b[0m') # hide cursor, reset attributes
204
205 def deinit():
206   global attr
207
208   if attr is not None:
209     write('\x1b[?25h\x1b[0m') # show cursor, reset attributes
210     termios.tcsetattr(fd_in, termios.TCSADRAIN, attr)
211     atexit.unregister(deinit)
212     attr = None
213
214 def pr_hash(n):
215   pass
216
217 def in_hash(n):
218   pass
219
220 def read_ready():
221   return len(poll_in.poll(1)) != 0
222
223 def read(n):
224   return str(os.read(fd_in, 1), 'utf-8')
225
226 def write(data):
227   os.write(fd_out, bytes(data, 'utf-8'))
228
229 def _print(data):
230   for ch in data:
231     if ch == '\a':
232       tone(1000, 100) # 1 kHz for .1 sec
233     elif ch == '\r':
234       # apple treats \r as \r\n, so if you write e.g. \r\n you'll get \r\n\n
235       write('\r\n')
236       if mem[ZP_CV] < 23:
237         mem[ZP_CV] += 1
238       mem[ZP_CH] = 0
239     else:
240       write(ch)
241       if ch == '\b':
242         if mem[ZP_CH] > 0:
243           mem[ZP_CH] -= 1
244       elif mem[ZP_CH] < 39:
245         mem[ZP_CH] += 1
246       else:
247         write('\r\n')
248         if mem[ZP_CV] < 23:
249           mem[ZP_CV] += 1
250         mem[ZP_CH] = 0
251
252 def get():
253   ch = read(1) 
254   if len(ch) == 0:
255     raise Exception('end of input') # due to piping or input redirection
256   if ch == '\x03':
257     raise Exception('user break')
258   if ch == '\x7f':
259     ch = '\b'
260   return ch
261
262 def input():
263   write('\x1b[?25h') # show cursor
264   line = ''
265   while True:
266     ch = get()
267     if ch == '\b':
268       if len(line):
269         _print('\b \b')
270         line = line[:-1]
271       elif mem[ZP_CH]:
272         _print('\r')
273     else:
274       _print(ch)
275       if ch == '\r':
276         write('\x1b[?25l') # hide cursor
277         return line
278       else:
279         if len(line) >= 247:
280           _print('\a')
281           if len(line) >= 255:
282             _print('\r')
283             line = ''
284             continue
285         line += ch
286
287 def htab(x):
288   write(f'\x1b[{x:d}G')
289   mem[ZP_CH] = x - 1
290
291 def vtab(y):
292   write(f'\x1b[{y:d}d')
293   mem[ZP_CV] = y - 1
294
295 def clreop():
296   write('\x1b[J')
297
298 def home():
299   write('\x1b[2J\x1b[H')
300   mem[ZP_CH] = 0
301   mem[ZP_CV] = 0
302
303 def normal():
304   write('\x1b[0m')
305
306 def inverse():
307   write('\x1b[0;7m')
308
309 def flash():
310   write('\x1b[0;7;5m')
311
312 def tone(freq, dur): # Hz, ms
313   # doesn't seem to work on any linux console on my laptop
314   #write(f'\x1b7\x1b[10;{freq:d}]\x1b[11;{dur:d}]\x07\x1b8')
315   u = time.monotonic() + dur * .001
316   buf = bytes(
317     [
318       0xff if (2 * freq * i // PCM_RATE) & 1 else 0
319       for i in range(PCM_RATE * dur // 1000)
320     ]
321   )
322   i = 0
323   while i < len(buf):
324     i += pcm.write(buf[i:])
325   t = time.monotonic()
326   while t < u:
327     time.sleep(u - t)
328     t = time.monotonic()
329
330 def himem(addr):
331   pass
332
333 def lomem(addr):
334   pass
335
336 def peek(addr):
337   addr &= 0xffff
338   return mem.get(addr, 0)
339
340 def poke(addr, data):
341   addr &= 0xffff
342   data &= 0xff
343   mem[addr] = data
344   if addr == ZP_WNDTOP or addr == ZP_WNDBTM:
345     # save cursor, set scrolling region, restore cursor
346     write(f'\x1b[s\x1b{mem[ZP_WNDTOP + 1]:d};{mem[ZP_WNDBTM]:d}24r\x1b[u')
347
348 def call(addr):
349   addr &= 0xffff
350   if addr == 0x302 or addr == 0x310:
351     # for lemonade, see test/lemonade_tone_patched.py
352     a = mem[0x301] # duration_count
353     b = a / (((mem[0x300] - 1) & 0xff) + 1) # duration_count / period_count
354     cycles = 1.37788799e-02 - 4.21513128e-06 * a + 1.27999925e+02 * b
355     duration = 1.27361219e-05 + 2.50702246e-03 * a + 2.50310997e-03 * b
356     tone(round(cycles / duration), round(duration * 1e3))
357   elif addr == 0x3600:
358     # for lemonade, lighting (setup)
359     pass
360   elif addr == 0x3603:
361     # for lemonade, lightning (execute)
362     pass
363   elif addr == ROM_CLREOP:
364     clreop()
365   elif addr == ROM_BELL2:
366     tone(1000, 100) # 1 kHz for .1 sec
367   else:
368     raise Exception(f'call {addr:04x}')
369
370 def text():
371   # save cursor, set scrolling region, restore cursor
372   write('\x1b[s\x1b1;24r\x1b[u')
373   mem[ZP_WNDLFT] = 0
374   mem[ZP_WNDWTH] = 40
375   mem[ZP_WNDTOP] = 0
376   mem[ZP_WNDBTM] = 24
377
378 def gr():
379   # clear screen, set scrolling region (homes cursor)
380   mem[ZP_WNDLFT] = 0
381   mem[ZP_WNDWTH] = 40
382   mem[ZP_WNDTOP] = 20
383   mem[ZP_WNDBTM] = 24
384   mem[ZP_CH] = 0
385   mem[ZP_CV] = 20
386   for i in range(40):
387     gr_mem[i][:] = [0 for j in range(40)]
388
389 def gr_update(x0, y0, x1, y1):
390   write('\x1b[s') # save cursor
391   bg = -1
392   fg = -1
393   br = -1
394   for i in range(y0, y1):
395     write(f'\x1b[{i + 1:d};{x0 * 2 + 1:d}H')
396     for j in range(x0, x1):
397       new_bg = colors[gr_mem[i * 2][j]]
398       new_fg = colors[gr_mem[i * 2 + 1][j]]
399       # we cannot draw two different intense colours in same block,
400       # so just do the best we can (the greater colour gets priority)
401       if new_bg < new_fg:
402         ch = '▄'
403       elif new_bg > new_fg:
404         new_bg, new_fg = new_fg, new_bg
405         ch = '▀'
406       elif new_bg < 8:
407         # we try to draw not-intense pixels using background, it
408         # might play nicer with terminals that use black-on-white
409         new_fg = 0
410         ch = ' '
411       else:
412         new_bg = 0
413         ch = '█'
414       new_br = new_fg >= 8
415       new_bg &= 7
416       new_fg &= 7
417       if new_bg != bg or new_fg != fg or new_br != br:
418         write(
419           '\x1b[0{0:s}{1:s}{2:s}m'.format(
420             f';4{new_bg:d}' if new_bg else '',
421             f';3{new_fg:d}' if new_fg else '',
422             ';1' if new_br else ''
423           )
424         )
425         bg = new_bg
426         fg = new_fg
427         br = new_br
428       write(ch * 2)
429   write('\x1b[u\x1b[0m') # restore cursor and attrs
430   time.sleep(.05)
431
432 def color(n):
433   global gr_color
434   gr_color = n
435
436 def plot(x, y):
437   gr_mem[y][x] = gr_color
438   y >>= 1
439   gr_update(x, y, x + 1, y + 1)
440
441 def hlin(x0, x1, y):
442   if x1 < x0:
443     x0, x1 = x1, x0
444   for x in range(x0, x1 + 1):
445     gr_mem[y][x] = gr_color
446   y >>= 1
447   gr_update(x0, y, x1 + 1, y + 1)
448
449 def vlin(y0, y1, x):
450   if y1 < y0:
451     y0, y1 = y1, y0
452   for y in range(y0, y1 + 1):
453     gr_mem[y][x] = gr_color
454   gr_update(x, y0 >> 1, x + 1, (y1 >> 1) + 1)
455
456 def usr(n):
457   return 0
458
459 def scrn(x, y):
460   return gr_mem[y][x]
461
462 def pdl(n):
463   return 127
464
465 def pos():
466   return mem[ZP_CH]
467
468 if __name__ == '__main__':
469   init()
470   while True:
471     while not read_ready():
472       pass
473     k = ord(read(1))
474     write(f'k {k:02x}\r')
475     if k == 0x1b:
476       break
477   #for i in range(12):
478   #  tone(int(round(220 * 2 ** (i / 12.))), 250)
479   deinit()