-
Notifications
You must be signed in to change notification settings - Fork 0
Chore/test audio bars #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| [](https://travis-ci.org/majames/react-audio-vis) | ||
| [](https://coveralls.io/github/majames/react-audio-vis?branch=chore%2Fcoverage-stats) | ||
| [](https://travis-ci.org/devlucky/react-audio-vis) | ||
| [](https://coveralls.io/github/devlucky/react-audio-vis) | ||
|
|
||
| *This library is not ready for consumption* | ||
| 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; | ||
|
|
@@ -13,6 +16,7 @@ export class Analyser { | |
| audioEl: HTMLAudioElement; | ||
| analyserNode: AnalyserNode; | ||
| private source: MediaElementAudioSourceNode; | ||
| private dataArray: Uint8Array; | ||
|
|
||
| constructor(spec: AnalyserSpec) { | ||
| this.audioEl = spec.audioEl; | ||
|
|
@@ -25,6 +29,26 @@ export class Analyser { | |
| if (source) { source.disconnect(); } | ||
| } | ||
|
|
||
| getBucketedByteFrequencyData = (maxNumBuckets: number): Array<number> => { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move logic for bucketing frequency data into |
||
| 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); | ||
|
|
||
|
|
||
| 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'); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| ); | ||
| }); | ||
| }); | ||
| }); | ||
| 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'; | ||
|
|
@@ -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; | ||
|
|
@@ -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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
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