13
0
livetrax/libs/surfaces/mackie/surface.cc
Paul Davis 6d46299df3 MCP: another patch from rodrigo:
* setting a (arbitraty) limit to zoom out to prevent segfaults because out of memory condition;
    * setting initial update of master fader, and read, play and stop leds on the Mackie;
    * changed the timecode display char selection for update algorithm as chars are sent one by one and not all right most;
    * implemented method of showing timecode at the mackie to better deal with the differences between Ardour's foramts and Mackie's, i.e, use spaces in place of the zeros that had no meaning;
    * preventing timecode display updates when the surface isn't yet active.


git-svn-id: svn://localhost/ardour2/branches/3.0@12541 d708f5d6-7413-0410-9779-e7cbd77b26cf
2012-06-02 14:51:53 +00:00

869 lines
22 KiB
C++

#include <sstream>
#include <iomanip>
#include <iostream>
#include <cstdio>
#include <cmath>
#include "midi++/port.h"
#include "midi++/manager.h"
#include "ardour/automation_control.h"
#include "ardour/debug.h"
#include "ardour/route.h"
#include "ardour/panner.h"
#include "ardour/panner_shell.h"
#include "ardour/rc_configuration.h"
#include "ardour/session.h"
#include "ardour/utils.h"
#include "control_group.h"
#include "surface_port.h"
#include "surface.h"
#include "strip.h"
#include "mackie_control_protocol.h"
#include "jog_wheel.h"
#include "strip.h"
#include "button.h"
#include "led.h"
#include "pot.h"
#include "fader.h"
#include "jog.h"
#include "meter.h"
#include "i18n.h"
using namespace std;
using namespace PBD;
using namespace Mackie;
using ARDOUR::Route;
using ARDOUR::Panner;
using ARDOUR::Pannable;
using ARDOUR::AutomationControl;
#define ui_context() MackieControlProtocol::instance() /* a UICallback-derived object that specifies the event loop for signal handling */
// The MCU sysex header.4th byte Will be overwritten
// when we get an incoming sysex that identifies
// the device type
static MidiByteArray mackie_sysex_hdr (5, MIDI::sysex, 0x0, 0x0, 0x66, 0x14);
// The MCU extender sysex header.4th byte Will be overwritten
// when we get an incoming sysex that identifies
// the device type
static MidiByteArray mackie_sysex_hdr_xt (5, MIDI::sysex, 0x0, 0x0, 0x66, 0x15);
static MidiByteArray empty_midi_byte_array;
Surface::Surface (MackieControlProtocol& mcp, const std::string& device_name, uint32_t number, surface_type_t stype)
: _mcp (mcp)
, _stype (stype)
, _number (number)
, _name (device_name)
, _active (false)
, _connected (false)
, _jog_wheel (0)
{
DEBUG_TRACE (DEBUG::MackieControl, "Surface::init\n");
_port = new SurfacePort (*this);
/* only the first Surface object has global controls */
if (_number == 0) {
if (_mcp.device_info().has_global_controls()) {
init_controls ();
}
if (_mcp.device_info().has_master_fader()) {
setup_master ();
}
}
uint32_t n = _mcp.device_info().strip_cnt();
if (n) {
init_strips (n);
}
connect_to_signals ();
DEBUG_TRACE (DEBUG::MackieControl, "Surface::init finish\n");
}
Surface::~Surface ()
{
DEBUG_TRACE (DEBUG::MackieControl, "Surface: destructor\n");
zero_all ();
// delete groups
for (Groups::iterator it = groups.begin(); it != groups.end(); ++it) {
delete it->second;
}
// delete controls
for (Controls::iterator it = controls.begin(); it != controls.end(); ++it) {
delete *it;
}
delete _jog_wheel;
delete _port;
}
const MidiByteArray&
Surface::sysex_hdr() const
{
switch (_stype) {
case mcu: return mackie_sysex_hdr;
case ext: return mackie_sysex_hdr_xt;
}
cout << "SurfacePort::sysex_hdr _port_type not known" << endl;
return mackie_sysex_hdr;
}
static GlobalControlDefinition mackie_global_controls[] = {
{ "external", Pot::External, Pot::factory, "none" },
{ "fader_touch", Led::FaderTouch, Led::factory, "master" },
{ "timecode", Led::Timecode, Led::factory, "none" },
{ "beats", Led::Beats, Led::factory, "none" },
{ "solo", Led::RudeSolo, Led::factory, "none" },
{ "relay_click", Led::RelayClick, Led::factory, "none" },
{ "", 0, Led::factory, "" }
};
void
Surface::init_controls()
{
Group* group;
groups["assignment"] = new Group ("assignment");
groups["automation"] = new Group ("automation");
groups["bank"] = new Group ("bank");
groups["cursor"] = new Group ("cursor");
groups["display"] = new Group ("display");
groups["functions"] = new Group ("functions");
groups["modifiers"] = new Group ("modifiers");
groups["none"] = new Group ("none");
groups["transport"] = new Group ("transport");
groups["user"] = new Group ("user");
groups["master"] = new Group ("master");
groups["view"] = new Group ("view");
if (_mcp.device_info().has_jog_wheel()) {
_jog_wheel = new Mackie::JogWheel (_mcp);
}
for (uint32_t n = 0; mackie_global_controls[n].name[0]; ++n) {
group = groups[mackie_global_controls[n].group_name];
Control* control = mackie_global_controls[n].factory (*this, mackie_global_controls[n].id, mackie_global_controls[n].name, *group);
controls_by_device_independent_id[mackie_global_controls[n].id] = control;
}
/* add global buttons */
const map<Button::ID,GlobalButtonInfo>& global_buttons (_mcp.device_info().global_buttons());
for (map<Button::ID,GlobalButtonInfo>::const_iterator b = global_buttons.begin(); b != global_buttons.end(); ++b){
group = groups[b->second.group];
controls_by_device_independent_id[b->first] = Button::factory (*this, b->first, b->second.id, b->second.label, *group);
}
}
void
Surface::init_strips (uint32_t n)
{
const map<Button::ID,StripButtonInfo>& strip_buttons (_mcp.device_info().strip_buttons());
for (uint32_t i = 0; i < n; ++i) {
char name[32];
snprintf (name, sizeof (name), "strip_%d", (8* _number) + i);
Strip* strip = new Strip (*this, name, i, strip_buttons);
groups[name] = strip;
strips.push_back (strip);
}
}
void
Surface::setup_master ()
{
_master_fader = dynamic_cast<Fader*> (Fader::factory (*this, 8, "master", *groups["master"]));
boost::shared_ptr<Route> m;
if ((m = _mcp.get_session().monitor_out()) == 0) {
m = _mcp.get_session().master_out();
}
if (!m) {
return;
}
_master_fader->set_control (m->gain_control());
m->gain_control()->Changed.connect (*this, MISSING_INVALIDATOR, boost::bind (&Surface::master_gain_changed, this), ui_context());
}
void
Surface::master_gain_changed ()
{
boost::shared_ptr<AutomationControl> ac = _master_fader->control();
float pos = ac->internal_to_interface (ac->get_value());
_port->write (_master_fader->set_position (pos));
}
float
Surface::scaled_delta (float delta, float current_speed)
{
/* XXX needs work before use */
const float sign = delta < 0.0 ? -1.0 : 1.0;
return ((sign * std::pow (delta + 1.0, 2.0)) + current_speed) / 100.0;
}
void
Surface::display_bank_start (uint32_t current_bank)
{
if (current_bank == 0) {
// send Ar. to 2-char display on the master
show_two_char_display ("Ar", "..");
} else {
// write the current first remote_id to the 2-char display
show_two_char_display (current_bank);
}
}
void
Surface::blank_jog_ring ()
{
Control* control = controls_by_device_independent_id[Jog::ID];
if (control) {
Pot* pot = dynamic_cast<Pot*> (control);
if (pot) {
_port->write (pot->set (0.0, false, Pot::spread));
}
}
}
float
Surface::scrub_scaling_factor () const
{
return 100.0;
}
void
Surface::connect_to_signals ()
{
if (!_connected) {
DEBUG_TRACE (DEBUG::MackieControl, string_compose ("Surface %1 connecting to signals on port %2\n",
number(), _port->input_port().name()));
MIDI::Parser* p = _port->input_port().parser();
/* Incoming sysex */
p->sysex.connect_same_thread (*this, boost::bind (&Surface::handle_midi_sysex, this, _1, _2, _3));
/* V-Pot messages are Controller */
p->controller.connect_same_thread (*this, boost::bind (&Surface::handle_midi_controller_message, this, _1, _2));
/* Button messages are NoteOn */
p->note_on.connect_same_thread (*this, boost::bind (&Surface::handle_midi_note_on_message, this, _1, _2));
/* Button messages are NoteOn. libmidi++ sends note-on w/velocity = 0 as note-off so catch them too */
p->note_off.connect_same_thread (*this, boost::bind (&Surface::handle_midi_note_on_message, this, _1, _2));
/* Fader messages are Pitchbend */
p->channel_pitchbend[0].connect_same_thread (*this, boost::bind (&Surface::handle_midi_pitchbend_message, this, _1, _2, 0U));
p->channel_pitchbend[1].connect_same_thread (*this, boost::bind (&Surface::handle_midi_pitchbend_message, this, _1, _2, 1U));
p->channel_pitchbend[2].connect_same_thread (*this, boost::bind (&Surface::handle_midi_pitchbend_message, this, _1, _2, 2U));
p->channel_pitchbend[3].connect_same_thread (*this, boost::bind (&Surface::handle_midi_pitchbend_message, this, _1, _2, 3U));
p->channel_pitchbend[4].connect_same_thread (*this, boost::bind (&Surface::handle_midi_pitchbend_message, this, _1, _2, 4U));
p->channel_pitchbend[5].connect_same_thread (*this, boost::bind (&Surface::handle_midi_pitchbend_message, this, _1, _2, 5U));
p->channel_pitchbend[6].connect_same_thread (*this, boost::bind (&Surface::handle_midi_pitchbend_message, this, _1, _2, 6U));
p->channel_pitchbend[7].connect_same_thread (*this, boost::bind (&Surface::handle_midi_pitchbend_message, this, _1, _2, 7U));
_connected = true;
}
}
void
Surface::handle_midi_pitchbend_message (MIDI::Parser&, MIDI::pitchbend_t pb, uint32_t fader_id)
{
/* Pitchbend messages are fader messages. Nothing in the data we get
* from the MIDI::Parser conveys the fader ID, which was given by the
* channel ID in the status byte.
*
* Instead, we have used bind() to supply the fader-within-strip ID
* when we connected to the per-channel pitchbend events.
*/
DEBUG_TRACE (DEBUG::MackieControl, string_compose ("handle_midi pitchbend on port %3, fader = %1 value = %2\n",
fader_id, pb, _number));
if (_mcp.device_info().no_handshake()) {
turn_it_on ();
}
Fader* fader = faders[fader_id];
if (fader) {
Strip* strip = dynamic_cast<Strip*> (&fader->group());
float pos = (pb >> 4)/1023.0; // only the top 10 bytes are used
if (strip) {
strip->handle_fader (*fader, pos);
} else {
/* master fader */
fader->set_value (pos); // alter master gain
_port->write (fader->set_position (pos)); // write back value (required for servo)
}
} else {
DEBUG_TRACE (DEBUG::MackieControl, "fader not found\n");
}
}
void
Surface::handle_midi_note_on_message (MIDI::Parser &, MIDI::EventTwoBytes* ev)
{
DEBUG_TRACE (DEBUG::MackieControl, string_compose ("SurfacePort::handle_note_on %1 = %2\n", (int) ev->note_number, (int) ev->velocity));
if (_mcp.device_info().no_handshake()) {
turn_it_on ();
}
Button* button = buttons[ev->note_number];
if (button) {
Strip* strip = dynamic_cast<Strip*> (&button->group());
if (strip) {
DEBUG_TRACE (DEBUG::MackieControl, string_compose ("strip %1 button %2 pressed ? %3\n",
strip->index(), button->name(), (ev->velocity > 64)));
strip->handle_button (*button, ev->velocity > 64 ? press : release);
} else {
/* global button */
DEBUG_TRACE (DEBUG::MackieControl, string_compose ("global button %1\n", button->id()));
_mcp.handle_button_event (*this, *button, ev->velocity > 64 ? press : release);
}
} else {
DEBUG_TRACE (DEBUG::MackieControl, string_compose ("no button found for %1\n", ev->note_number));
}
}
void
Surface::handle_midi_controller_message (MIDI::Parser &, MIDI::EventTwoBytes* ev)
{
DEBUG_TRACE (DEBUG::MackieControl, string_compose ("SurfacePort::handle_midi_controller %1 = %2\n", (int) ev->controller_number, (int) ev->value));
if (_mcp.device_info().no_handshake()) {
turn_it_on ();
}
Pot* pot = pots[ev->controller_number];
// bit 6 gives the sign
float sign = (ev->value & 0x40) == 0 ? 1.0 : -1.0;
// bits 0..5 give the velocity. we interpret this as "ticks
// moved before this message was sent"
float ticks = (ev->value & 0x3f);
if (ticks == 0) {
/* euphonix and perhaps other devices send zero
when they mean 1, we think.
*/
ticks = 1;
}
float delta = sign * (ticks / (float) 0x3f);
if (!pot) {
if (ev->controller_number == Jog::ID && _jog_wheel) {
DEBUG_TRACE (DEBUG::MackieControl, string_compose ("Jog wheel moved %1\n", ticks));
_jog_wheel->jog_event (delta);
return;
}
return;
}
Strip* strip = dynamic_cast<Strip*> (&pot->group());
if (strip) {
strip->handle_pot (*pot, delta);
}
}
void
Surface::handle_midi_sysex (MIDI::Parser &, MIDI::byte * raw_bytes, size_t count)
{
MidiByteArray bytes (count, raw_bytes);
DEBUG_TRACE (DEBUG::MackieControl, string_compose ("handle_midi_sysex: %1\n", bytes));
if (_mcp.device_info().no_handshake()) {
turn_it_on ();
}
/* always save the device type ID so that our outgoing sysex messages
* are correct
*/
if (_stype == mcu) {
mackie_sysex_hdr[4] = bytes[4];
} else {
mackie_sysex_hdr_xt[4] = bytes[4];
}
switch (bytes[5]) {
case 0x01:
/* MCP: Device Ready
LCP: Connection Challenge
*/
if (bytes[4] == 0x10 || bytes[4] == 0x11) {
write_sysex (host_connection_query (bytes));
} else {
if (!_active) {
turn_it_on ();
}
}
break;
case 0x03: /* LCP Connection Confirmation */
if (bytes[4] == 0x10 || bytes[4] == 0x11) {
write_sysex (host_connection_confirmation (bytes));
_active = true;
}
break;
case 0x04: /* LCP: Confirmation Denied */
_active = false;
break;
default:
error << "MCP: unknown sysex: " << bytes << endmsg;
}
}
static MidiByteArray
calculate_challenge_response (MidiByteArray::iterator begin, MidiByteArray::iterator end)
{
MidiByteArray l;
back_insert_iterator<MidiByteArray> back (l);
copy (begin, end, back);
MidiByteArray retval;
// this is how to calculate the response to the challenge.
// from the Logic docs.
retval << (0x7f & (l[0] + (l[1] ^ 0xa) - l[3]));
retval << (0x7f & ( (l[2] >> l[3]) ^ (l[0] + l[3])));
retval << (0x7f & ((l[3] - (l[2] << 2)) ^ (l[0] | l[1])));
retval << (0x7f & (l[1] - l[2] + (0xf0 ^ (l[3] << 4))));
return retval;
}
// not used right now
MidiByteArray
Surface::host_connection_query (MidiByteArray & bytes)
{
MidiByteArray response;
if (bytes[4] != 0x10 && bytes[4] != 0x11) {
/* not a Logic Control device - no response required */
return response;
}
// handle host connection query
DEBUG_TRACE (DEBUG::MackieControl, string_compose ("host connection query: %1\n", bytes));
if (bytes.size() != 18) {
cerr << "expecting 18 bytes, read " << bytes << " from " << _port->input_port().name() << endl;
return response;
}
// build and send host connection reply
response << 0x02;
copy (bytes.begin() + 6, bytes.begin() + 6 + 7, back_inserter (response));
response << calculate_challenge_response (bytes.begin() + 6 + 7, bytes.begin() + 6 + 7 + 4);
return response;
}
// not used right now
MidiByteArray
Surface::host_connection_confirmation (const MidiByteArray & bytes)
{
DEBUG_TRACE (DEBUG::MackieControl, string_compose ("host_connection_confirmation: %1\n", bytes));
// decode host connection confirmation
if (bytes.size() != 14) {
ostringstream os;
os << "expecting 14 bytes, read " << bytes << " from " << _port->input_port().name();
throw MackieControlException (os.str());
}
// send version request
return MidiByteArray (2, 0x13, 0x00);
}
void
Surface::turn_it_on ()
{
if (!_active) {
_active = true;
zero_controls ();
for (Strips::iterator s = strips.begin(); s != strips.end(); ++s) {
(*s)->notify_all ();
}
update_view_mode_display ();
if (_mcp.device_info ().has_master_fader ()) {
master_gain_changed ();
}
if (_mcp.device_info ().has_global_controls ()) {
_mcp.update_global_button (Button::Read, _mcp.metering_active ());
_mcp.update_global_button (Button::Stop, _mcp.get_session ().transport_stopped ());
_mcp.update_global_button (Button::Play, (_mcp.get_session ().transport_speed () == 1.0f));
}
}
}
void
Surface::handle_port_inactive (SurfacePort*)
{
_active = false;
}
void
Surface::write_sysex (const MidiByteArray & mba)
{
if (mba.empty()) {
return;
}
MidiByteArray buf;
buf << sysex_hdr() << mba << MIDI::eox;
_port->write (buf);
}
void
Surface::write_sysex (MIDI::byte msg)
{
MidiByteArray buf;
buf << sysex_hdr() << msg << MIDI::eox;
_port->write (buf);
}
uint32_t
Surface::n_strips (bool with_locked_strips) const
{
if (with_locked_strips) {
return strips.size();
}
uint32_t n = 0;
for (Strips::const_iterator it = strips.begin(); it != strips.end(); ++it) {
if (!(*it)->locked()) {
++n;
}
}
return n;
}
Strip*
Surface::nth_strip (uint32_t n) const
{
if (n > n_strips()) {
return 0;
}
return strips[n];
}
void
Surface::zero_all ()
{
if (_mcp.device_info().has_timecode_display ()) {
display_timecode (string (10, '0'), string (10, ' '));
}
if (_mcp.device_info().has_two_character_display()) {
show_two_char_display (string (2, '0'), string (2, ' '));
}
if (_mcp.device_info().has_master_fader ()) {
_port->write (_master_fader->zero ());
}
// zero all strips
for (Strips::iterator it = strips.begin(); it != strips.end(); ++it) {
(*it)->zero();
}
zero_controls ();
}
void
Surface::zero_controls ()
{
if (_stype != mcu || !_mcp.device_info().has_global_controls()) {
return;
}
// turn off global buttons and leds
// global buttons are only ever on mcu_port, so we don't have
// to figure out which port.
for (Controls::iterator it = controls.begin(); it != controls.end(); ++it) {
Control & control = **it;
if (!control.group().is_strip()) {
_port->write (control.zero());
}
}
// and the led ring for the master strip
blank_jog_ring ();
}
void
Surface::periodic (uint64_t now_usecs)
{
for (Strips::iterator s = strips.begin(); s != strips.end(); ++s) {
(*s)->periodic (now_usecs);
}
}
void
Surface::write (const MidiByteArray& data)
{
if (_active) {
_port->write (data);
}
}
void
Surface::map_routes (const vector<boost::shared_ptr<Route> >& routes)
{
vector<boost::shared_ptr<Route> >::const_iterator r;
Strips::iterator s = strips.begin();
for (r = routes.begin(); r != routes.end() && s != strips.end(); ++s) {
/* don't try to assign routes to a locked strip. it won't
use it anyway, but if we do, then we get out of sync
with the proposed mapping.
*/
if (!(*s)->locked()) {
(*s)->set_route (*r);
++r;
}
}
for (; s != strips.end(); ++s) {
(*s)->set_route (boost::shared_ptr<Route>());
}
}
static char
translate_seven_segment (char achar)
{
achar = toupper (achar);
if (achar >= 0x40 && achar <= 0x60) {
return achar - 0x40;
} else if (achar >= 0x21 && achar <= 0x3f) {
return achar;
} else {
return 0x00;
}
}
void
Surface::show_two_char_display (const std::string & msg, const std::string & dots)
{
if (_stype != mcu || !_mcp.device_info().has_two_character_display() || msg.length() != 2 || dots.length() != 2) {
return;
}
MidiByteArray right (3, 0xb0, 0x4b, 0x00);
MidiByteArray left (3, 0xb0, 0x4a, 0x00);
right[2] = translate_seven_segment (msg[0]) + (dots[0] == '.' ? 0x40 : 0x00);
left[2] = translate_seven_segment (msg[1]) + (dots[1] == '.' ? 0x40 : 0x00);
_port->write (right);
_port->write (left);
}
void
Surface::show_two_char_display (unsigned int value, const std::string & /*dots*/)
{
ostringstream os;
os << setfill('0') << setw(2) << value % 100;
show_two_char_display (os.str());
}
void
Surface::display_timecode (const std::string & timecode, const std::string & last_timecode)
{
if (!_active || !_mcp.device_info().has_timecode_display()) {
return;
}
// if there's no change, send nothing, not even sysex header
if (timecode == last_timecode) return;
// length sanity checking
string local_timecode = timecode;
// truncate to 10 characters
if (local_timecode.length() > 10) {
local_timecode = local_timecode.substr (0, 10);
}
// pad to 10 characters
while (local_timecode.length() < 10) {
local_timecode += " ";
}
// translate characters.
// Only the characters that actually changed are sent.
int position = 0x3f;
int i;
for (i = local_timecode.length () - 1; i >= 0; i--) {
position++;
if (local_timecode[i] == last_timecode[i]) {
continue;
}
MidiByteArray retval (2, 0xb0, position);
retval << translate_seven_segment (local_timecode[i]);
_port->write (retval);
}
}
void
Surface::update_flip_mode_display ()
{
for (Strips::iterator s = strips.begin(); s != strips.end(); ++s) {
(*s)->flip_mode_changed (true);
}
}
void
Surface::update_view_mode_display ()
{
string text;
int id = -1;
if (!_active) {
return;
}
switch (_mcp.view_mode()) {
case MackieControlProtocol::Mixer:
show_two_char_display ("Mx");
id = Button::Pan;
break;
case MackieControlProtocol::Dynamics:
show_two_char_display ("Dy");
id = Button::Dyn;
break;
case MackieControlProtocol::EQ:
show_two_char_display ("EQ");
id = Button::Eq;
break;
case MackieControlProtocol::Loop:
show_two_char_display ("LP");
id = Button::Loop;
break;
case MackieControlProtocol::AudioTracks:
show_two_char_display ("AT");
break;
case MackieControlProtocol::MidiTracks:
show_two_char_display ("MT");
break;
case MackieControlProtocol::Sends:
show_two_char_display ("Sn");
id = Button::Sends;
break;
case MackieControlProtocol::Plugins:
show_two_char_display ("Pl");
id = Button::Plugin;
break;
default:
break;
}
if (id >= 0) {
/* we are attempting to turn a global button/LED on */
map<int,Control*>::iterator x = controls_by_device_independent_id.find (id);
if (x != controls_by_device_independent_id.end()) {
Button* button = dynamic_cast<Button*> (x->second);
if (button) {
_port->write (button->set_state (on));
}
}
}
if (!text.empty()) {
for (Strips::iterator s = strips.begin(); s != strips.end(); ++s) {
_port->write ((*s)->display (1, text));
}
}
}
void
Surface::gui_selection_changed (const ARDOUR::StrongRouteNotificationList& routes)
{
for (Strips::iterator s = strips.begin(); s != strips.end(); ++s) {
(*s)->gui_selection_changed (routes);
}
}
void
Surface::say_hello ()
{
/* wakeup for Mackie Control */
MidiByteArray wakeup (7, MIDI::sysex, 0x00, 0x00, 0x66, 0x14, 0x00, MIDI::eox);
_port->write (wakeup);
wakeup[4] = 0x15; /* wakup Mackie XT */
_port->write (wakeup);
wakeup[4] = 0x10; /* wakupe Logic Control */
_port->write (wakeup);
wakeup[4] = 0x11; /* wakeup Logic Control XT */
_port->write (wakeup);
}
void
Surface::next_jog_mode ()
{
}
void
Surface::set_jog_mode (JogWheel::Mode)
{
}
bool
Surface::route_is_locked_to_strip (boost::shared_ptr<Route> r) const
{
for (Strips::const_iterator s = strips.begin(); s != strips.end(); ++s) {
if ((*s)->route() == r && (*s)->locked()) {
return true;
}
}
return false;
}
void
Surface::notify_metering_state_changed()
{
for (Strips::const_iterator s = strips.begin(); s != strips.end(); ++s) {
(*s)->notify_metering_state_changed ();
}
}