FAQ | This is a LIVE service | Changelog

Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • ssb22/gradint
  • st822/gradint
2 results
Show changes
Showing with 1740 additions and 620 deletions
# This file is part of the source code of
# gradint v0.9969 (c) 2002-2011 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
......@@ -27,7 +27,7 @@ def play(event):
timeout_time = time.time() + max(10,event.length/3) # don't loop *forever* if unable to start playing (especially if we're being used in a reminder system etc, it may be best to exit eventually)
if lessonStartTime and not soundCollector:
if hasattr(event,"max_lateness"): timeout_time = min(timeout_time, lessonStartTime + (copy_of_runner_events[0][2]+event.max_lateness))
if hasattr(event,"sequenceID") and event.sequenceID in sequenceIDs_to_cancel: timeout_time = 0
if hasattr(event,"sequenceID") and checkIn(event.sequenceID,sequenceIDs_to_cancel): timeout_time = 0
play_error = "firstTime"
while play_error and time.time()<=timeout_time: # use <= rather than < in case we have only 1sec precision
if not play_error=="firstTime":
......@@ -41,24 +41,26 @@ def play(event):
if hasattr(event,"sequenceID"): sequenceIDs_to_cancel[event.sequenceID]=True # TODO what if its last event has "endseq" attribute, do we want to decrement wordsLeft early?
if hasattr(event,"endseq"): wordsLeft[event.endseq] -= 1
del copy_of_runner_events[0]
if soundCollector: doLabel("%d%% completed" % (soundCollector.tell()*100/lessonLen))
else:
line2 = "" # report what you'd lose if you cancel now (in case you're deciding whether to answer the phone etc)
new,old=wordsLeft[True],wordsLeft[False]
if new: line2="%d new " % new
if old:
if line2: line2 += ("+ %d old " % old)
else: line2="%d old words " % old
elif new: line2 += "words "
if line2: line2=cond(app or appuifw,"\n",", ")+line2+"remain"
if not lessonStartTime: lessonStartTime = time.time() # the actual time of the FIRST event (don't set it before as there may be delays). (we're setting this at the END of the 1st event - the extra margin should be ok, and can help with start-of-lesson problems with slow disks.)
if finishTime and time.time() >= emergency_lessonHold_to: doLabel("%s (finish %s)%s" % (time.strftime("%H:%M",time.localtime(time.time())),time.strftime("%H:%M",time.localtime(finishTime)),line2)) # was %I:%M but don't like leading '0' in PM times. 2nd condition added because might press 'brief interrupt' while playing.
if soundCollector: return doLabel("%d%% completed" % (soundCollector.tell()*100/lessonLen))
line2 = "" # report what you'd lose if you cancel now (in case you're deciding whether to answer the phone etc), + say how many already cancelled (for diagnosing results of interruptions caused by phone events etc on those platforms)
new,old=wordsLeft[True],wordsLeft[False]
if new: line2="%d new " % new
if old:
if line2: line2 += ("+ %d old " % old)
else: line2="%d old words " % old
elif new: line2 += "words "
if line2:
line2=cond(app or appuifw or android,"\n",", ")+line2+"remain"
if cancelledFiles: line2 += "\n("+str(len(cancelledFiles))+" cancelled)"
if not lessonStartTime: lessonStartTime = time.time() # the actual time of the FIRST event (don't set it before as there may be delays). (we're setting this at the END of the 1st event - the extra margin should be ok, and can help with start-of-lesson problems with slow disks.)
if finishTime and time.time() >= emergency_lessonHold_to: doLabel("%s (finish %s)%s" % (time.strftime("%H:%M",time.localtime(time.time())),time.strftime("%H:%M",time.localtime(finishTime)),line2)) # was %I:%M but don't like leading '0' in PM times. 2nd condition added because might press 'brief interrupt' while playing.
def doLabel(labelText):
labelText = ensure_unicode(labelText)
if app: app.setLabel(labelText)
elif appuifw:
t=appuifw.Text() ; t.add(labelText)
appuifw.app.body = t
elif android: android.makeToast(labelText) # TODO alternatives? method to cancel lessons etc would be nice
elif not (riscos_sound or winCEsound): # (we don't have a way of updating a clock or progress indicator on those)
global doLabelLastLen
try: doLabelLastLen
......@@ -93,86 +95,107 @@ if ask_teacherMode:
old_mysleep(1)
def maybe_unicode(label):
if app or appuifw:
if app or appuifw or android:
try: return unicode(label,'utf-8')
except: return label # ??
except: return label # e.g. Python 3
else: return repr(label)
madplay_program = 0
if (winsound or mingw32) and fileExists("madplay.exe"): madplay_program = "madplay.exe"
madplay_path = None
if (winsound or mingw32) and fileExists("madplay.exe"): madplay_path = "madplay.exe"
elif riscos_sound:
amplay = "$.!Boot.Resources.!System.350.Modules.Audio.MP3.AMPlayer"
if fileExists(amplay): os.system(amplay) # seems it doesn't always load at boot; no-op if run again
else: amplay = 0
elif unix and hasattr(os,"popen"):
madplay_program = os.popen("PATH=$PATH:. which madplay 2>/dev/null").read().strip(wsp)
if not fileExists(cond(cygwin,madplay_program+".exe",madplay_program)): madplay_program=0 # in case of a Unix 'which' returning error on stdout
if madplay_program and not winsound and not mingw32: madplay_program='"'+madplay_program+'"' # in case there's spaces etc in the path
madplay_path = os.popen("PATH=$PATH:. which madplay 2>/dev/null").read()
try: madplay_path = wspstrip(madplay_path)
except: madplay_path = madplay_path.strip()
if not fileExists(cond(cygwin,madplay_path+".exe",madplay_path)): madplay_path="" # in case of a Unix 'which' returning error on stdout
if madplay_path and not winsound and not mingw32: madplay_path='"'+madplay_path+'"' # in case there's spaces etc in the path
if madplay_path and not mp3Player: mp3Player=madplay_path
def intor0(v):
try: return int(v)
except ValueError: return 0
def digitPrefix(v): # in case we don't have regexp library
l = []
for d in list(v):
if '0' <= d <= '9': l.append(d)
else: break
return intor0(''.join(l))
playProgram = mpg123 = "" ; sox_effect=""
sox_8bit, sox_16bit, sox_ignoreLen = "-b", "-w", ""
sox_effect=""
sox_8bit, sox_16bit, sox_ignoreLen, sox_signed = "-b", "-w", "", "-s"
# Older sox versions (e.g. the one bundled with Windows Gradint) recognise -b and -w only; sox v14+ recognises both that and -1/-2; newer versions recognise only -1/-2. We check for newer versions if unix. (TODO riscos / other?)
soundVolume_dB = math.log(soundVolume)*(-6/math.log(0.5))
if unix:
if macsound: got_qtplay = 1 # should be bundled
sox_formats=os.popen("sox --help 2>&1").read() # NOT .lower() yet
if sox_formats.lower().startswith("sox: sox v"):
soxMaj = intor0(sox_formats[10:sox_formats.index('.')])
if soxMaj>=14:
sox_8bit, sox_16bit = "-1", "-2" # see comment above
if soxMaj==14 and sox_formats[13]<'3': pass
else: sox_ignoreLen = "|sox --ignore-length -t wav - -t wav - 2>/dev/null"
if sox_formats.lower().find("wav")>-1: gotSox=1
else:
gotSox=0
if got_program("sox"):
if macsound and sox_formats.find("Rosetta")>-1:
try: u""+sox_formats
except: sox_formats="Please install Rosetta (from the Mac OS X optional CD) to run sox." # just in case there are encoding problems with localisation
show_warning(sox_formats.replace("sox.","some of the utilities Gradint uses.")+" Otherwise expect problems!") # (TODO need to check espeak separately in case they've compiled it x86, see in synth.py)
# TODO document a way to install Rosetta without the CD? (e.g. downloading a PowerPC-only Abiword binary seems to do it)
got_qtplay = 0
else: show_warning("SOX found, but it can't handle WAV files. Ubuntu users please install libsox-fmt-all.")
if macsound: got_afplay = got_program("afplay") # 10.5+, use in preference to the bundled qtplay which requires PowerPC or Rosetta
def sox_check():
global sox_8bit, sox_16bit, sox_ignoreLen, sox_signed, sox_formats
sox_formats=os.popen("sox --help 2>&1").read() # NOT .lower() yet
sf2 = ' '.join(sox_formats.lower().split())
if sf2.startswith("sox: sox v"):
if sf2[10]==' ': soxMaj=15 # guess (broken HomeBrew install)
else: soxMaj = intor0(sf2[10:sf2.index('.')])
else: soxMaj=0
if soxMaj>=14:
if soxMaj==14 and sf2[13]<'3': pass
else: sox_ignoreLen = "|sox --ignore-length -t wav - -t wav - 2>/dev/null"
if soxMaj==14 and sf2[13]<'4': sox_8bit, sox_16bit = "-1", "-2" # see comment above
else: sox_8bit, sox_16bit, sox_signed = "-b 8", "-b 16", "-e signed-integer" # TODO: check if 14.3 accepts these also (at least 14.4 complains -2 etc is deprecated)
return sf2.find("wav")>=0, sf2.find("mp3")>=0
gotSox,soxMp3 = sox_check()
if macsound:
if not gotSox and not os.system("mv sox-14.4.2 sox && rm sox.README"): gotSox,soxMp3 = sox_check() # see if that one works instead (NB must use os.system here: our system() has not yet been defined)
if not gotSox and got_program("sox"):
if macsound or cygwin: xtra=""
else: xtra=". Ubuntu users please install libsox-fmt-all."
show_warning("SoX found but can't handle WAV, so you won't be able to write lessons to files for later"+xtra)
else: gotSox = got_program("sox")
wavPlayer_override = not (not wavPlayer)
if winsound or mingw32:
# in winsound can use PlaySound() but better not use it for LONGER sounds - find a playProgram anyway for those (see self.length condition in play() method below)
# in winsound can use PlaySound() but better not use it for LONGER sounds - find a wavPlayer anyway for those (see self.length condition in play() method below)
# (TODO sndrec32.exe loads the whole of the file into memory before playing. but mplayer/mplay32 sometimes halts on a yes/no dialogue about settings, and Media Player can't take files on command line so needs correct file association and executable permissions. And many of the freeware command-line players have the same limitations as winsound.)
# TODO now that we (usually) have tkSnack bundled with the Windows version, can we try that also (with file=) before sndrec32?
if fileExists(os.environ.get("windir","C:\\Windows")+"\\system32\\sndrec32.exe"): playProgram = "start /min sndrec32 /play /close" # TODO could also use ShellExecute or some other utility to make it completely hidden
if not wavPlayer and fileExists(os.environ.get("windir","C:\\Windows")+"\\system32\\sndrec32.exe"): wavPlayer = "start /min sndrec32 /play /close" # TODO could also use ShellExecute or some other utility to make it completely hidden
elif unix and not macsound:
sox_type = "-t ossdsp -s "+sox_16bit # (we will check that sox can do ossdsp below) (always specify 16-bit because if we're adjusting the volume of 8-bit wav's then we could lose too many bits in the adjustment unless we first convert to 16-bit)
sox_type = "-t ossdsp "+sox_signed+" "+sox_16bit # (we will check that sox can do ossdsp below) (always specify 16-bit because if we're adjusting the volume of 8-bit wav's then we could lose too many bits in the adjustment unless we first convert to 16-bit)
if not soundVolume==1: sox_effect=" vol "+str(soundVolume)
if sox_effect and not gotSox:
show_warning("Warning: trying to adjust soundVolume when 'sox' is not on the system might not work")
# (need a warning here, because if using 'aplay' then sox o/p is 2>/dev/null (see below) so a missing sox won't be obvious)
if sox_formats.find("alsa")>=0 and isDirectory("/dev/snd"):
sox_type=sox_type.replace("ossdsp","alsa")
oss_sound_device = " " # older versions could take "hw:0,0" but just leave at -t alsa now?
if not oss_sound_device:
dsps_to_check = []
if sox_formats.find("ossdsp")>-1: dsps_to_check += ["/dev/sound/dsp","/dev/dsp"]
if sox_formats.find("sunau")>-1: dsps_to_check += ["/dev/audio"]
if sox_formats.find("ossdsp")>=0:
dsps_to_check += ["/dev/sound/dsp","/dev/dsp"]
if sox_formats.find("sunau")>=0: dsps_to_check += ["/dev/audio"]
for dsp in dsps_to_check:
if fileExists_stat(dsp):
oss_sound_device = dsp
if dsp=="/dev/audio": sox_type="-t sunau -s "+sox_16bit
if dsp=="/dev/audio": sox_type="-t sunau "+sox_signed+" "+sox_16bit
break
if sox_formats.find("-q")>-1: sox_type="-q "+sox_type
# Try to find playProgram (and maybe mpg123, for use if no madplay or mp3-playing playProgram)
if oss_sound_device and not cygwin and gotSox: playProgram = "sox"
elif cygwin and got_program("sndrec32"): # XP's Sound Recorder (vista's is called soundreorder.exe but won't do this) (+ don't have to worry about the >2G memory bug as not applicable to playing)
playProgram = "sndrec32 /play /close" # prefer this to esdplay due to cygwin esdplay delaying every other call and being asynchronous
if got_program("cmd"): playProgram = "cmd /c start /min "+playProgram # TODO could also use ShellExecute or some other utility to make it completely hidden
elif cygwin and oss_sound_device and got_program("play"): playProgram = "play" # this is part of sox, but it'll be the sox installed in cygwin rather than any sox.exe in gradint directory from Windows version
else:
if sox_formats.find("-q")>=0: sox_type="-q "+sox_type
if not wavPlayer:
if oss_sound_device and not cygwin and gotSox: wavPlayer = "sox"
elif cygwin and got_program("sndrec32"): # XP's Sound Recorder (vista's is called soundreorder.exe but won't do this) (+ don't have to worry about the >2G memory bug as not applicable to playing)
wavPlayer = "sndrec32 /play /close" # prefer this to esdplay due to cygwin esdplay delaying every other call and being asynchronous
if got_program("cmd"): wavPlayer = "cmd /c start /min "+wavPlayer # TODO could also use ShellExecute or some other utility to make it completely hidden
elif cygwin and oss_sound_device and got_program("play"): wavPlayer = "play" # this is part of sox, but it'll be the sox installed in cygwin rather than any sox.exe in gradint directory from Windows version
else:
otherPrograms = ["aplay","esdplay","auplay","wavp","playmus","mplayer","playwave","alsaplayer"] # alsaplayer is a last resort because the text-mode version may or may not be installed; hopefully they'll have alsa-utils installed which includes 'aplay'. (playwave has been known to clip some files)
for otherProgram in otherPrograms:
if got_program(otherProgram):
playProgram = otherProgram
wavPlayer = otherProgram
break
if not cygwin and not madplay_program:
if not cygwin and not mp3Player:
for mpg in ["mpg123","mpg321","mad123","mplayer"]:
if got_program(mpg):
mpg123 = mpg ; break
if not playProgram and not outputFile: show_warning("Warning: no known "+cond(mpg123,"non-MP3 ","")+"sound-playing command found on this system\n (checked for sox with /dev/dsp etc, also checked for play "+" ".join(otherPrograms)+")\n - expect problems with realtime lessons"+cond(mpg123," unless everything is MP3",""))
may_need_mp3_warning = ((playProgram or winsound or riscos_sound or mingw32) and not (mpg123 or gotSox or madplay_program))
mp3Player = mpg ; break
if not wavPlayer and not outputFile: show_warning("Warning: no known "+cond(mp3Player,"non-MP3 ","")+"sound-playing command found on this system\n (checked for sox with /dev/dsp etc, also checked for play "+" ".join(otherPrograms)+")\n - expect problems with realtime lessons"+cond(mp3Player," unless everything is MP3",""))
may_need_mp3_warning = ((wavPlayer or winsound or riscos_sound or mingw32) and not (mp3Player or gotSox))
def maybe_warn_mp3():
global may_need_mp3_warning
if may_need_mp3_warning:
......@@ -185,7 +208,7 @@ sox_same_endian = sox_little_endian = ""
if gotSox and unix:
# should only have to run this test if macsound (don't bother on NSLU2's etc):
# (wav is little-endian, so if it doesn't pass the string through then it interpreted the i/p as big-endian)
if macsound and os.popen('echo "This is a test" | sox -t raw -r 8000 '+sox_16bit+' -s -c 1 - -t wav - 2>/dev/null').read().find("This is a test")==-1:
if macsound and readB(os.popen('echo "This is a test" | sox -t raw -r 8000 '+sox_16bit+' '+sox_signed+' -c 1 - -t wav - 2>/dev/null')).find(B("This is a test"))==-1:
sox_little_endian = " -x"
if not big_endian: sox_same_endian = " -x"
elif big_endian: sox_little_endian = " -x"
......@@ -194,35 +217,47 @@ def changeToDirOf(file,winsound_also=0):
# used before running a non-cygwin program in the cygwin environment (due to directory differences etc)
# and (with winsound_also) before running a program on Windows without needing to quote the filename (e.g. because some versions of eSpeak won't write to a quoted wav file when called from popen). Note windows os.chdir DOES change the drive also. Use this only if filename will not contain special characters (e.g. should be able to use it for temp files).
# NB if winsound_also is set, will return file "quoted" on other systems (so can set winsound_also and not worry about whether or not it should be quoted)
file = S(file)
if winCEsound and not ' ' in file: return file # don't need to quote
elif winsound_also and not (winsound or mingw32 or cygwin): return '"'+file+'"'
elif (cygwin or ((winsound or mingw32) and winsound_also)) and os.sep in file:
os.chdir(file[:file.rfind(os.sep)])
return file[file.rfind(os.sep)+1:]
else: return file
elif riscos_sound:
if file.find("..")==-1:
c = os.getcwd()
return c[c.index('$'):]+os.sep+file
return file
def system(cmd):
# Don't call os.system for commands like sound playing, because if you do then any Control-C interrupt will go to that rather than to gradint as we want, and it will pop up a large blank console window in Windows GUI-only version
if riscos_sound or not hasattr(os,"popen"): return os.system(cmd) # no popen
if unix and ';' in cmd: cmd='/bin/bash -c "'+cmd.replace('\\','\\\\').replace('"','\\"')+'"' # not /bin/sh if it's complex
if riscos_sound and type("")==type(u""): # work around memory issues with os.system() in RISC OS Python 3.8 (not needed if the command is a module rather than a program)
import subprocess
return subprocess.Popen(S(cmd).replace('"','').split()).wait() # must be S() not B() here (TODO: what if space in a filename? TODO: catch swi.error and say please install TaskRunner module?)
if not hasattr(os,"popen"): return os.system(cmd)
if unix and (';' in cmd or '<' in cmd): cmd='/bin/bash -c "'+cmd.replace('\\','\\\\').replace('"','\\"').replace('$','\\$')+'"' # not /bin/sh if it's complex
try: r=os.popen(cmd)
except: return os.system(cmd) # too many file descriptors open or something
r.read() ; return r.close()
signal=0
if unix:
# Unix: make sure "kill" on gradint's pid includes the players:
# Unix: make sure "kill" on gradint's pid includes the players:
try:
os.setpgrp()
import signal
def siggrp(sigNo,*args):
os.killpg(os.getpgrp(),sigNo)
os.abort() # in case still here
signal.signal(sigNo,signal.SIG_IGN)
os.killpg(os.getpgrp(),sigNo) # players etc
raise KeyboardInterrupt # clean up, rm tempfiles etc
signal.signal(signal.SIGTERM,siggrp)
else: signal=0
except: pass
# Event(len) gives a pause of that length
# SampleEvent extends this to actually play something:
def soundFileType(file):
if extsep in file: return file[file.rindex(extsep)+1:].lower()
file,sep = B(file),B(extsep)
if sep in file: return S(file[file.rindex(sep)+1:].lower())
else: return "wav"
def lessonIsTight(): return maxLenOfLesson <= 10*60 * min(1.8,max(1,maxNewWords/5.0)) # ?
......@@ -237,8 +272,8 @@ class SampleEvent(Event):
if not lessonIsTight() and not useExactLen: approxLen = math.ceil(self.exactLen) # (if <=10min in lesson, don't round up to next second because we want a tighter fit)
Event.__init__(self,approxLen)
def __repr__(self):
if use_unicode_filenames: return self.file.encode('utf-8')
else: return self.file
if use_unicode_filenames: return self.file.encode('utf-8') # winCEsound, will be Python 2
else: return S(self.file)
def __del__(self):
if hasattr(self,"isTemp"):
import time,os # in case gc'd
......@@ -246,65 +281,80 @@ class SampleEvent(Event):
try: return os.unlink(self.file)
except: time.sleep(0.2) # may have taken extra time for the player to load
if not fileExists_stat(self.file): break # unlink suceeded and still threw exception ??
def makesSenseToLog(self): return not self.file.startswith(promptsDirectory) # (NB "not prompts" doesn't necessarily mean it'll be a sample - may be a customised additional comment)
def makesSenseToLog(self):
if hasattr(self,"is_prompt"): return not self.is_prompt # e.g. prompt from synth-cache
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 and not hasattr(self,"isTemp"): open(self.file) # ensure ready for reading
if paranoid_file_management:
if not hasattr(self,"isTemp"): open(self.file) # ensure ready for reading
fileType=soundFileType(self.file)
if soundCollector: soundCollector.addFile(self.file,self.exactLen)
elif appuifw:
fname = self.file
if not fname[1]==":": fname=os.getcwd()+cwd_addSep+fname # must be full drive:\path
sound = audio.Sound.open(fname)
if not B(fname[1:2])==B(":"): fname=B(os.getcwd()+cwd_addSep)+B(fname) # must be full drive:\path
sound = audio.Sound.open(ensure_unicode(fname))
sound.play()
try: time.sleep(self.length) # TODO or exactLen?
finally: sound.stop()
sound.close() # (probably not worth keeping it open for repeats - there may be a limit to how many can be open)
return
elif fileType=="mp3" and madplay_program and not macsound and not playProgram=="aplay":
elif android:
fname = self.file
if not B(fname[0])==B('/'): fname=B(os.getcwd()+'/')+fname
android.mediaPlay("file://"+S(fname))
return
elif fileType=="mp3" and madplay_path and mp3Player==madplay_path and not macsound and not wavPlayer=="aplay":
oldcwd = os.getcwd()
play_error = system(madplay_program+' -q -A '+str(soundVolume_dB)+' "'+changeToDirOf(self.file)+'"') # using changeToDirOf because on Cygwin it might be a non-cygwin madplay.exe that someone's put in the PATH. And keeping the full path to madplay.exe because the PATH may contain relative directories.
play_error = system(mp3Player+' -q -A '+str(soundVolume_dB)+' "'+changeToDirOf(self.file)+'"') # using changeToDirOf because on Cygwin it might be a non-cygwin madplay.exe that someone's put in the PATH. And keeping the full path to madplay.exe because the PATH may contain relative directories.
os.chdir(oldcwd)
return play_error
elif winCEsound and fileType=="mp3":
# we can handle MP3 on WinCE by opening in Media Player. Too bad it ignores requests to run minimized.
fname = self.file
if not fname[0]=="\\": fname=os.getcwd()+cwd_addSep+fname # must be full path
r=not ctypes.cdll.coredll.ShellExecuteEx(ctypes.byref(ShellExecuteInfo(60,File=u""+fname)))
if not B(fname[0])==B("\\"): fname=os.getcwd()+cwd_addSep+fname # must be full path
r=not ctypes.cdll.coredll.ShellExecuteEx(ctypes.byref(ShellExecuteInfo(60,File=ensure_unicode(fname))))
time.sleep(self.length) # exactLen may not be enough
elif (winsound and not (self.length>10 and playProgram)) or winCEsound: # (don't use winsound for long files if another player is available - it has been known to stop prematurely)
elif (winsound and not (self.length>10 and wavPlayer)) or winCEsound: # (don't use winsound for long files if another player is available - it has been known to stop prematurely)
if fileType=="mp3": file=theMp3FileCache.decode_mp3_to_tmpfile(self.file)
else: file=self.file
try:
if winsound: winsound.PlaySound(file,winsound.SND_FILENAME)
else: # winCEsound
fname = self.file
if not fname[0]=="\\": fname=os.getcwd()+cwd_addSep+fname # must be full path
ctypes.cdll.coredll.sndPlaySoundW(u""+fname,1) # 0=sync 1=async
if not B(fname[0])==B("\\"): fname=os.getcwd()+cwd_addSep+fname # must be full path
ctypes.cdll.coredll.sndPlaySoundW(ensure_unicode(fname),1) # 0=sync 1=async
time.sleep(self.exactLen) # if async. Async seems to be better at avoiding crashes on some handhelds.
except RuntimeError: return 1
elif macsound and got_qtplay:
try: unicode(self.file,"ascii")
except UnicodeDecodeError: # qtplay can't always handle non-ASCII
t=os.tempnam()+self.file[self.file.rindex(extsep):]
open(t,"w").write(open(self.file).read())
ret=system("qtplay \"%s\"" % (t,))
os.remove(t)
return ret
return system("qtplay \"%s\"" % (self.file,))
elif macsound:
if got_afplay: player="afplay"
else: player="qtplay"
try: unicode(self.file,"ascii")
except UnicodeDecodeError: # Mac command line can't always handle non-ASCII
t=os.tempnam()+self.file[self.file.rindex(extsep):]
write(t,open(self.file).read())
ret=system(player+" \"%s\"" % (t,))
os.remove(t)
return ret
return system(player+" \"%s\"" % (S(self.file),))
elif riscos_sound:
if fileType=="mp3": file=theMp3FileCache.decode_mp3_to_tmpfile(self.file) # (TODO find a RISC OS program that can play the MP3s directly?)
if fileType=="mp3":
if amplay: return os.system("AMPlay \"%s\"" % (S(self.file),)) # module call, so no need for subprocess
file=theMp3FileCache.decode_mp3_to_tmpfile(self.file)
else: file=self.file
system("PlayIt_Play \"%s\"" % (file,))
elif playProgram.find('sndrec32')>-1:
global playit_loaded
try: playit_loaded
except: playit_loaded=not os.system("Run PlayIt:Ensure -e 1.68") # relies on PlayIt$Path having been set by !PlayIt.!Boot when seen by filer
os.system("PlayIt_Play \"%s\"" % (S(file),)) # module call, so no need for subprocess; TODO: not ARMv7 compatible apparently (crashes on Pi400, sox does also, AMPlay can't play wav), saying "use mp3" in index.html for now
elif wavPlayer.find('sndrec32')>=0:
if fileType=="mp3": file=theMp3FileCache.decode_mp3_to_tmpfile(self.file)
else: file=self.file
oldDir = os.getcwd()
t=time.time()
os.system(playProgram+' "'+changeToDirOf(file)+'"') # don't need to call our version of system() here
if playProgram.find("start")>-1: time.sleep(max(0,self.length-(time.time()-t))) # better do this - don't want events overtaking each other if there are delays. exactLen not always enough. (but do subtract the time already taken, in case command extensions have been disabled and "start" is synchronous.)
os.system(wavPlayer+' "'+changeToDirOf(file)+'"') # don't need to call our version of system() here
if wavPlayer.find("start")>=0: time.sleep(max(0,self.length-(time.time()-t))) # better do this - don't want events overtaking each other if there are delays. exactLen not always enough. (but do subtract the time already taken, in case command extensions have been disabled and "start" is synchronous.)
os.chdir(oldDir)
elif fileType=="mp3" and mpg123 and not sox_effect and not (playProgram=="aplay" and madplay_program): return system(mpg123+' "'+self.file+'"')
elif playProgram=="sox":
elif fileType=="mp3" and mp3Player and not sox_effect and not (wavPlayer=="aplay" and mp3Player==madplay_path): return system(mp3Player+' "'+S(self.file)+'"')
elif wavPlayer=="sox" and (soxMp3 or not fileType=="mp3"):
# To make it more difficult:
# sox v12.x (c. 2001) - bug when filenames contain 2 spaces together, and needs input from re-direction in this case
# sox 14.0 on Cygwin - bug when input is from redirection, unless using cat | ..
......@@ -313,7 +363,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?
......@@ -322,18 +372,20 @@ class SampleEvent(Event):
if timeDiff==0 and self.exactLen < 1.5: return 0 # (we'll let that one off for systems that have limited clock precision)
if not app: show_info("play didn't take long enough - maybe ") # .. problem playing sound
return 1
elif playProgram=="aplay" and ((not fileType=="mp3") or madplay_program or gotSox):
if madplay_program and fileType=="mp3": return system(madplay_program+' -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 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)+' "'+S(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 "'+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+'"')
else: return system('aplay -q "'+S(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
# TODO: auplay can take -volume (int 0-100) and stdin; check esdplay capabilities also
elif fileType=="mp3" and mpg123: return system(mpg123+' "'+self.file+'"')
elif playProgram:
if fileType=="mp3" and not playProgram=="mplayer": file=theMp3FileCache.decode_mp3_to_tmpfile(self.file)
else: file=self.file
return system(playProgram+' "'+file+'"')
elif fileType=="mp3" and mp3Player and not sox_effect: return system(mp3Player+' "'+S(self.file)+'"')
elif wavPlayer:
if fileType=="mp3" and not wavPlayer=="mplayer": file=theMp3FileCache.decode_mp3_to_tmpfile(self.file)
else: file=S(self.file)
if sox_effect and wavPlayer.strip().endswith("<"): return system('sox "%s" -t wav - %s | %s' % (file,sox_effect,wavPlayer.strip()[:-1]))
return system(wavPlayer+' "'+file+'"')
elif fileType=="mp3" and mp3Player: return system(mp3Player+' "'+S(self.file)+'"') # ignore sox_effect
else: show_warning("Don't know how to play \""+self.file+'" on this system')
br_tab=[(0 , 0 , 0 , 0 , 0),
......@@ -356,19 +408,25 @@ def rough_guess_mp3_length(fname):
try:
maybe_warn_mp3() # in case there's no mp3 player
# (NB this is only a rough guess because it doesn't support VBR
# and doesn't even check all sync bits or scan beyond 128 bytes.
# It should be fairly quick though.)
head=open(fname).read(128)
i=head.find('\xFF')
b=ord(head[i+1])
# and doesn't even check all sync bits. It should be fairly quick though.)
o = open(fname,"rb") ; i = -1
while True:
head=o.read(512)
if len(head)==0: raise IndexError # read the whole file and not found a \xFF byte??
i=head.find(LB('\xFF'))
if i==-1: continue
if i+2 < len(head): head += o.read(3)
o.seek(o.tell()-len(head)+i+2) ; b=ord(head[i+1:i+2])
if b >= 0xE0: break # valid frame header starts w. 11 1-bits (not just 8: some files with embedded images could throw that off)
s = o.tell() ; o.close()
layer = 4-((b&6)>>1)
if b&24 == 24: # bits are 11 - MPEG version is 1
column = layer-1 # MPEG 1 layer 1, 2 or 3
elif layer==1: column = 3 # MPEG 2+ layer 1
else: column = 4 # MPEG 2+ layer 2+
bitrate = br_tab[ord(head[i+2])>>4][column]
bitrate = br_tab[ord(head[i+2:i+3])>>4][column]
if bitrate==0: bitrate=48 # reasonable guess for speech
return filelen(fname)*8.0/(bitrate*1000)
return (filelen(fname)-s)*8.0/(bitrate*1000)
except IndexError: raise Exception("Invalid MP3 header in file "+repr(fname))
def filelen(fname):
......@@ -377,15 +435,30 @@ def filelen(fname):
return fileLen
def lengthOfSound(file):
if file.lower().endswith(dotmp3): return rough_guess_mp3_length(file)
if B(file).lower().endswith(B(dotmp3)): return rough_guess_mp3_length(file)
else: return pcmlen(file)
if type("")==type(u""): # Python 3
import wave
def swhat(file):
if file.lower().endswith(os.extsep+"wav"):
o = wave.open(file,'rb')
return "wav",o.getframerate(),o.getnchannels(),o.getnframes(),8*o.getsampwidth()
else: # fallback non-WAV
import sndhdr # before Python 3.13
return sndhdr.what(file)
else: # Python 2
import sndhdr
swhat = sndhdr.what
def pcmlen(file):
header = sndhdr.what(file)
if not header: raise IOError("Problem opening file '%s'" % (file,))
header = swhat(file)
(wtype,wrate,wchannels,wframes,wbits) = header
divisor = wrate*wchannels*wbits/8 # do NOT optimise with (wbits>>3), because wbits could be 4
if not divisor: raise IOError("Cannot parse sample format of '%s'" % (file,))
if android:
if wrate==6144: # might be a .3gp from android_recordFile
d = open(file).read()
if 'mdat' in d: return (len(d)-d.index('mdat'))/1500.0 # this assumes the bitrate is roughly the same as in my tests, TODO figure it out properly
divisor = wrate*wchannels*int(wbits/8) # do NOT optimise with (wbits>>3), because wbits could be 4
if not divisor: raise IOError("Cannot parse sample format of '%s': %s" % (file,repr(header)))
return (filelen(file) - 44.0) / divisor # 44 is a typical header length, and .0 to convert to floating-point
##########################################################
......@@ -394,27 +467,32 @@ class SoundCollector(object):
def __init__(self):
self.rate = 44100 # so ok for oggenc etc
if out_type=="raw" and write_to_stdout: self.o=sys.stdout
elif out_type=="ogg": self.o=os.popen("oggenc -o \"%s\" -r -C 1 -q 0 -" % (cond(write_to_stdout,"-",outputFile),),"wb") # oggenc assumes little-endian, which is what we're going to give it
elif out_type=="aac": self.o=os.popen("faac -b 32 -P%s -C 1 -o \"%s\" -" % (cond(big_endian,""," -X"),cond(write_to_stdout,"-",outputFile)),"wb") # (TODO check that faac on big-endian needs the -X removed when we're giving it little-endian. It SHOULD if the compile is endian-dependent.)
elif out_type=="mp3": self.o=os.popen("lame -r%s%s -m m --vbr-new -V 9 - \"%s\"" % (lame_endian_parameters(),lame_quiet(),cond(write_to_stdout,"-",outputFile)),"wb") # (TODO check that old versions of lame won't complain about the --vbr-new switch. And some very old hardware players may insist on MPEG-1 rather than MPEG-2, which would need different parameters)
elif out_type=="ogg": self.o=os.popen(oggenc()+" -o \"%s\" -r -C 1 -q 0 -" % (cond(write_to_stdout,"-",outputFile),),popenWB) # oggenc assumes little-endian, which is what we're going to give it
elif out_type=="aac":
if got_program("neroAacEnc"): self.o=os.popen("sox %s - -t wav - | neroAacEnc -br 32000 -if - -of \"%s\"" % (self.soxParams(),cond(write_to_stdout,"-",outputFile)),popenWB) # (TODO optionally use -2pass, on a physical input file like the afconvert code)
else: self.o=os.popen("faac -b 32 -P%s -C 1 -o \"%s\" -" % (cond(big_endian,""," -X"),cond(write_to_stdout,"-",outputFile)),popenWB) # (TODO check that faac on big-endian needs the -X removed when we're giving it little-endian. It SHOULD if the compile is endian-dependent.)
elif out_type=="mp3": self.o=os.popen("lame -r%s%s -m m --vbr-new -V 9 - \"%s\"" % (lame_endian_parameters(),lame_quiet(),cond(write_to_stdout,"-",outputFile)),popenWB) # (TODO check that old versions of lame won't complain about the --vbr-new switch. And some very old hardware players may insist on MPEG-1 rather than MPEG-2, which would need different parameters)
# Older versions of gradint used BladeEnc, with these settings: "BladeEnc -br 48 -mono -rawmono STDIN \"%s\"", but lame gives much smaller files (e.g. 3.1M instead of 11M) - it handles the silences more efficiently for a start).
# Typical file sizes for a 30-minute lesson: OGG 2.7M, MP3 3.1M, MP2 3.4M, AAC 3.7M (all +/- at least 0.1M), WAV 152M
# Size regression would probably also happen if we used shineenc or comprec: fixed-point, so likely faster than floating-point lame on low-end ARM etc, but won't reduce bitrate for silences.
# Anyway, on a Raspberry 1 B+, oggenc runs ~5x faster than lame (probably due to the type of floating-point in use), and speexenc 10-15x faster than lame, and both produce smaller files. So if MP3-compatibility is not required (and you're not concerned about further quality loss of any samples already stored in mp3 format) then ogg or spx would be a better choice.
# Typical file sizes for a 30-minute lesson: OGG 2.7M, neroAacEnc 3.0M at 32000 (you might be able to put up with 1.8M at 18000 or 2.2M at 24000), MP3 3.1M, MP2 3.4M, faac 3.7M, WAV 152M
# TODO try AAC+? aacplusenc wavfile(or -) aacfile kbits, 10,12,14,18,20,24,32,40 (or 48 for stereo), but will need a player to test it
# (mp2 could possibly be made a bit smaller by decreasing the -5, but don't make it as low as -10)
elif out_type=="spx":
self.rate = 32000 # could also use 16000 and -w, or even 8000, but those are not so good for language learning
self.o=os.popen("speexenc -u --vbr --dtx - "+cond(write_to_stdout,"-",outputFile),"wb") # and write 16-bit little-endian mono
self.o=os.popen("speexenc -u --vbr --dtx - "+cond(write_to_stdout,"-",outputFile),popenWB) # and write 16-bit little-endian mono
elif out_type=="mp2":
self.rate = 22050
self.o=os.popen("toolame %s -s %f -v -5 -p 4 -m m - \"%s\"" % (cond(big_endian,"-x",""),self.rate/1000.0,cond(write_to_stdout,"-",outputFile)),"wb") # TODO check that toolame compiled on big-endian architectures really needs -x to accept little-endian input
self.o=os.popen("toolame %s -s %f -v -5 -p 4 -m m - \"%s\"" % (cond(big_endian,"-x",""),self.rate/1000.0,cond(write_to_stdout,"-",outputFile)),popenWB) # TODO check that toolame compiled on big-endian architectures really needs -x to accept little-endian input
elif not out_type=="raw":
if out_type=="wav": self.rate=22050 # try not to take TOO much disk space
self.o=os.popen("sox %s - -t %s \"%s\"" % (self.soxParams(),out_type,cond(write_to_stdout,"-",outputFile)),"wb")
self.o=os.popen("sox %s - -t %s \"%s\"" % (self.soxParams(),out_type,cond(write_to_stdout,"-",outputFile)),popenWB)
else: self.o = open(outputFile,"wb")
self.theLen = 0
self.silences = []
def soxParams(self):
# Have 16-bit mono, signed, little-endian
return ("-t raw "+sox_16bit+" -s -r %d -c 1" % (self.rate,))+sox_little_endian
return ("-t raw "+sox_16bit+" "+sox_signed+" -r %d -c 1" % (self.rate,))+sox_little_endian
def tell(self):
# How many seconds have we had? (2 because 16-bit)
return 1.0*self.theLen/self.rate/2
......@@ -425,17 +503,17 @@ class SoundCollector(object):
sampleNo = int(0.5+seconds*self.rate)
if not sampleNo: sampleNo=1 # so don't lock on rounding errors
byteNo = sampleNo*2 # since 16-bit
outfile_writeBytes(self.o,"\0"*byteNo)
outfile_writeBytes(self.o,chr(0)*byteNo)
self.theLen += byteNo
def addFile(self,file,length):
def addFile(self,file,length): # length ignored in this version
fileType=soundFileType(file)
if fileType=="mp3": file,fileType = theMp3FileCache.decode_mp3_to_tmpfile(file),"wav" # in case the system needs madplay rather than sox
if fileType=="mp3": file,fileType = theMp3FileCache.decode_mp3_to_tmpfile(file),"wav" # in case the system needs madplay etc rather than sox
if riscos_sound:
os.system("sox -t %s \"%s\" %s tmp0" % (fileType,file,self.soxParams()))
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)),"rb")
else: handle = os.popen(("cat \"%s\" | sox -t %s - %s -" % (file,fileType,self.soxParams())),"rb")
self.theLen += outfile_writeFile(self.o,handle)
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 -" % (S(file),fileType,self.soxParams())),popenRB)
self.theLen += outfile_writeFile(self.o,handle,file)
if riscos_sound:
handle.close() ; os.unlink("tmp0")
def addBeeps(self,gap):
......@@ -444,9 +522,9 @@ class SoundCollector(object):
t1 = self.tell()
self.addSilence(betweenBeeps/2.0)
if riscos_sound:
os.system(beepCmd() % (self.soxParams(),"tmp0"))
system(beepCmd(self.soxParams(),"tmp0"))
data=read("tmp0") ; os.unlink("tmp0")
else: data=os.popen((beepCmd() % (self.soxParams(),"-")),"rb").read()
else: data=readB(os.popen(beepCmd(self.soxParams(),"-"),popenRB))
outfile_writeBytes(self.o,data)
self.theLen += len(data)
self.addSilence(betweenBeeps/2.0)
......@@ -465,31 +543,38 @@ class SoundCollector(object):
if not app: show_info("Lengths of silences: %s (total %s)\n" % (self.silences,ttl))
if not outputFile=="-": outfile_close(self.o)
def outfile_writeBytes(o,bytes):
try: o.write(bytes)
try: writeB(o,bytes)
except IOError: outfile_write_error()
def outfile_close(o):
try: o.close()
except IOError: outfile_write_error()
def outfile_writeFile(o,handle):
def outfile_writeFile(o,handle,filename):
data,theLen = 1,0
while data:
data = handle.read(102400)
data = readB(handle,102400)
outfile_writeBytes(o,data)
theLen += len(data)
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
global cached_oggenc
try: return cached_oggenc
except: pass
if got_program("oggenc"): cached_oggenc = "oggenc"
else: cached_oggenc = "oggenc2"
return cached_oggenc
def lame_endian_parameters():
# The input to lame will always be little-endian regardless of which architecture we're on and what kind of sox build we're using.
# lame 3.97 has -x (swap endian) parameter, needed with little-endian i/p on little-endian architecture
# lame 3.98+ has changed the default of -x and introduced explicit --big-endian and --little-endian.
# (Note: None of this would be needed if we give lame a WAV input, as email-lesson.sh does. But lame 3.97 on Windows faults on wav inputs.)
lameVer = os.popen("lame --version").read()
if "version " in lameVer:
if lameVer.find("version ")>=0:
lameVer = lameVer[lameVer.index("version "):].split()[1]
if lameVer and '.' in lameVer and (lameVer[0]>'3' or intor0(lameVer[2:4])>97):
if lameVer and '.' in lameVer and (intor0(lameVer[:lameVer.index('.')])>3 or digitPrefix(lameVer[lameVer.index('.')+1:])>97):
# Got 3.98+ - explicitly tell it the endianness (but check for alpha releases first - some of them don't deal with either this or the 3.97 behaviour very well)
if "alpha" in lameVer and lameVer[0]=="3" and intor0(lameVer[2:4])==98: show_warning("Warning: You have a 3.98 alpha release of LAME.\nIf the MP3 file is white noise, try a different LAME version.")
if lameVer.find("alpha")>=0 and lameVer[0]=="3" and intor0(lameVer[2:4])==98: show_warning("Warning: You have a 3.98 alpha release of LAME.\nIf the MP3 file is white noise, try a different LAME version.")
return " --little-endian"
# otherwise fall-through to older lame behaviour:
if big_endian: return "" # TODO are we sure we don't need -x on lame 3.97 PPC as well?
......@@ -504,11 +589,15 @@ beepType = 0
beepCmds = ["sox -t nul - %s %s synth trapetz 880 trim 0 0:0.05",
"sox -t nul - %s %s synth sine 440 trim 0 0:0.05"]*3+["sox -t nul - %s %s synth trapetz 440 trim 0 0:0.05",
"sox -t nul - %s %s synth sine 440 trim 0 0:0.05"]*2+["sox -t nul - %s %s synth 220 trim 0 0:0.05"]
def beepCmd():
def beepCmd(soxParams,fname):
global beepType
r = beepCmds[beepType]
beepType += 1
if beepType==len(beepCmds): beepType=0
if unix:
# not all versions of sox support -t nul; /dev/zero is safer on Unix
r=r.replace("-t nul -","%s /dev/zero" % (soxParams,))
r = r % (soxParams,fname)
return r
# -----------------------------------------------------
......@@ -517,14 +606,14 @@ def beepCmd():
class ShSoundCollector(object):
def __init__(self):
self.file2command = {}
self.commands = ["C() { echo -n $1% completed $'\r' 1>&2;}"]
self.commands = ["C() { echo -n $1% completed $'\r' >&2;}"]
self.seconds = self.lastProgress = 0
if write_to_stdout: self.o=sys.stdout
else: self.o = open(outputFile,"wb")
start = """#!/bin/bash
if echo "$0"|grep / >/dev/null; then export S="$0"; else export S=$(which "$0"); fi
export P="-t raw %s -s -r 44100 -c 1"
tail -1 "$S" | bash\nexit\n""" % (sox_16bit,) # S=script P=params for sox (ignore endian issues because the wav header it generates below will specify the same as its natural endian-ness)
if echo "$0"|grep / >/dev/null; then S="$0"; else S=$(which "$0"); fi
P="-t raw %s %s -r 44100 -c 1"
tail -1 "$S" | bash\nexit\n""" % (sox_16bit,sox_signed) # S=script P=params for sox (ignore endian issues because the wav header it generates below will specify the same as its natural endian-ness)
outfile_writeBytes(self.o,start)
self.bytesWritten = len(start) # need to keep a count because it might be stdout
self.commands.append("sox $P - -t wav - </dev/null 2>/dev/null") # get the wav header with unspecified length
......@@ -542,7 +631,7 @@ tail -1 "$S" | bash\nexit\n""" % (sox_16bit,) # S=script P=params for sox (ignor
while gap > betweenBeeps+0.05:
t1 = self.tell()
self.addSilence(betweenBeeps/2.0)
self.commands.append(beepCmd() % ("$P","-"))
self.commands.append(beepCmd("$P","-"))
self.seconds += 0.05
self.addSilence(betweenBeeps/2.0)
gap -= (self.tell()-t1)
......@@ -550,11 +639,11 @@ tail -1 "$S" | bash\nexit\n""" % (sox_16bit,) # S=script P=params for sox (ignor
def addFile(self,file,length):
fileType=soundFileType(file)
self.seconds += length
if not file in self.file2command:
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","rb") # 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)
offset, length = self.bytesWritten, outfile_writeFile(self.o,handle,file)
self.bytesWritten += length
# dd is more efficient when copying large chunks - try to align to 1k
first_few_bytes = min(length,(1024-(offset%1024))%1024)
......@@ -574,7 +663,7 @@ tail -1 "$S" | bash\nexit\n""" % (sox_16bit,) # S=script P=params for sox (ignor
def finished(self):
if outputFile_appendSilence: self.addSilence(outputFile_appendSilence,False)
outfile_writeBytes(self.o,"\n") # so "tail" has a start of a line
self.commands.append("C 100;echo 1>&2;exit")
self.commands.append("C 100;echo >&2;exit")
for c in self.commands: outfile_writeBytes(self.o,c+"\n")
outfile_writeBytes(self.o,"tail -%d \"$S\" | bash\n" % (len(self.commands)+1))
if not write_to_stdout:
......@@ -589,30 +678,44 @@ def dd_command(offset,length):
warned_about_sox_decode = 0
def warn_sox_decode():
global warned_about_sox_decode
if not warned_about_sox_decode and not sox_ignoreLen:
show_warning("Had to use sox to decode MP3 (as no madplay etc); some versions of sox truncate the end of MP3s") # but 14.3+ (sox_ignoreLen set) should be OK
if not warned_about_sox_decode:
r = []
if macsound: r.append("the sox bundled with Mac Gradint was not compiled with MP3 support (please install madplay or a better sox)") # (or upgrade to a version of Mac OS that has afconvert)
if not sox_ignoreLen: r.append("some versions of sox truncate the end of MP3s (please upgrade sox or install madplay/mpg123)") # sox 14.3+ (sox_ignoreLen set) should be OK
if r: r.insert(0,"Had to use sox to decode MP3")
if r: show_warning('; '.join(r))
warned_about_sox_decode = 1
def decode_mp3(file):
def decode_mp3(file): # Returns WAV data including header. TODO: this assumes it's always small enough to read the whole thing into RAM (should be true if it's 1 word though, and decode_mp3 isn't usually used unless we're making a lesson file rather than running something in justSynthesize)
file = S(file)
if riscos_sound:
warn_sox_decode()
os.system("sox -t mp3 \""+file+"\" -t wav"+cond(compress_SH," "+sox_8bit,"")+" tmp0")
warn_sox_decode() # TODO: can use madplay or AMPlay to decode if correctly installed
system("sox -t mp3 \""+file+"\" -t wav"+cond(compress_SH," "+sox_8bit,"")+" tmp0")
data=read("tmp0") ; os.unlink("tmp0")
return data
elif madplay_program or got_program("mpg123"):
elif madplay_path:
oldDir = os.getcwd()
d=readB(os.popen(madplay_path+cond(compress_SH," -R 16000 -b 8","")+" -q \""+changeToDirOf(file)+"\" -o wav:-",popenRB))
if winsound: d=d.replace(B("data\xFF"),B("data\x7F"),1) # sox bug workaround
os.chdir(oldDir) ; return d
elif got_program("mpg123"): # do NOT try to read its stdout (not only does it write 0 length, which we can fix, but some versions can also write wrong bitrate, which is harder for us to fix)
oldDir = os.getcwd()
if madplay_program: d=os.popen(madplay_program+cond(compress_SH," -R 16000 -b 8","")+" -q \""+changeToDirOf(file)+"\" -o wav:-","rb").read()
else: d=os.popen("mpg123 -q -w - \""+changeToDirOf(file)+"\"","rb").read()
os.chdir(oldDir)
# fix length (especially if it's mpg123)
wavLen = len(d)-8 ; datLen = wavLen-36 # assumes no other chunks
if datLen<0: raise IOError("decode_mp3 got bad wav") # better than ValueError for the chr() in the following line
return d[:4] + chr(wavLen&0xFF)+chr((wavLen>>8)&0xFF)+chr((wavLen>>16)&0xFF)+chr(wavLen>>24) + d[8:40] + chr(datLen&0xFF)+chr((datLen>>8)&0xFF)+chr((datLen>>16)&0xFF)+chr(datLen>>24) + d[44:]
tfil = os.tempnam()+dotwav
system("mpg123 -q -w \""+tfil+"\" \""+changeToDirOf(file)+"\"")
if compress_SH and gotSox: dat = readB(os.popen("sox \""+tfil+"\" -t wav "+sox_8bit+" - ",popenRB))
else: dat = open(tfil,"rb").read()
os.unlink(tfil) ; os.chdir(oldDir) ; return dat
elif macsound and got_program("afconvert"):
tfil = os.tempnam()+dotwav
system("afconvert -f WAVE -d I16@44100 \""+file+"\" \""+tfil+"\"")
if compress_SH and gotSox: dat = readB(os.popen("sox \""+tfil+"\" -t wav "+sox_8bit+" - ",popenRB))
else: dat = open(tfil,"rb").read()
os.unlink(tfil) ; return dat
elif unix:
if gotSox:
if soxMp3:
warn_sox_decode()
return os.popen("cat \""+file+"\" | sox -t mp3 - -t wav"+cond(compress_SH," "+sox_8bit,"")+" - ","rb").read()
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")
show_warning("Don't know how to decode "+file+" on this system. Try installing madplay or mpg123.")
return ""
else: raise Exception("decode_mp3 called on a setup that's not Unix and doesn't have MADplay. Need to implement non-cat sox redirect.")
......@@ -620,14 +723,17 @@ def decode_mp3(file):
class Mp3FileCache(object):
def __init__(self): self.fileCache = {}
def __del__(self):
import os # as it might already have been gc'd
for v in self.fileCache.values():
try: import os # as it might already have been gc'd
except: pass
try:
for v in self.fileCache.values():
try: os.remove(v)
except: pass # somebody may have removed it already
except: pass
def decode_mp3_to_tmpfile(self,file):
if not file in self.fileCache:
if not checkIn(file,self.fileCache):
self.fileCache[file] = os.tempnam()+dotwav
open(self.fileCache[file],"wb").write(decode_mp3(file))
write(self.fileCache[file],decode_mp3(file))
return self.fileCache[file]
theMp3FileCache = Mp3FileCache()
......@@ -654,7 +760,7 @@ if outputFile:
if out_type=="sh": soundCollector,sample_table_hack = ShSoundCollector(), 1
else: soundCollector = SoundCollector()
waitBeforeStart = 0
if unix and out_type in ["ogg","mp3"] and os.uname()[4].startswith("arm"): show_warning("Note: On armel, compile lame or oggenc with -fno-finite-math-only, or use lame -b 64 (or higher). See http://martinwguy.co.uk/martin/debian/no-finite-math-only")
if unix and out_type in ["ogg","mp3"] and os.uname()[4].startswith("arm"): show_warning("Note: On armel, compile lame or oggenc with -fno-finite-math-only, or use lame -b 64 (or higher)")
if not (soundCollector and out_type=="sh"): compress_SH = False # regardless of its initial setting (because it's used outside ShSoundCollector)
def collector_time(): return soundCollector.tell()
def collector_sleep(s): soundCollector.addSilence(s)
......
# This file is part of the source code of
# gradint v0.9969 (c) 2002-2011 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
......@@ -24,29 +24,40 @@ class InputSource(object):
class MicInput(InputSource):
def __init__(self):
rates = tkSnack.audio.rates()
for rate in [22050, 16000, 24000, 44100]:
if rates:
for rate in [22050, 16000, 24000, 44100]:
if rate in rates:
self.rate = rate ; return
if rates: self.rate = max(rates)
self.rate = max(rates)
else: self.rate = None
def startRec(self,outFile,lastStopRecVal=None):
if not self.rate: return # TODO tell the user we can't record anything on this system (currently we just let them find out)
self.sound = tkSnack.Sound(file=outFile, rate=self.rate, channels=1, encoding="Lin16")
self.sound.record()
def stopRec(self): self.sound.stop()
if not self.rate: return self.err("Cannot record on this system (try aoss?)")
try: self.sound = tkSnack.Sound(file=outFile, rate=self.rate, channels=1, encoding="Lin16")
except: return self.err("Cannot write to sound file '"+outFile+"' with tkSnack")
try: self.sound.record()
except: # e.g. waveInOpen failed on Windows 7 (driver problems?)
self.err("sound.record() failed")
try: self.sound.stop()
except: pass
try: os.remove(outFile)
except: pass
del self.sound
def err(self,msg): app.todo.alert=msg
def stopRec(self):
if hasattr(self,"sound"): self.sound.stop()
class PlayerInput(InputSource): # play to speakers while recording to various destinations
def __init__(self,fileToPlay,startNow=True,startTime=0): # (if startNow=False, starts when you start recording)
global paranoid_file_management
if use_unicode_filenames: fileToPlay=ensure_unicode(fileToPlay)
else:
assert not type(fileToPlay)==type(u"")
assert type(fileToPlay)==type("")
if not paranoid_file_management and filter(lambda x:ord(x)>=128,list(fileToPlay)): paranoid_file_management = True # hack to try to work around a Tkinter fault on Linux with utf-8 filenames
if paranoid_file_management: # try to ensure it's ready for reading
if filelen(fileToPlay)<1048576:
# only small - copy to temp 1st
self.fileToDel = os.tempnam()+fileToPlay[fileToPlay.rfind(extsep):]
open(self.fileToDel,"wb").write(read(fileToPlay))
write(self.fileToDel,read(fileToPlay))
fileToPlay=self.fileToDel
else: open(fileToPlay)
if fileToPlay.lower().endswith(dotwav) and filelen(fileToPlay)<1048576: self.sound=tkSnack.Sound(load=fileToPlay) # in-memory if <1M (saves problems with Windows keeping them open even after object deleted), TODO is this still needed now that .destroy() is called properly? (but might be a good idea to keep it in anyway)
......@@ -62,7 +73,8 @@ class PlayerInput(InputSource): # play to speakers while recording to various de
def startPlaying(self,curSample=0):
theISM.nowPlaying = self
tkSnack.audio.stop() # as we might be still in c'tor and just about to be assigned to replace the previously-playing sound (i.e. it might not have stopped yet), and we don't want to confuse elapsedTime
self.sound.play(start=curSample)
try: self.sound.play(start=curSample)
except: app.todo.alert="tkSnack problem playing sound: try running gradint under aoss"
self.startSample = curSample ; self.startTime = time.time()
self.autostop()
def autostop(self,thread_id=None):
......@@ -74,11 +86,12 @@ class PlayerInput(InputSource): # play to speakers while recording to various de
elapsedTime = self.elapsedTime()
if elapsedTime>=self.length-self.startSample*1.0/self.sampleRate: self.close()
else:
import thread
def stopMe(self,thread_id):
try: import thread
except ImportError: import _thread as thread
def stopMe(self,thread_id,elapsedTime):
time.sleep(max(0.5,self.length-self.startSample*1.0/self.sampleRate-elapsedTime))
self.autostop(thread_id)
thread.start_new_thread(stopMe,(self,thread_id))
thread.start_new_thread(stopMe,(self,thread_id,elapsedTime))
def elapsedTime(self):
try: t=tkSnack.audio.elapsedTime()
except: t=0.0
......@@ -120,35 +133,16 @@ class PlayerInput(InputSource): # play to speakers while recording to various de
theRecorderControls.current_recordFrom_button = theRecorderControls.old_recordFrom_button
app.todo.undoRecordFrom=True # we might not be the GUI thread
if not tkSnack:
if macsound: # might still be able to use Audio Recorder
if fileExists("AudioRecorder.zip"): unzip_and_delete("AudioRecorder.zip")
if fileExists("Audio Recorder.app/plist"): # Audio Recorder with our special preferences list
runAudioRecorderYet = 0
def MacStartRecording():
global runAudioRecorderYet
if not runAudioRecorderYet: os.system("mv ~/Library/Preferences/com.benshan.AudioRecorder31.plist ~/Library/Preferences/com.benshan.AudioRecorder31.plist-OLD 2>/dev/null ; cp Audio\\ Recorder.app/plist ~/Library/Preferences/com.benshan.AudioRecorder31.plist; open Audio\\ Recorder.app")
os.system("osascript -e 'Tell application \"Audio Recorder\" to Record'")
runAudioRecorderYet = 1
def MacStopRecording(): os.system("osascript -e 'Tell application \"Audio Recorder\" to Stop'")
MacRecordingFile = "/tmp/audiorec-output-for-gradint.wav" # specified in the plist
def quitAudioRecorder():
if runAudioRecorderYet: os.system("osascript -e 'Tell application \"Audio Recorder\" to quit' ; rm ~/Library/Preferences/com.benshan.AudioRecorder31.plist ; mv ~/Library/Preferences/com.benshan.AudioRecorder31.plist-OLD ~/Library/Preferences/com.benshan.AudioRecorder31.plist 2>/dev/null")
import atexit ; atexit.register(quitAudioRecorder)
del MicInput
class MicInput(InputSource): # Mac Audio Recorder version
def startRec(self,outFile,lastStopRecVal=None):
self.fileToWrite = outFile
MacStartRecording()
def stopRec(self):
MacStopRecording()
os.rename(MacRecordingFile,self.fileToWrite)
tkSnack = "MicOnly"
elif unix and useTK and isDirectory("/dev/snd") and got_program("arecord"): # no tkSnack, but can record via ALSA (but no point doing the tests if not useTK)
if useTK and not tkSnack:
if unix and isDirectory("/dev/snd") and got_program("arecord"): # can record via ALSA
recParams = (os.P_NOWAIT,got_program("arecord"),"arecord","-f","S16_LE","-r","22050")
elif gotSox: recParams = (os.P_NOWAIT,got_program("sox"),"sox","-d","-r","22050","-c","1")+tuple(sox_16bit.split()+sox_signed.split())
else: recParams = None
if recParams:
del MicInput
class MicInput(InputSource):
def startRec(self,outFile,lastStopRecVal=0.5):
self.pid = os.spawnl(os.P_NOWAIT,"/bin/bash","/bin/bash","-c","arecord -f S16_LE -r 22050 "+shell_escape(outFile))
self.pid = os.spawnl(*(recParams+(outFile,)))
time.sleep(lastStopRecVal) # allow process to start
def stopRec(self):
os.kill(self.pid,2) # INT
......@@ -196,7 +190,7 @@ def wavToMp3(directory):
def makeMp3Zips(baseDir,outDir,zipNo=0,direc=None):
zipSplitThreshold = 5*1048576 # to be safe (as will split when it goes OVER that)
if baseDir==outDir: return zipNo # omit
if baseDir==outDir or baseDir.endswith(extsep+"zip"): return zipNo # omit the output dir, plus any existing ZIP files
elif not direc:
for f in os.listdir(baseDir): zipNo = makeMp3Zips(baseDir,outDir,zipNo,f)
elif isDirectory(baseDir+os.sep+direc): zipNo = makeMp3Zips(baseDir+os.sep+direc,outDir,zipNo)
......@@ -218,7 +212,7 @@ def getAmplify(directory):
for f in os.listdir(directory):
factor = None
if f.endswith(dotwav) and not system("sox \""+directory+os.sep+f+"\" "+out2nul+" stat 2> \""+statfile+"\""):
for l in read(statfile).replace("\r","\n").split("\n"):
for l in S(read(statfile)).replace("\r","\n").split("\n"):
if l.startswith("Volume adjustment:"): factor=l.split()[2]
if not factor: continue
tmplist.append([float(factor),f,factor])
......@@ -248,7 +242,7 @@ def doAmplify(directory,fileList,factor):
class ButtonScrollingMixin(object):
# expects self.ourCanvas
def bindFocusIn(self,b):
b.bind('<FocusIn>',lambda *args:self.scrollIntoView(b))
b.bind('<FocusIn>',lambda e=None,self=self,b=b:self.scrollIntoView(b))
if not hasattr(app,"gotFocusInHandler"):
# (see scrollIntoView method's use of justGotFocusIn)
app.gotFocusInHandler=1
......@@ -273,7 +267,7 @@ class ButtonScrollingMixin(object):
if by+bh<=cy+ch: return # make this the last one - don't loop consuming CPU on bottom of list
elif by < cy: self.ourCanvas.yview("scroll","-1","units")
else: return # done
app.after(10,lambda *args:self.continueScrollIntoView(button))
app.after(10,lambda e=None,self=self,button=button:self.continueScrollIntoView(button))
class RecorderControls(ButtonScrollingMixin):
def __init__(self):
......@@ -311,7 +305,7 @@ class RecorderControls(ButtonScrollingMixin):
self.frame.pack_forget()
theISM.setInputSource(None)
def addButton(self,row,col,text,command,colspan=None):
if (row,col) in self.coords2buttons: self.coords2buttons[(row,col)].grid_forget()
if checkIn((row,col),self.coords2buttons): self.coords2buttons[(row,col)].grid_forget()
b = makeButton(self.grid,text=text,command=command)
self.bindFocusIn(b)
self.coords2buttons[(row,col)] = b
......@@ -324,17 +318,56 @@ class RecorderControls(ButtonScrollingMixin):
if col: self.coords2buttons[(row,col)].grid(row=row,column=col,columnspan=colspan)
else: self.coords2buttons[(row,col)].grid(row=row,column=0,columnspan=colspan,sticky="w")
def addLabel(self,row,col,utext):
if (row,col) in self.coords2buttons: self.coords2buttons[(row,col)].grid_forget()
self.coords2buttons[(row,col)] = self.makeLabel_lenLimit(utext)
self.coords2buttons[(row,col)].grid(row=row,column=col,sticky="w")
if col==0: self.coords2buttons[(row,col)].bind('<Button-1>',lambda *args:self.startRename(row,col,utext))
if checkIn((row,col),self.coords2buttons): self.coords2buttons[(row,col)].grid_forget()
rc = self.coords2buttons[(row,col)] = self.makeLabel_lenLimit(utext)
rc.grid(row=row,column=col,sticky="w")
if col==0:
rc.bind('<Button-1>',lambda e=None,self=self,row=row,col=col,utext=utext:self.startRename(row,col,utext))
if not winCEsound:
def contextMenu(e,row=row,col=col,self=self,utext=utext):
# TODO: document this context menu?
m=Tkinter.Menu(None, tearoff=0, takefocus=0)
m.add_command(label="Rename",command=lambda e=None,self=self,row=row,col=col,utext=utext:self.startRename(row,col,utext))
if self.currentDir.startswith(samplesDirectory): m.add_command(label="Add extra revision",command=lambda e=None,self=self,utext=utext:self.addRevision(utext))
m.add_command(label="Delete",command=lambda e=None,self=self,utext=utext:self.delete(utext))
m.tk_popup(e.x_root-3, e.y_root+3,entry="0")
rc.bind('<ButtonRelease-3>',contextMenu)
if macsound:
rc.bind('<Control-ButtonRelease-1>',contextMenu)
rc.bind('<ButtonRelease-2>',contextMenu)
if not winCEsound:
def delete(self,filename):
toDel = [] ; fs=filename.encode('utf-8')
for f in os.listdir(self.currentDir):
if f.startswith(S(fs)): toDel.append(f)
if not toDel: return tkMessageBox.showinfo(filename,"No files found") # shouldn't happen
if tkMessageBox.askyesno(filename,"Really delete "+" ".join(toDel)+"?"):
for d in toDel: os.remove(self.currentDir+os.sep+d)
self.undraw() ; self.draw() # TODO incremental update? (need to check really does affect just that row; careful with variants, synth, etc)
def addRevision(self,filename):
# c.f. gui_event_loop menu_response=="add" when already in vocabList
app.set_watch_cursor = 1
d = ProgressDatabase(0)
found = 0
curDir = self.currentDir[len(samplesDirectory)+len(os.sep):]
if curDir: curDir += os.sep
for item in d.data: # TODO: don't run this loop in the GUI thread!
if not item[2].startswith(curDir+filename.encode('utf-8')+"_"): continue
if not item[0]: break # not done yet
newItem0 = reviseCount(item[0])
if tkMessageBox.askyesno(filename,localise("Repeat count is %d. Reduce this to %d for extra revision?" % (item[0],newItem0))):
d.data.remove(item)
d.data.append((newItem0,item[1],item[2]))
d.save()
found = 1 ; break
if not found: tkMessageBox.showinfo(filename,localise("Repeat count is 0, so we cannot reduce it for extra revision."))
def makeLabel_lenLimit(self,utext): return Tkinter.Label(self.grid,text=utext,wraplength=int(self.ourCanvas.winfo_screenwidth()/(1+len(self.languagesToDraw))))
def addSynthLabel(self,filename,row,col):
try: ftext = ensure_unicode(u8strip(read(filename).strip(wsp)))
try: ftext = ensure_unicode(u8strip(bwspstrip(read(filename))))
except IOError: return False
l = self.makeLabel_lenLimit(ftext)
l.grid(row=row,column=col,columnspan=2,sticky="w")
l.bind('<Button-1>',lambda *args:self.startSynthEdit(l,row,col,filename))
l.bind('<Button-1>',lambda e=None,self=self,l=l,row=row,col=col,filename=filename:self.startSynthEdit(l,row,col,filename))
return True # do NOT put it in self.coords2buttons (not to do with space bar stuff etc)
def startSynthEdit(self,l,row,col,filename):
if hasattr(self,"renameToCancel"):
......@@ -342,29 +375,29 @@ class RecorderControls(ButtonScrollingMixin):
self.cancelRename(rr,cc)
if l: l.grid_forget()
editText,editEntry = addTextBox(self.grid,"nopack")
try: editText.set(ensure_unicode(u8strip(read(filename).strip(wsp))))
try: editText.set(ensure_unicode(u8strip(bwspstrip(read(filename)))))
except IOError: pass
editEntry.grid(row=row,column=col,sticky='we',columnspan=2)
editEntry.bind('<Return>',lambda *args:self.doEdit(editText,editEntry,row,col,filename))
editEntry.bind('<Escape>',lambda *args:self.cancelEdit(editEntry,row,col,filename))
editEntry.bind('<Return>',lambda e=None,self=self,editText=editText,editEntry=editEntry,row=row,col=col,filename=filename:self.doEdit(editText,editEntry,row,col,filename))
editEntry.bind('<Escape>',lambda e=None,self=self,editEntry=editEntry,row=row,col=col,filename=filename:self.cancelEdit(editEntry,row,col,filename))
focusButton(editEntry)
if hasattr(self.coords2buttons.get((row-1,col+1),""),"is_synth_label"):
self.addLabel(row-1,col+1,localise("(synth'd)"))
self.coords2buttons[(row-1,col+1)].is_synth_label = True
def doEdit(self,editText,editEntry,row,col,filename):
text = asUnicode(editText.get()).encode("utf-8").strip(wsp)
if text: open(filename,"w").write(text+"\n")
text = bwspstrip(asUnicode(editText.get()).encode("utf-8"))
if text: writeB(open(filename,"w"),text+B("\n"))
else:
try: os.remove(filename)
except: pass
self.cancelEdit(editEntry,row,col,filename)
if row+1 < self.addMoreRow and (row+1,col+1) in self.coords2buttons: focusButton(self.coords2buttons[(row+1,col+1)]) # focus the next "synth" button if it exists (don't press it as well like file renaming because it might be a variant etc, TODO can we skip variants?)
if row+1 < self.addMoreRow and checkIn((row+1,col+1),self.coords2buttons): focusButton(self.coords2buttons[(row+1,col+1)]) # focus the next "synth" button if it exists (don't press it as well like file renaming because it might be a variant etc, TODO can we skip variants?)
def cancelEdit(self,editEntry,row,col,filename):
editEntry.grid_forget()
labelAdded = self.addSynthLabel(filename,row,col)
if hasattr(self.coords2buttons.get((row-1,col+1),""),"is_synth_label"):
if labelAdded: self.addLabel(row-1,col+1,localise("(synth'd)"))
else: self.addButton(row-1,col+1,text=localise("Synthesize"),command=(lambda *args:self.startSynthEdit(None,row,col,filename)))
else: self.addButton(row-1,col+1,text=localise("Synthesize"),command=(lambda e=None,self=self,row=row,col=col,filename=filename:self.startSynthEdit(None,row,col,filename)))
self.coords2buttons[(row-1,col+1)].is_synth_label = True
def amplify(self,*args):
self.AmplifyButton["text"] = localise("Please wait") # TODO not in the GUI thread !! (but lock our other buttons while it's doing it)
......@@ -386,7 +419,7 @@ class RecorderControls(ButtonScrollingMixin):
numZips = makeMp3Zips(self.currentDir,self.currentDir+os.sep+"zips")
if numZips:
openDirectory(self.currentDir+os.sep+"zips",1)
if numZips>1: app.todo.alert=localise("Please send the %d zip files as %d separate messages, in case one very large message doesn't get through.") % (zipNo,zipNo)
if numZips>1: app.todo.alert=localise("Please send the %d zip files as %d separate messages, in case one very large message doesn't get through.") % (numZips,numZips)
else: app.todo.alert=localise("You may now send the zip file by email.")
else: app.todo.alert=localise("No recordings found")
self.undraw() ; self.draw()
......@@ -394,11 +427,11 @@ class RecorderControls(ButtonScrollingMixin):
if hasattr(self,"renameToCancel"):
rr,cc = self.renameToCancel
self.cancelRename(rr,cc)
if self.has_variants and filename.find(" (")>-1:
if self.has_variants and filename.find(" (")>=0:
app.todo.alert=self.renamevar_msg
return
self.renameToCancel = (row,col)
if (row,col) in self.coords2buttons: self.coords2buttons[(row,col)].grid_forget()
if checkIn((row,col),self.coords2buttons): self.coords2buttons[(row,col)].grid_forget()
renameText,renameEntry = addTextBox(self.grid,"nopack")
renameEntry['width']=min(8,len(filename)+2)
renameEntry.theText = renameText
......@@ -415,20 +448,20 @@ class RecorderControls(ButtonScrollingMixin):
selectAllFunc = selectAll
class E: pass
e=E() ; e.widget = renameEntry
self.ourCanvas.after(50,lambda *args:(e.widget.focus(),self.scrollIntoView(e.widget),selectAllFunc(e)))
renameEntry.bind('<Return>',lambda *args:self.doRename(row,col))
renameEntry.bind('<Escape>',lambda *args:self.cancelRename(row,col))
self.ourCanvas.after(50,lambda _=None,e=e,self=self,selectAllFunc=selectAllFunc:(e.widget.focus(),self.scrollIntoView(e.widget),selectAllFunc(e)))
renameEntry.bind('<Return>',lambda e=None,self=self,row=row,col=col:self.doRename(row,col))
renameEntry.bind('<Escape>',lambda e=None,self=self,row=row,col=col:self.cancelRename(row,col))
def doRename(self,row,col):
if hasattr(self,"renameToCancel"): del self.renameToCancel
try: origName = self.coords2buttons[(row,col)].origName
except AttributeError: return # event must have fired twice for some reason?
newNames = filter(lambda x:x,asUnicode(self.coords2buttons[(row,col)].theText.get()).split("\n")) # multiline paste, ignore blank lines
newNames = filter(lambda x,self=self,row=row,col=col:x,asUnicode(self.coords2buttons[(row,col)].theText.get()).split("\n")) # multiline paste, ignore blank lines
for newName in newNames:
if not origName: # extra lines - need to get their origNames
if row==self.addMoreRow: self.addMore()
elif not (row,col) in self.coords2buttons: row += 1 # skip extra row if there are notes
elif not checkIn((row,col),self.coords2buttons): row += 1 # skip extra row if there are notes
origName=self.coords2buttons[(row,col)]["text"]
if self.has_variants and origName.find(" (")>-1:
if self.has_variants and origName.find(" (")>=0:
app.todo.alert=self.renamevar_msg
break
if len(newNames)>1 and not '0'<=newName[0]<='9': # multiline paste and not numbered - we'd better keep the original number
......@@ -440,7 +473,7 @@ class RecorderControls(ButtonScrollingMixin):
except:
tkMessageBox.showinfo(app.master.title(),localise("Could not rename %s to %s") % (origName,newName))
return
self.addButton(row,col,text=newName,command=(lambda e=None,f=self.currentDir+os.sep+newName:self.changeDir(f)))
self.addButton(row,col,text=newName,command=(lambda e=None,self=self,f=self.currentDir+os.sep+newName:self.changeDir(f)))
else: # not a directory - rename individual files
self.doStop() # just in case
for lang in list2set([firstLanguage,secondLanguage]+otherLanguages+self.languagesToDraw): # not just self.languagesToDraw, as a student of more languages than these might not have them all showing and still expect renames to work
......@@ -451,53 +484,53 @@ class RecorderControls(ButtonScrollingMixin):
except:
tkMessageBox.showinfo(app.master.title(),localise("Could not rename %s to %s") % (origName+"_"+lang+ext,newName+"_"+lang+ext)) # TODO undo any that did succeed first! + check for destination-already-exists (OS may not catch it)
return
if not lang in self.languagesToDraw: continue
if not checkIn(lang,self.languagesToDraw): continue
self.updateFile(unicode2filename(newName+"_"+lang+ext),row,self.languagesToDraw.index(lang),cond(ext==dottxt,0,2)) # TODO the 2 should be 1 if and only if we didn't just record it
updated=True
if not updated and lang in self.languagesToDraw: self.updateFile(unicode2filename(newName+"_"+lang+dotwav),row,self.languagesToDraw.index(lang),0)
if not updated and checkIn(lang,self.languagesToDraw): self.updateFile(unicode2filename(newName+"_"+lang+dotwav),row,self.languagesToDraw.index(lang),0)
self.addLabel(row,col,newName)
# TODO what about updating progress.txt with wildcard changes (cld be going too far - we have the move script in utilities)
origName = None # get any others from the form
row += 1
if len(newNames)==1 and row<self.addMoreRow: # put cursor on the next one
if not (row,col) in self.coords2buttons: row += 1 # skip extra row if there are notes
if not checkIn((row,col),self.coords2buttons): row += 1 # skip extra row if there are notes
if row<self.addMoreRow:
origName=self.coords2buttons[(row,col)]["text"]
if not isDirectory(unicode2filename(self.currentDir+os.sep+origName)): self.startRename(row,0,origName)
def cancelRename(self,row,col):
if hasattr(self,"renameToCancel"): del self.renameToCancel
origName = self.coords2buttons[(row,col)].origName
if isDirectory(unicode2filename(self.currentDir+os.sep+origName)): self.addButton(row,col,text=origName,command=(lambda e=None,f=ensure_unicode(self.currentDir+os.sep+origName).encode('utf-8'):self.changeDir(f)))
if isDirectory(unicode2filename(self.currentDir+os.sep+origName)): self.addButton(row,col,text=origName,command=(lambda e=None,self=self,f=ensure_unicode(self.currentDir+os.sep+origName).encode('utf-8'):self.changeDir(f)))
else: self.addLabel(row,col,origName)
def updateFile(self,filename,row,languageNo,state,txtExists="unknown"): # state: 0 not exist, 1 already existed, 2 we just created it
if not os.sep in filename: filename = self.currentDir+os.sep+filename
recFilename = filename
if recFilename.lower().endswith(dotmp3): recFilename=recFilename[:-len(dotmp3)]+dotwav # always record in WAV; can compress to MP3 after
if state: # exists
if not tkSnack or tkSnack=="MicOnly": self.addButton(row,2+3*languageNo,text=localise("Play"),command=(lambda e=None,f=filename:(self.doStop(),SampleEvent(f).play()))) # but if got full tkSnack, might as well use setInputSource instead to be consistent with the non-_ version:
else: self.addButton(row,2+3*languageNo,text=localise("Play"),command=(lambda e=None,f=filename:(self.doStop(),theISM.setInputSource(PlayerInput(f,not self.syncFlag)))))
if state: # sound file exists
if not tkSnack or tkSnack=="MicOnly" or wavPlayer_override: self.addButton(row,2+3*languageNo,text=localise("Play"),command=(lambda e=None,f=filename,self=self:(self.doStop(),SampleEvent(f).play()))) # but if got full tkSnack, might as well use setInputSource instead to be consistent with the non-_ version:
else: self.addButton(row,2+3*languageNo,text=localise("Play"),command=(lambda e=None,f=filename,self=self:(self.doStop(),theISM.setInputSource(PlayerInput(f,not self.syncFlag)),self.setSync(False))))
if tkSnack and (state==2 or self.always_enable_rerecord):
self.addButton(row,3+3*languageNo,text=localise("Re-record"),command=(lambda e=None,f=recFilename,r=row,l=languageNo:self.doRecord(f,r,l,needToUpdatePlayButton=(not filename==recFilename))))
self.addButton(row,3+3*languageNo,text=localise("Re-record"),command=(lambda e=None,f0=filename,f=recFilename,r=row,l=languageNo,self=self:self.doRecord(f,r,l,needToUpdatePlayButton=(not f0==f))))
else:
self.addLabel(row,3+3*languageNo,"")
self.need_reRecord_enabler = not (not tkSnack)
else: # does not exist
else: # sound file does not exist
synthFilename = filename[:filename.rfind(extsep)]+dottxt
if txtExists=="unknown": txtExists=fileExists(synthFilename)
if txtExists: self.addLabel(row,2+3*languageNo,localise("(synth'd)"))
elif self.always_enable_synth and get_synth_if_possible(self.languagesToDraw[languageNo],0): self.addButton(row,2+3*languageNo,text=localise("Synthesize"),command=(lambda *args:self.startSynthEdit(None,row+1,1+3*languageNo,synthFilename)))
elif self.always_enable_synth and get_synth_if_possible(self.languagesToDraw[languageNo],0): self.addButton(row,2+3*languageNo,text=localise("Synthesize"),command=(lambda e=None,self=self,row=row,languageNo=languageNo,synthFilename=synthFilename:self.startSynthEdit(None,row+1,1+3*languageNo,synthFilename)))
else: self.addLabel(row,2+3*languageNo,localise("(empty)"))
self.coords2buttons[(row,2+3*languageNo)].is_synth_label = True
if winCEsound and not tkSnack: self.addLabel(row,3+3*languageNo,"")
else: self.addButton(row,3+3*languageNo,text=localise("Record"),command=(lambda e=None,f=recFilename,r=row,l=languageNo:self.doRecord(f,r,l)))
else: self.addButton(row,3+3*languageNo,text=localise("Record"),command=(lambda e=None,f=recFilename,r=row,l=languageNo,self=self:self.doRecord(f,r,l)))
def add_addMore_button(self):
if winCEsound and not tkSnack: pass # no 'add more words' button on WinCE; use PocketPC record button instead
else: self.addButton(self.addMoreRow,0,text=localise("Add more words"),command=(lambda *args:self.addMore()),colspan=cond(self.need_reRecord_enabler,2,4))
if self.need_reRecord_enabler: self.addButton(self.addMoreRow,2,text=localise("Re-record"),command=(lambda *args:self.global_rerecord()),colspan=2)
self.addButton(self.addMoreRow,4,text=localise("New folder"),command=(lambda *args:self.newFolder()),colspan=3)
else: self.addButton(self.addMoreRow,0,text=localise("Add more words"),command=(lambda e=None,self=self:self.addMore()),colspan=cond(self.need_reRecord_enabler,2,4))
if self.need_reRecord_enabler: self.addButton(self.addMoreRow,2,text=localise("Re-record"),command=(lambda e=None,self=self:self.global_rerecord()),colspan=2)
self.addButton(self.addMoreRow,4,text=localise("New folder"),command=(lambda e=None,self=self:self.newFolder()),colspan=3)
def del_addMore_button(self):
if (self.addMoreRow,0) in self.coords2buttons: self.coords2buttons[(self.addMoreRow,0)].grid_forget() # old 'add more' button
if (self.addMoreRow,2) in self.coords2buttons: self.coords2buttons[(self.addMoreRow,2)].grid_forget() # old 're-record' button
if checkIn((self.addMoreRow,0),self.coords2buttons): self.coords2buttons[(self.addMoreRow,0)].grid_forget() # old 'add more' button
if checkIn((self.addMoreRow,2),self.coords2buttons): self.coords2buttons[(self.addMoreRow,2)].grid_forget() # old 're-record' button
self.coords2buttons[(self.addMoreRow,4)].grid_forget() # old 'new folder' button
def addMore(self,*args):
self.del_addMore_button()
......@@ -510,7 +543,7 @@ class RecorderControls(ButtonScrollingMixin):
self.gridLabel(lang,self.addMoreRow)
self.addMoreRow += 2 ; self.maxPrefix += 1
self.add_addMore_button()
def gridLabel(self,lang,row): Tkinter.Label(self.grid,text=" "+localise(cond(lang.find("-meaning_")>-1,"meaning",lang))+": ").grid(row=row,column=1+3*self.languagesToDraw.index(lang))
def gridLabel(self,lang,row): Tkinter.Label(self.grid,text=" "+localise(cond(lang.find("-meaning_")>=0,"meaning",lang))+": ").grid(row=row,column=1+3*self.languagesToDraw.index(lang))
def doRecord(self,filename,row,languageNo,needToUpdatePlayButton=False):
if not tkSnack: return tkMessageBox.showinfo(app.master.title(),localise("Sorry, cannot record on this computer because the tkSnack library (python-tksnack) is not installed."))
theISM.startRecording(filename)
......@@ -518,7 +551,7 @@ class RecorderControls(ButtonScrollingMixin):
self.coords2buttons[(row,3+3*languageNo)]["text"]=localise("Stop")
self.updateForStopOrChange()
self.currentRecording = (filename,row,languageNo)
self.coords2buttons[(row,3+3*languageNo)]["command"]=(lambda *args:self.doStop())
self.coords2buttons[(row,3+3*languageNo)]["command"]=(lambda e=None,self=self:self.doStop())
if app.scanrow.get()=="2": # "stop"
focusButton(self.coords2buttons[(row,3+3*languageNo)])
else:
......@@ -526,14 +559,14 @@ class RecorderControls(ButtonScrollingMixin):
if app.scanrow.get()=="1": # move along 1st
while languageNo+1<len(self.languagesToDraw):
languageNo += 1
if (row,3+3*languageNo) in self.coords2buttons:
if checkIn((row,3+3*languageNo),self.coords2buttons):
focusButton(self.coords2buttons[(row,3+3*languageNo)])
return
languageNo = 0 # start of the row
# fall-through - vertical movement
for r in [row+1,row+2]:
if r==self.addMoreRow: self.addMore()
if (r,3+3*languageNo) in self.coords2buttons:
if checkIn((r,3+3*languageNo),self.coords2buttons):
return focusButton(self.coords2buttons[(r,3+3*languageNo)])
def doStop(self,*args):
theISM.stopRecording()
......@@ -551,9 +584,9 @@ class RecorderControls(ButtonScrollingMixin):
if hasattr(self,"oldCanvasBbox") and bbox==self.oldCanvasBbox: pass
else:
self.oldCanvasBbox = bbox
c.config(scrollregion=bbox,width=bbox[2],height=min(c["height"],c.winfo_screenheight()/2,bbox[3]))
c.config(scrollregion=bbox,width=bbox[2],height=min(int(c["height"]),int(c.winfo_screenheight()/2),int(bbox[3])))
if hasattr(self,"currentRecording") and not theISM.currentOutfile: self.doStop() # ensure GUI updates the recording button after player auto-stop (for want of a better place to put it)
app.after(cond(winCEsound,3000,600),lambda *args:self.reconfigure_scrollbar())
app.after(cond(winCEsound,3000,600),lambda e=None,self=self:self.reconfigure_scrollbar())
def setSync(self,syncFlag): self.syncFlag = syncFlag
def newFolder(self,*args):
count=0
......@@ -573,12 +606,12 @@ class RecorderControls(ButtonScrollingMixin):
self.doStop()
theISM.setInputSource(PlayerInput(filename,not self.syncFlag))
self.current_recordFrom_button = (row, self.coords2buttons[(row,0)])
self.addButton(row,0,text=localise("Stop"),command=(lambda *args:(self.doStop(),theISM.setInputSource(MicInput()))),colspan=1)
self.addButton(row,0,text=localise("Stop"),command=(lambda e=None,self=self,theISM=theISM:(self.doStop(),theISM.setInputSource(MicInput()))),colspan=1)
col = 1
for inc in [-30, -5, 5, 30]:
if inc<0: text="<"+str(-inc)
else: text=str(inc)+">"
self.addButton(row,col,text=text,command=(lambda e=None,i=inc:self.handleSkip(filename,i)))
self.addButton(row,col,text=text,command=(lambda e=None,i=inc,self=self,filename=filename:self.handleSkip(filename,i)))
col += 1
def handleSkip(self,filename,i):
self.protect_currentRecordFrom()
......@@ -592,7 +625,7 @@ class RecorderControls(ButtonScrollingMixin):
if hasattr(self,"current_recordFrom_button") and self.current_recordFrom_button:
row, button = self.current_recordFrom_button
for col in range(1+3*len(self.languagesToDraw)):
if (row,col) in self.coords2buttons:
if checkIn((row,col),self.coords2buttons):
self.coords2buttons[(row,col)].grid_forget()
del self.coords2buttons[(row,col)]
button.grid(row=row,column=0,columnspan=1+3*len(self.languagesToDraw),sticky="w")
......@@ -631,17 +664,23 @@ class RecorderControls(ButtonScrollingMixin):
else: self.languagesToDraw = [secondLanguage,firstLanguage] # each lang cn take 3 columns, starting at column 1 (DO need to regenerate this every draw - languages may have changed!)
if self.currentDir==samplesDirectory: app.master.title(localise("Recordings manager"))
else: app.master.title(localise("Recordings manager: ")+filename2unicode((os.sep+self.currentDir)[(os.sep+self.currentDir).rindex(os.sep)+1:]))
if hasattr(app,"isBigPrint") and winsound:
# Vista sometimes has window placement problems here
try: app.master.geometry("+0+0")
except: pass
if not self.snack_initialized:
if tkSnack and not tkSnack=="MicOnly":
tkSnack.initializeSnack(app)
if paranoid_file_management and tkSnack.audio.playLatency()<500: tkSnack.audio.playLatency(500) # at least 500ms latency if we're paranoid_file_management, just in case (since tkSnack.audio.elapsedTime does not account for hold-ups)
if paranoid_file_management:
if tkSnack.audio.playLatency()<500: tkSnack.audio.playLatency(500) # at least 500ms latency if we're paranoid_file_management, just in case (since tkSnack.audio.elapsedTime does not account for hold-ups)
self.snack_initialized = True
if not hasattr(app,"scanrow"):
app.scanrow = Tkinter.StringVar(app) # (keep StringVars in app not self to avoid d'tor confusion when close box pressed)
app.scanrow.set("0")
self.reconfigure_scrollbar()
if tkSnack: theISM.setInputSource(MicInput())
self.frame=Tkinter.Frame(app.leftPanel) ; self.frame.pack()
self.frame=Tkinter.Frame(app.leftPanel)
self.frame.pack(fill=Tkinter.Y,expand=1)
self.need_reRecord_enabler = 0 # no previously-existing words yet (when we get existing words we 'lock' them and have to unlock by pressing a global 'rerecord' button 1st, just in case)
......@@ -649,6 +688,15 @@ class RecorderControls(ButtonScrollingMixin):
else:
r = Tkinter.Frame(self.frame)
r.grid(row=1,sticky="e",columnspan=2)
if hasattr(app,"isBigPrint") and macsound:
# Try to make up for the fact that we can't always increase the width of the scrollbar (and the keyboard often loses focus). Add extra up/down buttons. (TODO: does any other platform need this?)
r2 = Tkinter.Frame(r)
r2.pack({"side":"right"})
addButton(r2,unichr(8593),lambda e=None,app=app:app.ScrollUpHandler(),"left")
addButton(r2,unichr(8595),lambda e=None,app=app:app.ScrollDownHandler(),"left")
Tkinter.Label(r,text=" ").pack({"side":"right"}) # TODO: more flexible spacer
r = Tkinter.Frame(r)
r.pack({"side":"right"})
Tkinter.Label(r,text=localise("Action of spacebar during recording")).pack()
r=Tkinter.Frame(r) ; r.pack()
for button in [
......@@ -665,7 +713,7 @@ class RecorderControls(ButtonScrollingMixin):
maxPrefix = 0 ; self.has_recordFrom_buttons = False
if not self.currentDir==samplesDirectory and os.sep in self.currentDir:
self.addButton(curRow,0,text=localise("(Up)"),command=(lambda e=None,f=self.currentDir[:self.currentDir.rindex(os.sep)]:self.changeDir(f)))
self.addButton(curRow,0,text=localise("(Up)"),command=(lambda e=None,f=self.currentDir[:self.currentDir.rindex(os.sep)],self=self:self.changeDir(f)))
curRow += 1
l = os.listdir(self.currentDir)
def cmpfunc(a,b): # sort alphabetically but ensure L2 comes before L1 for tab order
......@@ -677,19 +725,19 @@ class RecorderControls(ButtonScrollingMixin):
if a>b: return 1
elif b>a: return -1
else: return 0
l.sort(cmpfunc)
sort(l,cmpfunc)
self.has_variants = check_has_variants(self.currentDir,l)
allLangs = list2set([firstLanguage,secondLanguage]+possible_otherLanguages)
hadDirectories = False
for fname in l:
flwr = fname.lower() ; isMeaning=0
if firstLanguage==secondLanguage and firstLanguage+"-meaning_"+secondLanguage in fname: isMeaning,languageOverride = True, firstLanguage+"-meaning_"+secondLanguage # hack for re-loading a dir of word+meaning in same language. TODO hope not combining -meaning_ with variants
elif self.has_variants and fname.find("_",fname.find("_")+1)>-1 and not fname.find("_explain_")>-1: languageOverride=fname[fname.find("_")+1:fname.find("_",fname.find("_")+1)]
elif self.has_variants and fname.find("_",fname.find("_")+1)>=0 and not fname.find("_explain_")>=0: languageOverride=fname[fname.find("_")+1:fname.find("_",fname.find("_")+1)]
else: languageOverride=None
if isDirectory(self.currentDir+os.sep+fname):
if not flwr in ["zips","utils","advanced utilities"]: # NOT "prompts", that can be browsed
newDir = self.currentDir+os.sep+fname
self.addButton(curRow,0,text=filename2unicode(fname),command=(lambda e=None,f=newDir:self.changeDir(f)))
self.addButton(curRow,0,text=filename2unicode(fname),command=(lambda e=None,f=newDir,self=self:self.changeDir(f)))
# TODO if _disabled have an Enable button ?
# if not have a Disable ??
# (NB though the above button will have a column span)
......@@ -697,12 +745,14 @@ class RecorderControls(ButtonScrollingMixin):
focusButton(self.coords2buttons[(curRow,0)])
dirToHighlight = None # done
curRow += 1
if fileExists(self.currentDir+os.sep+fname+os.sep+longDescriptionName): description=u8strip(read(self.currentDir+os.sep+fname+os.sep+longDescriptionName)).strip(wsp)
elif fileExists(self.currentDir+os.sep+fname+os.sep+shortDescriptionName): description=u8strip(read(self.currentDir+os.sep+fname+os.sep+shortDescriptionName)).strip(wsp)
if fileExists(self.currentDir+os.sep+fname+os.sep+longDescriptionName): description=wspstrip(u8strip(read(self.currentDir+os.sep+fname+os.sep+longDescriptionName)))
elif fileExists(self.currentDir+os.sep+fname+os.sep+shortDescriptionName): description=wspstrip(u8strip(read(self.currentDir+os.sep+fname+os.sep+shortDescriptionName)))
else: description=None
if description:
l = Tkinter.Label(self.grid,text=" "+description,wraplength=self.ourCanvas.winfo_screenwidth())
l.grid(row=curRow,column=0,columnspan=1+3*len(self.languagesToDraw),sticky="w")
try: sbarWidth = app.sbarWidth
except: sbarWidth = 16 # default
ll = Tkinter.Label(self.grid,text=" "+description,wraplength=self.ourCanvas.winfo_screenwidth()-sbarWidth-50) # allow for borders on Windows (TODO: is 50px always right?)
ll.grid(row=curRow,column=0,columnspan=1+3*len(self.languagesToDraw),sticky="w")
curRow += 1
if not flwr=="prompts": hadDirectories = True
elif "_" in fname and (languageOverride in allLangs or languageof(fname) in allLangs): # something_lang where lang is a recognised language (don't just take "any _" because some podcasts etc will have _ in them)
......@@ -727,24 +777,24 @@ class RecorderControls(ButtonScrollingMixin):
if iprefix>maxPrefix: maxPrefix=iprefix # max existing numerical prefix
if (flwr.endswith(dotwav) or flwr.endswith(dotmp3) or flwr.endswith(dottxt)): # even if not languageOverride in self.languagesToDraw e.g. for prompts - helps setting up gradint in a language it doesn't have prompts for (creates blank rows for the prefixes that other languages use). TODO do we want to add 'and languageOverride in self.languagesToDraw' if NOT in prompts?
if not prefix in prefix2row:
if not checkIn(prefix,prefix2row):
self.addLabel(curRow,0,utext=filename2unicode(prefix))
foundTxt = {}
for lang in self.languagesToDraw:
if realPrefix+"_"+lang+afterLang+dottxt in l: foundTxt[lang]=(self.currentDir+os.sep+realPrefix+"_"+lang+afterLang+dottxt,2+3*self.languagesToDraw.index(lang))
if checkIn(realPrefix+"_"+lang+afterLang+dottxt,l): foundTxt[lang]=(self.currentDir+os.sep+realPrefix+"_"+lang+afterLang+dottxt,2+3*self.languagesToDraw.index(lang))
prefix2row[prefix] = curRow
for lang in self.languagesToDraw: # preserve tab order
if lang==languageOverride and not flwr.endswith(dottxt):
self.updateFile(fname,curRow,self.languagesToDraw.index(lang),state=1)
languageOverride=None # so not done again
else: self.updateFile(prefix+"_"+lang+dotwav,curRow,self.languagesToDraw.index(lang),state=0,txtExists=(lang in foundTxt))
else: self.updateFile(prefix+"_"+lang+dotwav,curRow,self.languagesToDraw.index(lang),state=0,txtExists=checkIn(lang,foundTxt))
self.gridLabel(lang,curRow)
for filename,col in foundTxt.values(): self.addSynthLabel(filename,curRow+1,col)
curRow += 2
if languageOverride in self.languagesToDraw and not flwr.endswith(dottxt):
if checkIn(languageOverride,self.languagesToDraw) and not flwr.endswith(dottxt):
self.updateFile(fname,prefix2row[prefix],self.languagesToDraw.index(languageOverride),state=1)
elif (flwr.endswith(dotwav) or flwr.endswith(dotmp3)) and tkSnack and not tkSnack=="MicOnly": # no _ in it but we can still play it for splitting
self.addButton(curRow,0,text=(localise("Record from %s") % (filename2unicode(fname),)),command=(lambda e=None,r=curRow,f=self.currentDir+os.sep+fname:self.doRecordFrom(f,r)))
self.addButton(curRow,0,text=(localise("Record from %s") % (filename2unicode(fname),)),command=(lambda e=None,r=curRow,f=self.currentDir+os.sep+fname,self=self:self.doRecordFrom(f,r)))
self.has_recordFrom_buttons = True
curRow += 1
self.addMoreRow = curRow ; self.maxPrefix = maxPrefix+1
......@@ -763,14 +813,24 @@ class RecorderControls(ButtonScrollingMixin):
addButton(r2,localise("Record from file"),self.do_recordFromFile,"left")
if got_program("lame"): self.CompressButton = addButton(r2,localise("Compress all"),self.all2mp3_or_zip,"left") # was "Compress all recordings" but it takes too much width
# TODO else can we see if it's possible to get the encoder on the fly, like in the main screen? (would need some restructuring)
elif got_program("zip") and (explorerCommand or winCEsound): self.CompressButton = addButton(r2,localise("Zip for email"),lambda *args:self.all2mp3_or_zip,"left")
elif got_program("zip") and (explorerCommand or winCEsound): self.CompressButton = addButton(r2,localise("Zip for email"),lambda e=None,self=self:self.all2mp3_or_zip(),"left")
addButton(r2,localise(cond(recorderMode,"Quit","Back to main menu")),self.finished,"left")
if winCEsound and not tkSnack: msg="Click on filenames at left to rename; click synthesized text to edit it"
else: msg="Choose a word and start recording. Then press space to advance (see control at top). You can also browse and manage previous recordings. Click on filenames at left to rename (multi-line pastes are allowed); click synthesized text to edit it."
Tkinter.Label(self.frame,text=msg,wraplength=cond(hasattr(app,"isBigPrint") or olpc or winCEsound,self.ourCanvas.winfo_screenwidth(),min(int(self.ourCanvas.winfo_screenwidth()*.7),512))).grid(columnspan=2) # (512-pixel max. so the column isn't too wide to read on wide screens, TODO increase if the font is large)
if olpc or winCEsound: labelwidth = self.ourCanvas.winfo_screenwidth()
elif hasattr(app,"isBigPrint"): labelwidth = self.ourCanvas.winfo_screenwidth()-50 # allow for borders on Windows (TODO: is 50px always right?)
else: labelwidth=min(int(self.ourCanvas.winfo_screenwidth()*.7),512) # (512-pixel max. so the column isn't too wide to read on wide screens, TODO increase if the font is large)
Tkinter.Label(self.frame,text=msg,wraplength=labelwidth).grid(columnspan=2)
# (Don't worry about making the text files editable - editable filenames should be enough + easier to browse the result outside Gradint; can include both languages in the filename if you like - hope the users figure this out as we don't want to make the instructions too complex)
def reviseCount(num):
# suggested reduction for revision
thresholds=[1,2,knownThreshold,reallyKnownThreshold,meaningTestThreshold,randomDropThreshold,randomDropThreshold2] ; thresholds.sort() ; thresholds.reverse()
for i in range(len(thresholds)-1):
if num>thresholds[i]: return thresholds[i+1]
return 0
def doRecWords(): # called from GUI thread
if hasattr(app,"LessonRow"): app.thin_down_for_lesson() # else recorderMode
app.Label.pack_forget() ; app.CancelRow.pack_forget()
......@@ -780,18 +840,39 @@ def doRecWords(): # called from GUI thread
theRecorderControls.draw()
app.wordsExist = 1 # well not necessarily, but see comments re "Create word list"
# Functions for recording on S60 phones:
# Functions for recording on Android and S60 phones:
def android_recordFile(language):
fname = os.getcwd()+os.sep+"newfile_"+language+dotwav
while True:
android.recorderStartMicrophone(fname) # TODO: python-for-android's MediaRecorderFacade.java startAudioRecording uses default output format and encoder, which likely means that so-called .wav file is really a .3gp file. Have worked around in pcmlen for now, but don't know if the assumptions made there are universal, plus we don't want to name these files .wav if they're not really .wav
android.dialogCreateAlert("Recording",language)
android.dialogSetPositiveButtonText("Stop")
android.dialogShow() ; android.dialogGetResponse()
android.recorderStop()
android.mediaPlay("file://"+fname)
if not getYN("Are you happy with this?"):
os.remove(fname) ; continue
return fname
def android_recordWord():
if not getYN("Ready to record "+secondLanguage+" word?"): return
def ipFunc(prompt,value=u""): return android.dialogGetInput("Gradint",prompt,value).result
droidOrS60RecWord(android_recordFile,ipFunc)
def s60_recordWord():
def ipFunc(prompt,value=u""): return appuifw.query(prompt,"text",value)
droidOrS60RecWord(s60_recordFile,ipFunc)
def droidOrS60RecWord(recFunc,inputFunc):
if secondLanguage==firstLanguage: l1Suffix, l1Display = firstLanguage+"-meaning_"+firstLanguage, "meaning"
else: l1Suffix, l1Display = firstLanguage, firstLanguage
while True:
l2 = s60_recordFile(secondLanguage)
l2 = recFunc(secondLanguage)
if not l2: return
l1 = None
while not l1:
if (not maybeCanSynth(firstLanguage)) or getYN("Record "+l1Display+" too? (else computer voice)"): l1 = s60_recordFile(l1Suffix) # (TODO what if maybeCanSynth(secondLanguage) but not first, and we want to combine 2nd-lang synth with 1st-lang recorded? low priority as if recording will prob want to rec L2)
if (not maybeCanSynth(firstLanguage)) or getYN("Record "+l1Display+" too? (else computer voice)"): l1 = recFunc(l1Suffix) # (TODO what if maybeCanSynth(secondLanguage) but not first, and we want to combine 2nd-lang synth with 1st-lang recorded? low priority as if recording will prob want to rec L2)
else:
l1txt = appuifw.query(u""+firstLanguage+" text:","text")
l1txt = inputFunc(u""+firstLanguage+" text:")
if l1txt:
l1 = "newfile_"+firstLanguage+dottxt
open(l1,"w").write(l1txt.encode("utf-8"))
......@@ -799,21 +880,22 @@ def s60_recordWord():
os.remove(l2) ; break
if not l1: continue
ls = list2set(os.listdir(samplesDirectory))
def inLs(prefix):
for ext in [dotwav,dotmp3,dottxt]:
for l in [firstLanguage,secondLanguage]:
if prefix+"_"+l+ext in ls: return 1
c = 1
while inLs("%02d" % c): c += 1
origPrefix = prefix = u""+("%02d" % c)
def inLs(prefix,ls):
for l in ls:
if l.startswith(prefix) and len(l) > len(prefix) and l[len(prefix)] not in "0123456789": return True
global recCount
try: recCount += 1
except: recCount = 1
while inLs("%02d" % recCount,ls): recCount += 1
origPrefix = prefix = ensure_unicode("%02d" % recCount)
while True:
prefix = appuifw.query(u"Filename:","text",prefix)
prefix = inputFunc(u"Filename:",prefix)
if not prefix: # pressed cancel ??
if getYN("Discard this recording?"):
os.remove(l1) ; os.remove(l2) ; return
recCount-=1;os.remove(l1);os.remove(l2);return
else:
prefix = origPrefix ; continue
if not inLs(prefix) or getYN("File exists. overwrite?"): break
if not inLs(prefix,ls) or getYN("File exists. overwrite?"): break
if samplesDirectory: prefix=samplesDirectory+os.sep+prefix
os.rename(l1,prefix+l1[l1.index("_"):])
os.rename(l2,prefix+l2[l2.index("_"):])
......@@ -822,15 +904,15 @@ def s60_recordFile(language):
fname = "newfile_"+language+dotwav
while True:
S=audio.Sound.open(os.getcwd()+os.sep+fname)
def forgetS():
def forgetS(fname,S):
S.close()
try: os.remove(fname)
except: pass
if not getYN("Press OK to record "+language+" word"): return forgetS()
if not getYN("Press OK to record "+language+" word"): return forgetS(fname,S)
S.record()
ret = getYN("Press OK to stop") ; S.stop()
if not ret:
forgetS() ; continue
forgetS(fname,S) ; continue
S.play()
ret = getYN("Are you happy with this?")
S.stop() ; S.close()
......
# This file is part of the source code of
# gradint v0.9969 (c) 2002-2011 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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,7 +42,7 @@ def anticipation(promptFile,zhFile,numTimesBefore,promptsData):
# work out number of repetitions needed. not sure if this should be configurable somewhere.
first_repeat_is_unessential = 0
if not numTimesBefore: # New word. If there are L2 variants, introduce them all if possible.
numVariants = min(3,len(variantFiles.get(samplesDirectory+os.sep+zhFile,[0]))) # TODO really max to 3? or 4? or .. ?
numVariants = min(3,len(variantFiles.get(B(samplesDirectory)+B(os.sep)+B(zhFile),[0]))) # TODO really max to 3? or 4? or .. ?
if numVariants>1 and lessonIsTight(): numVariants = 1 # hack
numRepeats = numVariants + cond(numVariants>=cond(availablePrompts.user_is_advanced,2,3),0,1)
elif numTimesBefore == 1: numRepeats = 3
......@@ -53,7 +53,7 @@ def anticipation(promptFile,zhFile,numTimesBefore,promptsData):
else: numRepeats = 1
if numRepeats==1:
k,f = synthcache_lookup(zhFile,justQueryCache=1)
if f and k[0]=="_" and not textof(zhFile) in subst_synth_counters:
if f and B(k[:1])==B("_") and not checkIn(textof(zhFile),subst_synth_counters):
# Hack: an experimental cache entry but only 1 repetition - what do we do?
c=random.choice([1,2,3])
if c==1: pass # do nothing
......@@ -65,7 +65,7 @@ def anticipation(promptFile,zhFile,numTimesBefore,promptsData):
theList = []
if instrIsPrefix: theList = instructions
theList.append(promptEvent)
if promptFile==zhFile and not promptFile in singleLinePoems:
if promptFile==zhFile and not checkIn(promptFile,singleLinePoems):
# A multi-line poem with no first-language prompts, so we're using each fragment as a prompt for the next, but the first fragment is its own prompt, which means that 'prompt' is ambiguous. Say "beginning" to disambiguate it.
theList = theList + map(lambda x:fileToEvent(x,promptsDirectory), availablePrompts.getPromptList("begin",promptsData,languageof(zhFile)))
if not instrIsPrefix: theList += instructions
......@@ -111,10 +111,11 @@ def reverseAnticipation(promptFile,zhFile,promptsData):
return CompositeEvent(theList)
def languageof(file):
assert "_" in file, "no _ in %s" % (file,)
s=file[file.rindex("_")+1:]
if extsep in s: return s[:s.rindex(extsep)]
else: return s
file = B(file)
assert B("_") in file, "no _ in %s" % (repr(file),)
s=file[file.rindex(B("_"))+1:]
if B(extsep) in s: return S(s[:s.rindex(B(extsep))])
else: return S(s)
def commentSequence():
sequence = []
......@@ -128,7 +129,7 @@ def anticipationSequence(promptFile,zhFile,start,to,promptsData,introList):
# (try number from 'start' to 'to', EXCLUDING 'to')
sequence = []
# First one has initialGlue() whatever the value of 'start' is
if meaningTestThreshold and to==start+1 and start>meaningTestThreshold and random.choice([1,2])==1 and not type(promptFile)==type([]) and promptFile.find("_"+firstLanguage+extsep)>=0:
if meaningTestThreshold and to==start+1 and start>meaningTestThreshold and random.choice([1,2])==1 and not type(promptFile)==type([]) and B(promptFile).find(B("_"+firstLanguage+extsep))>=0:
# *** not sure about that condition - should the random be more biased?
# (the type() and following condition is a hack that ensures this is not used for poetry etc where there are composite prompts or the prompt is the previous line. TODO would be better to keep track of which samples are poetic, because the above breaks down on the first line of a poem that has a translation into the first language because that looks like a normal prompt/response - but ok for now)
firstItem = reverseAnticipation(promptFile,zhFile,promptsData)
......
# This file is part of the source code of
# gradint v0.9969 (c) 2002-2011 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
......@@ -26,14 +26,18 @@ class Synth(object):
################## don't have to re-implement below
def __init__(self): self.fileCache = {}
def __del__(self):
import os # as it might already have been gc'd
for v in self.fileCache.values():
try: import os # as it might already have been gc'd
except: pass # might still be able to use it
try:
for v in self.fileCache.values():
try: os.remove(v)
except: pass # someone may have removed it already, e.g. cache-synth.py's renaming
except: pass
self.fileCache = {} # essential for the globalEspeakSynth hack (or get crash when try to make multiple lessons to file)
def makefile_cached(self,lang,text):
if type(text)==type([]): textKey=repr(text)
else: textKey=text
if (lang,textKey) in self.fileCache: return self.fileCache[(lang,textKey)]
if checkIn((lang,textKey),self.fileCache): return self.fileCache[(lang,textKey)]
t = self.makefile(lang,text)
self.fileCache[(lang,textKey)] = t
return t
......@@ -64,24 +68,87 @@ def unzip_and_delete(f,specificFiles="",ignore_fail=0):
class OSXSynth_Say(Synth):
def __init__(self): Synth.__init__(self)
def supports_language(self,lang): return lang=="en"
def works_on_this_platform(self): return macsound and fileExists("/usr/bin/say")
def works_on_this_platform(self):
if not (macsound and fileExists("/usr/bin/say")): return False
global osxSayVoicesScan
try: osxSayVoicesScan # singleton
except: osxSayVoicesScan = self.scanVoices()
self.voices = osxSayVoicesScan ; return True
def supports_language(self,lang): return checkIn(lang,self.voices)
def guess_length(self,lang,text): return quickGuess(len(text),12) # TODO need a better estimate
def play(self,lang,text): return system("say \"%s\"" % (text.replace('"',''),))
def play(self,lang,text): return system("say %s\"%s\"" % (S(self.voices[lang]),S(self.transliterate(lang,text)).replace('"','')))
# TODO 10.7+ may also support -r rate (WPM), make that configurable in advanced.txt ?
def makefile(self,lang,text):
fname = os.tempnam()+extsep+"aiff"
system("say -o %s \"%s\"" % (fname,text.replace('"','')))
system("say %s-o %s \"%s\"" % (S(self.voices[lang]),fname,S(self.transliterate(lang,text)).replace('"','')))
return aiff2wav(fname)
def transliterate(self,lang,text,forPartials=0):
if not self.voices[lang] in ['-v "Ting-Ting" ','-v "Tingting" ']: return text
# The hanzi-to-pinyin conversion in the Ting-Ting voice is not always as good as eSpeak's, but it can be controlled with pinyin.
ut = ensure_unicode(text)
if u"\u513f" in ut or u"\u5152" in ut: return text # might be erhua - better pass to the synth as-is
es = ESpeakSynth()
if not es.works_on_this_platform() or not es.supports_language('zh'): return text
return es.transliterate('zh',text,forPartials)
def can_transliterate(self,lang):
if not self.voices.get(lang,0) in ['-v "Ting-Ting" ','-v "Tingting" ']: return 0
es = ESpeakSynth()
return es.works_on_this_platform() and es.supports_language('zh')
def scanVoices(self):
d = {}
try:
from AppKit import NSSpeechSynthesizer
voiceAttrs=[NSSpeechSynthesizer.attributesForVoice_(vocId) for vocId in NSSpeechSynthesizer.availableVoices()]
except: # maybe we're running under Homebrew Python instead of /usr/bin/python; in at least some recent OS X versions we should be able to get a voice list with 'say -v ?' instead (I'm not sure how far back that goes, so leaving in the above NSSpeechSynthesizer method as well)
voiceAttrs = []
for l in os.popen('say -v "?" </dev/null 2>/dev/null').readlines():
if not '#' in l: continue
name,lang=l[:l.index('#')].rsplit(None,1)
voiceAttrs.append({'VoiceName':name,'VoiceLanguage':lang.replace('_','-')})
if not voiceAttrs: return {"en":""} # maybe we're on ancient OS X: don't use a -v parameter at all
for vocAttrib in voiceAttrs:
if not checkIn('VoiceName',vocAttrib): continue
if not checkIn('VoiceLanguage',vocAttrib):
lang={"Damayanti":"id","Maged":"ar","Stine":"nb"}.get(vocAttrib['VoiceName'],None) # TODO: can sometimes use VoiceLocaleIdentifier instead, dropping the _ part (but can't even do that with Damayanti on 10.7)
if not lang: continue # TODO: output VoiceName in a warning?
else: lang = vocAttrib['VoiceLanguage']
if '-' in lang: lang=lang[:lang.index("-")]
d.setdefault(lang,[]).append(vocAttrib['VoiceName'].encode('utf-8'))
found=0 ; d2=d.copy()
class BreakOut(Exception): pass
# First, check for voice matches in same language beginning
for k,v in list(d.items()):
if k in macVoices:
try:
for m in macVoices[k].split():
for vv in v:
if B(m.lower()) == B(vv.lower()):
d2[k] = [S(vv)] ; found=1 ; del macVoices[k] ; raise BreakOut()
except BreakOut: pass
if len(d2[k])>1: d2[k]=[d2[k][0]]
# Then check across languages (e.g. cant -> zh-...)
for k,v in list(macVoices.items()):
try:
for kk,vv in list(d.items()):
for m in v.split():
for vvv in vv:
if B(m.lower()) == B(vvv.lower()):
d2[k] = [S(vvv)] ; found=1 ; raise BreakOut()
except BreakOut: pass
if list(d.keys())==['en'] and not found: return {"en":""} # just use the default
for k,v in list(d2.items()): d2[k]='-v "'+S(v[0])+'" '
return d2
def aiff2wav(fname):
if not system("sox \"%s\" \"%s\"" % (fname,fname[:-4]+"wav")):
# good, we converted it to wav
os.remove(fname)
fname=fname[:-4]+"wav"
# else just return aiff and hope for the best (TODO won't work with cache-synth)
# else just return aiff and hope for the best (TODO won't work with cache-synth; TODO can get here when 'say' gave empty output, e.g. just a dot, and the returned aiff might raise IOError when constructing a SampleEvent)
return fname
class OSXSynth_OSAScript(Synth):
# for old Macs that don't have a "say" command
def __init__(self): Synth.__init__(self)
def supports_language(self,lang): return lang=="en"
def works_on_this_platform(self): return macsound and fileExists("/usr/bin/osascript")
......@@ -91,21 +158,31 @@ class OSXSynth_OSAScript(Synth):
fname = os.tempnam()+extsep+"aiff"
os.popen("osascript","w").write('say "%s" saving to "%s"\n' % (text,fname))
return aiff2wav(fname)
# TODO: if the user has installed an OS X voice that supports another language, can use say -v voicename ( or 'using \"voicename\"' for the osascript version ) (but I have no access to a suitably-configured Mac for testing this)
class OldRiscosSynth(Synth):
def __init__(self): Synth.__init__(self)
def supports_language(self,lang): return lang=="en"
def works_on_this_platform(self): return riscos_sound and not os.system("sayw .")
def works_on_this_platform(self): return riscos_sound and got_program("*sayw")
def guess_length(self,lang,text): return quickGuess(len(text),12) # TODO need a better estimate
def play(self,lang,text): return system("sayw %s" % (text,))
class S60Synth(Synth):
class S60Synth(Synth): # TODO: figure out if S60 Python can call external programs; might be able to use eSpeak http://pvagner.webranet.sk/espeak/espeak.sisx
def __init__(self): Synth.__init__(self)
def supports_language(self,lang): return lang=="en" # (audio.say always uses English even when other languages are installed on the device)
def works_on_this_platform(self): return appuifw and hasattr(audio,"say")
def guess_length(self,lang,text): return quickGuess(len(text),12) # TODO need a better estimate
def play(self,lang,text): audio.say(text)
def play(self,lang,text):
if not text=="Error in graddint program.": # (just in case it's unclear)
if text.endswith(';'): doLabel(text[:-1])
else: doLabel(text)
audio.say(text)
class AndroidSynth(Synth):
def __init__(self): Synth.__init__(self)
def supports_language(self,lang): return lang==systemVoice # TODO others? (but ttsSpeak can't set language)
def works_on_this_platform(self): return android
def guess_length(self,lang,text): return quickGuess(len(text),12) # TODO need a better estimate
def play(self,lang,text): android.ttsSpeak(text)
if winsound or mingw32: toNull=" > nul"
else: toNull=" >/dev/null" # stdout only, not stderr, because we want to see any errors that happen
......@@ -127,9 +204,10 @@ class PttsSynth(Synth):
if not self.program:
for i in ["ptts.exe","ptts-offline.exe"]:
if fileExists(i):
if cygwin: self.program='"'+os.getcwd()+cwd_addSep+i+'"' # (don't just do this anyway because some windows systems don't like it)
# must keep the full path even on non-cygwin because we're adding ,1 to changeToDirOf (hope we don't hit a Windows version that doesn't like this). But we can keep relative paths if tempdir_is_curdir. (TODO if this breaks when not tempdir_is_curdir, could try copying ptts.exe to temp, but would need to delete it afterwards)
if cygwin or not tempdir_is_curdir: self.program='"'+os.getcwd()+cwd_addSep+i+'"'
else: self.program = i
self.offlineOnly = 'offline' in i
self.offlineOnly = checkIn('offline',i)
break
if not self.program:
# (in case someone's running on Windows from source)
......@@ -138,15 +216,15 @@ class PttsSynth(Synth):
else: self.lily_file = lily_file
if fileExists(self.lily_file):
self.old_lily_data=read(self.lily_file)
if "zh" in sapiVoices and sapiVoices["zh"][0].lower()=="vw lily": del sapiVoices["zh"] # because we don't want to bypass our own interface to lily if a user set that without realising it's not needed
if checkIn("zh",sapiVoices) and sapiVoices["zh"][0].lower()=="vw lily": del sapiVoices["zh"] # because we don't want to bypass our own interface to lily if a user set that without realising it's not needed
else: self.lily_file = None
def supports_language(self,lang): return lang in sapiVoices or lang=="en" or (self.lily_file and lang=="zh")
def supports_language(self,lang): return checkIn(lang,sapiVoices) or lang=="en" or (self.lily_file and lang=="zh")
# Voice list: os.popen("echo | "+self.program+" -vl").read().split("\n"). If any .lower() contain "espeak-" then after the "-" is an espeak language code see ESpeakSynth (it may not want unicode). Other voices may also have support for specific languages - may sometimes be able to use <lang langid="locale-hex-code"/> (e.g. 809 UK, 804 Chinese (PRC) 404 Taiwan, 411 Japan) but no way for gradint to tell if successful
def works_on_this_platform(self): return self.program
def guess_length(self,lang,text): return quickGuess(len(text),12) # TODO need a better estimate, especially if they're going to set the speed in the control panel!
def play(self,lang,text):
if self.offlineOnly: return SampleEvent(self.makefile_cached(lang,text)).play()
if lang in sapiVoices:
if checkIn(lang,sapiVoices):
d=os.getcwd()
ret=self.sapi_unicode(sapiVoices[lang][0],ensure_unicode(text),speed=sapiSpeeds.get(lang,None))
os.chdir(d) ; return ret
......@@ -161,7 +239,8 @@ class PttsSynth(Synth):
os.chdir(d) ; return ret
def sapi_unicode(self,voice,unicode_string,toFile=None,sampleRate=None,speed=None):
# Speaks unicode_string in 'voice'. toFile (if present) must be something that was returned by tempnam. May change the current directory.
unifile=os.tempnam() ; open(unifile,"wb").write(codecs.utf_16_encode(unicode_string)[0])
if voice=="Ekho Cantonese": unicode_string = preprocess_chinese_numbers(fix_compatibility(unicode_string),isCant=2) # hack to duplicate the functionality of EkhoSynth
unifile=os.tempnam() ; write(unifile,codecs.utf_16_encode(unicode_string)[0])
if not toFile: extra=""
else:
extra=' -w '+changeToDirOf(toFile,1)+' -c 1'
......@@ -174,13 +253,18 @@ class PttsSynth(Synth):
def makefile(self,lang,text):
fname = os.tempnam()+dotwav
oldcwd=os.getcwd()
if lang in sapiVoices: self.sapi_unicode(sapiVoices[lang][0],ensure_unicode(text),fname,sapiVoices[lang][1],speed=sapiSpeeds.get(lang,None))
elif lang=="en": os.popen(self.program+speed+' -c 1 -w '+changeToDirOf(fname,1)+self.speedParam(sapiSpeeds.get(lang,None))+toNull,"w").write(text+"\n") # (can specify mono but can't specify sample rate if it wasn't mentioned in sapiVoices - might make en synth-cache bigger than necessary but otherwise no great problem)
if checkIn(lang,sapiVoices): r=self.sapi_unicode(sapiVoices[lang][0],ensure_unicode(text),fname,sapiVoices[lang][1],speed=sapiSpeeds.get(lang,None))
elif lang=="en":
p=os.popen(self.program+' -c 1 -w '+changeToDirOf(fname,1)+self.speedParam(sapiSpeeds.get(lang,None))+toNull,"w") # (can specify mono but can't specify sample rate if it wasn't mentioned in sapiVoices - might make en synth-cache bigger than necessary but otherwise no great problem)
p.write(text+"\n")
r=p.close()
elif lang=='zh':
self.sapi_unicode("VW Lily",self.preparePinyinPhrase(text),fname,16000,speed=sapiSpeeds.get(lang,None))
r=self.sapi_unicode("VW Lily",self.preparePinyinPhrase(text),fname,16000,speed=sapiSpeeds.get(lang,None))
self.restore_lily_dict()
else: r=0 # shouldn't get here
os.chdir(oldcwd)
d = sapi_sox_bug_workaround(read(fname)); open(fname,"wb").write(d)
assert not r,"ptts.exe failed"
d = sapi_sox_bug_workaround(read(fname)); write(fname,d)
if cygwin: os.system("chmod -x '"+fname+"'")
return fname
def preparePinyinPhrase(self,pinyin):
......@@ -214,9 +298,9 @@ class PttsSynth(Synth):
kVal="_%d" % count ; count += 1 # better not make kVal too long otherwise the voice can insert awkward pauses
dicWrite.append('"%s","%s","p"\r\n' % (kVal,p2))
rVal.append(p.replace(p2,kVal)) # (leave in full stops etc; assumes p2 is a substring of p, which is why hyphens are taken out before stripPunc)
open(self.lily_file,"wb").write(''.join(dicWrite))
write(self.lily_file,''.join(dicWrite))
return ''.join(rVal).replace('@','') # (WITHOUT spaces, otherwise pauses far too much)
def restore_lily_dict(self): open(self.lily_file,"wb").write(self.old_lily_data) # done ASAP rather than on finalise, because need to make sure it happens (don't leave the system in an inconsistent state for long)
def restore_lily_dict(self): write(self.lily_file,self.old_lily_data) # done ASAP rather than on finalise, because need to make sure it happens (don't leave the system in an inconsistent state for long)
def sapi_sox_bug_workaround(wavdata):
# SAPI writes an 'EVNT' block after the sound data, and some versions of sox don't recognise this. NB this hack is not very portable (relies on SAPI5 guaranteeing to write exactly one EVNT chunk and the bytes 'EVNT' never occur inside it, otherwise would need proper parsing)
f=wavdata.rfind("EVNT")
......@@ -226,13 +310,14 @@ py_final_letters="aeginouvrAEGINOUVR:" # (don't just pick up on tone numbers, bu
def sort_out_pinyin_3rd_tones(pinyin):
# Tone sandhi blocking rules: Need to stop 3rd-tones sortout at end of any 2-syllable word + "gei3 ni3" + "wo3 xiang3".
# Also need to stop at phrase breaks and any English word (or hanzi, although may get awkward cases with 3rd-tone hanzi mixed with pinyin, but that's no big worry as lily isn't too reliable anyway and with partials it'll be transliterated)
segments = [] ; thisSeg = "" ; syls = 0
def endsWithSpecialWordpair(segLower): return segLower.endswith("gei3 ni3") or segLower.endswith("gei3 wo3") or segLower.endswith("ni3 xiang3") or segLower.endswith("wo3 xiang3")
for c in pinyin:
if ord(c)>128 or c in ".,?;" or (c==" " and syls==2) or endsWithSpecialWordpair(thisSeg.lower()):
segments.append(thisSeg) ; thisSeg="" ; syls = 0
elif c==" ": syls = 0
elif c in "12345": syls += 1
segments = [] ; thisSeg = B("") ; syls = 0 ; pinyin=B(pinyin)
def endsWithSpecialWordpair(segLower): return segLower.endswith(B("gei3 ni3")) or segLower.endswith(B("gei3 wo3")) or segLower.endswith(B("ni3 xiang3")) or segLower.endswith(B("wo3 xiang3"))
for i in xrange(len(pinyin)):
c = pinyin[i:i+1]
if ord(c)>128 or c in B(".,?;") or (c==B(" ") and syls==2) or endsWithSpecialWordpair(thisSeg.lower()):
segments.append(thisSeg) ; thisSeg=B("") ; syls = 0
elif c==B(" "): syls = 0
elif c in B("12345"): syls += 1
thisSeg += c
segments.append(thisSeg)
# Now go for each segment
......@@ -240,19 +325,19 @@ def sort_out_pinyin_3rd_tones(pinyin):
for seg in segments:
i=0
while i<len(seg):
while i<len(seg) and seg[i] not in '12345': i+=1
if i<len(seg) and seg[i]=='3' and i and seg[i-1] in py_final_letters:
while i<len(seg) and seg[i:i+1] not in B('12345'): i+=1
if i<len(seg) and seg[i:i+1]==B('3') and i and seg[i-1:i] in B(py_final_letters):
toneToChange = i ; numThirdsAfter = 0
j = i
while True:
j += 1
while j<len(seg) and seg[j] not in '12345': j+=1
if j<len(seg) and seg[j]=='3' and seg[j-1] in py_final_letters: numThirdsAfter+=1
while j<len(seg) and seg[j:j+1] not in B('12345'): j+=1
if j<len(seg) and seg[j:j+1]==B('3') and seg[j-1:j] in B(py_final_letters): numThirdsAfter+=1
else: break
if numThirdsAfter % 2: seg=seg[:toneToChange]+'2'+seg[toneToChange+1:]
if numThirdsAfter % 2: seg=seg[:toneToChange]+B('2')+seg[toneToChange+1:]
i += 1
ret.append(seg)
return "".join(ret)
return B("").join(ret)
class FliteSynth(Synth):
def __init__(self): Synth.__init__(self)
......@@ -268,10 +353,10 @@ if winsound or mingw32 or cygwin: del FliteSynth.play # because on some (even hi
if macsound:
# See if we need to unpack eSpeak and/or set the variables
import commands
f=commands.getoutput("echo espeak*-OSX.zip")
f=getoutput("echo espeak*-OSX.zip")
if fileExists(f): unzip_and_delete(f)
f=commands.getoutput("echo espeak*/speak")
if not os.system("espeak*/speak64 -h >/dev/null 2>/dev/null"): os.system("mv espeak*/speak64 espeak*/speak") # because some newer macOS releases want 64bit-only apps, and we won't need the dual PPC/i386 one on those
f=getoutput("echo espeak*/speak")
if fileExists(f) and fileExists(f[:-5]+"espeak-data/phontab"):
os.environ["ESPEAK_DATA_PATH"]=os.getcwd()+cwd_addSep+f[:-6]
os.environ["PATH"]=os.getcwd()+cwd_addSep+f[:-6]+":"+os.environ["PATH"]
......@@ -295,22 +380,27 @@ espeak_language_aliases = { "cant":"zhy" }
# (maybe not as 'cant' is probably the only example for now)
class SimpleZhTransliterator(object): # if not got eSpeak on system
def transliterate(self,lang,text,forPartials=1,from_espeak=0):
if lang=="zh" and not text.find("</")>-1: # (not </ - don't do this if got SSML)
text = preprocess_chinese_numbers(fix_compatibility(ensure_unicode(text))).encode("utf-8")
found=0
for t in text:
if ord(t)>=128:
found=1 ; break
if not found and text.lower()==fix_pinyin(text,[]): return text # don't need espeak
elif from_espeak: return [text] # This transliterate() and ESpeakSynth's transliterate() work together - don't call espeak if there aren't any special characters (this saves launching a lot of espeak processes unnecessarily when synthing from partials), but DO proceed if fix_pinyin changes something, as in this case we need to check for embedded en words so fix_pinyin doesn't add spurious 5's, + embedded letters etc.
elif not found: return fix_pinyin(text,[]) # No ESpeak on system and fix_pinyin needed to do something - best we can do is hope there aren't any embedded English words (because if there are, they'll have spurious 5's added)
def can_transliterate(self,lang): return lang=="zh"
def transliterate(self,lang,text,forPartials=1,for_espeak=0):
text = B(text)
if not lang=="zh": return text
if text.find(B("</")) > -1: return text # (SSML)
text = preprocess_chinese_numbers(fix_compatibility(ensure_unicode(text))).replace(u'\u0144g','ng2').replace(u'\u0148g','ng3').replace(u'\u01f9g','ng4').encode("utf-8") # (ng2/3/4 substitution here because not all versions of eSpeak can do it)
found=0
for i in xrange(len(text)):
if ord(text[i:i+1])>=128:
found=1 ; break
if not found and text.lower()==fix_pinyin(text,[]): return text # don't need espeak if no non-ASCII (but DO need espeak if fix_pinyin changes something, as in this case we need to check for embedded en words so fix_pinyin doesn't add spurious 5's, + embedded letters etc)
elif for_espeak:
for s,r in [('\xc4\x80', '\xc4\x81'), ('\xc3\x81', '\xc3\xa1'), ('\xc7\x8d', '\xc7\x8e'), ('\xc3\x80', '\xc3\xa0'), ('\xc4\x92', '\xc4\x93'), ('\xc3\x89', '\xc3\xa9'), ('\xc4\x9a', '\xc4\x9b'), ('\xc3\x88', '\xc3\xa8'), ('\xc5\x8c', '\xc5\x8d'), ('\xc3\x93', '\xc3\xb3'), ('\xc7\x91', '\xc7\x92'), ('\xc3\x92', '\xc3\xb2')]: text = text.replace(LB(s),LB(r)) # work around espeak bug where capital pinyin letters with tone marks can result in bad transiterations
return [text] # as list so ESpeakSynth's transliterate_multiple will further process it
elif not found: return fix_pinyin(text,[]) # No ESpeak on system and fix_pinyin needed to do something - best we can do is hope there aren't any embedded English words (because if there are, they'll have spurious 5's added)
simpleZhTransliterator = SimpleZhTransliterator()
def shell_escape(text):
text = text.replace('\\','\\\\').replace('"','\\"')
if unix: text=text.replace("$","\\$").replace("`","\\`").replace("!","! ")
return '"'+text+'"'
text = B(text).replace(B('\\'),B('\\\\')).replace(B('"'),B('\\"'))
if unix: text=text.replace(B("$"),B("\\$")).replace(B("`"),B("\\`")).replace(B("!"),B("! "))
return B('"')+text+B('"')
espeakTranslitCacheFile = "espeak-translit-cache"+extsep+"bin" # TODO to advanced.txt?
class ESpeakSynth(Synth):
......@@ -318,11 +408,14 @@ class ESpeakSynth(Synth):
Synth.__init__(self)
self.languages = {} ; self.program=""
tryList = []
if riscos_sound and 'eSpeak$dir' in os.environ: tryList=[os.environ['eSpeak$dir']+'.espeak-dat',os.environ['eSpeak$dir']+'.espeak-data']
if riscos_sound:
if checkIn('eSpeak$Dir',os.environ):
os.system("RMEnsure speak 3.49 Run <eSpeak$Dir>.speakmod")
tryList=[os.environ['eSpeak$Dir']+'.espeak-dat',os.environ['eSpeak$Dir']+'.espeak-data']
elif winsound or mingw32: tryList=[programFiles+"\\eSpeak\\espeak-data"]
elif winCEsound: tryList=["\\espeak-data"] # Can't try \\Storage Card because our eSpeak compile can't cope with spaces (and quoting it does not work)
else:
tryList=[os.environ.get("HOME","")+"espeak-data","/usr/share/espeak-data","/usr/local/share/espeak-data"]
tryList=[os.environ.get("HOME","")+"espeak-data","/usr/share/espeak-data","/usr/local/share/espeak-data","/usr/lib/x86_64-linux-gnu/espeak-data"]
if cygwin: tryList.append(programFiles+"/eSpeak/espeak-data")
if os.environ.get("ESPEAK_DATA_PATH",""): tryList.insert(0,os.environ["ESPEAK_DATA_PATH"]+os.sep+"espeak-data")
langList = []
......@@ -332,6 +425,13 @@ class ESpeakSynth(Synth):
langList = os.listdir(self.place+os.sep+"voices")
except: self.place = None
if langList: break
if unix: # espeak might know where its data is
if not self.place:
versionLine = (filter(lambda x:x.strip(),os.popen("(speak --help||espeak --help) 2>/dev/null").read().split("\n"))+[""])[0]
if versionLine.find("Data at:")>=0:
self.place = versionLine[versionLine.index("Data at:")+8:].strip()
try: langList = os.listdir(self.place+os.sep+"voices")
except: self.place = None
for l in langList[:]:
if l in ["default","!v","mb"]: langList.remove(l)
elif isDirectory(self.place+os.sep+"voices"+os.sep+l):
......@@ -340,40 +440,42 @@ class ESpeakSynth(Synth):
else: self._add_lang(l,l)
self.theProcess = None
self.translitCache = {}
if pickle and fileExists(espeakTranslitCacheFile):
if self.place:
if pickle and fileExists(espeakTranslitCacheFile):
try: placeStat,tc = pickle.Unpickler(open(espeakTranslitCacheFile,"rb")).load()
except: placeStat,tc = (),{}
if placeStat==tuple(os.stat(self.place)): self.translitCache = tc # otherwise regenerate it because eSpeak installation has changed (TODO if you overwrite an existing _dict file in-place, it might not update the stat() of espeak-data and the cache might not be re-generated when it should; espeak's --compile seems ok though)
if self.place: self.place=self.place[:self.place.rindex(os.sep)] # drop the \espeak-data, so can be used in --path=
self.place=self.place[:self.place.rindex(os.sep)] # drop the \espeak-data, so can be used in --path=
def _add_lang(self,lang,fname):
lang,fname = S(lang),S(fname)
if "~" in lang: return # emacs backup files
self.languages[lang]=fname
for l in open(self.place+os.sep+"voices"+os.sep+fname).read(256).replace("\r","\n").split("\n"):
if l.startswith("language "):
l=l[9:].strip(wsp)
if not l==lang:
if l in espeak_language_aliases.values(): # aliasing to an alias - update it
for k,v in espeak_language_aliases.items():
if v==l: espeak_language_aliases[k]=lang
espeak_language_aliases[l] = lang
for l in open(self.place+os.sep+"voices"+os.sep+fname,"rb").read(256).replace(B("\r"),B("\n")).split(B("\n")):
if l.startswith(B("language ")):
l=bwspstrip(l[9:])
if not l==B(lang):
Sl = S(l)
if Sl in espeak_language_aliases.values(): # aliasing to an alias - update it
for k,v in list(espeak_language_aliases.items()):
if v==Sl: espeak_language_aliases[k]=lang
espeak_language_aliases[Sl] = lang
def describe_supported_languages(self):
ret=[]
items=self.languages.items() ; items.sort()
items=list(self.languages.items()) ; items.sort()
for k,v in items:
if "-" in k and not k=="zh-yue": continue # skip variants in the report (but do recognise them)
o=open(self.place+os.sep+"espeak-data"+os.sep+"voices"+os.sep+v)
line=""
o=open(self.place+os.sep+"espeak-data"+os.sep+"voices"+os.sep+v,'rb')
for t in range(10):
line=o.readline()
if line.find("name")>-1:
lname = line.split()[1].replace("_test","").replace("-test","").replace("-experimental","").replace("-expertimental","") # (delete the -test etc for more screen real-estate, as this is used only for explaining what the language abbreviations mean)
if line.find(B("name"))>=0:
lname = S(line.split()[1]).replace("_test","").replace("-test","").replace("-experimental","").replace("-expertimental","") # (delete the -test etc for more screen real-estate, as this is used only for explaining what the language abbreviations mean)
if not lname: continue
lname=lname[0].upper()+lname[1:]
ret.append(k+"="+lname)
break
return " ".join(ret)
def supports_language(self,lang): return espeak_language_aliases.get(lang,lang) in self.languages
def not_so_good_at(self,lang): return lang not in ['en'] # 'zh' is sort-of OK, but if the user has explicitly installed another synth for it then had probably better defer to that
def supports_language(self,lang): return checkIn(espeak_language_aliases.get(lang,lang),self.languages)
def not_so_good_at(self,lang): return not checkIn(lang,prefer_espeak)
def works_on_this_platform(self):
if len(self.languages.items())==0: return 0
if winCEsound:
......@@ -385,7 +487,7 @@ class ESpeakSynth(Synth):
elif cygwin: toTry=[programFiles+"/eSpeak/command_line/espeak.exe"]
else: toTry = []
if toTry: # windows or cygwin
if "ESPEAK_DATA_PATH" in os.environ:
if checkIn("ESPEAK_DATA_PATH",os.environ):
toTry.insert(0,os.environ["ESPEAK_DATA_PATH"]+os.sep+"espeak.exe")
toTry.insert(0,os.environ["ESPEAK_DATA_PATH"]+os.sep+"command_line"+os.sep+"espeak.exe")
for t in toTry:
......@@ -395,11 +497,9 @@ class ESpeakSynth(Synth):
return True
return False
else: # not windows or cygwin
if macsound and not got_qtplay: return False # Rosetta required (TODO need to check espeak separately in case they've compiled it x86)
self.program="speak"
if riscos_sound: return True # we've already confirmed <eSpeak$dir> works in the constructor
import commands
loc=commands.getoutput("locale -a|grep -i 'utf-*8$'|head -1").strip(wsp)
if riscos_sound: return True # we've already confirmed <eSpeak$Dir> works in the constructor (if it doesn't, we'll just have an empty language list)
loc=wspstrip(getoutput("locale -a 2>/dev/null|grep -i 'utf-*8$'|head -1"))
if loc: loc="LC_CTYPE="+loc+" " # in case espeak can't find a utf-8 locale by itself
self.program=loc+"speak"
if got_program("speak"): return True
......@@ -407,26 +507,38 @@ class ESpeakSynth(Synth):
self.program = loc+"espeak"
return got_program("espeak")
def guess_length(self,lang,text):
if text.find("</")>-1: # might be SSML - don't count inside <...>
text=B(text)
if text.find(B("</"))>=0: # might be SSML - don't count inside <...>
l=inSsml=0
for c in text:
if c=="<": inSsml=1
elif c==">": inSsml=0
for i in xrange(len(text)):
c = text[i:i+1]
if c==B("<"): inSsml=1
elif c==B(">"): inSsml=0
elif not inSsml: l += 1
else: l=len(text)
return quickGuess(l,12)+cond(winCEsound,1.3,0) # TODO need a better estimate. Overhead on 195MHz Vario (baseline?) >1sec (1.3 seems just about ok)
latency = 0
if winCEsound: latency = 1.3 # TODO need a better estimate. Overhead on 195MHz Vario (baseline?) >1sec (1.3 seems just about ok)
elif unix:
if espeak_pipe_through and not outputFile:
if not hasattr(self,"latency"):
t = time.time()
self.play("en","")
self.latency = time.time() - t # 2secs on eeePC Ubuntu 11.10, mostly AFTER the utterance
if self.latency > 0.5: show_info("espeak_pipe_through latency is "+str(int(self.latency*10)/10.0)+"\n",True)
latency = self.latency
return quickGuess(l,12)+latency
def can_transliterate(self,lang): return espeak_language_aliases.get(lang,lang) in ["zh","zhy","zh-yue"] and not riscos_sound # TODO it's OK on RISC OS if the eSpeak version is recent enough to do --phonout=filename; TODO aliases for zhy (but not usually a problem as can_transliterate is called only for preference)
def winCE_run(self,parameters,expectedOutputFile,infileToDel=None):
self.winCE_start(parameters)
time.sleep(0.3) # 0.2 not always long enough for transliterations (get empty output file if try to read too soon, then loop waiting for it to have contents)
return self.winCE_wait(expectedOutputFile,infileToDel)
def winCE_start(self,parameters):
s = ShellExecuteInfo(60,File=u""+self.program,Parameters=u"--path="+self.place+" "+parameters,fMask=0x40)
s = ShellExecuteInfo(60,File=ensure_unicode(self.program),Parameters=u"--path="+self.place+" "+parameters,fMask=0x40)
ctypes.cdll.coredll.ShellExecuteEx(ctypes.byref(s))
self.hProcess = s.hProcess # TODO check it's not NULL (failed to run)
def winCE_wait(self,expectedOutputFile,infileToDel=None):
def winCE_wait(self,expectedOutputFile,infileToDel=None,needDat=1):
# won't always work: if app and not app.Label["text"].strip(): app.setLabel("Waiting for eSpeak") # in case it doesn't produce output
ctypes.cdll.coredll.WaitForSingleObject(self.hProcess,4294967295) # i.e. 0xFFFFFFFF but that throws up a warning on Python 2.3
ctypes.cdll.coredll.WaitForSingleObject(self.hProcess,long(65535)*long(65537)) # i.e. 0xFFFFFFFF but that throws up a warning on Python 2.3; Python 2.1 won't accept 4294967295 without L but Python 3 says syntax error if L, so need to multiply
ctypes.cdll.coredll.CloseHandle(self.hProcess)
# In some rare circumstances, that command won't wait (e.g. process hadn't started despite the fact we delayed), so check the output files also.
# (Leave WaitForSingleObject in as it could save some CPU cycles / potential OS crashes on some WinCE versions)
......@@ -434,10 +546,12 @@ class ESpeakSynth(Synth):
while True:
if firstIter: firstIter -= 1
else: time.sleep(0.2),check_for_interrupts() # (latter needed in case it gets stuck)
try: dat=read(u""+expectedOutputFile)
try:
if needDat: dat=read(ensure_unicode(expectedOutputFile))
else: dat=open(ensure_unicode(expectedOutputFile)).read(8)
except: continue # error on trying to read output
if not dat: continue # output read as empty
if expectedOutputFile.endswith(dotwav) and (len(dat)<8 or dat[6:8]=="\xff\x7f"): continue # length field not yet written
if expectedOutputFile.endswith(dotwav) and (len(dat)<8 or dat[6:8]==LB("\xff\x7f")): continue # length field not yet written
# (TODO how could we make sure a long transliteration has finished, if the OS lets us open the file before done and if WaitForSingleObject doesn't work?)
if not firstIter: time.sleep(0.2) # just in case
if infileToDel:
......@@ -446,7 +560,7 @@ class ESpeakSynth(Synth):
return dat
def update_translit_cache(self,lang,textList): # forPartials=1 assumed
if not lang=="zh": return # TODO if expand 'transliterate' to do other languages, make sure to update this also, and the cache format
if self.translitCache: textList=filter(lambda x:x not in self.translitCache, textList)
if self.translitCache: textList=filter(lambda x,self=self:not checkIn(x,self.translitCache), textList)
step = 1000 # should be about right?
for i in range(0,len(textList),step):
tl = textList[i:i+step]
......@@ -458,94 +572,135 @@ class ESpeakSynth(Synth):
try: pickle.Pickler(open(espeakTranslitCacheFile,"wb"),-1).dump((tuple(os.stat(self.place+os.sep+"espeak-data")),self.translitCache))
except IOError: pass # 'permission denied' is ok
def transliterate(self,lang,text,forPartials=1):
if lang=="zh" and text in self.translitCache: return self.translitCache[text] # (TODO add "and forPartials"? but don't need to bother with this extra condition on slow systems)
if lang=="zh" and checkIn(text,self.translitCache): return self.translitCache[text] # (TODO add "and forPartials"? but don't need to bother with this extra condition on slow systems)
return self.transliterate_multiple(lang,[text],forPartials)[0] # and don't cache it - could be experimental, and we don't want cache to grow indefinitely
if unix:
def check_dicts(self,lang,txt):
if not hasattr(self,"dictsChecked"): self.dictsChecked = {}
if checkIn(lang,self.dictsChecked) or not lang in ["zh","zhy","ru"]: return
if type(txt)==list: txt=B("").join(txt)
if re.match(B("^[ -~]*$"),txt): return # don't need to warn about dictionaries if we were given all-ASCII input (TODO: and tone marks?)
if filelen(self.place+os.sep+"espeak-data"+os.sep+lang+"_dict")<100000: show_warning("Warning: the eSpeak on this system has only a short dictionary for language '"+lang+"' - please install the Additional Data at espeak.sourceforge.net/data")
# TODO: Ubuntu 18.04's espeak-data package does NOT include the additional data; IS included by espeak-ng-data; on unix prefer the espeak-ng command and ask it for its own data path?
self.dictsChecked[lang]=1
else:
def check_dicts(self,lang,txt): pass
def transliterate_multiple(self,lang,textList,forPartials=1,keepIndexList=0):
# Call eSpeak once for multiple transliterations, for greater efficiency (especially on systems where launching a process is expensive e.g. WinCE).
# Note: Don't make textList TOO long, because the resulting data must fit on the (RAM)disk and in memory.
retList = [] ; write_to_espeak = [] ; indexList = []
split_token = "^^^" # must be something not defined in the _rules files
for text in textList:
textList = map(lambda x:B(x), textList)
self.check_dicts(lang,textList)
for text in textList: # DON'T escape_jyutping (treat as en words)
if lang=="zh":
if keepIndexList: # making the cache - can we go a bit faster?
try: t = unicode(text,"ascii") # if no utf, know is OK (but ONLY if keepIndexList, as the result is imprecise)
except UnicodeDecodeError: t = simpleZhTransliterator.transliterate(lang,text,from_espeak=1)
else: t = simpleZhTransliterator.transliterate(lang,text,from_espeak=1)
except UnicodeDecodeError: t = simpleZhTransliterator.transliterate(lang,text,for_espeak=1)
else: t = simpleZhTransliterator.transliterate(lang,text,for_espeak=1)
else: t=[fix_compatibility(ensure_unicode(text)).encode("utf-8")]
if t and not riscos_sound: # same TODO as above re RISC OS
if type(t)==type([]):
indexList.append(len(retList))
retList.append(None) # result not filled in yet
if lang=="zh": tt=pinyin_uColon_to_V(t[0].replace("-","/")) # NB fix_compatibility has already been done (as has preprocess_chinese_numbers), by simpleZhTransliterator above
if lang=="zh": tt=pinyin_uColon_to_V(t[0].replace(B("-"),B("/"))) # NB fix_compatibility has already been done (as has preprocess_chinese_numbers), by simpleZhTransliterator above
else: tt=t[0]
write_to_espeak.append(fix_commas(tt).replace(split_token," "))
write_to_espeak.append(fix_commas(tt).replace(B(split_token),B(" ")))
# (replacing - with / because espeak zh voice treats / as a silent word separator but - is ignored; - is used as a word separator in MeiLing etc. so if you want to write the hanzi for wei2ren2 but you want it to be wei4+ren2, you can hack in this way. TODO document?)
else: retList.append(t)
else: retList.append(None)
if keepIndexList: self.lastIndexList = indexList
if not indexList: return retList
overruns = [] # elements that need to be merged with their following elements (duplicates allowed because indices change after each merge), used when we're transliterating very long texts (not usually as part of a lesson) because some versions of espeak truncate very long lines
i = 0
while i < len(write_to_espeak):
if len(write_to_espeak[i]) > 500:
x = write_to_espeak[i].decode('utf-8')
write_to_espeak[i] = x[:150].encode('utf-8')
write_to_espeak.insert(i+1,x[150:].encode('utf-8'))
overruns.append(i-len(overruns))
i += 1
fname = os.tempnam()
open(fname,"w").write((".\n"+split_token+" ").join(write_to_espeak))
writeB(open(fname,"w"),(B(".\n")+B(split_token)+B(" ")).join(write_to_espeak))
oldcwd=os.getcwd()
if winCEsound:
translit_out = os.tempnam()
data=self.winCE_run(' -v%s -q -X -f %s --phonout=%s' % (espeak_language_aliases.get(lang,lang),fname,translit_out),translit_out)
os.remove(translit_out)
else: data=os.popen(self.program+' -v%s -q -X -f %s%s' % (espeak_language_aliases.get(lang,lang),changeToDirOf(fname,1),cond(unix," 2>&1","")),"rb").read() # popen2 might not work, so had better do it this way:
elif checkIn(lang,espeak_preprocessors): data=readB(os.popen('%s < %s | %s -v%s -q -X %s' % (espeak_preprocessors[lang],changeToDirOf(fname,1),self.program,espeak_language_aliases.get(lang,lang),cond(unix," 2>&1","")),popenRB))
else: data=readB(os.popen('%s -v%s -q -X -f %s%s' % (self.program,espeak_language_aliases.get(lang,lang),changeToDirOf(fname,1),cond(unix," 2>&1","")),popenRB)) # popen2 might not work, so had better do it this way
os.chdir(oldcwd) ; os.remove(fname)
data = data.replace("\r\n","\n").split("\nTranslate '"+split_token+"'\n")
if len(data)==2*len(indexList)-1:
data = data.replace(B("\r\n"),B("\n")).split(B("\nTranslate '")+B(split_token)+B("'\n"))
if len(data)==2*(len(indexList)+len(overruns))-1:
# split points are doubled - better take every ODD item. (NB the text in between is NOT necessarily blank - espeak can flush its sentence cache there)
d2 = []
for i in xrange(0,len(data),2): d2.append(data[i])
data = d2
for o in overruns:
data[o] += data[o+1]
del data[o+1]
if not len(data)==len(indexList):
if not (winsound or macsound): show_warning("Warning: eSpeak's transliterate returned wrong number of items (%d instead of %d). Falling back to separate runs for each item (slower)." % (len(data),len(indexList)))
return None
for index,dat in zip(indexList,data):
en_words={} # any en words that espeak found embedded in the text
r=[] ; lastWasBlank=False
delete_last_r_if_blank = 0
delete_last_r_if_blank = 0 ; appendNext = 0
thisgroup_max_priority,thisgroup_enWord_priority = 0.5,0
for l in dat.strip(wsp).split("\n"):
if lang=="zh": # sort out en_words
lWords = l.split()
if lWords: int0 = intor0(lWords[0])
else: int0 = 0
if int0:
if int0 > thisgroup_max_priority:
thisgroup_max_priority = int0
if lWords[-1]=="[_^_]": thisgroup_enWord_priority = int0 # so far it looks like this is going to be an English word
else: # a split between the groups
if thisgroup_enWord_priority == thisgroup_max_priority: # the choice with the highest priority was the one containing the [_^_] to put the word into English
en_words[r[-1]]=1
thisgroup_max_priority,thisgroup_enWord_priority = 0.5,0
# end of sort out en_words
if lang=="zh" and not lastWasBlank and r and (l.startswith("Replace") or l.startswith("Translate") or l.startswith("Found")): r[-1]+="," # (because not-blank is probably the line of phonemes)
for l in bwspstrip(dat).split(B("\n")):
if appendNext: # (see below)
r.append(l[l.index(B("["))+1:l.index(B("]"))])
appendNext = 0 ; continue
# print "Debugger:",l.strip()
# get en_words for fix_pinyin (and for making sure we embed them in cant)
lWords = l.split()
if lWords: int0 = intor0(lWords[0])
else: int0 = 0
if int0:
if int0 > thisgroup_max_priority:
thisgroup_max_priority = int0
if lWords[-1]==B("[_^_]"): thisgroup_enWord_priority = int0 # so far it looks like this is going to be an English word
else: # a split between the groups
if thisgroup_enWord_priority == thisgroup_max_priority: # the choice with the highest priority was the one containing the [_^_] to put the word into English
en_words[r[-1]]=1
thisgroup_max_priority,thisgroup_enWord_priority = 0.5,0
# end of getting en_words
if lang=="zh" and r and ((not lastWasBlank and (l.startswith(B("Replace")) or l.startswith(B("Translate")) or l.startswith(B("Found")))) or l.find(B("';'"))>1 or l.find(B("','"))>1): r[-1]+=B(",") # (because not-blank is probably the line of phonemes)
elif not lang=="zh" and l.startswith(B("_|")) and r: r[-1] += B(",") # works for zh-yue
if delete_last_r_if_blank and not l: r=r[:-1] # "Translate" followed by blank line is probably corner-brackets or something; don't want that confusing the transliteration (especially if it's for partials)
delete_last_r_if_blank = 0
foundLetter=0
if l.startswith("Translate "):
toAppend=l[l.index("'")+1:-1].replace("\xc3\xbc","v")
if lang=="zh" and not (toAppend in en_words and r and toAppend==r[-1]):
# TODO what about partial English words? e.g. try "kao3 testing" - translate 'testing' results in a translate of 'test' also (which assumes it's already in en mode), resulting in a spurious word "test" added to the text box; not sure how to pick this up without parsing the original text and comparing with the Replace rules that occurred
r.append(toAppend)
delete_last_r_if_blank = 1
else: # lang=="zhy"/"cant"/etc, or it's a duplicate of a word we already know to be in en_words
en_words[toAppend]=1 # make sure it's in there
if l.startswith(B("Translate ")):
toAppend=l[l.index(B("'"))+1:-1].replace(LB("\xc3\xbc"),B("v"))
if toAppend==LB("\xc2\xa0"): continue # stray no-break space (don't let this interfere with being able to do partials)
if not (checkIn(toAppend,en_words) and r and toAppend==r[-1]):
# TODO what about partial English words? e.g. try "kao3 testing" - translate 'testing' results in a translate of 'test' also (which assumes it's already in en mode), resulting in a spurious word "test" added to the text box; not sure how to pick this up without parsing the original text and comparing with the Replace rules that occurred
r.append(toAppend)
delete_last_r_if_blank = 1 # this depends on original Jonathan Duddington eSpeak and is not compatible with eSpeak NG which does not always give us blank lines. TODO: if want eSpeak NG compatibility (which might be a good idea as mainstream GNU/Linux distributions now ship eSpeak NG and just call it "espeak"), we might want to pick up on THIS l ending with B("[_^_]") and next Translate being an identical syllable. That might not be the only problem.
else: en_words[toAppend]=1
else: # not Translate
if lang=="zh" and l.startswith("Found: ") and l[8]==" " and "a"<=l[7]<="z": # an alphabetical letter - we can say this as a Chinese letter and it should be compatible with more partials-based synths. But DON'T do this if going to give it to a unit-selection synth - 'me1' and 'ne1' don't have hanzi and some synths will have difficulty saying them.
if forPartials: r.append("a1 bo1 ci1 de1 e1 fou1 ge1 he1 yi1 ji1 ke1 le1 me1 ne1 wo1 po1 qi1 ri4 si1 te4 yu1 wei4 wu1 xi1 ye1 zi1".split()[ord(l[7])-ord('a')])
else: r.append(l[7])
if lang=="zh" and l.startswith(B("Found: ")) and ((l[7:8]==l[9:10]==B("'") and B("a")<=l[8:9]<=B("z")) or (l[8:9]==B(" ") and B("a")<=l[7:8]<=B("z"))): # an alphabetical letter - we can say this as a Chinese letter and it should be compatible with more partials-based synths. But DON'T do this if going to give it to a unit-selection synth - 'me1' and 'ne1' don't have hanzi and some synths will have difficulty saying them.
if l[8:9]==B(' '): letter=l[7:8]
else: letter=l[8:9]
if forPartials: r.append(B("a1 bo1 ci1 de1 e1 fou1 ge1 he1 yi1 ji1 ke1 le1 me1 ne1 wo1 po1 qi1 ri4 si1 te4 yu1 wei4 wu1 xi1 ye1 zi1").split()[ord(letter)-ord('a')])
else: # a letter in something we're transliterating for a pinyin-driven unit-selection synth
r.append(letter)
en_words[r[-1]]=1
foundLetter = 1
elif not lang=="zh" and l.startswith("Found: ") and (ord(l[7])>127 or (l[7]=="'" and ord(l[8])>127)): # (espeak 1.40 puts in l[7], 1.44 surrounds in quotes)
r.append(l[l.index("[")+1:l.index("]")])
lastWasBlank=(l.startswith("Replace") or not l or foundLetter) # (take 'Replace' lines as blank, so 'Translate' doesn't add a second comma. ditto letters thing.)
if lang=="zh": retList[index]=fix_pinyin(" ".join(r),en_words)
else: retList[index]=" ".join(r)
elif not lang=="zh" and l.startswith(B("Found: ")) and (ord(l[7:8])>127 or (l[7:8]==B("'") and ord(l[8:9])>127)): # (espeak 1.40 puts in l[7], 1.44 surrounds in quotes)
if not B("[") in l: appendNext=1 # probably a spurious newline in the Found quote (espeak 1.48.03)
else: r.append(l[l.index(B("["))+1:l.index(B("]"))])
lastWasBlank=(l.startswith(B("Replace")) or not l or foundLetter) # (take 'Replace' lines as blank, so 'Translate' doesn't add a second comma. ditto letters thing.)
while r and r[-1] and r[-1][-1:]==B(','): r[-1]=r[-1][:-1] # strip any trailing commas
if lang=="zh": retList[index]=fix_pinyin(B(" ").join(r),en_words)
else: retList[index]=B(" ").join(r)
return retList
def escape_jyutping(self,text): return re.sub(B(r"([abcdefghjklmnopstuwz][a-z]*[1-7])"),B(r"[[\1]]"),B(text)) # TODO what if user already escaped it?
def play(self,lang,text):
if lang=="zh": text=fix_commas(fix_compatibility(ensure_unicode(text)).encode('utf-8'))
lang = S(lang)
self.check_dicts(lang,text)
if espeak_language_aliases.get(lang,lang) in ["zhy","zh-yue"]: text=self.escape_jyutping(preprocess_chinese_numbers(fix_compatibility(ensure_unicode(text)),isCant=1).encode("utf-8"))
elif lang=="zh": text=fix_commas(preprocess_chinese_numbers(fix_compatibility(ensure_unicode(text))).encode('utf-8'))
if winCEsound: # need to play via makefile, and careful not to leave too many tempfiles or take too long
ret = 0
if len(text)>15: # not a short phrase - let's split it up
......@@ -568,22 +723,25 @@ class ESpeakSynth(Synth):
finally:
if hasattr(self,"winCEhint"): os.unlink(self.makefile(lang,"")) # make sure to clean up on interrupt
return ret
elif winsound or mingw32 or cygwin:
# Windows command line is not always 100% UTF-8 safe, so we'd better use a pipe. (Command line ok on other platforms, and must do it on riscos - no pipes.) (espeak_pipe_through only needs supporting on non-Windows - it's for aplay etc)
p=os.popen(self.program+cond(text.find("</")>-1," -m","")+' -v%s -a%d' % (espeak_language_aliases.get(lang,lang),100*soundVolume),"wb")
p.write(text+"\n") ; return p.close()
else: return system(self.program+cond(text.find("</")>-1," -m","")+' -v%s -a%d %s %s' % (espeak_language_aliases.get(lang,lang),100*soundVolume,shell_escape(text),espeak_pipe_through)) # (-m so accepts SSML tags)
elif unix or winsound or mingw32 or cygwin:
# Windows command line is not always 100% UTF-8 safe, so we'd better use a pipe. Unix command line OK but some espeak versions have a length limit. (No pipes on riscos.)
p=os.popen(self.program+cond(B(text).find(B("</"))>=0," -m","")+' -v%s -a%d %s' % (espeak_language_aliases.get(lang,lang),100*soundVolume,espeak_pipe_through),"w")
writeB(p,B(text).replace(B(". "),B(".\n"))+B("\n")) ; return p.close() # (see comment below re adding newlines)
else: return system(B(self.program+cond(B(text).find(B("</"))>=0," -m","")+' -v%s -a%d ' % (espeak_language_aliases.get(lang,lang),100*soundVolume))+shell_escape(text)+B(' '+espeak_pipe_through)) # (-m so accepts SSML tags)
def makefile(self,lang,text,is_winCEhint=0):
self.check_dicts(lang,text)
if espeak_language_aliases.get(lang,lang) in ["zhy","zh-yue"]: text=self.escape_jyutping(preprocess_chinese_numbers(fix_compatibility(ensure_unicode(text)),isCant=1).encode("utf-8"))
elif lang=="zh": text=fix_commas(preprocess_chinese_numbers(fix_compatibility(ensure_unicode(text))).encode('utf-8'))
if hasattr(self,"winCEhint"): # waiting for a previous async one that was started with is_winCEhint=1
fname,fnameIn = self.winCEhint
del self.winCEhint
self.winCE_wait(fname,fnameIn)
self.winCE_wait(fname,fnameIn,needDat=0)
return fname
fname = os.tempnam()+dotwav
oldcwd=os.getcwd()
sysCommand = cond(winCEsound,"",self.program)+cond(text.find("</")>-1," -m","")+' -w %s -v%s' % (changeToDirOf(fname,1),espeak_language_aliases.get(lang,lang))
sysCommand = cond(winCEsound,"",self.program)+cond(B(text).find(B("</"))>=0," -m","")+' -v%s -w %s%s' % (espeak_language_aliases.get(lang,lang),cond(unix,"/dev/stdout|cat>",""),changeToDirOf(fname,1))
# (Unix use stdout and cat because some espeak versions truncate the output file mid-discourse)
# (eSpeak wavs are 22.05k 16-bit mono; not much point down-sampling to 16k to save 30% storage at expense of CPU)
if lang=="zh": text=fix_commas(fix_compatibility(ensure_unicode(text)).encode('utf-8'))
if winsound or mingw32: os.popen(sysCommand,"w").write(text+"\n") # must pipe the text in
elif riscos_sound: os.system(sysCommand+' '+shell_escape(text))
elif winCEsound:
......@@ -593,11 +751,11 @@ class ESpeakSynth(Synth):
self.winCE_start(sysCommand+' -f '+fnameIn)
else: self.winCE_run(sysCommand+' -f '+fnameIn,fname,fnameIn)
else:
# we can make it asynchronously
if cygwin: sysCommand='echo "'+text.replace('"','\\"')+'"|'+sysCommand # (still need to pipe)
else: sysCommand += (' "'+text.replace('"','\\"')+'"')
# we can make it asynchronously (still need to pipe)
# (add end-of-sentence newlines due to short line buffer in some versions of espeak)
sysCommand=B('echo ')+shell_escape(B(text).replace(B(". "),B(".\n")))+B('|')+B(sysCommand)
if not self.theProcess: self.theProcess = os.popen("/bin/bash","w")
self.theProcess.write('cd "'+os.getcwd()+'"\n'+sysCommand+"\n")
writeB(self.theProcess,B('cd "'+os.getcwd()+'"\n')+sysCommand+B("\n"))
self.theProcess.flush()
os.chdir(oldcwd)
return fname
......@@ -608,12 +766,12 @@ class ESpeakSynth(Synth):
def fix_commas(text):
# some versions of espeak won't honour ordinary commas in among Chinese text if the ordinary commas don't have spaces after
# also put 2 spaces after full stop, and make sure capitalised
i=0
i=0 ; text=B(text)
while i<len(text)-1:
if text[i] in '.,?;!':
tRest = text[i+1:].strip(wsp)
if tRest and (ord(tRest[0])>=128 or 'a'<=tRest[0].lower()<='z'):
text=text[:i+1]+cond(text[i] in ".?!"," "+tRest[0].upper()," "+tRest[0])+tRest[1:]
if text[i:i+1] in B('.,?;!'):
tRest = bwspstrip(text[i+1:])
if tRest and (ord(tRest[:1])>=128 or B('a')<=tRest[:1].lower()<=B('z')):
text=text[:i+1]+cond(text[i:i+1] in B(".?!"),B(" ")+tRest[:1].upper(),B(" ")+tRest[:1])+tRest[1:]
i+=1
return text
......@@ -622,33 +780,33 @@ def fix_pinyin(pinyin,en_words):
if en_words:
ret=[]
def stripPunc(w):
i=0 ; j=len(w) ; w=w.lower()
while i<len(w) and not 'a'<=w[i]<='z': i+=1
while j>1 and not ('a'<=w[j-1]<='z' or '1'<w[j-1]<='5'): j-=1
w=B(w) ; i=0 ; j=len(w) ; w=w.lower()
while i<len(w) and not B('a')<=w[i:i+1]<=B('z'): i+=1
while j>1 and not (B('a')<=w[j-1:j]<=B('z') or B('1')<w[j-1:j]<=B('5')): j-=1
return w[i:j]
for w in pinyin.split():
if stripPunc(w) in en_words: ret.append(w)
if checkIn(stripPunc(w),en_words): ret.append(w)
else: ret.append(fix_pinyin(w,[]))
return ' '.join(ret)
return B(' ').join(ret)
i=0
pinyin=pinyin_uColon_to_V(pinyin)+"@@@" # (includes .lower; @@@ for termination)
pinyin=pinyin_uColon_to_V(pinyin)+B("@@@") # (includes .lower; @@@ for termination)
while i<len(pinyin):
if pinyin[i] in "12345":
if pinyin[i:i+1] in B("12345"):
moveBy=0
if pinyin[i+1] in "iuv": moveBy=1 # these are never initial letters
elif pinyin[i+1]=="o" and not pinyin[i+2] in "u12345": moveBy=1 # "o" and "ou" are valid syllables, but a number before "o" is likely to be premature especially if the "o" is not itself followed by a number (or "u")
elif pinyin[i+1:i+3]=="ng" and not pinyin[i+3] in "aeiouv": moveBy=2 # before an -ng, but NOT before a -n g-(vowel)
elif pinyin[i+1] in "nr" and not pinyin[i+2] in "aeiouv" and not (pinyin[i+1]=="r" and i and not pinyin[i-1]=="e") and not pinyin[i+1:i+3]=="r5": moveBy=1 # before -n or -r (as final not as initial) (but NB -r is only on -er, otherwise it's an r5. and if it already says r5, leave it.)
if moveBy: pinyin=pinyin[:i]+pinyin[i+1:i+moveBy+1]+pinyin[i]+pinyin[i+moveBy+1:]
if pinyin[i+1:i+2] in B("iuv"): moveBy=1 # these are never initial letters
elif pinyin[i+1:i+2]==B("o") and not pinyin[i+2:i+3] in B("u12345"): moveBy=1 # "o" and "ou" are valid syllables, but a number before "o" is likely to be premature especially if the "o" is not itself followed by a number (or "u")
elif pinyin[i+1:i+3]==B("ng") and not pinyin[i+3:i+4] in B("aeiouv"): moveBy=2 # before an -ng, but NOT before a -n g-(vowel)
elif pinyin[i+1:i+2] in B("nr") and not pinyin[i+2:i+3] in B("aeiouv") and not (pinyin[i+1:i+2]==B("r") and i and not pinyin[i-1:i]==B("e")) and not pinyin[i+1:i+3]==B("r5"): moveBy=1 # before -n or -r (as final not as initial) (but NB -r is only on -er, otherwise it's an r5. and if it already says r5, leave it.)
if moveBy: pinyin=pinyin[:i]+pinyin[i+1:i+moveBy+1]+pinyin[i:i+1]+pinyin[i+moveBy+1:]
i+=1
i=0
while i<len(pinyin): # check for missing 5's
if (pinyin[i] in "aeiouvr" and pinyin[i+1] not in "aeiouv12345") or (ord('a')<=ord(pinyin[i])<=ord('z') and not (ord("a")<=ord(pinyin[i+1])<=ord("z") or pinyin[i+1] in "12345")): # ("alnum and next is not alnum" is not strictly necessary, but we do need to add 5's after en-like words due to 'fix_pinyin(t)==t' being used as a do-we-need-proper-translit. condition in SimpleZhTransliterator, otherwise get problems with things like "c diao4" going to eSpeak when it could go to partials-with-letter-substitutions)
if pinyin[i+1:i+3]=="ng" and not pinyin[i+3] in "aeiouv":
if pinyin[i+3] not in "12345": pinyin=pinyin[:i+3]+"5"+pinyin[i+3:]
elif (pinyin[i+1]=="n" or pinyin[i:i+2]=="er") and not pinyin[i+2] in "aeiouv" and not pinyin[i]=="r":
if pinyin[i+2] not in "12345": pinyin=pinyin[:i+2]+"5"+pinyin[i+2:]
else: pinyin=pinyin[:i+1]+"5"+pinyin[i+1:]
if (pinyin[i:i+1] in B("aeiouvr") and pinyin[i+1:i+2] not in B("aeiouv12345")) or (ord('a')<=ord(pinyin[i:i+1])<=ord('z') and not (ord("a")<=ord(pinyin[i+1:i+2])<=ord("z") or pinyin[i+1:i+2] in B("12345"))): # ("alnum and next is not alnum" is not strictly necessary, but we do need to add 5's after en-like words due to 'fix_pinyin(t)==t' being used as a do-we-need-proper-translit. condition in SimpleZhTransliterator, otherwise get problems with things like "c diao4" going to eSpeak when it could go to partials-with-letter-substitutions)
if pinyin[i+1:i+3]==B("ng") and not pinyin[i+3:i+4] in B("aeiouv"):
if pinyin[i+3:i+4] not in B("12345"): pinyin=pinyin[:i+3]+B("5")+pinyin[i+3:]
elif (pinyin[i+1:i+2]==B("n") or pinyin[i:i+2]==B("er")) and not pinyin[i+2:i+3] in B("aeiouv") and not pinyin[i:i+1]==B("r"):
if pinyin[i+2:i+3] not in B("12345"): pinyin=pinyin[:i+2]+B("5")+pinyin[i+2:]
else: pinyin=pinyin[:i+1]+B("5")+pinyin[i+1:]
i+=1
return pinyin[:-3] # remove the @@'s
......@@ -658,20 +816,22 @@ def remove_tone_numbers(utext): # for hanzi_and_punc to take out numbers that ca
if "1"<=utext[i]<="5" and "a"<=utext[i-1].lower()<="z" and (i==len(utext)-1 or not "0"<=utext[i+1]<="9"): utext=utext[:i]+utext[i+1:]
i+=1
return utext
def preprocess_chinese_numbers(utext):
def preprocess_chinese_numbers(utext,isCant=0): # isCant=1 for Cantonese, 2 for hanzi (and if 1 or 2, also assumes input may be jyutping not just pinyin)
# Hack for reading years digit by digit:
for year in ["nian2",u"\u5e74"]: # TODO also " nian2" to catch that? what of multiple spaces?
while utext.find(year)>=4 and 1200 < intor0(utext[utext.find(year)-4:utext.find(year)]) < 2300: # TODO is that range right?
yrStart = utext.find(year)-4
while utext.find(year)>=4 and 1200 < intor0(utext[utext.index(year)-4:utext.index(year)]) < 2300: # TODO is that range right?
yrStart = utext.index(year)-4
utext = utext[:yrStart] + " ".join(list(utext[yrStart:yrStart+4]))+" "+utext[yrStart+4:]
# End of hack for reading years
i=0
while i<len(utext):
if "0"<=utext[i]<="9" and not ("1"<=utext[i]<="5" and i and "a"<=utext[i-1].lower()<="z" and (i==len(utext)-1 or not "0"<=utext[i+1]<="9")): # number that isn't a tone digit
if "0"<=utext[i]<="9" and not ("1"<=utext[i]<=cond(isCant,"7","5") and i and "a"<=utext[i-1].lower()<="z" and (i==len(utext)-1 or not "0"<=utext[i+1]<="9")): # number that isn't a tone digit
j=i
while j<len(utext) and utext[j] in "0123456789.": j += 1
while utext[j-1]==".": j -= 1 # exclude trailing point(s)
num = read_chinese_number(utext[i:j])
if isCant:
for mand,cant in zip("ling2 yi1 er4 san1 si4 wu3 liu4 qi1 ba1 jiu3 dian3 yi4 qian1 bai3 shi2 wan4".split(),cond(isCant==2,u"\u96f6 \u4e00 \u4e8c \u4e09 \u56db \u4e94 \u516d \u4e03 \u516b \u4e5d \u70b9 \u4ebf \u5343 \u767e \u5341 \u4e07","ling4 jat1 ji6 saam7 sei3 ng5 luk6 cat7 baat3 gau2 dim2 jik1 cin7 baak3 sap6 maan6").split()): num=num.replace(mand,cant)
utext=utext[:i]+num+utext[j:]
i += len(num)
else: i += 1
......@@ -724,9 +884,9 @@ def espeak_stdout_works():
except ValueError: return False
def espeak_volume_ok():
# if has "zh", should be recent enough
return "zh" in ESpeakSynth().languages
if unix and not macsound and not (oss_sound_device=="/dev/sound/dsp" or oss_sound_device=="/dev/dsp"):
if playProgram=="aplay" and espeak_stdout_works(): espeak_pipe_through="--stdout|aplay -q" # e.g. NSLU2
return checkIn("zh",ESpeakSynth().languages)
if wavPlayer_override or (unix and not macsound and not (oss_sound_device=="/dev/sound/dsp" or oss_sound_device=="/dev/dsp")):
if wavPlayer=="aplay" and espeak_stdout_works(): espeak_pipe_through="--stdout|aplay -q" # e.g. NSLU2
else: del ESpeakSynth.play # because we have no way of sending it to the alternative device, so do it via a file
if hasattr(FliteSynth,"play"): del FliteSynth.play
if hasattr(ESpeakSynth,"play") and (soundVolume<0.04 or (soundVolume<0.1 and not espeak_volume_ok()) or soundVolume>2): del ESpeakSynth.play # old versions of espeak are not very good at less than 10% volume, so generate offline and use sox
......@@ -736,6 +896,28 @@ globalEspeakSynth = ESpeakSynth()
class ESpeakSynth(ESpeakSynth):
def __init__(self): self.__dict__ = globalEspeakSynth.__dict__
class EkhoSynth(Synth):
def __init__(self):
Synth.__init__(self)
if ekho_speed_delta: self.prog="ekho -s %d" % ekho_speed_delta
else: self.prog="ekho"
def supports_language(self,lang): return lang in ["zhy","zh-yue","cant"] # not Mandarin unless we can check we have a version of ekho that does 3rd tones correctly
def works_on_this_platform(self): return got_program("ekho")
def guess_length(self,lang,text): return quickGuess(len(text),6) # TODO need a better estimate
def play(self,lang,text):
text = preprocess_chinese_numbers(fix_compatibility(ensure_unicode(text)),isCant=2).encode("utf-8")
infile = os.tempnam()+dottxt ; open(infile,"w").write(text) # Ekho 4.5 takes "-f -" for stdin, but 4.1 can't
r = system(self.prog+" --voice=Cantonese -f \""+infile+"\"")
os.remove(infile)
return r
def makefile(self,lang,text):
text = preprocess_chinese_numbers(fix_compatibility(ensure_unicode(text)),isCant=2).encode("utf-8")
fname = os.tempnam()+dotwav # TODO can also have dotmp3 (with -t mp3 added), and the resulting mp3 can be smaller than gradint's
infile = os.tempnam()+dottxt ; open(infile,"w").write(text)
system(self.prog+" --voice=Cantonese -f \""+infile+"\" -o \""+fname+"\"")
os.remove(infile)
return fname
class FestivalSynth(Synth):
def __init__(self): Synth.__init__(self)
def startProcess(self):
......@@ -750,19 +932,78 @@ class FestivalSynth(Synth):
if oss_sound_device:
def play(self,lang,text):
if not self.theProcess: self.startProcess()
self.theProcess.write("(Parameter.set 'Audio_Command \"play --device=%s \$FILE vol %.1f\")\n(tts_text \"%s\" nil)\n" % (oss_sound_device,5*soundVolume,text)) # (tts_text text nil) can be better than (SayText text) because it splits into multiple utterances if necessary
self.theProcess.write("(Parameter.set 'Audio_Command \"play --device=%s \\$FILE vol %.1f\")\n(tts_text \"%s\" nil)\n" % (oss_sound_device,5*soundVolume,text)) # (tts_text text nil) can be better than (SayText text) because it splits into multiple utterances if necessary
self.theProcess.flush()
# else send it via a file, because we haven't got code to give it to play to the other devices directly
def makefile(self,lang,text):
if not self.theProcess: self.startProcess()
fname = os.tempnam()+dotwav
self.theProcess.write("(Parameter.set 'Audio_Command \"sox \$FILE %s vol 5\")\n(SayText \"%s\")\n" % (fname,text))
self.theProcess.write("(Parameter.set 'Audio_Command \"sox \\$FILE %s vol 5\")\n(SayText \"%s\")\n" % (fname,text))
self.theProcess.flush()
return fname
def finish_makefile(self):
if self.theProcess: self.theProcess.close()
self.theProcess = None
class CoquiSynth(Synth):
def __init__(self):
Synth.__init__(self)
self.synths = {}
def works_on_this_platform(self):
if not unix: return 0 # I'm unable to test elsewhere
self.base = os.environ.get("HOME","")+"/.local/share/tts"
return isDirectory(self.base) # Voices require large downloads the first time they are used, so we'll use only already-downloaded voices
def supports_language(self,lang):
for a in os.listdir(self.base): # don't use any() with a generator func because we need to be Python 2.3 compatible
if a.startswith("tts_models--"+lang+"-"): return True # TODO: might not want to use all downloaded models, or might not want to use for all input types (e.g. zh does not support pinyin)
def guess_length(self,lang,text): return quickGuess(len(text),cond(lang in ["zh"],6,12)) # need better estimate
def makefile(self,lang,text):
text = ensure_unicode(text)
if lang=="zh": text += u"\u3002" # otherwise that model can glitch and repeat the last word of the phrase
if not lang in self.synths:
import torch;from TTS.api import TTS # shouldn't fault if models are downloaded to ~/.local/share/tts (unless uninstalled and not cleaned up...)
# We can assume Python 3 by this point, but must still use syntax compatible with Python 2
for a in sorted(os.listdir(self.base)):
if a.startswith("tts_models--"+lang+"-"):
self.synths[lang]=TTS(a.replace("--","/")).to(cond(torch.cuda.is_available(),"cuda","cpu"))
break
fname = os.tempnam()+dotwav
self.synths[lang].tts_to_file(text,file_path=fname)
return fname
class PiperSynth(Synth):
def __init__(self):
Synth.__init__(self)
self.lCache = {}
def works_on_this_platform(self):
if not unix: return 0 # I can't test on other platforms
for self.program in ["piper/piper","./piper"]:
if fileExists(self.program): return True
def supports_language(self,lang):
if lang in self.lCache: return self.lCache[lang]
for d in [".","piper"]:
foundSubdir = False
for f in os.listdir(d):
if f=="piper": foundSubdir=True
if (f.startswith(lang+"_") or f.startswith(lang+"-")) and f.endswith('.onnx'):
self.lCache[lang] = d+"/"+f
return self.lCache[lang]
if not foundSubdir: break
def guess_length(self,lang,text): return quickGuess(len(text),cond(lang in ["zh"],6,12)) # need better estimate
def transliterate(self,lang,text,forPartials=0):
# Piper TTS models are controlled by eSpeak phonemes, so we should be able to get eSpeak to do this
es = ESpeakSynth()
if not es.works_on_this_platform() or not es.supports_language(lang): return text
return es.transliterate(lang,text,forPartials)
def can_transliterate(self,lang):
es = ESpeakSynth()
return es.works_on_this_platform() and es.supports_language(lang)
def makefile(self,lang,text):
fname = os.tempnam()+dotwav
f=os.popen(self.program+' --model "'+self.supports_language(lang)+'" --output_file "'+fname+'"',popenWB)
f.write(text+"\n") ; f.close()
return fname
class GeneralSynth(Synth):
def __init__(self): Synth.__init__(self)
def supports_language(self,lang):
......@@ -785,7 +1026,7 @@ class GeneralFileSynth(Synth):
return 0
def works_on_this_platform(self): return extra_speech_tofile
def guess_length(self,lang,text):
if not lang in self.letters: self.letters[lang]=self.duration[lang]=0
if not checkIn(lang,self.letters): self.letters[lang]=self.duration[lang]=0
if self.letters[lang]<25:
self.letters[lang] += len(text)
self.duration[lang] += SampleEvent(self.makefile_cached(lang,text)).exactLen
......@@ -801,14 +1042,17 @@ class GeneralFileSynth(Synth):
else: os.rename(f,fname)
return fname
all_synth_classes = [GeneralSynth,GeneralFileSynth] # first so user can override
for s in synth_priorities.split():
if s.lower()=="espeak": all_synth_classes.append(ESpeakSynth)
all_synth_classes = [GeneralSynth,GeneralFileSynth] # at the beginning so user can override
all_synth_classes += [CoquiSynth,PiperSynth] # override espeak if present (especially PiperSynth)
for s in synth_priorities.split(): # synth_priorities no longer in advanced.txt (see system.py above) but we can still support it
if s.lower()=="ekho": all_synth_classes.append(EkhoSynth)
elif s.lower()=="espeak": all_synth_classes.append(ESpeakSynth)
elif s.lower()=="macos":
all_synth_classes.append(OSXSynth_Say)
all_synth_classes.append(OSXSynth_OSAScript) # (prefer _Say if >=10.3 because it's faster)
elif s.lower()=="sapi": all_synth_classes.append(PttsSynth)
all_synth_classes = all_synth_classes + [FestivalSynth,FliteSynth,OldRiscosSynth,S60Synth]
all_synth_classes += [FestivalSynth,FliteSynth,OldRiscosSynth,S60Synth,AndroidSynth]
prefer_espeak = prefer_espeak.split()
viable_synths = []
warned_about_nosynth = {}
......@@ -818,9 +1062,10 @@ def setSoundCollector(sc):
global soundCollector, viable_synths, getsynth_cache
soundCollector,viable_synths,getsynth_cache = sc,[],{}
def get_synth_if_possible(language,warn=1,to_transliterate=False):
if language in getsynth_cache and not to_transliterate: return getsynth_cache[language] # most common case (vocab.txt parse)
language = S(language)
if checkIn(language,getsynth_cache) and not to_transliterate: return getsynth_cache[language] # most common case (vocab.txt parse)
if language==None:
if not None in getsynth_cache: getsynth_cache[None]=Partials_Synth()
if not checkIn(None,getsynth_cache): getsynth_cache[None]=Partials_Synth()
return getsynth_cache[None]
global viable_synths, warned_about_nosynth
if not viable_synths:
......@@ -830,18 +1075,24 @@ def get_synth_if_possible(language,warn=1,to_transliterate=False):
if to_transliterate: # for partials: return a synth that can transliterate the language, if possible
for synth in viable_synths:
if synth.supports_language(language) and synth.can_transliterate(language): return synth
if language=="zh": return simpleZhTransliterator # in case haven't got eSpeak
for synth in viable_synths:
if language=="zh": return simpleZhTransliterator # fallback if no eSpeak
for synth in viable_synths: # find a good one ?
if synth.supports_language(language) and not synth.not_so_good_at(language):
getsynth_cache[language]=synth ; return synth
for synth in viable_synths:
for synth in viable_synths: # OK, not-so-good one ?
if synth.supports_language(language):
getsynth_cache[language]=synth ; return synth
if warn and language not in synth_partials_voices and not language==firstLanguage and language in ["zh","cant","zhy","zh-yue"] and not language in warned_about_nosynth:
# Some people fail to install Yali etc because web advertising taught them to ignore sidebars :-(
warned_about_nosynth[language] = 1
show_warning("You don't have "+cond(language=="zh","Yali Cheng's Mandarin","Cameron Wong's Cantonese")+" voice installed, only a more basic robot voice. Please see the sidebar on the Gradint website for downloads.")
if app: waitOnMessage("In the meantime, you'll have to bear with this....")
getsynth_cache[language]=synth # only if warn (otherwise wait until we're called again, then warn)
return synth
if (not warn) or language not in [firstLanguage,secondLanguage]+possible_otherLanguages: return None # without printing a warning
if not language in warned_about_nosynth:
if not checkIn(language,warned_about_nosynth):
warned_about_nosynth[language] = 1
canSay = []
if language in partials_langs: canSay.append("recorded syllables (partials)")
if checkIn(language,synth_partials_voices): canSay.append("recorded syllables (partials)")
if synthCache: canSay.append("recorded phrases (synthCache)")
if canSay: canSay="\n - can use only "+" and ".join(canSay)
else: canSay="\n (did you read ALL the comments in vocab.txt?)"
......@@ -856,25 +1107,28 @@ def pinyin_uColon_to_V(pinyin):
pinyin = pinyin.lower()
pristineU = unichr(0xfc).encode('utf-8')
pinyin = pinyin.replace("j"+pristineU,"ju").replace("q"+pristineU,"qu").replace("x"+pristineU,"xu").replace(pristineU,"v").replace(unichr(0xea).encode('utf-8'),"e") # for pristine's pinyin
pinyin = B(pinyin).replace(B("j")+pristineU,B("ju")).replace(B("q")+pristineU,B("qu")).replace(B("x")+pristineU,B("xu")).replace(pristineU,B("v")).replace(unichr(0xea).encode('utf-8'),B("e")) # for pristine's pinyin
return pinyin.replace("u:","v").replace("leu","lv").replace("neu","nv")
return pinyin.replace(B("u:"),B("v")).replace(B("leu"),B("lv")).replace(B("neu"),B("nv"))
class SynthEvent(Event):
def __init__(self,text,synthesizer,language,is_prompt=0):
self.text = text ; self.synthesizer = synthesizer
assert text,"Trying to speak zero-length text"
self.text = B(text) ; self.synthesizer = synthesizer
self.modifiedText = self.text
if language=="en" and not self.text[-1] in ";.!?-" and not (';' in self.text and ';' in self.text[self.text.index(';')+1:]): self.modifiedText += ';' # prosody hack (some synths sound a bit too much like 'disjointed strict commands' without this)
if language=="en":
self.modifiedText = self.modifiedText.replace(LB("\xE2\x80\xA7"),B("")).replace(LB("\xE2\x80\xB2"),B("")) # remove syllable boundaries and primes (usually just confuse speech synths)
if not self.text[-1:] in B(";.!?-") and not (B(';') in self.text and B(';') in self.text[self.text.index(B(';'))+1:]): self.modifiedText += B(';') # prosody hack (some synths sound a bit too much like 'disjointed strict commands' without this)
elif language=="zh":
# normalise pinyin
# (note - this code is NOT used for partials synth, only for passing to espeak etc. see elsewhere for partials synth)
self.modifiedText = pinyin_uColon_to_V(self.modifiedText) # includes .lower()
# and put space between every syllable of w, if it's one word only (the Lily voice seems to stand a better chance of getting it right that way, and occasionally other voices do too, e.g. "chang2yuan3" in at least some versions of eSpeak, not to mention Loquendo Lisheng
self.modifiedText = self.modifiedText.replace("-"," ") # for Lily, Lisheng etc. NB replace hyphen with space not with "", otherwise can get problems with phrases like "wang4en1-fu4yi4"
if (not " " in self.modifiedText) and ("1" in self.modifiedText or "2" in self.modifiedText or "3" in self.modifiedText or "4" in self.modifiedText or "5" in self.modifiedText):
for t in ["1","2","3","4","5"]: self.modifiedText = self.modifiedText.replace(B(t+"-"),B(t+" ")) # for Lily, Lisheng etc. NB replace hyphen with space not with "", otherwise can get problems with phrases like "wang4en1-fu4yi4". DON'T do it except after tone marks, because for hanzi we might want to use hyphens for word-boundary disambiguation.
if (not B(" ") in self.modifiedText) and (B("1") in self.modifiedText or B("2") in self.modifiedText or B("3") in self.modifiedText or B("4") in self.modifiedText or B("5") in self.modifiedText):
self.modifiedText=fix_pinyin(self.modifiedText,[]) # better call that before doing the following (in case any digits in the wrong place)
for f in py_final_letters:
for t in "12345": self.modifiedText=self.modifiedText.replace(f+t,f+t+" ")
for t in "12345": self.modifiedText=self.modifiedText.replace(B(f+t),B(f+t+" "))
if synthesizer.__class__ in [GeneralSynth, GeneralFileSynth]:
# some e.g. eSpeak use capitals to start a new sentence, so need to undo some of the .lower() that pinyin_uColon_to_V did.
# (ESpeakSynth already calls fix_commas in play() and makefile() so don't need to do it here.)
......@@ -909,7 +1163,7 @@ class SynthEvent(Event):
self.sound = self.synthesizer.makefile_cached(self.language,self.modifiedText)
self.synthesizer.finish_makefile()
if sample_table_hack:
if not self.sound in sample_table_hack_lengthDic: sample_table_hack_lengthDic[self.sound]=SampleEvent(self.sound).exactLen
if not checkIn(self.sound,sample_table_hack_lengthDic): sample_table_hack_lengthDic[self.sound]=SampleEvent(self.sound).exactLen
soundCollector.addFile(self.sound,sample_table_hack_lengthDic[self.sound])
open(self.sound,"wb") # i.e. truncate at 0 bytes to save space (but keep around so no name clashes)
elif self.sound:
......@@ -919,7 +1173,7 @@ class SynthEvent(Event):
except IOError:
time.sleep(1) ; continue
return e.play()
raise IOError("IOError after 10 tries")
raise IOError("IOError after 10 tries on "+repr(self.sound))
else:
assert (not soundCollector) and hasattr(self.synthesizer,"play"),"Should have called will_be_played before playing offline"
return self.synthesizer.play(self.language,self.modifiedText)
......@@ -969,7 +1223,7 @@ def abspath_from_start(p): # for just_synthesize to check for paths relative to
os.chdir(d)
return r
def just_synthesize(callSanityCheck=0,lastLang_override=None):
def just_synthesize(callGeneralCheck=0,lastLang_override=None):
# Handle the justSynthesize setting (see advanced.txt)
global startAnnouncement,endAnnouncement,logFile,synth_partials_cache
synth_partials_cache = {} # to stop 'memory leak' when running from the GUI
......@@ -979,7 +1233,7 @@ def just_synthesize(callSanityCheck=0,lastLang_override=None):
called_synth = 0
# we re-generate the lesson on each repeat, so sporadic-synthcache stuff works
global repeatMode ; repeatMode = 1
while repeatMode:
while repeatMode and not repeatMode=="interrupted":
repeatMode = 0
less = Lesson()
lastStartTime = lastEndTime = lastWasDelay = 0
......@@ -988,21 +1242,24 @@ def just_synthesize(callSanityCheck=0,lastLang_override=None):
def checkCanSynth(fname):
ret=can_be_synthesized(fname)
if ret: return fileToEvent(fname)
else: show_warning("Can't say %s in %s" % (repr(text),repr(lang))) # previous warnings should have said why (e.g. partials-only language)
for line in justSynthesize.split("#"):
line = line.strip(wsp) ; l = line.split(None,1)
if extsep in line and fileExists(line): event = fileToEvent(line,"")
elif extsep in line and fileExists(abspath_from_start(line)): event = fileToEvent(abspath_from_start(line),"")
elif line=='R':
else: show_warning("Can't say "+repr(fname)) # previous warnings should have said why (e.g. partials-only language)
for line in B(justSynthesize).split(B('#')):
line = bwspstrip(line)
l = line.split(None,1)
if B(extsep) in line and fileExists(line): event = fileToEvent(line,"")
elif B(extsep) in line and fileExists(abspath_from_start(line)): event = fileToEvent(abspath_from_start(line),"")
elif line==B('R'):
repeatMode=1 ; continue
elif len(l)==1:
try: delayVal = float(l[0])
except ValueError: delayVal = None
if delayVal==None:
# no float value; assume it's a single word to synth in secondLanguage or whatever was the last language used
show_warning("Assuming that %s is a word to synthesize in language '%s'" % (repr(l[0]),lastLanguage))
if callSanityCheck and sanityCheck(l[0],lastLanguage,1): return
event = checkCanSynth("!synth:"+l[0]+"_"+lastLanguage)
r = repr(l[0])
if r[:1]=="b": r=r[1:]
show_warning("Assuming that %s is a word to synthesize in language '%s'" % (r,lastLanguage))
if callGeneralCheck and generalCheck(l[0],lastLanguage,1): return
event = checkCanSynth("!synth:"+S(l[0])+"_"+S(lastLanguage))
if not event: continue # couldn't synth
called_synth = 1
else:
......@@ -1014,17 +1271,17 @@ def just_synthesize(callSanityCheck=0,lastLang_override=None):
lang, text = l
if lang=="sh:": event = ShellEvent(text)
else:
fname = "!synth:"+text+"_"+lang
fname = B("!synth:")+B(text)+B("_")+B(lang)
if not can_be_synthesized(fname):
if lang in [firstLanguage,secondLanguage]+otherLanguages:
show_warning("Can't say %s in %s" % (repr(text),repr(lang)))
lastLanguage=lang ; continue
# otherwise, user might have omitted lang by mistake
show_warning("Assuming %s was meant to be synthesized in language '%s'" % (cond("#" in justSynthesize or len(repr(line))<10,"that '"+repr(line)+"'","this line"),lastLanguage))
if callSanityCheck and sanityCheck(line,lastLanguage,1): return
event = checkCanSynth("!synth:"+line+"_"+lastLanguage)
show_warning("Assuming %s was meant to be synthesized in language '%s'" % (cond(B('#') in B(justSynthesize) or len(repr(line))<10,"that '"+repr(line)+"'","this line"),lastLanguage))
if callGeneralCheck and generalCheck(line,lastLanguage,1): return
event = checkCanSynth("!synth:"+S(line)+"_"+S(lastLanguage))
else:
if callSanityCheck and sanityCheck(text,lang,1): return
if callGeneralCheck and generalCheck(text,lang,1): return
event = checkCanSynth(fname)
lastLanguage = lang
if not event: continue
......@@ -1038,5 +1295,6 @@ def just_synthesize(callSanityCheck=0,lastLang_override=None):
global dbase ; dbase = None # for handleInterrupt
less.play()
startAnnouncement,endAnnouncement,logFile = oldStart,oldEnd,oldLogfile
if repeatMode=="interrupted": sys.exit(1) # better tell the calling script
if not called_synth: return None
return lastLanguage
# This file is part of the source code of
# gradint v0.9969 (c) 2002-2011 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
......@@ -63,40 +63,48 @@ if winsound:
try: ctypes.cdll.commdlg
except: WMstandard = True
if macsound and __name__=="__main__": os.system("clear 1>&2") # so warnings etc start with a clear terminal (1>&2 just in case using stdout for something else)
if macsound and __name__=="__main__": os.system("clear >&2") # so warnings etc start with a clear terminal (>&2 just in case using stdout for something else)
if riscos_sound: sys.stderr.write("Loading Gradint...\n") # in case it takes a while
wsp = '\t\n\x0b\x0c\r ' # whitespace characters - ALWAYS use .strip(wsp) not .strip(), because someone added \xa0 (iso8859-1 no-break space) to string.whitespace on WinCE Python, and that can break processing of un-decoded UTF8 strings, e.g. a Chinese phrase ending "\xe5\x86\xa0"! (and assign to string.whitespace does not work around this.)
try: import androidhelper as android
except:
try: import android
except: android = 0
if android:
try: android = android.Android()
except:
print ("\n"*50+" *** Your SL4A server has crashed ***\n Please restart SL4A\n (or restart your phone)\n and try running Gradint again.\n\n\n")
raise SystemExit
wsp = '\t\n\x0b\x0c\r ' ; bwsp=B(wsp) # whitespace characters - ALWAYS use .strip(wsp) not .strip(), because someone added \xa0 (iso8859-1 no-break space) to string.whitespace on WinCE Python, and that can break processing of un-decoded UTF8 strings, e.g. a Chinese phrase ending "\xe5\x86\xa0"! (and assign to string.whitespace does not work around this.)
# As .split() can't take alternative characters (and re-writing in Python is probably slow), just be careful with using it on un-decoded utf-8 stuff. (split(None,1) is ok if 1st word won't end in an affected character)
warnings_printed = [] ; app = None
warnings_printed = [] ; app = False # False is a hack for "maybe later"
warnings_toprint = []
def show_warning(w):
if not app and not appuifw:
if w+"\n" in warnings_printed: return
if not app and not app==False and not appuifw and not android:
if winCEsound and len(w)>100: w=w[:100]+"..." # otherwise can hang winCEsound's console (e.g. a long "assuming that" message from justSynthesize)
sys.stderr.write(w+"\n")
warnings_printed.append(w+"\n")
if app==False: warnings_toprint.append(w) # may need to output them if app/appuifw/android turns out not to be created
def show_info(i,always_stderr=False):
# == sys.stderr.write(i) with no \n and no error if closed (+ redirect to app or appuifw if exists)
if (app or appuifw) and not always_stderr: return doLabel(i)
if not always_stderr and hasattr(sys.stderr,"isatty") and not sys.stderr.isatty(): return # be quiet if o/p is being captured by cron etc
if winCEsound and len(i)>101: i=i[:100]+"..."+i[-1] # otherwise can hang winCEsound's console (e.g. a long "Not in cache" message)
if (app or appuifw or android) and not always_stderr: return doLabel(i)
if not riscos_sound and not always_stderr and hasattr(sys.stderr,"isatty") and not sys.stderr.isatty(): return # be quiet if o/p is being captured by cron etc (but isatty() might always return false on RISC OS
if winCEsound and len(i)>101: i=i[:100]+"..."+i[-1] # otherwise can hang winCEsound's console
if type(i)==type(u""): i=i.encode('utf-8')
try: sys.stderr.write(i)
try: writeB(sys.stderr,i)
except IOError: pass
# For pre-2.3 versions of Python (e.g. 2.2 on Symbian S60 and Mac OS 10.3):
try: True
except: exec("True = 1 ; False = 0")
# TODO make sure to avoid writing "string1 in string2" without thinking - if string1 is multiple characters it won't work on pre-2.3
# TODO check all lambda functions for Python2.2 compatibility
# (TODO: GUI_translations, if not set in advanced.txt, won't work properly on pre-2.3 - it'll take them as Latin-1)
# (TODO: and if it *IS* set in advanced.txt, will 2.2's exec() correctly exec a unicode string?)
# Check if we're on big-endian architecture (relevant to sox etc)
try: import struct
except: struct=0
if struct and struct.pack("h",1)[0]=='\x00': big_endian = 1
if struct and B(struct.pack("h",1)[0])==B('\x00'): big_endian = 1
else: big_endian = 0
# RISC OS has a different extension separator because "." is used as a directory separator (from the original 1982 BBC Micro DFS with 1-character directories)
......@@ -114,6 +122,14 @@ def list2dict(l):
return d
try: list2set = set
except NameError: list2set = list2dict
def checkIn(k,obj):
try: return k in obj # dict or set
except:
try: return obj.has_key(k) # Python 2.1 (may raise TypeError, AttributeError etc if try to use the "in" operator as above, but has_key rm'd from Python3)
except: return obj.find(k) > -1 # Python 2.1 strings
try: object
except:
class object: pass # Python 2.1
# settings.txt and advanced.txt
# (done here before the variables start to be used in
......@@ -162,16 +178,23 @@ if use_unicode_filenames:
def u8strip(d):
global last_u8strip_found_BOM ; last_u8strip_found_BOM = 0
if d.startswith('\xef\xbb\xbf'):
d = B(d)
if d.startswith(LB('\xef\xbb\xbf')):
last_u8strip_found_BOM = 1
return d[3:] # ignore Notepad's UTF-8 BOM's
else: return d
def bwspstrip(s):
try: return s.strip(bwsp)
except: return s.strip() # Python 2.1
def wspstrip(s):
try: return s.strip(wsp)
except: return s.strip() # Python 2.1
GUI_translations_old = GUI_translations
configFiles = map(lambda x:x+dottxt,["advanced","settings"]) # MUST have settings last so can have per-user override of scriptVariants
if not hasattr(sys,"argv"): sys.argv=" " # some Symbian versions
starting_directory = os.getcwd()
if not fileExists(configFiles[0]):
if macsound and "_" in os.environ:
if macsound and checkIn("_",os.environ):
s=os.environ["_"] ; s=s[:s.rfind(os.sep)]
os.chdir(s)
if not fileExists(configFiles[0]):
......@@ -180,9 +203,14 @@ if not fileExists(configFiles[0]):
os.chdir(s)
if not fileExists(configFiles[0]) and sys.argv and (os.sep in sys.argv[0] or (os.sep=='\\' and '/' in sys.argv[0])):
# try the sys.argv[0] directory, in case THAT works
if os.sep=="\\" and '/' in sys.argv[0] and fileExists(sys.argv[0].replace('/','\\')): sys.argv[0]=sys.argv[0].replace('/','\\') # hack for some Windows Python builds accepting / in command line but reporting os.sep as \
if os.sep=="\\" and '/' in sys.argv[0] and fileExists(sys.argv[0].replace('/','\\')): sys.argv[0]=sys.argv[0].replace('/','\\') # hack for some Windows Python builds accepting slash in command line but reporting os.sep as backslash
os.chdir(starting_directory)
os.chdir(sys.argv[0][:sys.argv[0].rfind(os.sep)])
if not fileExists(configFiles[0]): # argv[0] might be a symlink
os.chdir(starting_directory)
try: rp = os.path.realpath(sys.argv[0])
except: rp = 0 # e.g. no os.path, or no os.path.realpath
if rp: os.chdir(rp[:rp.rfind(os.sep)])
if not fileExists(configFiles[0]):
# Finally, try the module pathname, in case some other Python program has imported us without changing directory. Apparently we need to get this from an exception.
try: raise 0
......@@ -196,7 +224,7 @@ if not fileExists(configFiles[0]):
# directory should be OK by now
if sys.platform.find("ymbian")>-1: sys.path.insert(0,os.getcwd()+os.sep+"lib")
import time,sched,sndhdr,random,math,pprint,codecs
import time,sched,random,math,pprint,codecs
def exc_info(inGradint=True):
import sys # in case it's been gc'd
......@@ -208,32 +236,36 @@ def exc_info(inGradint=True):
while tbObj and hasattr(tbObj,"tb_next") and tbObj.tb_next: tbObj=tbObj.tb_next
if tbObj and hasattr(tbObj,"tb_lineno"): w += (" at line "+str(tbObj.tb_lineno))
if inGradint:
if tbObj and hasattr(tbObj,"tb_frame") and hasattr(tbObj.tb_frame,"f_code") and hasattr(tbObj.tb_frame.f_code,"co_filename") and not tbObj.tb_frame.f_code.co_filename.find("gradint"+extsep+"py")>-1: w += (" in "+tbObj.tb_frame.f_code.co_filename+"\n")
else: w += (" in "+program_name[:program_name.index("(c)")]+"\n")
if tbObj and hasattr(tbObj,"tb_frame") and hasattr(tbObj.tb_frame,"f_code") and hasattr(tbObj.tb_frame.f_code,"co_filename") and not tbObj.tb_frame.f_code.co_filename.find("gradint"+extsep+"py")>=0: w += (" in "+tbObj.tb_frame.f_code.co_filename)
else: w += (" in "+program_name[:program_name.index("(c)")])
w += " on Python "+sys.version.split()[0]+"\n"
del tbObj
return w
def read(fname): return open(fname,"rb").read()
def write(fname,data): open(fname,"wb").write(data)
def readSettings(f):
try: fdat = u8strip(read(f)).replace("\r","\n")
try: fdat = u8strip(read(f)).replace(B("\r"),B("\n"))
except: return show_warning("Warning: Could not load "+f)
try: fdat = unicode(fdat,"utf-8")
except: return show_warning("Problem decoding utf-8 in "+f)
try: exec(fdat) in globals()
try: exec(fdat,globals())
except: show_warning("Error in "+f+" ("+exc_info(False)+")")
dir1 = list2set(dir()+["dir1","f","last_u8strip_found_BOM"])
synth_priorities = "eSpeak MacOS SAPI Ekho" # old advanced.txt had this instead of prefer_espeak; we can still support it
dir1 = list2set(dir()+["dir1","f","last_u8strip_found_BOM","__warningregistry__"])
for f in configFiles: readSettings(f)
for d in dir():
if not d in dir1 and eval(d) and not type(eval(d))==type(lambda *args:0): # (ignore unrecognised options that evaluate false - these might be an OLD unused option with a newer gradint rather than vice versa; also ignore functions as these could be used in command-line parameters)
if not checkIn(d,dir1) and eval(d) and not type(eval(d))==type(lambda *args:0): # (ignore unrecognised options that evaluate false - these might be an OLD unused option with a newer gradint rather than vice versa; also ignore functions as these could be used in command-line parameters)
show_warning("Warning: Unrecognised option in config files: "+d)
del dir1
GUI_translations_old.update(GUI_translations) ; GUI_translations = GUI_translations_old # in case more have been added since advanced.txt last update
def cond(a,b,c):
# Python 2.4 can inline "b if a else c" but Python 2.3 can't
if a: return b
else: return c
unix = not (winsound or mingw32 or riscos_sound or appuifw or winCEsound)
unix = not (winsound or mingw32 or riscos_sound or appuifw or android or winCEsound)
if unix: os.environ["PATH"] = os.environ.get("PATH","/usr/local/bin:/usr/bin:/bin")+cond(macsound,":"+os.getcwd()+"/start-gradint.app:",":")+os.getcwd() # for qtplay and sox, which may be in current directory or may be in start-gradint.app if it's been installed that way, and for lame etc. Note we're specifying a default PATH because very occasionally it's not set at all when using 'ssh system command' (some versions of DropBear?)
# Any options in the environment?
......@@ -252,24 +284,44 @@ if paranoid_file_management:
# For ftpfs etc. Retry on errno 13 (permission denied), and turn append into a copy. Otherwise occasionally get vocab.txt truncated.
_old_open = open
def tryIO(func):
for tries in range(10)+["last"]:
for tries in list(range(10))+["last"]:
try: return func()
except IOError,err:
if tries=="last" or not err.errno in [5,13]: raise
except IOError:
err = sys.exc_info()[1]
if tries=="last" or not err.errno in [5,13,None]: raise
time.sleep(0.5)
def read(file): return tryIO(lambda x=file:_old_open(x,"rb").read())
def open(file,mode="r"):
def _write(fn,data):
tryIO(lambda x=fn,y=data:_old_open(x,"wb").write(y))
time.sleep(0.5)
if not filelen(fn)==len(data):
# might be a version of curlftpfs that can't shorten files - try delete and restart (although this can erase permissions info)
os.remove(fn)
tryIO(lambda x=fn,y=data:_old_open(x,"wb").write(y))
if not filelen(fn)==len(data): raise IOError("wrong length")
if not read(fn)==data: raise IOError("verification failure on "+repr(fn))
def write(fn,data): return tryIO(lambda x=fn,y=data:_write(x,y))
def open(file,mode="r",forAppend=0):
if "a" in mode:
try: dat = open(file,mode.replace("a","r")).read()
except IOError,err:
try: dat = open(file,"rb").read()
except IOError:
err = sys.exc_info()[1]
if err.errno==2: dat = "" # no such file or directory
else: raise
if len(dat) < filelen(file): raise IOError("short read")
try: os.rename(file,file+"~") # just in case!
except: pass
o=open(file,mode.replace("a","w"))
o=open(file,"wb",1)
o.write(dat)
return o
return tryIO(lambda x=file,m=mode:_old_open(x,m))
r=tryIO(lambda x=file,m=mode:_old_open(x,m))
if "w" in mode and not forAppend and filelen(file): # it's not truncating (see _write above)
r.close()
os.unlink(file)
r=tryIO(lambda x=file,m=mode:_old_open(x,m))
return r
if seedless: random.seed(0)
# Different extension separators again
if not extsep==".":
......@@ -291,11 +343,11 @@ for p in [progressFile,progressFileBackup,pickledProgressFile]:
if extsep in p[1]: p=(p[0],p[1][:p[1].rfind(extsep)]) # here rather than earlier to cover cases where extsep is in a directory name but not in the filename
if oldDir==None: oldDir=p
elif not oldDir==p:
sys.stderr.write("ERROR: progressFile, progressFileBackup and pickledProgressFile, if not None, must have same directory and major part of filename. Gradint will not run otherwise. This sanity-check was added in case some script sets progressFile to something special but forgets to set the others.\n")
sys.stderr.write("ERROR: progressFile, progressFileBackup and pickledProgressFile, if not None, must have same directory and major part of filename. Gradint will not run otherwise. This coherence check was added in case some script sets progressFile to something special but forgets to set the others.\n")
sys.exit(1)
# Check for RISC OS pre-1970 clock problem (actually quite likely if testing on the rpcemu emulator without setting the clock)
if riscos_sound and hex(int(time.time())).find("0xFFFFFFFF")>-1 and not outputFile:
if riscos_sound and hex(int(time.time())).find("0xFFFFFFFF")>=0 and not outputFile:
sys.stderr.write("ERROR: time.time() is not usable - gradint cannot run interactively.\n")
sys.stderr.write("This error can be caused by the RISC OS clock being at 1900 (the Unix time functions start at 1970).\nClose this task window, set the clock and try again.\n")
sys.exit()
......@@ -318,7 +370,7 @@ Tk_might_display_wrong_hanzi = wrong_hanzi_message = "" ; forceRadio=0
if macsound:
try: os.remove("_tkinter.so") # it might be an old patched version for the wrong OS version
except: pass
def tkpatch(): # patch Mac OS Tk to the included v8.6 (as v8.4 on OS10.5 has hanzi problem and v8.5 on 10.6 has fontsize problems etc)
def tkpatch(): # (called only on specific older versions of Mac OS X) patch Mac OS Tk to the included v8.6 (as v8.4 on OS10.5 has hanzi problem and v8.5 on 10.6 has fontsize problems etc)
f="/System/Library/Frameworks/Python.framework/Versions/"+sys.version[:3]+"/lib/python"+sys.version[:3]+"/lib-dynload/_tkinter.so"
if fileExists(f): # we might be able to patch this one up
if not isDirectory("Frameworks") and fileExists("Frameworks.tbz"): os.system("tar -jxvf Frameworks.tbz && rm Frameworks.tbz && chmod -R +w Frameworks")
......@@ -333,6 +385,7 @@ if macsound:
elif sys.version[:5] == "2.5.1": # 10.5
if not tkpatch(): Tk_might_display_wrong_hanzi="10.5"
elif sys.version[:5] == "2.6.1": tkpatch() # 10.6 (still has Tk8.5, hanzi ok but other problems)
elif sys.version[:5] == "2.7.5": tkpatch() # 10.9 (problems with "big print" button if don't do this)
if Tk_might_display_wrong_hanzi: wrong_hanzi_message = "NB: In Mac OS "+Tk_might_display_wrong_hanzi+", Chinese\ncan display wrongly here." # so they don't panic when it does
# Handle keeping progress file and temp directories etc if we're running from a live CD
......@@ -348,8 +401,8 @@ def progressFileOK():
except: return 0
if winsound: # will try these dirs in reverse order:
tryList = ["C:\\TEMP\\gradint-progress.txt", "C:\\gradint-progress.txt", "C:gradint-progress.txt"]
if "HOMEDRIVE" in os.environ and "HOMEPATH" in os.environ: tryList.append(os.environ["HOMEDRIVE"]+os.environ["HOMEPATH"]+os.sep+"gradint-progress.txt")
elif "HOME" in os.environ: tryList=[os.environ["HOME"]+os.sep+"gradint-progress.txt"]
if checkIn("HOMEDRIVE",os.environ) and checkIn("HOMEPATH",os.environ): tryList.append(os.environ["HOMEDRIVE"]+os.environ["HOMEPATH"]+os.sep+"gradint-progress.txt")
elif checkIn("HOME",os.environ): tryList=[os.environ["HOME"]+os.sep+"gradint-progress.txt"]
elif riscos_sound: tryList=["$.gradint-progress/txt"]
else: tryList = []
foundPF = okPF = 0 ; defaultProgFile = progressFile
......@@ -367,10 +420,11 @@ if need_say_where_put_progress:
progressFileBackup = progressFile[:-3]+"bak"
pickledProgressFile = progressFile[:-3]+"bin"
logFile = None # for now
if winsound or winCEsound or mingw32 or riscos_sound or not hasattr(os,"tempnam"):
tempdir_is_curdir = False
if winsound or winCEsound or mingw32 or riscos_sound or not hasattr(os,"tempnam") or android:
tempnam_no = 0
if os.sep in progressFile: tmpPrefix=progressFile[:progressFile.rindex(os.sep)+1]+"gradint-tempfile"
else: tmpPrefix="gradint-tempfile"
else: tmpPrefix,tempdir_is_curdir="gradint-tempfile",True
if winCEsound or ((winsound or mingw32) and not os.sep in tmpPrefix and not tmpPrefix.startswith("C:")):
# put temp files in the current directory, EXCEPT if the current directory contains non-ASCII characters then check C:\TEMP and C:\ first (just in case the non-ASCII characters create problems for command lines etc; gradint *should* be able to cope but it's not possible to test in advance on *everybody's* localised system so best be on the safe side). TODO check for quotes etc in pathnames too.
def isAscii():
......@@ -379,39 +433,46 @@ if winsound or winCEsound or mingw32 or riscos_sound or not hasattr(os,"tempnam"
return True
tmpPrefix = None
if winCEsound or not isAscii():
# WinCE: If a \Ramdisk has been set up (e.g. with http://www.amv007.narod.ru/Ramdisk_WinCE.zip), try that first. (Could next try storage card if on WM5+ to save hitting internal flash, but that would be counterproductive on WM2003, and anyway the space in the pathname would be awkward.)
# WinCE: If a \Ramdisk has been set up, try that first. (Could next try storage card if on WM5+ to save hitting internal flash, but that would be counterproductive on WM2003, and anyway the space in the pathname would be awkward.)
for t in cond(winCEsound,["\\Ramdisk\\","\\TEMP\\", "\\"],["C:\\TEMP\\", "C:\\"]):
try:
open(t+"gradint-tempfile-test","w")
os.unlink(t+"gradint-tempfile-test")
except: continue
tmpPrefix = t ; break
tmpPrefix,tempdir_is_curdir = t,False ; break
if not tmpPrefix: tmpPrefix = os.getcwd()+os.sep
tmpPrefix += "gradint-tempfile"
def tempnam():
global tempnam_no ; tempnam_no += 1
return tmpPrefix+str(tempnam_no)
os.tempnam = os.tmpnam = tempnam
elif (macsound or sys.platform.lower().find("bsd")>0) and os.environ.get("TMPDIR",""): # BSD tempnam uses P_tmpdir instead, override
tempnam0 = os.tempnam
os.tempnam=lambda *args:tempnam0(os.environ["TMPDIR"])
if disable_once_per_day==1:
if once_per_day==3: sys.exit()
else: once_per_day=0
if once_per_day&2 and not hasattr(sys,"_gradint_innerImport"): # run every day
currentDay = None
# markerFile logic to avoid 2 background copies etc (can't rely on taskkill beyond WinXP)
# (however this doesn't protect against uninstall + immediate reinstall)
markerFile,toDel="background1"+dottxt,"background2"+dottxt
if fileExists(markerFile): markerFile,toDel=toDel,markerFile
try: os.remove(toDel)
except OSError: pass
open(markerFile,"w").write("(delete this file to make the background process quit on next check)\n")
while fileExists(markerFile):
# markerFile logic to avoid 2+ background copies (can't rely on taskkill beyond WinXP)
myID = str(time.time())
try: myID += str(os.getpid())
except: pass
markerFile="background"+dottxt
open(markerFile,"w").write(myID)
def reador0(f):
try: return read(f)
except: return 0
while reador0(markerFile)==myID:
if not currentDay == time.localtime()[:3]: # first run of day
currentDay = time.localtime()[:3]
if __name__=="__main__": # can do it by importing gradint
sys._gradint_innerImport = 1
try:
try: reload(gradint)
except NameError: import gradint
gradint.orig_onceperday = once_per_day
gradint.main()
try: reload(gradint)
except NameError: import gradint
gradint.orig_onceperday = once_per_day
try: gradint.main()
except SystemExit: pass
elif winsound and fileExists("gradint-wrapper.exe"): # in this setup we can do it by recursively calling gradint-wrapper.exe
s=" ".join(sys.argv[1:])
......@@ -424,6 +485,8 @@ if once_per_day&2 and not hasattr(sys,"_gradint_innerImport"): # run every day
show_warning("Not doing once_per_day&2 logic because not running as main program")
# (DO need to be able to re-init the module - they might change advanced.txt etc)
break
if len(sys.argv)>1: sys.argv.append(";")
sys.argv.append("disable_once_per_day=0") # don't let a disable_once_per_day=2 in argv result in repeated questioning
time.sleep(3600) # delay 1 hour at a time (in case hibernated)
if once_per_day&1 and fileExists(progressFile) and time.localtime(os.stat(progressFile).st_mtime)[:3]==time.localtime()[:3]: sys.exit() # already run today
try: orig_onceperday
......@@ -432,9 +495,9 @@ except: orig_onceperday=0
if winsound:
# check for users putting support files/folders in the desktop shortcuts folder and thinking it's the gradint folder
# We can't do much about detecting users on non-English Windows who have heeded the warning about moving the "Desktop" folder to the real desktop but then mistook this for the gradint folder when adding flite (but hopefully they'll be using ptts/espeak anyway, and yali has an installer)
if "HOMEDRIVE" in os.environ and "HOMEPATH" in os.environ: dr=os.environ["HOMEDRIVE"]+os.environ["HOMEPATH"]
if checkIn("HOMEDRIVE",os.environ) and checkIn("HOMEPATH",os.environ): dr=os.environ["HOMEDRIVE"]+os.environ["HOMEPATH"]
else: dr="C:\\Program Files" # as setup.bat (location for gradint on Win95 etc)
if "USERPROFILE" in os.environ: dr=os.environ["USERPROFILE"]
if checkIn("USERPROFILE",os.environ): dr=os.environ["USERPROFILE"]
if not dr[-1]=="\\": dr += "\\"
try: dirList = os.listdir(dr+"Desktop\\gradint\\") # trailing \ important, otherwise it can include gradint.zip etc on Desktop
except: dirList = []
......@@ -450,11 +513,42 @@ elif macsound:
os.system('open ../Gradint.app')
sys.exit(0)
elif fileExists_stat("../Gradint 2.app/deleteme"):
import thread ; thread.start_new_thread(lambda *x:(time.sleep(2),os.system('rm -rf "../Gradint 2.app"')),())
try: import thread
except ImportError: import _thread as thread
thread.start_new_thread(lambda *x:(time.sleep(2),os.system('rm -rf "../Gradint 2.app"')),())
def got_program(prog):
# Test to see if the program 'prog' is on the system, as portable as possible. NB some Unix 'which' output an error to stdout instead of stderr, so check the result exists.
return (winsound and fileExists(prog+".exe")) or (unix and fileExists_stat(os.popen("which "+prog+" 2>/dev/null").read().strip(wsp)))
if winsound:
if fileExists(prog+".exe"): return prog+".exe"
elif riscos_sound:
if prog[:1]=="*": # module
os.system("help "+prog[1:]+" { > _tstCmd_ }")
got = open("_tstCmd_").read().find(prog[1:].upper())>-1
os.unlink("_tstCmd_") ; return got
return checkIn("Alias$"+prog,os.environ) # works in Python 3.8 but not 2.7 (Alias$ vars hidden)
elif unix:
try:
try: from shutil import which as find_executable # PEP 632
except: from distutils.spawn import find_executable
if (":"+os.environ.get("PATH","")).find(":.")>-1:
prog = find_executable(prog)
else: # at least some distutils assume that "." is in the PATH even when it isn't, so do it ourselves without checking "."
oldCwd = os.getcwd()
pList = os.environ.get("PATH","").split(':')
if pList:
done=0
for p in pList:
try: os.chdir(p)
except: continue
done=1 ; break
if done:
prog = find_executable(prog)
os.chdir(oldCwd)
except ImportError:
# fall back to running 'which' in a shell (probably slower if got_program is called repeatedly)
prog = wspstrip(os.popen("which "+prog+" 2>/dev/null").read())
if not fileExists_stat(prog): prog=None # some Unix 'which' output an error to stdout instead of stderr, so check the result exists
return prog
def win2cygwin(path): # convert Windows path to Cygwin path
if path[1]==":": return "/cygdrive/"+path[0].lower()+path[2:].replace("\\","/")
......@@ -486,23 +580,17 @@ def check_for_interrupts(): # used on platforms where thread.interrupt_main won'
raise KeyboardInterrupt
# If forking, need to do so BEFORE importing any Tk module (we can't even verify Tk exists 1st)
if outputFile or justSynthesize or appuifw or not (winsound or winCEsound or mingw32 or macsound or riscos_sound or cygwin or "DISPLAY" in os.environ): useTK = 0
if useTK and runInBackground and not (winsound or mingw32) and hasattr(os,"fork") and not "gradint_no_fork" in os.environ:
import fcntl, termios
if outputFile or justSynthesize or appuifw or not (winsound or winCEsound or mingw32 or macsound or riscos_sound or cygwin or checkIn("DISPLAY",os.environ)): useTK = 0
if useTK and runInBackground and not (winsound or mingw32) and hasattr(os,"fork") and not checkIn("gradint_no_fork",os.environ):
if os.fork(): sys.exit()
os.setpgid(0,0)
os.setsid()
if os.fork(): sys.exit()
try: tty = os.open("/dev/tty", os.O_RDWR)
except: tty = None
if not tty==None:
fcntl.ioctl(tty, termios.TIOCNOTTY, 0)
os.close(tty)
devnull = os.open("/dev/null", os.O_RDWR)
for fd in range(3): os.dup2(devnull,fd)
else: runInBackground = 0
try: import readline # enable readline editing of raw_input()
except: pass
except: readline=0
try: import cPickle as pickle
except:
......@@ -520,5 +608,10 @@ try:
import locale
locale.setlocale(locale.LC_ALL, 'C')
except: pass
if not LB('\xc4').lower()==LB('\xc4'): # buggy setlocale (e.g. S60) can create portability issues with progress files
lTrans=B("").join([chr(c) for c in range(ord('A'))]+[chr(c) for c in range(ord('a'),ord('z')+1)]+[chr(c) for c in range(ord('Z')+1,256)])
def lower(s): return s.translate(lTrans) # (may crash if Unicode)
else:
def lower(s): return s.lower()
# -------------------------------------------------------
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# (Python 2 or Python 3, but more fully tested on 2)
program_name = "gradint v0.9969 (c) 2002-2011 Silas S. Brown. GPL v3+."
program_name = "gradint v3.095 (c) 2002-25 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
......@@ -21,8 +22,48 @@ appTitle = "Language lesson"
import sys,os
if not sys.version_info[0]==2: # oh panic, someone's probably trying to run us on Py3k
sys.stderr.write("Sorry, Gradint cannot run on Python "+repr(sys.version_info[0])+"\nPlease install a 2.x version of Python (must be 2.2+).\n")
sys.exit(1)
if sys.version_info[0]>2:
_map,_filter = map,filter
def map(*args): return list(_map(*args))
def filter(*args): return list(_filter(*args))
from functools import cmp_to_key
def sort(l,c): l.sort(key=cmp_to_key(c))
raw_input,unichr,xrange,long = input,chr,range,int
def chr(x): return unichr(x).encode('latin1')
from subprocess import getoutput
popenRB,popenWB = "r","w"
def unicode(b,enc):
if type(b)==str: return b
return b.decode(enc)
else: # Python 2
def sort(l,c): l.sort(c)
popenRB,popenWB = "rb","wb"
bytes = str
try: from commands import getoutput
except ImportError: pass
# For pre-2.3 versions of Python (e.g. 2.2 on Symbian S60 and Mac OS 10.3):
try: True
except: exec("True = 1 ; False = 0")
def readB(f,m=None):
if hasattr(f,"buffer"): f0,f=f,f.buffer # Python 3 non-"b" file
if m: return f.read(m)
else: return f.read() # no "None" in Python 2
def writeB(f,b):
if hasattr(f,"buffer"): f0,f=f,f.buffer # Python 3 non-"b" file
f.write(b)
def B(x):
if type(x)==bytes: return x
try: return x.encode('utf-8')
except: return x # maybe not a string
def LB(x):
if type(x)==bytes: return x
try: return x.encode('latin1')
except: return x
def S(x):
if type(x)==bytes and not bytes==str: return x.decode('utf-8')
return x
def S2(s):
try: return S(s)
except: return s # coding errors OK in unavail, leave as byte-string
# --------------------------------------------------------
# This file is part of the source code of
# gradint v0.9969 (c) 2002-2011 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
......@@ -43,21 +43,21 @@ def setup_samplesDir_ifNec(d=0): # if the user doesn't have a samples directory,
if not d: d=samplesDirectory
if not isDirectory(d):
os.mkdir(d)
if fileExists(user0[0]+os.sep+"README"+dottxt): open(d+os.sep+"README"+dottxt,'wb').write(read(user0[0]+os.sep+"README"+dottxt))
if fileExists(user0[0]+os.sep+"README"+dottxt): write(d+os.sep+"README"+dottxt,read(user0[0]+os.sep+"README"+dottxt))
def get_userNames(): # list of unicode user names or []
ret=[]
u=userNameFile ; c=0
while fileExists(u):
ret.append(unicode(u8strip(read(u)).strip(wsp),'utf-8'))
ret.append(unicode(bwspstrip(u8strip(read(u))),'utf-8'))
c += 1 ; u=addUserToFname(userNameFile,c)
global lastUserNames ; lastUserNames = ret
return ret
def set_userName(N,unicodeName): open(addUserToFname(userNameFile,N),"w").write(unicodeName.encode("utf-8")+"\n") # implicitly adds if N=num+1
def set_userName(N,unicodeName): writeB(open(addUserToFname(userNameFile,N),"w"),unicodeName.encode("utf-8")+B("\n")) # implicitly adds if N=num+1
def wrapped_set_userName(N,unicodeName):
if unicodeName.strip(wsp): set_userName(N,unicodeName)
if wspstrip(unicodeName): set_userName(N,unicodeName)
else: app.todo.alert="You need to type the person's name in the box before you press "+localise("Add new name") # don't waitOnMessage because we're in the GUI thread
GUI_usersRow = lastUserNames = None
......@@ -91,7 +91,7 @@ def updateUserRow(fromMainMenu=0):
userBSM = ButtonScrollingMixin() ; userBSM.ourCanvas = c
else: userBSM = None
for i in range(len(names)):
if names[i].strip(wsp):
if wspstrip(names[i]):
r=Tkinter.Radiobutton(row, text=names[i], variable=app.userNo, value=str(i), takefocus=0)
r.grid(row=i+1,column=0,sticky="w")
r["command"]=cmd=lambda e=None,i=i: select_userNumber(i)
......@@ -107,41 +107,42 @@ def updateUserRow(fromMainMenu=0):
r=Tkinter.Frame(row) ; r.grid(row=i+1,column=0,columnspan=4)
text,entry = addTextBox(r)
if not fromMainMenu: entry.focus() # because user has just pressed the "add other students" button, or has just added a name and may want to add another
l=lambda *args:(wrapped_set_userName(i,asUnicode(text.get())),updateUserRow())
l=lambda e=None,wrapped_set_userName=wrapped_set_userName,i=i,text=text:(wrapped_set_userName(i,asUnicode(text.get())),updateUserRow())
addButton(r,localise("Add new name"),l)
entry.bind('<Return>',l)
if not i: Tkinter.Label(row,text="The first name should be that of the\nEXISTING user (i.e. YOUR name).").grid(row=i+2,column=0,columnspan=4)
if userBSM: userBSM.bindFocusIn(r) # for shift-tab from the bottom
if hasattr(row,"widgetsToDel"): row.widgetsToDel.append(r)
if not names[i]: break
if userBSM: c.after(cond(winCEsound,1500,300),lambda *args:c.config(scrollregion=c.bbox(Tkinter.ALL),width=c.bbox(Tkinter.ALL)[2],height=min(c["height"],c.winfo_screenheight()/2,c.bbox(Tkinter.ALL)[3]))) # hacky (would be better if it could auto shrink on resize)
else: row.widgetsToDel.append(addButton(row,localise("Family mode (multiple user)"),lambda *args:(set_userName(0,""),updateUserRow())))
if userBSM: c.after(cond(winCEsound,1500,300),lambda e=None,c=c:c.config(scrollregion=c.bbox(Tkinter.ALL),width=c.bbox(Tkinter.ALL)[2],height=min(c["height"],c.winfo_screenheight()/2,c.bbox(Tkinter.ALL)[3]))) # hacky (would be better if it could auto shrink on resize)
else: row.widgetsToDel.append(addButton(row,localise("Family mode (multiple user)"),lambda e=None:(set_userName(0,""),updateUserRow())))
def renameUser(i,radioButton,parent,cancel=0):
if hasattr(radioButton,"in_renaming"):
if hasattr(radioButton,"in_renaming"): # finish the rename
del radioButton.in_renaming
n=asUnicode(radioButton.renameText.get())
if cancel: pass
elif not n.strip(wsp) and len(lastUserNames)>1: tkMessageBox.showinfo(app.master.title(),"You can't have blank user names unless there is only one user. Keeping the original name instead.")
elif not wspstrip(n) and (len(lastUserNames)>1 and not (len(lastUserNames)==2 and not lastUserNames[-1])): tkMessageBox.showinfo(app.master.title(),"You can't have blank user names unless there is only one user. Keeping the original name instead.")
else:
set_userName(i,n)
lastUserNames[i] = n
radioButton["text"]=n
radioButton.renameEntry.grid_forget()
radioButton.grid(row=i+1,column=0,sticky="w")
else:
else: # start the rename
radioButton.in_renaming = 1
radioButton.grid_forget()
radioButton.renameText,radioButton.renameEntry = addTextBox(parent,"nopack")
radioButton.renameEntry.grid(row=i+1,column=0)
radioButton.renameText.set(lastUserNames[i])
radioButton.renameEntry.focus()
radioButton.after(10,lambda *args:radioButton.renameEntry.event_generate('<End>'))
radioButton.renameEntry.bind('<Return>',lambda *args:renameUser(i,radioButton,parent))
radioButton.renameEntry.bind('<Escape>',lambda *args:renameUser(i,radioButton,parent,cancel=1))
radioButton.after(10,lambda e=None,radioButton=radioButton:radioButton.renameEntry.event_generate('<End>'))
radioButton.renameEntry.bind('<Return>',lambda e=None,radioButton=radioButton,i=i,parent=parent:renameUser(i,radioButton,parent))
radioButton.renameEntry.bind('<Escape>',lambda e=None,i=i,radioButton=radioButton,parent=parent:renameUser(i,radioButton,parent,cancel=1))
def deleteUser(i):
for n in ["Are you sure","Are you REALLY sure","This is your last chance: Are you REALLY SURE"]:
if not tkMessageBox.askyesno(app.master.title(),u""+n+" you want to delete "+lastUserNames[i]+" permanently, including any vocabulary list and recordings?"): return
if not tkMessageBox.askyesno(app.master.title(),ensure_unicode(n)+" you want to delete "+lastUserNames[i]+" permanently, including any vocabulary list and recordings?"): return
numUsers=len(lastUserNames)
for fileOrDir in user0+(userNameFile,):
d=addUserToFname(fileOrDir,i)
......
#!/usr/bin/env python
# (works on either Python 2 or Python 3)
# program to "thin down" the gradint .py for low memory environments
# by taking out some of the code that's unused on that platform
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
import sys, re
tk_only = [ # we want these on WinCE but not S60:
# note: comments are stripped BEFORE checking against this list
"def words_exist():",
"def reviseCount(num):", # used only in Tk for now
"if mp3web:",
"class InputSourceManager(object):",
"class InputSource(object):",
"class MicInput(InputSource):",
"class PlayerInput(InputSource):",
"class ButtonScrollingMixin(object):",
"class RecorderControls(ButtonScrollingMixin):",
"def doRecWords():",
"if app:","elif app:",
"def addStatus(widget,status,mouseOnly=0):",
"def addButton(parent,text,command,packing=None,status=None):",
"def addLabel(row,label):",
"def CXVMenu(e):",
"def selectAll(e):",
"def selectAllButNumber(e):",
"def addTextBox(row,wide=0):",
"def addLabelledBox(row,wide=0,status=None):",
"def addRow(parent,wide=0):",
"def addRightRow(widerow):",
"def make_output_row(parent):",
"def select_userNumber(N,updateGUI=1):",
"def select_userNumber2(N):",
"def updateUserRow(fromMainMenu=0):","def get_userNames():",
"def set_userName(N,unicodeName):",
"def wrapped_set_userName(N,unicodeName):",
"def renameUser(i,radioButton,parent,cancel=0):",
"def deleteUser(i):",
"def addUserToFname(fname,userNo):",
"def setupScrollbar(parent,rowNo):",
"def focusButton(button):",
"def bindUpDown(o,alsoLeftRight=False):",
"class ExtraButton(object):",
"def make_extra_buttons_waiting_list():",
"def startTk():",
# "def guiVocabList(parsedVocab):", # now actually used on S60
"def synchronizeListbox(listbox,masterList):",
"if useTK:",
"if useTK and not tkSnack:",
"def openDirectory(dir,inGuiThread=0):",
"def gui_event_loop():",
"def makeButton(parent,text,command):",
"def vocabLinesWithLangs():",
"if Tk_might_display_wrong_hanzi:",
"def setup_samplesDir_ifNec(d=0):",
"def filename2unicode(f):",
"def unicode2filename(u):",
]
not_S60_or_android = [ # but may still need on winCE
"if winsound:",
"if winsound or mingw32:",
"class ESpeakSynth(Synth):","def espeak_volume_ok():",
'if winCEsound or ((winsound or mingw32) and not os.sep in tmpPrefix and not tmpPrefix.startswith("C:")):',
'def got_program(prog):', # as no non-winsound/unix
'if useTK and runInBackground and not (winsound or mingw32) and hasattr(os,"fork") and not "gradint_no_fork" in os.environ:',
'def maybe_warn_mp3():',
'elif (cygwin or ((winsound or mingw32) and winsound_also)) and os.sep in file:',
'elif (winsound and not (self.length>10 and wavPlayer)) or winCEsound:',
"elif wavPlayer.find('sndrec32')>=0:",
'elif wavPlayer:', # it'll take appuifw/android 1st
'if winsound or mingw32 or cygwin:',
'elif winsound or mingw32 or cygwin:',
'for s in synth_priorities.split():', # Ekho/eSpeak/MacOS/SAPI not available on S60/Android (well, not that we can yet call into)
'def import_recordings(destDir=None):', # TODO: document in advanced.txt that this option is non-functional on S60/Android? (code WOULD work if suitably configured, but unlikely to be used and we need to save size)
"elif msvcrt:",
]
not_android = [
"if not app and not app==False and not appuifw and not android:",
"elif not android:",
"def fileExists(f):", # assume we got os.path
"def fileExists_stat(f):",
"def isDirectory(directory):",
"for p in [progressFile,progressFileBackup,pickledProgressFile]:", # this coherence check is not likely to be a problem on Android, and we could do with saving the space
"if need_say_where_put_progress:", # ditto
'def check_for_interrupts():','if emulated_interruptMain:','if emulated_interruptMain or winCEsound:','def handleInterrupt():', # no current way to do this on Android (unlike S60/WinCE)
r"if not '\xc4'.lower()=='\xc4':", # this workaround is not needed on Android
r"if not fileExists(configFiles[0]) and sys.argv and (os.sep in sys.argv[0] or (os.sep=='\\' and '/' in sys.argv[0])):", # that logic not likely to work on Android (but we do need the rest of that block)
"def guiVocabList(parsedVocab):", # not yet available on Android (unlike S60, TODO?)
]
riscos_only = [
"if riscos_sound:",
"elif riscos_sound:",
'if riscos_sound and hex(int(time.time())).find("0xFFFFFFFF")>=0 and not outputFile:',
"class OldRiscosSynth(Synth):",
'if not extsep==".":', # RISC OS
]
mac_only = [
'if macsound and __name__=="__main__":',
'if macsound and "_" in os.environ:',
"if macsound:","elif macsound:",
'if hasattr(app,"isBigPrint") and macsound:',
'elif macsound and got_program("afconvert"):',
]
desktop_only = [ # Don't want these on either WinCE or S60:
'if hasattr(app,"isBigPrint") and winsound:',
"if unix:","elif unix:",
"def disable_lid(restore):",
'if unix and isDirectory("/dev/snd") and got_program("arecord"):',
"if unix and (';' in cmd or '<' in cmd):",
'elif wavPlayer=="sox":',
'elif wavPlayer=="aplay" and ((not fileType=="mp3") or madplay_path or gotSox):',
"def simplified_header(fname):",
"def win2cygwin(path):","elif cygwin:",
"if paranoid_file_management:",
"elif unix and not macsound:",
"elif unix and hasattr(os,\"popen\"):",
"def wavToMp3(directory):",
"def makeMp3Zips(baseDir,outDir,zipNo=0,direc=None):",
"def check_for_slacking():",
"def checkAge(fname,message):",
"def downloadLAME():",
"def decode_mp3(file):",
"class Mp3FileCache(object):",
"class OSXSynth_Say(Synth):",
"def aiff2wav(fname):", # (used only on Mac)
"class OSXSynth_OSAScript(Synth):",
"class PttsSynth(Synth):",
"def sapi_sox_bug_workaround(wavdata):",
"class FliteSynth(Synth):",
"def espeak_stdout_works():", # called only if unix
# (keep ESpeakSynth for WinCE)
"class EkhoSynth(Synth):",
"class FestivalSynth(Synth):",
"class GeneralSynth(Synth):", # (needs os.system, so not S60/WinCE)
"class GeneralFileSynth(Synth):", # (ditto)
"class ShellEvent(Event):",
# And the following are desktop only because they need sox:
"if gotSox and unix:",
"class SoundCollector(object):","if soundCollector:",
"def oggenc():",
"def outfile_writeBytes(o,bytes):",
"def outfile_close(o):",
"def outfile_writeFile(o,handle,filename):",
"class ShSoundCollector(object):",
"def outfile_write_error():",
"def lame_quiet():",
"def beepCmd(soxParams,fname):",
"def collector_time():",
"def collector_sleep(s):",
"def dd_command(offset,length):",
"def lame_endian_parameters():",
"if outputFile:",
"def setSoundCollector(sc):",
"def getAmplify(directory):",
"def doAmplify(directory,fileList,factor):",
"def gui_outputTo_end(openDir=True):",
"def gui_outputTo_start():",
"def warn_sox_decode():",
'if disable_once_per_day==1:',
'if once_per_day&2 and not hasattr(sys,"_gradint_innerImport"):',
'if once_per_day&1 and fileExists(progressFile) and time.localtime(os.stat(progressFile).st_mtime)[:3]==time.localtime()[:3]:',
'def optimise_partial_playing(ce):',
'def optimise_partial_playing_list(ceList):',
]
winCE_only = [
"if use_unicode_filenames:",
"if winCEsound:",'elif winCEsound:',
'if winCEsound and __name__=="__main__":',
'elif winCEsound and fileType=="mp3":',
"if WMstandard:",
]
not_winCE = [
"if not winCEsound:",
]
S60_only = [
'if sys.platform.find("ymbian")>-1:',
"class S60Synth(Synth):",
"if appuifw:","elif appuifw:",
"def s60_recordWord():",
"def s60_recordFile(language):",
"def s60_addVocab():",
"def s60_changeLang():",
"def s60_runLesson():",
"def s60_viewVocab():",
"def s60_main_menu():",
]
android_only = [
"if android:",
"elif android:",
"class AndroidSynth(Synth):",
"def android_recordWord():",
"def android_recordFile(language):",
"def android_main_menu():",
"def android_addVocab():",
"def android_changeLang():",
]
unix_only = [
"class CoquiSynth(Synth):",
"class PiperSynth(Synth):",
]
android_or_S60 = [
"def droidOrS60RecWord(recFunc,inputFunc):",
]
if "s60" in sys.argv: # S60 version
version = "S60"
to_omit = tk_only + desktop_only + winCE_only + not_S60_or_android + android_only + riscos_only + mac_only + unix_only
elif "android" in sys.argv: # Android version
version = "Android"
to_omit = tk_only + desktop_only + winCE_only + S60_only + not_S60_or_android + not_android + riscos_only + mac_only + unix_only
elif "wince" in sys.argv: # Windows Mobile version
version = "WinCE"
to_omit = desktop_only + S60_only + android_only + android_or_S60 + not_winCE + riscos_only + mac_only + unix_only
elif "core" in sys.argv: # experimental "core code only" for 'minimal embedded porting' starting point (no UI, no synth, limited file I/O; you'll probably have to load up the event data yourself)
version = "core"
to_omit = tk_only + not_S60_or_android + not_android + riscos_only + mac_only + desktop_only + winCE_only + S60_only + android_only + android_or_S60 + unix_only + [
"def main():",
"def rest_of_main():",
'if __name__=="__main__":',
"def transliterates_differently(text,lang):",
"def primitive_synthloop():",
"def appendVocabFileInRightLanguages():",
'def delOrReplace(L2toDel,L1toDel,newL2,newL1,action="delete"):',
"def generalCheck(text,language,pauseOnError=0):",
"def localise(s):",
"def singular(number,s):",
"def readText(l):",
"def asUnicode(x):",
"def updateSettingsFile(fname,newVals):",
"def clearScreen():",
"def startBrowser(url):",'def getYN(msg,defaultIfEof="n"):',"def waitOnMessage(msg):",
"def interrupt_instructions():",
"def parseSynthVocab(fname,forGUI=0):",
"def scanSamples_inner(directory,retVal,doLimit):",
"def getLsDic(directory):",
"def check_has_variants(directory,ls):",
"def exec_in_a_func(x):",
"def scanSamples(directory=None):",
"def synth_from_partials(text,lang,voice=None,isStart=1):",
"def partials_langname(lang):",
"if partialsDirectory and isDirectory(partialsDirectory):",
'for zipToCheck in ["yali-voice","yali-lower","cameron-voice"]:',
'def stripPuncEtc(text):',
'def can_be_synthesized(fname,dirBase=None,lang=None):',
'def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=None):',
'def textof(fname):',
'if synthCache and transTbl in synthCache_contents:',
'if synthCache:',
'class Partials_Synth(Synth):',
'def abspath_from_start(p):',
'class SynthEvent(Event):',
'def pinyin_uColon_to_V(pinyin):',
'def synth_event(language,text,is_prompt=0):',
'def get_synth_if_possible(language,warn=1,to_transliterate=False):',
'if wavPlayer_override or (unix and not macsound and not (oss_sound_device=="/dev/sound/dsp" or oss_sound_device=="/dev/dsp")):',
'def fix_compatibility(utext):',
'def read_chinese_number(num):',
'def preprocess_chinese_numbers(utext,isCant=0):',
'def intor0(v):',
'def fix_pinyin(pinyin,en_words):',
'def fix_commas(text):',
'def shell_escape(text):',
'class SimpleZhTransliterator(object):',
'def sort_out_pinyin_3rd_tones(pinyin):',
'def ensure_unicode(text):',
'def unzip_and_delete(f,specificFiles="",ignore_fail=0):',
'class Synth(object):',
'def quickGuess(letters,lettersPerSec):',"def changeToDirOf(file,winsound_also=0):",'if app or appuifw or android:',
'def subst_some_synth_for_synthcache(events):',
'def decide_subst_synth(cache_fname):',
'if winsound or winCEsound or mingw32 or riscos_sound or not hasattr(os,"tempnam") or android:',
'if len(sys.argv)>1:',
'def readSettings(f):',
'def exc_info(inGradint=True):',
'if not fileExists(configFiles[0]):',
'def u8strip(d):']
else: assert 0, "Unrecognised version on command line"
revertToIndent = lastIndentLevel = indentLevel = -1
lCount = -1 ; inTripleQuotes=0 ; orig = []
for l in sys.stdin:
orig.append(l)
lCount += 1
if lCount==2: print ("\n# NOTE: this version has been automatically TRIMMED for "+version+" (some non-"+version+" code taken out)\n")
l=l.rstrip()
assert not "\t" in l, "can't cope with tabs"
lastIndentLevel,indentLevel = indentLevel,-1
for i in range(len(l)):
if not l[i]==" ":
indentLevel = i ; break
was_inTripleQuotes = inTripleQuotes
if (len(l.split('"""'))%2) == 0: inTripleQuotes = not inTripleQuotes
if indentLevel<0 or indentLevel==len(l) or (revertToIndent>=0 and (indentLevel>revertToIndent or was_inTripleQuotes)): continue
justRevertedI,revertToIndent = revertToIndent,-1
code0 = (l+"#")[:l.find("#")].rstrip()
code = code0.lstrip()
if (code in to_omit or (':' in code and code[:code.index(':')+1] in to_omit)) and not was_inTripleQuotes:
if ':' in code and code[:code.index(':')+1] in to_omit: code = code[:code.index(':')+1]
if code.startswith("def "): code=re.sub(r"\([^)][^)][^)]+\)",r"(*_)",code)
if code.startswith("elif "): pass # can always remove those lines completely, even if will be followed by an 'else' (and will never be the only thing in its block)
else:
if code.startswith("if "): code="if 0:"
print (" "*indentLevel+code+" pass # trimmed")
revertToIndent = indentLevel
elif not code:
if "# " in l or lCount < 2: print (l) # keep start and GPL comments
elif ('"' in code and '"' in l[len(code):]) or ("'" in code and "'" in l[len(code):]): print (l) # perhaps # was in a string, keep it
else: print (code0)
orig = "".join(orig)
for o in to_omit:
if not o in orig: sys.stderr.write("Warning: line not matched: "+o)
......@@ -18,92 +18,11 @@
# file 'advanced.txt' as well.
#
# IMPORTANT: Before speech synthesis will work, make sure
# that your computer has the necessary speech synthesizers
# for BOTH languages. Gradint supports:
#
# 1. eSpeak, a very multilingual and multiplatform speech
# synthesizer available at http://espeak.sourceforge.net/
# and bundled with the Windows and Mac versions of Gradint.
#
# - Just install it and gradint will find it. Any of
# eSpeak's languages can be used as long as you use the
# same language abbreviations as eSpeak does, e.g. "en"
# for English, "zh" for Zhongwen (Mandarin).
#
# - By default eSpeak will be used for English too; if you want
# to use Windows's or Mac OS X's voices for English, set the variable
# 'synth_priorities' in advanced.txt.
#
# - You can improve eSpeak's English by installing
# Festival's dictionary and using lexconvert to convert
# it, see http://people.pwf.cam.ac.uk/ssb22/gradint/lexconvert.html
# (this has already been done in the bundled version).
#
# - eSpeak is not very natural-sounding, but it is very
# clear and accurate in English, Mandarin and some other
# languages (although some languages may be poor e.g. the
# ones marked "testing" or "feedback needed").
# that your computer has the necessary speech synthesizers.
# See advanced.txt if you need to change the speech synth setup.
#
# 2. Microsoft SAPI 5 (included with Windows XP, and can be
# added to earlier versions of Windows)
#
# - can be used for English (language abbreviation 'en')
#
# - can be used for Chinese if you have installed the
# NeoSpeech "Lily" Mandarin voice (language abbreviation
# must be 'zh', and use pinyin with tone numbers, 5 for
# neutral tone, v or u: for u-umlaut and if possible put spaces
# between meaningful words).
# You can also include hanzi, if you make sure to save the file in UTF-8 format.
# - Lily is very natural-sounding, but it has been known
# to get quite a few phrases subtly wrong,
# so make sure you can tell the difference.
#
# - can be used for other voices if you set
# sapiVoices in advanced.txt.
#
# The control panel's "Speech" option can choose the English
# voice (if you're using SAPI for English), and also the
# speed of all SAPI voices.
#
# 3. Other speech synthesizers:
#
# MANDARIN CHINESE - language abbreviation must be 'zh';
# text should use Hanyu Pinyin with a tone number after
# each syllable (use 5 for neutral) and v or u: for u-umlaut
# (and if possible put spaces between meaningful words but
# not between syllables within a word):
#
# - eSpeak or NeoSpeech Lily, as described above.
# (you can also include hanzi in UTF-8)
# + others (MeiLing, Loquendo, etc) if you set sapiVoices
#
# - Yali Cheng's Mandarin syllable recordings - see
# instructions on website. (You can also include
# hanzi in UTF-8 if eSpeak is present.) Recommended.
#
# ENGLISH (language abbreviation must be "en"):
#
# - Mac OS X comes with English speech as standard
# (you can choose the voice and speed in system preferences)
#
# - Festival Lite on Windows (if all else fails) :
# put flite.exe in the gradint folder
#
# - Linux: install Festival, or flite if you want a US
# accent, or (as already mentioned) eSpeak
#
# - RISC OS: if for some reason you can't install eSpeak
# as mentioned above, you can instead install an older
# version of Jonathan Duddington's !Speak, or the even
# older "Speech!" utility. These can be used only for
# playing in real-time, not for generating files.
# If using non-ASCII characters, please choose UTF-8 coding.
#
#
# 5. Other speech software can be used if you can tell
# gradint how to run it. See 'advanced.txt' for details.
#
#
# OTHER POINTS TO NOTE:
#
# If you want to specify that a group of words should be
......@@ -126,7 +45,7 @@
# quotes) on a line by itself, and the line immediately under it will be
# interpreted normally without being linked into the poem. This is useful
# for setting relevant vocabulary to be introduced part-way through learning
# the poem.
# the poem. (In recent versions of Gradint the colon can be omitted.)
#
# If you have recordings in one language and you want the
# equivalents in another language to be synthesized, you can
......@@ -136,7 +55,6 @@
# somefile_lang2.txt to make a version in lang2 (where lang1
# and lang2 are any language abbreviations). Each .txt file
# should contain only 1 phrase in 1 language and nothing else.
# (RISC OS users should replace '.' with '/'.)
# Verbal annotations, and -meaning files (see samples README
# file for details) can also be in .txt files.
......@@ -144,7 +62,8 @@
# on the top line of this file, Gradint's GUI will assume
# you don't need to be asked if you're sure when editing files)
#The three lines below are to help the Emacs editor.
# The three lines below are to help the Emacs editor.
# (XEmacs users might like to try winmgr instead of m4)
#Local Variables:
#mode: winmgr
#mode: m4
#End:
#!/bin/bash
# Convert a Windows gradint.exe or gradint-bundle.exe
# (optionally with bundled samples, partials etc) into
# a GNU/Linux installation of Gradint. Use this to
# support both Windows and GNU/Linux from one bundle.
# (c) 2013,2021-22 Silas S. Brown. License: GPL v3 (as Gradint)
# This script can be placed on a USB stick or whatever, in
# the same directory as:
# (1) gradint.exe or gradint-bundle.exe
# (2) 7za binary (for correct CPU type and libraries), if
# 7za is not installed on the system,
# (3) espeak binary (ditto)
# (4) any *.deb files to install with dpkg, e.g. python-tk
# and its dependencies (e.g. blt tk-8.5)
# (these will be installed with --force-depends, in case
# you're doing an offline install and can manage without
# having the most up-to-date dependencies - do this at
# your own risk)
# You can also put the 7za, espeak and deb files into a
# bin/ subdirectory.
DoneDeb=0
if ! test "$(echo *.deb)" == "*.deb"; then
echo "Installing *.deb (with --force-depends)"
sudo dpkg --force-depends -i *.deb
DoneDeb=1
elif ! test "$(echo bin/*.deb)" == "bin/*.deb"; then
echo "Installing bin/*.deb (with --force-depends)"
sudo dpkg --force-depends -i bin/*.deb
DoneDeb=1
fi
# TODO: if got internet, sudo apt-get update ; sudo apt-get -f install
if [ -f espeak ] || [ -f bin/espeak ]; then
echo "Copying espeak binary to /usr/local/bin"
if [ -f espeak ]; then sudo cp espeak /usr/local/bin/
else sudo cp bin/espeak /usr/local/bin/; fi
elif ! which espeak 2>/dev/null >/dev/null && ! which speak 2>/dev/null >/dev/null; then
echo "Warning: no espeak binary found on system, and none to install" # TODO: try to apt-get it? but might not have an Internet connection
echo -n "Press Enter: " ; read
fi
mkdir -p "$HOME/gradint0"
if [ -e gradint-bundle.exe ]; then
PATH="$PATH:.:./bin" 7za "-o$HOME/gradint0" x gradint-bundle.exe || exit 1
else PATH="$PATH:.:./bin" 7za "-o$HOME/gradint0" x gradint.exe || exit 1; fi
cd "$HOME/gradint0" || exit 1
mv gradint .. || exit 1
cd .. && rm -rf gradint0
cd gradint || exit 1
unzip library.zip gradint.py || exit 1
rm -rf tcl library.zip ./*.exe ./*.pyd ./*.dll
if python -c 'import tkinter'; then
if [ -e ~/Desktop ] && ! [ -e ~/Desktop/Gradint ]; then
echo "Creating symlink on Desktop"
ln -s "$(pwd)/gradint.py" ~/Desktop/Gradint
# TODO: what about the menus of various front-ends? (might need to make .desktop files in .local/share/applications or something)
else echo "Not creating symlink on desktop" # TODO: cater for more possibilities
fi
else echo "Warning: no tkinter on this system; gradint will be command-line only" ; echo -n "Press Enter: " ; read # TODO: if internet, try sudo apt-get install python-tk
fi
echo "Copying espeak-data to /usr/share/"
sudo cp -r espeak/espeak-data /usr/share/ && rm -rf espeak
echo "win2linux.sh finished"
if test $DoneDeb == 1; then
echo
echo "WARNING: Have installed packages with dpkg --force-depends."
echo "If you're connected to the Internet, you might now wish to do:"
echo " sudo apt-get update ; sudo apt-get -f install"
echo "or if the machine will later be connected to the"
echo "Internet without you, you might wish to do:"
echo " (echo apt-get update; echo apt-get -yf install) | sudo at -M midnight"
fi
File added
File moved
File added
File added
@echo off
rem This is Gradint's setup file for Windows.
rem If you are seeing this, when you expected
rem the Gradint installer to be running, then
rem it probably means your copy of Windows has
rem an incorrect file association. That will
rem affect other programs, not just Gradint.
rem To fix the incorrect association, try
rem running regedit and deleting the key:
rem Hkey_Current_User\Software\Microsoft\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.bat
rem then restart your system and the Gradint
rem installer should work normally.
rem This problem has been reported on some
rem (but not all) Windows 7 setups.
rem --------------------------------------------
rem Find a good place to put Gradint. On Windows 9x this can be C:\Program Files. On XP/NT/etc we'd better check for different home directories. Also check where the profile is.
if not exist "%HOMEDRIVE%%HOMEPATH%" set HOMEDRIVE=C:
......@@ -42,6 +58,9 @@ rem copy all program files, even the ones that have never been changed, in case
tskill gradint-wrapper 2>nul
taskkill /f /im gradint-wrapper.exe 2>nul >nul
cd gradint
rem clean up after old versions
if exist background1.txt del background1.txt
if exist background2.txt del background2.txt
rem support bundles
for /D %%g in (*_disabled) do xcopy /I %%g "%HOMEDRIVE%%HOMEPATH%\gradint\%%g" /S
rem support users who install yali BEFORE gradint
......@@ -119,6 +138,7 @@ rem (deliberately saying "press any key" ourselves not from 'pause', otherwise t
mkdir "%USERPROFILE%\Desktop\gradint"
copy /Y shortcuts\*.* "%USERPROFILE%\Desktop\gradint"
if exist "%AppData%\Microsoft\Windows\Start Menu\Programs\Startup" goto win8
mkdir "%USERPROFILE%\Start Menu\Programs\gradint"
copy /Y shortcuts\*.* "%USERPROFILE%\Start Menu\Programs\gradint"
......@@ -126,10 +146,18 @@ rem Install startup once-per-day thing
mkdir "%USERPROFILE%\Start Menu\Programs\Startup"
copy /Y startup\*.* "%USERPROFILE%\Start Menu\Programs\Startup"
goto win8skip
:win8
copy /Y startup\*.* "%AppData%\Microsoft\Windows\Start Menu\Programs\Startup"
mkdir "%AppData%\Microsoft\Windows\Start Menu\Programs\gradint"
copy /Y shortcuts\*.* "%AppData%\Microsoft\Windows\Start Menu\Programs\gradint"
:win8skip
cd /D "%USERPROFILE%\Desktop"
goto end
:PRC
rem This is a special case for Chinese (Simplified) Windows, configured to use the "Chinese (PRC)" locale for legacy apps (which means these strings should be gb2312 coded).
rem This is a special case for Chinese (Simplified) Windows (XP etc), configured to use the "Chinese (PRC)" locale for legacy apps (which means these strings should be gb2312 coded).
rem (You can get the names of Start Menu etc folders coded in the current locale by doing dir > file.txt at a command prompt and inspecting file.txt)
mkdir "%USERPROFILE%\戮충\gradint"
ren shortcuts\uninstall.bat "shortcuts\탤뇜.bat"
......@@ -147,4 +175,4 @@ rem start explorer gradint
rem (actually, since there's only 1 shortcut now,
rem we might as well just launch it directly)
cd /D "%HOMEDRIVE%%HOMEPATH%\gradint"
start gradint-wrapper.exe once_per_day=2
start gradint-wrapper.exe once_per_day=disable_once_per_day=2