[gnome-sound-recorder] row: add waveform seekbar



commit c532f211345b7656a9a2a0ccc8059286a030f8b8
Author: Kavan Mevada <kavanmevada gmail com>
Date:   Wed Jul 29 21:50:14 2020 +0530

    row: add waveform seekbar

 data/ui/row.ui                                   |   2 +-
 meson.build                                      |   1 +
 po/POTFILES.in                                   |   1 -
 src/application.js                               |   4 +-
 src/main.js                                      |   1 +
 src/org.gnome.SoundRecorder.src.gresource.xml.in |   1 -
 src/player.js                                    | 121 -----------------------
 src/recorder.js                                  |  15 ++-
 src/recording.js                                 |  50 +++++++++-
 src/recordingsListBox.js                         |  45 +++++++--
 src/row.js                                       |  25 ++++-
 src/waveform.js                                  | 109 ++++++++++++++++----
 src/window.js                                    |  15 ++-
 13 files changed, 225 insertions(+), 165 deletions(-)
---
diff --git a/data/ui/row.ui b/data/ui/row.ui
index 624260f0..c9a61c5a 100644
--- a/data/ui/row.ui
+++ b/data/ui/row.ui
@@ -127,7 +127,7 @@
             <property name="visible">True</property>
             <property name="can_focus">False</property>
             <child>
-              <object class="GtkBox">
+              <object class="GtkBox" id="optionBox">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
                 <property name="orientation">vertical</property>
diff --git a/meson.build b/meson.build
index 34750a6f..6580ef0c 100644
--- a/meson.build
+++ b/meson.build
@@ -38,6 +38,7 @@ gjs_console = gjs_dep.get_pkgconfig_variable('gjs_console')
 dependency('gio-2.0', version: '>= 2.43.4')
 dependency('glib-2.0', version: '>= 2.39.3')
 dependency('gtk+-3.0', version: '>= 3.13.2')
+dependency('gstreamer-player-1.0', version: '>= 1.12')
 dependency('libhandy-1', version: '>= 0.80.0')
 dependency('gobject-introspection-1.0', version: '>= 1.31.6')
 
diff --git a/po/POTFILES.in b/po/POTFILES.in
index a476c180..fa54f7c7 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -7,7 +7,6 @@ data/ui/row.ui
 data/ui/window.ui
 src/application.js
 src/main.js
-src/player.js
 src/recorder.js
 src/row.js
 src/utils.js
diff --git a/src/application.js b/src/application.js
index bab5b6d0..079e5656 100644
--- a/src/application.js
+++ b/src/application.js
@@ -1,4 +1,4 @@
-/* exported Application RecordingsDir Settings */
+/* exported Application RecordingsDir CacheDir Settings */
 /*
 * Copyright 2013 Meg Ford
 * This library is free software; you can redistribute it and/or
@@ -22,6 +22,7 @@ const { Gdk, Gio, GLib, GObject, Gst, Gtk, Handy } = imports.gi;
 
 /* Translators: "Recordings" here refers to the name of the directory where the application places files */
 var RecordingsDir = Gio.file_new_for_path(GLib.build_filenamev([GLib.get_home_dir(), _('Recordings')]));
+var CacheDir = Gio.file_new_for_path(GLib.build_filenamev([GLib.get_user_cache_dir(), pkg.name]));
 var Settings = new Gio.Settings({ schema: pkg.name });
 
 const { Window } = imports.window;
@@ -139,6 +140,7 @@ var Application = GObject.registerClass(class Application extends Gtk.Applicatio
     ensureDirectory() {
         // Ensure Recordings directory
         GLib.mkdir_with_parents(RecordingsDir.get_path(), 0o0755);
+        GLib.mkdir_with_parents(CacheDir.get_path(), 0o0755);
     }
 
     vfunc_activate() {
diff --git a/src/main.js b/src/main.js
index 8fdfd226..3b42332e 100644
--- a/src/main.js
+++ b/src/main.js
@@ -36,6 +36,7 @@ pkg.require({
     'Gtk': '3.0',
     'Gst': '1.0',
     'GstAudio': '1.0',
+    'GstPlayer': '1.0',
     'GstPbutils': '1.0',
     'Handy': '1',
 });
diff --git a/src/org.gnome.SoundRecorder.src.gresource.xml.in 
b/src/org.gnome.SoundRecorder.src.gresource.xml.in
index eb48cd7d..1495586c 100644
--- a/src/org.gnome.SoundRecorder.src.gresource.xml.in
+++ b/src/org.gnome.SoundRecorder.src.gresource.xml.in
@@ -8,7 +8,6 @@
     <file>recording.js</file>
     <file>row.js</file>
     <file>main.js</file>
-    <file>player.js</file>
     <file>recorder.js</file>
     <file>waveform.js</file>
     <file>window.js</file>
diff --git a/src/recorder.js b/src/recorder.js
index 31468bc0..d50bf742 100644
--- a/src/recorder.js
+++ b/src/recorder.js
@@ -18,7 +18,6 @@
  *
  */
 const { GLib, GObject, Gst, GstPbutils } = imports.gi;
-
 const { RecordingsDir, Settings } = imports.application;
 const { Recording } = imports.recording;
 
@@ -70,6 +69,7 @@ var Recorder = new GObject.registerClass({
     },
 }, class Recorder extends GObject.Object {
     _init() {
+        this.peaks = [];
         super._init({});
 
         let srcElement, audioConvert, caps;
@@ -100,7 +100,9 @@ var Recorder = new GObject.registerClass({
     }
 
     start() {
+        this.peaks.length = 0;
         let index = 1;
+
         do {
             /* Translators: ""Recording %d"" is the default name assigned to a file created
             by the application (for example, "Recording 1"). */
@@ -154,8 +156,14 @@ var Recorder = new GObject.registerClass({
             this.recordBus = null;
         }
 
-        return this.file && this.file.query_exists(null)
-            ? new Recording(this.file) : null;
+
+        if (this.file && this.file.query_exists(null) && this.peaks.length > 0) {
+            const recording = new Recording(this.file);
+            recording.peaks = this.peaks;
+            return recording;
+        }
+
+        return null;
     }
 
     _onMessageReceived(message) {
@@ -225,6 +233,7 @@ var Recorder = new GObject.registerClass({
             peak = 0;
 
         this._current_peak = Math.pow(10, peak / 20);
+        this.peaks.push(this._current_peak);
         this.notify('current-peak');
     }
 
diff --git a/src/recording.js b/src/recording.js
index 05b8117f..d490bde4 100644
--- a/src/recording.js
+++ b/src/recording.js
@@ -1,7 +1,12 @@
 /* exported Recording */
-const { GLib, GObject, GstPbutils } = imports.gi;
+const { Gio, GLib, GObject, GstPbutils } = imports.gi;
+const { CacheDir } = imports.application;
+const ByteArray = imports.byteArray;
 
 var Recording = new GObject.registerClass({
+    Signals: {
+        'peaks-updated': {},
+    },
     Properties: {
         'duration': GObject.ParamSpec.int(
             'duration',
@@ -16,8 +21,9 @@ var Recording = new GObject.registerClass({
     },
 }, class Recording extends GObject.Object {
     _init(file) {
-        super._init({});
         this._file = file;
+        this._peaks = [];
+        super._init({});
 
         let info = file.query_info('time::created,time::modified', 0, null);
 
@@ -26,6 +32,8 @@ var Recording = new GObject.registerClass({
         this._timeModified = GLib.DateTime.new_from_unix_local(timeModified);
         this._timeCreated = GLib.DateTime.new_from_unix_local(timeCreated);
 
+        this.readPeaks();
+
         var discoverer = new GstPbutils.Discoverer();
         discoverer.start();
         discoverer.connect('discovered', (_discoverer, audioInfo) => {
@@ -70,8 +78,44 @@ var Recording = new GObject.registerClass({
         return this._file.get_uri();
     }
 
+    // eslint-disable-next-line camelcase
+    set peaks(data) {
+        if (data.length > 0) {
+            this._peaks = data;
+            this.emit('peaks-updated');
+            const buffer = new GLib.Bytes(JSON.stringify(data));
+            this.waveformCache.replace_contents_bytes_async(buffer, null, false, 
Gio.FileCreateFlags.REPLACE_DESTINATION, null, (obj, res) => {
+                obj.replace_contents_finish(res);
+            });
+        }
+    }
+
+    // eslint-disable-next-line camelcase
+    get peaks() {
+        return this._peaks;
+    }
+
     delete() {
-        return this._file.trash_async(GLib.PRIORITY_DEFAULT, null, null);
+        this._file.trash_async(GLib.PRIORITY_HIGH, null, null);
+        this.waveformCache.trash_async(GLib.PRIORITY_DEFAULT, null, null);
+    }
+
+    get waveformCache() {
+        return CacheDir.get_child(`${this.name}_data`);
+    }
+
+    readPeaks() {
+        if (this.waveformCache.query_exists(null)) {
+            this.waveformCache.load_bytes_async(null, (obj, res) => {
+                const bytes = obj.load_bytes_finish(res)[0];
+                try {
+                    this._peaks = JSON.parse(ByteArray.toString(bytes.get_data()));
+                    this.emit('peaks-updated');
+                } catch (error) {
+                    log(`Error reading waveform data file: ${this.name}_data`);
+                }
+            });
+        }
     }
 
 });
diff --git a/src/recordingsListBox.js b/src/recordingsListBox.js
index e4638db5..fd8b0132 100644
--- a/src/recordingsListBox.js
+++ b/src/recordingsListBox.js
@@ -1,5 +1,5 @@
 /* exported RecordingsListBox */
-const { GObject, Gtk, Gst } = imports.gi;
+const { GObject, GstPlayer, Gtk, Gst } = imports.gi;
 const { Row, RowState } = imports.row;
 
 var RecordingsListBox = new GObject.registerClass(class RecordingsListBox extends Gtk.ListBox {
@@ -16,30 +16,55 @@ var RecordingsListBox = new GObject.registerClass(class RecordingsListBox extend
 
         this.get_style_context().add_class('preferences');
 
-        this._player.connect('notify::state', _player => {
-            if (_player.state === Gst.State.NULL && this.activePlayingRow)
+        this._player.connect('state-changed', (_player, state) => {
+            if (state === GstPlayer.PlayerState.STOPPED && this.activePlayingRow) {
                 this.activePlayingRow.state = RowState.PAUSED;
+                this.activePlayingRow.waveform.position = 0.0;
+            } else if (state === GstPlayer.PlayerState.PLAYING) {
+                this.activePlayingRow.state = RowState.PLAYING;
+            }
+        });
+
+        this._player.connect('position-updated', (_player, pos) => {
+            const duration = this.activePlayingRow._recording.duration;
+            this.activePlayingRow.waveform.position = pos / duration;
         });
 
         this.bind_model(model, recording => {
             let row = new Row(recording);
+
+            row.waveform.connect('position-changed', (_wave, _position) => {
+                this._player.seek(_position * row._recording.duration);
+            });
+
             row.connect('play', _row => {
-                if (this.activePlayingRow && this.activePlayingRow !== _row)
-                    this.activePlayingRow.state = RowState.PAUSED;
+                if (this.activePlayingRow) {
+                    if (this.activePlayingRow !== _row) {
+                        this.activePlayingRow.state = RowState.PAUSED;
+                        this.activePlayingRow.waveform.position = 0.0;
+                        this._player.set_uri(recording.uri);
+                    }
+                } else {
+                    this._player.set_uri(recording.uri);
+                }
 
-                player.play(recording.uri);
                 this.activePlayingRow = _row;
+                this._player.play();
             });
 
             row.connect('pause', _row => {
                 this._player.pause();
             });
 
-            row.connect('seek-backward', _ => {
-                player.position -= 10 * Gst.SECOND;
+            row.connect('seek-backward', _row => {
+                let position = this._player.position - 10 * Gst.SECOND;
+                position = position < 0 || position > _row._recording.duration ? 0 : position;
+                this._player.seek(position);
             });
-            row.connect('seek-forward', _ => {
-                player.position += 10 * Gst.SECOND;
+            row.connect('seek-forward', _row => {
+                let position = this._player.position + 10 * Gst.SECOND;
+                position = position < 0 || position > _row._recording.duration ? 0 : position;
+                this._player.seek(position);
             });
 
             row.connect('deleted', () => {
diff --git a/src/row.js b/src/row.js
index 0319ab89..c7c9dd52 100644
--- a/src/row.js
+++ b/src/row.js
@@ -1,6 +1,7 @@
 /* exported Row */
 const { Gdk, GObject, Gst, Gtk } = imports.gi;
 const { displayDateTime, formatTime } = imports.utils;
+const { WaveForm, WaveType } = imports.waveform;
 
 var RowState = {
     PLAYING: 0,
@@ -9,7 +10,7 @@ var RowState = {
 
 var Row = GObject.registerClass({
     Template: 'resource:///org/gnome/SoundRecorder/ui/row.ui',
-    InternalChildren: ['playbackStack', 'mainStack', 'playButton', 'pauseButton', 'name', 'entry', 'date', 
'duration', 'saveBtn', 'revealer', 'seekBackward', 'seekForward', 'renameStack', 'renameBtn', 'deleteBtn'],
+    InternalChildren: ['playbackStack', 'mainStack', 'playButton', 'pauseButton', 'name', 'entry', 'date', 
'duration', 'saveBtn', 'revealer', 'optionBox', 'seekBackward', 'seekForward', 'renameStack', 'renameBtn', 
'deleteBtn'],
     Signals: {
         'play': { param_types: [GObject.TYPE_STRING] },
         'pause': {},
@@ -30,6 +31,18 @@ var Row = GObject.registerClass({
         this._expanded = false;
         super._init({});
 
+        this.waveform = new WaveForm({
+            hexpand: true,
+            halign: Gtk.Align.FILL,
+            margin_bottom: 24,
+            height_request: 60,
+            margin_left: 12,
+            margin_right: 12,
+        }, WaveType.PLAYER);
+
+        this._optionBox.add(this.waveform);
+        this._optionBox.reorder_child(this.waveform, 0);
+
         recording.bind_property('name', this._name, 'label', GObject.BindingFlags.SYNC_CREATE | 
GObject.BindingFlags.DEFAULT);
         recording.bind_property('name', this._entry, 'text', GObject.BindingFlags.SYNC_CREATE | 
GObject.BindingFlags.DEFAULT);
         this.bind_property('expanded', this._revealer, 'reveal_child', GObject.BindingFlags.SYNC_CREATE | 
GObject.BindingFlags.DEFAULT);
@@ -49,10 +62,20 @@ var Row = GObject.registerClass({
         else
             this._date.label = displayDateTime(recording.timeModified);
 
+        this.waveform.peaks = this._recording.peaks;
+        this._recording.connect('peaks-updated', _ => {
+            this.waveform.peaks = this._recording.peaks;
+        });
+
         recording.connect('notify::duration', () => {
             this._duration.label = formatTime(recording.duration / Gst.SECOND);
         });
 
+        this.waveform.connect('button-press-event', _ => {
+            this.emit('pause');
+            this.state = RowState.PAUSED;
+        });
+
         this._playButton.connect('clicked', () => {
             this.emit('play', recording.uri);
             this.state = RowState.PLAYING;
diff --git a/src/waveform.js b/src/waveform.js
index 869f21e9..6484321b 100644
--- a/src/waveform.js
+++ b/src/waveform.js
@@ -23,67 +23,140 @@
 
 // based on code from Pitivi
 
-const { GObject, Gtk } = imports.gi;
+const { Gdk, GObject, Gtk } = imports.gi;
 const Cairo = imports.cairo;
 
+var WaveType = {
+    RECORDER: 0,
+    PLAYER: 1,
+};
+
 const GUTTER = 4;
 
 var WaveForm = GObject.registerClass({
     Properties: {
+        'position': GObject.ParamSpec.float(
+            'position',
+            'Waveform position', 'Waveform position',
+            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
+            0.0, 1.0, 0.0),
         'peak': GObject.ParamSpec.float(
             'peak',
             'Waveform current peak', 'Waveform current peak in float [0, 1]',
             GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
             0.0, 1.0, 0.0),
     },
+    Signals: {
+        'position-changed': {  param_types: [GObject.TYPE_FLOAT]  },
+    },
 }, class WaveForm extends Gtk.DrawingArea {
-    _init() {
-        this.peaks = [];
-        super._init({
-            vexpand: true,
-            valign: Gtk.Align.FILL,
-        });
+    _init(params, type) {
+        this._peaks = [];
+        this._position = 0;
+        this.lastPosition = 0;
+        this.waveType = type;
+        super._init(params);
+
+        if (this.waveType === WaveType.PLAYER) {
+            this.add_events(Gdk.EventMask.BUTTON_PRESS_MASK |
+                Gdk.EventMask.BUTTON_RELEASE_MASK |
+                Gdk.EventMask.BUTTON1_MOTION_MASK);
+        }
+
+
         this.show();
     }
 
+    vfunc_button_press_event(event) {
+        this._startX = event.x;
+        return true;
+    }
+
+    vfunc_motion_notify_event(event) {
+        this._position = this._clamped(event.x - this._startX + this._lastX);
+        this.queue_draw();
+        return true;
+    }
+
+    vfunc_button_release_event(_) {
+        this._lastX = this._position;
+        this.emit('position-changed', this.position);
+        return true;
+    }
+
     vfunc_draw(ctx) {
         const maxHeight = this.get_allocated_height();
         const vertiCenter = maxHeight / 2;
         const horizCenter = this.get_allocated_width() / 2;
 
-        let pointer = horizCenter;
+        let pointer = horizCenter + this._position;
 
         ctx.setLineCap(Cairo.LineCap.ROUND);
         ctx.setLineWidth(2);
-        ctx.setSourceRGBA(255, 0, 0, 1);
+
+        if (this.waveType === WaveType.PLAYER)
+            ctx.setSourceRGB(28 / 255, 113 / 255, 216 / 255);
+        else
+            ctx.setSourceRGB(1, 0, 0);
 
         ctx.moveTo(horizCenter, vertiCenter - maxHeight);
         ctx.lineTo(horizCenter, vertiCenter + maxHeight);
         ctx.stroke();
 
         ctx.setLineWidth(1);
-        ctx.setSourceRGBA(0, 0, 0, 1);
-        for (let index = this.peaks.length; index > 0; index--) {
-            const peak = this.peaks[index];
+
+        this._peaks.forEach(peak => {
+            if (pointer > horizCenter)
+                ctx.setSourceRGB(192 / 255, 191 / 255, 188 / 255);
+            else
+                ctx.setSourceRGB(46 / 255, 52 / 255, 54 / 255);
 
             ctx.moveTo(pointer, vertiCenter + peak * maxHeight);
             ctx.lineTo(pointer, vertiCenter - peak * maxHeight);
             ctx.stroke();
 
-            pointer -= GUTTER;
-        }
+            if (this.waveType === WaveType.PLAYER)
+                pointer += GUTTER;
+            else
+                pointer -= GUTTER;
+        });
     }
 
     set peak(p) {
-        if (this.peaks.length > this.get_allocated_width() / (2 * GUTTER))
-            this.peaks.shift();
+        if (this._peaks.length > this.get_allocated_width() / (2 * GUTTER))
+            this._peaks.pop();
 
-        this.peaks.push(p.toFixed(2));
+        this._peaks.unshift(p.toFixed(2));
         this.queue_draw();
     }
 
+    set peaks(p) {
+        this._peaks = p;
+        this.queue_draw();
+    }
+
+    set position(pos) {
+        this._position = this._clamped(-pos * this._peaks.length * GUTTER);
+        this._lastX = this._position;
+        this.queue_draw();
+        this.notify('position');
+    }
+
+    get position() {
+        return -this._position / (this._peaks.length * GUTTER);
+    }
+
+    _clamped(position) {
+        if (position > 0)
+            position = 0;
+        else if (position < -this._peaks.length * GUTTER)
+            position = -this._peaks.length * GUTTER;
+
+        return position;
+    }
+
     destroy() {
-        this.peaks.length = 0;
+        this._peaks.length = 0;
         this.queue_draw();
     }
 });
diff --git a/src/window.js b/src/window.js
index 32c3adb2..17e56266 100644
--- a/src/window.js
+++ b/src/window.js
@@ -18,14 +18,13 @@
 *
 */
 
-const { GObject, Handy } = imports.gi;
+const { GObject, GstPlayer, Gtk, Handy } = imports.gi;
 
-const { Player } = imports.player;
 const { Recorder } = imports.recorder;
 const { RecordingList } = imports.recordingList;
 const { RecordingsListBox } = imports.recordingsListBox;
 const { formatTime } = imports.utils;
-const { WaveForm } = imports.waveform;
+const { WaveForm, WaveType } = imports.waveform;
 
 
 var Window = GObject.registerClass({
@@ -39,10 +38,16 @@ var Window = GObject.registerClass({
         }, params));
 
         this.recorder = new Recorder();
-        this.player = new Player();
-        this.waveform = new WaveForm();
+        this.waveform = new WaveForm({
+            vexpand: true,
+            valign: Gtk.Align.FILL,
+        }, WaveType.RECORDER);
         this._recorderBox.add(this.waveform);
 
+        const dispatcher = GstPlayer.PlayerGMainContextSignalDispatcher.new(null);
+        this.player = GstPlayer.Player.new(null, dispatcher);
+        this.player.connect('end-of-stream', _p => this.player.stop());
+
         this.recorder.bind_property('current-peak', this.waveform, 'peak', GObject.BindingFlags.SYNC_CREATE 
| GObject.BindingFlags.DEFAULT);
         this.recorder.connect('notify::duration', _recorder => {
             this._recorderTime.label = formatTime(_recorder.duration);


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]