From 8bdde3227b59f4a3b33edc81b80d69b01a2789bc Mon Sep 17 00:00:00 2001
From: "Silas S. Brown" <ssb22@cam.ac.uk>
Date: Sat, 22 May 2021 22:41:25 +0100
Subject: [PATCH] trace.py basic video support and Python 3 compatibility

---
 gradint-build/Makefile               |  2 +-
 gradint-build/samples/utils/trace.py | 97 +++++++++++++++++-----------
 gradint-build/src/booktime.py        |  2 +-
 gradint-build/src/filescan.py        |  2 +-
 gradint-build/src/frontend.py        |  2 +-
 gradint-build/src/lessonplan.py      |  2 +-
 gradint-build/src/loop.py            |  4 +-
 gradint-build/src/makeevent.py       |  4 +-
 gradint-build/src/play.py            | 16 ++---
 gradint-build/src/recording.py       |  2 +-
 gradint-build/src/sequence.py        |  2 +-
 gradint-build/src/synth.py           |  2 +-
 gradint-build/src/system.py          |  2 +-
 gradint-build/src/top.py             |  2 +-
 gradint-build/src/users.py           |  2 +-
 gradint-build/thindown.py            |  2 +-
 16 files changed, 85 insertions(+), 60 deletions(-)

diff --git a/gradint-build/Makefile b/gradint-build/Makefile
index 8197cad..068d8a9 100644
--- a/gradint-build/Makefile
+++ b/gradint-build/Makefile
@@ -1,5 +1,5 @@
 # This file is part of the source code of
-# gradint v3.062 (c) 2002-21 Silas S. Brown. GPL v3+.
+# gradint v3.063 (c) 2002-21 Silas S. Brown. GPL v3+.
 #    This program is free software; you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
 #    the Free Software Foundation; either version 3 of the License, or
diff --git a/gradint-build/samples/utils/trace.py b/gradint-build/samples/utils/trace.py
index c7fa906..94822f2 100644
--- a/gradint-build/samples/utils/trace.py
+++ b/gradint-build/samples/utils/trace.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python2
 
 # trace.py: script to generate raytraced animations of Gradint lessons
-# Version 1.11 (c) 2018-19 Silas S. Brown.  License: GPL
+# Version 1.2 (c) 2018-19,2021 Silas S. Brown.  License: GPL
 
 #  The Disney Pixar film "Inside Out" (2015) represented
 #  memories as spheres.  I don't have their CGI models, but
@@ -20,7 +20,11 @@
 # placed onto the spheres, and projected onto the back wall
 # when that word is being recalled)
 # e.g. word1_en.wav, word1_zh.wav, word1.jpg
-# (or png or gif, static only for now).
+# (or png or gif).
+
+# Optionally add an mp4 video of a word in a particular language
+# e.g. word1_en.mp4 (probably best synchronised to word1_en.wav)
+# (can also do this for commentsToAdd and orderlessCommentsToAdd files)
 
 # Requires POV-Ray, ffmpeg, and the Python packages vapory
 # and futures (use sudo pip install futures vapory) -
@@ -40,10 +44,10 @@ width_height_antialias = (640,480,0.001) # 480p (DVD)
 translucent_spheres_when_picture_visible = False # True slows down the rendering
 
 debug_frame_limit = None
-# debug_frame_limit = 60*theFPS # first minute only
 
 povray_quality=9 # default 9: 1=ambient light only, 2=lighting, 4,5=shadows, 8=reflections 9-11=radiosity etc
-# povray_quality = 2
+
+# debug_frame_limit = 60*theFPS ; povray_quality = 2 # first minute only + rough
 
 import sys,os,traceback
 oldName = __name__ ; from vapory import * ; __name__ = oldName
@@ -53,6 +57,10 @@ assert os.path.exists("gradint.py"), "You must move trace.py to the top-level Gr
 import gradint
 assert gradint.outputFile, "You must run trace.py with gradint parameters that include outputFile"
 
+try: xrange
+except: xrange = range
+S,B = gradint.S,gradint.B
+
 class MovableParam:
     def __init__(self): self.fixed = []
     def fixAt(self,t,value):
@@ -100,7 +108,7 @@ class MovableSphere(MovablePos):
         if self.imageFilename:
             if translucent_spheres_when_picture_visible and bkgScrFade.getPos(t) < 1: transmittence = 0.5
             else: transmittence = 0.3
-            return Sphere(list(pos),r,colour(self.colour,t),Texture(Pigment(ImageMap('"'+self.imageFilename+'"',"once","interpolate 2","transmit all "+str(transmittence)),'scale',[1.5*r,1.5*r,1],'translate',list(pos),'translate',[-.75*r,-.75*r,0])))
+            return Sphere(list(pos),r,colour(self.colour,t),Texture(Pigment(ImageMap('"'+S(self.imageFilename)+'"',"once","interpolate 2","transmit all "+str(transmittence)),'scale',[1.5*r,1.5*r,1],'translate',list(pos),'translate',[-.75*r,-.75*r,0])))
         else: return Sphere(list(pos),r,colour(self.colour,t))
 
 class ObjCollection:
@@ -161,9 +169,18 @@ bkgScrX = MovableParam()
 def wall(t):
     picToUse = None
     for st,et,pic in background_screen:
-        if st <= t: picToUse = pic
-        else: break
-    if picToUse and bkgScrFade.getPos(t) < 1: return [Plane([0, 0, 1], 60, Texture(Pigment('color', [1, 1, 1])), Texture(Pigment(ImageMap('"'+picToUse+'"',"once","transmit all "+str(bkgScrFade.getPos(t))),'scale',[background_screen_size,background_screen_size,1],'translate',[bkgScrX.getPos(t)-background_screen_size/2,0,0])), Finish('ambient',0.9))]
+        if st <= t <= et:
+            picToUse = pic
+            if B(picToUse).endswith(B(os.extsep+"mp4")):
+                # need to take single frame from time t-st
+                out = B(picToUse)[:-4]+B("-"+str(t-st)+os.extsep+"jpg")
+                if not os.path.exists(out): # (TODO: if its frame rate is low enough, we might already have the same frame even at a slightly different t-st)
+                    cmd = "ffmpeg -n -threads 1 -accurate_seek -ss "+str(t-st)+" -i "+S(picToUse)+" -vframes 1 -q:v 1 "+S(out)+" </dev/null >/dev/null"
+                    print (cmd)
+                    os.system(cmd)
+                picToUse = out
+        elif st > t: break
+    if picToUse and bkgScrFade.getPos(t) < 1: return [Plane([0, 0, 1], 60, Texture(Pigment('color', [1, 1, 1])), Texture(Pigment(ImageMap('"'+S(picToUse)+'"',"once","transmit all "+str(bkgScrFade.getPos(t))),'scale',[background_screen_size,background_screen_size,1],'translate',[bkgScrX.getPos(t)-background_screen_size/2,0,0])), Finish('ambient',0.9))]
     else: return [Plane([0, 0, 1], 60, Texture(Pigment('color', [1, 1, 1])), Finish('ambient',0.9))] # TODO: why does this look brighter than with ImageMap at transmit all 1.0 ?
 
 ground = Plane( [0, 1, 0], -1, Texture( Pigment( 'color', [1, 1, 1]), Finish( 'phong', 0.1, 'reflection',0.4, 'metallic', 0.3))) # from vapory example
@@ -227,29 +244,18 @@ def eDraw(startTime,length,rowNo,colour):
     else: r.fixAt(startTime+length/2.0,maxR)
 
 def SampleEvent_draw(self,startTime,rowNo,inRepeat):
-    if self.file.startswith(gradint.partialsDirectory): l=self.file.split(os.sep)[1]
+    if B(self.file).startswith(B(gradint.partialsDirectory)): l=B(self.file).split(B(os.sep))[1]
     else: l = gradint.languageof(self.file)
-    eDraw(startTime,self.length,rowNo,self.colour(l))
+    eDraw(startTime,self.length,rowNo,self.colour(S(l)))
 gradint.SampleEvent.draw = SampleEvent_draw
 def SynthEvent_draw(self,startTime,rowNo,inRepeat): eDraw(startTime,self.length,rowNo,self.colour(self.language))
 gradint.SynthEvent.draw = SynthEvent_draw
 
-def sgn(i):
-    if i>0: return 1
-    elif i<0: return -1
-    else: return 0
-
-def byFirstLen(e1,e2):
-    r = e1[0].glue.length+e1[0].glue.adjustment-e2[0].glue.length-e2[0].glue.adjustment
-    # but it must return int not float, so
-    return sgn(r)
-def byStart(e1,e2): return sgn(e1.start-e2.start)
-
 def runGradint():
   gradint.gluedListTracker=[]
   gradint.waitBeforeStart=0
   gradint.main()
-  gradint.gluedListTracker.sort(byFirstLen)
+  gradint.gluedListTracker.sort(key=lambda e:e[0].glue.length+e[0].glue.adjustment)
   duration = 0
   for l,row in zip(gradint.gluedListTracker,xrange(len(gradint.gluedListTracker))):
     def check_for_pictures():
@@ -261,9 +267,9 @@ def runGradint():
        try: el2=j.eventList
        except: el2=[j]
        for i in el2:
-        if hasattr(i,"file") and "_" in i.file:
+        if hasattr(i,"file") and B("_") in B(i.file):
          for imgExt in ["gif","png","jpeg","jpg"]:
-          imageFilename = i.file[:i.file.rindex("_")]+os.extsep+imgExt # TODO: we're assuming no _en etc in the image filename (projected onto both L1 and L2)
+          imageFilename = B(i.file)[:B(i.file).rindex(B("_"))]+B(os.extsep+imgExt) # TODO: we're assuming no _en etc in the image filename (projected onto both L1 and L2)
           if os.path.exists(imageFilename):
               return EventTracker(row,os.path.abspath(imageFilename))
     check_for_pictures()
@@ -275,9 +281,20 @@ def runGradint():
       i.event.draw(i.getEventStart(glueStart),row,False)
       glueStart = i.getAdjustedEnd(glueStart)
       duration = max(duration,glueStart)
+  for t,e in gradint.lastLessonMade.events:
+      if hasattr(e,"file") and hasattr(e,"exactLen"):
+          video = B(e.file)[:B(e.file).rindex(B(os.extsep))]+B(os.extsep+"mp4")
+          if os.path.exists(video): # overwrite static image while playing
+              background_screen.append((t,t+e.exactLen,os.path.abspath(video)))
   background_screen.sort()
   i = 0
   while i < len(background_screen)-1:
+      if background_screen[i][1] > background_screen[i+1][1]: # overlap: we end after next one ends: insert a jump-back-to-us after
+          background_screen.insert(i+2,(background_screen[i+1][1],background_screen[i][1],background_screen[i][2])) # restore old after new one ends
+      if background_screen[i][1] > background_screen[i+1][0] and background_screen[i][0] < background_screen[i+1][0]: # overlap: we end after next one starts, but we start before it starts
+          background_screen[i] = (background_screen[i][0],background_screen[i+1][0],background_screen[i][2]) # new one takes precedence
+      if background_screen[i][0]==background_screen[i+1][0]: # equal start, but next one might be longer
+          background_screen[i+1]=(background_screen[i][1],background_screen[i+1][1],background_screen[i+1][2])
       if background_screen[i][-1]==background_screen[i+1][-1] and background_screen[i][1]+5>=background_screen[i+1][0]:
           # turning off for 5 seconds or less, then turning back on again with the SAME image: might as well merge
           background_screen[i] = (background_screen[i][0],background_screen[i+1][1],background_screen[i][2])
@@ -285,25 +302,31 @@ def runGradint():
       else: i += 1
   for i in xrange(len(background_screen)):
       startTime,endTime,img = background_screen[i]
-      bkgScrFade.fixAt(startTime,1)
+      if i and startTime > background_screen[i-1][1] + 0.5:
+          bkgScrFade.fixAt(startTime,1) # start faded out
+      # else (less than 0.5sec between images) don't try to fade out
       fadeOutTime = endTime
       if i<len(background_screen)-1:
-          fadeOutTime = max(fadeOutTime,min(background_screen[i+1][0]-1,fadeOutTime+5))
-          # and don't move the screen while fading out:
-          for ii in xrange(len(bkgScrX.fixed)):
-              if bkgScrX.fixed[ii][0]==endTime:
-                  bkgScrX.fixed[ii]=((fadeOutTime,bkgScrX.fixed[ii][1]))
-                  break
-      bkgScrFade.fixAt(fadeOutTime,1)
+          if endTime + 0.5 > background_screen[i-1][0]:
+              fadeOutTime = None # as above (< 0.5sec between images)
+          else: fadeOutTime = max(fadeOutTime,min(background_screen[i+1][0]-1,fadeOutTime+5))
+          if not fadeOutTime == None:
+              # don't move the screen during any extended fade-out:
+              for ii in xrange(len(bkgScrX.fixed)):
+                  if bkgScrX.fixed[ii][0]==endTime:
+                      bkgScrX.fixed[ii]=((fadeOutTime,bkgScrX.fixed[ii][1]))
+                      break
+      if not fadeOutTime==None: bkgScrFade.fixAt(fadeOutTime,1)
       if endTime >= startTime+1:
           bkgScrFade.fixAt(startTime+0.5,0.3)
           bkgScrFade.fixAt(endTime-0.5,0.3)
       else:
-          bkgScrFade.fixAt((startTime+endTime)/2.0,0.5) # TODO: do we really want to bother with fade, or even any background image at all, if it's less than 1 second ??
+          bkgScrFade.fixAt((startTime+endTime)/2.0,0.3)
   return duration
 
-def tryFrame((frame,numFrames)):
-    print "Making frame",frame,"of",numFrames
+def tryFrame(f):
+    frame,numFrames = f
+    print ("Making frame "+str(frame)+" of "+str(numFrames))
     try:
         try: os.mkdir("/tmp/"+repr(frame)) # vapory writes a temp .pov file and does not change its name per process, so better be in a process-unique directory
         except: pass
@@ -328,7 +351,7 @@ def main():
         "ffmpeg -nostdin -y -framerate "+repr(theFPS)+" -i /tmp/frame%05d.png -i "+gradint.outputFile+" -movflags faststart -pix_fmt yuv420p /tmp/gradint.mp4 && if test -d /Volumes; then open /tmp/gradint.mp4; fi" #  (could alternatively run with -vcodec huffyuv /tmp/gradint.avi for lossless, insead of --movflags etc, but will get over 6 gig and may get A/V desync problems in mplayer/VLC that -delay doesn't fix, however -b:v 1000k seems to look OK; for WeChat etc you need to recode to h.264, and for HTML 5 video need recode to WebM (but ffmpeg -c:v libvpx no good if not compiled with support for those libraries; may hv to convert on another machine i.e. ffmpeg -i gradint.mp4 -vf scale=320:240 -c:v libvpx -b:v 500k gradint.webm))
         ]:
         if c: # patch up skipped frames, then run ffmpeg
-            print c ; os.system(c)
+            print (c) ; os.system(c)
     for f in xrange(numFrames): os.remove("/tmp/frame%05d.png" % f) # wildcard from command line could get 'argument list too long' on BSD etc
 if __name__=="__main__": main()
-else: print __name__
+else: print (__name__)
diff --git a/gradint-build/src/booktime.py b/gradint-build/src/booktime.py
index 44bfe2c..30ee794 100644
--- a/gradint-build/src/booktime.py
+++ b/gradint-build/src/booktime.py
@@ -1,5 +1,5 @@
 # This file is part of the source code of
-# gradint v3.062 (c) 2002-21 Silas S. Brown. GPL v3+.
+# gradint v3.063 (c) 2002-21 Silas S. Brown. GPL v3+.
 #    This program is free software; you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
 #    the Free Software Foundation; either version 3 of the License, or
diff --git a/gradint-build/src/filescan.py b/gradint-build/src/filescan.py
index 84ca59d..32c4af0 100644
--- a/gradint-build/src/filescan.py
+++ b/gradint-build/src/filescan.py
@@ -1,5 +1,5 @@
 # This file is part of the source code of
-# gradint v3.062 (c) 2002-21 Silas S. Brown. GPL v3+.
+# gradint v3.063 (c) 2002-21 Silas S. Brown. GPL v3+.
 #    This program is free software; you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
 #    the Free Software Foundation; either version 3 of the License, or
diff --git a/gradint-build/src/frontend.py b/gradint-build/src/frontend.py
index 846db20..fa6e616 100644
--- a/gradint-build/src/frontend.py
+++ b/gradint-build/src/frontend.py
@@ -1,5 +1,5 @@
 # This file is part of the source code of
-# gradint v3.062 (c) 2002-21 Silas S. Brown. GPL v3+.
+# gradint v3.063 (c) 2002-21 Silas S. Brown. GPL v3+.
 #    This program is free software; you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
 #    the Free Software Foundation; either version 3 of the License, or
diff --git a/gradint-build/src/lessonplan.py b/gradint-build/src/lessonplan.py
index 1331403..dc533af 100644
--- a/gradint-build/src/lessonplan.py
+++ b/gradint-build/src/lessonplan.py
@@ -1,5 +1,5 @@
 # This file is part of the source code of
-# gradint v3.062 (c) 2002-21 Silas S. Brown. GPL v3+.
+# gradint v3.063 (c) 2002-21 Silas S. Brown. GPL v3+.
 #    This program is free software; you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
 #    the Free Software Foundation; either version 3 of the License, or
diff --git a/gradint-build/src/loop.py b/gradint-build/src/loop.py
index c54db75..23dde22 100644
--- a/gradint-build/src/loop.py
+++ b/gradint-build/src/loop.py
@@ -1,5 +1,5 @@
 # This file is part of the source code of
-# gradint v3.062 (c) 2002-21 Silas S. Brown. GPL v3+.
+# gradint v3.063 (c) 2002-21 Silas S. Brown. GPL v3+.
 #    This program is free software; you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
 #    the Free Software Foundation; either version 3 of the License, or
@@ -42,6 +42,8 @@ def doOneLesson(dbase):
             app.setLabel("Starting lesson")
             app.cancelling = 0
         lesson.play()
+      if not gluedListTracker==None:
+          global lastLessonMade ; lastLessonMade = lesson # used by utils/trace.py
       if dbase and saveProgress and not dbase.saved_completely: # justSaveLesson is a no-op if not first time through lesson (because scripts that use it probably mean "save if not already save"; certainly don't mean "play if is saved")
           if cancelledFiles: dbase.savePartial(cancelledFiles)
           else: dbase.save()
diff --git a/gradint-build/src/makeevent.py b/gradint-build/src/makeevent.py
index c77944f..bf45a18 100644
--- a/gradint-build/src/makeevent.py
+++ b/gradint-build/src/makeevent.py
@@ -1,5 +1,5 @@
 # This file is part of the source code of
-# gradint v3.062 (c) 2002-21 Silas S. Brown. GPL v3+.
+# gradint v3.063 (c) 2002-21 Silas S. Brown. GPL v3+.
 #    This program is free software; you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
 #    the Free Software Foundation; either version 3 of the License, or
@@ -467,7 +467,7 @@ def optimise_partial_playing(ce):
             if wavPlayer=="aplay": wpMod="aplay -q"
             elif wavPlayer.strip().endswith("<"): wpMod=wavPlayer.strip()[:-1] # nc etc
             else: wpMod='sox -t wav - '+sox_type+' '+oss_sound_device
-            s=ShellEvent('set -o pipefail;('+'&&'.join(['cat "%s" | sox -t %s - -t wav - $Vol$ 2>/dev/null' % (ce.eventList[0].file,fileType)]+['cat "%s" | sox -t %s - -t raw - $Vol$'%(e.file,fileType) for e in ce.eventList[1:]])+')'+sox_ignoreLen+'|'+wpMod,True)
+            s=ShellEvent('set -o pipefail;('+'&&'.join(['cat "%s" | sox -t %s - -t wav - $Vol$ 2>/dev/null' % (S(ce.eventList[0].file),fileType)]+['cat "%s" | sox -t %s - -t raw - $Vol$'%(S(e.file),fileType) for e in ce.eventList[1:]])+')'+sox_ignoreLen+'|'+wpMod,True)
             s.VolReplace="sox_effect"
         elif wavPlayer=="aplay" and not sox_effect: s=ShellEvent('aplay -q '+''.join(map(lambda x:' "'+x.file+'"', ce.eventList)),True) # (which is not quite as good but is the next best thing) (and hope they don't then try to re-play a saved lesson with a volume adjustment)
     if s:
diff --git a/gradint-build/src/play.py b/gradint-build/src/play.py
index 284b9c2..15ad66a 100644
--- a/gradint-build/src/play.py
+++ b/gradint-build/src/play.py
@@ -1,5 +1,5 @@
 # This file is part of the source code of
-# gradint v3.062 (c) 2002-21 Silas S. Brown. GPL v3+.
+# gradint v3.063 (c) 2002-21 Silas S. Brown. GPL v3+.
 #    This program is free software; you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
 #    the Free Software Foundation; either version 3 of the License, or
@@ -273,7 +273,7 @@ class SampleEvent(Event):
           if not fileExists_stat(self.file): break # unlink suceeded and still threw exception ??
     def makesSenseToLog(self):
         if hasattr(self,"is_prompt"): return not self.is_prompt # e.g. prompt from synth-cache
-        return not self.file.startswith(promptsDirectory) # (NB "not prompts" doesn't necessarily mean it'll be a sample - may be a customised additional comment)
+        return not B(self.file).startswith(B(promptsDirectory)) # (NB "not prompts" doesn't necessarily mean it'll be a sample - may be a customised additional comment)
     def play(self): # returns a non-{False,0,None} value on error
         if paranoid_file_management:
             if not hasattr(self,"isTemp"): open(self.file) # ensure ready for reading
@@ -348,7 +348,7 @@ class SampleEvent(Event):
             # riscos can't do re-direction (so hope not using a buggy sox) (but again don't have to worry about this if playing because will use PlayIt)
             # + on some setups (e.g. Linux 2.6 ALSA with OSS emulation), it can fail without returning an error code if the DSP is busy, which it might be if (for example) the previous event is done by festival and is taking slightly longer than estimated
             t = time.time()
-            play_error = system('cat "%s" | sox -t %s - %s %s%s >/dev/null' % (self.file,fileType,sox_type,oss_sound_device,sox_effect))
+            play_error = system('cat "%s" | sox -t %s - %s %s%s >/dev/null' % (S(self.file),fileType,sox_type,oss_sound_device,sox_effect))
             if play_error: return play_error
             else:
                 # no error, but did it take long enough?
@@ -359,7 +359,7 @@ class SampleEvent(Event):
                 return 1
         elif wavPlayer=="aplay" and ((not fileType=="mp3") or madplay_path or gotSox):
             if madplay_path and fileType=="mp3": return system(madplay_path+' -q -A '+str(soundVolume_dB)+' "'+self.file+'" -o wav:-|aplay -q') # changeToDirOf() not needed because this won't be cygwin (hopefully)
-            elif gotSox and (sox_effect or fileType=="mp3"): return system('cat "'+self.file+'" | sox -t '+fileType+' - -t wav '+sox_16bit+' - '+sox_effect+' 2>/dev/null|aplay -q') # (make sure o/p is 16-bit even if i/p is 8-bit, because if sox_effect says "vol 0.1" or something then applying that to 8-bit would lose too many bits)
+            elif gotSox and (sox_effect or fileType=="mp3"): return system('cat "'+S(self.file)+'" | sox -t '+fileType+' - -t wav '+sox_16bit+' - '+sox_effect+' 2>/dev/null|aplay -q') # (make sure o/p is 16-bit even if i/p is 8-bit, because if sox_effect says "vol 0.1" or something then applying that to 8-bit would lose too many bits)
             # (2>/dev/null to suppress sox "can't seek to fix wav header" problems, but don't pick 'au' as the type because sox wav->au conversion can take too long on NSLU2 (probably involves rate conversion))
             else: return system('aplay -q "'+self.file+'"')
         # May also be able to support alsa directly with sox (aplay not needed), if " alsa" is in sox -h's output and there is /dev/snd/pcmCxDxp (e.g. /dev/snd/pcmC0D0p), but sometimes it doesn't work, so best stick with aplay
@@ -489,7 +489,7 @@ class SoundCollector(object):
             os.system("sox -t %s \"%s\" %s tmp0" % (fileType,file,self.soxParams()))
             handle=open("tmp0","rb")
         elif winsound or mingw32: handle = os.popen(("sox -t %s - %s - < \"%s\"" % (fileType,self.soxParams(),file)),popenRB)
-        else: handle = os.popen(("cat \"%s\" | sox -t %s - %s -" % (file,fileType,self.soxParams())),popenRB)
+        else: handle = os.popen(("cat \"%s\" | sox -t %s - %s -" % (S(file),fileType,self.soxParams())),popenRB)
         self.theLen += outfile_writeFile(self.o,handle,file)
         if riscos_sound:
             handle.close() ; os.unlink("tmp0")
@@ -531,7 +531,7 @@ def outfile_writeFile(o,handle,filename):
         data = readB(handle,102400)
         outfile_writeBytes(o,data)
         theLen += len(data)
-    if not filename.startswith(partialsDirectory+os.sep): assert theLen, "No data when reading "+filename+": check for sox crash" # (but allow empty partials e.g. r5.  TODO if it's from EkhoSynth it could be a buggy version of Ekho)
+    if not B(filename).startswith(B(partialsDirectory+os.sep)): assert theLen, "No data when reading "+S(filename)+": check for sox crash" # (but allow empty partials e.g. r5.  TODO if it's from EkhoSynth it could be a buggy version of Ekho)
     return theLen
 def outfile_write_error(): raise IOError("Error writing to outputFile: either you are missing an encoder for "+out_type+", or the disk is full or something.")
 def oggenc(): # 2016: some Windows builds are now called oggenc2
@@ -618,7 +618,7 @@ tail -1 "$S" | bash\nexit\n""" % (sox_16bit,sox_signed) # S=script P=params for
         self.seconds += length
         if not checkIn(file,self.file2command):
             if fileType=="mp3": fileData,fileType = decode_mp3(file),"wav" # because remote sox may not be able to do it
-            elif compress_SH and unix: handle=os.popen("cat \""+file+"\" | sox -t "+fileType+" - -t "+fileType+" "+sox_8bit+" - 2>/dev/null",popenRB) # 8-bit if possible (but don't change sample rate, as we might not have floating point)
+            elif compress_SH and unix: handle=os.popen("cat \""+S(file)+"\" | sox -t "+fileType+" - -t "+fileType+" "+sox_8bit+" - 2>/dev/null",popenRB) # 8-bit if possible (but don't change sample rate, as we might not have floating point)
             else: handle = open(file,"rb")
             offset, length = self.bytesWritten, outfile_writeFile(self.o,handle,file)
             self.bytesWritten += length
@@ -689,7 +689,7 @@ def decode_mp3(file): # Returns WAV data including header.  TODO: this assumes i
     elif unix:
         if soxMp3:
             warn_sox_decode()
-            return readB(os.popen("cat \""+file+"\" | sox -t mp3 - -t wav"+cond(compress_SH," "+sox_8bit,"")+" - ",popenRB))
+            return readB(os.popen("cat \""+S(file)+"\" | sox -t mp3 - -t wav"+cond(compress_SH," "+sox_8bit,"")+" - ",popenRB))
         else:
             show_warning("Don't know how to decode "+file+" on this system.  Try installing madplay or mpg123.")
             return ""
diff --git a/gradint-build/src/recording.py b/gradint-build/src/recording.py
index ad3e45c..f23c8e5 100644
--- a/gradint-build/src/recording.py
+++ b/gradint-build/src/recording.py
@@ -1,5 +1,5 @@
 # This file is part of the source code of
-# gradint v3.062 (c) 2002-21 Silas S. Brown. GPL v3+.
+# gradint v3.063 (c) 2002-21 Silas S. Brown. GPL v3+.
 #    This program is free software; you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
 #    the Free Software Foundation; either version 3 of the License, or
diff --git a/gradint-build/src/sequence.py b/gradint-build/src/sequence.py
index 35b9380..0d6758f 100644
--- a/gradint-build/src/sequence.py
+++ b/gradint-build/src/sequence.py
@@ -1,5 +1,5 @@
 # This file is part of the source code of
-# gradint v3.062 (c) 2002-21 Silas S. Brown. GPL v3+.
+# gradint v3.063 (c) 2002-21 Silas S. Brown. GPL v3+.
 #    This program is free software; you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
 #    the Free Software Foundation; either version 3 of the License, or
diff --git a/gradint-build/src/synth.py b/gradint-build/src/synth.py
index 0259c0b..3eff1d0 100644
--- a/gradint-build/src/synth.py
+++ b/gradint-build/src/synth.py
@@ -1,5 +1,5 @@
 # This file is part of the source code of
-# gradint v3.062 (c) 2002-21 Silas S. Brown. GPL v3+.
+# gradint v3.063 (c) 2002-21 Silas S. Brown. GPL v3+.
 #    This program is free software; you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
 #    the Free Software Foundation; either version 3 of the License, or
diff --git a/gradint-build/src/system.py b/gradint-build/src/system.py
index 398a0aa..46e4f7b 100644
--- a/gradint-build/src/system.py
+++ b/gradint-build/src/system.py
@@ -1,5 +1,5 @@
 # This file is part of the source code of
-# gradint v3.062 (c) 2002-21 Silas S. Brown. GPL v3+.
+# gradint v3.063 (c) 2002-21 Silas S. Brown. GPL v3+.
 #    This program is free software; you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
 #    the Free Software Foundation; either version 3 of the License, or
diff --git a/gradint-build/src/top.py b/gradint-build/src/top.py
index c1134e6..460e0c3 100644
--- a/gradint-build/src/top.py
+++ b/gradint-build/src/top.py
@@ -2,7 +2,7 @@
 # -*- coding: utf-8 -*-
 #   (Python 2 or Python 3, but more fully tested on 2)
 
-program_name = "gradint v3.062 (c) 2002-21 Silas S. Brown. GPL v3+."
+program_name = "gradint v3.063 (c) 2002-21 Silas S. Brown. GPL v3+."
 
 #    This program is free software; you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
diff --git a/gradint-build/src/users.py b/gradint-build/src/users.py
index c31669b..95399f5 100644
--- a/gradint-build/src/users.py
+++ b/gradint-build/src/users.py
@@ -1,5 +1,5 @@
 # This file is part of the source code of
-# gradint v3.062 (c) 2002-21 Silas S. Brown. GPL v3+.
+# gradint v3.063 (c) 2002-21 Silas S. Brown. GPL v3+.
 #    This program is free software; you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
 #    the Free Software Foundation; either version 3 of the License, or
diff --git a/gradint-build/thindown.py b/gradint-build/thindown.py
index d5e295e..9e479a2 100644
--- a/gradint-build/thindown.py
+++ b/gradint-build/thindown.py
@@ -1,5 +1,5 @@
 # This file is part of the source code of
-# gradint v3.062 (c) 2002-21 Silas S. Brown. GPL v3+.
+# gradint v3.063 (c) 2002-21 Silas S. Brown. GPL v3+.
 #    This program is free software; you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
 #    the Free Software Foundation; either version 3 of the License, or
-- 
GitLab