Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/TamTamJam.activity/Jam/JamMain.py
diff options
context:
space:
mode:
Diffstat (limited to 'TamTamJam.activity/Jam/JamMain.py')
-rw-r--r--TamTamJam.activity/Jam/JamMain.py1112
1 files changed, 1112 insertions, 0 deletions
diff --git a/TamTamJam.activity/Jam/JamMain.py b/TamTamJam.activity/Jam/JamMain.py
new file mode 100644
index 0000000..c339ae6
--- /dev/null
+++ b/TamTamJam.activity/Jam/JamMain.py
@@ -0,0 +1,1112 @@
+
+import pygtk
+pygtk.require( '2.0' )
+import gtk
+import pango
+
+from SubActivity import SubActivity
+
+import os, sys, shutil
+
+import Config
+from gettext import gettext as _
+import sugar.graphics.style as style
+
+from Jam.Desktop import Desktop
+import Jam.Picker as Picker
+import Jam.Block as Block
+from Jam.Toolbars import JamToolbar, DesktopToolbar
+
+
+from Util.CSoundNote import CSoundNote
+from Util.CSoundClient import new_csound_client
+import Util.InstrumentDB as InstrumentDB
+from Util import NoteDB
+
+from Fillin import Fillin
+from RythmGenerator import generator
+from Generation.GenerationConstants import GenerationConstants
+from Util.NoteDB import Note, Page
+
+from Util import ControlStream
+
+import xdrlib
+import time
+import gobject
+import Util.Network as Net
+from sugar.presence import presenceservice
+from sugar.graphics.xocolor import XoColor
+
+from math import sqrt
+
+class JamMain(SubActivity):
+
+ def __init__(self, activity, set_mode):
+ SubActivity.__init__(self, set_mode)
+
+ self.activity = activity
+
+ self.instrumentDB = InstrumentDB.getRef()
+ self.noteDB = NoteDB.NoteDB()
+
+ #-- initial settings ----------------------------------
+ self.tempo = Config.PLAYER_TEMPO
+ self.beatDuration = 60.0/self.tempo
+ self.ticksPerSecond = Config.TICKS_PER_BEAT*self.tempo/60.0
+ self.volume = 0.5
+
+ self.csnd = new_csound_client()
+ for i in range(0,9):
+ self.csnd.setTrackVolume( 100, i )
+ self.csnd.setMasterVolume( self.volume*100 ) # csnd expects a range 0-100 for now
+ self.csnd.setTempo( self.tempo )
+
+ self.paused = False
+
+ presenceService = presenceservice.get_instance()
+ self.xoOwner = presenceService.get_owner()
+
+ #-- Drawing -------------------------------------------
+ def darken( colormap, hex ):
+ hexToDec = { "0":0, "1":1, "2":2, "3":3, "4":4, "5":5, "6":6, "7":7, "8":8, "9":9, "A":10, "B":11, "C":12, "D":13, "E":14, "F":15, "a":10, "b":11, "c":12, "d":13, "e":14, "f":15 }
+ r = int( 0.7*(16*hexToDec[hex[1]] + hexToDec[hex[2]]) )
+ g = int( 0.7*(16*hexToDec[hex[3]] + hexToDec[hex[4]]) )
+ b = int( 0.7*(16*hexToDec[hex[5]] + hexToDec[hex[6]]) )
+ return colormap.alloc_color( r*256, g*256, b*256 )
+ def lighten( colormap, hex ):
+ hexToDec = { "0":0, "1":1, "2":2, "3":3, "4":4, "5":5, "6":6, "7":7, "8":8, "9":9, "A":10, "B":11, "C":12, "D":13, "E":14, "F":15, "a":10, "b":11, "c":12, "d":13, "e":14, "f":15 }
+ r = 255 - int( 0.7*(255-(16*hexToDec[hex[1]] + hexToDec[hex[2]])) )
+ g = 255 - int( 0.7*(255-(16*hexToDec[hex[3]] + hexToDec[hex[4]])) )
+ b = 255 - int( 0.7*(255-(16*hexToDec[hex[5]] + hexToDec[hex[6]])) )
+ return colormap.alloc_color( r*256, g*256, b*256 )
+
+ xoColorKey = self.xoOwner.props.color
+ if not xoColorKey:
+ xoColorKey = ( "#8D8D8D,#FFDDEA" )
+ xoColor = XoColor( xoColorKey )
+
+ win = gtk.gdk.get_default_root_window()
+ self.gc = gtk.gdk.GC( win )
+ colormap = gtk.gdk.colormap_get_system()
+ self.colors = { "bg": colormap.alloc_color( Config.PANEL_BCK_COLOR ),
+ "black": colormap.alloc_color( style.COLOR_BLACK.get_html() ),
+ #"Picker_Bg": colormap.alloc_color( "#404040" ),
+ #"Picker_Bg_Inactive": colormap.alloc_color( "#808080" ),
+ "Picker_Bg": colormap.alloc_color( style.COLOR_TOOLBAR_GREY.get_html() ),
+ "Picker_Bg_Inactive": colormap.alloc_color( style.COLOR_BUTTON_GREY.get_html() ),
+ "Picker_Fg": colormap.alloc_color( style.COLOR_WHITE.get_html() ),
+ "Border_Active": colormap.alloc_color( xoColor.get_stroke_color() ), #colormap.alloc_color( "#590000" ),
+ "Border_Inactive": colormap.alloc_color( "#8D8D8D" ),
+ "Border_Highlight": colormap.alloc_color( "#FFFFFF" ),
+ "Bg_Active": colormap.alloc_color( xoColor.get_fill_color() ), #colormap.alloc_color( "#FFDDEA" ),
+ "Bg_Inactive": colormap.alloc_color( "#DBDBDB" ),
+ "Preview_Note_Fill": colormap.alloc_color( Config.BG_COLOR ),
+ "Preview_Note_Border": colormap.alloc_color( Config.FG_COLOR ),
+ "Preview_Note_Selected": colormap.alloc_color( style.COLOR_WHITE.get_html() ),
+ "Note_Fill_Active": lighten( colormap, "#590000" ), # base "Border_Active"
+ "Note_Fill_Inactive": lighten( colormap, "#8D8D8D" ), # base "Border_Inactive"
+ "Beat_Line": colormap.alloc_color( "#959595" ) }
+ self.colors[ "Note_Border_Active"] = self.colors["Border_Active"]
+ self.colors[ "Note_Border_Inactive"] = self.colors["Border_Inactive"]
+
+
+ if True: # load block clipmask
+ pix = gtk.gdk.pixbuf_new_from_file(Config.IMAGE_ROOT+'jam-blockMask.png')
+ pixels = pix.get_pixels()
+ stride = pix.get_rowstride()
+ channels = pix.get_n_channels()
+ bitmap = ""
+ byte = 0
+ shift = 0
+ for j in range(pix.get_height()):
+ offset = stride*j
+ for i in range(pix.get_width()):
+ r = pixels[i*channels+offset]
+ if r != "\0": byte += 1 << shift
+ shift += 1
+ if shift > 7:
+ bitmap += "%c" % byte
+ byte = 0
+ shift = 0
+ if shift > 0:
+ bitmap += "%c" % byte
+ byte = 0
+ shift = 0
+ self.blockMask = gtk.gdk.bitmap_create_from_data( None, bitmap, pix.get_width(), pix.get_height() )
+
+ pix = gtk.gdk.pixbuf_new_from_file( Config.IMAGE_ROOT+"sampleBG.png" )
+ self.sampleBg = gtk.gdk.Pixmap( win, pix.get_width(), pix.get_height() )
+ self.sampleBg.draw_pixbuf( self.gc, pix, 0, 0, 0, 0, pix.get_width(), pix.get_height(), gtk.gdk.RGB_DITHER_NONE )
+ self.sampleBg.endOffset = pix.get_width()-5
+ self.sampleNoteHeight = 7
+ if True: # load sample note clipmask
+ pix = gtk.gdk.pixbuf_new_from_file(Config.IMAGE_ROOT+'sampleNoteMask.png')
+ pixels = pix.get_pixels()
+ stride = pix.get_rowstride()
+ channels = pix.get_n_channels()
+ bitmap = ""
+ byte = 0
+ shift = 0
+ for j in range(pix.get_height()):
+ offset = stride*j
+ for i in range(pix.get_width()):
+ r = pixels[i*channels+offset]
+ if r != "\0": byte += 1 << shift
+ shift += 1
+ if shift > 7:
+ bitmap += "%c" % byte
+ byte = 0
+ shift = 0
+ if shift > 0:
+ bitmap += "%c" % byte
+ byte = 0
+ shift = 0
+ self.sampleNoteMask = gtk.gdk.bitmap_create_from_data( None, bitmap, pix.get_width(), pix.get_height() )
+ self.sampleNoteMask.endOffset = pix.get_width()-3
+
+ self.loopPitchOffset = 4
+ self.loopTickOffset = 13
+ self.pitchPerPixel = float(Config.NUMBER_OF_POSSIBLE_PITCHES-1) / (Block.Loop.HEIGHT - 2*self.loopPitchOffset - self.sampleNoteHeight)
+ self.pixelsPerPitch = float(Block.Loop.HEIGHT - 2*self.loopPitchOffset - self.sampleNoteHeight)/(Config.MAXIMUM_PITCH - Config.MINIMUM_PITCH)
+ self.pixelsPerTick = Block.Loop.BEAT/float(Config.TICKS_PER_BEAT)
+ self.ticksPerPixel = 1.0/self.pixelsPerTick
+
+ #-- Instrument Images ---------------------------------
+ self.instrumentImage = {}
+ self.instrumentImageActive = {}
+ for inst in self.instrumentDB.getSet( "All" ):
+ self.prepareInstrumentImage( inst.id, inst.img )
+
+ #-- Loop Images ---------------------------------------
+ self.loopImage = {} # get filled in through updateLoopImage
+ self.loopImageActive = {} #
+
+ #-- Key Images ----------------------------------------
+ self.keyImage = {}
+ self.keyImageActive = {}
+ # use hardware key codes to work on any keyboard layout (hopefully)
+ self.valid_shortcuts = { 18:"9", 19:"0", 20:"-", 21:"=",
+ 32:"O", 33:"P", 34:"[", 35:"]",
+ 47:";", 48:"'", 51:"\\",
+ 60:".", 61:"/",
+ None:" " }
+ for key in self.valid_shortcuts.keys():
+ self.prepareKeyImage( key )
+
+ #-- Toolbars ------------------------------------------
+ self.activity.activity_toolbar.keep.show()
+
+ self.jamToolbar = JamToolbar( self )
+ self.activity.toolbox.add_toolbar( _("Jam"), self.jamToolbar )
+
+ self.desktopToolbar = DesktopToolbar( self )
+ self.activity.toolbox.add_toolbar( _("Desktop"), self.desktopToolbar )
+
+ #-- GUI -----------------------------------------------
+ if True: # GUI
+ self.modify_bg( gtk.STATE_NORMAL, self.colors["bg"] ) # window bg
+
+ self.GUI = {}
+ self.GUI["mainVBox"] = gtk.VBox()
+ self.add( self.GUI["mainVBox"] )
+
+ #-- Desktop -------------------------------------------
+ self.desktop = self.GUI["desktop"] = Desktop( self )
+ self.GUI["mainVBox"].pack_start( self.GUI["desktop"] )
+
+ #-- Bank ----------------------------------------------
+ separator = gtk.Label( " " )
+ separator.set_size_request( -1, style.TOOLBOX_SEPARATOR_HEIGHT )
+ self.GUI["mainVBox"].pack_start( separator, False )
+ self.GUI["notebook"] = gtk.Notebook()
+ self.GUI["notebook"].set_scrollable( True )
+ self.GUI["notebook"].modify_bg( gtk.STATE_NORMAL, self.colors["Picker_Bg"] ) # active tab
+ self.GUI["notebook"].modify_bg( gtk.STATE_ACTIVE, self.colors["Picker_Bg_Inactive"] ) # inactive tab
+ self.GUI["notebook"].props.tab_vborder = style.TOOLBOX_TAB_VBORDER
+ self.GUI["notebook"].props.tab_hborder = style.TOOLBOX_TAB_HBORDER
+ self.GUI["notebook"].set_size_request( -1, 160 )
+ self.GUI["notebook"].connect( "switch-page", self.setPicker )
+ self.GUI["mainVBox"].pack_start( self.GUI["notebook"], False, False )
+ self.pickers = {}
+ self.pickerScroll = {}
+ for type in [ Picker.Instrument, Picker.Drum, Picker.Loop ]:
+ self.pickers[type] = type( self )
+
+ def prepareLabel( name ):
+ label = gtk.Label( _(name) )
+ label.set_alignment( 0.0, 0.5 )
+ label.modify_fg( gtk.STATE_NORMAL, self.colors["Picker_Fg"] )
+ label.modify_fg( gtk.STATE_ACTIVE, self.colors["Picker_Fg"] )
+ return label
+
+ self.GUI["notebook"].append_page( self.pickers[Picker.Drum], prepareLabel("Drum Kits") )
+ self.GUI["notebook"].append_page( self.pickers[Picker.Loop], prepareLabel("Loops") )
+
+ sets = self.instrumentDB.getLabels()[:]
+ sets.sort()
+ for set in sets:
+ page = gtk.HBox()
+ page.set = set
+ self.GUI["notebook"].append_page( page, prepareLabel( set ) )
+
+ self.show_all()
+
+ self.GUI["notebook"].set_current_page( 0 )
+
+ #-- Keyboard ------------------------------------------
+ self.key_dict = {}
+ self.nextTrack = 1
+ self.keyboardListener = None
+ self.recordingNote = None
+
+ self.keyMap = {}
+
+ # default instrument
+ self._updateInstrument( Config.INSTRUMENTS["kalimba"].instrumentId, 0.5 )
+ self.instrumentStack = []
+
+ #-- Drums ---------------------------------------------
+ self.drumLoopId = None
+ # use dummy values for now
+ self.drumFillin = Fillin( 2, 100, Config.INSTRUMENTS["drum1kit"].instrumentId, 0, 1 )
+
+ #-- Desktops ------------------------------------------
+ self.curDesktop = None
+ # copy preset desktops
+ path = Config.TAM_TAM_ROOT+"/Resources/Desktops/"
+ filelist = os.listdir( path )
+ for file in filelist:
+ shutil.copyfile( path+file, Config.SCRATCH_DIR+file )
+
+ #-- Network -------------------------------------------
+ self.network = Net.Network()
+ self.network.addWatcher( self.networkStatusWatcher )
+ self.network.connectMessage( Net.HT_SYNC_REPLY, self.processHT_SYNC_REPLY )
+ self.network.connectMessage( Net.HT_TEMPO_UPDATE, self.processHT_TEMPO_UPDATE )
+ self.network.connectMessage( Net.PR_SYNC_QUERY, self.processPR_SYNC_QUERY )
+ self.network.connectMessage( Net.PR_TEMPO_QUERY, self.processPR_TEMPO_QUERY )
+ self.network.connectMessage( Net.PR_REQUEST_TEMPO_CHANGE, self.processPR_REQUEST_TEMPO_CHANGE )
+
+ # sync
+ self.syncQueryStart = {}
+ self.syncTimeout = None
+ self.heartbeatLoop = self.csnd.loopCreate()
+ self.csnd.loopSetNumTicks( Config.TICKS_PER_BEAT, self.heartbeatLoop )
+ self.heartbeatStart = time.time()
+ self.csnd.loopStart( self.heartbeatLoop )
+
+ # data packing classes
+ self.packer = xdrlib.Packer()
+ self.unpacker = xdrlib.Unpacker("")
+
+ # handle forced networking
+ if self.network.isHost():
+ self.updateSync()
+ self.syncTimeout = gobject.timeout_add( 1000, self.updateSync )
+ elif self.network.isPeer():
+ self.sendTempoQuery()
+ self.syncTimeout = gobject.timeout_add( 1000, self.updateSync )
+
+ #-- Final Set Up --------------------------------------
+ self.setVolume( self.volume )
+ self.setTempo( self.tempo )
+ self.activity.toolbox.set_current_toolbar(1) # JamToolbar
+ self.setDesktop( 0, True )
+
+
+ #==========================================================
+ # SubActivity Handlers
+
+ def onActivate( self, arg ):
+ SubActivity.onActivate( self, arg )
+
+ def onDeactivate( self ):
+ SubActivity.onDeactivate( self )
+
+ def onDestroy( self ):
+ SubActivity.onDestroy( self )
+
+ # clear up scratch folder
+ path = Config.SCRATCH_DIR
+ filelist = os.listdir( path )
+ for file in filelist:
+ os.remove( path+file )
+
+
+ #==========================================================
+ # Playback
+
+ def onKeyPress( self, widget, event ):
+ key = event.hardware_keycode
+
+ if key in self.keyMap.keys():
+ activate = True
+ for block in self.keyMap[key]:
+ if block.isActive():
+ activate = False
+ break
+ if activate:
+ for block in self.keyMap[key]:
+ if not block.isActive():
+ if block.type == Block.Drum: self.desktop.activateDrum( block )
+ elif block.type == Block.Loop: self.desktop.activateLoop( block )
+ else:
+ for block in self.keyMap[key]:
+ if block.isActive():
+ if block.type == Block.Drum: self.desktop.deactivateDrum( block )
+ elif block.type == Block.Loop: self.desktop.deactivateLoop( block )
+ return
+
+ if self.key_dict.has_key( key ): # repeated press
+ return
+
+ if Config.KEY_MAP_PIANO.has_key( key ):
+ pitch = Config.KEY_MAP_PIANO[key]
+ inst = Config.INSTRUMENTSID[self.instrument["id"]]
+
+ if inst.kit: # drum kit
+ if pitch in GenerationConstants.DRUMPITCH:
+ pitch = GenerationConstants.DRUMPITCH[pitch]
+ csnote = self._playNote( key,
+ 36,
+ self.instrument["amplitude"]*0.5, # trackVol*noteVol
+ self.instrument["pan"],
+ 100,
+ inst.kit[pitch].instrumentId,
+ self.instrument["reverb"] )
+ else:
+ if event.state == gtk.gdk.MOD1_MASK:
+ pitch += 5
+
+ if inst.csoundInstrumentId == Config.INST_PERC: #Percussions resonance
+ duration = 60
+ else:
+ duration = -1
+
+ csnote = self._playNote( key,
+ pitch,
+ self.instrument["amplitude"]*0.5, # trackVol*noteVol
+ self.instrument["pan"],
+ duration,
+ self.instrument["id"],
+ self.instrument["reverb"] )
+
+ if self.keyboardListener:
+ self.keyboardListener.recordNote( csnote.pitch )
+ self.recordingNote = True
+
+ def onKeyRelease( self, widget, event ):
+ key = event.hardware_keycode
+
+ if self.key_dict.has_key( key ):
+ self._stopNote( key )
+
+ if self.recordingNote:
+ if self.keyboardListener:
+ self.keyboardListener.finishNote()
+ self.recordingNote = False
+
+ def _playNote( self, key, pitch, amplitude, pan, duration, instrumentId, reverb ):
+ self.key_dict[key] = CSoundNote( 0, # onset
+ pitch,
+ amplitude,
+ pan,
+ duration,
+ self.nextTrack,
+ instrumentId,
+ reverbSend = reverb,
+ tied = True,
+ mode = 'mini' )
+ self.nextTrack += 1
+ if self.nextTrack > 8:
+ self.nextTrack = 1
+ self.csnd.play(self.key_dict[key], 0.3)
+
+ return self.key_dict[key]
+
+ def _stopNote( self, key ):
+ csnote = self.key_dict[key]
+ if Config.INSTRUMENTSID[ csnote.instrumentId ].csoundInstrumentId == Config.INST_TIED:
+ csnote.duration = .5
+ csnote.decay = 0.7
+ csnote.tied = False
+ self.csnd.play(csnote, 0.3)
+ del self.key_dict[key]
+
+ def _updateInstrument( self, id, volume, pan = 0, reverb = 0 ):
+ self.instrument = { "id": id,
+ "amplitude": volume,
+ "pan": pan,
+ "reverb": reverb }
+
+ def pushInstrument( self, instrument ):
+ self.instrumentStack.append( self.instrument )
+ self.instrument = instrument
+
+ def popInstrument( self ):
+ self.instrument = self.instrumentStack.pop()
+
+ def _playDrum( self, id, pageId, volume, reverb, beats, regularity, loopId = None ):
+
+ if loopId == None: # create new loop
+ startTick = 0
+ firstTime = True
+ else: # update loop
+ startTick = self.csnd.loopGetTick( loopId )
+ self.csnd.loopDestroy( loopId )
+ firstTime = False
+
+ loopId = self.csnd.loopCreate()
+
+ # TODO update track volume
+
+ noteOnsets = []
+ notePitchs = []
+ for n in self.noteDB.getNotesByTrack( pageId, 0 ):
+ n.pushState()
+ noteOnsets.append( n.cs.onset )
+ notePitchs.append( n.cs.pitch )
+ n.cs.amplitude = volume * n.cs.amplitude # TODO remove me once track volume is working
+ n.cs.reverbSend = reverb
+ self.csnd.loopPlay( n, 1, loopId = loopId ) #add as active
+ n.popState()
+
+ ticks = self.noteDB.getPage( pageId ).ticks
+
+ self.csnd.loopSetNumTicks( ticks, loopId )
+
+ self.drumFillin.setLoopId( loopId )
+ self.drumFillin.setProperties( self.tempo, Config.INSTRUMENTSID[id].name, volume, beats, reverb )
+ self.drumFillin.unavailable( noteOnsets, notePitchs )
+
+ self.drumFillin.play()
+
+ # sync to heartbeat
+ if False: # firstTime: # always force first note to play rather than snaping to nearest beat.. good idea?
+ startTick = ticks - Config.TICKS_PER_BEAT + self.csnd.loopGetTick( self.heartbeatLoop )
+ else:
+ while startTick > ticks: # align with last beat
+ startTick -= Config.TICKS_PER_BEAT
+ beatTick = int(startTick) % Config.TICKS_PER_BEAT
+ heartTick = self.csnd.loopGetTick( self.heartbeatLoop )
+ if beatTick > heartTick:
+ if beatTick - heartTick < heartTick + Config.TICKS_PER_BEAT - beatTick:
+ startTick = (int(startTick)//Config.TICKS_PER_BEAT)*Config.TICKS_PER_BEAT + self.csnd.loopGetTick( self.heartbeatLoop )
+ else:
+ startTick = (1 + int(startTick)//Config.TICKS_PER_BEAT)*Config.TICKS_PER_BEAT + self.csnd.loopGetTick( self.heartbeatLoop )
+ else:
+ if heartTick - beatTick < beatTick + Config.TICKS_PER_BEAT - heartTick:
+ startTick = (int(startTick)//Config.TICKS_PER_BEAT)*Config.TICKS_PER_BEAT + self.csnd.loopGetTick( self.heartbeatLoop )
+ else:
+ startTick = (-1 + int(startTick)//Config.TICKS_PER_BEAT)*Config.TICKS_PER_BEAT + self.csnd.loopGetTick( self.heartbeatLoop )
+
+ if startTick >= ticks:
+ startTick -= ticks
+ elif startTick < 0:
+ startTick += ticks
+
+ self.csnd.loopSetTick( startTick, loopId )
+
+ if not self.paused:
+ self.csnd.loopStart( loopId )
+
+ return loopId
+
+ def _stopDrum( self, loopId ):
+ self.drumFillin.stop()
+ self.csnd.loopDestroy( loopId )
+
+ def _playLoop( self, id, volume, reverb, tune, loopId = None, force = False ):
+ if loopId == None: # create new loop
+ startTick = 0
+ firstTime = True
+ else: # update loop
+ startTick = self.csnd.loopGetTick( loopId )
+ self.csnd.loopDestroy( loopId )
+ firstTime = False
+
+ loopId = self.csnd.loopCreate()
+
+ # TODO update track volume
+
+ inst = Config.INSTRUMENTSID[id]
+
+ offset = 0
+ for page in tune:
+ for n in self.noteDB.getNotesByTrack( page, 0 ):
+ n.pushState()
+ n.cs.instrumentId = id
+ n.cs.amplitude = volume * n.cs.amplitude # TODO remove me once track volume is working
+ n.cs.reverbSend = reverb
+ if inst.kit: # drum kit
+ if n.cs.pitch in GenerationConstants.DRUMPITCH:
+ n.cs.pitch = GenerationConstants.DRUMPITCH[n.cs.pitch]
+ n.cs.onset += offset
+ self.csnd.loopPlay( n, 1, loopId = loopId )
+ n.popState()
+ offset += self.noteDB.getPage(page).ticks
+
+
+ self.csnd.loopSetNumTicks( offset, loopId )
+
+ # sync to heartbeat
+ if False: # firstTime: # always force first note to play rather than snaping to nearest beat.. good idea?
+ startTick = offset - Config.TICKS_PER_BEAT + self.csnd.loopGetTick( self.heartbeatLoop )
+ else:
+ while startTick > offset: # align with last beat
+ startTick -= Config.TICKS_PER_BEAT
+ beatTick = int(startTick) % Config.TICKS_PER_BEAT
+ heartTick = self.csnd.loopGetTick( self.heartbeatLoop )
+ if beatTick > heartTick:
+ if beatTick - heartTick < heartTick + Config.TICKS_PER_BEAT - beatTick:
+ startTick = (int(startTick)//Config.TICKS_PER_BEAT)*Config.TICKS_PER_BEAT + self.csnd.loopGetTick( self.heartbeatLoop )
+ else:
+ startTick = (1 + int(startTick)//Config.TICKS_PER_BEAT)*Config.TICKS_PER_BEAT + self.csnd.loopGetTick( self.heartbeatLoop )
+ else:
+ if heartTick - beatTick < beatTick + Config.TICKS_PER_BEAT - heartTick:
+ startTick = (int(startTick)//Config.TICKS_PER_BEAT)*Config.TICKS_PER_BEAT + self.csnd.loopGetTick( self.heartbeatLoop )
+ else:
+ startTick = (-1 + int(startTick)//Config.TICKS_PER_BEAT)*Config.TICKS_PER_BEAT + self.csnd.loopGetTick( self.heartbeatLoop )
+
+ if startTick >= offset:
+ startTick -= offset
+ elif startTick < 0:
+ startTick += offset
+
+
+ self.csnd.loopSetTick( startTick, loopId )
+
+ if not self.paused or force:
+ self.csnd.loopStart( loopId )
+
+ return loopId
+
+ def _stopLoop( self, loopId ):
+ self.csnd.loopDestroy( loopId )
+
+ def setPaused( self, paused ):
+ if self.paused == paused:
+ return
+
+ loops = self.desktop.getLoopIds()
+
+ if self.paused: # unpause
+ self.paused = False
+ for loop in loops:
+ self.csnd.loopStart( loop )
+ else: # pause
+ self.paused = True
+ for loop in loops:
+ self.csnd.loopPause( loop )
+
+ #==========================================================
+ # Generate
+
+ def _generateDrumLoop( self, instrumentId, beats, regularity, reverb, pageId = -1 ):
+ def flatten(ll):
+ rval = []
+ for l in ll:
+ rval += l
+ return rval
+
+ notes = flatten( generator( Config.INSTRUMENTSID[instrumentId].name, beats, 0.8, regularity, reverb) )
+
+ if pageId == -1:
+ page = Page( beats )
+ pageId = self.noteDB.addPage( -1, page )
+ else:
+ self.noteDB.deleteNotesByTrack( [ pageId ], [ 0 ] )
+
+ if len(notes):
+ self.noteDB.addNotes( [ pageId, 0, len(notes) ] + notes + [-1] )
+
+ return pageId
+
+ def _generateTrack( self, instrumentId, page, track, parameters, algorithm ):
+ dict = { track: { page: self.noteDB.getCSNotesByTrack( page, track ) } }
+ instruments = { page: [ Config.INSTRUMENTSID[instrumentId].name for i in range(Config.NUMBER_OF_TRACKS) ] }
+ beatsOfPages = { page: self.noteDB.getPage(page).beats }
+
+ algorithm( parameters,
+ [ 0.5 for i in range(Config.NUMBER_OF_TRACKS) ],
+ instruments,
+ self.tempo,
+ beatsOfPages,
+ [ track ],
+ [ page ],
+ dict,
+ 4)
+
+ # filter & fix input ...WTF!?
+ for track in dict:
+ for page in dict[track]:
+ for note in dict[track][page]:
+ intdur = int(note.duration)
+ note.duration = intdur
+ note.pageId = page
+ note.trackId = track
+
+ # prepare the new notes
+ newnotes = []
+ for tid in dict:
+ for pid in dict[tid]:
+ newnotes += dict[tid][pid]
+
+ # delete the notes and add the new
+ self.noteDB.deleteNotesByTrack( [ page ], [ track ] )
+
+ self.noteDB.addNotes(
+ [ page, track, len(dict[track][page]) ]
+ + dict[track][page]
+ + [ -1 ] )
+
+
+ #==========================================================
+ # Get/Set
+
+ def getVolume( self ):
+ return self.volume
+
+ def setVolume( self, volume ):
+ self.jamToolbar.volumeSlider.set_value( volume )
+
+ def _setVolume( self, volume ):
+ self.volume = volume
+ self.csnd.setMasterVolume( self.volume*100 ) # csnd expects a range 0-100 for now
+
+ def getTempo( self ):
+ return self.tempo
+
+ def setTempo( self, tempo ):
+ self.jamToolbar.tempoSlider.set_value( tempo )
+
+ def _setTempo( self, tempo ):
+ if self.network.isHost() or self.network.isOffline():
+ t = time.time()
+ percent = self.heartbeatElapsed() / self.beatDuration
+
+ self.tempo = tempo
+ self.beatDuration = 60.0/self.tempo
+ self.ticksPerSecond = Config.TICKS_PER_BEAT*self.tempo/60.0
+ self.csnd.setTempo( self.tempo )
+
+ if self.network.isHost() or self.network.isOffline():
+ self.heatbeatStart = t - percent*self.beatDuration
+ self.updateSync()
+ self.sendTempoUpdate()
+
+ def getInstrument( self ):
+ return self.instrument
+
+ def getDesktop( self ):
+ return self.desktop
+
+ def _clearDesktop( self, save = True ):
+ if self.curDesktop == None:
+ return
+
+ if save:
+ self._saveDesktop()
+
+ self.desktop._clearDesktop()
+
+ self.curDesktop = None
+
+ def setDesktop( self, desktop, force = False ):
+ radiobtn = self.desktopToolbar.getDesktopButton( desktop )
+ if force and radiobtn.get_active():
+ self._setDesktop( desktop )
+ else:
+ radiobtn.set_active( True )
+
+ def _setDesktop( self, desktop ):
+ self._clearDesktop()
+
+ self.curDesktop = desktop
+
+ TTTable = ControlStream.TamTamTable( self.noteDB, jam = self )
+
+ filename = self.getDesktopScratchFile( self.curDesktop )
+ try:
+ stream = open( filename, "r" )
+ TTTable.parseFile( stream )
+ stream.close()
+ except IOError, (errno, strerror):
+ if Config.DEBUG > 3: print "IOError:: _setDesktop:", errno, strerror
+
+ def getInstrumentImage( self, id, active = False ):
+ if active: return self.instrumentImageActive[id]
+ else: return self.instrumentImage[id]
+
+ def getKeyImage( self, key, active = False ):
+ if active: return self.keyImageActive[key]
+ else: return self.keyImage[key]
+
+ def getLoopImage( self, id, active = False ):
+ if active: return self.loopImageActive[id]
+ else: return self.loopImage[id]
+
+ def setPicker( self, widget, pagePointer, page_num ):
+ page = self.GUI["notebook"].get_nth_page( page_num )
+ if page == self.pickers[Picker.Drum]:
+ pass
+ elif page == self.pickers[Picker.Loop]:
+ pass
+ else:
+ self.pickers[Picker.Instrument].setFilter( ( page.set ) )
+ parent = self.pickers[Picker.Instrument].get_parent()
+ if parent != page:
+ if parent != None:
+ parent.remove( self.pickers[Picker.Instrument] )
+ page.add( self.pickers[Picker.Instrument] )
+
+ def setKeyboardListener( self, listener ):
+ self.keyboardListener = listener
+
+ def mapKey( self, key, block, oldKey = None ):
+ if oldKey != None and block in self.keyMap[oldKey]:
+ self.keyMap[oldKey].remove( block )
+
+ if key == None:
+ return
+
+ if key not in self.keyMap.keys():
+ self.keyMap[key] = []
+
+ if block not in self.keyMap[key]:
+ self.keyMap[key].append( block )
+
+ #==========================================================
+ # Pixmaps
+
+ def prepareInstrumentImage( self, id, img_path ):
+ try:
+ win = gtk.gdk.get_default_root_window()
+ pix = gtk.gdk.pixbuf_new_from_file( img_path )
+ x = (Block.Block.WIDTH-pix.get_width())//2
+ y = (Block.Block.HEIGHT-pix.get_height())//2
+ img = gtk.gdk.Pixmap( win, Block.Block.WIDTH, Block.Block.HEIGHT )
+ self.gc.foreground = self.colors["Bg_Inactive"]
+ img.draw_rectangle( self.gc, True, 0, 0, Block.Block.WIDTH, Block.Block.HEIGHT )
+ img.draw_pixbuf( self.gc, pix, 0, 0, x, y, pix.get_width(), pix.get_height(), gtk.gdk.RGB_DITHER_NONE )
+ self.instrumentImage[id] = img
+ img = gtk.gdk.Pixmap( win, Block.Block.WIDTH, Block.Block.HEIGHT )
+ self.gc.foreground = self.colors["Bg_Active"]
+ img.draw_rectangle( self.gc, True, 0, 0, Block.Block.WIDTH, Block.Block.HEIGHT )
+ img.draw_pixbuf( self.gc, pix, 0, 0, x, y, pix.get_width(), pix.get_height(), gtk.gdk.RGB_DITHER_NONE )
+ self.instrumentImageActive[id] = img
+ except:
+ if Config.DEBUG >= 5: print "JamMain:: file does not exist: " + img_path
+ img = gtk.gdk.Pixmap( win, Block.Block.WIDTH, Block.Block.HEIGHT )
+ self.gc.foreground = self.colors["Bg_Inactive"]
+ img.draw_rectangle( self.gc, True, 0, 0, Block.Block.WIDTH, Block.Block.HEIGHT )
+ self.instrumentImage[id] = img
+ img = gtk.gdk.Pixmap( win, Block.Block.WIDTH, Block.Block.HEIGHT )
+ self.gc.foreground = self.colors["Bg_Active"]
+ img.draw_rectangle( self.gc, True, 0, 0, Block.Block.WIDTH, Block.Block.HEIGHT )
+ self.instrumentImageActive[id] = img
+
+ def _drawNotes( self, pixmap, beats, notes, active ):
+ self.gc.set_clip_mask( self.sampleNoteMask )
+ for note in notes: # draw N notes
+ x = self.ticksToPixels( note.cs.onset )
+ endX = self.ticksToPixels( note.cs.onset + note.cs.duration ) - 3 # include end cap offset
+ width = endX - x
+ if width < 5:
+ width = 5
+ endX = x + width
+ y = self.pitchToPixels( note.cs.pitch )
+ # draw fill
+ if active: self.gc.foreground = self.colors["Note_Fill_Active"]
+ else: self.gc.foreground = self.colors["Note_Fill_Inactive"]
+ self.gc.set_clip_origin( x, y-self.sampleNoteHeight )
+ pixmap.draw_rectangle( self.gc, True, x+1, y+1, width+1, self.sampleNoteHeight-2 )
+ # draw border
+ if active: self.gc.foreground = self.colors["Note_Border_Active"]
+ else: self.gc.foreground = self.colors["Note_Border_Inactive"]
+ self.gc.set_clip_origin( x, y )
+ pixmap.draw_rectangle( self.gc, True, x, y, width, self.sampleNoteHeight )
+ self.gc.set_clip_origin( endX-self.sampleNoteMask.endOffset, y )
+ pixmap.draw_rectangle( self.gc, True, endX, y, 3, self.sampleNoteHeight )
+
+ def prepareKeyImage( self, key ):
+ win = gtk.gdk.get_default_root_window()
+ pangolayout = self.create_pango_layout( _(self.valid_shortcuts[key]) )
+ fontDesc = pango.FontDescription( "bold" )
+ pangolayout.set_font_description( fontDesc )
+ extents = pangolayout.get_pixel_extents()
+ x = ( Block.Block.KEYSIZE - extents[1][2] ) // 2
+ y = ( Block.Block.KEYSIZE - extents[1][3] ) // 2
+
+ pixmap = gtk.gdk.Pixmap( win, Block.Block.KEYSIZE, Block.Block.KEYSIZE )
+ self.gc.foreground = self.colors["Border_Inactive"]
+ pixmap.draw_rectangle( self.gc, True, 0, 0, Block.Block.KEYSIZE, Block.Block.KEYSIZE )
+ self.gc.foreground = self.colors["Bg_Inactive"]
+ pixmap.draw_layout( self.gc, x, y, pangolayout )
+ self.keyImage[key] = pixmap
+
+ pixmap = gtk.gdk.Pixmap( win, Block.Block.KEYSIZE, Block.Block.KEYSIZE )
+ self.gc.foreground = self.colors["Border_Active"]
+ pixmap.draw_rectangle( self.gc, True, 0, 0, Block.Block.KEYSIZE, Block.Block.KEYSIZE )
+ self.gc.foreground = self.colors["Bg_Active"]
+ pixmap.draw_layout( self.gc, x, y, pangolayout )
+ self.keyImageActive[key] = pixmap
+
+ def updateLoopImage( self, id ):
+ page = self.noteDB.getPage( id )
+
+ win = gtk.gdk.get_default_root_window()
+ width = Block.Loop.WIDTH[page.beats]
+ height = Block.Loop.HEIGHT
+
+ self.gc.set_clip_rectangle( gtk.gdk.Rectangle( 0, 0, width, height ) )
+
+ pixmap = gtk.gdk.Pixmap( win, width, height )
+ self.gc.foreground = self.colors["Bg_Inactive"]
+ pixmap.draw_rectangle( self.gc, True, 0, 0, width, height )
+ self._drawNotes( pixmap, page.beats, self.noteDB.getNotesByTrack( id, 0 ), False )
+ self.loopImage[id] = pixmap
+
+ self.gc.set_clip_rectangle( gtk.gdk.Rectangle( 0, 0, width, height ) )
+
+ pixmap = gtk.gdk.Pixmap( win, width, height )
+ self.gc.foreground = self.colors["Bg_Active"]
+ pixmap.draw_rectangle( self.gc, True, 0, 0, width, height )
+ self._drawNotes( pixmap, page.beats, self.noteDB.getNotesByTrack( id, 0 ), True )
+ self.loopImageActive[id] = pixmap
+
+ def ticksToPixels( self, ticks ):
+ return self.loopTickOffset + int(round( ticks * self.pixelsPerTick ))
+ def pitchToPixels( self, pitch ):
+ return self.loopPitchOffset + int(round( ( Config.MAXIMUM_PITCH - pitch ) * self.pixelsPerPitch ))
+
+ #==========================================================
+ # Load/Save
+
+ def _saveDesktop( self ):
+ if self.curDesktop == None:
+ return
+
+ filename = self.getDesktopScratchFile( self.curDesktop )
+ if os.path.isfile( filename ):
+ os.remove( filename )
+
+ try:
+ scratch = open( filename, "w" )
+ stream = ControlStream.TamTamOStream(scratch)
+
+ self.noteDB.dumpToStream( stream, True )
+ self.desktop.dumpToStream( stream )
+
+ scratch.close()
+ except IOError, (errno, strerror):
+ if Config.DEBUG > 3: print "IOError:: _saveDesktop:", errno, strerror
+
+ def getDesktopScratchFile( self, i ):
+ return Config.SCRATCH_DIR+"desktop%d" % i
+
+ def handleJournalLoad( self, filepath ):
+
+ self._clearDesktop( False )
+
+ TTTable = ControlStream.TamTamTable( self.noteDB, jam = self )
+
+ try:
+ stream = open( filepath, "r" )
+ TTTable.parseFile( stream )
+ stream.close()
+
+ self.setVolume( TTTable.masterVolume )
+ self.setTempo( TTTable.tempo )
+
+ except IOError, (errno, strerror):
+ if Config.DEBUG > 3: print "IOError:: handleJournalLoad:", errno, strerror
+
+ def handleJournalSave( self, filepath ):
+
+ self._saveDesktop()
+
+ try:
+ streamF = open( filepath, "w" )
+ stream = ControlStream.TamTamOStream( streamF )
+
+ for i in range(10):
+ desktop_file = self.getDesktopScratchFile( i )
+ stream.desktop_store( desktop_file, i )
+
+ stream.desktop_set( self.curDesktop )
+
+ stream.master_vol( self.volume )
+ stream.tempo( self.tempo )
+
+ streamF.close()
+
+ except IOError, (errno, strerror):
+ if Config.DEBUG > 3: print "IOError:: handleJournalSave:", errno, strerror
+
+ #==========================================================
+ # Network
+
+ #-- Activity ----------------------------------------------
+
+ def shared( self, activity ):
+ if Config.DEBUG: print "miniTamTam:: successfully shared, start host mode"
+ self.activity._shared_activity.connect( "buddy-joined", self.buddy_joined )
+ self.activity._shared_activity.connect( "buddy-left", self.buddy_left )
+ self.network.setMode( Net.MD_HOST )
+ self.updateSync()
+ self.syncTimeout = gobject.timeout_add( 1000, self.updateSync )
+
+ def joined( self, activity ):
+ if Config.DEBUG:
+ print "miniTamTam:: joined activity!!"
+ for buddy in self.activity._shared_activity.get_joined_buddies():
+ print buddy.props.ip4_address
+
+ def buddy_joined( self, activity, buddy ):
+ if Config.DEBUG:
+ print "buddy joined " + str(buddy)
+ try:
+ print buddy.props.ip4_address
+ except:
+ print "bad ip4_address"
+ if self.network.isHost():
+ if buddy == self.xoOwner:
+ return
+ if buddy.props.ip4_address:
+ self.network.introducePeer( buddy.props.ip4_address )
+ else:
+ print "miniTamTam:: new buddy does not have an ip4_address!!"
+
+ def buddy_left( self, activity, buddy):
+ if Config.DEBUG: print "buddy left"
+
+ #def joined( self, activity ):
+ # if Config.DEBUG: print "miniTamTam:: successfully joined, wait for host"
+ # self.net.waitForHost()
+
+ #-- Senders -----------------------------------------------
+
+ def sendSyncQuery( self ):
+ self.packer.pack_float(random.random())
+ hash = self.packer.get_buffer()
+ self.packer.reset()
+ self.syncQueryStart[hash] = time.time()
+ self.network.send( Net.PR_SYNC_QUERY, hash)
+
+ def sendTempoUpdate( self ):
+ self.packer.pack_int(self.tempo)
+ self.network.sendAll( Net.HT_TEMPO_UPDATE, self.packer.get_buffer() )
+ self.packer.reset()
+
+ def sendTempoQuery( self ):
+ self.network.send( Net.PR_TEMPO_QUERY )
+
+ def requestTempoChange( self, val ):
+ self.packer.pack_int(val)
+ self.network.send( Net.PR_REQUEST_TEMPO_CHANGE, self.packer.get_buffer() )
+ self.packer.reset()
+
+ #-- Handlers ----------------------------------------------
+
+ def networkStatusWatcher( self, mode ):
+ if mode == Net.MD_OFFLINE:
+ if self.syncTimeout:
+ gobject.source_remove( self.syncTimeout )
+ self.syncTimeout = None
+ if mode == Net.MD_PEER:
+ self.updateSync()
+ if not self.syncTimeout:
+ self.syncTimeout = gobject.timeout_add( 1000, self.updateSync )
+ self.sendTempoQuery()
+
+ def processHT_SYNC_REPLY( self, sock, message, data ):
+ t = time.time()
+ hash = data[0:4]
+ latency = t - self.syncQueryStart[hash]
+ self.unpacker.reset(data[4:8])
+ nextBeat = self.unpacker.unpack_float()
+ #print "mini:: got sync: next beat in %f, latency %d" % (nextBeat, latency*1000)
+ self.heartbeatStart = t + nextBeat - self.beatDuration - latency/2
+ self.correctSync()
+ self.syncQueryStart.pop(hash)
+
+ def processHT_TEMPO_UPDATE( self, sock, message, data ):
+ self.unpacker.reset(data)
+ val = self.unpacker.unpack_int()
+ if self.tempoSliderActive:
+ self.delayedTempo = val
+ return
+ self.tempoAdjustment.handler_block( self.tempoAdjustmentHandler )
+ self.tempoAdjustment.set_value( val )
+ self._updateTempo( val )
+ self.tempoAdjustment.handler_unblock( self.tempoAdjustmentHandler )
+ self.sendSyncQuery()
+
+ def processPR_SYNC_QUERY( self, sock, message, data ):
+ self.packer.pack_float(self.nextHeartbeat())
+ self.network.send( Net.HT_SYNC_REPLY, data + self.packer.get_buffer(), sock )
+ self.packer.reset()
+
+ def processPR_TEMPO_QUERY( self, sock, message, data ):
+ self.packer.pack_int(self.tempo)
+ self.network.send( Net.HT_TEMPO_UPDATE, self.packer.get_buffer(), to = sock )
+ self.packer.reset()
+
+ def processPR_REQUEST_TEMPO_CHANGE( self, sock, message, data ):
+ if self.tempoSliderActive:
+ return
+ self.unpacker.reset(data)
+ val = self.unpacker.unpack_int()
+ self.tempoAdjustment.set_value( val )
+
+ #==========================================================
+ # Sync
+
+ def nextHeartbeat( self ):
+ delta = time.time() - self.heartbeatStart
+ return self.beatDuration - (delta % self.beatDuration)
+
+ def nextHeartbeatInTicks( self ):
+ delta = time.time() - self.heartbeatStart
+ next = self.beatDuration - (delta % self.beatDuration)
+ return self.ticksPerSecond*next
+
+ def heartbeatElapsed( self ):
+ delta = time.time() - self.heartbeatStart
+ return delta % self.beatDuration
+
+ def heartbeatElapsedTicks( self ):
+ delta = time.time() - self.heartbeatStart
+ return self.ticksPerSecond*(delta % self.beatDuration)
+
+ def updateSync( self ):
+ if self.network.isOffline():
+ return False
+ elif self.network.isWaiting():
+ return True
+ elif self.network.isHost():
+ self.correctSync()
+ else:
+ self.sendSyncQuery()
+ return True
+
+ def correctSync( self ):
+ curTick = self.csnd.loopGetTick( self.heartbeatLoop )
+ curTicksIn = curTick % Config.TICKS_PER_BEAT
+ ticksIn = self.heartbeatElapsedTicks()
+ err = curTicksIn - ticksIn
+ if err > Config.TICKS_PER_BEAT_DIV2:
+ err -= Config.TICKS_PER_BEAT
+ elif err < -Config.TICKS_PER_BEAT_DIV2:
+ err += Config.TICKS_PER_BEAT
+ correct = curTick - err
+ if correct > Config.TICKS_PER_BEAT:
+ correct -= Config.TICKS_PER_BEAT
+ elif correct < 0:
+ correct += Config.TICKS_PER_BEAT
+ #print "correct:: %f ticks, %f ticks in, %f expected, %f err, correct %f" % (curTick, curTicksIn, ticksIn, err, correct)
+ if abs(err) > 0.25:
+ self.csnd.adjustTick(-err)
+
+