Ableton Framework Classes

Share Embed Donate


Short Description

Ableton Framework Classes...

Description

Introduction to the Framework Classes Part 4 - the Final Chapter Introduction

In Part 1 of this series, I mentioned that my journey into the world of MIDI remote scripts began with a search for a better way of integrating my FCB1010 foot cont roller into my Live setup. Well, it’s been a fun trip - with lots of interesting side investigations along the way - but now we’ve come full circle and it’s time to finish up what I set out to do in the beginning. In this article, we’ll have a look a coupl e of new  _Framework methods introduced in version 8.1.3, which will allow us to operate scripts in Combination Mode, Mode, and we’ll create a generic script which will allow the FCB1010 to work in concert with the APC40 – in fact, we’ll set it up to emulate the APC40. And then we’re pretty much d one. Let’s start with combination mode. Combination Mode – red and yellow and pink and green…

As we saw in Part 1, set_show_highlight , a SessionComponent method, can be used to display the famous “red box”, which represents the portion of a Session View which a control surface is controlling. First see n with the APC40 and Launchpad, the red box is a must have for any clip launcher. New since 8.1.4, however, is a functional Combination Mode – specifically announced for the APC40 and A PC20 – which “glues” two or more session highlights together. From the 8.1.4 changelog: “Combination Mode is now active when multiple Akai APC40/20s are in use. This means that the topmost controller selected in your preferences will control tracks 1-8, the second controller selected will control tracks 9-16, 9-16, and so on.”

Sounds like fun – let’s see how it’s done. By looking through the APC sources, we can work our way back to the essential change at work here: the SessionComponent class now has new methods called  _link and _unlink (together with a new attribute _is_linked   _is_linked , and a new list object known as  _linked_session_instan  _linked_session_instances ces ). Linking sessions turns out to be no more difficult than calling the SessionComponent’s SessionComponent’s _link method, as follows: session = SessionComponent(num_tracks, num_scenes) session. session._link()

Now, although multiple session boxes can be linked in this way, we need to manage our session offsets, if we want our sessions to sit beside each other, and not one on top of the other. In other words, we will need to sequentially assign non-zero offsets to our linked sessions, based on the adjacent session’s width. We'll add the required code to the ProjectX script from Part 1, as an illustration. First, we’ll need a list object to hold  the active instances of our ProjectX ControlSurface class, and a static method which will be called at the end of initialisation: _active_instances = []

def _combine_active_instances(): _combine_active_instances(): track_offset = 0 for instance in ProjectX.  ProjectX._active_instances: instance. instance._activate_combination_mode(track_offset) track_offset += += session  session. .width() _combine_active_instances = staticmethod staticmethod(_combine_active_instances) (_combine_active_instances)  _do_combine call at the end of our init sequence, which in turn calls We add a  _do_combine the _combine_active_instanc  _combine_active_instances es  static method – and _combine_active_inst  _combine_active_instances ances  calls  _activate_combination_  _activate_combination_mode mode on each instance.

def _do_combine( _do_combine(self self): ): if self not in ProjectX.  ProjectX._active_instances: ProjectX. ProjectX._active_instances _active_instances. .append(self append(self) ) ProjectX. ProjectX._combine_active_instances() def _activate_combination_mode( _activate_combination_mode(self self, , track_offset): if session.  session._is_linked(): session. session._unlink() session. session.set_offsets(track_offset, 0) session. session._link() Now that the linking is all set up, we’ll define a _do_uncombin a  _do_uncombine e method, to clean things up when we disconnect; we’ll unlink our SessionComponent and remove ourselves from the list of active instances here.

def _do_uncombine( _do_uncombine(self self): ): if ((  ((self self in ProjectX.  ProjectX._active_instances) and  ProjectX. ProjectX._active_instances _active_instances. .remove(self remove(self)): )): self. self ._session. _session.unlink() ProjectX. ProjectX._combine_active_instances() def disconnect( disconnect(self self): ): self. self ._do_uncombine() ControlSurface. ControlSurface.disconnect(self disconnect(self) ) So here’s what we get when we load several instances of our new ProjectX script via Live's MIDI preferences dialog:

The session highlights are all linked (with an automatic track offset for each instance, which matches the adjacent session highlight width), and when we move any one of them, the others move along together. together. By  _activate_combination_mode ode , we can get them to stack changing the offsets in  _activate_combination_m s tack side-by-side, or one above the other, or if we wanted to, we could indeed stack them one on top of the other . By stacking them one on top of the other, we can control a si ngle session highlight zone wi th multiple controllers – which is exactly what we want to do with th e APC40 and FCB1010 in combination mode. As of version 8.1.3, the Framework s cripts have included support for session linking, but so far, the only official scripts which implement combination mode are the APC scripts (as of 8.1.4). As shown above, however, support for combination mode can be extended to pretty much any Framework -based script. What we want to create then, is a script which will allow the FCB1010 to operate in combination mode together with the APC40. Let’s build one.

def _do_combine( _do_combine(self self): ): if self not in ProjectX.  ProjectX._active_instances: ProjectX. ProjectX._active_instances _active_instances. .append(self append(self) ) ProjectX. ProjectX._combine_active_instances() def _activate_combination_mode( _activate_combination_mode(self self, , track_offset): if session.  session._is_linked(): session. session._unlink() session. session.set_offsets(track_offset, 0) session. session._link() Now that the linking is all set up, we’ll define a _do_uncombin a  _do_uncombine e method, to clean things up when we disconnect; we’ll unlink our SessionComponent and remove ourselves from the list of active instances here.

def _do_uncombine( _do_uncombine(self self): ): if ((  ((self self in ProjectX.  ProjectX._active_instances) and  ProjectX. ProjectX._active_instances _active_instances. .remove(self remove(self)): )): self. self ._session. _session.unlink() ProjectX. ProjectX._combine_active_instances() def disconnect( disconnect(self self): ): self. self ._do_uncombine() ControlSurface. ControlSurface.disconnect(self disconnect(self) ) So here’s what we get when we load several instances of our new ProjectX script via Live's MIDI preferences dialog:

The session highlights are all linked (with an automatic track offset for each instance, which matches the adjacent session highlight width), and when we move any one of them, the others move along together. together. By  _activate_combination_mode ode , we can get them to stack changing the offsets in  _activate_combination_m s tack side-by-side, or one above the other, or if we wanted to, we could indeed stack them one on top of the other . By stacking them one on top of the other, we can control a si ngle session highlight zone wi th multiple controllers – which is exactly what we want to do with th e APC40 and FCB1010 in combination mode. As of version 8.1.3, the Framework s cripts have included support for session linking, but so far, the only official scripts which implement combination mode are the APC scripts (as of 8.1.4). As shown above, however, support for combination mode can be extended to pretty much any Framework -based script. What we want to create then, is a script which will allow the FCB1010 to operate in combination mode together with the APC40. Let’s build one.

FCB1020 - a new script for the FCB1010

The first thing to do when setting out to create a new script is to decide on a functional layout. If we know what we’re trying to achieve in terms of functionality and operation - before we touch a line of code - we can save ourselves a good deal of time down t he road. In this case, we’re looking for an arrangement which will allow the FCB1010 to mirror the operation of the APC40, in so far as possible, and allow a llow for optimized hands-free operation. Although the options for designing a control script are almost unlimited, generally, the resulting method of operation needs to be intuitive. The FCB1010 has some built-in constraints, but also offers a great deal of flexibility. We have 10 banks of 10 switches and 2 pedals to work with – equivalent to 100 “button” controls and 20 “sliders”. Interestingly, the APC40 has a similar number of buttons and knobs. If we look at the two controllers side -by-side, a pattern emerges. Each column of the APC40’s grid consists of 5 clip launch buttons (one per scene) and 5 track control buttons (clip stop, track select, activate, activate, solo, & record). Each of the FCB1010’s 10 banks has 5 switches on the top row, and 5 switches on the bottom row. Based on this parallel, if we assign one FCB1010 bank to each of the APC40’s track columns,  the resulting operation will indeed be intuitive, and will closely follow the APC’s layout. We only have 2 pedals per bank, however, so we’ll map them to Track Volume, and Send A – at least for now. Here’s how a typical FCB1010 bank will lay out, together with the A PC40 layout for comparison.

Bank 01

APC40

We’ll use this layout for banks 1 through 8 (since the APC40 has 8 track control columns), but because the

FCB1010 has 10 banks in total, we have 2 banks left over. Let’s use bank 00 for the Master Track controls, and the scene launch controls, in a similar arrangement to banks 1 t hrough 8. There are no activate, solo or record buttons for the master track, so instead, we’ll map global play, stop and record here:

Bank 00

Now we only have one bank left - bank 09. We’ll use bank 09 for session and track navigation, and for device control. Here’s how it will look:

Bank 09

Although fairly intuitive, the layout described above might not suit everyone’s preferences. Wouldn’t it be nice if the end user could decide on his or her own preferred layout? Live’s User Remote Scripts allow for this kind of thing, so we’ll take a similar approach. Rather than hard -coding the note and controller mappings deep within our new script, we’ll pull all of the assignments out into a separate file, for easy access and editing. It will rem ain a python .py file (not a .txt file) - in the tradition of consts.py, of Mackie emulation fame - but since .py files are simple text files, they can be edited using any te xt editor. We’ll call our file MIDI_map.py . Here’s a sample of what it will contain:

# General PLAY = 7 #Global play  STOP = 8 #Global stop REC = 9 #Global record  TAPTEMPO = -1 #Tap tempo NUDGEUP = -1 #Tempo Nudge Up NUDGEDOWN = -1 #Tempo Nudge Down UNDO = -1 #Undo REDO = -1 #Redo LOOP = -1 #Loop on/off  PUNCHIN = -1 #Punch in PUNCHOUT = -1 #Punch out OVERDUB = -1 #Overdub on/off  METRONOME = -1 #Metronome on/off  RECQUANT = -1 #Record quantization on/off  DETAILVIEW = -1 #Detail view switch CLIPTRACKVIEW = -1 #Clip/Track view switch

# Device Control DEVICELOCK = 99 #Device Lock (lock "blue hand") DEVICEONOFF = 94 #Device on/off  DEVICENAVLEFT = 92 #Device nav left DEVICENAVRIGHT = 93 #Device nav right DEVICEBANKNAVLEFT = -1 #Device bank nav left DEVICEBANKNAVRIGHT = -1 #Device bank nav right # Arrangement View Controls SEEKFWD = -1 #Seek forward  SEEKRWD = -1 #Seek rewind  # Session Navigation (aka "red box") SESSIONLEFT = 95 #Session left SESSIONRIGHT = 96 #Session right SESSIONUP = -1 #Session up SESSIONDOWN = -1 #Session down ZOOMUP = 97 #Session Zoom up ZOOMDOWN = 98 #Session Zoom down ZOOMLEFT = -1 #Session Zoom left ZOOMRIGHT = -1 #Session Zoom right # Track Navigation TRACKLEFT = 90 #Track left TRACKRIGHT = 91 #Track right # Scene Navigation SCENEUP = -1 #Scene down SCENEDN = -1 #Scene up # Scene Launch SELSCENELAUNCH = -1 #Selected scene launch Now we can easily change the layout of any of our banks, by editing this one file. In fact, pretty much anything goes – if we wanted to, we could hav e different layouts for each of the 10 banks, or leave some banks unassigned, for use with guitar effects, etc. To help with layout planning, an editable PDF template for the FCB1010 is included with the source code on the Support Files page. Okay, so now it’s time to assemble the code. Since we’re essentially emulating the APC40 here (yes, I admit that I was wrong in Part 2; APC40 emulation is not so crazy after all), we h ave a choice between starting with the APC scripts and customizing, or building a new set of scripts which have similar functionality. Since we won’t be supporting shifted operations in our script (for the FCB1010 this would require operation with two feet – difficult to do in an upright position), we will need to make significant changes to the APC scripts. Starting from scratch is definitely an option worth considering. On the other hand, the APC40 script will make for a good roadmap, and while we’re at it, we can include some of the special features of the APC40_22 script from Part 3 here as well. The structure will be fairly simple. We’ll have an __init__.py  module (to identify the directory as a python package), a main module (called FCB1020.py ), a MIDI_map.py  constants file, and several “special” Framework component override modules. Here’s the file list (compete source code is available on the Support Files page): __init__.py FCB1020.py MIDI_Map.py

SpecialChannelStripComponent.py SpecialMixerComponent.py SpecialSessionComponent.py SpecialTransportComponent.py SpecialViewControllerComponent.py SpecialZoomingComponent.py We won’t go into detail on the Special components, since that topic was covered in Part 3. The main module follows the structure outlined in Part 1, but here's a quick overview. We start with the imports, and then define the combination mode static method (as discussed above):

import Live from   _Framework.ControlSurface import ControlSurface from   _Framework.InputControlEl ement import * from   _Framework.SliderElement import SliderElement from   _Framework.ButtonElement import ButtonElement from   _Framework.ButtonMatrixEl ement import ButtonMatrixElement from   _Framework.ChannelStripCo mponent import ChannelStripComponent from   _Framework.DeviceComponen t import DeviceComponent from   _Framework.ControlSurface Component import ControlSurfaceComponent from   _Framework.SessionZooming Component import SessionZoomingComponent from  SpecialMixerComponent import SpecialMixerComponent from  SpecialTransportComponent import SpecialTransportComponent from  SpecialSessionComponent import SpecialSessionComponent from  SpecialZoomingComponent import SpecialZoomingComponent from  SpecialViewControllerComponent import DetailViewControllerComponent from   MIDI_Map import * class FCB1020(ControlSurface): __doc__ = " Script for FCB1010 in APC emulation mode " _active_instances = [] def _combine_active_instances(): track_offset = 0 scene_offset = 0 for instance in FCB1020._active_instances: instance._activate_combination_mode(track_offset, scene_offset) track_offset += instance._session.width() _combine_active_instances = staticmethod(_combine_active_instances) Next we have our init method, where we instantiate our ControlSurface component and call the various setup methods. We setup the session, then setup the mixer, then assign the mixer to the session, to keep them in sync. The disconnect method follows, where we provide some cleanup for when the control surface is disconnected: def __init__(self, c_instance): ControlSurface.__init__(self, c_instance) self.set_suppress_rebuild_requests(True) self._note_map = [] self._ctrl_map = [] self._load_MIDI_map() self._session = None self._session_zoom = None self._mixer = None self._setup_session_control() self._setup_mixer_control()

self._session.set_mixer(self._mixer) self._setup_device_and_transport_control() self.set_suppress_rebuild_requests(False) self._pads = [] self._load_pad_translations() self._do_combine()

def disconnect(self): self._note_map = None self._ctrl_map = None self._pads = None self._do_uncombine() self._shift_button = None self._session = None self._session_zoom = None self._mixer = None ControlSurface.disconnect(self) The balance of the combination mode methods are next: def _do_combine(self): if self not in FCB1020._active_instances: FCB1020._active_instances.append(self) FCB1020._combine_active_instances() def _do_uncombine(self): if ((self in FCB1020._active_instances) and  FCB1020._active_instances.remove(self)): self._session.unlink() FCB1020._combine_active_instances() def _activate_combination_mode(self, track_offset, scene_offset): if TRACK_OFFSET != -1: track_offset = TRACK_OFFSET if SCENE_OFFSET != -1: scene_offset = SCENE_OFFSET self._session.link_with_track_offset(track_offset, scene_offset) The session setup is based on Framework SessionComponent methods, with SessionZoomingComponent navigation thrown in for good measure: def _setup_session_control(self): is_momentary = True self._session = SpecialSessionComponent(8, 5) self._session.name = 'Session_Control' self._session.set_track_bank_buttons(self._note_map[SESSIONRIGHT], self._note_map[SESSIONLEFT]) self._session.set_scene_bank_buttons(self._note_map[SESSIONDOWN], self._note_map[SESSIONUP]) self._session.set_select_buttons(self._note_map[SCENEDN], self._note_map[SCENEUP]) self._scene_launch_buttons = [self._note_map[SCENELAUNCH[index]] for index in range(5) ] self._track_stop_buttons = [self._note_map[TRACKSTOP[index]] for index in range(8) ]

self._session.set_stop_all_clips_button(self._note_map[STOPALLCLIPS]) self._session.set_stop_track_clip_buttons(tuple(self._track_stop_buttons)) self._session.set_stop_track_clip_value(2) self._session.selected_scene().name = 'Selected_Scene' self._session.selected_scene().set_launch_button(self._note_map[SELSCENELAUNC H]) self._session.set_slot_launch_button(self._note_map[SELCLIPLAUNCH]) for scene_index in range(5): scene = self._session.scene(scene_index) scene.name = 'Scene_' + str(scene_index) button_row = [] scene.set_launch_button(self._scene_launch_buttons[scene_index]) scene.set_triggered_value(2) for track_index in range(8): button = self._note_map[CLIPNOTEMAP[scene_index][track_index]] button_row.append(button) clip_slot = scene.clip_slot(track_index) clip_slot.name = str(track_index) + '_Clip_Slot_' + str(scene_index) clip_slot.set_launch_button(button) self._session_zoom = SpecialZoomingComponent(self._session) self._session_zoom.name = 'Session_Overview' self._session_zoom.set_nav_buttons(self._note_map[ZOOMUP], self._note_map[ZOOMDOWN], self._note_map[ZOOMLEFT], self._note_map[ZOOMRIGHT])

Mixer, device, and transport setup methods are similar. def _setup_mixer_control(self): is_momentary = True self._mixer = SpecialMixerComponent(8) self._mixer.name = 'Mixer' self._mixer.master_strip().name = 'Master_Channel_Strip' self._mixer.master_strip().set_select_button(self._note_map[MASTERSEL]) self._mixer.selected_strip().name = 'Selected_Channel_Strip' self._mixer.set_select_buttons(self._note_map[TRACKRIGHT], self._note_map[TRACKLEFT]) self._mixer.set_crossfader_control(self._ctrl_map[CROSSFADER]) self._mixer.set_prehear_volume_control(self._ctrl_map[CUELEVEL]) self._mixer.master_strip().set_volume_control(self._ctrl_map[MASTERVOLUME]) for track in range(8): strip = self._mixer.channel_strip(track) strip.name = 'Channel_Strip_' + str(track) strip.set_arm_button(self._note_map[TRACKREC[track]]) strip.set_solo_button(self._note_map[TRACKSOLO[track]]) strip.set_mute_button(self._note_map[TRACKMUTE[track]]) strip.set_select_button(self._note_map[TRACKSEL[track]]) strip.set_volume_control(self._ctrl_map[TRACKVOL[track]]) strip.set_pan_control(self._ctrl_map[TRACKPAN[track]]) strip.set_send_controls((self._ctrl_map[TRACKSENDA[track]], self._ctrl_map[TRACKSENDB[track]], self._ctrl_map[TRACKSENDC[track]]))

 

strip.set_invert_mute_feedback(True)

def _setup_device_and_transport_control(self): is_momentary = True self._device = DeviceComponent() self._device.name = 'Device_Component' device_bank_buttons = [] device_param_controls = [] for index in range(8): device_param_controls.append(self._ctrl_map[PARAMCONTROL[index]]) device_bank_buttons.append(self._note_map[DEVICEBANK[index]]) if None not in device_bank_buttons: self._device.set_bank_buttons(tuple(device_bank_buttons)) self._device.set_parameter_controls(tuple(device_param_controls)) self._device.set_on_off_button(self._note_map[DEVICEONOFF]) self._device.set_bank_nav_buttons(self._note_map[DEVICEBANKNAVLEFT], self._note_map[DEVICEBANKNAVRIGHT]) self._device.set_lock_button(self._note_map[DEVICELOCK]) self.set_device_component(self._device) detail_view_toggler =  DetailViewControllerComponent() detail_view_toggler.name = 'Detail_View_Control' detail_view_toggler.set_device_clip_toggle_button(self._note_map[CLIPTRACKVIE W]) detail_view_toggler.set_detail_toggle_button(self._note_map[DETAILVIEW]) detail_view_toggler.set_device_nav_buttons(self._note_map[DEVICENAVLEFT], self._note_map[DEVICENAVRIGHT] ) transport =  SpecialTransportComponent() transport.name = 'Transport' transport.set_play_button(self._note_map[PLAY]) transport.set_stop_button(self._note_map[STOP]) transport.set_record_button(self._note_map[REC]) transport.set_nudge_buttons(self._note_map[NUDGEUP], self._note_map[NUDGEDOWN]) transport.set_undo_button(self._note_map[UNDO]) transport.set_redo_button(self._note_map[REDO]) transport.set_tap_tempo_button(self._note_map[TAPTEMPO]) transport.set_quant_toggle_button(self._note_map[RECQUANT]) transport.set_overdub_button(self._note_map[OVERDUB]) transport.set_metronome_button(self._note_map[METRONOME]) transport.set_tempo_control(self._ctrl_map[TEMPOCONTROL]) transport.set_loop_button(self._note_map[LOOP]) transport.set_seek_buttons(self._note_map[SEEKFWD], self._note_map[SEEKRWD]) transport.set_punch_buttons(self._note_map[PUNCHIN], self._note_map[PUNCHOUT]) We’ve also included a DetailViewComponent  above, which communicates session view changes via the Live API. Next is _on_selected_track_changed , a ControlSurface class method override, which keeps the selected track’s device in focus. And for drum rack note mapping, we’ve included a _load_pad_translationsmethod, which adds x and y offsets to the Drum Rack note and channel assignments, which are set in

the MIDI_map.py  file. This allows us to pass the translations array as an argument to the ControlSurface set_pad_translations method in the expected format. def _on_selected_track_changed(self): ControlSurface._on_selected_track_changed(self) track = self.song().view.selected_track device_to_select = track.view.selected_device if device_to_select == None and  len(track.devices) > 0: device_to_select = track.devices[0] if device_to_select != None: self.song().view.select_device(device_to_select) self._device_component.set_device(device_to_select) def _load_pad_translations(self): if -1 not in DRUM_PADS: pad = [] for row in range(4): for col in range(4): pad = (col, row, DRUM_PADS[row*4 + col], PADCHANNEL,) self._pads.append(pad) self.set_pad_translations(tuple(self._pads)) Finally, we have  _load_MIDI_map. Here, we create a list of ButtonElements  and a list of SliderElements . When we make mapping assignments in our MIDI_map.py file, we are actually indexing objects from these lists. By instantiating the ButtonElements and SliderElements as independent objects, we limit the risk of duplicate MIDI assignments, which would prevent our script from loading. Any particular MIDI note/channel message from a control surface can only be assigned to a single InputControlElement (such as a button or slider), however, an InputControlElement can be used more than once, with different components. This setup also allows us to append None to the end of each list, so that null assignments can be specified in the MIDI_map file, by using -1 in place of a note number (in python, [-1] corresponds to the last element of a list). def _load_MIDI_map(self): is_momentary = True for note in range(128): button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, NOTECHANNEL, note) button.name = 'Note_' + str(note) self._note_map.append(button) self._note_map.append(None) #add None to the end of the list, selectable with [-1] for ctrl in range(128): control = SliderElement(MIDI_CC_TYPE, CTRLCHANNEL, ctrl) control.name = 'Ctrl_' + str(ctrl) self._ctrl_map.append(control) self._ctrl_map.append(None) Now, speaking of MIDI assignments, since all of our mappings are editable, and grouped in a separate file, couldn’t we use our script with just about any  control surface, and not only the FCB1010? Yes, indeed we could. Generic APC Emulation

Our new FCB10120 script can be used as a generic APC emulator, since it merely maps MIDI Note and CC input to specific _Framework component functions, mimicking the APC script setup. In fact, none of this is very different from the User script mechanism provided by Ableton - although our script has a few extra

features that the User script does not support, including Session Highlighting (aka “red box”), and the ability to work in combination mode, with an A PC40, or with another instance of itself, or with any other controller which supports combination mode.

Some setup is required, however, to accommodate alternate controller configurations. These configurations could be alternate configurations for the same control surface, or alternate configurations for different control surfaces. Probably the simplest way of setting up an alternate configuration, is to create one or more copies of the FCB1020 script folder, modify the assignments in the MIDI_map.py file as required, and then re-name the directory to suit. The new folder will be selectable by name from Live’s control surface drop -down list, the next time Live is started. This way, one could have, say, FCB1020_device_mode and FCB1020_transport_mode as separate configurations, listed one above the other in the control surface drop-down. Note however , that one should avoid leading unders cores in folder name, unless a folder is intended to be hidden. Another way to accommodate alternate setups would be to reprogram the control surface itself – where this is possible - to match the note and CC mappings found in the MIDI_map.py file. Depending on the control surface, this could be done manually, with stand-alone software, or by using Live’s “dump” button from the preferences dialog (in fact, a sysex file for the FCB1010 is included with the sup port files package which accompanies this article, for this purpose). Of course, our script can also be used as a generic APC emulator for multiple controllers at the same time . This can be done a number of ways, including: 1) Daisy-chain several control surfaces using MIDI Thru ports; 2) Load the script multiple times, using multiple slots; 3) Create multiple copies of the script folder, r ename to suit, and load into different slots. For multiple control surfaces which use different MIDI channels, separate instances of the script would need to be loaded, from separate folders, with the channel assignments in the MIDI_maps.py modified to suit in each folder. And getting back to our origi nal design setup, we can see that the FCB1010 and the APC40 now work w ell together in Combination Mode, and the FCB1010 is able to control of most of the APC’s functions –  within one session highlight area, and without loss of focus. We have included a good deal of flexibility in our script too, so we can easily modify the various bank layouts to suit our needs, as they develop and change. Conclusion

In this new era of multi-touch controllers, it’s nice to know that a sturdy old workhorse like the FCB1010  still has a place in our arsenal of control surfaces - and that i t works well in combination with the soon -to-be-aclassic and not-yet- obsolete APC40. As for MIDI remote scripts, they’re still at the heart of all control surface communications with the Live API - and the _Framework classes have been holding their own for quite a while now, with interesting new methods being added from time to time. Hopefully, this series of articles has been useful, and will encourage others to share their findings with the Live community. Happy scripting. Hanz Petrov September 7, 2010 hanz.petrov at gmail.com

Introduction to the Framework Classes Part 2 Background In this post, we’ll have a look at the differences between Live 7 and Live 8 remote scripts, get the Max for Live point-of-view on control surfaces, take a detailed look at the newest APC40 (and APC20) scripts – and demonstrate a few APC script hacks along the way. If you’re new to MIDI remote scripts, you might want to have a look at Part 1 of the Introduction to the Framework Classes before coming back here for Part 2. Keeping up with recent changes

As discussed previously, most of what we know about MIDI remote scripting has been based on exploring decompiled Python pyc files. The Live 7 remote scripts are Python 2.2 files, and hav e proven to be relatively easy to decompile. Live 8’s integration of Python 2.5, on the other hand, presents a new challenge. At present, there is no reliable, freely accessible method for decompiling 2.5 pyc files. It i s possible, however, to get a good sense of the latest changes made to the MIDI remote scripts using the tools at hand. Unpyc is one such tool, and unpyc can decompile python 2.5 files - but only up to a point. In most cases, it will only produce partial source code, but at least it lets us know where it is having trouble. It can, however, disassemble 2.5 files without fail. When we’re armed with a partial decompile, and a complete disassembly, it is possible to reconstruct working script source code - although the process is tedious at the best of times. But even without reconstructing complete sources, Unpyc allows us to take a peek behind the scenes and understand the nature of the changes implemented with the most recent MIDI remote scripts. As an example, here is the mixer setup method from the APC40.py script - Live 8.1.1 version: def _setup_mixer_control(self): is_momentary = True mixer = SpecialMixerComponent(8) mixer.name = 'Mixer' mixer.master_strip().name = 'Master_Channel_Strip' mixer.selected_strip().name = 'Selected_Channel_Strip' for track in range(8): strip = mixer.channel_strip(track) strip.name = 'Channel_Strip_' + str(track) volume_control = SliderElement(MIDI_CC_TYPE, track, 7) arm_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, track, 48) solo_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, track, 49) mute_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, track, 50) select_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, track, 51) volume_control.name = str(track) + '_Volume_Control' arm_button.name = str(track) + '_Arm_Button' solo_button.name = str(track) + '_Solo_Button' mute_button.name = str(track) + '_Mute_Button' select_button.name = str(track) + '_Select_Button' strip.set_volume_control(volume_control) strip.set_arm_button(arm_button) strip.set_solo_button(solo_button) strip.set_mute_button(mute_button) strip.set_select_button(select_button) strip.set_shift_button(self._shift_button) strip.set_invert_mute_feedback(True) crossfader = SliderElement(MIDI_CC_TYPE, 0, 15)

 

master_volume_control = SliderElement(MIDI_CC_TYPE, 0, 14) master_select_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, 0,

80) prehear_control = EncoderElement(MIDI_CC_TYPE, 0, 47, Live.MidiMap.MapMode.relative_two_compliment) crossfader.name = 'Crossfader' master_volume_control.name = 'Master_Volume_Control' master_select_button.name = 'Master_Select_Button' prehear_control.name = 'Prehear_Volume_Control' mixer.set_crossfader_control(crossfader) mixer.set_prehear_volume_control(prehear_control) mixer.master_strip().set_volume_control(master_volume_control) mixer.master_strip().set_select_button(master_select_button) return mixer

Compare the above with the equivalent code from the 7.0.18 version: def _setup_mixer_control(self): is_momentary = True mixer = MixerComponent(8) for track in range(8): strip = mixer.channel_strip(track) strip.set_volume_control(SliderElement(MIDI_CC_TYPE, track, 7)) strip.set_arm_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, track, 48)) strip.set_solo_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, track, 49)) strip.set_mute_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, track, 50)) strip.set_select_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, track, 51)) strip.set_shift_button(self._shift_button) strip.set_invert_mute_feedback(True) mixer.set_crossfader_control(SliderElement(MIDI_CC_TYPE, 0, 15)) mixer.set_prehear_volume_control(EncoderElement(MIDI_CC_TYPE, 0, 47, Live.MidiMap.MapMode.relative_two_compliment)) mixer.master_strip().set_volume_control(SliderElement(MIDI_CC_TYPE, 0, 14)) mixer.master_strip().set_select_button(ButtonElement(is_momentary, MIDI_NOTE_TYPE, 0, 80)) return mixer As we can see, the main difference between the Live 7.0.18 code and the Live 8.1.1 code is that in the new script, name attributes have been assigned to most of the components and elements. So who needs names? Max for Live needs names. Max for Live – keeping up with Cycling ‘74

Max for Live requires Live 8.1 or higher, and, accordingly, new Live 8 versions include Max-compatible MIDI remote scripts. The name attributes assigned to the python objects and methods allow Max for Live to see and access python methods and objects more easi ly (objects including control surfaces, components, and control elements). This can be verified with the help of Max for Live’s LiveAPI resource patches (M4L.api.SelectComponent, M4L.api.SelectControlSurface, M4L.api.SelectControl, etc.). Before we demonstrate, however, let’s have a look at how Control Surfaces fit into the world of Max’s Live Object Model (LOM).

The three main root objects in the Live Object Model are known as live_app  (Application), live_set  (Song), and control_surfaces (Control Surface). Cycling ‘74 provides somewhat detailed documentation for the first two, however, the last one seems to have been left out - intentionally, no doubt. Based on the LOM diagram , we can see that the Control Surfaces root object includes classes for components and controls. These m ap directly to python Framework modules (modules with name s like ControlSurface, TransportComponent, SessionComponent , ButtonElement , etc.). Now, although documentation which explains this is almost non-exi stent, it turns out that the python methods and objects of the Framework classes are both v isible and accessible from Max for L ive. The remote script methods appear to Max as functions – functions which can be called using the syntax and arguments defined in the Python scripts. It is now clear that in order to gain a proper understanding of how to manipulate more complicated Control Surfaces, a study of the python remote scripts is essential - because that's where the Control Surface functions originate. We'll be demonstrating this link between Max for Live and the python scripts, but before we do, let’s take a peek under the hood of what has proven to be a very popular Live controller the APC40 (now almost a year old). The APC40 – under the hood

The latest Live 7 versions include MIDI remote scripts for the APC40, and, of course, Live 8.1 and higher also support the APC40. As mentioned above, the Live 8.1 versions of the APC40 scripts show slight differences generally, name attributes have been added to most of the methods and objects. We’ll base our investigation here on the 8.1.1 scripts, in an effort to stay somewhat current. There are 11 files in the APC40 MIDI remote scripts directory: __init__.pyc APC40.pyc DetailViewControllerComponent.pyc EncoderMixerModeSelectorComponent.pyc PedaledSessionComponent.pyc ShiftableDeviceComponent.pyc ShiftableTranslatorComponent.pyc ShiftableTransportComponent.pyc SpecialChannelStripComponent.pyc SpecialMixerComponent.pyc RingedEncoderElement.pyc

The __init__.py  script is rather uninteresting, as it is with most scripts: import Live from   APC40 import APC40 def create_instance(c_instance): """ Creates and returns the APC40 script """ return APC40(c_instance) This is a standard init, as shown in Part 1 of the Introduction to Remote Scripts. Now, the script files here with the longish names are special classes, which inherit from Framework modules, but add custom functionality. What they do (and the name of the Framework classes they inherit from) is described in the Docstrings of the scripts themselves: class DetailViewControllerComponent(ControlSurfaceComponent): ' Component that can toggle the device chain- and clip view of the selected track ' class EncoderMixerModeSelectorComponent(ModeSelectorComponent): ' Class that reassigns encoders on the AxiomPro to different mixer functions '

class PedaledSessionComponent(SessionComponent): ' Special SessionComponent with a button (pedal) to fire the selected clip slot ' class RingedEncoderElement(EncoderElement): ' Class representing a continuous control on the controller enclosed with an LED ring ' class ShiftableDeviceComponent(DeviceComponent): ' DeviceComponent that only uses bank buttons if a shift button is pressed ' class ShiftableTranslatorComponent(ChannelTranslationSelector): ' Class that translates the channel of some buttons as long as a shift button is held ' class ShiftableTransportComponent(TransportComponent): ' TransportComponent that only uses certain buttons if a shift button is pressed ' It is interesting to note that the EncoderMixerModeSelectorComponent module appears to have been recycled from the AxiomPro script. And, for anyone in terested, the rest of the python code can be examined here. The most interesting module of the l ot by far, is APC40.py - which is where most of the action is. This file begins with the imports: import Live from   _Framework.ControlSurface import ControlSurface from   _Framework.InputControlEl ement import * from   _Framework.SliderElement import SliderElement from   _Framework.ButtonElement import ButtonElement from   _Framework.EncoderElement import EncoderElement from   _Framework.ButtonMatrixEl ement import ButtonMatrixElement from   _Framework.MixerComponent import MixerComponent from   _Framework.ClipSlotCompon ent import ClipSlotComponent from   _Framework.ChannelStripCo mponent import ChannelStripComponent from   _Framework.SceneComponent import SceneComponent from   _Framework.SessionZooming Component import SessionZoomingComponent from   _Framework.ChannelTransla tionSelector import ChannelTranslationSelector from  EncoderMixerModeSelectorComponent import EncoderMixerModeSelectorComponent from  RingedEncoderElement import RingedEncoderElement from  DetailViewControllerComponent import  DetailViewControllerComponent from  ShiftableDeviceComponent import ShiftableDeviceComponent from  ShiftableTransportComponent import ShiftableTransportComponent from  ShiftableTranslatorComponent import  ShiftableTranslatorComponent from  PedaledSessionComponent import PedaledSessionComponent from  SpecialMixerComponent import SpecialMixerComponent As expected, in addition to the Live import (which provides direct access to the Live API), and the special modules listed previously, all of the other imports are _Framework modules. Again, see Part 1 for more detail. Next are the constants, which are used in the APC40 sysex exchange (more on this later): SYSEX_INQUIRY = (240, 126, 0, 6, 1, 247) MANUFACTURER_ID = 71 PRODUCT_MODEL_ID = 115 APPLICTION_ID = 65

CONFIGURATION_ID = 1

Then, after the name and docstring, we have the obligatory __init__ method. The APC40 __init__ looks like this: def __init__(self, c_instance): ControlSurface.__init__(self, c_instance) self.set_suppress_rebuild_requests(True) self._suppress_session_highlight = True is_momentary = True self._shift_button = ButtonElement(is_momentary, MIDI_NOTE_TYPE, 0, 98) self._shift_button.name = 'Shift_Button' self._suggested_input_port = 'Akai APC40' self._suggested_output_port = 'Akai APC40' session = self._setup_session_control() mixer = self._setup_mixer_control() self._setup_device_and_transport_control() self._setup_global_control(mixer) session.set_mixer(mixer) for component in self.components: component.set_enabled(False) self.set_suppress_rebuild_requests(False) self._device_id = 0 self._common_channel = 0 self._dongle_challenge = (Live.Application.get_random_int(0, 2000000), Live.Application.get_random_int(2000001, 4000000)) As we can see, standard Framework-based scripting is used to create a session object, a mixer, device and transport components, and global controls, and then to assign the mixer to the session. There’s nothing very mysterious here – except perhaps _dongle_challenge. And - you may be wondering - where does the infamous “secret handshake” live? Why, in handle_sysex  of course. The Secret Handshake – not so secret anymore

Within days of the APC40 hitting retail shelves, it was discovered that the secret handshake is based on a sysex exchange. But how does it work exactly, and what is it hiding? It’s not hiding anything that can’t b e done with basic Framework scripting (as we saw in Part 1), and it works by sending a “dongle challenge” sysex string, then looking for a correct response from the controller. If the response from the controller matches the expected response (i.e. the handshake succeeds), then all of the controls on the controller are enabled and the session highlight (aka “red box”) is turned on. Here’s part of the handle_sysex method, in native python: def handle_sysex(self, midi_bytes): if ((midi_bytes[3] == 6) and   (midi_bytes[4] == 2)): assert (midi_bytes[5] == MANUFACTURER_ID) assert (midi_bytes[6] == PRODUCT_MODEL_ID) version_bytes = midi_bytes[9:13] self._device_id = midi_bytes[13] self._send_midi((240, MANUFACTURER_ID, self._device_id, PRODUCT_MODEL_ID, 96, 0, 4,

 

APPLICTION_ID, self.application().get_major_version(), self.application().get_minor_version(), self.application().get_bugfix_version(), 247)) challenge1 = [0,0,0,0,0,0,0,0] challenge2 = [0,0,0,0,0,0,0,0] #... dongle_message = ((((240, MANUFACTURER_ID, self._device_id, PRODUCT_MODEL_ID, 80, 0, 16) + tuple(challenge1)) + tuple(challenge2)) + (247)) self._send_midi(dongle_message) message = ((('APC40: Got response from controller, version ' + str(((version_bytes[0]
View more...

Comments

Copyright ©2017 KUPDF Inc.
SUPPORT KUPDF