Thursday, May 13, 2010

Introduction to the Framework Classes Part 3

Introduction

As we’ve demonstrated previously, the _Framework classes provide a very useful framework for coding MIDI control surface scripts in Python. We’ve looked at basic script setup, worked with simple Control Surface components and objects, and over-ridden high-level setup methods to achieve certain customization goals. Here in this post, we’ll have a look at the relationship between the _Framework classes and the “special” subclass modules which are used with many of the Framework-based MIDI remote scripts for newer controllers.

As a working example, we’ll be adding APC20 functionality to the APC40 by modifying the APC40’s MIDI remote scripts. Our modifications will include Note Mode for the matrix, and User Mode for the sliders. We’ll also have another look at Device Lock, create mappings for Undo and Redo, and re-map the APC40’s endless encoder to control Tempo. For those who wish to follow along, the complete Python source files which accompany this article can be downloaded here.

Sysex and Subclasses

As we’ve seen previously, the APC20 can be put into Note Mode by sending an appropriate Sysex string to the controller. In this mode, the 8x5 matrix can be used to send MIDI notes, trigger samples, control drum racks, etc. - similar to the Launchpad’s User modes. For the APC40, however, a firmware upgrade would be required in order enable Note Mode via Sysex. Fortunately, Sysex is not the only way. The Launchpad’s User Modes do not rely on Sysex, and we can use the same approach to add a Note Mode to the APC40. Before we do, however, let’s collect the scripts we need in order to combine the useful new features of the APC20 with those of the APC40.

In Live 8.1.3, the APC20 and APC40 scripts are both subclasses of the APC “super-class” script, although the APC module actually resides in the APC40 folder, while the APC20 scripts are found in a separate folder. Here, we’ll copy all of the required scripts over into a new folder, which we’ll call APC40_20. Our modified controller script will then be selectable in Live’s MIDI preferences drop-down list as APC40_20.

Since any “shifted” controls generally require a new subclass (as they inherit from and override Framework base class methods), and since many of the controls on both the APC40 and on the APC20 work in a shifted mode, we’ll need to include a fair number of “special” subclass modules. Here is a list of the modules we’ll be needing (in addition to the Framework modules, which are not listed):

__init__.py
APC.py (APC40 and APC20)
APC40plus20.py (based on APC40 script)
APCSessionComponent.py (APC20)
ConfigurableButtonElement.py (Launchpad)
DetailViewControllerComponent.py (APC40)
EncoderMixerModeSelectorComponent.py (APC40)
PedaledSessionComponent.py (APC40)
RingedEncoderElement.py (APC40)
ShiftableDeviceComponent.py (APC40)
ShiftableSelectorComponent.py (APC20)
ShiftableTranslatorComponent.py (APC40)
ShiftableTransportComponent.py (APC40)
ShiftableZoomingComponent.py (APC20)
SliderModesComponent.py (APC20)
SpecialChannelStripComponent.py (APC40)
SpecialMixerComponent.py (APC40)
SpecialTransportComponent.py (APC40)

Note that we’ve included one non-APC module here, ConfigurableButtonElement, which is from the Launchpad scripts. One advantage of using the Framework classes for scripting is that special subclass modules are generally re-usable and interchangeable, especially for similar control surfaces.

Now, with all of the subclass scripts gathered together in one place (for ease of editing and transportability), we’ll merge the APC20 and APC40 scripts. Both of these scripts are Framework-based, so there will be some import duplicates, which we can eliminate. There are also some APC20 special subclass modules which we will be using instead of the APC40’s plain vanilla Framework imports. Here is the merged import list:

import Live
from APC import APC
from _Framework.ControlSurface import ControlSurface
from _Framework.InputControlElement import *
from _Framework.SliderElement import SliderElement
from _Framework.ButtonElement import ButtonElement
from _Framework.EncoderElement import EncoderElement
from _Framework.ButtonMatrixElement import ButtonMatrixElement
from _Framework.MixerComponent import MixerComponent
from _Framework.ClipSlotComponent import ClipSlotComponent
from _Framework.ChannelStripComponent import ChannelStripComponent
from _Framework.SceneComponent import SceneComponent
#from _Framework.SessionZoomingComponent import SessionZoomingComponent #use ShiftableZoomingComponent from APC20 scripts instead
from _Framework.ChannelTranslationSelector 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

# Additional imports from APC20.py:
from ShiftableZoomingComponent import ShiftableZoomingComponent
from ShiftableSelectorComponent import ShiftableSelectorComponent
from SliderModesComponent import SliderModesComponent

# Import added from Launchpad scripts - needed for Note Mode:
from ConfigurableButtonElement import ConfigurableButtonElement 

Next we compare the APC20 and APC40 scripts for differences, retaining the most useful parts of each. Since most of the setting-up in the APC20 script setup is identical to that of APC40, and since the APC40 has more controls than the APC20, we’ll base our modified script on APC40 script, adding relevant code from the APC20 script as needed. The basic set-up of our script includes the following components:

SessionComponent (includes session matrix, “red box” navigation controls, scene and clip launch buttons, and stop buttons)
SessionZoomingComponent (includes “zoomed out” controls: matrix, selection, navigation, etc., for “banks of scenes”)
MixerComponent (includes solo, mute, arm, and volume controls for tracks)
TransportComponent (includes play, stop, record, tempo, metronome, quantize, etc., for song)
DeviceComponent (includes device control, device on-off, device navigation, etc.)

These are basic Framework components, however, with a complex controller like the APC40, most are actually instantiated as special subclass objects. The special subclass modules are typically used for over-riding existing Framework class methods, and for adding new methods - and often include direct calls to the LiveAPI. However, since they generally provide easily understood features, their usage is straight-forward and we can include most of them as-is, without modification, and without further investigation. We do need to sort out the conflicts between the APC40 and APC20 scripts, however - and add new code where we want to do new things or do things in different ways. It turns out that the only special modules which we’ll need to modify here are the following:

SpecialMixerComponent.py – we will map Cue Level to Tempo here
ShiftableDeviceComponent.py – we will check the Device Lock state here
ShiftableSelectorComponent.py – we will implement Note Mode here
ShiftableTransportComponent.py – we will add Undo and Redo, and handle the true endless encoder here
ShiftableZoomingComponent.py – we will modify the scope of Note Mode here (keep scene launch active)

Mapping Cue Level to Tempo provides a good example of subclass modification. It turns out that there is only one true endless encoder on the APC40/20, which is the Cue Level control. This is the control which we want to use as a Tempo control, due to its unique placement on the board. Unfortunately, however, the simple set_tempo_control Framework TransportComponent method, which we would normally use, throws an assertion error when mapped to a true endless encoder:

def set_tempo_control(self, control, fine_control = None):
        assert ((control == None) or (isinstance(control, EncoderElement) and (control.message_map_mode() is Live.MidiMap.MapMode.absolute)))
        assert ((fine_control == None) or (isinstance(fine_control, EncoderElement) and (fine_control.message_map_mode() is Live.MidiMap.MapMode.absolute)))

Our true endless encoder is an EncoderElement with a map_mode value of Live.MidiMap.MapMode.relative_two_compliment, not Live.MidiMap.MapMode.absolute, so, we’ll need to dig deeper. Luckily, there’s an “old-fashioned” script which shows us the way - we can adapt code from the Mackie Control script to suit our purpose here (the Mackie jog wheel is also a true endless encoder). In the ShiftableTransportComponent module, we add a method for assigning the control, which now will throw an assertion error if the mapped control is not an endless encoder:

def set_tempo_encoder(self, control):
        assert ((control == None) or (isinstance(control, EncoderElement) and (control.message_map_mode() is Live.MidiMap.MapMode.relative_two_compliment)))
        if (self._tempo_encoder_control != None):
            self._tempo_encoder_control.remove_value_listener(self._tempo_encoder_value)
        self._tempo_encoder_control = control
        if (self._tempo_encoder_control != None):
            self._tempo_encoder_control.add_value_listener(self._tempo_encoder_value)
        self.update()

And we need to add the callback (i.e., the “listener value”). This is the method which is called whenever the controller (encoder) is adjusted:

def _tempo_encoder_value(self, value):
        if not self._shift_pressed:
            assert (self._tempo_encoder_control != None)
            assert (value in range(128))
            backwards = (value >= 64)
            step = 0.1 #step = 1.0 #reduce this for finer control; 1.0 is 1 bpm
            if backwards:
                amount = (value - 128)
            else:
                amount = value
            tempo = max(20, min(999, (self.song().tempo + (amount * step))))
            self.song().tempo = tempo

Note that we’ve changed the step increment to a reasonably smooth value of 0.1 bpm, since we’re only using one encoder for tempo control here (the Framework basic transport.set_tempo_control method, which we’re not using, is actually set up to accept two controller assignments - one for coarse tuning and one for fine tempo tuning). If we wanted to, we could also adjust the Tempo range min and max values here as well.

We can see that the _tempo_encoder_value method ends with a call to the LiveAPI; self.song().tempo = tempo. The LiveAPI is well documented, so these types of calls are easy to look up, when they're not easliy understood at first glance. We can also refer to the many MIDI remote scripts, and the _Framework scripts themselves, for examples of how to make the calls and what arguments to use.

In the _tempo_encoder_value method above, we check to see if shift is pressed before doing anything else, because we want to revert to the original Cue Volume mapping when in shifted mode. We’ll also need to add some code to the SpecialMixerComponent module to do this. The _shift_value method is called when the shift button is pressed, which in turn calls the update() method; this is where we’ll connect our “prehear” (Cue Volume) control - right before the Crossfader control connection is made:

def _shift_value(self, value): #added
        assert (self._shift_button != None)
        assert (value in range(128))
        self._shift_pressed = (value != 0)
        self.update()

def update(self): #added override
        if self._allow_updates:
            master_track = self.song().master_track
            if self.is_enabled():
                if (self._prehear_volume_control != None):
                    if self._shift_pressed: #added 
                        self._prehear_volume_control.connect_to(master_track.mixer_device.cue_volume)
                    else:
                        self._prehear_volume_control.release_parameter() #added        
                if (self._crossfader_control != None):
                    self._crossfader_control.connect_to(master_track.mixer_device.crossfader)
            else:
                if (self._prehear_volume_control != None):
                    self._prehear_volume_control.release_parameter()
                if (self._crossfader_control != None):
                    self._crossfader_control.release_parameter()
                if (self._bank_up_button != None):
                    self._bank_up_button.turn_off()
                if (self._bank_down_button != None):
                    self._bank_down_button.turn_off()
                if (self._next_track_button != None):
                    self._next_track_button.turn_off()
                if (self._prev_track_button != None):
                    self._prev_track_button.turn_off()
            self._rebuild_callback()
        else:
            self._update_requests += 1

So far, so good. With some higher-level remote script adjustments and some lower-level subclass code modifications, we’ve successfully mapped Cue Level to Tempo. Now, on to Note Mode.

Note Mode

In order to enable Note Mode, first we need to disable the clip slots, so that we can re-assign the matrix buttons to the MIDI notes of our chosing. We don’t want to totally disconnect the ButtonElements, we only want to change the mappings. The original APC20 script actually disables the SessionComponent altogether, however, this also puts the Scene Launch and Track Stop buttons out of action. To take out only the matrix, we need to disable the clip slots, and nothing else. We’ll do this in the set_ignore_buttons method of the ShiftableZoomingComponent class:

def set_ignore_buttons(self, ignore):
        assert isinstance(ignore, type(False))
        if (self._ignore_buttons != ignore): #if ignore state changes..
            self._ignore_buttons = ignore #set new state
            if (not self._is_zoomed_out): #if in session/clip view..
                if ignore: #disable clip slots on ignore
                    for scene_index in range(5):
                        scene = self._session.scene(scene_index)
                        for track_index in range(8):
                            clip_slot = scene.clip_slot(track_index)
                            clip_slot.set_enabled(False)                          
                else: #re-enable clip slots on ignore
                    for scene_index in range(5):
                        scene = self._session.scene(scene_index)
                        for track_index in range(8):
                            clip_slot = scene.clip_slot(track_index)
                            clip_slot.set_enabled(True)                          
                #self._session.set_enabled((not ignore)) 

            self.update()


Now that the clip slots are disabled, we can re-map the matrix buttons to MIDI notes. We’ll do this in the _on_note_mode_changed method of the ShiftableSelectorComponent class. We’ll use a note layout which is similar to the APC20’s Note Mode layout, and to the Launchpad’s User 1 mode layout – both of which are based on split rows. Each half row will ascend in sets of 4 notes, which works well with the typical Live Rack (also 4 notes wide). Here's a map of our new Note Mode layout (together with the other mods in our script):


In order to avoid conflict with the APC40 assignments, we’ll send all of our Note Mode notes out on channel 10 (the APC40 uses channels 1 through 8 - and possibly 9, according to the original note map). To map the notes and to change the channels, we’ll use the Framework set_channel and set_identifier methods. We’ll also use the Framework button.send_value method to create different colour patterns for the left and right sides of the grid – so that our note mapping is visually obvious. Here’s the code:

def _on_note_mode_changed(self):
        if not self._master_button != None:
            raise AssertionError
        if self.is_enabled() and self._invert_assignment == self._toggle_pressed:
            if self._note_mode_active:
                self._master_button.turn_on()
                for scene_index in range(5):
                    #TODO: re-map scene_launch buttons to note velocity...
                    scene = self._session.scene(scene_index)
                    for track_index in range(8):
                        clip_slot = scene.clip_slot(track_index)
                        button = self._matrix.get_button(track_index, scene_index)
                        clip_slot.set_launch_button(None)                        
                        button.set_enabled(False)
                        button.set_channel(9) #remap all Note Mode notes to channel 10
                        if track_index < 4:
                            button.set_identifier(52 - (4 * scene_index) + track_index) #top row of left group (first 4 columns) notes 52 to 55
                            if (track_index % 2 == 0 and scene_index % 2 != 0) or (track_index % 2 != 0 and scene_index % 2 == 0):
                                button.send_value(1) #0=off, 1=green, 2=green blink, 3=red, 4=red blink, 5=yellow, 6=yellow blink, 7-127=green
                            else:
                                button.send_value(5)
                        else:
                            button.set_identifier(72 - (4 * scene_index) + (track_index -4)) #top row of right group (next 4 columns) notes 72 to 75
                            if (track_index % 2 == 0 and scene_index % 2 != 0) or (track_index % 2 != 0 and scene_index % 2 == 0):
                                button.send_value(1) #0=off, 1=green, 2=green blink, 3=red, 4=red blink, 5=yellow, 6=yellow blink, 7-127=green
                            else:
                                button.send_value(3)
                self._rebuild_callback()
            else:
                self._master_button.turn_off()
        return None

Of course, we need to turn the lights off and re-enable the clip slots when Note Mode is switched back off. We’ll do that in the _master_value method (which is also where the APC20 Sysex Note Mode string normally gets queued-up):

def _master_value(self, value): #this is the master_button value_listener, i.e. called when the master_button is pressed
        if not self._master_button != None:
            raise AssertionError
        if not value in range(128):
            raise AssertionError
        if self.is_enabled() and self._invert_assignment == self._toggle_pressed:
            if not self._master_button.is_momentary() or value > 0: #if the master button is pressed:
                #for button in self._select_buttons: #turn off track select buttons (only needed for APC20)
                    #button.turn_off()
                self._matrix.reset() #turn off the clip launch grid LEDs
                #mode_byte = NOTE_MODE #= 67 for APC20 Note Mode, send as part of sysex string to enable Note Mode
                if self._note_mode_active: #if note mode is already on, turn it off:
                    #mode_byte = ABLETON_MODE #= 65 for APC40 Ableton Mode 1
                    for scene_index in range(5):
                        scene = self._session.scene(scene_index)
                        for track_index in range(8):
                            clip_slot = scene.clip_slot(track_index)
                            button = self._matrix.get_button(track_index, scene_index)
                            clip_slot.set_launch_button(button)
                            button.set_enabled(True)
                            button.turn_off()
                    self._rebuild_callback()
                #self._mode_callback(mode_byte) #send sysex to set Mode (NOTE_MODE or ABLETON_MODE)
                self._note_mode_active = not self._note_mode_active
                self._zooming.set_ignore_buttons(self._note_mode_active) #turn off matrix, scene launch, and clip stop buttons when in Note Mode
                #self._transport.update() #only needed for APC20
                self._on_note_mode_changed()
        return None

So now we have a working Note Mode on the APC40 (don’t forget that we do have to enable the MIDI Input Track switch in Live's MIDI preferences dialog, so that our notes get passed along to the track). Not too difficult, was it? Of course, I wouldn’t expect to see official support for Note Mode on the APC40 from Ableton or Akai anytime soon, if only because thousands of units are already out there, without any red stenciling to tell users which button to press to enable Note Mode – and they might get confused. Well, that, and it might hurt sales of the APC20.

Undo/Redo

Now, before we sign off, we’ll take a minute and re-map the Tempo Nudge buttons to Undo and Redo. It just so happens that the OpenLabs scripts have a nice SpecialTransportComponent module, which provides methods for setting Undo, Redo, and Back to Start (BTS) buttons; we’ll copy the required code over to our equivalent ShiftableTransportComponent module. Here is the Undo code (Redo is almost identical):

def set_undo_button(self, undo_button):
        assert isinstance(undo_button, (ButtonElement,
                                        type(None)))
        if (undo_button != self._undo_button):
            if (self._undo_button != None):
                self._undo_button.remove_value_listener(self._undo_value)
            self._undo_button = undo_button
            if (self._undo_button != None):
                self._undo_button.add_value_listener(self._undo_value)
            self.update()

And the callback (featuring some more LiveAPI calls):

def _undo_value(self, value):
        assert (self._undo_button != None)
        assert (value in range(128))
        if self.is_enabled():
            if ((value != 0) or (not self._undo_button.is_momentary())):
                if self.song().can_undo:
                    self.song().undo()

Conclusion

Well, that’s it for now. We’ve shown that implementing Note Mode on the APC40 is relatively straight-forward, and does not in fact require a firmware update. We’ve also demonstrated that much can be accomplished simply by extending Framework-based scripts and subclasses. Our custom tweaks and mappings do not require templates or add-ons, and best part is that they are free – free as in free beer! Cheers.

Hanz Petrov
May 13, 2010

hanz.petrov
at gmail.com

PostScript: It would seem that many (if not all) of the Live MIDI remote scripts, for all of the various control surfaces, were coded by Jan B. over at Ableton. His code is a pleasure to work with and fun to learn from - my appreciation goes out to Jan and to the entire Ableton team.

19 comments:

Juan Segovia said...

Dude this has got to be one of the best explained tutorials out there. Many thanks for all your work in figuring all this stuff out!

Keep up the good work!

Juan

Anonymous said...

Man ! :) This is what i have been looking for! Access to the live api without the buggy max :) Now i can really tweak my apc as i want it :) Thanks again!

b szczesny said...

You are great. I want to learn programming right now!! :)

Anonymous said...

thank you very much for this useful information you provided over these 3 articles!

i am trying to change the "track control" section to a "device control" section, so that you have 2 "blue hands"

one problem, that could happen, would be that live only supports one blue hand per control surface, so i will have to get deep into the code and look if its possible

nevertheless if you had the same idea, plz post your opinions/findings. i ll keep you informed if i make any progress (if its possible at all)

Hanz Petrov said...

>Anonymous said...
>i am trying to change the "track control" section to a "device control" section, so that you have 2 "blue hands"

Already done - have a look at the Support Files page. This feature is included with the APC40_22 scripts.

Anonymous said...

nice, thanks for pointing me to it, have to read more carefully next time :)

keep up the good work!!!

Anonymous said...

Thank you very nice.

I am trying to call a dll from Live API is it possible?

interrupt said...

Hey Hanz, I really appreciate that you have shared your work! Thank you so much! I have set up pydev in eclipse and built a simple debugging class for using with the embedded pyton in live. I am getting to work on my own controller extension ideas, which I have a prototype of in Max for Live. (thanks to you) So, I decompiled the MIDI Remote Script that was created for the NI Maschine at depython.com and got almost all of all the pyc files reversed. But some of the files had simple variable bindings with no right side values. This threw errors into the log file, so I set them to zero, False, or None, based on what seemed correct from the context. To no avail, the newly reversed scripts didn't work. I wonder if you have any ideas about this.

p.s. http://soundcloud.com/misterinterrupt cheers!

Hanz Petrov said...

Hey interrupt, sounds like an interesting project. All script errors get logged to the log.txt file:
http://www.ableton.com/pages/faq/log_files
Once identified, the errors need to corrected one by one. Unpyc might help with stubborn pycs. You could also try depython.net for files under 5kb.
I was not aware that there was a Maschine script; if you could email the files to me, I'd love to take a look.
Best,
Hanz

PS: It has been my experience that with depython.com the missing values are almost always "None"

Quirksmode said...

This is great stuff, I am currently trying to get my head round it all and keep hitting brickwalls.

Is there an easy way to assign the cue level dial to the red box so twisting it makes it scroll up and down. I have tried and failed many times so far and think it would be a really nice way to browse through my tunes. My ideal set up would be red box scroll as primary and tempo as secondary.

Any help is so appreciated :-)

Anonymous said...

Hi Hanz,

I'm a recent user of this script, great changes that you can bring to the APC's behaviour is what
all dj's dreamed about when we were still turntablists.

I've got two questions related to the scripts themself.
I'm unfortunatly not a programmer but by messing around with the python language in the apcadvanced,
i quickly figured the logic and the simplicity of the code.

My first question is about the matrix led color attribution i mode 1.
the solution to be found is i guess in the "Matrix_Maps.PY" file but i still didn't find where exactly.

The second question i found this: http://forum.ableton.com/viewtopic.php?f=4&t=154242&hilit=vcm+600the
There's obviously a way to adapt this script on top of apc advanced.
But once's again i don't know where and how...

Please masta enlighten me!;-)

Cheers Nico

Bas said...

Hi Hanz,

Seeing as I can't find a direct email address, I'll post it here:

I have a request for a (I assume fairly simple) custom script. Been trying to build it myself, haven't been able to complete it as Python is not my strongest point.

Would you be available to code it, for a reasonable compensation? If so, contact me at bas@basvermolen.com

Thanks a lot.

Bas

Anonymous said...

Hi Hanz!

Thank you very much for examining all this for us! These articles have inspired me a lot!
At the moment I am playing around with the scripts trying to create a custom APC-script.
My aim is to get rid of features i don't need, so i can use them for other purposes.
most things seem pretty easy to accomplish thanks to your tutorial-like articles.
But for me there is one big question left: Is it possible to map the cutoff-frequency of the Track-EQs to the apc's track control pots (pan, sends, etc.) via a script?
my disired mapping is as follows:
Bank1:
The upper row of pots is supposed to control the lowcut-cutoff-frequency of track-Eqs of tracks 1-4.
The lower row of pots is supposed to control the highcut-cuttoff-frequency track-Eqs of tracks 1-4.
Bank2:
same as Bank1 but for tracks 5-8.

Is that even possible? I already figured out that i reach the TrackEQComponents using the mixer object (track_filter(self, index)). But how can i address the parameters and assign a control to it?

if you find some spare time to answer please write me to "atlantik.post att googlemail.com".

thx again for this great work!
Clemens

Matt F said...

Hi Hanz,

Thank you for the very thorough explanation of RMS's. I am no programmer and have read through your tutorial very closely a few times trying to figure this whole python thing out. however, when i select my "controller" in the midi prefs in ableton it does not compile my .py files. They do not become .pyc files in my directory and in the log file i get a message that no such module exists. I am using live 8.2.2 and Mac OS X 10.6.8. any help would be much appreciated.

Anonymous said...

im having a similar problem, i load fc1020 in ableton in midi, but i dont see any red boxes..

Anonymous said...

Hi Hanz,

Just want to say thanks for the great work! Much appreciated!

Anonymous said...

Hanz,
i am colorblind(red/green) & I LOVE YOU, I LOVE YOU, I LOVE U!

I am a developer on my own, and so i know what effort it was to build this tutorial.

This tutorial is precise, correct and at the point.

THANK YOU 4 SAVING MY LIFE! Thank you for saving me lots of time and thanks for sharing topics where i would never have been imagined of.

GJ I will contact you for donation.

Anonymous said...

Track Control Modes are not working properly in the Live 9 revision.

june74 said...

@Anonymous

Could you be more specific on what the issue is with Track Control modes?

In my opinion in the Live9 version it does what it should be doing.

Cheers / Fabrizio