Skip to content

Commit 16821bc

Browse files
authored
Merge pull request #14 from devlucky/chore/test-audio-bars
Chore/test audio bars
2 parents a8d324e + b49a1f9 commit 16821bc

File tree

8 files changed

+318
-33
lines changed

8 files changed

+318
-33
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[![Build Status](https://travis-ci.org/majames/react-audio-vis.svg?branch=master)](https://travis-ci.org/majames/react-audio-vis)
2-
[![Coverage Status](https://coveralls.io/repos/github/majames/react-audio-vis/badge.svg?branch=chore%2Fcoverage-stats)](https://coveralls.io/github/majames/react-audio-vis?branch=chore%2Fcoverage-stats)
1+
[![Build Status](https://travis-ci.org/devlucky/react-audio-vis.svg?branch=master)](https://travis-ci.org/devlucky/react-audio-vis)
2+
[![Coverage Status](https://coveralls.io/repos/github/devlucky/react-audio-vis/badge.svg)](https://coveralls.io/github/devlucky/react-audio-vis)
33

44
*This library is not ready for consumption*

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
"@kadira/storybook": "^2.21.0",
2020
"@kadira/storybook-deployer": "^1.2.0",
2121
"coveralls": "^2.13.1",
22+
"enzyme": "^2.8.2",
2223
"react-scripts-ts": "^1.4.0",
24+
"react-test-renderer": "^15.5.4",
2325
"styled-components": "^1.4.6",
2426
"typescript": "^2.3.2"
2527
},

src/analyser/index.test.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ interface MockNode {
55
disconnect: Function;
66
}
77

8+
interface MockAnalyserNode extends MockNode {
9+
frequencyBinCount: number;
10+
getByteFrequencyData: (array: Int8Array) => void;
11+
}
12+
813
describe('Analyser', () => {
914
let sourceNode: MockNode;
10-
let analyserNode: MockNode;
15+
let analyserNode: MockAnalyserNode;
1116
let destinationNode: MockNode;
1217
let audioContext: AudioContext;
1318
let audioEl: HTMLAudioElement;
@@ -21,7 +26,9 @@ describe('Analyser', () => {
2126

2227
analyserNode = {
2328
connect: jest.fn(),
24-
disconnect: jest.fn()
29+
disconnect: jest.fn(),
30+
frequencyBinCount: 10,
31+
getByteFrequencyData: () => {}
2532
};
2633

2734
destinationNode = {
@@ -77,4 +84,38 @@ describe('Analyser', () => {
7784
expect(analyserNode.disconnect).toHaveBeenCalledTimes(1);
7885
});
7986
});
87+
88+
describe('getBucketedByteFrequencyData', () => {
89+
const mockAnalyserNodeWithFrequencies = (analyserNodeFrequencies: Array<number>) => {
90+
analyserNode.frequencyBinCount = analyserNodeFrequencies.length;
91+
92+
analyserNode.getByteFrequencyData = jest.fn().mockImplementation(
93+
(dataArray) => {
94+
for (let i = 0; i < analyserNodeFrequencies.length; i++) {
95+
dataArray[i] = analyserNodeFrequencies[i];
96+
}
97+
}
98+
);
99+
};
100+
101+
it('returns the unaltered array when there are more buckets than array entries', () => {
102+
const numberOfBuckets = 10;
103+
const analyserNodeFrequencies = [1, 2, 3, 4];
104+
mockAnalyserNodeWithFrequencies(analyserNodeFrequencies);
105+
106+
const bucketedArray = analyser.getBucketedByteFrequencyData(numberOfBuckets);
107+
expect(bucketedArray).toEqual(analyserNodeFrequencies);
108+
});
109+
110+
it('correctly buckets array when there are less buckets than array entries', () => {
111+
const numberOfBuckets = 4;
112+
const analyserNodeFrequencies = [1, 2, 3, 4, 5, 6, 7, 8];
113+
const expectedBucketArray = [1.5, 3.5, 5.5, 7.5];
114+
115+
mockAnalyserNodeWithFrequencies(analyserNodeFrequencies);
116+
117+
const bucketedArray = analyser.getBucketedByteFrequencyData(numberOfBuckets);
118+
expect(bucketedArray).toEqual(expectedBucketArray);
119+
});
120+
});
80121
});

src/analyser/index.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import chunk = require('lodash.chunk');
2+
import sum = require('lodash.sum');
3+
14
export interface AnalyserSpec {
25
audioEl: HTMLAudioElement;
36
audioContext: AudioContext;
@@ -13,6 +16,7 @@ export class Analyser {
1316
audioEl: HTMLAudioElement;
1417
analyserNode: AnalyserNode;
1518
private source: MediaElementAudioSourceNode;
19+
private dataArray: Uint8Array;
1620

1721
constructor(spec: AnalyserSpec) {
1822
this.audioEl = spec.audioEl;
@@ -25,6 +29,26 @@ export class Analyser {
2529
if (source) { source.disconnect(); }
2630
}
2731

32+
getBucketedByteFrequencyData = (maxNumBuckets: number): Array<number> => {
33+
const {analyserNode} = this;
34+
35+
const bufferLength = analyserNode.frequencyBinCount;
36+
if (!this.dataArray) {
37+
this.dataArray = new Uint8Array(bufferLength);
38+
}
39+
40+
const {dataArray} = this;
41+
analyserNode.getByteFrequencyData(dataArray);
42+
43+
const numBuckets = Math.min(dataArray.length, maxNumBuckets);
44+
45+
// bucket values
46+
const numValuesPerChunk = Math.ceil(bufferLength / numBuckets);
47+
const chunkedData = chunk(dataArray, numValuesPerChunk);
48+
49+
return chunkedData.map((arr: Array<number>) => sum(arr) / arr.length);
50+
}
51+
2852
private createAnalyserNode = ({audioEl, audioContext}: AnalyserSpec): void => {
2953
this.source = audioContext.createMediaElementSource(audioEl);
3054

src/bars/index.test.tsx

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import * as React from 'react';
2+
import {shallow} from 'enzyme';
3+
4+
import {AudioBars} from './';
5+
6+
describe('AudioBars', () => {
7+
describe('componentDidMount', () => {
8+
it('registers event listeners', () => {
9+
const audioEl = {addEventListener: jest.fn()};
10+
const analyser = {audioEl} as any;
11+
12+
const wrapper = shallow(<AudioBars analyser={analyser} />);
13+
wrapper.instance().componentDidMount();
14+
15+
const {addEventListener} = audioEl;
16+
expect(addEventListener).toHaveBeenCalledTimes(3);
17+
expect(addEventListener.mock.calls[0][0]).toEqual('playing');
18+
expect(addEventListener.mock.calls[1][0]).toEqual('pause');
19+
expect(addEventListener.mock.calls[2][0]).toEqual('ended');
20+
});
21+
});
22+
23+
describe('render', () => {
24+
it('default width passed to BarsCanvas is 400px', () => {
25+
const analyser = {} as any;
26+
const wrapper = shallow(<AudioBars analyser={analyser} />);
27+
28+
expect(wrapper.props().width).toEqual(400);
29+
});
30+
31+
it('default height passed to BarsCanvas is 400px', () => {
32+
const analyser = {} as any;
33+
const wrapper = shallow(<AudioBars analyser={analyser} />);
34+
35+
expect(wrapper.props().height).toEqual(400);
36+
});
37+
38+
it('passes dimensions to BarsCanvas', () => {
39+
const analyser = {} as any;
40+
const dims = {width: 200, height: 200};
41+
const wrapper = shallow(<AudioBars analyser={analyser} dimensions={dims} />);
42+
43+
expect(wrapper.props().height).toEqual(dims.height);
44+
expect(wrapper.props().width).toEqual(dims.width);
45+
});
46+
});
47+
48+
describe('drawBars', () => {
49+
let freqData: Array<number>;
50+
let analyser;
51+
let canvasContext;
52+
53+
beforeEach(() => {
54+
window.requestAnimationFrame = jest.fn();
55+
56+
freqData = [1, 2, 3, 4];
57+
analyser = {
58+
getBucketedByteFrequencyData: jest.fn().mockReturnValue(freqData)
59+
} as any;
60+
61+
canvasContext = {clearRect: jest.fn(), fillRect: jest.fn()};
62+
});
63+
64+
it('clears the canvas', () => {
65+
const canvasDimensions = {width: 100, height: 200};
66+
const wrapper = shallow(<AudioBars analyser={analyser} dimensions={canvasDimensions} />);
67+
68+
wrapper.instance().canvasContext = canvasContext;
69+
wrapper.instance().drawBars();
70+
71+
expect(canvasContext.clearRect).toHaveBeenCalledTimes(1);
72+
expect(canvasContext.clearRect).toHaveBeenLastCalledWith(
73+
0 , 0, canvasDimensions.width, canvasDimensions.height
74+
);
75+
});
76+
77+
it('draws the correct number of bars to the canvas', () => {
78+
const wrapper = shallow(<AudioBars analyser={analyser} />);
79+
80+
wrapper.instance().canvasContext = canvasContext;
81+
wrapper.instance().drawBars();
82+
83+
expect(canvasContext.fillRect).toHaveBeenCalledTimes(freqData.length);
84+
});
85+
86+
it('draws the first bar with the correct dimensions to the canvas', () => {
87+
const canvasDimensions = {width: 100, height: 200};
88+
const wrapper = shallow(<AudioBars analyser={analyser} dimensions={canvasDimensions} />);
89+
90+
wrapper.instance().canvasContext = canvasContext;
91+
wrapper.instance().drawBars();
92+
93+
const oneByte = 256;
94+
const firstBarHeight = (1 / oneByte) * canvasDimensions.height;
95+
const firstBarWidth = canvasDimensions.width / freqData.length;
96+
expect(canvasContext.fillRect).toHaveBeenCalledWith(
97+
0,
98+
canvasDimensions.height - firstBarHeight,
99+
firstBarWidth,
100+
firstBarHeight
101+
);
102+
});
103+
});
104+
});

src/bars/index.tsx

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import * as React from 'react';
22
import {Component} from 'react';
3-
import chunk = require('lodash.chunk');
4-
import sum = require('lodash.sum');
53

64
import {Analyser} from '../analyser';
75
import {Dimensions} from '../utils/dimensions';
@@ -16,7 +14,6 @@ export class AudioBars extends Component<AudioBarsProps, {}> {
1614
private canvasEl: HTMLCanvasElement;
1715
private canvasContext: CanvasRenderingContext2D;
1816
private animationId: number;
19-
private dataArray: Uint8Array;
2017

2118
componentDidMount() {
2219
const {audioEl} = this.props.analyser;
@@ -60,35 +57,22 @@ export class AudioBars extends Component<AudioBarsProps, {}> {
6057
}
6158

6259
this.canvasContext = context;
63-
64-
const {analyserNode} = this.props.analyser;
65-
const bufferLength = analyserNode.frequencyBinCount;
66-
this.dataArray = new Uint8Array(bufferLength);
67-
6860
this.drawBars();
6961
}
7062

7163
private drawBars = (): void => {
72-
const {canvasContext, width: canvasWidth, height: canvasHeight, dataArray} = this;
64+
const {canvasContext, width: canvasWidth, height: canvasHeight} = this;
65+
const {analyser} = this.props;
7366

7467
// clear the canvas
7568
this.canvasContext.clearRect(0, 0, canvasWidth, canvasHeight);
7669

77-
const {analyserNode} = this.props.analyser;
78-
const maxByteValue = 256;
79-
analyserNode.getByteFrequencyData(dataArray);
80-
81-
const numBarsToDraw = Math.min(dataArray.length, 64);
82-
const bufferLength = dataArray.length;
83-
84-
// chunk values if too many to display
85-
const numValuesPerChunk = Math.ceil(bufferLength / numBarsToDraw);
86-
const chunkedData = chunk(dataArray, numValuesPerChunk);
87-
const barValues = chunkedData.map((arr: Array<number>) => sum(arr) / arr.length);
88-
89-
const barWidth = canvasWidth / numBarsToDraw;
70+
const maxNumBarsToDraw = 64;
71+
const barValues = analyser.getBucketedByteFrequencyData(maxNumBarsToDraw);
72+
const barWidth = canvasWidth / barValues.length;
9073

9174
// draw the bars
75+
const maxByteValue = 256;
9276
for (let i = 0; i < barValues.length; i++) {
9377
const x = i * barWidth + i;
9478

src/bars/styled.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ export const BarsCanvas = styled.canvas`
44
border: 2px solid blue;
55
border-radius: 3px;
66
`;
7+
8+
BarsCanvas.displayName = 'BarsCanvas';

0 commit comments

Comments
 (0)