Cue.POINT,
18 | * Cue.SEGMENT_START, or Cue.SEGMENT_END.
19 | * @param {String} id The id of the {@link Point} or {@link Segment}.
20 | */
21 |
22 | function Cue(time, type, id) {
23 | this.time = time;
24 | this.type = type;
25 | this.id = id;
26 | }
27 |
28 | /**
29 | * @constant
30 | * @type {Number}
31 | */
32 |
33 | Cue.POINT = 0;
34 | Cue.SEGMENT_START = 1;
35 | Cue.SEGMENT_END = 2;
36 |
37 | /**
38 | * Callback function for use with Array.prototype.sort().
39 | *
40 | * @static
41 | * @param {Cue} a
42 | * @param {Cue} b
43 | * @return {Number}
44 | */
45 |
46 | Cue.sorter = function(a, b) {
47 | return a.time - b.time;
48 | };
49 |
50 | return Cue;
51 | });
52 |
--------------------------------------------------------------------------------
/src/main/views/waveform.timecontroller.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file
3 | *
4 | * Defines the {@link TimeController} class.
5 | *
6 | * @module peaks/views/waveform.timecontroller.js
7 | */
8 |
9 | define([], function() {
10 | 'use strict';
11 |
12 | /**
13 | * Creates an object to get/set the playback position. This interface is
14 | * deprecated, use the {@link Player} interface instead.
15 | *
16 | * @class
17 | * @alias TimeController
18 | *
19 | * @param {Peaks} peaks
20 | */
21 |
22 | function TimeController(peaks) {
23 | this._peaks = peaks;
24 | }
25 |
26 | TimeController.prototype.setCurrentTime = function(time) {
27 | // eslint-disable-next-line max-len
28 | this._peaks.options.deprecationLogger('peaks.time.setCurrentTime(): this function is deprecated. Call peaks.player.seek() instead');
29 | return this._peaks.player.seek(time);
30 | };
31 |
32 | TimeController.prototype.getCurrentTime = function() {
33 | // eslint-disable-next-line max-len
34 | this._peaks.options.deprecationLogger('peaks.time.getCurrentTime(): this function is deprecated. Call peaks.player.getCurrentTime() instead');
35 | return this._peaks.player.getCurrentTime();
36 | };
37 |
38 | return TimeController;
39 | });
40 |
--------------------------------------------------------------------------------
/test/unit/waveform-segments-spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('./setup');
4 |
5 | var Peaks = require('../../src/main');
6 |
7 | describe('SegmentsLayer', function() {
8 | var p;
9 |
10 | beforeEach(function(done) {
11 | p = Peaks.init({
12 | container: document.getElementById('container'),
13 | mediaElement: document.getElementById('media'),
14 | dataUri: {
15 | json: 'base/test_data/sample.json'
16 | },
17 | keyboard: true,
18 | height: 240
19 | });
20 |
21 | p.on('peaks.ready', done);
22 | });
23 |
24 | afterEach(function() {
25 | if (p) {
26 | p.destroy();
27 | }
28 | });
29 |
30 | describe('segments.add', function() {
31 | it('should redraw the view after adding a segment that is visible', function() {
32 | var zoomview = p.views.getView('zoomview');
33 |
34 | expect(zoomview).to.be.ok;
35 |
36 | var spy = sinon.spy(zoomview._segmentsLayer._layer, 'draw');
37 |
38 | p.segments.add({ startTime: 0, endTime: 10, editable: true, id: 'segment1' });
39 |
40 | expect(spy.callCount).to.equal(1);
41 | });
42 |
43 | it('should not redraw the view after adding a segment that is not visible', function() {
44 | var zoomview = p.views.getView('zoomview');
45 |
46 | expect(zoomview).to.be.ok;
47 |
48 | var spy = sinon.spy(zoomview._segmentsLayer._layer, 'draw');
49 |
50 | p.segments.add({ startTime: 28, endTime: 32, editable: true, id: 'segment2' });
51 |
52 | expect(spy.callCount).to.equal(0);
53 | });
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/test/unit/api-view-apec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('./setup');
4 |
5 | var Peaks = require('../../src/main');
6 |
7 | describe('WaveformView', function() {
8 | var p;
9 |
10 | beforeEach(function(done) {
11 | p = Peaks.init({
12 | container: document.getElementById('container'),
13 | mediaElement: document.getElementById('media'),
14 | dataUri: 'base/test_data/sample.json'
15 | });
16 |
17 | p.on('peaks.ready', done);
18 | });
19 |
20 | afterEach(function() {
21 | if (p) {
22 | p.destroy();
23 | p = null;
24 | }
25 | });
26 |
27 | describe('setAmplitudeScale', function() {
28 | ['zoomview', 'overview'].forEach(function(viewName) {
29 | describe(viewName, function() {
30 | it('should set the amplitude scale to default', function() {
31 | var view = p.views.getView(viewName);
32 |
33 | expect(function() {
34 | view.setAmplitudeScale(1.0);
35 | }).to.not.throw;
36 | });
37 |
38 | it('should throw if no scale is given', function() {
39 | var view = p.views.getView(viewName);
40 |
41 | expect(function() {
42 | view.setAmplitudeScale();
43 | }).to.throw(/Scale must be a valid number/);
44 | });
45 |
46 | it('should throw if an invalid scale is given', function() {
47 | var view = p.views.getView(viewName);
48 |
49 | expect(function() {
50 | view.setAmplitudeScale('test');
51 | }).to.throw(/Scale must be a valid number/);
52 | });
53 |
54 | it('should throw if an invalid number is given', function() {
55 | var view = p.views.getView(viewName);
56 |
57 | expect(function() {
58 | view.setAmplitudeScale(Infinity);
59 | }).to.throw(/Scale must be a valid number/);
60 | });
61 | });
62 | });
63 | });
64 | });
65 |
--------------------------------------------------------------------------------
/test/unit/point-spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('./setup');
4 |
5 | var Peaks = require('../../src/main');
6 | var Point = require('../../src/main/markers/point');
7 |
8 | describe('Point', function() {
9 | describe('update()', function() {
10 | var p;
11 |
12 | beforeEach(function(done) {
13 | p = Peaks.init({
14 | container: document.getElementById('container'),
15 | mediaElement: document.getElementById('media'),
16 | dataUri: { arraybuffer: 'base/test_data/sample.dat' }
17 | });
18 |
19 | p.on('peaks.ready', done);
20 | });
21 |
22 | afterEach(function() {
23 | if (p) {
24 | p.destroy();
25 | }
26 | });
27 |
28 | it('should be possible to update all properties programatically', function() {
29 | p.points.add({ time: 10, editable: true, color: '#ff0000', labelText: 'A point' });
30 |
31 | var newLabelText = 'new label text';
32 | var newTime = 12;
33 | var point = p.points.getPoints()[0];
34 |
35 | point.update({
36 | time: newTime,
37 | labelText: newLabelText
38 | });
39 |
40 | expect(point.time).to.equal(newTime);
41 | expect(point.labelText).to.equal(newLabelText);
42 | });
43 |
44 | it('should not allow invalid updates', function() {
45 | p.points.add({ time: 10, editable: true, color: '#ff0000', labelText: 'A point' });
46 |
47 | var point = p.points.getPoints()[0];
48 |
49 | expect(function() {
50 | point.update({ time: NaN });
51 | }).to.throw(TypeError);
52 |
53 | expect(function() {
54 | point.update({ time: -10 });
55 | }).to.throw(TypeError);
56 |
57 | point.update({ labelText: undefined });
58 | expect(point.labelText).to.equal('');
59 | });
60 | });
61 |
62 | describe('isVisible', function() {
63 | it('should return false if point is before visible range', function() {
64 | var point = new Point({}, 'point.1', 9.0);
65 |
66 | expect(point.isVisible(10.0, 20.0)).to.equal(false);
67 | });
68 |
69 | it('should return false if point is after visible range', function() {
70 | var point = new Point({}, 'point.1', 20.0);
71 |
72 | expect(point.isVisible(10.0, 20.0)).to.equal(false);
73 | });
74 |
75 | it('should return true if point is within visible range', function() {
76 | var point = new Point({}, 'point.1', 10.0);
77 |
78 | expect(point.isVisible(10.0, 20.0)).to.equal(true);
79 | });
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/src/main/player/player.keyboard.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file
3 | *
4 | * Defines the {@link KeyboardHandler} class.
5 | *
6 | * @module peaks/player/player.keyboard
7 | */
8 |
9 | define([], function() {
10 | 'use strict';
11 |
12 | var nodes = ['OBJECT', 'TEXTAREA', 'INPUT', 'SELECT', 'OPTION'];
13 |
14 | var SPACE = 32,
15 | TAB = 9,
16 | LEFT_ARROW = 37,
17 | RIGHT_ARROW = 39;
18 |
19 | var keys = [SPACE, TAB, LEFT_ARROW, RIGHT_ARROW];
20 |
21 | /**
22 | * Configures keyboard event handling.
23 | *
24 | * @class
25 | * @alias KeyboardHandler
26 | *
27 | * @param {EventEmitter} eventEmitter
28 | */
29 | function KeyboardHandler(eventEmitter) {
30 | this.eventEmitter = eventEmitter;
31 |
32 | document.addEventListener('keydown', this.handleKeyEvent.bind(this));
33 | document.addEventListener('keypress', this.handleKeyEvent.bind(this));
34 | document.addEventListener('keyup', this.handleKeyEvent.bind(this));
35 | }
36 |
37 | /**
38 | * Keyboard event handler function.
39 | *
40 | * @note Arrow keys only triggered on keydown, not keypress.
41 | *
42 | * @param {KeyboardEvent} event
43 | * @private
44 | */
45 |
46 | KeyboardHandler.prototype.handleKeyEvent = function handleKeyEvent(event) {
47 | if (nodes.indexOf(event.target.nodeName) === -1) {
48 | if (keys.indexOf(event.type) > -1) {
49 | event.preventDefault();
50 | }
51 |
52 | if (event.type === 'keydown' || event.type === 'keypress') {
53 | switch (event.keyCode) {
54 | case SPACE:
55 | this.eventEmitter.emit('keyboard.space');
56 | break;
57 |
58 | case TAB:
59 | this.eventEmitter.emit('keyboard.tab');
60 | break;
61 | }
62 | }
63 | else if (event.type === 'keyup') {
64 | switch (event.keyCode) {
65 | case LEFT_ARROW:
66 | if (event.shiftKey) {
67 | this.eventEmitter.emit('keyboard.shift_left');
68 | }
69 | else {
70 | this.eventEmitter.emit('keyboard.left');
71 | }
72 | break;
73 |
74 | case RIGHT_ARROW:
75 | if (event.shiftKey) {
76 | this.eventEmitter.emit('keyboard.shift_right');
77 | }
78 | else {
79 | this.eventEmitter.emit('keyboard.right');
80 | }
81 | break;
82 | }
83 | }
84 | }
85 | };
86 |
87 | return KeyboardHandler;
88 | });
89 |
--------------------------------------------------------------------------------
/test/unit/api-zoom-spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('./setup');
4 |
5 | var Peaks = require('../../src/main');
6 |
7 | describe('Peaks.zoom', function() {
8 | var p;
9 |
10 | beforeEach(function(done) {
11 | p = Peaks.init({
12 | container: document.getElementById('container'),
13 | mediaElement: document.getElementById('media'),
14 | dataUri: {
15 | json: 'base/test_data/sample.json'
16 | },
17 | zoomLevels: [512, 1024]
18 | });
19 |
20 | p.on('peaks.ready', done);
21 | });
22 |
23 | afterEach(function() {
24 | if (p) {
25 | p.destroy();
26 | }
27 | });
28 |
29 | describe('getZoom', function() {
30 | it('should return the initial zoom level index', function() {
31 | expect(p.zoom.getZoom()).to.equal(0);
32 | });
33 | });
34 |
35 | describe('setZoom', function() {
36 | it('should update the zoom level index', function() {
37 | p.zoom.setZoom(1);
38 |
39 | expect(p.zoom.getZoom()).to.equal(1);
40 | });
41 |
42 | it('should emit a zoom.update event with the new zoom level index', function() {
43 | var spy = sinon.spy();
44 |
45 | p.on('zoom.update', spy);
46 | p.zoom.setZoom(1);
47 |
48 | expect(spy).to.have.been.calledWith(1024, 512);
49 | });
50 |
51 | it('should limit the zoom level index value to the minimum valid index', function() {
52 | p.zoom.setZoom(-1);
53 |
54 | expect(p.zoom.getZoom()).to.equal(0);
55 | });
56 |
57 | it('should limit the zoom level index to the maximum valid index', function() {
58 | p.zoom.setZoom(2);
59 |
60 | expect(p.zoom.getZoom()).to.equal(1);
61 | });
62 |
63 | it('should not throw an exception if an existing zoom level does not have sufficient data', function() {
64 | expect(function() {
65 | p.zoom.setZoom(3);
66 | }).not.to.throw();
67 | });
68 | });
69 |
70 | describe('zoomOut', function() {
71 | it('should call setZoom with a bigger zoom level', function() {
72 | var spy = sinon.spy();
73 |
74 | p.on('zoom.update', spy);
75 | p.zoom.zoomOut();
76 |
77 | expect(spy).to.have.been.calledWith(1024, 512);
78 | });
79 | });
80 |
81 | describe('zoomIn', function() {
82 | it('should call setZoom with a smaller zoom level', function() {
83 | p.zoom.setZoom(1);
84 |
85 | var spy = sinon.spy();
86 |
87 | p.on('zoom.update', spy);
88 | p.zoom.zoomIn();
89 |
90 | expect(spy).to.have.been.calledWith(512, 1024);
91 | });
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/src/main/views/view-controller.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file
3 | *
4 | * Defines the {@link ViewController} class.
5 | *
6 | * @module peaks/views/view-controller
7 | */
8 |
9 | define([
10 | 'peaks/views/waveform.overview',
11 | 'peaks/views/waveform.zoomview',
12 | 'peaks/waveform/waveform.utils'
13 | ], function(
14 | WaveformOverview,
15 | WaveformZoomView,
16 | Utils) {
17 | 'use strict';
18 |
19 | /**
20 | * Creates an object that allows users to create and manage waveform views.
21 | *
22 | * @class
23 | * @alias ViewController
24 | *
25 | * @param {Peaks} peaks
26 | */
27 |
28 | function ViewController(peaks) {
29 | this._peaks = peaks;
30 | this._overview = null;
31 | this._zoomview = null;
32 | }
33 |
34 | ViewController.prototype.createOverview = function(container) {
35 | if (this._overview) {
36 | return this._overview;
37 | }
38 |
39 | var waveformData = this._peaks.getWaveformData();
40 |
41 | this._overview = new WaveformOverview(
42 | waveformData,
43 | container,
44 | this._peaks
45 | );
46 |
47 | return this._overview;
48 | };
49 |
50 | ViewController.prototype.createZoomview = function(container) {
51 | if (this._zoomview) {
52 | return this._zoomview;
53 | }
54 |
55 | var waveformData = this._peaks.getWaveformData();
56 |
57 | this._zoomview = new WaveformZoomView(
58 | waveformData,
59 | container,
60 | this._peaks
61 | );
62 |
63 | return this._zoomview;
64 | };
65 |
66 | ViewController.prototype.destroy = function() {
67 | if (this._overview) {
68 | this._overview.destroy();
69 | this._overview = null;
70 | }
71 |
72 | if (this._zoomview) {
73 | this._zoomview.destroy();
74 | this._zoomview = null;
75 | }
76 | };
77 |
78 | ViewController.prototype.getView = function(name) {
79 | if (Utils.isNullOrUndefined(name)) {
80 | if (this._overview && this._zoomview) {
81 | return null;
82 | }
83 | else if (this._overview) {
84 | return this._overview;
85 | }
86 | else if (this._zoomview) {
87 | return this._zoomview;
88 | }
89 | else {
90 | return null;
91 | }
92 | }
93 | else {
94 | switch (name) {
95 | case 'overview':
96 | return this._overview;
97 |
98 | case 'zoomview':
99 | return this._zoomview;
100 |
101 | default:
102 | return null;
103 | }
104 | }
105 | };
106 |
107 | return ViewController;
108 | });
109 |
--------------------------------------------------------------------------------
/src/main/views/waveform.zoomcontroller.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file
3 | *
4 | * Defines the {@link ZoomController} class.
5 | *
6 | * @module peaks/views/waveform.zoomcontroller.js
7 | */
8 |
9 | define([], function() {
10 | 'use strict';
11 |
12 | /**
13 | * Creates an object to control zoom levels in a {@link WaveformZoomView}.
14 | *
15 | * @class
16 | * @alias ZoomController
17 | *
18 | * @param {Peaks} peaks
19 | * @param {Arraytrue the segment start and
46 | * end times can be adjusted via the user interface.
47 | */
48 |
49 | function Point(parent, id, time, labelText, color, editable) {
50 | labelText = labelText || '';
51 | validatePoint(time, labelText);
52 | this._parent = parent;
53 | this._id = id;
54 | this._time = time;
55 | this._labelText = labelText;
56 | this._color = color;
57 | this._editable = editable;
58 | }
59 |
60 | Object.defineProperties(Point.prototype, {
61 | parent: {
62 | get: function() {
63 | return this._parent;
64 | }
65 | },
66 | id: {
67 | enumerable: true,
68 | get: function() {
69 | return this._id;
70 | }
71 | },
72 | time: {
73 | enumerable: true,
74 | get: function() {
75 | return this._time;
76 | },
77 |
78 | set: function(time) {
79 | this._time = time;
80 | }
81 | },
82 | labelText: {
83 | get: function() {
84 | return this._labelText;
85 | }
86 | },
87 | color: {
88 | enumerable: true,
89 | get: function() {
90 | return this._color;
91 | }
92 | },
93 | editable: {
94 | enumerable: true,
95 | get: function() {
96 | return this._editable;
97 | }
98 | }
99 | });
100 |
101 | Point.prototype.update = function(options) {
102 | var time = Object.prototype.hasOwnProperty.call(options, 'time') ? options.time : this.time;
103 | var labelText = Object.prototype.hasOwnProperty.call(options, 'labelText') ? options.labelText || '' : this.labelText;
104 | var color = Object.prototype.hasOwnProperty.call(options, 'color') ? options.color : this.color;
105 | var editable = Object.prototype.hasOwnProperty.call(options, 'editable') ? options.editable : this.editable;
106 |
107 | validatePoint(time, labelText);
108 |
109 | this._time = time;
110 | this._labelText = labelText;
111 | this._color = color;
112 | this._editable = editable;
113 | this._parent._peaks.emit('points.update', this);
114 | };
115 |
116 | /**
117 | * Returns true if the point lies with in a given time range.
118 | *
119 | * @param {Number} startTime The start of the time region, in seconds.
120 | * @param {Number} endTime The end of the time region, in seconds.
121 | * @returns {Boolean}
122 | */
123 |
124 | Point.prototype.isVisible = function(startTime, endTime) {
125 | return this.time >= startTime && this.time < endTime;
126 | };
127 |
128 | return Point;
129 | });
130 |
--------------------------------------------------------------------------------
/test/unit/segment-spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('./setup');
4 |
5 | var Peaks = require('../../src/main');
6 | var Segment = require('../../src/main/markers/segment');
7 |
8 | describe('Segment', function() {
9 | describe('update()', function() {
10 | var p;
11 |
12 | beforeEach(function(done) {
13 | p = Peaks.init({
14 | container: document.getElementById('container'),
15 | mediaElement: document.getElementById('media'),
16 | dataUri: { arraybuffer: 'base/test_data/sample.dat' }
17 | });
18 |
19 | p.on('peaks.ready', done);
20 | });
21 |
22 | afterEach(function() {
23 | if (p) {
24 | p.destroy();
25 | }
26 | });
27 |
28 | it('should be possible to update all properties programatically', function() {
29 | p.segments.add({ startTime: 0, endTime: 10, labelText: 'label text' });
30 |
31 | var newLabelText = 'new label text';
32 | var newStartTime = 2;
33 | var newEndTime = 9;
34 | var segment = p.segments.getSegments()[0];
35 |
36 | segment.update({
37 | startTime: newStartTime,
38 | endTime: newEndTime,
39 | labelText: newLabelText
40 | });
41 | expect(segment.startTime).to.equal(newStartTime);
42 | expect(segment.endTime).to.equal(newEndTime);
43 | expect(segment.labelText).to.equal(newLabelText);
44 | });
45 |
46 | it('should not allow invalid updates', function() {
47 | p.segments.add({ startTime: 0, endTime: 10 });
48 |
49 | var segment = p.segments.getSegments()[0];
50 |
51 | expect(function() {
52 | segment.update({ startTime: NaN });
53 | }).to.throw(TypeError);
54 |
55 | expect(function() {
56 | segment.update({ endTime: NaN });
57 | }).to.throw(TypeError);
58 |
59 | expect(function() {
60 | segment.update({ startTime: 8, endTime: 3 });
61 | }).to.throw(RangeError);
62 | });
63 | });
64 |
65 | describe('isVisible', function() {
66 | it('should return false if segment is before visible range', function() {
67 | var segment = new Segment({}, 'segment.1', 0.0, 10.0);
68 |
69 | expect(segment.isVisible(10.0, 20.0)).to.equal(false);
70 | });
71 |
72 | it('should return false if segment is after visible range', function() {
73 | var segment = new Segment({}, 'segment.1', 20.0, 30.0);
74 |
75 | expect(segment.isVisible(10.0, 20.0)).to.equal(false);
76 | });
77 |
78 | it('should return true if segment is within visible range', function() {
79 | var segment = new Segment({}, 'segment.1', 12.0, 18.0);
80 |
81 | expect(segment.isVisible(10.0, 20.0)).to.equal(true);
82 | });
83 |
84 | it('should return true if segment starts before and ends within visible range', function() {
85 | var segment = new Segment({}, 'segment.1', 9.0, 19.0);
86 |
87 | expect(segment.isVisible(10.0, 20.0)).to.equal(true);
88 | });
89 |
90 | it('should return true if segment starts before and ends at end of visible range', function() {
91 | var segment = new Segment({}, 'segment.1', 9.0, 20.0);
92 |
93 | expect(segment.isVisible(10.0, 20.0)).to.equal(true);
94 | });
95 |
96 | it('should return true if segment starts after and ends after visible range', function() {
97 | var segment = new Segment({}, 'segment.1', 11.0, 21.0);
98 |
99 | expect(segment.isVisible(10.0, 20.0)).to.equal(true);
100 | });
101 |
102 | it('should return true if segment starts after and ends at the end of visible range', function() {
103 | var segment = new Segment({}, 'segment.1', 11.0, 20.0);
104 |
105 | expect(segment.isVisible(10.0, 20.0)).to.equal(true);
106 | });
107 |
108 | it('should return true if segment is same as visible range', function() {
109 | var segment = new Segment({}, 'segment.1', 10.0, 20.0);
110 |
111 | expect(segment.isVisible(10.0, 20.0)).to.equal(true);
112 | });
113 |
114 | it('should return true if segment contains visible range', function() {
115 | var segment = new Segment({}, 'segment.1', 9.0, 21.0);
116 |
117 | expect(segment.isVisible(10.0, 20.0)).to.equal(true);
118 | });
119 | });
120 | });
121 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "peaks.js",
3 | "version": "0.13.0",
4 | "description": "JavaScript UI component for displaying audio waveforms",
5 | "main": "./peaks.js",
6 | "types": "./peaks.js.d.ts",
7 | "contributors": [
8 | "Chris Finch (https://github.com/chrisfinch)",
9 | "Thomas Parisot (https://github.com/oncletom)",
10 | "Chris Needham (https://github.com/chrisn)"
11 | ],
12 | "keywords": [
13 | "audio",
14 | "visualisation",
15 | "bbc",
16 | "webaudio",
17 | "browser",
18 | "interactive",
19 | "waveform"
20 | ],
21 | "browser": {
22 | "EventEmitter": "eventemitter2",
23 | "peaks/player/player": "./src/main/player/player.js",
24 | "peaks/player/player.keyboard": "./src/main/player/player.keyboard.js",
25 | "peaks/waveform/waveform.axis": "./src/main/waveform/waveform.axis.js",
26 | "peaks/waveform/waveform-builder": "./src/main/waveform/waveform-builder.js",
27 | "peaks/waveform/waveform.mixins": "./src/main/waveform/waveform.mixins.js",
28 | "peaks/waveform/waveform.utils": "./src/main/waveform/waveform.utils.js",
29 | "peaks/views/playhead-layer": "./src/main/views/playhead-layer.js",
30 | "peaks/views/points-layer": "./src/main/views/points-layer.js",
31 | "peaks/views/segments-layer": "./src/main/views/segments-layer.js",
32 | "peaks/views/view-controller": "./src/main/views/view-controller.js",
33 | "peaks/views/waveform-shape": "./src/main/views/waveform-shape.js",
34 | "peaks/views/waveform.overview": "./src/main/views/waveform.overview.js",
35 | "peaks/views/waveform.timecontroller": "./src/main/views/waveform.timecontroller.js",
36 | "peaks/views/waveform.zoomcontroller": "./src/main/views/waveform.zoomcontroller.js",
37 | "peaks/views/waveform.zoomview": "./src/main/views/waveform.zoomview.js",
38 | "peaks/views/helpers/mousedraghandler": "./src/main/views/helpers/mousedraghandler.js",
39 | "peaks/views/zooms/animated": "./src/main/views/zooms/animated.js",
40 | "peaks/views/zooms/static": "./src/main/views/zooms/static.js",
41 | "peaks/markers/point": "./src/main/markers/point",
42 | "peaks/markers/segment": "./src/main/markers/segment",
43 | "peaks/markers/waveform.points": "./src/main/markers/waveform.points.js",
44 | "peaks/markers/waveform.segments": "./src/main/markers/waveform.segments.js",
45 | "peaks/cues/cue-emitter": "./src/main/cues/cue-emitter.js",
46 | "peaks/cues/cue": "./src/main/cues/cue.js"
47 | },
48 | "repository": {
49 | "type": "git",
50 | "url": "git://github.com/bbc/peaks.js.git"
51 | },
52 | "license": "LGPL-3.0",
53 | "engines": {
54 | "node": ">= 8.11.2"
55 | },
56 | "scripts": {
57 | "prebuild": "npm run lint",
58 | "build": "browserify -d -e ./src/main.js -t deamdify -s peaks | exorcist peaks.js.map | derequire - > peaks.js",
59 | "postbuild": "cp peaks.js demo && cp peaks.js.map demo",
60 | "doc": "jsdoc --private --destination docs --recurse src",
61 | "lint": "eslint src/**/*.js test/**/*.js karma.conf.js",
62 | "pretest": "npm run build",
63 | "test": "./node_modules/karma/bin/karma start",
64 | "test-watch": "npm test -- --auto-watch --no-single-run",
65 | "copy-demo-files": "cp test_data/sample.mp3 demo && cp test_data/07023003.mp3 demo && cp test_data/07023003-2channel.dat demo",
66 | "prestart": "npm run build && npm run copy-demo-files",
67 | "prepublish": "npm run build",
68 | "start": "serve --listen 8080 --no-clipboard demo"
69 | },
70 | "devDependencies": {
71 | "browserify": "~16.5.0",
72 | "chai": "~4.2.0",
73 | "deamdify": "~0.3.0",
74 | "derequire": "~2.0.3",
75 | "eslint": "~6.3.0",
76 | "exorcist": "~1.0.1",
77 | "jsdoc": "~3.6.3",
78 | "karma": "~4.3.0",
79 | "karma-browserify": "~6.1.0",
80 | "karma-browserstack-launcher": "~1.5.1",
81 | "karma-chai-sinon": "~0.1.5",
82 | "karma-chrome-launcher": "~3.1.0",
83 | "karma-firefox-launcher": "~1.2.0",
84 | "karma-html2js-preprocessor": "~1.1.0",
85 | "karma-mocha": "~1.3.0",
86 | "karma-safari-launcher": "~1.0.0",
87 | "karma-spec-reporter": "~0.0.32",
88 | "mocha": "~6.2.0",
89 | "serve": "~11.1.0",
90 | "sinon": "~7.4.1",
91 | "sinon-chai": "~3.3.0",
92 | "watchify": "~3.11.1"
93 | },
94 | "dependencies": {
95 | "colors.css": "~3.0.0",
96 | "eventemitter2": "~5.0.1",
97 | "konva": "~4.0.6",
98 | "waveform-data": "~3.0.0"
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /* eslint-env node */
3 |
4 | function filterBrowsers(browsers, re) {
5 | return Object.keys(browsers).filter(function(key) {
6 | return re.test(key);
7 | });
8 | }
9 |
10 | module.exports = function(config) {
11 | var isCI = Boolean(process.env.CI) && Boolean(process.env.BROWSER_STACK_ACCESS_KEY);
12 | var glob = config.glob || '**/*.js';
13 |
14 | // Karma configuration
15 | config.set({
16 | // The root path location that will be used to resolve all relative paths
17 | // defined in 'files' and 'exclude'.
18 | basePath: '',
19 |
20 | frameworks: ['mocha', 'chai-sinon', 'browserify'],
21 |
22 | client: {
23 | chai: {
24 | includeStack: true
25 | },
26 | mocha: {
27 | timeout: 5000
28 | }
29 | },
30 |
31 | browserify: {
32 | debug: true,
33 | transform: [
34 | 'deamdify'
35 | ]
36 | },
37 |
38 | // list of files / patterns to load in the browser
39 | files: [
40 | { pattern: 'test/test_img/*', included: false },
41 | { pattern: 'test_data/*', included: false },
42 | { pattern: 'test_data/sample.{dat,json}', included: false, served: true },
43 | { pattern: 'test/unit/' + glob, included: true }
44 | ],
45 |
46 | mime: {
47 | 'application/octet-stream': ['dat']
48 | },
49 |
50 | preprocessors: {
51 | 'test/unit/**/*.js': ['browserify']
52 | },
53 |
54 | // test results reporter to use
55 | // possible values: dots || progress || growl || spec
56 | reporters: 'spec',
57 |
58 | // web server port
59 | port: 8080,
60 |
61 | // CLI runner port
62 | runnerPort: 9100,
63 |
64 | // enable / disable colors in the output (reporters and logs)
65 | colors: true,
66 |
67 | // level of logging
68 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
69 | logLevel: config.LOG_INFO,
70 |
71 | // enable / disable watching file and executing tests whenever any file changes
72 | autoWatch: false,
73 |
74 | browserDisconnectTolerance: isCI ? 3 : 1,
75 | browserNoActivityTimeout: isCI ? 120000 : null,
76 |
77 | browserStack: {
78 | build: process.env.TRAVIS_JOB_NUMBER || ('localhost ' + Date.now()),
79 | project: 'bbcrd/peaks.js'
80 | },
81 |
82 | customLaunchers: {
83 | 'BSChrome27': {
84 | base: 'BrowserStack',
85 | browser: 'chrome',
86 | browser_version: '27.0',
87 | os: 'Windows',
88 | os_version: 'XP'
89 | },
90 | 'BSChromeLatest': {
91 | base: 'BrowserStack',
92 | browser: 'chrome',
93 | browser_version: 'latest',
94 | os: 'OS X',
95 | os_version: 'Mavericks'
96 | },
97 | 'BSFirefox26': {
98 | base: 'BrowserStack',
99 | browser: 'firefox',
100 | browser_version: '26.0',
101 | os: 'Windows',
102 | os_version: '7'
103 | },
104 | 'BSFirefoxLatest': {
105 | base: 'BrowserStack',
106 | browser: 'firefox',
107 | browser_version: 'latest',
108 | os: 'OS X',
109 | os_version: 'Mavericks'
110 | },
111 | 'BSSafari6': {
112 | base: 'BrowserStack',
113 | browser: 'safari',
114 | browser_version: '6.0',
115 | os: 'OS X',
116 | os_version: 'Lion'
117 | },
118 | 'BSSafari7': {
119 | base: 'BrowserStack',
120 | browser: 'safari',
121 | browser_version: '7.0',
122 | os: 'OS X',
123 | os_version: 'Mavericks'
124 | },
125 | 'BSIE9': {
126 | base: 'BrowserStack',
127 | browser: 'ie',
128 | browser_version: '9.0',
129 | os: 'Windows',
130 | os_version: '7'
131 | },
132 | 'BSIE10': {
133 | base: 'BrowserStack',
134 | browser: 'ie',
135 | browser_version: '10',
136 | os: 'Windows',
137 | os_version: '8'
138 | },
139 | 'BSIE11': {
140 | base: 'BrowserStack',
141 | browser: 'ie',
142 | browser_version: '11',
143 | os: 'Windows',
144 | os_version: '8.1'
145 | }
146 | },
147 |
148 | // If browser does not capture in given timeout [ms], kill it
149 | captureTimeout: 120000,
150 |
151 | // Continuous Integration mode
152 | // if true, it capture browsers, run tests and exit
153 | singleRun: true
154 | });
155 |
156 | config.set({
157 | browsers: isCI ? filterBrowsers(config.customLaunchers, /^BS/) : ['ChromeHeadless']
158 | });
159 | };
160 |
--------------------------------------------------------------------------------
/src/main/waveform/waveform.axis.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file
3 | *
4 | * Defines the {@link WaveformAxis} class.
5 | *
6 | * @module peaks/waveform/waveform.axis
7 | */
8 |
9 | define(['peaks/waveform/waveform.utils', 'konva'], function(Utils, Konva) {
10 | 'use strict';
11 |
12 | /**
13 | * Creates the waveform axis shapes and adds them to the given view layer.
14 | *
15 | * @class
16 | * @alias WaveformAxis
17 | *
18 | * @param {WaveformOverview|WaveformZoomView} view
19 | * @param {Konva.Layer} layer
20 | * @param {Object} options
21 | * @param {String} options.axisGridlineColor
22 | * @param {String} options.axisLabelColor
23 | */
24 |
25 | function WaveformAxis(view, layer, options) {
26 | var self = this;
27 |
28 | self._options = options;
29 |
30 | var axisShape = new Konva.Shape({
31 | fill: 'rgba(38, 255, 161, 1)',
32 | strokeWidth: 0,
33 | opacity: 1,
34 | sceneFunc: function(context) {
35 | self.drawAxis(context, view);
36 | }
37 | });
38 |
39 | layer.add(axisShape);
40 | }
41 |
42 | /**
43 | * Returns number of seconds for each x-axis marker, appropriate for the
44 | * current zoom level, ensuring that markers are not too close together
45 | * and that markers are placed at intuitive time intervals (i.e., every 1,
46 | * 2, 5, 10, 20, 30 seconds, then every 1, 2, 5, 10, 20, 30 minutes, then
47 | * every 1, 2, 5, 10, 20, 30 hours).
48 | *
49 | * @param {WaveformOverview|WaveformZoomView} view
50 | * @returns {Number}
51 | */
52 |
53 | WaveformAxis.prototype.getAxisLabelScale = function(view) {
54 | var baseSecs = 1; // seconds
55 | var steps = [1, 2, 5, 10, 20, 30];
56 | var minSpacing = 60;
57 | var index = 0;
58 |
59 | var secs;
60 |
61 | for (;;) {
62 | secs = baseSecs * steps[index];
63 | var pixels = view.timeToPixels(secs);
64 |
65 | if (pixels < minSpacing) {
66 | if (++index === steps.length) {
67 | baseSecs *= 60; // seconds -> minutes -> hours
68 | index = 0;
69 | }
70 | }
71 | else {
72 | break;
73 | }
74 | }
75 |
76 | return secs;
77 | };
78 |
79 | /**
80 | * Draws the time axis and labels onto a view.
81 | *
82 | * @param {Konva.Context} context The context to draw on.
83 | * @param {WaveformOverview|WaveformZoomView} view
84 | */
85 |
86 | WaveformAxis.prototype.drawAxis = function(context, view) {
87 | var currentFrameStartTime = view.pixelsToTime(view.getFrameOffset());
88 |
89 | // Draw axis markers
90 | var markerHeight = 10;
91 |
92 | // Time interval between axis markers (seconds)
93 | var axisLabelIntervalSecs = this.getAxisLabelScale(view);
94 |
95 | // Time of first axis marker (seconds)
96 | var firstAxisLabelSecs = Utils.roundUpToNearest(currentFrameStartTime, axisLabelIntervalSecs);
97 |
98 | // Distance between waveform start time and first axis marker (seconds)
99 | var axisLabelOffsetSecs = firstAxisLabelSecs - currentFrameStartTime;
100 |
101 | // Distance between waveform start time and first axis marker (pixels)
102 | var axisLabelOffsetPixels = view.timeToPixels(axisLabelOffsetSecs);
103 |
104 | context.setAttr('strokeStyle', this._options.axisGridlineColor);
105 | context.setAttr('lineWidth', 1);
106 |
107 | // Set text style
108 | context.setAttr('font', '11px sans-serif');
109 | context.setAttr('fillStyle', this._options.axisLabelColor);
110 | context.setAttr('textAlign', 'left');
111 | context.setAttr('textBaseline', 'bottom');
112 |
113 | var secs = firstAxisLabelSecs;
114 | var x;
115 |
116 | var width = view.getWidth();
117 | var height = view.getHeight();
118 |
119 | for (;;) {
120 | // Position of axis marker (pixels)
121 | x = axisLabelOffsetPixels + view.timeToPixels(secs - firstAxisLabelSecs);
122 | if (x >= width) {
123 | break;
124 | }
125 |
126 | context.beginPath();
127 | context.moveTo(x + 0.5, 0);
128 | context.lineTo(x + 0.5, 0 + markerHeight);
129 | context.moveTo(x + 0.5, height);
130 | context.lineTo(x + 0.5, height - markerHeight);
131 | context.stroke();
132 |
133 | var label = Utils.formatTime(secs, true);
134 | var labelWidth = context.measureText(label).width;
135 | var labelX = x - labelWidth / 2;
136 | var labelY = height - 1 - markerHeight;
137 |
138 | if (labelX >= 0) {
139 | context.fillText(label, labelX, labelY);
140 | }
141 |
142 | secs += axisLabelIntervalSecs;
143 | }
144 | };
145 |
146 | return WaveformAxis;
147 | });
148 |
--------------------------------------------------------------------------------
/src/main/markers/segment.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file
3 | *
4 | * Defines the {@link Segment} class.
5 | *
6 | * @module peaks/markers/segment
7 | */
8 |
9 | define([
10 | 'peaks/waveform/waveform.utils'
11 | ], function(Utils) {
12 | 'use strict';
13 |
14 | function validateSegment(startTime, endTime, validationContext) {
15 | if (!Utils.isValidTime(startTime)) {
16 | // eslint-disable-next-line max-len
17 | throw new TypeError('peaks.segments.' + validationContext + ': startTime should be a valid number');
18 | }
19 |
20 | if (!Utils.isValidTime(endTime)) {
21 | // eslint-disable-next-line max-len
22 | throw new TypeError('peaks.segments.' + validationContext + ': endTime should be a valid number');
23 | }
24 |
25 | if (startTime < 0) {
26 | // eslint-disable-next-line max-len
27 | throw new RangeError('peaks.segments.' + validationContext + ': startTime should not be negative');
28 | }
29 |
30 | if (endTime < 0) {
31 | // eslint-disable-next-line max-len
32 | throw new RangeError('peaks.segments.' + validationContext + ': endTime should not be negative');
33 | }
34 |
35 | if (endTime <= startTime) {
36 | // eslint-disable-next-line max-len
37 | throw new RangeError('peaks.segments.' + validationContext + ': endTime should be greater than startTime');
38 | }
39 | }
40 |
41 | /**
42 | * A segment is a region of time, with associated label and color.
43 | *
44 | * @class
45 | * @alias Segment
46 | *
47 | * @param {Object} parent A reference to the parent WaveformSegments instance
48 | * @param {String} id A unique identifier for the segment.
49 | * @param {Number} startTime Segment start time, in seconds.
50 | * @param {Number} endTime Segment end time, in seconds.
51 | * @param {String} labelText Segment label text.
52 | * @param {String} color Segment waveform color.
53 | * @param {Boolean} editable If true the segment start and
54 | * end times can be adjusted via the user interface.
55 | */
56 |
57 | function Segment(parent, id, startTime, endTime, labelText, color, editable) {
58 | validateSegment(startTime, endTime, 'add()');
59 | this._parent = parent;
60 | this._id = id;
61 | this._startTime = startTime;
62 | this._endTime = endTime;
63 | this._labelText = labelText;
64 | this._color = color;
65 | this._editable = editable;
66 | }
67 |
68 | Object.defineProperties(Segment.prototype, {
69 | parent: {
70 | get: function() {
71 | return this._parent;
72 | }
73 | },
74 | id: {
75 | enumerable: true,
76 | get: function() {
77 | return this._id;
78 | }
79 | },
80 | startTime: {
81 | enumerable: true,
82 | get: function() {
83 | return this._startTime;
84 | },
85 |
86 | set: function(time) {
87 | this._startTime = time;
88 | }
89 | },
90 | endTime: {
91 | enumerable: true,
92 | get: function() {
93 | return this._endTime;
94 | },
95 |
96 | set: function(time) {
97 | this._endTime = time;
98 | }
99 | },
100 | labelText: {
101 | enumerable: true,
102 | get: function() {
103 | return this._labelText;
104 | }
105 | },
106 | color: {
107 | enumerable: true,
108 | get: function() {
109 | return this._color;
110 | }
111 | },
112 | editable: {
113 | enumerable: true,
114 | get: function() {
115 | return this._editable;
116 | }
117 | }
118 | });
119 |
120 | Segment.prototype.update = function(options) {
121 | var startTime = Object.prototype.hasOwnProperty.call(options, 'startTime') ? options.startTime : this.startTime;
122 | var endTime = Object.prototype.hasOwnProperty.call(options, 'endTime') ? options.endTime : this.endTime;
123 | var labelText = Object.prototype.hasOwnProperty.call(options, 'labelText') ? options.labelText : this.labelText;
124 | var color = Object.prototype.hasOwnProperty.call(options, 'color') ? options.color : this.color;
125 | var editable = Object.prototype.hasOwnProperty.call(options, 'editable') ? options.editable : this.editable;
126 |
127 | validateSegment(startTime, endTime, 'updateTime()');
128 |
129 | this._startTime = startTime;
130 | this._endTime = endTime;
131 | this._labelText = labelText;
132 | this._color = color;
133 | this._editable = editable;
134 | this._parent._peaks.emit('segments.update', this);
135 | };
136 |
137 | /**
138 | * Returns true if the segment overlaps a given time region.
139 | *
140 | * @param {Number} startTime The start of the time region, in seconds.
141 | * @param {Number} endTime The end of the time region, in seconds.
142 | * @returns {Boolean}
143 | *
144 | * @see http://wiki.c2.com/?TestIfDateRangesOverlap
145 | */
146 |
147 | Segment.prototype.isVisible = function(startTime, endTime) {
148 | return this.startTime < endTime && startTime < this.endTime;
149 | };
150 |
151 | return Segment;
152 | });
153 |
--------------------------------------------------------------------------------
/src/main/views/waveform-shape.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file
3 | *
4 | * Defines the {@link WaveformShape} class.
5 | *
6 | * @module peaks/views/waveform-shape
7 | */
8 |
9 | define(['peaks/waveform/waveform.utils', 'konva'], function(Utils, Konva) {
10 | 'use strict';
11 |
12 | /**
13 | * Scales the waveform data for drawing on a canvas context.
14 | *
15 | * @param {Number} amplitude The waveform data point amplitude.
16 | * @param {Number} height The height of the waveform, in pixels.
17 | * @param {Number} scale Amplitude scaling factor.
18 | * @returns {Number} The scaled waveform data point.
19 | */
20 |
21 | function scaleY(amplitude, height, scale) {
22 | var range = 256;
23 | var offset = 128;
24 |
25 | var scaledAmplitude = (amplitude * scale + offset) * height / range;
26 |
27 | return height - Utils.clamp(height - scaledAmplitude, 0, height);
28 | }
29 |
30 | /**
31 | * Waveform shape options.
32 | *
33 | * @typedef {Object} WaveformShapeOptions
34 | * @global
35 | * @property {String} color Waveform color.
36 | * @property {WaveformOverview|WaveformZoomView} view The view object
37 | * that contains the waveform shape.
38 | * @property {Segment?} segment If given, render a waveform image
39 | * covering the segment's time range. Otherwise, render the entire
40 | * waveform duration.
41 | */
42 |
43 | /**
44 | * Creates a Konva.Shape object that renders a waveform image.
45 | *
46 | * @class
47 | * @alias WaveformShape
48 | *
49 | * @param {WaveformShapeOptions} options
50 | */
51 |
52 | function WaveformShape(options) {
53 | Konva.Shape.call(this, {
54 | fill: options.color
55 | });
56 |
57 | this._view = options.view;
58 | this._segment = options.segment;
59 | this._scale = 1.0;
60 |
61 | this.sceneFunc(this._sceneFunc);
62 | }
63 |
64 | WaveformShape.prototype = Object.create(Konva.Shape.prototype);
65 |
66 | WaveformShape.prototype.setAmplitudeScale = function(scale) {
67 | this._scale = scale;
68 | };
69 |
70 | WaveformShape.prototype.setWaveformColor = function(color) {
71 | this.fill(color);
72 | };
73 |
74 | WaveformShape.prototype._sceneFunc = function(context) {
75 | var frameOffset = this._view.getFrameOffset();
76 | var width = this._view.getWidth();
77 | var height = this._view.getHeight();
78 |
79 | this._drawWaveform(
80 | context,
81 | this._view.getWaveformData(),
82 | frameOffset,
83 | this._segment ? this._view.timeToPixels(this._segment.startTime) : frameOffset,
84 | this._segment ? this._view.timeToPixels(this._segment.endTime) : frameOffset + width,
85 | width,
86 | height
87 | );
88 | };
89 |
90 | /**
91 | * Draws a waveform on a canvas context.
92 | *
93 | * @param {Konva.Context} context The canvas context to draw on.
94 | * @param {WaveformData} waveformData The waveform data to draw.
95 | * @param {Number} frameOffset The start position of the waveform shown
96 | * in the view, in pixels.
97 | * @param {Number} startPixels The start position of the waveform to draw,
98 | * in pixels.
99 | * @param {Number} endPixels The end position of the waveform to draw,
100 | * in pixels.
101 | * @param {Number} width The width of the waveform area, in pixels.
102 | * @param {Number} height The height of the waveform area, in pixels.
103 | */
104 |
105 | WaveformShape.prototype._drawWaveform = function(context, waveformData,
106 | frameOffset, startPixels, endPixels, width, height) {
107 | if (startPixels < frameOffset) {
108 | startPixels = frameOffset;
109 | }
110 |
111 | var limit = frameOffset + width;
112 |
113 | if (endPixels > limit) {
114 | endPixels = limit;
115 | }
116 |
117 | if (endPixels > waveformData.length) {
118 | endPixels = waveformData.length;
119 | }
120 |
121 | var channels = waveformData.channels;
122 |
123 | var waveformTop = 0;
124 | var waveformHeight = Math.floor(height / channels);
125 |
126 | for (var i = 0; i < channels; i++) {
127 | if (i === channels - 1) {
128 | waveformHeight = height - (channels - 1) * waveformHeight;
129 | }
130 |
131 | this._drawChannel(
132 | context,
133 | waveformData.channel(i),
134 | frameOffset,
135 | startPixels,
136 | endPixels,
137 | waveformTop,
138 | waveformHeight
139 | );
140 |
141 | waveformTop += waveformHeight;
142 | }
143 | };
144 |
145 | WaveformShape.prototype._drawChannel = function(context, channel,
146 | frameOffset, startPixels, endPixels, top, height) {
147 | var x, val;
148 |
149 | context.beginPath();
150 |
151 | for (x = startPixels; x < endPixels; x++) {
152 | val = channel.min_sample(x);
153 |
154 | context.lineTo(x - frameOffset + 0.5, top + scaleY(val, height, this._scale) + 0.5);
155 | }
156 |
157 | for (x = endPixels - 1; x >= startPixels; x--) {
158 | val = channel.max_sample(x);
159 |
160 | context.lineTo(x - frameOffset + 0.5, top + scaleY(val, height, this._scale) + 0.5);
161 | }
162 |
163 | context.closePath();
164 |
165 | context.fillShape(this);
166 | };
167 |
168 | return WaveformShape;
169 | });
170 |
--------------------------------------------------------------------------------
/src/main/views/helpers/mousedraghandler.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file
3 | *
4 | * Defines the {@link MouseDragHandler} class.
5 | *
6 | * @module peaks/views/helpers/mousedraghandler
7 | */
8 |
9 | define(function() {
10 | 'use strict';
11 |
12 | /**
13 | * An object to receive callbacks on mouse drag events. Each function is
14 | * called with the current mouse X position, relative to the stage's
15 | * container HTML element.
16 | *
17 | * @typedef {Object} MouseDragHandlers
18 | * @global
19 | * @property {Function} onMouseDown Mouse down event handler.
20 | * @property {Function} onMouseMove Mouse move event handler.
21 | * @property {Function} onMouseUp Mouse up event handler.
22 | */
23 |
24 | /**
25 | * Creates a handler for mouse events to allow interaction with the waveform
26 | * views by clicking and dragging the mouse.
27 | *
28 | * @class
29 | * @alias MouseDragHandler
30 | *
31 | * @param {Konva.Stage} stage
32 | * @param {MouseDragHandlers} handlers
33 | */
34 |
35 | function MouseDragHandler(stage, handlers) {
36 | this._stage = stage;
37 | this._handlers = handlers;
38 | this._dragging = false;
39 | this._mouseDown = this.mouseDown.bind(this);
40 | this._mouseUp = this.mouseUp.bind(this);
41 | this._mouseMove = this.mouseMove.bind(this);
42 |
43 | this._stage.on('mousedown', this._mouseDown);
44 | this._stage.on('touchstart', this._mouseDown);
45 |
46 | this._mouseDownClientX = null;
47 | }
48 |
49 | /**
50 | * Mouse down event handler.
51 | *
52 | * @param {MouseEvent} event
53 | */
54 |
55 | MouseDragHandler.prototype.mouseDown = function(event) {
56 | if (!event.target) {
57 | return;
58 | }
59 |
60 | if (event.target.attrs.draggable) {
61 | return;
62 | }
63 |
64 | if (event.target.parent && event.target.parent.attrs.draggable) {
65 | return;
66 | }
67 |
68 | if (event.type === 'touchstart') {
69 | this._mouseDownClientX = Math.floor(event.evt.touches[0].clientX);
70 | }
71 | else {
72 | this._mouseDownClientX = event.evt.clientX;
73 | }
74 |
75 | if (this._handlers.onMouseDown) {
76 | var mouseDownPosX = this._getMousePosX(this._mouseDownClientX);
77 |
78 | this._handlers.onMouseDown(mouseDownPosX);
79 | }
80 |
81 | // Use the window mousemove and mouseup handlers instead of the
82 | // Konva.Stage ones so that we still receive events if the user moves the
83 | // mouse outside the stage.
84 | window.addEventListener('mousemove', this._mouseMove, false);
85 | window.addEventListener('touchmove', this._mouseMove, false);
86 | window.addEventListener('mouseup', this._mouseUp, false);
87 | window.addEventListener('touchend', this._mouseUp, false);
88 | window.addEventListener('blur', this._mouseUp, false);
89 | };
90 |
91 | /**
92 | * Mouse move event handler.
93 | *
94 | * @param {MouseEvent} event
95 | */
96 |
97 | MouseDragHandler.prototype.mouseMove = function(event) {
98 | var clientX = null;
99 |
100 | if (event.type === 'touchmove') {
101 | clientX = Math.floor(event.changedTouches[0].clientX);
102 | }
103 | else {
104 | clientX = event.clientX;
105 | }
106 |
107 | // Don't update on vertical mouse movement.
108 | if (clientX === this._mouseDownClientX) {
109 | return;
110 | }
111 |
112 | this._dragging = true;
113 |
114 | if (this._handlers.onMouseMove) {
115 | var mousePosX = this._getMousePosX(clientX);
116 |
117 | this._handlers.onMouseMove(mousePosX);
118 | }
119 | };
120 |
121 | /**
122 | * Mouse up event handler.
123 | *
124 | * @param {MouseEvent} event
125 | */
126 |
127 | MouseDragHandler.prototype.mouseUp = function(event) {
128 | var clientX = null;
129 |
130 | if (event.type === 'touchend') {
131 | clientX = Math.floor(event.changedTouches[0].clientX);
132 | if (event.cancelable) {
133 | event.preventDefault();
134 | }
135 | }
136 | else {
137 | clientX = event.clientX;
138 | }
139 |
140 | if (this._handlers.onMouseUp) {
141 | var mousePosX = this._getMousePosX(clientX);
142 |
143 | this._handlers.onMouseUp(mousePosX);
144 | }
145 |
146 | window.removeEventListener('mousemove', this._mouseMove, false);
147 | window.removeEventListener('touchmove', this._mouseMove, false);
148 | window.removeEventListener('mouseup', this._mouseUp, false);
149 | window.removeEventListener('touchend', this._mouseUp, false);
150 | window.removeEventListener('blur', this._mouseUp, false);
151 |
152 | this._dragging = false;
153 | };
154 |
155 | /**
156 | * @returns {Number} The mouse X position, relative to the container that
157 | * received the mouse down event.
158 | *
159 | * @private
160 | * @param {Number} clientX Mouse client X position.
161 | */
162 |
163 | MouseDragHandler.prototype._getMousePosX = function(clientX) {
164 | var containerPos = this._stage.getContainer().getBoundingClientRect();
165 |
166 | return clientX - containerPos.left;
167 | };
168 |
169 | /**
170 | * Returns true if the mouse is being dragged, i.e., moved with
171 | * the mouse button held down.
172 | *
173 | * @returns {Boolean}
174 | */
175 |
176 | MouseDragHandler.prototype.isDragging = function() {
177 | return this._dragging;
178 | };
179 |
180 | return MouseDragHandler;
181 | });
182 |
--------------------------------------------------------------------------------
/demo/overview-waveform.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 56 | Peaks.js is a JavaScript library that allows you to display and 57 | interaction with audio waveforms in the browser. 58 |
59 |60 | It was developed by BBC R&D 61 | to allow audio editors to make accurate clippings of audio content. 62 | You can read more about the project 63 | here. 64 |
65 | 66 |68 | The following pages demonstrate various configuration options: 69 |
70 |71 | Precomputed Waveform Data | 72 | Web Audio API | 73 | Single Zoomable Waveform | 74 | Single Fixed Waveform | 75 | Cue Events | 76 | Changing the Media URL | 77 | Multi-Channel Waveform 78 |
79 |81 | This demo shows how to configure Peaks.js to render a single waveform 82 | view, scaled to fit a given container element. 83 |
84 |56 | Peaks.js is a JavaScript library that allows you to display and 57 | interaction with audio waveforms in the browser. 58 |
59 |60 | It was developed by BBC R&D 61 | to allow audio editors to make accurate clippings of audio content. 62 | You can read more about the project 63 | here. 64 |
65 | 66 |68 | The following pages demonstrate various configuration options: 69 |
70 |71 | Precomputed Waveform Data | 72 | Web Audio API | 73 | Single Zoomable Waveform | 74 | Single Fixed Waveform | 75 | Cue Events | 76 | Changing the Media URL | 77 | Multi-Channel Waveform 78 |
79 |81 | This demo shows how configure Peaks.js to render a single zoomable 82 | waveform view. 83 |
84 |<audio>
21 | * or <video> element to associate with the
22 | * {@link Peaks} instance.
23 | */
24 |
25 | function Player(peaks, mediaElement) {
26 | var self = this;
27 |
28 | self._peaks = peaks;
29 | self._listeners = [];
30 | self._mediaElement = mediaElement;
31 | self._duration = self.getDuration();
32 | self._isPlaying = false;
33 |
34 | if (self._mediaElement.readyState === 4) {
35 | self._peaks.emit('player_load', self);
36 | }
37 |
38 | self._addMediaListener('timeupdate', function() {
39 | self._peaks.emit('player_time_update', self.getCurrentTime());
40 | });
41 |
42 | self._addMediaListener('play', function() {
43 | self._isPlaying = true;
44 | self._peaks.emit('player_play', self.getCurrentTime());
45 | });
46 |
47 | self._addMediaListener('pause', function() {
48 | self._isPlaying = false;
49 | self._peaks.emit('player_pause', self.getCurrentTime());
50 | });
51 |
52 | self._addMediaListener('seeked', function() {
53 | self._peaks.emit('player_seek', self.getCurrentTime());
54 | });
55 |
56 | self._addMediaListener('canplay', function() {
57 | self._peaks.emit('player_canplay', self);
58 | });
59 |
60 | self._addMediaListener('error', function(event) {
61 | self._peaks.emit('player_error', event.target.error);
62 | });
63 |
64 | self._interval = null;
65 | }
66 |
67 | /**
68 | * Adds an event listener to the media element.
69 | *
70 | * @private
71 | * @param {String} type The event type to listen for.
72 | * @param {Function} callback An event handler function.
73 | */
74 |
75 | Player.prototype._addMediaListener = function(type, callback) {
76 | this._listeners.push({ type: type, callback: callback });
77 | this._mediaElement.addEventListener(type, callback);
78 | };
79 |
80 | /**
81 | * Cleans up the player object, removing all event listeners from the
82 | * associated media element.
83 | */
84 |
85 | Player.prototype.destroy = function() {
86 | for (var i = 0; i < this._listeners.length; i++) {
87 | var listener = this._listeners[i];
88 |
89 | this._mediaElement.removeEventListener(
90 | listener.type,
91 | listener.callback
92 | );
93 | }
94 |
95 | this._listeners.length = 0;
96 |
97 | if (this._interval !== null) {
98 | clearTimeout(this._interval);
99 | this._interval = null;
100 | }
101 | };
102 |
103 | Player.prototype.setSource = function(source) {
104 | this._mediaElement.setAttribute('src', source);
105 | };
106 |
107 | Player.prototype.getSource = function() {
108 | return this._mediaElement.src;
109 | };
110 |
111 | Player.prototype.getCurrentSource = function() {
112 | return this._mediaElement.currentSrc;
113 | };
114 |
115 | /**
116 | * Starts playback.
117 | */
118 |
119 | Player.prototype.play = function() {
120 | this._mediaElement.play();
121 | };
122 |
123 | /**
124 | * Pauses playback.
125 | */
126 |
127 | Player.prototype.pause = function() {
128 | this._mediaElement.pause();
129 | };
130 |
131 | /**
132 | * @returns {Boolean} true if playing, false
133 | * otherwise.
134 | */
135 |
136 | Player.prototype.isPlaying = function() {
137 | return this._isPlaying;
138 | };
139 |
140 | /**
141 | * @returns {boolean} true if seeking
142 | */
143 | Player.prototype.isSeeking = function() {
144 | return this._mediaElement.seeking;
145 | };
146 |
147 | /**
148 | * Returns the current playback time position, in seconds.
149 | *
150 | * @returns {Number}
151 | */
152 |
153 | Player.prototype.getCurrentTime = function() {
154 | return this._mediaElement.currentTime;
155 | };
156 |
157 | /**
158 | * Returns the media duration, in seconds.
159 | *
160 | * @returns {Number}
161 | */
162 |
163 | Player.prototype.getDuration = function() {
164 | return this._mediaElement.duration;
165 | };
166 |
167 | /**
168 | * Seeks to a given time position within the media.
169 | *
170 | * @param {Number} time The time position, in seconds.
171 | */
172 |
173 | Player.prototype.seek = function(time) {
174 | if (!Utils.isValidTime(time)) {
175 | this._peaks.logger('peaks.player.seek(): parameter must be a valid time, in seconds');
176 | return;
177 | }
178 |
179 | this._mediaElement.currentTime = time;
180 | };
181 |
182 | /**
183 | * Plays the given segment.
184 | *
185 | * @param {Segment} segment The segment denoting the time region to play.
186 | */
187 |
188 | Player.prototype.playSegment = function(segment) {
189 | var self = this;
190 |
191 | if (!segment ||
192 | !Utils.isValidTime(segment.startTime) ||
193 | !Utils.isValidTime(segment.endTime)) {
194 | self._peaks.logger('peaks.player.playSegment(): parameter must be a segment object');
195 | return;
196 | }
197 |
198 | clearTimeout(self._interval);
199 | self._interval = null;
200 |
201 | // Set audio time to segment start time
202 | self.seek(segment.startTime);
203 |
204 | // Start playing audio
205 | self._mediaElement.play();
206 |
207 | // We need to use setInterval here as the timeupdate event doesn't fire
208 | // often enough.
209 | self._interval = setInterval(function() {
210 | if (self.getCurrentTime() >= segment.endTime || self._mediaElement.paused) {
211 | clearTimeout(self._interval);
212 | self._interval = null;
213 | self._mediaElement.pause();
214 | }
215 | }, 30);
216 | };
217 |
218 | return Player;
219 | });
220 |
--------------------------------------------------------------------------------
/demo/set-source.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 84 | Peaks.js is a JavaScript library that allows you to display and 85 | interaction with audio waveforms in the browser. 86 |
87 |88 | It was developed by BBC R&D 89 | to allow audio editors to make accurate clippings of audio content. 90 | You can read more about the project 91 | here. 92 |
93 | 94 |96 | The following pages demonstrate various configuration options: 97 |
98 |99 | Precomputed Waveform Data | 100 | Web Audio API | 101 | Single Zoomable Waveform | 102 | Single Fixed Waveform | 103 | Cue Events | 104 | Changing the Media URL | 105 | Multi-Channel Waveform 106 |
107 |109 | This demo shows how to change the media URL and update the waveform. 110 |
111 |true The playback time position
24 | * is shown next to the playhead.
25 | * @param {Number} time Initial position of the playhead, in seconds.
26 | */
27 |
28 | function PlayheadLayer(peaks, view, showTime, time) {
29 | this._peaks = peaks;
30 | this._view = view;
31 | this._playheadPixel = 0;
32 | this._playheadLineAnimation = null;
33 | this._playheadVisible = false;
34 | this._playheadColor = peaks.options.playheadColor;
35 | this._playheadTextColor = peaks.options.playheadTextColor;
36 |
37 | this._playheadLayer = new Konva.Layer();
38 |
39 | this._createPlayhead(this._playheadColor);
40 |
41 | if (showTime) {
42 | this._createPlayheadText(this._playheadTextColor);
43 | }
44 |
45 | this.zoomLevelChanged();
46 | this._syncPlayhead(time);
47 | }
48 |
49 | /**
50 | * Adds the layer to the given {Konva.Stage}.
51 | *
52 | * @param {Konva.Stage} stage
53 | */
54 |
55 | PlayheadLayer.prototype.addToStage = function(stage) {
56 | stage.add(this._playheadLayer);
57 | };
58 |
59 | /**
60 | * Decides whether to use an animation to update the playhead position.
61 | *
62 | * If the zoom level is such that the number of pixels per second of audio is
63 | * low, we can use timeupdate events from the HTMLMediaElement to
64 | * set the playhead position. Otherwise, we use an animation to update the
65 | * playhead position more smoothly. The animation is CPU intensive, so we
66 | * avoid using it where possible.
67 | */
68 |
69 | PlayheadLayer.prototype.zoomLevelChanged = function() {
70 | var pixelsPerSecond = this._view.timeToPixels(1.0);
71 | var time;
72 |
73 | this._useAnimation = pixelsPerSecond >= 5;
74 |
75 | if (this._useAnimation) {
76 | if (this._peaks.player.isPlaying() && !this._playheadLineAnimation) {
77 | // Start the animation
78 | this._start();
79 | }
80 | }
81 | else {
82 | if (this._playheadLineAnimation) {
83 | // Stop the animation
84 | time = this._peaks.player.getCurrentTime();
85 |
86 | this.stop(time);
87 | }
88 | }
89 | };
90 |
91 | /**
92 | * Creates the playhead UI objects.
93 | *
94 | * @private
95 | * @param {String} color
96 | */
97 |
98 | PlayheadLayer.prototype._createPlayhead = function(color) {
99 | this._playheadLine = new Konva.Line({
100 | points: [0.5, 0, 0.5, this._view.getHeight()],
101 | stroke: color,
102 | strokeWidth: 1
103 | });
104 |
105 | this._playheadGroup = new Konva.Group({
106 | x: 0,
107 | y: 0
108 | });
109 |
110 | this._playheadGroup.add(this._playheadLine);
111 | this._playheadLayer.add(this._playheadGroup);
112 | };
113 |
114 | PlayheadLayer.prototype._createPlayheadText = function(color) {
115 | this._playheadText = new Konva.Text({
116 | x: 2,
117 | y: 12,
118 | text: '00:00:00',
119 | fontSize: 11,
120 | fontFamily: 'sans-serif',
121 | fill: color,
122 | align: 'right'
123 | });
124 |
125 | this._playheadGroup.add(this._playheadText);
126 | };
127 |
128 | /**
129 | * Updates the playhead position.
130 | *
131 | * @param {Number} time Current playhead position, in seconds.
132 | */
133 |
134 | PlayheadLayer.prototype.updatePlayheadTime = function(time) {
135 | this._syncPlayhead(time);
136 |
137 | if (this._peaks.player.isPlaying()) {
138 | this._start();
139 | }
140 | };
141 |
142 | /**
143 | * Updates the playhead position.
144 | *
145 | * @private
146 | * @param {Number} time Current playhead position, in seconds.
147 | */
148 |
149 | PlayheadLayer.prototype._syncPlayhead = function(time) {
150 | var pixelIndex = this._view.timeToPixels(time);
151 |
152 | var frameOffset = this._view.getFrameOffset();
153 | var width = this._view.getWidth();
154 |
155 | var isVisible = (pixelIndex >= frameOffset) &&
156 | (pixelIndex < frameOffset + width);
157 |
158 | this._playheadPixel = pixelIndex;
159 |
160 | if (isVisible) {
161 | var playheadX = this._playheadPixel - frameOffset;
162 |
163 | if (!this._playheadVisible) {
164 | this._playheadVisible = true;
165 | this._playheadGroup.show();
166 | }
167 |
168 | this._playheadGroup.setAttr('x', playheadX);
169 |
170 | if (this._playheadText) {
171 | var text = Utils.formatTime(time, false);
172 |
173 | this._playheadText.setText(text);
174 | }
175 |
176 | this._playheadLayer.draw();
177 | }
178 | else {
179 | if (this._playheadVisible) {
180 | this._playheadVisible = false;
181 | this._playheadGroup.hide();
182 |
183 | this._playheadLayer.draw();
184 | }
185 | }
186 | };
187 |
188 | /**
189 | * Starts a playhead animation in sync with the media playback.
190 | *
191 | * @private
192 | */
193 |
194 | PlayheadLayer.prototype._start = function() {
195 | var self = this;
196 |
197 | if (self._playheadLineAnimation) {
198 | self._playheadLineAnimation.stop();
199 | self._playheadLineAnimation = null;
200 | }
201 |
202 | if (!self._useAnimation) {
203 | return;
204 | }
205 |
206 | var lastPlayheadPosition = null;
207 |
208 | self._playheadLineAnimation = new Konva.Animation(function() {
209 | var time = self._peaks.player.getCurrentTime();
210 | var playheadPosition = self._view.timeToPixels(time);
211 |
212 | if (playheadPosition !== lastPlayheadPosition) {
213 | self._syncPlayhead(time);
214 | lastPlayheadPosition = playheadPosition;
215 | }
216 | }, self._playheadLayer);
217 |
218 | self._playheadLineAnimation.start();
219 | };
220 |
221 | PlayheadLayer.prototype.stop = function(time) {
222 | if (this._playheadLineAnimation) {
223 | this._playheadLineAnimation.stop();
224 | this._playheadLineAnimation = null;
225 | }
226 |
227 | this._syncPlayhead(time);
228 | };
229 |
230 | /**
231 | * Returns the position of the playhead marker, in pixels relative to the
232 | * left hand side of the waveform view.
233 | *
234 | * @return {Number}
235 | */
236 |
237 | PlayheadLayer.prototype.getPlayheadOffset = function() {
238 | return this._playheadPixel - this._view.getFrameOffset();
239 | };
240 |
241 | PlayheadLayer.prototype.getPlayheadPixel = function() {
242 | return this._playheadPixel;
243 | };
244 |
245 | PlayheadLayer.prototype.showPlayheadTime = function(show) {
246 | var updated = false;
247 |
248 | if (show) {
249 | if (!this._playheadText) {
250 | // Create it
251 | this._createPlayheadText(this._playheadTextColor);
252 | updated = true;
253 | }
254 | }
255 | else {
256 | if (this._playheadText) {
257 | this._playheadText.remove();
258 | this._playheadText.destroy();
259 | this._playheadText = null;
260 | updated = true;
261 | }
262 | }
263 |
264 | if (updated) {
265 | this._playheadLayer.draw();
266 | }
267 | };
268 |
269 | return PlayheadLayer;
270 | });
271 |
--------------------------------------------------------------------------------
/test/unit/waveform-builder-spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('./setup');
4 |
5 | var WaveformBuilder = require('../../src/main/waveform/waveform-builder');
6 | var WaveformData = require('waveform-data');
7 |
8 | var TestAudioContext = window.AudioContext || window.mozAudioContext || window.webkitAudioContext;
9 |
10 | describe('WaveformBuilder', function() {
11 | describe('init', function() {
12 | it('should use the dataUriDefaultFormat value as a format URL if dataUri is provided as string', function(done) {
13 | var peaks = {
14 | options: {
15 | mediaElement: document.getElementById('media'),
16 | dataUri: 'base/test_data/sample.json'
17 | }
18 | };
19 |
20 | var waveformBuilder = new WaveformBuilder(peaks);
21 |
22 | var spy = sinon.spy(waveformBuilder, '_createXHR');
23 |
24 | waveformBuilder.init(peaks.options, function(err, waveformData) {
25 | expect(err).to.equal(null);
26 | expect(waveformData).to.be.an.instanceOf(WaveformData);
27 |
28 | var requestType = spy.getCall(0).args[1];
29 |
30 | expect(requestType).to.equal('json');
31 |
32 | done();
33 | });
34 | });
35 |
36 | it('should return an error if the data handling fails', function(done) {
37 | var peaks = {
38 | options: {
39 | mediaElement: document.getElementById('media'),
40 | dataUri: 'base/test_data/404-file.json'
41 | }
42 | };
43 |
44 | var waveformBuilder = new WaveformBuilder(peaks);
45 |
46 | waveformBuilder.init(peaks.options, function(err, waveformData) {
47 | expect(err).to.be.an.instanceOf(Error);
48 | expect(waveformData).to.not.be.ok;
49 |
50 | done();
51 | });
52 | });
53 |
54 | it('should return an error if the data handling fails due to a network error', function(done) {
55 | var peaks = {
56 | options: {
57 | mediaElement: document.getElementById('media'),
58 | dataUri: 'file:///test.json'
59 | }
60 | };
61 |
62 | var waveformBuilder = new WaveformBuilder(peaks);
63 |
64 | waveformBuilder.init(peaks.options, function(err, waveformData) {
65 | expect(err).to.be.an.instanceof(Error);
66 | expect(err.message).to.equal('XHR Failed');
67 | expect(waveformData).to.equal(undefined);
68 |
69 | done();
70 | });
71 | });
72 |
73 | it('should use the JSON dataUri connector', function(done) {
74 | var peaks = {
75 | options: {
76 | mediaElement: document.getElementById('media'),
77 | dataUri: {
78 | json: 'base/test_data/sample.json'
79 | }
80 | }
81 | };
82 |
83 | var waveformBuilder = new WaveformBuilder(peaks);
84 |
85 | var spy = sinon.spy(waveformBuilder, '_createXHR');
86 |
87 | waveformBuilder.init(peaks.options, function(err, waveformData) {
88 | expect(err).to.equal(null);
89 | expect(waveformData).to.be.an.instanceOf(WaveformData);
90 |
91 | var url = spy.getCall(0).args[0];
92 |
93 | expect(url).to.equal(peaks.options.dataUri.json);
94 |
95 | done();
96 | });
97 | });
98 |
99 | it('should not use credentials if withCredentials is not set', function(done) {
100 | var peaks = {
101 | options: {
102 | mediaElement: document.getElementById('media'),
103 | dataUri: {
104 | json: 'base/test_data/sample.json'
105 | }
106 | }
107 | };
108 |
109 | var waveformBuilder = new WaveformBuilder(peaks);
110 |
111 | var spy = sinon.spy(waveformBuilder, '_createXHR');
112 |
113 | waveformBuilder.init(peaks.options, function(err, waveformData) {
114 | expect(err).to.equal(null);
115 | expect(waveformData).to.be.an.instanceOf(WaveformData);
116 |
117 | var xhr = spy.getCall(0).returnValue;
118 |
119 | expect(xhr.withCredentials).to.equal(false);
120 |
121 | done();
122 | });
123 | });
124 |
125 | it('should use credentials if withCredentials is set', function(done) {
126 | var peaks = {
127 | options: {
128 | mediaElement: document.getElementById('media'),
129 | withCredentials: true,
130 | dataUri: {
131 | json: 'base/test_data/sample.json'
132 | }
133 | }
134 | };
135 |
136 | var waveformBuilder = new WaveformBuilder(peaks);
137 |
138 | var spy = sinon.spy(waveformBuilder, '_createXHR');
139 |
140 | waveformBuilder.init(peaks.options, function(err, waveformData) {
141 | expect(err).to.equal(null);
142 | expect(waveformData).to.be.an.instanceOf(WaveformData);
143 |
144 | var xhr = spy.getCall(0).returnValue;
145 |
146 | expect(xhr.withCredentials).to.equal(true);
147 |
148 | done();
149 | });
150 | });
151 |
152 | ('ArrayBuffer' in window) && it('should use the arraybuffer dataUri connector', function(done) {
153 | var peaks = {
154 | options: {
155 | mediaElement: document.getElementById('media'),
156 | dataUri: {
157 | arraybuffer: 'base/test_data/sample.dat'
158 | }
159 | }
160 | };
161 |
162 | var waveformBuilder = new WaveformBuilder(peaks);
163 |
164 | var spy = sinon.spy(waveformBuilder, '_createXHR');
165 |
166 | waveformBuilder.init(peaks.options, function(err, waveformData) {
167 | expect(err).to.equal(null);
168 | expect(waveformData).to.be.an.instanceOf(WaveformData);
169 |
170 | var url = spy.getCall(0).args[0];
171 |
172 | expect(url).to.equal(peaks.options.dataUri.arraybuffer);
173 |
174 | done();
175 | });
176 | });
177 |
178 | !('ArrayBuffer' in window) && it('should throw an exception if the only available format is browser incompatible', function() {
179 | expect(function() {
180 | var peaks = {
181 | options: {
182 | mediaElement: document.getElementById('media'),
183 | dataUri: {
184 | arraybuffer: 'base/test_data/sample.dat'
185 | }
186 | }
187 | };
188 |
189 | var waveformBuilder = new WaveformBuilder(peaks);
190 |
191 | waveformBuilder.init(peaks.options, function() {
192 | });
193 | }).to.throw();
194 | });
195 |
196 | it('should prefer binary waveform data over JSON', function(done) {
197 | var peaks = {
198 | options: {
199 | mediaElement: document.getElementById('media'),
200 | dataUri: {
201 | arraybuffer: 'base/test_data/sample.dat',
202 | json: 'base/test_data/sample.json'
203 | }
204 | }
205 | };
206 |
207 | var waveformBuilder = new WaveformBuilder(peaks);
208 |
209 | var spy = sinon.spy(waveformBuilder, '_createXHR');
210 |
211 | waveformBuilder.init(peaks.options, function(err, waveformData) {
212 | expect(err).to.equal(null);
213 | expect(waveformData).to.be.an.instanceOf(WaveformData);
214 |
215 | var url = spy.getCall(0).args[0];
216 | var expectedDataUri = window.ArrayBuffer ? peaks.options.dataUri.arraybuffer : peaks.options.dataUri.json;
217 |
218 | expect(url).to.equal(expectedDataUri);
219 |
220 | done();
221 | });
222 | });
223 |
224 | it('should build using WebAudio if the API is available and audioContext is provided', function(done) {
225 | var peaks = {
226 | options: {
227 | mediaElement: document.getElementById('media'),
228 | webAudio: {
229 | audioContext: new TestAudioContext(),
230 | scale: 512
231 | },
232 | zoomLevels: [512, 1024, 2048, 4096]
233 | },
234 | player: {
235 | getCurrentSource: sinon.stub().returns(document.getElementById('media').currentSrc)
236 | }
237 | };
238 |
239 | var waveformBuilder = new WaveformBuilder(peaks);
240 |
241 | waveformBuilder.init(peaks.options, function(err, waveformData) {
242 | expect(err).to.equal(null);
243 | expect(waveformData).to.be.an.instanceOf(WaveformData);
244 | done();
245 | });
246 | });
247 | });
248 | });
249 |
--------------------------------------------------------------------------------
/test/unit/api-views-spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('./setup');
4 |
5 | var Peaks = require('../../src/main');
6 | var WaveformOverview = require('../../src/main/views/waveform.overview');
7 | var WaveformZoomView = require('../../src/main/views/waveform.zoomview');
8 |
9 | describe('Peaks.views', function() {
10 | var p;
11 |
12 | var overviewContainer = null;
13 | var zoomviewContainer = null;
14 |
15 | beforeEach(function() {
16 | overviewContainer = document.createElement('div');
17 | overviewContainer.style.width = '400px';
18 | overviewContainer.style.height = '100px';
19 | document.body.appendChild(overviewContainer);
20 |
21 | zoomviewContainer = document.createElement('div');
22 | zoomviewContainer.style.width = '400px';
23 | zoomviewContainer.style.height = '100px';
24 | document.body.appendChild(zoomviewContainer);
25 | });
26 |
27 | afterEach(function() {
28 | document.body.removeChild(overviewContainer);
29 | document.body.removeChild(zoomviewContainer);
30 | });
31 |
32 | afterEach(function() {
33 | if (p) {
34 | p.destroy();
35 | }
36 | });
37 |
38 | describe('createZoomview', function() {
39 | context('with existing zoomview', function() {
40 | beforeEach(function(done) {
41 | p = Peaks.init({
42 | containers: {
43 | zoomview: zoomviewContainer
44 | },
45 | mediaElement: document.getElementById('media'),
46 | dataUri: {
47 | json: 'base/test_data/sample.json'
48 | }
49 | });
50 |
51 | p.on('peaks.ready', done);
52 | });
53 |
54 | it('should return the existing zoomview instance', function() {
55 | var view = p.views.getView('zoomview');
56 |
57 | expect(view).to.be.an.instanceOf(WaveformZoomView);
58 |
59 | expect(p.views.createZoomview(zoomviewContainer)).to.equal(view);
60 | });
61 | });
62 |
63 | context('without existing zoomview', function() {
64 | beforeEach(function(done) {
65 | p = Peaks.init({
66 | containers: {
67 | overview: overviewContainer
68 | },
69 | mediaElement: document.getElementById('media'),
70 | dataUri: {
71 | json: 'base/test_data/sample.json'
72 | }
73 | });
74 |
75 | p.on('peaks.ready', done);
76 | });
77 |
78 | it('should return a new zoomview instance', function() {
79 | expect(p.views.getView('zoomview')).to.equal(null);
80 |
81 | var view = p.views.createZoomview(zoomviewContainer);
82 |
83 | expect(view).to.be.an.instanceOf(WaveformZoomView);
84 |
85 | expect(p.views.getView('zoomview')).to.equal(view);
86 | });
87 | });
88 | });
89 |
90 | describe('createOverview', function() {
91 | context('with existing overview', function() {
92 | beforeEach(function(done) {
93 | p = Peaks.init({
94 | containers: {
95 | overview: overviewContainer
96 | },
97 | mediaElement: document.getElementById('media'),
98 | dataUri: {
99 | json: 'base/test_data/sample.json'
100 | }
101 | });
102 |
103 | p.on('peaks.ready', done);
104 | });
105 |
106 | it('should return the existing overview instance', function() {
107 | var view = p.views.getView('overview');
108 |
109 | expect(view).to.be.an.instanceOf(WaveformOverview);
110 |
111 | expect(p.views.createOverview(overviewContainer)).to.equal(view);
112 | });
113 | });
114 |
115 | context('without existing overview', function() {
116 | beforeEach(function(done) {
117 | p = Peaks.init({
118 | containers: {
119 | zoomview: zoomviewContainer
120 | },
121 | mediaElement: document.getElementById('media'),
122 | dataUri: {
123 | json: 'base/test_data/sample.json'
124 | }
125 | });
126 |
127 | p.on('peaks.ready', done);
128 | });
129 |
130 | it('should return a new overview instance', function() {
131 | expect(p.views.getView('overview')).to.equal(null);
132 |
133 | var view = p.views.createOverview(overviewContainer);
134 |
135 | expect(view).to.be.an.instanceOf(WaveformOverview);
136 |
137 | expect(p.views.getView('overview')).to.equal(view);
138 | });
139 | });
140 | });
141 |
142 | describe('getView', function() {
143 | context('with zoomview and overview containers', function() {
144 | beforeEach(function(done) {
145 | p = Peaks.init({
146 | containers: {
147 | zoomview: zoomviewContainer,
148 | overview: overviewContainer
149 | },
150 | mediaElement: document.getElementById('media'),
151 | dataUri: {
152 | json: 'base/test_data/sample.json'
153 | }
154 | });
155 |
156 | p.on('peaks.ready', done);
157 | });
158 |
159 | it('should return the zoomview', function() {
160 | var view = p.views.getView('zoomview');
161 | expect(view).to.be.an.instanceOf(WaveformZoomView);
162 | });
163 |
164 | it('should return the overview', function() {
165 | var view = p.views.getView('overview');
166 | expect(view).to.be.an.instanceOf(WaveformOverview);
167 | });
168 |
169 | it('should return null if given no view name', function() {
170 | var view = p.views.getView();
171 | expect(view).to.equal(null);
172 | });
173 |
174 | it('should return null if given an invalid view name', function() {
175 | var view = p.views.getView('unknown');
176 | expect(view).to.equal(null);
177 | });
178 | });
179 |
180 | context('with only a zoomview container', function() {
181 | beforeEach(function(done) {
182 | p = Peaks.init({
183 | containers: {
184 | zoomview: zoomviewContainer
185 | },
186 | mediaElement: document.getElementById('media'),
187 | dataUri: {
188 | json: 'base/test_data/sample.json'
189 | }
190 | });
191 |
192 | p.on('peaks.ready', done);
193 | });
194 |
195 | it('should return the zoomview', function() {
196 | var view = p.views.getView('zoomview');
197 | expect(view).to.be.an.instanceOf(WaveformZoomView);
198 | });
199 |
200 | it('should return null if given the overview view name', function() {
201 | var view = p.views.getView('overview');
202 | expect(view).to.equal(null);
203 | });
204 |
205 | it('should return the zoomview if given no view name', function() {
206 | var view = p.views.getView();
207 | expect(view).to.be.an.instanceOf(WaveformZoomView);
208 | });
209 |
210 | it('should return null if given an invalid view name', function() {
211 | var view = p.views.getView('unknown');
212 | expect(view).to.equal(null);
213 | });
214 | });
215 |
216 | context('with only an overview container', function() {
217 | beforeEach(function(done) {
218 | p = Peaks.init({
219 | containers: {
220 | overview: overviewContainer
221 | },
222 | mediaElement: document.getElementById('media'),
223 | dataUri: {
224 | json: 'base/test_data/sample.json'
225 | }
226 | });
227 |
228 | p.on('peaks.ready', done);
229 | });
230 |
231 | it('should return null if given the zoomview view name', function() {
232 | var view = p.views.getView('zoomview');
233 | expect(view).to.equal(null);
234 | });
235 |
236 | it('should return the overview', function() {
237 | var view = p.views.getView('overview');
238 | expect(view).to.be.an.instanceOf(WaveformOverview);
239 | });
240 |
241 | it('should return the overview if given no view name', function() {
242 | var view = p.views.getView();
243 | expect(view).to.be.an.instanceOf(WaveformOverview);
244 | });
245 |
246 | it('should return null if given an invalid view name', function() {
247 | var view = p.views.getView('unknown');
248 | expect(view).to.equal(null);
249 | });
250 | });
251 | });
252 | });
253 |
--------------------------------------------------------------------------------
/peaks.js.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Peaks.js TypeScript Definitions
3 | * @author Evan Louie true the point time can be
22 | * adjusted via the user interface.
23 | * Default: false.
24 | * @property {String=} color Point marker color.
25 | * Default: a random color.
26 | * @property {String=} labelText Point label text.
27 | * Default: an empty string.
28 | * @property {String=} id A unique point identifier.
29 | * Default: an automatically generated identifier.
30 | */
31 |
32 | /**
33 | * Handles all functionality related to the adding, removing and manipulation
34 | * of points. A point is a single instant of time.
35 | *
36 | * @class
37 | * @alias WaveformPoints
38 | *
39 | * @param {Peaks} peaks The parent Peaks object.
40 | */
41 |
42 | function WaveformPoints(peaks) {
43 | this._peaks = peaks;
44 | this._points = [];
45 | this._pointsById = {};
46 | this._pointIdCounter = 0;
47 | }
48 |
49 | /**
50 | * Returns a new unique point id value.
51 | *
52 | * @returns {String}
53 | */
54 |
55 | WaveformPoints.prototype._getNextPointId = function() {
56 | return 'peaks.point.' + this._pointIdCounter++;
57 | };
58 |
59 | /**
60 | * Adds a new point object.
61 | *
62 | * @private
63 | * @param {Point} point
64 | */
65 |
66 | WaveformPoints.prototype._addPoint = function(point) {
67 | this._points.push(point);
68 |
69 | this._pointsById[point.id] = point;
70 | };
71 |
72 | /**
73 | * Creates a new point object.
74 | *
75 | * @private
76 | * @param {PointOptions} options
77 | * @returns {Point}
78 | */
79 |
80 | WaveformPoints.prototype._createPoint = function(options) {
81 | if (Object.prototype.hasOwnProperty.call(options, 'timestamp') ||
82 | !Object.prototype.hasOwnProperty.call(options, 'time')) {
83 | // eslint-disable-next-line max-len
84 | this._peaks.options.deprecationLogger("peaks.points.add(): The 'timestamp' attribute is deprecated; use 'time' instead");
85 | options.time = options.timestamp;
86 | }
87 |
88 | var point = new Point(
89 | this,
90 | Utils.isNullOrUndefined(options.id) ? this._getNextPointId() : options.id,
91 | options.time,
92 | options.labelText,
93 | options.color,
94 | Boolean(options.editable)
95 | );
96 |
97 | return point;
98 | };
99 |
100 | /**
101 | * Returns all points.
102 | *
103 | * @returns {Arraynull if not found.
112 | *
113 | * @param {String} id
114 | * @returns {Point|null}
115 | */
116 |
117 | WaveformPoints.prototype.getPoint = function(id) {
118 | return this._pointsById[id] || null;
119 | };
120 |
121 | /**
122 | * Returns all points within a given time region.
123 | *
124 | * @param {Number} startTime The start of the time region, in seconds.
125 | * @param {Number} endTime The end of the time region, in seconds.
126 | * @returns {Arraypoints.remove event with the removed {@link Point}
227 | * objects.
228 | *
229 | * @private
230 | * @param {Function} predicate A predicate function that identifies which
231 | * points to remove.
232 | * @returns {Arraypoints.remove_all event.
289 | */
290 |
291 | WaveformPoints.prototype.removeAll = function() {
292 | this._points = [];
293 | this._pointsById = {};
294 | this._peaks.emit('points.remove_all');
295 | };
296 |
297 | return WaveformPoints;
298 | });
299 |
--------------------------------------------------------------------------------
/demo/multi-channel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 88 | Peaks.js is a JavaScript library that allows you to display and 89 | interaction with audio waveforms in the browser. 90 |
91 |92 | It was developed by BBC R&D 93 | to allow audio editors to make accurate clippings of audio content. 94 | You can read more about the project 95 | here. 96 |
97 | 98 |100 | The following pages demonstrate various configuration options: 101 |
102 |103 | Precomputed Waveform Data | 104 | Web Audio API | 105 | Single Zoomable Waveform | 106 | Single Fixed Waveform | 107 | Cue Events | 108 | Changing the Media URL | 109 | Multi-Channel Waveform 110 |
111 |113 | This demo shows how to show a stereo waveform. Use the Select source 114 | control to switch between pre-computed waveform data, fetched from the 115 | web server, and waveform data generated in the browser using the Web 116 | Audio API. 117 |
118 |119 | The pre-computed waveform data was produced using 120 | audiowaveform, with 121 | the following options: 122 |
123 |
124 | audiowaveform -i 07023003.mp3 -o 07023003-2channel.dat -b 8 --split-channels
125 |
127 | Audio content is copyright BBC, from the BBC Sound Effects 128 | library, used under the terms of the 129 | RemArc Licence. 130 |
131 |points.enter,
87 | * segments.enter, and segments.exit events.
88 | *
89 | * @class
90 | * @alias CueEmitter
91 | *
92 | * @param {Peaks} peaks Parent {@link Peaks} instance.
93 | */
94 |
95 | function CueEmitter(peaks) {
96 | this._cues = [];
97 | this._peaks = peaks;
98 | this._previousTime = -1;
99 | this._updateCues = this._updateCues.bind(this);
100 | // Event handlers:
101 | this._onPlay = this.onPlay.bind(this);
102 | this._onSeek = this.onSeek.bind(this);
103 | this._onTimeUpdate = this.onTimeUpdate.bind(this);
104 | this._onAnimationFrame = this.onAnimationFrame.bind(this);
105 | this._rAFHandle = null;
106 | this._activeSegments = {};
107 | this._attachEventHandlers();
108 | }
109 |
110 | /**
111 | * This function is bound to all {@link Peaks} events relating to mutated
112 | * [Points]{@link Point} or [Segments]{@link Segment}, and updates the
113 | * list of cues accordingly.
114 | *
115 | * @private
116 | */
117 |
118 | CueEmitter.prototype._updateCues = function() {
119 | var self = this;
120 |
121 | var points = self._peaks.points.getPoints();
122 | var segments = self._peaks.segments.getSegments();
123 |
124 | self._cues.length = 0;
125 |
126 | points.forEach(function(point) {
127 | self._cues.push(new Cue(point.time, Cue.POINT, point.id));
128 | });
129 |
130 | segments.forEach(function(segment) {
131 | self._cues.push(new Cue(segment.startTime, Cue.SEGMENT_START, segment.id));
132 | self._cues.push(new Cue(segment.endTime, Cue.SEGMENT_END, segment.id));
133 | });
134 |
135 | self._cues.sort(Cue.sorter);
136 |
137 | var time = self._peaks.player.getCurrentTime();
138 |
139 | self._updateActiveSegments(time);
140 | };
141 |
142 | /**
143 | * Emits events for any cues passed through during media playback.
144 | *
145 | * @param {Number} time The current time on the media timeline.
146 | * @param {Number} previousTime The previous time on the media timeline when
147 | * this function was called.
148 | */
149 |
150 | CueEmitter.prototype._onUpdate = function(time, previousTime) {
151 | var isForward = time > previousTime;
152 | var start;
153 | var end;
154 | var step;
155 |
156 | if (isForward) {
157 | start = 0;
158 | end = this._cues.length;
159 | step = 1;
160 | }
161 | else {
162 | start = this._cues.length - 1;
163 | end = -1;
164 | step = -1;
165 | }
166 |
167 | // Cues are sorted
168 | for (var i = start; isForward ? i < end : i > end; i += step) {
169 | var cue = this._cues[i];
170 |
171 | if (isForward ? cue.time > previousTime : cue.time < previousTime) {
172 | if (isForward ? cue.time > time : cue.time < time) {
173 | break;
174 | }
175 |
176 | // Cue falls between time and previousTime
177 |
178 | var marker = getPointOrSegment(this._peaks, cue);
179 |
180 | var eventType = isForward ? eventTypes.forward[cue.type] :
181 | eventTypes.reverse[cue.type];
182 |
183 | if (eventType === EVENT_TYPE_SEGMENT_ENTER) {
184 | this._activeSegments[marker.id] = marker;
185 | }
186 | else if (eventType === EVENT_TYPE_SEGMENT_EXIT) {
187 | delete this._activeSegments[marker.id];
188 | }
189 |
190 | this._peaks.emit(eventNames[eventType], marker);
191 | }
192 | }
193 | };
194 |
195 | // the next handler and onAnimationFrame are bound together
196 | // when the window isn't in focus, rAF is throttled
197 | // falling back to timeUpdate
198 |
199 | CueEmitter.prototype.onTimeUpdate = function(time) {
200 | if (windowIsVisible()) {
201 | return;
202 | }
203 |
204 | if (this._peaks.player.isPlaying() && !this._peaks.player.isSeeking()) {
205 | this._onUpdate(time, this._previousTime);
206 | }
207 |
208 | this._previousTime = time;
209 | };
210 |
211 | CueEmitter.prototype.onAnimationFrame = function() {
212 | var time = this._peaks.player.getCurrentTime();
213 |
214 | if (!this._peaks.player.isSeeking()) {
215 | this._onUpdate(time, this._previousTime);
216 | }
217 |
218 | this._previousTime = time;
219 |
220 | if (this._peaks.player.isPlaying()) {
221 | this._rAFHandle = requestAnimationFrame(this._onAnimationFrame);
222 | }
223 | };
224 |
225 | CueEmitter.prototype.onPlay = function() {
226 | this._previousTime = this._peaks.player.getCurrentTime();
227 | this._rAFHandle = requestAnimationFrame(this._onAnimationFrame);
228 | };
229 |
230 | CueEmitter.prototype.onSeek = function(time) {
231 | this._previousTime = time;
232 |
233 | this._updateActiveSegments(time);
234 | };
235 |
236 | function getSegmentIdComparator(id) {
237 | return function compareSegmentIds(segment) {
238 | return segment.id === id;
239 | };
240 | }
241 |
242 | /**
243 | * The active segments is the set of all segments which overlap the current
244 | * playhead position. This function updates that set and emits
245 | * segments.enter and segments.exit events.
246 | */
247 |
248 | CueEmitter.prototype._updateActiveSegments = function(time) {
249 | var self = this;
250 |
251 | var activeSegments = self._peaks.segments.getSegmentsAtTime(time);
252 |
253 | // Remove any segments no longer active.
254 |
255 | for (var id in self._activeSegments) {
256 | if (Object.prototype.hasOwnProperty.call(self._activeSegments, id)) {
257 | var segment = activeSegments.find(getSegmentIdComparator(id));
258 |
259 | if (!segment) {
260 | self._peaks.emit('segments.exit', self._activeSegments[id]);
261 | delete self._activeSegments[id];
262 | }
263 | }
264 | }
265 |
266 | // Add new active segments.
267 |
268 | activeSegments.forEach(function(segment) {
269 | if (!(segment.id in self._activeSegments)) {
270 | self._activeSegments[segment.id] = segment;
271 | self._peaks.emit('segments.enter', segment);
272 | }
273 | });
274 | };
275 |
276 | var triggerUpdateOn = Array(
277 | 'points.update',
278 | 'points.dragmove',
279 | 'points.add',
280 | 'points.remove',
281 | 'points.remove_all',
282 | 'segments.update',
283 | 'segments.dragged',
284 | 'segments.add',
285 | 'segments.remove',
286 | 'segments.remove_all'
287 | );
288 |
289 | CueEmitter.prototype._attachEventHandlers = function() {
290 | this._peaks.on('player_time_update', this._onTimeUpdate);
291 | this._peaks.on('player_play', this._onPlay);
292 | this._peaks.on('player_seek', this._onSeek);
293 |
294 | for (var i = 0; i < triggerUpdateOn.length; i++) {
295 | this._peaks.on(triggerUpdateOn[i], this._updateCues);
296 | }
297 |
298 | this._updateCues();
299 | };
300 |
301 | CueEmitter.prototype._detachEventHandlers = function() {
302 | this._peaks.off('player_time_update', this._onTimeUpdate);
303 | this._peaks.off('player_play', this._onPlay);
304 | this._peaks.off('player_seek', this._onSeek);
305 |
306 | for (var i = 0; i < triggerUpdateOn.length; i++) {
307 | this._peaks.off(triggerUpdateOn[i], this._updateCues);
308 | }
309 | };
310 |
311 | CueEmitter.prototype.destroy = function() {
312 | if (this._rAFHandle) {
313 | cancelAnimationFrame(this._rAFHandle);
314 | this._rAFHandle = null;
315 | }
316 |
317 | this._detachEventHandlers();
318 |
319 | this._previousTime = -1;
320 | this._marks.length = 0;
321 | };
322 |
323 | return CueEmitter;
324 | });
325 |
--------------------------------------------------------------------------------
/src/main/waveform/waveform.mixins.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file
3 | *
4 | * Common functions used in multiple modules are collected here for DRY purposes.
5 | *
6 | * @module peaks/waveform/waveform.mixins
7 | */
8 |
9 | define(['konva'], function(Konva) {
10 | 'use strict';
11 |
12 | /**
13 | * Parameters for the {@link createSegmentMarker} function.
14 | *
15 | * @typedef {Object} CreateSegmentMarkerOptions
16 | * @global
17 | * @property {Boolean} draggable If true, marker is draggable.
18 | * @property {Number} height Height of handle group container (canvas).
19 | * @property {String} color Colour hex value for handle and line marker.
20 | * @property {Boolean} inMarker Is this marker the inMarker (LHS) or outMarker (RHS).
21 | * @property {Konva.Group} segmentGroup
22 | * @property {Object} segment
23 | * @property {Konva.Layer} layer
24 | * @property {Function} onDrag Callback after drag completed.
25 | */
26 |
27 | /**
28 | * Creates a Left or Right side segment handle group in Konva based on the
29 | * given options.
30 | *
31 | * @param {CreateSegmentMarkerOptions} options
32 | * @returns {Konva.Group} Konva group object of handle marker element.
33 | */
34 |
35 | function createSegmentMarker(options) {
36 | var handleHeight = 20;
37 | var handleWidth = handleHeight / 2;
38 | var handleY = (options.height / 2) - 10.5;
39 | var handleX = -(handleWidth / 2) + 0.5;
40 |
41 | var group = new Konva.Group({
42 | draggable: options.draggable,
43 | dragBoundFunc: function(pos) {
44 | var limit;
45 |
46 | if (options.inMarker) {
47 | limit = options.segmentGroup.outMarker.getX() - options.segmentGroup.outMarker.getWidth();
48 |
49 | if (pos.x > limit) {
50 | pos.x = limit;
51 | }
52 | }
53 | else {
54 | limit = options.segmentGroup.inMarker.getX() + options.segmentGroup.inMarker.getWidth();
55 |
56 | if (pos.x < limit) {
57 | pos.x = limit;
58 | }
59 | }
60 |
61 | return {
62 | x: pos.x,
63 | y: this.getAbsolutePosition().y
64 | };
65 | }
66 | });
67 |
68 | var xPosition = options.inMarker ? -24 : 24;
69 |
70 | var text = new Konva.Text({
71 | x: xPosition,
72 | y: (options.height / 2) - 5,
73 | text: '',
74 | fontSize: 10,
75 | fontFamily: 'sans-serif',
76 | fill: '#000',
77 | textAlign: 'center'
78 | });
79 |
80 | text.hide();
81 | group.label = text;
82 |
83 | var handle = new Konva.Rect({
84 | x: handleX,
85 | y: handleY,
86 | width: handleWidth,
87 | height: handleHeight,
88 | fill: options.color,
89 | stroke: options.color,
90 | strokeWidth: 1
91 | });
92 |
93 | // Vertical Line
94 |
95 | var line = new Konva.Line({
96 | x: 0,
97 | y: 0,
98 | points: [0.5, 0, 0.5, options.height],
99 | stroke: options.color,
100 | strokeWidth: 1
101 | });
102 |
103 | // Events
104 |
105 | if (options.draggable && options.onDrag) {
106 | group.on('dragmove', function(event) {
107 | options.onDrag(options.segmentGroup, options.segment);
108 | });
109 | group.on('dragstart', function(event) {
110 | if (options.inMarker) {
111 | text.setX(xPosition - text.getWidth());
112 | }
113 | text.show();
114 | options.layer.draw();
115 | });
116 | group.on('dragend', function(event) {
117 | text.hide();
118 | options.layer.draw();
119 | });
120 | }
121 |
122 | handle.on('mouseover touchstart', function(event) {
123 | if (options.inMarker) {
124 | text.setX(xPosition - text.getWidth());
125 | }
126 | text.show();
127 | options.layer.draw();
128 | });
129 |
130 | handle.on('mouseout touchend', function(event) {
131 | text.hide();
132 | options.layer.draw();
133 | });
134 |
135 | group.add(text);
136 | group.add(line);
137 | group.add(handle);
138 |
139 | return group;
140 | }
141 |
142 | /**
143 | * Creates a Konva.Text object that renders a segment's label text.
144 | *
145 | * @param {Konva.Group} segmentGroup
146 | * @param {Segment} segment
147 | * @returns {Konva.Text}
148 | */
149 |
150 | function createSegmentLabel(segmentGroup, segment) {
151 | return new Konva.Text({
152 | x: 12,
153 | y: 12,
154 | text: segment.labelText,
155 | textAlign: 'center',
156 | fontSize: 12,
157 | fontFamily: 'Arial, sans-serif',
158 | fill: '#000'
159 | });
160 | }
161 |
162 | /**
163 | * Parameters for the {@link createPointMarker} function.
164 | *
165 | * @typedef {Object} CreatePointMarkerOptions
166 | * @global
167 | * @property {Boolean} draggable If true, marker is draggable.
168 | * @property {Boolean} showLabel If true, show the label text next to the marker.
169 | * @property {String} handleColor Color hex value for handle and line marker.
170 | * @property {Number} height Height of handle group container (canvas).
171 | * @property {Konva.Group} pointGroup Point marker UI object.
172 | * @property {Object} point Point object with timestamp.
173 | * @property {Konva.Layer} layer Layer that contains the pointGroup.
174 | * @property {Function} onDblClick
175 | * @property {Function} onDragStart
176 | * @property {Function} onDragMove Callback during mouse drag operations.
177 | * @property {Function} onDragEnd
178 | * @property {Function} onMouseOver
179 | * @property {Function} onMouseLeave
180 | */
181 |
182 | /**
183 | * Creates a point handle group in Konva based on the given options.
184 | *
185 | * @param {CreatePointMarkerOptions} options
186 | * @returns {Konva.Group} Konva group object of handle marker elements
187 | */
188 |
189 | function createPointMarker(options) {
190 | var handleTop = (options.height / 2) - 10.5;
191 | var handleWidth = 10;
192 | var handleHeight = 20;
193 | var handleX = -(handleWidth / 2) + 0.5; // Place in the middle of the marker
194 |
195 | var group = new Konva.Group({
196 | draggable: options.draggable,
197 | dragBoundFunc: function(pos) {
198 | return {
199 | x: pos.x, // No constraint horizontally
200 | y: this.getAbsolutePosition().y // Constrained vertical line
201 | };
202 | }
203 | });
204 |
205 | if (options.onDragStart) {
206 | group.on('dragstart', function(event) {
207 | options.onDragStart(options.point);
208 | });
209 | }
210 |
211 | if (options.onDragMove) {
212 | group.on('dragmove', function(event) {
213 | options.onDragMove(options.point);
214 | });
215 | }
216 |
217 | if (options.onDragEnd) {
218 | group.on('dragend', function(event) {
219 | options.onDragEnd(options.point);
220 | });
221 | }
222 |
223 | if (options.onDblClick) {
224 | group.on('dblclick', function(event) {
225 | options.onDblClick(options.point);
226 | });
227 | }
228 |
229 | if (options.onMouseEnter) {
230 | group.on('mouseenter', function(event) {
231 | options.onMouseEnter(options.point);
232 | });
233 | }
234 |
235 | if (options.onMouseLeave) {
236 | group.on('mouseleave', function(event) {
237 | options.onMouseLeave(options.point);
238 | });
239 | }
240 |
241 | // Label
242 | var text = null;
243 |
244 | if (options.showLabel) {
245 | text = new Konva.Text({
246 | x: 2,
247 | y: 12,
248 | text: options.point.labelText,
249 | textAlign: 'left',
250 | fontSize: 10,
251 | fontFamily: 'sans-serif',
252 | fill: '#000'
253 | });
254 |
255 | group.label = text;
256 | }
257 |
258 | // Handle
259 | var handle = null;
260 |
261 | if (options.draggable) {
262 | handle = new Konva.Rect({
263 | x: handleX,
264 | y: handleTop,
265 | width: handleWidth,
266 | height: handleHeight,
267 | fill: options.handleColor
268 | });
269 | }
270 |
271 | // Line
272 | var line = new Konva.Line({
273 | x: 0,
274 | y: 0,
275 | points: [0, 0, 0, options.height],
276 | stroke: options.handleColor,
277 | strokeWidth: 1
278 | });
279 |
280 | // Events
281 | var time = null;
282 |
283 | if (handle) {
284 | // Time
285 | time = new Konva.Text({
286 | x: -24,
287 | y: (options.height / 2) - 5,
288 | text: '',
289 | fontSize: 10,
290 | fontFamily: 'sans-serif',
291 | fill: '#000',
292 | textAlign: 'center'
293 | });
294 |
295 | time.hide();
296 | group.time = time;
297 |
298 | handle.on('mouseover touchstart', function(event) {
299 | // Position text to the left of the marker
300 | time.setX(-24 - time.getWidth());
301 | time.show();
302 | options.layer.draw();
303 | });
304 |
305 | handle.on('mouseout touchend', function(event) {
306 | time.hide();
307 | options.layer.draw();
308 | });
309 |
310 | group.on('dragstart', function(event) {
311 | time.setX(-24 - time.getWidth());
312 | time.show();
313 | options.layer.draw();
314 | });
315 |
316 | group.on('dragend', function(event) {
317 | time.hide();
318 | options.layer.draw();
319 | });
320 | }
321 |
322 | if (handle) {
323 | group.add(handle);
324 | }
325 |
326 | group.add(line);
327 |
328 | if (text) {
329 | group.add(text);
330 | }
331 |
332 | if (time) {
333 | group.add(time);
334 | }
335 |
336 | return group;
337 | }
338 |
339 | // Public API
340 |
341 | return {
342 | createSegmentMarker: createSegmentMarker,
343 | createPointMarker: createPointMarker,
344 | createSegmentLabel: createSegmentLabel
345 | };
346 | });
347 |
--------------------------------------------------------------------------------
/src/main/views/waveform.overview.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file
3 | *
4 | * Defines the {@link WaveformOverview} class.
5 | *
6 | * @module peaks/views/waveform.overview
7 | */
8 |
9 | define([
10 | 'peaks/views/playhead-layer',
11 | 'peaks/views/points-layer',
12 | 'peaks/views/segments-layer',
13 | 'peaks/views/waveform-shape',
14 | 'peaks/views/helpers/mousedraghandler',
15 | 'peaks/waveform/waveform.axis',
16 | 'peaks/waveform/waveform.utils',
17 | 'konva'
18 | ], function(
19 | PlayheadLayer,
20 | PointsLayer,
21 | SegmentsLayer,
22 | WaveformShape,
23 | MouseDragHandler,
24 | WaveformAxis,
25 | Utils,
26 | Konva) {
27 | 'use strict';
28 |
29 | /**
30 | * Creates the overview waveform view.
31 | *
32 | * @class
33 | * @alias WaveformOverview
34 | *
35 | * @param {WaveformData} waveformData
36 | * @param {HTMLElement} container
37 | * @param {Peaks} peaks
38 | */
39 |
40 | function WaveformOverview(waveformData, container, peaks) {
41 | var self = this;
42 |
43 | self._originalWaveformData = waveformData;
44 | self._container = container;
45 | self._peaks = peaks;
46 |
47 | self._options = peaks.options;
48 |
49 | self._width = container.clientWidth;
50 | self._height = container.clientHeight || self._options.height;
51 |
52 | if (self._width !== 0) {
53 | self._data = waveformData.resample(self._width);
54 | }
55 | else {
56 | self._data = waveformData;
57 | }
58 |
59 | self._resizeTimeoutId = null;
60 |
61 | self._stage = new Konva.Stage({
62 | container: container,
63 | width: self._width,
64 | height: self._height
65 | });
66 |
67 | self._waveformLayer = new Konva.FastLayer();
68 |
69 | self._axis = new WaveformAxis(self, self._waveformLayer, peaks.options);
70 |
71 | self._createWaveform();
72 |
73 | self._segmentsLayer = new SegmentsLayer(peaks, self, false);
74 | self._segmentsLayer.addToStage(self._stage);
75 |
76 | self._pointsLayer = new PointsLayer(peaks, self, false, false);
77 | self._pointsLayer.addToStage(self._stage);
78 |
79 | self._createHighlightLayer();
80 |
81 | self._playheadLayer = new PlayheadLayer(
82 | peaks,
83 | self,
84 | false, // showPlayheadTime
85 | self._options.mediaElement.currentTime
86 | );
87 |
88 | self._playheadLayer.addToStage(self._stage);
89 |
90 | self._mouseDragHandler = new MouseDragHandler(self._stage, {
91 | onMouseDown: function(mousePosX) {
92 | mousePosX = Utils.clamp(mousePosX, 0, self._width);
93 |
94 | var time = self.pixelsToTime(mousePosX);
95 |
96 | self._playheadLayer.updatePlayheadTime(time);
97 |
98 | peaks.player.seek(time);
99 | },
100 |
101 | onMouseMove: function(mousePosX) {
102 | mousePosX = Utils.clamp(mousePosX, 0, self._width);
103 |
104 | var time = self.pixelsToTime(mousePosX);
105 |
106 | // Update the playhead position. This gives a smoother visual update
107 | // than if we only use the player_time_update event.
108 | self._playheadLayer.updatePlayheadTime(time);
109 |
110 | self._peaks.player.seek(time);
111 | }
112 | });
113 |
114 | // Events
115 |
116 | peaks.on('player_play', function(time) {
117 | self._playheadLayer.updatePlayheadTime(time);
118 | });
119 |
120 | peaks.on('player_pause', function(time) {
121 | self._playheadLayer.stop(time);
122 | });
123 |
124 | peaks.on('player_time_update', function(time) {
125 | self._playheadLayer.updatePlayheadTime(time);
126 | });
127 |
128 | peaks.on('zoomview.displaying', function(startTime, endTime) {
129 | if (!self._highlightRect) {
130 | self._createHighlightRect(startTime, endTime);
131 | }
132 |
133 | self._updateHighlightRect(startTime, endTime);
134 | });
135 |
136 | peaks.on('window_resize', function() {
137 | if (self._resizeTimeoutId) {
138 | clearTimeout(self._resizeTimeoutId);
139 | self._resizeTimeoutId = null;
140 | }
141 |
142 | // Avoid resampling waveform data to zero width
143 | if (self._container.clientWidth !== 0) {
144 | self._width = self._container.clientWidth;
145 | self._stage.setWidth(self._width);
146 |
147 | self._resizeTimeoutId = setTimeout(function() {
148 | self._width = self._container.clientWidth;
149 | self._data = self._originalWaveformData.resample(self._width);
150 | self._stage.setWidth(self._width);
151 |
152 | self._updateWaveform();
153 | }, 500);
154 | }
155 | });
156 | }
157 |
158 | WaveformOverview.prototype.setWaveformData = function(waveformData) {
159 | this._originalWaveformData = waveformData;
160 |
161 | if (this._width !== 0) {
162 | this._data = waveformData.resample(this._width);
163 | }
164 | else {
165 | this._data = waveformData;
166 | }
167 |
168 | this._updateWaveform();
169 | };
170 |
171 | /**
172 | * Returns the pixel index for a given time, for the current zoom level.
173 | *
174 | * @param {Number} time Time, in seconds.
175 | * @returns {Number} Pixel index.
176 | */
177 |
178 | WaveformOverview.prototype.timeToPixels = function(time) {
179 | return Math.floor(time * this._data.sample_rate / this._data.scale);
180 | };
181 |
182 | /**
183 | * Returns the time for a given pixel index, for the current zoom level.
184 | *
185 | * @param {Number} pixels Pixel index.
186 | * @returns {Number} Time, in seconds.
187 | */
188 |
189 | WaveformOverview.prototype.pixelsToTime = function(pixels) {
190 | return pixels * this._data.scale / this._data.sample_rate;
191 | };
192 |
193 | /**
194 | * @returns {Number} The start position of the waveform shown in the view,
195 | * in pixels.
196 | */
197 |
198 | WaveformOverview.prototype.getFrameOffset = function() {
199 | return 0;
200 | };
201 |
202 | /**
203 | * @returns {Number} The width of the view, in pixels.
204 | */
205 |
206 | WaveformOverview.prototype.getWidth = function() {
207 | return this._width;
208 | };
209 |
210 | /**
211 | * @returns {Number} The height of the view, in pixels.
212 | */
213 |
214 | WaveformOverview.prototype.getHeight = function() {
215 | return this._height;
216 | };
217 |
218 | /**
219 | * Adjusts the amplitude scale of waveform shown in the view, which allows
220 | * users to zoom the waveform vertically.
221 | *
222 | * @param {Number} scale The new amplitude scale factor
223 | */
224 |
225 | WaveformOverview.prototype.setAmplitudeScale = function(scale) {
226 | if (!Utils.isNumber(scale) || !Number.isFinite(scale)) {
227 | throw new Error('view.setAmplitudeScale(): Scale must be a valid number');
228 | }
229 |
230 | this._waveformShape.setAmplitudeScale(scale);
231 | this._waveformLayer.draw();
232 | };
233 |
234 | /**
235 | * @returns {WaveformData} The view's waveform data.
236 | */
237 |
238 | WaveformOverview.prototype.getWaveformData = function() {
239 | return this._data;
240 | };
241 |
242 | /**
243 | * Creates a {WaveformShape} object that draws the waveform in the view,
244 | * and adds it to the wav
245 | */
246 |
247 | WaveformOverview.prototype._createWaveform = function() {
248 | this._waveformShape = new WaveformShape({
249 | color: this._options.overviewWaveformColor,
250 | view: this
251 | });
252 |
253 | this._waveformLayer.add(this._waveformShape);
254 | this._stage.add(this._waveformLayer);
255 | };
256 |
257 | WaveformOverview.prototype._createHighlightLayer = function() {
258 | this._highlightLayer = new Konva.FastLayer();
259 | this._stage.add(this._highlightLayer);
260 | };
261 |
262 | WaveformOverview.prototype._createHighlightRect = function(startTime, endTime) {
263 | this._highlightRectStartTime = startTime;
264 | this._highlightRectEndTime = endTime;
265 |
266 | var startOffset = this.timeToPixels(startTime);
267 | var endOffset = this.timeToPixels(endTime);
268 |
269 | this._highlightRect = new Konva.Rect({
270 | startOffset: 0,
271 | y: 11,
272 | width: endOffset - startOffset,
273 | stroke: this._options.overviewHighlightRectangleColor,
274 | strokeWidth: 1,
275 | height: this._height - (11 * 2),
276 | fill: this._options.overviewHighlightRectangleColor,
277 | opacity: 0.3,
278 | cornerRadius: 2
279 | });
280 |
281 | this._highlightLayer.add(this._highlightRect);
282 | };
283 |
284 | /**
285 | * Updates the position of the highlight region.
286 | *
287 | * @param {Number} startTime The start of the highlight region, in seconds.
288 | * @param {Number} endTime The end of the highlight region, in seconds.
289 | */
290 |
291 | WaveformOverview.prototype._updateHighlightRect = function(startTime, endTime) {
292 | this._highlightRectStartTime = startTime;
293 | this._highlightRectEndTime = endTime;
294 |
295 | var startOffset = this.timeToPixels(startTime);
296 | var endOffset = this.timeToPixels(endTime);
297 |
298 | this._highlightRect.setAttrs({
299 | x: startOffset,
300 | width: endOffset - startOffset
301 | });
302 |
303 | this._highlightLayer.draw();
304 | };
305 |
306 | WaveformOverview.prototype._updateWaveform = function() {
307 | this._waveformLayer.draw();
308 |
309 | var playheadTime = this._peaks.player.getCurrentTime();
310 |
311 | this._playheadLayer.updatePlayheadTime(playheadTime);
312 |
313 | if (this._highlightRect) {
314 | this._updateHighlightRect(
315 | this._highlightRectStartTime,
316 | this._highlightRectEndTime
317 | );
318 | }
319 |
320 | var frameStartTime = 0;
321 | var frameEndTime = this.pixelsToTime(this._width);
322 |
323 | this._pointsLayer.updatePoints(frameStartTime, frameEndTime);
324 | this._segmentsLayer.updateSegments(frameStartTime, frameEndTime);
325 | };
326 |
327 | WaveformOverview.prototype.setWaveformColor = function(color) {
328 | this._waveformShape.setWaveformColor(color);
329 | this._waveformLayer.draw();
330 | };
331 |
332 | WaveformOverview.prototype.showPlayheadTime = function(show) {
333 | this._playheadLayer.showPlayheadTime(show);
334 | };
335 |
336 | WaveformOverview.prototype.enableAutoScroll = function() {
337 | // The overview waveform doesn't support scrolling,
338 | // so nothing to do here.
339 | };
340 |
341 | WaveformOverview.prototype.destroy = function() {
342 | if (this._resizeTimeoutId) {
343 | clearTimeout(this._resizeTimeoutId);
344 | this._resizeTimeoutId = null;
345 | }
346 |
347 | if (this._stage) {
348 | this._stage.destroy();
349 | this._stage = null;
350 | }
351 | };
352 |
353 | return WaveformOverview;
354 | });
355 |
--------------------------------------------------------------------------------
/src/main/markers/waveform.segments.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file
3 | *
4 | * Defines the {@link WaveformSegments} class.
5 | *
6 | * @module peaks/markers/waveform.segments
7 | */
8 |
9 | define([
10 | 'colors.css',
11 | 'peaks/markers/segment',
12 | 'peaks/waveform/waveform.utils'
13 | ], function(Colors, Segment, Utils) {
14 | 'use strict';
15 |
16 | /**
17 | * Segment parameters.
18 | *
19 | * @typedef {Object} SegmentOptions
20 | * @global
21 | * @property {Number} startTime Segment start time, in seconds.
22 | * @property {Number} endTime Segment end time, in seconds.
23 | * @property {Boolean=} editable If true the segment start and
24 | * end times can be adjusted via the user interface.
25 | * Default: false.
26 | * @property {String=} color Segment waveform color.
27 | * Default: a random color.
28 | * @property {String=} labelText Segment label text.
29 | * Default: an empty string.
30 | * @property {String=} id A unique segment identifier.
31 | * Default: an automatically generated identifier.
32 | */
33 |
34 | /**
35 | * Handles all functionality related to the adding, removing and manipulation
36 | * of segments.
37 | *
38 | * @class
39 | * @alias WaveformSegments
40 | *
41 | * @param {Peaks} peaks The parent Peaks object.
42 | */
43 |
44 | function WaveformSegments(peaks) {
45 | this._peaks = peaks;
46 | this._segments = [];
47 | this._segmentsById = {};
48 | this._segmentIdCounter = 0;
49 | this._colorIndex = 0;
50 | }
51 |
52 | /**
53 | * Returns a new unique segment id value.
54 | *
55 | * @private
56 | * @returns {String}
57 | */
58 |
59 | WaveformSegments.prototype._getNextSegmentId = function() {
60 | return 'peaks.segment.' + this._segmentIdCounter++;
61 | };
62 |
63 | var colors = [
64 | Colors.navy,
65 | Colors.blue,
66 | Colors.aqua,
67 | Colors.teal,
68 | // Colors.olive,
69 | // Colors.lime,
70 | // Colors.green,
71 | Colors.yellow,
72 | Colors.orange,
73 | Colors.red,
74 | Colors.maroon,
75 | Colors.fuchsia,
76 | Colors.purple
77 | // Colors.black,
78 | // Colors.gray,
79 | // Colors.silver
80 | ];
81 |
82 | /**
83 | * @private
84 | * @returns {String}
85 | */
86 |
87 | WaveformSegments.prototype._getSegmentColor = function() {
88 | if (this._peaks.options.randomizeSegmentColor) {
89 | if (++this._colorIndex === colors.length) {
90 | this._colorIndex = 0;
91 | }
92 |
93 | return colors[this._colorIndex];
94 | }
95 | else {
96 | return this._peaks.options.segmentColor;
97 | }
98 | };
99 |
100 | /**
101 | * Adds a new segment object.
102 | *
103 | * @private
104 | * @param {Segment} segment
105 | */
106 |
107 | WaveformSegments.prototype._addSegment = function(segment) {
108 | this._segments.push(segment);
109 |
110 | this._segmentsById[segment.id] = segment;
111 | };
112 |
113 | /**
114 | * Creates a new segment object.
115 | *
116 | * @private
117 | * @param {SegmentOptions} options
118 | * @return {Segment}
119 | */
120 |
121 | WaveformSegments.prototype._createSegment = function(options) {
122 | // Watch for anyone still trying to use the old
123 | // createSegment(startTime, endTime, ...) API
124 | if (!Utils.isObject(options)) {
125 | // eslint-disable-next-line max-len
126 | throw new TypeError('peaks.segments.add(): expected a Segment object parameter');
127 | }
128 |
129 | var segment = new Segment(
130 | this,
131 | Utils.isNullOrUndefined(options.id) ? this._getNextSegmentId() : options.id,
132 | options.startTime,
133 | options.endTime,
134 | options.labelText || '',
135 | options.color || this._getSegmentColor(),
136 | options.editable || false
137 | );
138 |
139 | return segment;
140 | };
141 |
142 | /**
143 | * Returns all segments.
144 | *
145 | * @returns {Arraynull if not found.
154 | *
155 | * @param {String} id
156 | * @returns {Segment|null}
157 | */
158 |
159 | WaveformSegments.prototype.getSegment = function(id) {
160 | return this._segmentsById[id] || null;
161 | };
162 |
163 | /**
164 | * Returns all segments that overlap a given point in time.
165 | *
166 | * @param {Number} time
167 | * @returns {Arraysegments.remove event with the removed {@link Segment}
284 | * objects.
285 | *
286 | * @private
287 | * @param {Function} predicate A predicate function that identifies which
288 | * segments to remove.
289 | * @returns {Arraysegments.remove_all event.
361 | */
362 |
363 | WaveformSegments.prototype.removeAll = function() {
364 | this._segments = [];
365 | this._segmentsById = {};
366 | this._peaks.emit('segments.remove_all');
367 | };
368 |
369 | return WaveformSegments;
370 | });
371 |
--------------------------------------------------------------------------------