From fbd7bca5fcb8758ce5cd774f5dc6805763ec824b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 20 Feb 2010 01:22:14 +0100 Subject: initial checkin --- gnome-speaker-setup.vala | 840 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 840 insertions(+) create mode 100644 gnome-speaker-setup.vala (limited to 'gnome-speaker-setup.vala') diff --git a/gnome-speaker-setup.vala b/gnome-speaker-setup.vala new file mode 100644 index 0000000..343acfc --- /dev/null +++ b/gnome-speaker-setup.vala @@ -0,0 +1,840 @@ +using Gtk; +using PulseAudio; +using Gee; +using Canberra; + +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(p.to_pretty_string()); + 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("canberra.force_channel", position.to_string()); + + 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 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 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 PulseAudio.Context context; + private PulseAudio.GLibMainLoop main_loop; + + private Canberra.Context canberra; + + 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"); + + 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(6); + channel_table.set_row_spacings(36); + 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; + + destroy += Gtk.main_quit; + + main_loop = new PulseAudio.GLibMainLoop(); + context = new PulseAudio.Context(main_loop.get_api(), "Speaker Test"); + + context.connect(); + + context.set_state_callback(context_state_cb); + context.set_subscribe_callback(context_subscribe_cb); + + CellRenderer r = new CellRendererText(); + card_combo_box.pack_start(r, true); + card_combo_box.set_attributes(r, "text", 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; + + TreeIter i; + cards.append(out i); + cards.set(i, 0, "Non-Hardware", 1, INVALID_INDEX); + } + + 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("Couldn't 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; + } + } + + void pick_initial_card() { + if (finished_startup) + return; + + finished_startup = true; + + if (card_combo_box.get_active() >= 0) + return; + + TreeIter i; + if (cards.get_iter_first(out i) && + cards.iter_next(ref i)) + card_combo_box.set_active(1); + else + card_combo_box.set_active(0); + } + + void context_card_info_cb(PulseAudio.Context c, CardInfo? info, int eol) { + + if (info == null) { + pick_initial_card(); + 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) + 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() { + Gtk.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); + } +} + +int main (string[] args) { + Gtk.init (ref args); + + var window = new SpeakerSetupWindow(); + window.show_all(); + + Gtk.main(); + + return 0; +} -- cgit