[gnome-sound-recorder] row: add waveform seekbar
- From: Bilal Elmoussaoui <bilelmoussaoui src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-sound-recorder] row: add waveform seekbar
- Date: Sat, 8 Aug 2020 15:46:20 +0000 (UTC)
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]