/*** This file is part of gnome-speaker-setup. Copyright 2009 Lennart Poettering gnome-speaker-setup is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. gnome-speaker-setup is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with gnome-speaker-setup; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. ***/ using Gtk; using PulseAudio; using Gee; using Canberra; int64 default_card_index; int64 default_device_index; bool lock_selection; public class BoldLabel : Label { public BoldLabel(string? text = null) { if (text != null) set_markup("%s".printf(text)); set_alignment(0, 0.5f); } } public class FieldLabel : Label { public FieldLabel(string? text = null) { if (text != null) set_markup_with_mnemonic(text); set_alignment(0, 0.5f); } } public class ChannelControl : VBox { private Label label; private Image image; private Button test_button; private ChannelPosition position; private Canberra.Context *canberra; private bool playing; public ChannelControl(Canberra.Context? canberra, ChannelPosition p) { position = p; set_spacing(6); image = new Image.from_icon_name("audio-volume-medium", IconSize.DIALOG); pack_start(image, false, false, 0); label = new Label(pretty_position()); pack_start(label, false, false, 0); test_button = new Button.with_label("Test"); test_button.clicked += on_test_button_clicked; Box box = new HBox(false, 0); box.pack_start(test_button, true, false, 0); pack_start(box, false, false, 0); this.canberra = canberra; } public void on_test_button_clicked() { Canberra.Proplist p; canberra->cancel(1); if (playing) playing = false; else { Canberra.Proplist.create(out p); p.sets(PROP_MEDIA_ROLE, "test"); p.sets(PROP_MEDIA_NAME, pretty_position()); p.sets(PROP_CANBERRA_FORCE_CHANNEL, position.to_string()); p.sets(PROP_CANBERRA_ENABLE, "1"); unowned string? name = sound_name(); if (name != null) { p.sets(PROP_EVENT_ID, name); playing = canberra->play_full(1, p, finish_cb) >= 0; } if (!playing) { p.sets(PROP_EVENT_ID, "audio-test-signal"); playing = canberra->play_full(1, p, finish_cb) >= 0; } if (!playing) { p.sets(PROP_EVENT_ID, "bell-window-system"); playing = canberra->play_full(1, p, finish_cb) >= 0; } } update_button(); } public unowned string pretty_position() { if (position == ChannelPosition.LFE) return "Subwoofer"; return position.to_pretty_string(); } public void update_button() { test_button.set_label(playing ? "Stop" : "Test"); } public void finish_cb(Canberra.Context c, uint32 id, int code) { /* This is called in the background thread, hence * forward to main thread via idle callback */ Idle.add(idle_cb); } public bool idle_cb() { playing = false; update_button(); return false; } public unowned string? sound_name() { switch (position) { case ChannelPosition.FRONT_LEFT: return "audio-channel-front-left"; case ChannelPosition.FRONT_RIGHT: return "audio-channel-front-right"; case ChannelPosition.FRONT_CENTER: return "audio-channel-front-center"; case ChannelPosition.REAR_LEFT: return "audio-channel-rear-left"; case ChannelPosition.REAR_RIGHT: return "audio-channel-rear-right"; case ChannelPosition.REAR_CENTER: return "audio-channel-rear-center"; case ChannelPosition.LFE: return "audio-channel-lfe"; case ChannelPosition.SIDE_LEFT: return "audio-channel-side-left"; case ChannelPosition.SIDE_RIGHT: return "audio-channel-side-right"; default: return null; } } } public class SpeakerSetupWindow : Window { private PulseAudio.Context context; private PulseAudio.GLibMainLoop main_loop; private Canberra.Context canberra; private Label card_title_label; private Label card_label; private Label profile_label; private Alignment device_title_alignment; private Label device_title_label; private Label device_label; private Label port_label; private ComboBox card_combo_box; private ComboBox profile_combo_box; private ComboBox device_combo_box; private ComboBox port_combo_box; private ChannelControl[] channel_controls = new ChannelControl[ChannelPosition.MAX]; private Table channel_table; private Notebook channel_notebook; private Button close_button; private bool suppress_feedback; private bool finished_startup; [Compact] class CardData { public uint32 index; public string description; public int active_profile; public ListStore profiles; public ListStore devices; } [Compact] class DeviceData { public uint32 index; public string name; public string description; public ChannelMap channel_map; public int active_port; public ListStore ports; public CardData* card; } private ListStore unowned_devices; private ListStore cards; private ListStore *current_profiles; private ListStore *current_devices; private ListStore *current_ports; private HashMap device_lookup; private HashMap card_lookup; public SpeakerSetupWindow() { title = "Speaker Setup"; icon_name = "audio-card"; position = WindowPosition.CENTER; border_width = 12; Canberra.Context.create(out canberra); canberra.set_driver("pulse"); canberra.change_props(PROP_APPLICATION_NAME, "Speaker Setup"); canberra.change_props(PROP_APPLICATION_ID, "org.gnome.SpeakerSetup"); canberra.change_props(PROP_APPLICATION_ICON_NAME, "audio-card"); device_lookup = new HashMap(); card_lookup = new HashMap(); unowned_devices = new ListStore(2, typeof(string), typeof(uint32)); cards = new ListStore(2, typeof(string), typeof(uint32)); Box vbox = new VBox(false, 0); add(vbox); Table upper_table = new Table(7, 2, false); vbox.pack_start(upper_table, false, true, 0); upper_table.set_col_spacings(6); upper_table.set_row_spacings(0); card_title_label = new BoldLabel("Hardware"); Alignment a = new Alignment(0, 0, 1, 1); a.set_padding(0, 3, 0, 0); a.add(card_title_label); upper_table.attach(a, 0, 2, 0, 1, AttachOptions.FILL, AttachOptions.FILL, 0, 0); card_label = new FieldLabel("Sound _Card:"); upper_table.attach(card_label, 0, 1, 1, 2, AttachOptions.FILL, AttachOptions.FILL, 0, 3); card_combo_box = new ComboBox.with_model(cards); upper_table.attach(card_combo_box, 1, 2, 1, 2, AttachOptions.EXPAND|AttachOptions.FILL, AttachOptions.FILL, 0, 3); profile_label = new FieldLabel("_Profile:"); upper_table.attach(profile_label, 0, 1, 2, 3, AttachOptions.FILL, AttachOptions.FILL, 0, 3); profile_combo_box = new ComboBox(); upper_table.attach(profile_combo_box, 1, 2, 2, 3, AttachOptions.EXPAND|AttachOptions.FILL, AttachOptions.FILL, 0, 3); device_title_label = new BoldLabel("Output"); device_title_alignment = new Alignment(0, 0, 1, 1); device_title_alignment.set_padding(15, 3, 0, 0); device_title_alignment.add(device_title_label); upper_table.attach(device_title_alignment, 0, 2, 3, 4, AttachOptions.FILL, AttachOptions.FILL, 0, 0); device_label = new FieldLabel("Sound _Output:"); upper_table.attach(device_label, 0, 1, 4, 5, AttachOptions.FILL, AttachOptions.FILL, 0, 3); device_combo_box = new ComboBox(); upper_table.attach(device_combo_box, 1, 2, 4, 5, AttachOptions.EXPAND|AttachOptions.FILL, AttachOptions.FILL, 0, 3); port_label = new FieldLabel("_Connector:"); upper_table.attach(port_label, 0, 1, 5, 6, AttachOptions.FILL, AttachOptions.FILL, 0, 3); port_combo_box = new ComboBox(); upper_table.attach(port_combo_box, 1, 2, 5, 6, AttachOptions.EXPAND|AttachOptions.FILL, AttachOptions.FILL, 0, 3); Label label = new BoldLabel("Speaker Placement and Testing"); a = new Alignment(0, 0, 1, 1); a.set_padding(15, 6, 0, 0); a.add(label); upper_table.attach(a, 0, 2, 6, 7, AttachOptions.FILL, AttachOptions.FILL, 0, 0); Frame frame = new Frame(null); frame.shadow_type = ShadowType.OUT; vbox.pack_start(frame, true, true, 0); channel_notebook = new Notebook(); channel_notebook.set_show_border(false); channel_notebook.set_show_tabs(false); frame.add(channel_notebook); Label empty_label = new Label(null); empty_label.set_markup("Please select a sound card and output to configure."); channel_notebook.append_page(empty_label, null); channel_table = new Table(3, 5, true); channel_notebook.append_page(channel_table, null); channel_table.set_col_spacings(12); channel_table.set_row_spacings(12); channel_table.set_border_width(36); create_channel_controls(); channel_table.attach(new Image.from_icon_name("face-smile", IconSize.DIALOG), 2, 3, 1, 2, AttachOptions.EXPAND, AttachOptions.EXPAND, 0, 0); ButtonBox button_box = new HButtonBox(); button_box.set_layout(ButtonBoxStyle.END); close_button = new Button.from_stock(STOCK_CLOSE); button_box.pack_start(close_button, false, false, 0); a = new Alignment(0, 0, 1, 1); a.set_padding(12, 0, 0, 0); a.add(button_box); vbox.pack_start(a, false, false, 0); close_button.clicked += on_close_button_clicked; CellRenderer r = new CellRendererText(); card_combo_box.pack_start(r, true); card_combo_box.set_attributes(r, "markup", 0, null); r = new CellRendererText(); profile_combo_box.pack_start(r, true); profile_combo_box.set_attributes(r, "text", 0, null); r = new CellRendererText(); device_combo_box.pack_start(r, true); device_combo_box.set_attributes(r, "text", 0, null); r = new CellRendererText(); port_combo_box.pack_start(r, true); port_combo_box.set_attributes(r, "text", 0, null); card_combo_box.changed += on_card_combo_box_changed; profile_combo_box.changed += on_profile_combo_box_changed; device_combo_box.changed += on_device_combo_box_changed; port_combo_box.changed += on_port_combo_box_changed; device_combo_box.set_model(unowned_devices); current_devices = unowned_devices; destroy += main_quit; TreeIter i; cards.append(out i); cards.set(i, 0, "Independent Devices", 1, INVALID_INDEX); main_loop = new PulseAudio.GLibMainLoop(); context = new PulseAudio.Context(main_loop.get_api(), "Speaker Test"); if (context == null) { stderr.printf("Cannot allocate connection.\n"); main_quit(); } else { context.set_state_callback(context_state_cb); context.set_subscribe_callback(context_subscribe_cb); if (context.connect() < 0) { stderr.printf("Cannot initiate connection: %s\n", PulseAudio.strerror(context.errno())); main_quit(); } } vbox.show_all(); } private const int position_table[] = { /* Position, X, Y */ ChannelPosition.FRONT_LEFT, 0, 0, ChannelPosition.FRONT_LEFT_OF_CENTER, 1, 0, ChannelPosition.FRONT_CENTER, 2, 0, ChannelPosition.MONO, 2, 0, ChannelPosition.FRONT_RIGHT_OF_CENTER, 3, 0, ChannelPosition.FRONT_RIGHT, 4, 0, ChannelPosition.SIDE_LEFT, 0, 1, ChannelPosition.SIDE_RIGHT, 4, 1, ChannelPosition.REAR_LEFT, 0, 2, ChannelPosition.REAR_CENTER, 2, 2, ChannelPosition.REAR_RIGHT, 4, 2, ChannelPosition.LFE, 3, 2 }; void create_channel_controls() { for (int i = 0; i < position_table.length; i += 3) { channel_controls[position_table[i]] = new ChannelControl(canberra, (ChannelPosition) position_table[i]); channel_table.attach(channel_controls[position_table[i]], position_table[i+1], position_table[i+1]+1, position_table[i+2], position_table[i+2]+1, AttachOptions.EXPAND, AttachOptions.EXPAND, 0, 0); } } void update_channel_map(ChannelMap? m) { if (m == null) channel_notebook.set_current_page(0); else { for (int i = 0; i < position_table.length; i += 3) channel_controls[position_table[i]].set_visible( m != null && m.has_position((ChannelPosition) position_table[i])); channel_notebook.set_current_page(1); } } void context_state_cb(PulseAudio.Context c) { PulseAudio.Context.State state = c.get_state(); if (!state.IS_GOOD()) { stderr.printf("Cannot establish connection: %s\n", PulseAudio.strerror(c.errno())); Gtk.main_quit(); return; }; if (state == PulseAudio.Context.State.READY) { /* Connection is ready, let's query cards and sinks */ context.subscribe(PulseAudio.Context.SubscriptionMask.SINK | PulseAudio.Context.SubscriptionMask.CARD); c.get_card_info_list(context_card_info_cb); c.get_sink_info_list(context_sink_info_cb); } } bool find_card_iter(uint32 idx, out TreeIter iter) { if (!cards.get_iter_first(out iter)) return false; do { uint32 j; cards.get(iter, 1, out j); if (j == idx) return true; } while (cards.iter_next(ref iter)); return false; } bool find_device_iter(ListStore devices, uint32 idx, out TreeIter iter) { if (!devices.get_iter_first(out iter)) return false; do { uint32 j; devices.get(iter, 1, out j); if (j == idx) return true; } while (devices.iter_next(ref iter)); return false; } void context_subscribe_cb(PulseAudio.Context c, PulseAudio.Context.SubscriptionEventType t, uint32 idx) { switch (t & PulseAudio.Context.SubscriptionEventType.FACILITY_MASK) { case PulseAudio.Context.SubscriptionEventType.CARD: if ((t & PulseAudio.Context.SubscriptionEventType.TYPE_MASK) == PulseAudio.Context.SubscriptionEventType.REMOVE) { if (idx in card_lookup) { CardData *d = card_lookup[idx]; TreeIter iter; if (find_card_iter(idx, out iter)) cards.remove(iter); if (current_devices == d->devices) current_devices = null; card_lookup.remove(idx); delete d; /* Make sure the UI is properly updated when the last device of a card is gone */ on_card_combo_box_changed(); } } else context.get_card_info_by_index(idx, context_card_info_cb); break; case PulseAudio.Context.SubscriptionEventType.SINK: if ((t & PulseAudio.Context.SubscriptionEventType.TYPE_MASK) == PulseAudio.Context.SubscriptionEventType.REMOVE) { if (idx in device_lookup) { DeviceData *d = device_lookup[idx]; TreeIter iter; if (d->card != null) { if (find_device_iter(d->card->devices, idx, out iter)) d->card->devices.remove(iter); } else if (find_device_iter(unowned_devices, idx, out iter)) unowned_devices.remove(iter); device_lookup.remove(idx); delete d; /* Make sure the UI is properly updated when the last device of a card is gone */ on_card_combo_box_changed(); } } else context.get_sink_info_by_index(idx, context_sink_info_cb); break; } } bool select_card(uint32 index) { TreeIter iter; if (!find_card_iter(index, out iter)) return false; card_combo_box.set_active_iter(iter); return true; } bool select_device(uint32 index) { if (!(index in device_lookup)) return false; DeviceData *d = device_lookup[index]; ListStore *devices; if (d->card != null) { if (!select_card(d->card->index)) return false; devices = d->card->devices; } else { card_combo_box.set_active(0); /* that's the independant devices entry */ devices = unowned_devices; } TreeIter iter; if (!find_device_iter(devices, index, out iter)) return false; device_combo_box.set_active_iter(iter); return true; } bool pick_initial_selection() { if (finished_startup) return true; finished_startup = true; if (default_device_index != INVALID_INDEX) { if (!select_device((uint32) default_device_index)) { stderr.printf("Cannot find device.\n"); main_quit(); return false; } profile_combo_box.set_sensitive(!lock_selection); card_combo_box.set_sensitive(!lock_selection); device_combo_box.set_sensitive(!lock_selection); } else if (default_card_index != INVALID_INDEX) { if (!select_card((uint32) default_card_index)) { stderr.printf("Cannot find card.\n"); main_quit(); return false; } card_combo_box.set_sensitive(!lock_selection); } else { if (card_combo_box.get_active() >= 0) return true; /* Select a device if there is one */ MapIterator diter = device_lookup.map_iterator(); if (diter.first()) { DeviceData *d = diter.get_value(); if (select_device(d->index)) return true; } /* No device, so let's startup with a card if there is one */ MapIterator citer = card_lookup.map_iterator(); if (citer.first()) { CardData *d = citer.get_value(); if (select_card(d->index)) return true; } card_combo_box.set_active(0); } return true; } void context_card_info_cb(PulseAudio.Context c, CardInfo? info, int eol) { if (info == null) return; CardData *d; TreeIter iter; if (info.index in card_lookup) { d = card_lookup[info.index]; d->profiles.clear(); find_card_iter(d->index, out iter); } else { d = new CardData(); d->devices = new ListStore(2, typeof(string), typeof(uint32)); d->profiles = new ListStore(2, typeof(string), typeof(string)); d->index = info.index; card_lookup[d->index] = d; cards.append(out iter); } d->description = info.proplist.gets(PulseAudio.Proplist.PROP_DEVICE_DESCRIPTION); if (d->description == null) d->description = info.name; cards.set(iter, 0, d->description, 1, d->index); d->active_profile = -1; for (int j = 0; j < info.n_profiles; j++) { d->profiles.append(out iter); d->profiles.set(iter, 0, info.profiles[j].description, 1, info.profiles[j].name); if (info.active_profile == &info.profiles[j]) d->active_profile = j; } /* The card we are showing might have changed, so * let's make sure to show all fields correctly */ on_card_combo_box_changed(); } void context_sink_info_cb(PulseAudio.Context c, SinkInfo? info, int eol) { if (info == null) { if (pick_initial_selection()) show(); return; } DeviceData *d; TreeIter iter; ListStore *devices; if (info.index in device_lookup) { d = device_lookup[info.index]; d->ports.clear(); if (d->card == null) devices = unowned_devices; else devices = d->card->devices; find_device_iter(devices, d->index, out iter); } else { d = new DeviceData(); d->ports = new ListStore(2, typeof(string), typeof(string)); d->index = info.index; d->name = info.name; if (info.card == INVALID_INDEX) { d->card = null; devices = unowned_devices; } else { d->card = card_lookup[info.card]; devices = d->card->devices; } device_lookup[info.index] = d; devices->append(out iter); } d->channel_map = info.channel_map; d->description = info.proplist.gets(PulseAudio.Proplist.PROP_DEVICE_DESCRIPTION); if (d->description == null) d->description = info.name; devices->set(iter, 0, d->description, 1, d->index); d->active_port = -1; for (int j = 0; j < info.n_ports; j++) { d->ports.append(out iter); d->ports.set(iter, 0, info.ports[j]->description, 1, info.ports[j]->name); if (info.active_port == info.ports[j]) d->active_port = j; } /* A card might just have added a sink, so let's make * sure the sink dropdown is visible */ on_card_combo_box_changed(); } void on_close_button_clicked() { main_quit(); } unowned CardData? get_current_card() { TreeIter i; uint32 index; if (!card_combo_box.get_active_iter(out i)) return null; cards.get(i, 1, out index); if (index in card_lookup) return card_lookup[index]; return null; } unowned string? get_current_profile() { TreeIter i; unowned string name; if (current_profiles == null) return null; if (!profile_combo_box.get_active_iter(out i)) return null; current_profiles->get(i, 1, out name); return name; } unowned DeviceData? get_current_device() { TreeIter i; uint32 index; if (current_devices == null) return null; if (!device_combo_box.get_active_iter(out i)) return null; current_devices->get(i, 1, out index); if (index in device_lookup) return device_lookup[index]; return null; } unowned string? get_current_port() { TreeIter i; unowned string name; if (current_ports == null) return null; if (!port_combo_box.get_active_iter(out i)) return null; current_ports->get(i, 1, out name); return name; } void show_ports(ListStore? ports, int active_port) { TreeIter i; if (ports != null && ports.get_iter_first(out i)) { current_ports = ports; port_combo_box.set_model(current_ports); port_label.set_visible(true); port_combo_box.set_visible(true); if (active_port >= 0) port_combo_box.set_active(active_port); } else { current_ports = null; port_label.set_visible(false); port_combo_box.set_visible(false); port_combo_box.set_model(current_ports); } } void show_profiles(ListStore? profiles, int active_profile) { TreeIter i; if (profiles != null && profiles.get_iter_first(out i)) { current_profiles = profiles; profile_combo_box.set_model(current_profiles); profile_label.set_visible(true); profile_combo_box.set_visible(true); if (active_profile >= 0) profile_combo_box.set_active(active_profile); } else { current_profiles = null; profile_label.set_visible(false); profile_combo_box.set_visible(false); profile_combo_box.set_model(current_profiles); } } void show_devices(ListStore? devices) { TreeIter i; if (devices != null && devices.get_iter_first(out i)) { current_devices = devices; device_combo_box.set_model(current_devices); device_title_label.set_visible(true); device_title_alignment.set_visible(true); device_label.set_visible(true); device_combo_box.set_visible(true); } else { current_devices = null; device_title_label.set_visible(false); device_title_alignment.set_visible(false); device_label.set_visible(false); device_combo_box.set_visible(false); device_combo_box.set_model(current_devices); show_ports(null, -1); update_channel_map(null); } } void select_something(ComboBox combo_box) { TreeIter iter; unowned TreeModel m; if (combo_box.get_active_iter(out iter)) return; m = combo_box.get_model(); if (m == null) return; if (!m.get_iter_first(out iter)) return; device_combo_box.set_active_iter(iter); } void on_card_combo_box_changed() { unowned CardData info; suppress_feedback = true; info = get_current_card(); if (info == null) { show_profiles(null, -1); show_devices(unowned_devices); } else { show_profiles(info.profiles, info.active_profile); show_devices(info.devices); } select_something(device_combo_box); suppress_feedback = false; on_device_combo_box_changed(); } void on_profile_combo_box_changed() { unowned CardData? info; unowned string? profile; if (suppress_feedback) return; info = get_current_card(); if (info == null) return; profile = get_current_profile(); if (profile == null) return; context.set_card_profile_by_index(info.index, profile); } void on_device_combo_box_changed() { unowned DeviceData info; suppress_feedback = true; info = get_current_device(); if (info == null) { show_ports(null, -1); update_channel_map(null); } else { show_ports(info.ports, info.active_port); canberra.change_device(info.name); update_channel_map(info.channel_map); } suppress_feedback = false; } void on_port_combo_box_changed() { unowned DeviceData? info; unowned string? port; if (suppress_feedback) return; info = get_current_device(); if (info == null) return; port = get_current_port(); if (port == null) return; context.set_sink_port_by_index(info.index, port); } } static const OptionEntry entries[] = { { "card", 0, 0, OptionArg.INT64, out default_card_index, "Show this card by default", "INDEX" }, { "device", 0, 0, OptionArg.INT64, out default_device_index, "Show this device by default", "INDEX" }, { "lock", 0, 0, OptionArg.NONE, out lock_selection, "Don't allow other selection", null }, { null } }; int main (string[] args) { Gtk.init(ref args); default_card_index = INVALID_INDEX; default_device_index = INVALID_INDEX; lock_selection = false; OptionContext context = new OptionContext("- Speaker Setup Tool"); context.add_main_entries(entries, null); context.add_group(Gtk.get_option_group(false)); try { context.parse(ref args); } catch (GLib.OptionError e) { stderr.printf("Failed to parse command line: %s\n", e.message); return 1; } new SpeakerSetupWindow(); Gtk.main(); return 0; }