Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[![Build Status](https://travis-ci.org/majames/react-audio-vis.svg?branch=master)](https://travis-ci.org/majames/react-audio-vis)
[![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)
[![Build Status](https://travis-ci.org/devlucky/react-audio-vis.svg?branch=master)](https://travis-ci.org/devlucky/react-audio-vis)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update badges to point at devlucky

[![Coverage Status](https://coveralls.io/repos/github/devlucky/react-audio-vis/badge.svg)](https://coveralls.io/github/devlucky/react-audio-vis)

*This library is not ready for consumption*
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"devDependencies": {
"@kadira/storybook": "^2.21.0",
"coveralls": "^2.13.1",
"enzyme": "^2.8.2",
"react-scripts-ts": "^1.4.0",
"react-test-renderer": "^15.5.4",
"styled-components": "^1.4.6",
"typescript": "^2.3.2"
},
Expand Down
45 changes: 43 additions & 2 deletions src/analyser/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ interface MockNode {
disconnect: Function;
}

interface MockAnalyserNode extends MockNode {
frequencyBinCount: number;
getByteFrequencyData: (array: Int8Array) => void;
}

describe('Analyser', () => {
let sourceNode: MockNode;
let analyserNode: MockNode;
let analyserNode: MockAnalyserNode;
let destinationNode: MockNode;
let audioContext: AudioContext;
let audioEl: HTMLAudioElement;
Expand All @@ -21,7 +26,9 @@ describe('Analyser', () => {

analyserNode = {
connect: jest.fn(),
disconnect: jest.fn()
disconnect: jest.fn(),
frequencyBinCount: 10,
getByteFrequencyData: () => {}
};

destinationNode = {
Expand Down Expand Up @@ -77,4 +84,38 @@ describe('Analyser', () => {
expect(analyserNode.disconnect).toHaveBeenCalledTimes(1);
});
});

describe('getBucketedByteFrequencyData', () => {
const mockAnalyserNodeWithFrequencies = (analyserNodeFrequencies: Array<number>) => {
analyserNode.frequencyBinCount = analyserNodeFrequencies.length;

analyserNode.getByteFrequencyData = jest.fn().mockImplementation(
(dataArray) => {
for (let i = 0; i < analyserNodeFrequencies.length; i++) {
dataArray[i] = analyserNodeFrequencies[i];
}
}
);
};

it('returns the unaltered array when there are more buckets than array entries', () => {
const numberOfBuckets = 10;
const analyserNodeFrequencies = [1, 2, 3, 4];
mockAnalyserNodeWithFrequencies(analyserNodeFrequencies);

const bucketedArray = analyser.getBucketedByteFrequencyData(numberOfBuckets);
expect(bucketedArray).toEqual(analyserNodeFrequencies);
});

it('correctly buckets array when there are less buckets than array entries', () => {
const numberOfBuckets = 4;
const analyserNodeFrequencies = [1, 2, 3, 4, 5, 6, 7, 8];
const expectedBucketArray = [1.5, 3.5, 5.5, 7.5];

mockAnalyserNodeWithFrequencies(analyserNodeFrequencies);

const bucketedArray = analyser.getBucketedByteFrequencyData(numberOfBuckets);
expect(bucketedArray).toEqual(expectedBucketArray);
});
});
});
24 changes: 24 additions & 0 deletions src/analyser/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import chunk = require('lodash.chunk');
import sum = require('lodash.sum');

export interface AnalyserSpec {
audioEl: HTMLAudioElement;
audioContext: AudioContext;
Expand All @@ -13,6 +16,7 @@ export class Analyser {
audioEl: HTMLAudioElement;
analyserNode: AnalyserNode;
private source: MediaElementAudioSourceNode;
private dataArray: Uint8Array;

constructor(spec: AnalyserSpec) {
this.audioEl = spec.audioEl;
Expand All @@ -25,6 +29,26 @@ export class Analyser {
if (source) { source.disconnect(); }
}

getBucketedByteFrequencyData = (maxNumBuckets: number): Array<number> => {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move logic for bucketing frequency data into Analyser (other visualisations can use this)

const {analyserNode} = this;

const bufferLength = analyserNode.frequencyBinCount;
if (!this.dataArray) {
this.dataArray = new Uint8Array(bufferLength);
}

const {dataArray} = this;
analyserNode.getByteFrequencyData(dataArray);

const numBuckets = Math.min(dataArray.length, maxNumBuckets);

// bucket values
const numValuesPerChunk = Math.ceil(bufferLength / numBuckets);
const chunkedData = chunk(dataArray, numValuesPerChunk);

return chunkedData.map((arr: Array<number>) => sum(arr) / arr.length);
}

private createAnalyserNode = ({audioEl, audioContext}: AnalyserSpec): void => {
this.source = audioContext.createMediaElementSource(audioEl);

Expand Down
104 changes: 104 additions & 0 deletions src/bars/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import * as React from 'react';
import {shallow} from 'enzyme';

import {AudioBars} from './';

describe('AudioBars', () => {
describe('componentDidMount', () => {
it('registers event listeners', () => {
const audioEl = {addEventListener: jest.fn()};
const analyser = {audioEl} as any;

const wrapper = shallow(<AudioBars analyser={analyser} />);
wrapper.instance().componentDidMount();

const {addEventListener} = audioEl;
expect(addEventListener).toHaveBeenCalledTimes(3);
expect(addEventListener.mock.calls[0][0]).toEqual('playing');
expect(addEventListener.mock.calls[1][0]).toEqual('pause');
expect(addEventListener.mock.calls[2][0]).toEqual('ended');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those cases are gold!

});
});

describe('render', () => {
it('default width passed to BarsCanvas is 400px', () => {
const analyser = {} as any;
const wrapper = shallow(<AudioBars analyser={analyser} />);

expect(wrapper.props().width).toEqual(400);
});

it('default height passed to BarsCanvas is 400px', () => {
const analyser = {} as any;
const wrapper = shallow(<AudioBars analyser={analyser} />);

expect(wrapper.props().height).toEqual(400);
});

it('passes dimensions to BarsCanvas', () => {
const analyser = {} as any;
const dims = {width: 200, height: 200};
const wrapper = shallow(<AudioBars analyser={analyser} dimensions={dims} />);

expect(wrapper.props().height).toEqual(dims.height);
expect(wrapper.props().width).toEqual(dims.width);
});
});

describe('drawBars', () => {
let freqData: Array<number>;
let analyser;
let canvasContext;

beforeEach(() => {
window.requestAnimationFrame = jest.fn();

freqData = [1, 2, 3, 4];
analyser = {
getBucketedByteFrequencyData: jest.fn().mockReturnValue(freqData)
} as any;

canvasContext = {clearRect: jest.fn(), fillRect: jest.fn()};
});

it('clears the canvas', () => {
const canvasDimensions = {width: 100, height: 200};
const wrapper = shallow(<AudioBars analyser={analyser} dimensions={canvasDimensions} />);

wrapper.instance().canvasContext = canvasContext;
wrapper.instance().drawBars();

expect(canvasContext.clearRect).toHaveBeenCalledTimes(1);
expect(canvasContext.clearRect).toHaveBeenLastCalledWith(
0 , 0, canvasDimensions.width, canvasDimensions.height
);
});

it('draws the correct number of bars to the canvas', () => {
const wrapper = shallow(<AudioBars analyser={analyser} />);

wrapper.instance().canvasContext = canvasContext;
wrapper.instance().drawBars();

expect(canvasContext.fillRect).toHaveBeenCalledTimes(freqData.length);
});

it('draws the first bar with the correct dimensions to the canvas', () => {
const canvasDimensions = {width: 100, height: 200};
const wrapper = shallow(<AudioBars analyser={analyser} dimensions={canvasDimensions} />);

wrapper.instance().canvasContext = canvasContext;
wrapper.instance().drawBars();

const oneByte = 256;
const firstBarHeight = (1 / oneByte) * canvasDimensions.height;
const firstBarWidth = canvasDimensions.width / freqData.length;
expect(canvasContext.fillRect).toHaveBeenCalledWith(
0,
canvasDimensions.height - firstBarHeight,
firstBarWidth,
firstBarHeight
);
});
});
});
28 changes: 6 additions & 22 deletions src/bars/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import * as React from 'react';
import {Component} from 'react';
import chunk = require('lodash.chunk');
import sum = require('lodash.sum');

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

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

this.canvasContext = context;

const {analyserNode} = this.props.analyser;
const bufferLength = analyserNode.frequencyBinCount;
this.dataArray = new Uint8Array(bufferLength);

this.drawBars();
}

private drawBars = (): void => {
const {canvasContext, width: canvasWidth, height: canvasHeight, dataArray} = this;
const {canvasContext, width: canvasWidth, height: canvasHeight} = this;
const {analyser} = this.props;

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

const {analyserNode} = this.props.analyser;
const maxByteValue = 256;
analyserNode.getByteFrequencyData(dataArray);

const numBarsToDraw = Math.min(dataArray.length, 64);
const bufferLength = dataArray.length;

// chunk values if too many to display
const numValuesPerChunk = Math.ceil(bufferLength / numBarsToDraw);
const chunkedData = chunk(dataArray, numValuesPerChunk);
const barValues = chunkedData.map((arr: Array<number>) => sum(arr) / arr.length);

const barWidth = canvasWidth / numBarsToDraw;
const maxNumBarsToDraw = 64;
const barValues = analyser.getBucketedByteFrequencyData(maxNumBarsToDraw);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one

const barWidth = canvasWidth / barValues.length;

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

Expand Down
2 changes: 2 additions & 0 deletions src/bars/styled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export const BarsCanvas = styled.canvas`
border: 2px solid blue;
border-radius: 3px;
`;

BarsCanvas.displayName = 'BarsCanvas';
Loading