/* $Id$ */ /*** This file is part of pavumeter. pavumeter is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. pavumeter 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 General Public License along with pavumeter; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. ***/ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #define LOGARITHMIC 1 class MainWindow : public Gtk::Window { public: MainWindow(const pa_channel_map &map, const char *source_name, const char *description); virtual ~MainWindow(); protected: class ChannelInfo { public: ChannelInfo(MainWindow &w, const Glib::ustring &l); Gtk::Label *label; Gtk::ProgressBar *progress; }; Gtk::VBox vbox, titleVBox; Gtk::HBox titleHBox; Gtk::Table table; std::vector channels; Gtk::Image image; Gtk::Label titleLabel; Gtk::Label subtitleLabel; Gtk::HSeparator separator; Gtk::EventBox eventBox; float *levels; virtual void addChannel(const Glib::ustring &l); virtual bool on_delete_event(GdkEventAny* e); virtual bool on_display_timeout(); virtual bool on_calc_timeout(); virtual void decayLevels(); sigc::connection display_timeout_signal_connection; sigc::connection calc_timeout_signal_connection; pa_usec_t latency; class LevelInfo { public: LevelInfo(float *levels, pa_usec_t l); virtual ~LevelInfo(); bool elapsed(); struct timeval tv; float *levels; }; std::deque levelQueue; public: virtual void pushData(const float *d, unsigned l); virtual void showLevels(const LevelInfo& i); virtual void updateLatency(pa_usec_t l); }; MainWindow::MainWindow(const pa_channel_map &map, const char *, const char *description) : Gtk::Window(), table(1, 2), latency(0) { char t[256]; int n; set_title("PulseAudio Volume Meter"); gtk_window_set_icon_name(GTK_WINDOW(gobj()), "audio-input-microphone"); add(vbox); Gdk::Color c("white"); eventBox.modify_bg(Gtk::STATE_NORMAL, c); vbox.pack_start(eventBox, false, false); image.set_from_icon_name("audio-input-microphone", Gtk::ICON_SIZE_DIALOG); eventBox.add(titleHBox); titleHBox.pack_start(image, false, false); titleHBox.pack_end(titleVBox, true, true); titleHBox.set_border_width(12); titleHBox.set_spacing(12); titleVBox.add(titleLabel); titleVBox.add(subtitleLabel); titleVBox.set_spacing(6); titleLabel.set_markup("PulseAudio Volume Meter"); titleLabel.set_alignment(0, 1); snprintf(t, sizeof(t), "Showing signal levels of %s", description); subtitleLabel.set_markup(t); subtitleLabel.set_alignment(0, 0); vbox.pack_start(separator, false, false); table.set_border_width(12); table.set_row_spacings(6); table.set_col_spacings(12); vbox.pack_start(table, true, true); for (n = 0; n < map.channels; n++) { snprintf(t, sizeof(t), "%s", pa_channel_position_to_pretty_string(map.map[n])); addChannel(t); } g_assert(channels.size() == map.channels); levels = NULL; display_timeout_signal_connection = Glib::signal_timeout().connect(sigc::mem_fun(*this, &MainWindow::on_display_timeout), 40); calc_timeout_signal_connection = Glib::signal_timeout().connect(sigc::mem_fun(*this, &MainWindow::on_calc_timeout), 100); show_all(); } MainWindow::~MainWindow() { while (channels.size() > 0) { ChannelInfo *i = channels.back(); channels.pop_back(); delete i; } while (levelQueue.size() > 0) { LevelInfo *i = levelQueue.back(); levelQueue.pop_back(); delete i; } if (levels) delete[] levels; display_timeout_signal_connection.disconnect(); calc_timeout_signal_connection.disconnect(); } bool MainWindow::on_delete_event(GdkEventAny*) { Gtk::Main::quit(); return true; } void MainWindow::addChannel(const Glib::ustring &l) { channels.push_back(new ChannelInfo(*this, l)); } MainWindow::ChannelInfo::ChannelInfo(MainWindow &w, const Glib::ustring &l) { label = Gtk::manage(new Gtk::Label(l, 1.0, 0.5)); label->set_markup(l); progress = Gtk::manage(new Gtk::ProgressBar()); progress->set_fraction(0); w.table.resize(w.channels.size()+1, 2); w.table.attach(*label, 0, 1, w.channels.size(), w.channels.size()+1, Gtk::FILL, (Gtk::AttachOptions) 0); w.table.attach(*progress, 1, 2, w.channels.size(), w.channels.size()+1, Gtk::EXPAND|Gtk::FILL, (Gtk::AttachOptions) 0); } void MainWindow::pushData(const float *d, unsigned samples) { unsigned nchan = channels.size(); if (!levels) { levels = new float[nchan]; for (unsigned c = 0; c < nchan; c++) levels[c] = 0; } while (samples >= nchan) { for (unsigned c = 0; c < nchan; c++) { float v = fabs(d[c]); if (v > levels[c]) levels[c] = v; } d += nchan; samples -= nchan; } } void MainWindow::showLevels(const LevelInfo &i) { unsigned nchan = channels.size(); g_assert(i.levels); for (unsigned n = 0; n < nchan; n++) { double level; ChannelInfo *c = channels[n]; level = i.levels[n]; #ifdef LOGARITHMIC level = log10(level*9+1); #endif c->progress->set_fraction(level > 1 ? 1 : level); } } #define DECAY_LEVEL (0.005) void MainWindow::decayLevels() { unsigned nchan = channels.size(); for (unsigned n = 0; n < nchan; n++) { double level; ChannelInfo *c = channels[n]; level = c->progress->get_fraction(); if (level <= 0) continue; level = level > DECAY_LEVEL ? level - DECAY_LEVEL : 0; c->progress->set_fraction(level); } } bool MainWindow::on_display_timeout() { LevelInfo *i = NULL; if (levelQueue.empty()) { decayLevels(); return true; } while (levelQueue.size() > 0) { if (i) delete i; i = levelQueue.back(); levelQueue.pop_back(); if (!i->elapsed()) break; } if (i) { showLevels(*i); delete i; } return true; } bool MainWindow::on_calc_timeout() { if (levels) { levelQueue.push_front(new LevelInfo(levels, latency)); levels = NULL; } return true; } void MainWindow::updateLatency(pa_usec_t l) { latency = l; } static void timeval_add_usec(struct timeval *tv, pa_usec_t v) { uint32_t sec = v/1000000; tv->tv_sec += sec; v -= sec*1000000; tv->tv_usec += v; while (tv->tv_usec >= 1000000) { tv->tv_sec++; tv->tv_usec -= 1000000; } } MainWindow::LevelInfo::LevelInfo(float *l, pa_usec_t latency) { levels = l; gettimeofday(&tv, NULL); timeval_add_usec(&tv, latency); } MainWindow::LevelInfo::~LevelInfo() { delete[] levels; } bool MainWindow::LevelInfo::elapsed() { struct timeval now; gettimeofday(&now, NULL); if (now.tv_sec != tv.tv_sec) return now.tv_sec > tv.tv_sec; return now.tv_usec >= tv.tv_usec; } static MainWindow *mainWindow = NULL; static pa_context *context = NULL; static pa_stream *stream = NULL; static char* device_name = NULL; static char* device_description = NULL; static enum { PLAYBACK, RECORD } mode = PLAYBACK; void show_error(const char *txt, bool show_pa_error = true) { char buf[256]; if (show_pa_error) snprintf(buf, sizeof(buf), "%s: %s", txt, pa_strerror(pa_context_errno(context))); Gtk::MessageDialog dialog(show_pa_error ? buf : txt, false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_CLOSE, true); dialog.run(); Gtk::Main::quit(); } static void stream_update_timing_info_callback(pa_stream *s, int success, void *) { pa_usec_t t; int negative = 0; if (!success || pa_stream_get_latency(s, &t, &negative) < 0) { show_error("Failed to get latency information"); return; } if (!mainWindow) return; mainWindow->updateLatency(negative ? 0 : t); } static gboolean latency_func(gpointer) { pa_operation *o; if (!stream) return false; if (!(o = pa_stream_update_timing_info(stream, stream_update_timing_info_callback, NULL))) g_message("pa_stream_update_timing_info() failed: %s", pa_strerror(pa_context_errno(context))); else pa_operation_unref(o); return true; } static void stream_read_callback(pa_stream *s, size_t l, void *) { const void *p; g_assert(mainWindow); if (pa_stream_peek(s, &p, &l) < 0) { g_message("pa_stream_peek() failed: %s", pa_strerror(pa_context_errno(context))); return; } mainWindow->pushData((const float*) p, l/sizeof(float)); pa_stream_drop(s); } static void stream_state_callback(pa_stream *s, void *) { switch (pa_stream_get_state(s)) { case PA_STREAM_UNCONNECTED: case PA_STREAM_CREATING: break; case PA_STREAM_READY: g_assert(!mainWindow); mainWindow = new MainWindow(*pa_stream_get_channel_map(s), device_name, device_description); g_timeout_add(100, latency_func, NULL); pa_operation_unref(pa_stream_update_timing_info(stream, stream_update_timing_info_callback, NULL)); break; case PA_STREAM_FAILED: show_error("Connection failed"); break; case PA_STREAM_TERMINATED: Gtk::Main::quit(); } } static void create_stream(const char *name, const char *description, const pa_sample_spec &ss, const pa_channel_map &cmap) { char t[256]; pa_sample_spec nss; g_free(device_name); device_name = g_strdup(name); g_free(device_description); device_description = g_strdup(description); nss.format = PA_SAMPLE_FLOAT32; nss.rate = ss.rate; nss.channels = ss.channels; g_message("Using sample format: %s", pa_sample_spec_snprint(t, sizeof(t), &nss)); g_message("Using channel map: %s", pa_channel_map_snprint(t, sizeof(t), &cmap)); stream = pa_stream_new(context, "PulseAudio Volume Meter", &nss, &cmap); pa_stream_set_state_callback(stream, stream_state_callback, NULL); pa_stream_set_read_callback(stream, stream_read_callback, NULL); pa_stream_connect_record(stream, name, NULL, (enum pa_stream_flags) 0); } static void context_get_source_info_callback(pa_context *, const pa_source_info *si, int is_last, void *) { if (is_last < 0) { show_error("Failed to get source information"); return; } if (!si) return; create_stream(si->name, si->description, si->sample_spec, si->channel_map); } static void context_get_sink_info_callback(pa_context *, const pa_sink_info *si, int is_last, void *) { if (is_last < 0) { show_error("Failed to get sink information"); return; } if (!si) return; create_stream(si->monitor_source_name, si->description, si->sample_spec, si->channel_map); } static void context_get_server_info_callback(pa_context *c, const pa_server_info*si, void *) { if (!si) { show_error("Failed to get server information"); return; } if (mode == PLAYBACK) { if (!si->default_sink_name) { show_error("No default sink set.", false); return; } pa_operation_unref(pa_context_get_sink_info_by_name(c, si->default_sink_name, context_get_sink_info_callback, NULL)); } else if (mode == RECORD) { if (!si->default_source_name) { show_error("No default source set.", false); return; } pa_operation_unref(pa_context_get_source_info_by_name(c, si->default_source_name, context_get_source_info_callback, NULL)); } } static void context_state_callback(pa_context *c, void *) { switch (pa_context_get_state(c)) { case PA_CONTEXT_UNCONNECTED: case PA_CONTEXT_CONNECTING: case PA_CONTEXT_AUTHORIZING: case PA_CONTEXT_SETTING_NAME: break; case PA_CONTEXT_READY: g_assert(!stream); if (device_name && mode == RECORD) pa_operation_unref(pa_context_get_source_info_by_name(c, device_name, context_get_source_info_callback, NULL)); else if (device_name && mode == PLAYBACK) pa_operation_unref(pa_context_get_sink_info_by_name(c, device_name, context_get_sink_info_callback, NULL)); else pa_operation_unref(pa_context_get_server_info(c, context_get_server_info_callback, NULL)); break; case PA_CONTEXT_FAILED: show_error("Connection failed"); break; case PA_CONTEXT_TERMINATED: Gtk::Main::quit(); } } int main(int argc, char *argv[]) { pa_glib_mainloop *m; bool record = false; signal(SIGPIPE, SIG_IGN); try { Glib::OptionGroup og("PulseAudio Volume Meter", "Control the volume of your PulseAudio Sound Server"); Glib::OptionEntry oe; oe.set_long_name("record"); oe.set_description("Show Record Levels"); og.add_entry(oe, record); Glib::OptionContext oc; oc.set_main_group(og); Gtk::Main kit(argc, argv, oc); mode = record ? RECORD : PLAYBACK; g_message("Starting in %s mode.", mode == RECORD ? "record" : "playback"); /* Rather ugly and incomplete */ if (argc > 1) device_name = g_strdup(argv[1]) ; else { char *e; if ((e = getenv(mode == RECORD ? "PULSE_SOURCE" : "PULSE_SINK"))) device_name = g_strdup(e); } if (device_name) g_message("Using device '%s'", device_name); m = pa_glib_mainloop_new(g_main_context_default()); g_assert(m); context = pa_context_new(pa_glib_mainloop_get_api(m), "PulseAudio Volume Meter"); g_assert(context); pa_context_set_state_callback(context, context_state_callback, NULL); pa_context_connect(context, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL); Gtk::Main::run(); if (stream) pa_stream_unref(stream); if (context) pa_context_unref(context); if (mainWindow) delete mainWindow; if(device_name) g_free(device_name); pa_glib_mainloop_free(m); } catch (Glib::OptionError) { g_message("Bad parameter"); } return 0; }