# This file is part of the source code of # gradint v0.998 (c) 2002-2012 Silas S. Brown. GPL v3+. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (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. # Start of recording.py - GUI-based management of recorded words try: import tkSnack except: tkSnack = 0 class InputSource(object): def startRec(self,outFile,lastStopRecVal=None): pass # start recording to outFile def stopRec(self): pass # stop recording def close(self): pass # stop everything def currentTime(self): return 0 # (makes sense only for PlayerInput, but here just in case) def __del__(self): self.close() class MicInput(InputSource): def __init__(self): rates = tkSnack.audio.rates() if rates: for rate in [22050, 16000, 24000, 44100]: if rate in rates: self.rate = rate ; return self.rate = max(rates) else: self.rate = None def startRec(self,outFile,lastStopRecVal=None): 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"") 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)) 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) else: self.sound = tkSnack.Sound(file=fileToPlay) self.startSample = 0 self.sampleRate = self.sound.info()[1] self.length = self.sound.length()*1.0/self.sampleRate if not self.length: self.length=lengthOfSound(fileToPlay) # tkSnack bug workaround. NB don't just set it to 3 because it may be less than that, and user may press Record before the 3secs are up, expecting to record from mic. self.autostop_thread_id = 0 self.inCtor = 1 if startNow: self.startPlaying(max(0,int(startTime*self.sampleRate))) self.inCtor = 0 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 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): if thread_id==None: self.autostop_thread_id += 1 thread_id=self.autostop_thread_id elif not thread_id==self.autostop_thread_id: return # a stale autostop thread if not theISM or not theISM.nowPlaying==self or not tkSnack or not tkSnack.audio: return # closing down anyway elapsedTime = self.elapsedTime() if elapsedTime>=self.length-self.startSample*1.0/self.sampleRate: self.close() else: import thread def stopMe(self,thread_id): 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)) def elapsedTime(self): try: t=tkSnack.audio.elapsedTime() except: t=0.0 if t==0.0: t=time.time()-self.startTime return t def currentSample(self): return int(self.elapsedTime()*self.sampleRate)+self.startSample def currentTime(self): return self.currentSample()*1.0/self.sampleRate def startRec(self,outFile,lastStopRecVal=None): if lastStopRecVal: self.recordingStartSample = lastStopRecVal elif theISM.nowPlaying==self: self.recordingStartSample = self.currentSample() else: self.startPlaying() self.recordingStartSample = 0 self.fileToWrite = outFile def stopRec(self,closing = False): if not hasattr(self,"fileToWrite"): return curSample = self.currentSample() self.sound.stop() self.sound.write(self.fileToWrite,start=self.recordingStartSample,end=curSample) if not closing: self.startPlaying(curSample) del self.fileToWrite return curSample def close(self): if not hasattr(self,"sound"): return # called twice? self.stopRec(True) if theISM.nowPlaying == self: theISM.nowPlaying = None self.sound.stop() self.sound.destroy() del self.sound if hasattr(self,"fileToDel"): os.unlink(self.fileToDel) theISM.finished(self) global theRecorderControls try: theRecorderControls except: theRecorderControls=0 if theRecorderControls: if self.inCtor: # tried to skip off end - DO ensure the GUI resets its controls when that happens, even if it has "protected" itself due to restarting the sample at different position 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) 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)) time.sleep(lastStopRecVal) # allow process to start def stopRec(self): os.kill(self.pid,2) # INT return 0.3 tkSnack = "MicOnly" class InputSourceManager(object): def __init__(self): self.currentInputSource = None self.currentOutfile = self.nowPlaying = None # (nowPlaying is for PlayerInputSource to manage) def setInputSource(self,inputSource): self.stopRecording() if self.currentInputSource: self.currentInputSource.close() self.currentInputSource = inputSource def finished(self,inputSource): # called by the inputSource itself; we don't need to call its close() if not self.currentInputSource==inputSource: return # irrelevant self.stopRecording() self.currentInputSource = None def startRecording(self, newOutfile): if not self.currentInputSource: self.currentInputSource = MicInput() if self.currentOutfile: self.currentInputSource.startRec(newOutfile,self.currentInputSource.stopRec()) else: self.currentInputSource.startRec(newOutfile) self.currentOutfile = newOutfile def stopRecording(self): if self.currentOutfile: self.currentInputSource.stopRec() self.currentOutfile = None theISM = InputSourceManager() ; del InputSourceManager # singleton def wavToMp3(directory): # Compress all WAVs in directory to MP3 (CBR for gradint i/p) # don't worry about progress.txt - mergeProgress will recognise WAVs replaced with mp3 for l in os.listdir(directory): if l.lower().endswith(dotwav): needRetry = 1 ; tries = 0 while needRetry: tries += 1 needRetry = system("lame \"%s\" --cbr -b 48 -m m -o \"%s\"" % (directory+os.sep+l, directory+os.sep+l[:-len(dotwav)]+dotmp3)) if not needRetry: os.remove(directory+os.sep+l) elif paranoid_file_management and tries<10: time.sleep(0.5) else: show_warning("lame failed on "+directory+os.sep+l) ; break elif isDirectory(directory+os.sep+l): wavToMp3(directory+os.sep+l) 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 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) else: if zipNo: zipNo -= 1 zipfile = None while not zipfile or (fileExists(zipfile) and filelen(zipfile) >= zipSplitThreshold): zipNo += 1 zipfile = outDir+os.sep+"zipfile"+str(zipNo)+extsep+"zip" system("zip -9 \"%s\" \"%s\"" % (zipfile,baseDir+os.sep+direc)) return zipNo def getAmplify(directory): statfile = os.tempnam() tmplist = [] if unix: out2nul="-t wav /dev/null" # sox bug workaround on some versions elif winsound: out2nul="-t wav nul" # sox bug workaround else: out2nul="-t nul nul" 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"): if l.startswith("Volume adjustment:"): factor=l.split()[2] if not factor: continue tmplist.append([float(factor),f,factor]) try: os.remove(statfile) except: pass if not tmplist: return [],"",0 tmplist.sort() minFactor = tmplist[-1][0]/2 # amplify softest by at least half its required amp (TODO parameterise that 2) i=len(tmplist)-1 while i>=0: if tmplist[i][0]<minFactor: break i -= 1 i += 1 if not tmplist[i][2]==tmplist[0][2]: tmplist[i][2]="%.3f" % (tmplist[i][0]/tmplist[0][0]) # don't make the softest ones louder than the existing loudest one if tmplist[i][2].startswith("1.0"): return [],"",len(tmplist) else: return map(lambda x:x[1],tmplist[i:]),tmplist[i][2],i def doAmplify(directory,fileList,factor): failures = 0 for f in fileList: if system("sox \""+directory+os.sep+f+"\" -t wav \""+directory+os.sep+"tmp0\" vol "+factor): failures += 1 else: os.remove(directory+os.sep+f) os.rename(directory+os.sep+"tmp0",directory+os.sep+f) return failures class ButtonScrollingMixin(object): # expects self.ourCanvas def bindFocusIn(self,b): b.bind('<FocusIn>',lambda *args:self.scrollIntoView(b)) if not hasattr(app,"gotFocusInHandler"): # (see scrollIntoView method's use of justGotFocusIn) app.gotFocusInHandler=1 def set(*args): def clear(*args): try: del app.justGotFocusIn except: pass app.justGotFocusIn = 1 app.after(1, clear) # (that delay is quite short, but it shouldn't execute until after the current chain of FocusIn events finishes) app.bind('<FocusIn>',set) def scrollIntoView(self,button): if hasattr(app,"justGotFocusIn"): return # ignore double <FocusIn> event - allows switch out of app and back in again w/out scrolling back to the keyboard-focused button self.scrollingIntoView = button self.continueScrollIntoView(button) def continueScrollIntoView(self,button): if not self.scrollingIntoView==button: return # some other button took over if not hasattr(self,"ourCanvas"): return # closing down? by,bh,cy,ch = button.winfo_rooty(),button.winfo_height(),self.ourCanvas.winfo_rooty(),self.ourCanvas.winfo_height() if not by or not bh or not cy or not ch: pass # wait a bit longer elif by+bh >= cy+ch-cond(ch>2*bh,bh,0): self.ourCanvas.yview("scroll","1","units") # can't specify pixels, so have to keep advancing until we get it 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)) class RecorderControls(ButtonScrollingMixin): def __init__(self): self.snack_initialized = 0 self.currentDir = samplesDirectory setup_samplesDir_ifNec() self.coords2buttons = {} self.syncFlag = False self.always_enable_rerecord = self.always_enable_synth = False self.old_recordFrom_button = None self.renamevar_msg = "Renaming a variant from the GUI is not implemented yet. Please press the Advanced button and do it from the file manager." def changeDir(self,newDir): self.undraw() oldDir = self.currentDir self.currentDir = newDir self.draw(oldDir) def global_rerecord(self,*args): self.undraw() self.always_enable_rerecord = True self.draw() def enable_synth(self,*args): self.undraw() self.always_enable_synth = True self.draw() def finished(self,*args): app.master.title(appTitle) self.undraw() del app.scanrow if recorderMode: app.cancel() else: app.todo.set_main_menu=1 def undraw(self): if hasattr(self,"renameToCancel"): del self.renameToCancel self.coords2buttons = {} del self.ourCanvas 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() b = makeButton(self.grid,text=text,command=command) self.bindFocusIn(b) self.coords2buttons[(row,col)] = b if not colspan: if not col: colspan=1+3*len(self.languagesToDraw) else: colspan = 1 if olpc and colspan==1: # don't have the biggest font otherwise can't get to Record buttons on rightmost column if len(text)>6: self.coords2buttons[(row,col)]["font"]="Helvetica 9" else: self.coords2buttons[(row,col)]["font"]="Helvetica 12" 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)) 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))) 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)) 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"): rr,cc = self.renameToCancel 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)))) 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)) 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") 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?) 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))) 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) toAmp,factor,numOutliers = getAmplify(self.currentDir) if not toAmp: if numOutliers>1: app.todo.alert=localise("Found %d files but they were too loud to amplify") % numOutliers elif numOutliers: app.todo.alert=localise("Found 1 file but it was too loud to amplify") else: app.todo.alert=localise("No WAV files found in this folder") elif tkMessageBox.askyesno(app.master.title(),localise("Amplify %d files by %s? (exceptions: %d)") % (len(toAmp),factor,numOutliers)): f=doAmplify(self.currentDir,toAmp,factor) if f: app.todo.alert="%d sox failures" % f self.AmplifyButton["text"] = localise("Amplify") def all2mp3_or_zip(self,*args): self.CompressButton["text"] = localise("Compressing, please wait") if got_program("lame"): wavToMp3(self.currentDir) # TODO not in the GUI thread !! (but lock our other buttons while it's doing it) if got_program("zip") and (explorerCommand or winCEsound) and (not got_program("lame") or tkMessageBox.askyesno(app.master.title(),localise("All recordings have been compressed to MP3. Do you also want to make a ZIP file for sending as email?"))): try: os.mkdir(self.currentDir+os.sep+"zips") except: pass # already exists? 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) 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() def startRename(self,row,col,filename): if hasattr(self,"renameToCancel"): rr,cc = self.renameToCancel self.cancelRename(rr,cc) if self.has_variants and filename.find(" (")>-1: app.todo.alert=self.renamevar_msg return self.renameToCancel = (row,col) if (row,col) in self.coords2buttons: self.coords2buttons[(row,col)].grid_forget() renameText,renameEntry = addTextBox(self.grid,"nopack") renameEntry['width']=min(8,len(filename)+2) renameEntry.theText = renameText renameEntry.origName = filename self.coords2buttons[(row,col)] = renameEntry renameEntry.grid(row=row,column=col,sticky='we') number=filename if number.startswith("word"): number=number[4:] if number and ("0"<=number[0]<="9" or (len(number)>=2 and number[0]=="_" and "0"<=number[1]<="9")): # the format of addMore method renameText.set(number) selectAllFunc = selectAllButNumber else: renameText.set(filename) 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)) 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 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 origName=self.coords2buttons[(row,col)]["text"] if self.has_variants and origName.find(" (")>-1: 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 o2 = origName if o2.startswith("word"): o2=o2[4:] if intor0(o2): newName=o2+"-"+newName if isDirectory(unicode2filename(self.currentDir+os.sep+origName)): try: os.rename(unicode2filename(self.currentDir+os.sep+origName),unicode2filename(self.currentDir+os.sep+newName)) 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))) 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 updated=False for ext in [dottxt, dotwav, dotmp3]: if fileExists_stat(unicode2filename(self.currentDir+os.sep+origName+"_"+lang+ext)): try: os.rename(unicode2filename(self.currentDir+os.sep+origName+"_"+lang+ext),unicode2filename(self.currentDir+os.sep+newName+"_"+lang+ext)) 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 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) 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 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))) 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)),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)))) else: self.addLabel(row,3+3*languageNo,"") self.need_reRecord_enabler = not (not tkSnack) else: # 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))) 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))) 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) 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 self.coords2buttons[(self.addMoreRow,4)].grid_forget() # old 'new folder' button def addMore(self,*args): self.del_addMore_button() for r in range(5): if self.maxPrefix<=99: prefix = "word%02d" % self.maxPrefix else: prefix = "word_%04d" % self.maxPrefix # if changing this, change startRename and selectAllButNumber also, AND the code that works out maxPrefix (and NB legacy collections). TODO use of _ may not suit variants if in same dir. self.addLabel(self.addMoreRow,0,utext=prefix) for lang in self.languagesToDraw: self.updateFile(unicode2filename(prefix+"_"+lang+dotwav),self.addMoreRow,self.languagesToDraw.index(lang),state=0) 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 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) if needToUpdatePlayButton: self.updateFile(filename,row,languageNo,2) 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()) if app.scanrow.get()=="2": # "stop" focusButton(self.coords2buttons[(row,3+3*languageNo)]) else: moved = 0 if app.scanrow.get()=="1": # move along 1st while languageNo+1<len(self.languagesToDraw): languageNo += 1 if (row,3+3*languageNo) in 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: return focusButton(self.coords2buttons[(r,3+3*languageNo)]) def doStop(self,*args): theISM.stopRecording() self.updateForStopOrChange() def updateForStopOrChange(self): if hasattr(self,"currentRecording"): filename,row,languageNo = self.currentRecording del self.currentRecording self.updateFile(filename,row,languageNo,2) def reconfigure_scrollbar(self): if not hasattr(app,"scanrow"): return # closing down if hasattr(self,"ourCanvas"): c = self.ourCanvas bbox = c.bbox(Tkinter.ALL) 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])) 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()) def setSync(self,syncFlag): self.syncFlag = syncFlag def newFolder(self,*args): count=0 while True: fname = "folder%d" % count # DON'T default its name to today's date etc, as this may make it less obvious what the GUI's renaming step is try: os.mkdir(unicode2filename(self.currentDir+os.sep+fname)) except: count += 1 ; continue break self.del_addMore_button() row=self.addMoreRow ; col=0 self.addLabel(row,col,fname) self.addMoreRow += 1 self.add_addMore_button() self.startRename(row,col,fname) def doRecordFrom(self,filename,row): 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) 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))) col += 1 def handleSkip(self,filename,i): self.protect_currentRecordFrom() self.doStop() theISM.setInputSource(PlayerInput(filename,True,theISM.currentInputSource.currentTime()+i)) if hasattr(app.todo,"undoRecordFrom"): del app.todo.undoRecordFrom self.restore_currentRecordFrom() def protect_currentRecordFrom(self): self.old_recordFrom_button, self.current_recordFrom_button = self.current_recordFrom_button, None def restore_currentRecordFrom(self): self.current_recordFrom_button, self.old_recordFrom_button = self.old_recordFrom_button, None def undoRecordFrom(self): 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: 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") self.coords2buttons[(row,0)] = button del self.current_recordFrom_button def do_openInExplorer(self,*args): l=os.listdir(self.currentDir) ; l.sort() openDirectory(self.currentDir,1) tkMessageBox.showinfo(app.master.title(),localise("Gradint has opened the current folder for you to work on. When you press OK, Gradint will re-scan the folder for new files.")) l2=os.listdir(self.currentDir) ; l2.sort() if l==l2: return # no change self.undraw() ; self.draw() def pocketPCrecord(self,*args): # (apparently get 11.025kHz 16-bit mono. Can set Notes to NOT switch to notes app when holding Recording button, in which case you then need the task manager to actually get into Notes.) if firstLanguage==secondLanguage: tup=("word","meaning","word") else: tup=(secondLanguage,firstLanguage,secondLanguage) if tkMessageBox.askyesno(app.master.title(),localise("Press and hold the PocketPC's Record button to record; release to stop. Record %s to 1st Note, %s to 2nd, %s to 3rd etc. Import all Notes now?") % tup): try: if import_recordings(self.currentDir): getLsDic(self.currentDir) # to rename them self.undraw() ; self.draw() else: app.todo.alert="No files found to import. Check this setting: import_recordings_from = "+repr(import_recordings_from) except CannotOverwriteExisting: app.todo.alert="Filenames conflict with those already in this folder. Clear the folder first, or choose another, then press the button again (your recordings have been left in the Notes app)." def do_recordFromFile(self,*args): if not tkSnack or tkSnack=="MicOnly": return tkMessageBox.showinfo(app.master.title(),localise("Sorry, cannot record from file on this computer because the tkSnack library (python-tksnack) is not installed")) msg1 = localise("You can record from an existing recording (i.e. copy parts from it) if you first put the existing recording into the samples folder and then press its Play button.")+"\n\n" if not self.has_recordFrom_buttons: openDirectory(self.currentDir,1) tkMessageBox.showinfo(app.master.title(),msg1+localise("Gradint has opened the current folder for you to do this. When you press OK, Gradint will re-scan the folder for new files.")) self.undraw() self.draw() msg1 = "" self.setSync(tkMessageBox.askyesno(app.master.title(),localise(msg1+"Do you want your next Play operation to be delayed until you also press a Record button?"))) def draw(self,dirToHighlight=1): # 1 is used as a "not exist" token if secondLanguage==firstLanguage: self.languagesToDraw = [secondLanguage, firstLanguage+"-meaning_"+firstLanguage] 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 not self.snack_initialized: if tkSnack and not tkSnack=="MicOnly": tkSnack.initializeSnack(app) 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.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) if winCEsound and not tkSnack: makeButton(self.frame,text=localise("PocketPC record..."),command=self.pocketPCrecord).grid(row=1,columnspan=2) else: r = Tkinter.Frame(self.frame) r.grid(row=1,sticky="e",columnspan=2) Tkinter.Label(r,text=localise("Action of spacebar during recording")).pack() r=Tkinter.Frame(r) ; r.pack() for button in [ Tkinter.Radiobutton(r, text=localise("move down"), variable=app.scanrow, value="0", indicatoron=1), Tkinter.Radiobutton(r, text=localise("move along"), variable=app.scanrow, value="1", indicatoron=1), Tkinter.Radiobutton(r, text=localise("stop"), variable=app.scanrow, value="2", indicatoron=1)]: bindUpDown(button,True) button.pack({"side":"left"}) self.grid,self.ourCanvas = setupScrollbar(self.frame,2) if hasattr(self,"oldCanvasBbox"): del self.oldCanvasBbox # unconditionally reconfigure scrollbar even if bounds are unchanged for languageNo in range(len(self.languagesToDraw)): self.grid.grid_columnconfigure(3+3*languageNo,weight=1) # prefer expanding the last col of each language rather than evenly curRow = 0 ; prefix2row = {} 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))) curRow += 1 l = os.listdir(self.currentDir) def cmpfunc(a,b): # sort alphabetically but ensure L2 comes before L1 for tab order if "_" in a and "_" in b and a[:a.rindex("_")]==b[:b.rindex("_")]: # the same apart from language? (TODO this won't work with variants) if languageof(a)==secondLanguage: a='1' elif languageof(a)==firstLanguage: a='2' if languageof(b)==secondLanguage: b='1' elif languageof(b)==firstLanguage: b='2' if a>b: return 1 elif b>a: return -1 else: return 0 l.sort(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)] 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))) # TODO if _disabled have an Enable button ? # if not have a Disable ?? # (NB though the above button will have a column span) if self.currentDir+os.sep+fname == dirToHighlight: 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) 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") 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) # TODO what about letting them record _explain_ files etc from the GUI (can be done but have to manually enter the _zh_explain bit), + toggling !poetry etc? afterLang = "" if languageOverride: realPrefix = prefix = fname[:fname.index("_")] if not isMeaning: afterLang = (fname+extsep)[fname.find("_",fname.find("_")+1):fname.rfind(extsep)] prefix += (" ("+afterLang[1:]+")") else: realPrefix = prefix = fname[:fname.rindex("_")] languageOverride = languageof(fname) wprefix = prefix for ww in ["word_","word","_"]: if wprefix.startswith(ww): wprefix=wprefix[len(ww):] ; break ii=0 while ii<len(wprefix) and "0"<=wprefix[ii]<="9": ii += 1 try: iprefix = int(wprefix[:ii]) except: iprefix = -1 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: 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)) 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)) 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): 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.has_recordFrom_buttons = True curRow += 1 self.addMoreRow = curRow ; self.maxPrefix = maxPrefix+1 self.add_addMore_button() if curRow<3 and not hadDirectories: self.addMore() # anyway if not dirToHighlight==None: # didn't find it, so focus the first one b = self.coords2buttons.get((0,2),None) if not b: b = self.coords2buttons.get((0,0),None) if b: b.focus() # don't focusButton in this case - no Mac flashing r1=Tkinter.Frame(self.frame) ; r1.grid(columnspan=2) ; r1=Tkinter.Frame(r1) ; r1.pack() r2=Tkinter.Frame(self.frame) ; r2.grid(columnspan=2) ; r2=Tkinter.Frame(r2) ; r2.pack() # note: addButton NOT self.addButton : addButton(r1,localise("Advanced"),self.do_openInExplorer,"left") if gotSox: self.AmplifyButton=addButton(r1,localise("Amplify"),self.amplify,"left") if not self.always_enable_synth: addButton(r1,localise("Mix with computer voice"),self.enable_synth,"left") 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") 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) # (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 doRecWords(): # called from GUI thread if hasattr(app,"LessonRow"): app.thin_down_for_lesson() # else recorderMode app.Label.pack_forget() ; app.CancelRow.pack_forget() global theRecorderControls try: theRecorderControls except: theRecorderControls=RecorderControls() theRecorderControls.draw() app.wordsExist = 1 # well not necessarily, but see comments re "Create word list" # Functions for recording on S60 phones: def s60_recordWord(): if secondLanguage==firstLanguage: l1Suffix, l1Display = firstLanguage+"-meaning_"+firstLanguage, "meaning" else: l1Suffix, l1Display = firstLanguage, firstLanguage while True: l2 = s60_recordFile(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) else: l1txt = appuifw.query(u""+firstLanguage+" text:","text") if l1txt: l1 = "newfile_"+firstLanguage+dottxt open(l1,"w").write(l1txt.encode("utf-8")) if not l1 and getYN("Discard the "+secondLanguage+" recording?"): 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) while True: prefix = appuifw.query(u"Filename:","text",prefix) if not prefix: # pressed cancel ?? if getYN("Discard this recording?"): os.remove(l1) ; os.remove(l2) ; return else: prefix = origPrefix ; continue if not inLs(prefix) 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("_"):]) if not getYN("Record another?"): break def s60_recordFile(language): fname = "newfile_"+language+dotwav while True: S=audio.Sound.open(os.getcwd()+os.sep+fname) def forgetS(): S.close() try: os.remove(fname) except: pass if not getYN("Press OK to record "+language+" word"): return forgetS() S.record() ret = getYN("Press OK to stop") ; S.stop() if not ret: forgetS() ; continue S.play() ret = getYN("Are you happy with this?") S.stop() ; S.close() if not ret: os.remove(fname) ; continue return fname