Implement ALSA PCM audio sounds for console beep and lemonade (via pyalsaaudio)
authorNick Downing <nick@ndcode.org>
Thu, 19 May 2022 01:46:05 +0000 (11:46 +1000)
committerNick Downing <nick@ndcode.org>
Thu, 19 May 2022 01:46:05 +0000 (11:46 +1000)
apple_io.py
test/timing.py [new file with mode: 0755]
test_io.bas

index d79aebd..44e5ae4 100755 (executable)
@@ -1,5 +1,6 @@
 #!/usr/bin/env python3
 
+import alsaaudio
 import atexit
 import os
 import select
@@ -8,6 +9,9 @@ import termios
 import time
 import tty
 
+PCM_RATE = 44100
+PCM_PERIODSIZE = 44100
+
 # see https://www.tinaja.com/ebooks/tearing_rework.pdf
 ZP_WNDLFT = 0x20 # Left side of scroll window
 ZP_WNDWTH = 0x21 # Width of scroll window
@@ -172,11 +176,18 @@ poll_in.register(fd_in, select.POLLIN)
 mem = {
   ZP_CH: 0,
   ZP_CV: 0,
-  0x300: 0, # tone freq
+  0x300: 0, # tone period
   0x301: 0, # tone dur
 }
 gr_color = 0
 gr_mem = [[0 for j in range(40)] for i in range(40)]
+pcm = alsaaudio.PCM(
+  device = 'default',
+  channels = 1,
+  rate = PCM_RATE,
+  format = alsaaudio.PCM_FORMAT_U8,
+  periodsize = PCM_PERIODSIZE
+)
 
 def init():
   global attr
@@ -211,7 +222,9 @@ def write(data):
 
 def _print(data):
   for ch in data:
-    if ch == '\r':
+    if ch == '\a':
+      tone(1000, 100) # 1 kHz for .1 sec
+    elif ch == '\r':
       # apple treats \r as \r\n, so if you write e.g. \r\n you'll get \r\n\n
       write('\r\n')
       if mem[ZP_CV] < 23:
@@ -219,9 +232,7 @@ def _print(data):
       mem[ZP_CH] = 0
     else:
       write(ch)
-      if ch == '\a':
-         pass
-      elif ch == '\b':
+      if ch == '\b':
         if mem[ZP_CH] > 0:
           mem[ZP_CH] -= 1
       elif mem[ZP_CH] < 39:
@@ -235,6 +246,8 @@ def get():
   ch = read(1) 
   if len(ch) == 0:
     raise Exception('end of input') # due to piping or input redirection
+  if ch == '\x03':
+    raise Exception('user break')
   if ch == '\x7f':
     ch = '\b'
   return ch
@@ -291,7 +304,20 @@ def flash():
 def tone(freq, dur): # Hz, ms
   # doesn't seem to work on any linux console on my laptop
   #write(f'\x1b7\x1b[10;{freq:d}]\x1b[11;{dur:d}]\x07\x1b8')
-  time.sleep(dur * .001)
+  u = time.monotonic() + dur * .001
+  buf = bytes(
+    [
+      0xff if (2 * freq * i // PCM_RATE) & 1 else 0
+      for i in range(PCM_RATE * dur // 1000)
+    ]
+  )
+  i = 0
+  while i < len(buf):
+    i += pcm.write(buf[i:])
+  t = time.monotonic()
+  while t < u:
+    time.sleep(u - t)
+    t = time.monotonic()
 
 def himem(addr):
   pass
@@ -308,15 +334,21 @@ def poke(addr, data):
 def call(addr):
   addr &= 0xffff
   if addr == 0x302:
-    tone(mem[0x300], mem[0x301])
+    # for lemonade, see test/timing.py
+    tone(
+      round(49273.64 / mem[0x300]), # freq
+      round(mem[0x301] * 2.579329) # dur
+    )
   elif addr == 0x3600:
-    # lighting (setup)
+    # for lemonade, lighting (setup)
     pass
   elif addr == 0x3603:
-    # lightning (execute)
+    # for lemonade, lightning (execute)
     pass
   elif addr == ROM_CLREOP:
     clreop()
+  elif addr == ROM_BELL2:
+    tone(1000, 100) # 1 kHz for .1 sec
   else:
     raise Exception(f'call {addr:04x}')
 
@@ -371,7 +403,7 @@ def gr_update(x0, y0, x1, y1):
         br = new_br
       write(ch * 2)
   write('\x1b[u\x1b[0m') # restore cursor and attrs
-  time.sleep(.1)
+  time.sleep(.05)
 
 def color(n):
   global gr_color
diff --git a/test/timing.py b/test/timing.py
new file mode 100755 (executable)
index 0000000..00350a8
--- /dev/null
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+
+import py65.devices.mpu6502
+import sys
+
+CPU_HZ = 3579545 * 4 / 14 # 1.02272714 MHz (from NTSC clock)
+
+mem = [0] * 0x10000
+
+# lemonade.bas
+
+# 10100POKE770,173:POKE771,48:POKE772,192:POKE773,136:POKE774,208:POKE775,5:POKE776,206:POKE777,1:POKE778,3:POKE779,240:POKE780,9:POKE781,202
+mem[770] = 173
+mem[771] = 48
+mem[772] = 192
+mem[773] = 136
+mem[774] = 208
+mem[775] = 5
+mem[776] = 206
+mem[777] = 1
+mem[778] = 3
+mem[779] = 240
+mem[780] = 9
+mem[781] = 202
+
+#10110POKE782,208:POKE783,245:POKE784,174:POKE785,0:POKE786,3:POKE787,76:POKE788,2:POKE789,3:POKE790,96:POKE791,0:POKE792,0
+mem[782] = 208
+mem[783] = 245
+mem[784] = 174
+mem[785] = 0
+mem[786] = 3
+mem[787] = 76
+mem[788] = 2
+mem[789] = 3
+mem[790] = 96
+#mem[791] = 0
+#mem[792] = 0
+
+cpu = py65.devices.mpu6502.MPU(memory = mem, pc = 0x400)
+
+print('jsr 0x0302')
+mem[0x400] = 0x20
+mem[0x401] = 0x02
+mem[0x402] = 0x03
+total_duration_count = 0
+total_duration_secs = 0.
+total_period_count = 0
+total_period_secs = 0.
+for duration_count in [0x10, 0x20, 0x40, 0x80]:
+  for period_count in [0x10, 0x20, 0x40, 0x80]:
+    mem[0x300] = period_count
+    mem[0x301] = duration_count
+    cpu.reset()
+    toggles = 0
+    while cpu.pc != 0x403:
+      if cpu.pc == 0x302:
+        toggles += 1
+      cpu.step()
+    duration_secs = cpu.processorCycles / CPU_HZ
+    period_secs = duration_secs * 2 / toggles
+    frequency = 1. / period_secs
+    print(f'duration_count {duration_count:02x} duration_secs {duration_secs:.6f} period_count {period_count:02x} period_secs {period_secs:.9f} frequency {frequency:10.3f}')
+    total_duration_count += duration_count
+    total_duration_secs += duration_secs
+    total_period_count += period_count
+    total_period_secs += period_secs
+print(f'duration_secs / duration_count = {total_duration_secs / total_duration_count:.6e}')
+print(f'period_secs / period_count = {total_period_secs / total_period_count:.6e}')
+print(f'period_count / period_secs = {total_period_count / total_period_secs:.6e}')
+print()
+
+# monitor ROM
+
+# fca8: 38           WAIT        sec
+# fca9: 48           WAIT2       pha
+# fcaa: e9 01        WAIT3       sbc     #$01            ;1.0204 usec [wrong]
+# fcac: d0 fc                    bne     WAIT3           ;(13+2712*A+512*A*A) [wrong]
+# fcae: 68                       pla
+# fcaf: e9 01                    sbc     #$01
+# fcb1: d0 f6                    bne     WAIT2
+# fcb3: 60                       rts
+mem[0xfca8] = 0x38
+mem[0xfca9] = 0x48
+mem[0xfcaa] = 0xe9
+mem[0xfcab] = 0x01
+mem[0xfcac] = 0xd0
+mem[0xfcad] = 0xfc
+mem[0xfcae] = 0x68
+mem[0xfcaf] = 0xe9
+mem[0xfcb0] = 0x01
+mem[0xfcb1] = 0xd0
+mem[0xfcb2] = 0xf6
+mem[0xfcb3] = 0x60
+
+print('jsr 0xfca8')
+mem[0x400] = 0x20 # jsr
+mem[0x401] = 0xa8
+mem[0x402] = 0xfc
+total_duration_count = 0
+total_duration_secs = 0.
+for duration_count in [0x10, 0x20, 0x40, 0x80]:
+  cpu.reset()
+  cpu.a = duration_count
+  while cpu.pc != 0x403:
+    cpu.step()
+  duration_secs = cpu.processorCycles / CPU_HZ
+  frequency = 1. / period_secs
+  print(f'duration_count {duration_count:02x} duration_secs {duration_secs:.6f}')
+  total_duration_count += duration_count
+  total_duration_secs += duration_secs
+print(f'duration_secs / duration_count = {total_duration_secs / total_duration_count:.6e}')
+print()
index cbec8ac..bcbcb4c 100644 (file)
@@ -2,7 +2,7 @@
 20 HTAB 10
 30 PRINT "MY PROGRAM"
 40 VTAB 5
-50 PRINT "TYPE SOMETHING: ";
+50 PRINT CHR$(7)"TYPE SOMETHING: ";
 60 GET I$
 70 PRINT "YOU TYPED: "I$
 80 INPUT "WHAT'S YOUR NAME? ";J$