diff --git a/README.md b/README.md index a689aff..5396b85 100644 --- a/README.md +++ b/README.md @@ -3,25 +3,26 @@ A Sound Visualizer for Gnome Shell based on Gstreamer specially for Wayland ![demo](assets/visualization.gif) -For Desktop Widgets I'm Using [Circular Widgets](https://extensions.gnome.org/extension/5530/circular-widgets/) Extension. +For the Desktop Widgets, I'm using the [Circular Widgets](https://extensions.gnome.org/extension/5530/circular-widgets/) extension. # Features -- Drag and Drop Supports -- Change Audio source from Menu (To change right/left click on Visualizer) -- Change Visualizer size +- Drag and Drop Support +- Change Audio Source from the Menu (To change right/left click on Visualizer) +- Change Visualizer Size - Increase or Decrease Bands -- Choose how many bands will appear on display -- Now you can Flip Visualizer -- Added older version Gnome Shell v3.36 to v43.0 +- Flip Visualizer Horizontally or Vertically +- Ability to Change the FPS of the Visualizer +- Ability to choose how many bands will appear on display +- Added older version Gnome Shell support, from v3.36 to v43.0 -More Feature will be added in Future +More features will be added in the future # Installation -1. Download zip file : https://github.com/raihan2000/visualizer/archive/refs/heads/main.zip +1. Download the zip file : https://github.com/raihan2000/visualizer/archive/refs/heads/main.zip 2. Extract to visualizer-main -4. make install +3. `make install` or @@ -33,4 +34,4 @@ make install # Credits -This Extension is inspired by [Glava](https://github.com/jarcode-foss/glava) +This extension is inspired by [Glava](https://github.com/jarcode-foss/glava). diff --git a/src/metadata.json b/src/metadata.json index 7991f79..2325d8f 100644 --- a/src/metadata.json +++ b/src/metadata.json @@ -1,6 +1,6 @@ { "_generated": "Generated by SweetTooth, do not edit", - "description": "A Real Time Sound Visualizer Based On Gstreamer", + "description": "A Real Time Sound Visualizer Based On Gstreamer\nFor any Issues,Bugs and Suggestions please open an issue on Github", "name": "Sound Visualizer", "settings-schema": "org.gnome.shell.extensions.visualizer", "shell-version": [ @@ -13,5 +13,5 @@ ], "url": "https://github.com/raihan2000/visualizer", "uuid": "visualizer@sound.org", - "version": 4 -} + "version": 5 +} \ No newline at end of file diff --git a/src/prefs.js b/src/prefs.js index c9e5226..f9196a2 100644 --- a/src/prefs.js +++ b/src/prefs.js @@ -1,13 +1,23 @@ 'use strict'; const { Gio, Gtk, Gdk, GLib, GObject } = imports.gi; -const Params = imports.misc.params; const ExtensionUtils = imports.misc.extensionUtils; -const Me = ExtensionUtils.getCurrentExtension(); +const Params = imports.misc.params; const Config = imports.misc.config; -const [major, minor] = Config.PACKAGE_VERSION.split('.').map(s => Number(s)); +const Me = ExtensionUtils.getCurrentExtension(); + +const [MajorVersion, MinorVersion] = Config.PACKAGE_VERSION.split('.').map(s => Number(s)); let Adw; +const DEFAULT_SPIN_MIN = 1; // Minimum pixel size +const DEFAULT_SPIN_MAX = 200; // Maximum pixel size +const VISUALIZER_WIDTH_MAX = 1920; // Maximum pixel size for width +const SPECTS_LINE_WIDTH_MAX = 20; // Maximum pixel size for spect line widths +const TOTAL_SPECTS_BAND_MAX = 256; // Maximum # of spect bands possible to be chosen +const FPS_OPTIONS = ["15", "30", "60", "90", "120"]; // Frame counts; going from 15 to 120 fps +const GRID_COLUMN_SPACING = 200; // Spacing between columns +const GRID_ROW_SPACING = 25; // Spacing between rows + function init() { } @@ -19,44 +29,68 @@ function fillPreferencesWindow(window) { function buildPrefsWidget() { let widget = new prefsWidget(); - (major < 40) ? widget.show_all(): widget.show(); + (MajorVersion < 40) ? widget.show_all(): widget.show(); return widget; } +function attachItems(grid, label, widget, row) { + grid.set_column_spacing(GRID_COLUMN_SPACING); + grid.set_row_spacing(GRID_ROW_SPACING); + grid.attach(label, 0, row, 1, 1); + grid.attach(widget, 1, row, 1, 1); +} + + const prefsWidget = GObject.registerClass( - class prefsWidget extends Gtk.Notebook { - - _init(params) { - super._init(params); - this._settings = ExtensionUtils.getSettings('org.gnome.shell.extensions.visualizer'); - this.margin = 20; - - let grid = new Gtk.Grid(); - attachItems(grid, new Gtk.Label({ label: 'Flip the Visualizer' }), getSwitch('flip-visualizer', this._settings), 0); - attachItems(grid, new Gtk.Label({ label: 'Visualizer Height' }), getSpinButton(false, 'visualizer-height', 1, 200, 1, this._settings), 1); - attachItems(grid, new Gtk.Label({ label: 'Visualizer Width' }), getSpinButton(false, 'visualizer-width', 1, 1920, 1, this._settings), 2); - attachItems(grid, new Gtk.Label({ label: 'Spects Line Width' }), getSpinButton(false, 'spects-line-width', 1, 20, 1, this._settings), 3); - attachItems(grid, new Gtk.Label({ label: 'Change Spects Band to Get' }), getSpinButton(false, 'total-spects-band', 1, 256, 1, this._settings), 4); - this.attachHybridRow(grid, new Gtk.Label({ label: 'Override Spect Value' }), new Gtk.Label({ label: 'Set Spects Value' }), getSwitch('spect-over-ride-bool', this._settings), getSpinButton(false, 'spect-over-ride', 1, 256, 1, this._settings), 5); - this.append_page(grid, new Gtk.Label({ label: 'Visualizer' })); - let aboutBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); - if (major < 40) { - aboutBox.add(new Gtk.Label({ label: Me.metadata.name })); - aboutBox.add(new Gtk.Label({ label: 'Version: ' + Me.metadata.version.toString() })); - } else { - aboutBox.append(new Gtk.Label({ label: Me.metadata.name })); - aboutBox.append(new Gtk.Label({ label: 'Version: ' + Me.metadata.version.toString() })); + class prefsWidget extends Gtk.Notebook { + + _init(params) { + super._init(params); + let grid = new Gtk.Grid(); + this._settings = ExtensionUtils.getSettings('org.gnome.shell.extensions.visualizer'); + this.setupFpsOptions(); + this.margin = 20; + + attachItems(grid, new Gtk.Label({ label: 'Flip the Visualizer Vertically' }), getSwitch('flip-visualizer', this._settings), 0); + attachItems(grid, new Gtk.Label({ label: 'Flip the Visualizer Horizontally' }), getSwitch('horizontal-flip', this._settings), 1); + attachItems(grid, new Gtk.Label({ label: 'Visualizer Height' }), getSpinButton(false, 'visualizer-height', DEFAULT_SPIN_MIN, DEFAULT_SPIN_MAX, 1, this._settings), 2); + attachItems(grid, new Gtk.Label({ label: 'Visualizer Width' }), getSpinButton(false, 'visualizer-width', DEFAULT_SPIN_MIN, VISUALIZER_WIDTH_MAX, 1, this._settings), 3); + attachItems(grid, new Gtk.Label({ label: 'Spects Line Width' }), getSpinButton(false, 'spects-line-width', DEFAULT_SPIN_MIN, SPECTS_LINE_WIDTH_MAX, 1, this._settings), 5); + attachItems(grid, new Gtk.Label({ label: 'Change Spects Band to Get' }), getSpinButton(false, 'total-spects-band', DEFAULT_SPIN_MIN, TOTAL_SPECTS_BAND_MAX, 1, this._settings), 4); + attachItems(grid, new Gtk.Label({ label: 'Frames Per Second (FPS)' }), getDropDown(this._settings), 7); + attachItems(grid, new Gtk.Label({ label: 'Visualizer Color' }), getColorButton(this._settings), 8); + this.attachHybridRow(grid, new Gtk.Label({ label: 'Override Spect Value' }), new Gtk.Label({ label: 'Set Spects Value' }), getSwitch('spect-over-ride-bool', this._settings), getSpinButton(false, 'spect-over-ride', 1, 256, 1, this._settings), 6); + this.append_page(grid, new Gtk.Label({ label: 'Visualizer' })); + + let aboutBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); + if (MajorVersion < 40) { + aboutBox.add(new Gtk.Label({ label: Me.metadata.name })); + aboutBox.add(new Gtk.Label({ label: 'Version: ' + Me.metadata.version.toString() })); + } else { + aboutBox.append(new Gtk.Label({ label: Me.metadata.name })); + aboutBox.append(new Gtk.Label({ label: 'Version: ' + Me.metadata.version.toString() })); + } + this.append_page(aboutBox, new Gtk.Label({ label: 'About' })); } - this.append_page(aboutBox, new Gtk.Label({ label: 'About' })); - } - attachHybridRow(grid, label, label1, button, button1, row) { - grid.attach(label, 0, row, 1, 1); - grid.attach(button, 1, row, 1, 1); - grid.attach(label1, 0, row + 1, 1, 1); - grid.attach(button1, 1, row + 1, 1, 1); - } - }); + setupFpsOptions() { + let fpsOptions = new Gtk.ComboBoxText(); + FPS_OPTIONS.forEach(fps => fpsOptions.append_text(fps)); + fpsOptions.connect('changed', (widget) => { + let fps = widget.get_active_text(); + this._settings.set_int('fps', parseInt(fps, 10)); + }); + let currentFps = this._settings.get_int('fps'); + fpsOptions.set_active_id(currentFps.toString()); + } + + attachHybridRow(grid, label, label1, button, button1, row) { + grid.attach(label, 0, row, 1, 1); + grid.attach(button, 1, row, 1, 1); + grid.attach(label1, 0, row + 1, 1, 1); + grid.attach(button1, 1, row + 1, 1, 1); + } + }); class PrefsWindow { constructor(window) { @@ -65,10 +99,12 @@ class PrefsWindow { } create_page(title) { - let page = new Adw.PreferencesPage({ - title: title, - //icon_name: icon, - }); + let page = new Adw.PreferencesPage( + { + title: title, + //icon_name: icon, + } + ); this._window.add(page); // get the headerbar @@ -99,9 +135,7 @@ class PrefsWindow { } append_row(group, title, widget) { - let row = new Adw.ActionRow({ - title: title, - }); + let row = new Adw.ActionRow({ title: title }); group.add(row); row.add_suffix(widget); row.activatable_widget = widget; @@ -114,12 +148,10 @@ class PrefsWindow { expanded: this._settings.get_boolean(key), enable_expansion: this._settings.get_boolean(key) }); - let row = new Adw.ActionRow({ - title: title, - }); + + let row = new Adw.ActionRow({ title: title }); expand_row.connect("notify::enable-expansion", (widget) => { - let settingArray = this._settings.get_boolean(key); - settingArray = widget.enable_expansion; + let settingArray = widget.enable_expansion; this._settings.set_value(key, new GLib.Variant('b', settingArray)); }); row.add_suffix(key1); @@ -129,20 +161,9 @@ class PrefsWindow { append_info_group(group, name, title) { let adw_group = new Adw.PreferencesGroup(); - let infoBox = new Gtk.Box({ - orientation: Gtk.Orientation.VERTICAL, - hexpand: false, - vexpand: false - }); - - let name_label = new Gtk.Label({ - label: name, - }); - - let version = new Gtk.Label({ - label: 'Version: ' + title, - }); - + let infoBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, hexpand: false, vexpand: false}); + let name_label = new Gtk.Label({ label: name }); + let version = new Gtk.Label({ label: 'Version: ' + title }); infoBox.append(name_label); infoBox.append(version); adw_group.add(infoBox); @@ -152,12 +173,15 @@ class PrefsWindow { fillPrefsWindow() { let visualWidget = this.create_page('Visualizer'); { let groupVisual = this.create_group(visualWidget); - this.append_row(groupVisual, 'Flip the Visualizer', getSwitch('flip-visualizer', this._settings)); - this.append_row(groupVisual, 'Visualizer Height', getSpinButton(false, 'visualizer-height', 1, 200, 1, this._settings)); - this.append_row(groupVisual, 'Visualizer Width', getSpinButton(false, 'visualizer-width', 1, 1920, 1, this._settings)); - this.append_row(groupVisual, 'Spects Line Width', getSpinButton(false, 'spects-line-width', 1, 20, 1, this._settings)); - this.append_row(groupVisual, 'Change Spects Band to Get', getSpinButton(false, 'total-spects-band', 1, 256, 1, this._settings)); - this.append_expander_row(groupVisual, 'Override Spect Value', 'Set Spects Value', 'spect-over-ride-bool', getSpinButton(false, 'spect-over-ride', 1, 256, 1, this._settings)); + this.append_row(groupVisual, 'Flip the Visualizer Vertically', getSwitch('flip-visualizer', this._settings)); + this.append_row(groupVisual, 'Flip the Visualizer Horizontally', getSwitch('horizontal-flip', this._settings)); + this.append_row(groupVisual, 'Visualizer Height (px)', getSpinButton(false, 'visualizer-height', 1, 200, 1, this._settings)); + this.append_row(groupVisual, 'Visualizer Width (px)', getSpinButton(false, 'visualizer-width', 1, 1920, 1, this._settings)); + this.append_row(groupVisual, 'Spects Line Width (px)', getSpinButton(false, 'spects-line-width', 1, 20, 1, this._settings)); + this.append_row(groupVisual, 'Change # of Spect Bands to Get', getSpinButton(false, 'total-spects-band', 1, 256, 1, this._settings)); + this.append_row(groupVisual, 'Frames Per Second (FPS)', getDropDown(this._settings)); + this.append_row(groupVisual, 'Visualizer Color', getColorButton(this._settings)); + this.append_expander_row(groupVisual, 'Override Spect Value', 'Set Spect Value', 'spect-over-ride-bool', getSpinButton(false, 'spect-over-ride', 1, 256, 1, this._settings)); } let aboutPage = this.create_page('About'); { @@ -167,13 +191,6 @@ class PrefsWindow { } } -function attachItems(grid, label, widget, row) { - grid.set_column_spacing(200); - grid.set_row_spacing(25); - grid.attach(label, 0, row, 1, 1); - grid.attach(widget, 1, row, 1, 1); -} - function getSwitch(key, settings) { let button = new Gtk.Switch({ active: key, valign: Gtk.Align.CENTER }); settings.bind(key, button, 'active', Gio.SettingsBindFlags.DEFAULT); @@ -181,10 +198,41 @@ function getSwitch(key, settings) { } function getSpinButton(is_double, key, min, max, step, settings) { - let v = 0; - (is_double) ? v = settings.get_double(key) : v = settings.get_int(key); + let value = is_double ? settings.get_double(key) : settings.get_int(key); let spin = Gtk.SpinButton.new_with_range(min, max, step); - spin.set_value(v); + spin.set_value(value); settings.bind(key, spin, 'value', Gio.SettingsBindFlags.DEFAULT); return spin; } + +function getDropDown(settings) { + let dropDown = new Gtk.ComboBoxText(); + FPS_OPTIONS.forEach(fps => dropDown.append_text(fps)); + + dropDown.connect('changed', (widget) => { + let fps = widget.get_active_text(); + settings.set_int('fps', parseInt(fps, 10)); + }); + + let currentFps = settings.get_int('fps').toString(); + let currentIndex = FPS_OPTIONS.indexOf(currentFps); + if (currentIndex !== -1) { + dropDown.set_active(currentIndex); + } + return dropDown; +} + +function getColorButton(settings) { + let button = new Gtk.ColorButton(); + let rgbaString = settings.get_string('visualizer-color'); + let rgbaParts = rgbaString.split(',').map(parseFloat); + let gdkRGBA = new Gdk.RGBA({red: rgbaParts[0], green: rgbaParts[1], blue: rgbaParts[2], alpha: rgbaParts[3]}); + button.set_rgba(gdkRGBA); + button.connect('color-set', () => { + let gdkRGBA = button.get_rgba(); + let rgbaString = `${gdkRGBA.red},${gdkRGBA.green},${gdkRGBA.blue},${gdkRGBA.alpha}`; + settings.set_string('visualizer-color', rgbaString); + }); + + return button; +} \ No newline at end of file diff --git a/src/schemas/gschemas.compiled b/src/schemas/gschemas.compiled new file mode 100644 index 0000000..ea68312 Binary files /dev/null and b/src/schemas/gschemas.compiled differ diff --git a/src/schemas/org.gnome.shell.extensions.visualizer.gschema.xml b/src/schemas/org.gnome.shell.extensions.visualizer.gschema.xml index c20986b..9129dc8 100644 --- a/src/schemas/org.gnome.shell.extensions.visualizer.gschema.xml +++ b/src/schemas/org.gnome.shell.extensions.visualizer.gschema.xml @@ -8,25 +8,18 @@ Location of visualizer - - 150 - Vertical Size of DrawingArea - - - 720 - Horizontal Size of DrawingArea - - - 5 - All Spect Bands width - - - 64 - Count Total Spects Bands + + '1.0,0.0,1.0,1.0' + Visualizer color + The color of the visualizer false - Flip Visualizer + Flip Visualizer Vertically + + + false + Flip Visualizer Horizontally false @@ -38,5 +31,26 @@ Override Spects Bands Override Spects Bands + + 30 + Frames Per Second + The number of frames per second for the visualizer. Possible values are 15, 30, 60, 90, and 120. + + + 5 + All Spect Bands width + + + 64 + Count Total Spects Bands + + + 150 + Vertical Size of DrawingArea + + + 720 + Horizontal Size of DrawingArea + diff --git a/src/visual.js b/src/visual.js index cd335d8..30ff45a 100644 --- a/src/visual.js +++ b/src/visual.js @@ -5,48 +5,127 @@ const ExtensionUtils = imports.misc.extensionUtils; const Main = imports.ui.main; const PopupMenu = imports.ui.popupMenu; const Config = imports.misc.config; -const [major, minor] = Config.PACKAGE_VERSION.split('.').map(s => Number(s)); +const [MajorVersion, MinorVersion] = Config.PACKAGE_VERSION.split('.').map(s => Number(s)); + +// General Constants +const REFRESH_RATE_BASE = 1000; // Base ms count for calculating refresh rate +const INTERVAL_BASE = 1000000000; // Base ns count for calculating interval +const SPECTRUM_THRESHOLD = -80; // Default threshold for the spectrum +const MENU_POSITION_Y = 0.5; // Y position for popup menu +const MENU_SIDE = St.Side.TOP; // Side for popup menu +const POPUP_TIMEOUT = 600; // Timeout for popup in milliseconds + +// drawStuff Constants +const MAX_FREQUENCY = 80; // Maximum frequency value +const MIN_INTENSITY = 0.2; // Minimum intensity +const VERTICAL_FLIP_FACTOR = 80; // Factor used in calculating yPosition for vertical flip +const SQRT_VALUE = 0.5; // Square root exponent used in calculating intensity +const MAX_COLOR_VALUE = 1.0; // Maximum color value for unit rgb used in calculations +const MIN_RANGE = 0; // Minimum range value when normalizing frequency +const MAX_RANGE = 1; // Maximum range value when normalizing frequency +const MIRROR_VALUE = 1; // Mirror value used to invert factors for horizontal flipping +const MIDDLE_DIVISOR = 2; // Divisor used in calculating xPosition, used for finding the middle of the line width of lineW +const START_DRAW_Y_VALUE = 0; // Value used for drawing line segments; marks the start of the drawn line +const END_DRAW_Y_VALUE = 1; // Value used for drawing line segments; marks the end of the drawn line var Visualizer = GObject.registerClass( class musicVisualizer extends St.BoxLayout { + /* + * Initialization and Destruction Methods + */ _init() { + this._settings = ExtensionUtils.getSettings('org.gnome.shell.extensions.visualizer'); super._init({ reactive: true, track_hover: true, can_focus: true }); + this._initializeSettings(); this._visualMenuManager = new PopupMenu.PopupMenuManager(this); this._freq = []; this._actor = new St.DrawingArea(); this.add_child(this._actor); - this._settings = ExtensionUtils.getSettings(); this.settingsChanged(); + this.setupDraggable(); + this.setupActor(); + this.setupGst(); + this.setDefaultSrc(); + this.getMenuItems(); + this._update(); + this.setPosition(); + this._refreshLoopId = null; + this.startRefreshLoop(); + Main.layoutManager._backgroundGroup.add_child(this); + } + + setupDraggable() { this._draggable = DND.makeDraggable(this); this._draggable._animateDragEnd = (eventTime) => { this._draggable._animationInProgress = true; this._draggable._onAnimationComplete(this._draggable._dragActor, eventTime); }; + this._draggable.connect('drag-begin', this._onDragBegin.bind(this)); this._draggable.connect('drag-end', this._onDragEnd.bind(this)); + this.connect('notify::hover', () => this._onHover()); + + this.isDragging = false; + this._dragMonitor = null; + this.startX = 0; + this.startY = 0; + this.oldX = 0; + this.oldY = 0; + this.deltaX = 0; + this.deltaY = 0; + } + + actorInit() { + this._spectBands = this._settings.get_int('total-spects-band'); + this._spectHeight = this._settings.get_int('visualizer-height'); + this._spectWidth = this._settings.get_int('visualizer-width'); + this._actor.height = this._spectHeight; + this._actor.width = this._spectWidth; + } + + setupActor() { this.actorInit(); this._actor.connect('repaint', (area) => this.drawStuff(area)); - this.setupGst(); - this.setDefaultSrc(); - this.getMenuItems(); - this._update(); - this.setPosition(); - Main.layoutManager._backgroundGroup.add_child(this); } + _initializeSettings() { + this._settings = ExtensionUtils.getSettings('org.gnome.shell.extensions.visualizer'); + this._connectSetting('fps', this._updateRefreshRate.bind(this)); + this._connectSetting('horizontal-flip', this._updateFlipSettings.bind(this)); + this._connectSetting('visualizer-pos-x', this.setPosition.bind(this)); + this._connectSetting('visualizer-pos-y', this.setPosition.bind(this)); + this._connectSetting('visualizer-color', this._update.bind(this)); + this._refreshRate = REFRESH_RATE_BASE / this._settings.get_int('fps'); + } + + onDestroy() { + if (this._refreshLoopId !== null) { + GLib.Source.remove(this._refreshLoopId); + } + this._removeSource(this._menuTimeoutId); + this._removeSource(this._streamId); + this._removeSource(this._defaultSrcId); + this._pipeline.set_state(Gst.State.NULL); + Main.layoutManager._backgroundGroup.remove_child(this); + } + + /* + * GStreamer Methods + */ setupGst() { Gst.init(null); this._pipeline = Gst.Pipeline.new("bin"); this._src = Gst.ElementFactory.make("pulsesrc", "src"); this._spectrum = Gst.ElementFactory.make("spectrum", "spectrum"); this._spectrum.set_property("bands", this._spectBands); - this._spectrum.set_property("threshold", -80); + this._spectrum.set_property("threshold", SPECTRUM_THRESHOLD); this._spectrum.set_property("post-messages", true); + this.updateGstInterval(); let _sink = Gst.ElementFactory.make("fakesink", "sink"); this._pipeline.add(this._src); this._pipeline.add(this._spectrum); @@ -60,6 +139,14 @@ var Visualizer = GObject.registerClass( this._pipeline.set_state(Gst.State.PLAYING); } + updateGstInterval() { + let fps = this._settings.get_int('fps'); + let interval = INTERVAL_BASE / fps; + if (this._spectrum) { + this._spectrum.set_property("interval", interval); + } + } + onMessage(bus, msg) { let struct = msg.get_structure(); let [magbool, magnitudes] = struct.get_list("magnitude"); @@ -72,34 +159,94 @@ var Visualizer = GObject.registerClass( } } - actorInit() { - this._spectBands = this._settings.get_int('total-spects-band'); - this._spectHeight = this._settings.get_int('visualizer-height'); - this._spectWidth = this._settings.get_int('visualizer-width'); - this._actor.height = this._spectHeight; - this._actor.width = this._spectWidth; + /* + * Event Handlers + */ + _onDragBegin() { + this.isDragging = true; + this._dragMonitor = { + dragMotion: this._onDragMotion.bind(this) + }; + DND.addDragMonitor(this._dragMonitor); + let p = this.get_transformed_position(); + this.startX = this.oldX = p[0]; + this.startY = this.oldY = p[1]; + this.get_allocation_box(); + this.rowHeight = this.height; + this.rowWidth = this.width; + } + + _onDragEnd() { + if (this._dragMonitor) { + DND.removeDragMonitor(this._dragMonitor); + this._dragMonitor = null; + } + this.set_position(this.deltaX, this.deltaY); + this.ignoreUpdatePosition = true; + this._settings.set_value('visualizer-location', new GLib.Variant('(ii)', [this.deltaX, this.deltaY])); + this.ignoreUpdatePosition = false; + } + + _onHover() { + if (!this.hover) + this._removeMenuTimeout(); + } + + _onDragMotion(dragEvent) { + this.deltaX = dragEvent.x - (dragEvent.x - this.oldX); + this.deltaY = dragEvent.y - (dragEvent.y - this.oldY); + let p = this.get_transformed_position(); + this.oldX = p[0]; + this.oldY = p[1]; + return DND.DragMotionResult.CONTINUE; + } + + vfunc_button_press_event() { + let event = Clutter.get_current_event(); + if (event.get_button() === 1) + this._setPopupTimeout(); + else if (event.get_button() === 3) { + this._popupMenu(); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE; } + /* + * Drawing and Rendering Methods + */ drawStuff(area) { - let values = this.getSpectBands(); let [width, height] = area.get_surface_size(); let cr = area.get_context(); + let values = this.getSpectBands(); let lineW = this._settings.get_int('spects-line-width'); - let flip = this._settings.get_boolean('flip-visualizer'); + let horizontal_flip = this._settings.get_boolean('horizontal-flip'); + let vertical_flip = this._settings.get_boolean('flip-visualizer'); + let [r, g, b, a] = this._settings.get_string('visualizer-color').split(',').map(parseFloat); + cr.setLineWidth(lineW); + for (let i = 0; i < values; i++) { - cr.setSourceRGBA(1, this._freq[i] / 80, 1, 1); - cr.setLineWidth(lineW); - if (!flip) { - cr.moveTo(lineW / 2 + i * width / values, height); - cr.lineTo(lineW / 2 + i * width / values, height - 1); - cr.lineTo(lineW / 2 + i * width / values, height * this._freq[i] / 80); - } else { - cr.moveTo(lineW / 2 + i * width / values, 0); - cr.lineTo(lineW / 2 + i * width / values, 1); - cr.lineTo(lineW / 2 + i * width / values, height / 80 * (80 - this._freq[i])); - } + let normalizedFreq = Math.max(Math.min(this._freq[i] / MAX_FREQUENCY, MAX_RANGE), MIN_RANGE); + let intensity = Math.pow(normalizedFreq, SQRT_VALUE); + intensity = Math.max(intensity, MIN_INTENSITY); + + let horizontalFlip = this._settings.get_boolean('horizontal-flip'); + let positionFactor = horizontalFlip ? MIRROR_VALUE - (i / (values - MIRROR_VALUE)) : i / (values - MIRROR_VALUE); + let blendFactor = horizontalFlip ? MIRROR_VALUE - positionFactor : positionFactor; + let blendedR = r + blendFactor * (MAX_COLOR_VALUE - r); + let blendedG = g + blendFactor * (MAX_COLOR_VALUE - g); + let blendedB = b + blendFactor * (MAX_COLOR_VALUE - b); + cr.setSourceRGBA(blendedR * intensity, blendedG * intensity, blendedB * intensity, a); + + let xPosition = horizontal_flip ? width - (lineW / MIDDLE_DIVISOR + i * width / values) : lineW / MIDDLE_DIVISOR + i * width / values; + let yPosition = vertical_flip ? height / VERTICAL_FLIP_FACTOR * (VERTICAL_FLIP_FACTOR - this._freq[i]) : height * this._freq[i] / MAX_FREQUENCY; + + cr.moveTo(xPosition, vertical_flip ? START_DRAW_Y_VALUE : height); + cr.lineTo(xPosition, vertical_flip ? END_DRAW_Y_VALUE : height - END_DRAW_Y_VALUE); + cr.lineTo(xPosition, yPosition); cr.stroke(); } + cr.$dispose(); } @@ -107,12 +254,14 @@ var Visualizer = GObject.registerClass( this._actor.queue_repaint(); } - getSpectBands() { - let override = this._settings.get_boolean('spect-over-ride-bool'); - let values = this._settings.get_int('spect-over-ride'); - return (!override) ? this._spectBands : (values <= this._spectBands) ? values : this._spectBands + _updateFlipSettings() { + this._horizontalFlip = this._settings.get_boolean('horizontal-flip'); + this._update(); } + /* + * Utility Methods + */ _getMetaRectForCoords(x, y) { this.get_allocation_box(); let rect = new Meta.Rectangle(); @@ -126,12 +275,6 @@ var Visualizer = GObject.registerClass( return Main.layoutManager.getWorkAreaForMonitor(monitorIndex); } - _isOnScreen(x, y) { - let rect = this._getMetaRectForCoords(x, y); - let monitorWorkArea = this._getWorkAreaForRect(rect); - return monitorWorkArea.contains_rect(rect); - } - _keepOnScreen(x, y) { let rect = this._getMetaRectForCoords(x, y); let monitorWorkArea = this._getWorkAreaForRect(rect); @@ -142,6 +285,72 @@ var Visualizer = GObject.registerClass( return [x, y]; } + _removeMenuTimeout() { + if (this._menuTimeoutId > 0) { + GLib.source_remove(this._menuTimeoutId); + this._menuTimeoutId = 0; + } + } + + _setPopupTimeout() { + this._removeMenuTimeout(); + this._menuTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, POPUP_TIMEOUT, () => { + this._menuTimeoutId = 0; + this._popupMenu(); + return GLib.SOURCE_REMOVE; + }); + GLib.Source.set_name_by_id(this._menuTimeoutId, '[visualizer] this.popupMenu'); + } + + _connectSetting(key, callback) { + this._settings.connect(`changed::${key}`, callback); + } + + _updateRefreshRate() { + let fps = this._settings.get_int('fps'); + this._refreshRate = REFRESH_RATE_BASE / fps; + this.updateGstInterval(); + this.startRefreshLoop(); + } + + /* + * Getters, Setters + */ + getStreams() { + return new Promise((resolve, reject) => { + this._streamId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => { + let control = (MajorVersion < 43) ? Main.panel.statusArea.aggregateMenu._volume._control : Main.panel.statusArea.quickSettings._volume._control; + if (control.get_state() == Gvc.MixerControlState.READY) { + let streams = control.get_streams(); + (streams.length > 0) ? resolve(streams): reject(Error('failure')) + } + return GLib.SOURCE_REMOVE; + }); + }); + } + + getDefaultSrc() { + return new Promise((resolve, reject) => { + this._defaultSrcId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => { + let stream = (MajorVersion < 43) ? Main.panel.statusArea.aggregateMenu._volume._volumeMenu._output.stream : Main.panel.statusArea.quickSettings._volume._output.stream; + (stream !== null) ? resolve(stream.get_name() + '.monitor'): reject(Error('failure')); + return GLib.SOURCE_REMOVE; + }); + }); + } + + getSpectBands() { + let override = this._settings.get_boolean('spect-over-ride-bool'); + let values = this._settings.get_int('spect-over-ride'); + return (!override) ? this._spectBands : (values <= this._spectBands) ? values : this._spectBands + } + + getDragActor() {} + + getDragActorSource() { + return this; + } + setPosition() { if (this._ignorePositionUpdate) return; @@ -163,46 +372,49 @@ var Visualizer = GObject.registerClass( } } - _onDragBegin() { - this.isDragging = true; - this._dragMonitor = { - dragMotion: this._onDragMotion.bind(this) - }; - DND.addDragMonitor(this._dragMonitor); - let p = this.get_transformed_position(); - this.startX = this.oldX = p[0]; - this.startY = this.oldY = p[1]; - this.get_allocation_box(); - this.rowHeight = this.height; - this.rowWidth = this.width; + /* + * Lifecycle-related Methods + */ + startRefreshLoop() { + if (this._refreshLoopId !== null) { + GLib.Source.remove(this._refreshLoopId); + } + this._refreshLoopId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._refreshRate, () => { + this._actor.queue_repaint(); + return true; + }); } - _onDragMotion(dragEvent) { - this.deltaX = dragEvent.x - (dragEvent.x - this.oldX); - this.deltaY = dragEvent.y - (dragEvent.y - this.oldY); - let p = this.get_transformed_position(); - this.oldX = p[0]; - this.oldY = p[1]; - return DND.DragMotionResult.CONTINUE; + settingsChanged() { + this._settings.connect('changed::visualizer-location', () => this.setPosition()); + this._settings.connect('changed::total-spects-band', () => { + this.actorInit(); + this._spectrum.set_property("bands", this._spectBands); + this._update(); + }); + this._settings.connect('changed::visualizer-height', () => { + this.actorInit(); + this._update(); + }); + this._settings.connect('changed::visualizer-width', () => { + this.actorInit(); + this._update(); + }); + this._settings.connect('changed::spect-over-ride', () => this.getSpectBands()); + this._settings.connect('changed::spect-over-ride-bool', () => this.getSpectBands()); + this._settings.connect('changed::spects-line-width', () => this._update()); } - _onDragEnd() { - if (this._dragMonitor) { - DND.removeDragMonitor(this._dragMonitor); - this._dragMonitor = null; + _removeSource(src) { + if (src) { + GLib.Source.remove(src); + src = null; } - this.set_position(this.deltaX, this.deltaY); - this.ignoreUpdatePosition = true; - this._settings.set_value('visualizer-location', new GLib.Variant('(ii)', [this.deltaX, this.deltaY])); - this.ignoreUpdatePosition = false; - } - - getDragActor() {} - - getDragActorSource() { - return this; } + /* + * Async Methods + */ async getMenuItems() { try { this._menuItems = []; @@ -234,67 +446,14 @@ var Visualizer = GObject.registerClass( } } - getDefaultSrc() { - return new Promise((resolve, reject) => { - this._defaultSrcId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => { - let stream = (major < 43) ? Main.panel.statusArea.aggregateMenu._volume._volumeMenu._output.stream : Main.panel.statusArea.quickSettings._volume._output.stream; - (stream !== null) ? resolve(stream.get_name() + '.monitor'): reject(Error('failure')); - return GLib.SOURCE_REMOVE; - }); - }); - } - - getStreams() { - return new Promise((resolve, reject) => { - this._streamId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => { - let control = (major < 43) ? Main.panel.statusArea.aggregateMenu._volume._control : Main.panel.statusArea.quickSettings._volume._control; - if (control.get_state() == Gvc.MixerControlState.READY) { - let streams = control.get_streams(); - (streams.length > 0) ? resolve(streams): reject(Error('failure')) - } - return GLib.SOURCE_REMOVE; - }); - }); - } - - vfunc_button_press_event() { - let event = Clutter.get_current_event(); - if (event.get_button() === 1) - this._setPopupTimeout(); - else if (event.get_button() === 3) { - this._popupMenu(); - return Clutter.EVENT_STOP; - } - return Clutter.EVENT_PROPAGATE; - } - - _onHover() { - if (!this.hover) - this._removeMenuTimeout(); - } - - _removeMenuTimeout() { - if (this._menuTimeoutId > 0) { - GLib.source_remove(this._menuTimeoutId); - this._menuTimeoutId = 0; - } - } - - _setPopupTimeout() { - this._removeMenuTimeout(); - this._menuTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 600, () => { - this._menuTimeoutId = 0; - this._popupMenu(); - return GLib.SOURCE_REMOVE; - }); - GLib.Source.set_name_by_id(this._menuTimeoutId, '[visualizer] this.popupMenu'); - } - + /* + * Miscellaneous Methods + */ _popupMenu() { this._removeMenuTimeout(); if (!this._menu) { this._subMenuItem = []; - this._menu = new PopupMenu.PopupMenu(this, 0.5, St.Side.TOP); + this._menu = new PopupMenu.PopupMenu(this, MENU_POSITION_Y, MENU_SIDE); let srcDevice = new PopupMenu.PopupSubMenuMenuItem('Change Audio Source'); this._menu.addMenuItem(srcDevice); for (let i = 0; i < this._menuItems.length; i++) { @@ -322,38 +481,9 @@ var Visualizer = GObject.registerClass( return false; } - onDestroy() { - this._removeSource(this._menuTimeoutId); - this._removeSource(this._streamId); - this._removeSource(this._defaultSrcId); - this._pipeline.set_state(Gst.State.NULL); - Main.layoutManager._backgroundGroup.remove_child(this); - } - - settingsChanged() { - this._settings.connect('changed::visualizer-location', () => this.setPosition()); - this._settings.connect('changed::total-spects-band', () => { - this.actorInit(); - this._spectrum.set_property("bands", this._spectBands); - this._update(); - }); - this._settings.connect('changed::visualizer-height', () => { - this.actorInit(); - this._update(); - }); - this._settings.connect('changed::visualizer-width', () => { - this.actorInit(); - this._update(); - }); - this._settings.connect('changed::spect-over-ride', () => this.getSpectBands()); - this._settings.connect('changed::spect-over-ride-bool', () => this.getSpectBands()); - this._settings.connect('changed::spects-line-width', () => this._update()); - } - - _removeSource(src) { - if (src) { - GLib.Source.remove(src); - src = null; - } + _isOnScreen(x, y) { + let rect = this._getMetaRectForCoords(x, y); + let monitorWorkArea = this._getWorkAreaForRect(rect); + return monitorWorkArea.contains_rect(rect); } });