Skip to content

Commit f7060b2

Browse files
authored
Merge pull request #2 from samanthaberk/the-krillers
Civic Tech Hackathon- Spectogram feature
2 parents 0364b94 + 51bccee commit f7060b2

File tree

8 files changed

+213
-3
lines changed

8 files changed

+213
-3
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Finally, in another terminal, run the server with
5858

5959
`iex -S mix phx.server`
6060

61+
To open the client page locally, go to localhost:4000 in your browser
6162

6263
## Deployment
6364

@@ -66,4 +67,3 @@ For the moment, this app is running in a heroku instance with `mix phx.server`.
6667
`heroku run POOL_SIZE=2 iex -S mix`
6768

6869
The `POOL_SIZE` config var is necessary due to the current Postgres db having 20 connections. You can read more [about it here](https://hexdocs.pm/phoenix/heroku.html#creating-environment-variables-in-heroku).
69-

assets/package-lock.json

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"react-apollo": "^2.1.9",
2828
"react-dom": "^16.4.1",
2929
"react-router-dom": "^4.3.1",
30+
"spectrogram": "0.0.6",
3031
"video.js": "^7.2.0",
3132
"yup": "^0.26.0"
3233
},

assets/src/components/MediaStreamer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export default class MediaStreamer extends Component {
105105
const {src} = this.props
106106

107107
return (
108-
<audio ref={node => {this.audioNode = node}} className="video-js" />
108+
<audio id='audio_element' ref={node => {this.audioNode = node}} className="video-js" />
109109
)
110110
}
111111
}

assets/src/components/Player.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
55
import {faPlay, faPause, faSpinner} from '@fortawesome/free-solid-svg-icons'
66

77
import MediaStreamer from './MediaStreamer'
8+
import Spectrogram from './Spectrogram'
89

910
import {feedType} from 'types/feedType'
1011
import {storeCurrentFeed, getCurrentFeed} from 'utils/feedStorage'
@@ -60,6 +61,12 @@ export default class Player extends Component {
6061
this.startTimestampFetcher()
6162
}
6263

64+
test(){
65+
66+
spec = this.spectrogram('viewport')
67+
navigator.getUserMedia({audio: true}, spec.gotStream, function() { console.log("error"); })
68+
}
69+
6370
componentWillUnmount() {
6471
const {intervalId} = this.state
6572
clearInterval(intervalId)
@@ -93,6 +100,7 @@ export default class Player extends Component {
93100
<a href={awsConsoleUri} className="mx-2" target="_blank">
94101
AWS
95102
</a>
103+
96104
</div>
97105
)
98106

@@ -163,6 +171,7 @@ export default class Player extends Component {
163171
{currentFeed.name}
164172
</Link>
165173
)}
174+
166175
{hlsURI && (
167176
<MediaStreamer
168177
src={hlsURI}
@@ -186,7 +195,10 @@ export default class Player extends Component {
186195
}
187196
/>
188197
)}
198+
{document.getElementsByTagName('audio')[0] && (<Spectrogram componentId='viewport' audioElement={document.getElementsByTagName('audio')[0]}
199+
src={hlsURI}/>)}
189200
{this.debugInfo(hlsURI, awsConsoleUri)}
201+
190202
</div>
191203
)
192204
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
window.AudioContext = window.AudioContext || window.webkitAudioContext;
2+
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;
3+
4+
import React, {Component} from 'react'
5+
6+
export default class Spectrogram extends Component {
7+
constructor(props) {
8+
super(props)
9+
}
10+
componentDidMount() {
11+
12+
var self = this;
13+
self.canvas = document.getElementById(this.props.componentId);
14+
self.width = self.canvas.width;
15+
self.height = self.canvas.height;
16+
self.ctx = self.canvas.getContext('2d');
17+
self.imageData = self.ctx.getImageData(0, 0, self.width, self.height);
18+
19+
self.data = self.imageData.data;
20+
21+
self.buf = new ArrayBuffer(self.imageData.data.length);
22+
self.buf8 = new Uint8ClampedArray(self.buf);
23+
self.data32 = new Uint32Array(self.buf);
24+
25+
self.x = 0;
26+
self.bufferSize = 1024;
27+
self.dataBuffer = new Float32Array(self.height);
28+
self.uint8array = new Uint8Array(4);
29+
self.colorData = new Uint8Array(4 * self.bufferSize / 2);
30+
this.interval = setInterval(() => this.drawFrame(), 20)
31+
32+
//this.gotStream()
33+
//this.audio = new Audio()
34+
//this.audio.src = this.props.src
35+
//console.log("audio")
36+
//console.log(this.props)
37+
//console.log(this.audio)
38+
//document.body.appendChild(this.audio)
39+
navigator.getUserMedia({audio: true}, this.gotStream, function() { console.log("error"); });
40+
//this.gotStream()
41+
}
42+
43+
componentWillUnmount() {
44+
clearInterval(this.interval)
45+
}
46+
47+
gotStream = (stream) => {
48+
var self = this;
49+
self.context = new window.AudioContext();
50+
self.sampleRate = self.context.sampleRate;
51+
52+
var input = self.context.createMediaStreamSource(stream);
53+
54+
//var input = self.context.createMediaElementSource(this.audio)
55+
//var input = self.context.createMediaElementSource(this.props.audio.captureStream())
56+
self.analyser = self.context.createAnalyser();
57+
self.analyser.fftSize = self.bufferSize;
58+
input.connect(self.analyser);
59+
}
60+
61+
setPixel = (x, y, red, green, blue, alpha) => {
62+
var self = this;
63+
self.data32[y * self.width + x] =
64+
(alpha << 24) | // alpha
65+
(blue << 16) | // blue
66+
(green << 8) | // green
67+
red; // red
68+
}
69+
70+
getPixel = (x, y) => {
71+
var self = this;
72+
var value = self.data32[y * self.width + x];
73+
var channels = self.uint32Touint8(value)
74+
return channels;
75+
}
76+
77+
uint32Touint8 = (uint32) => {
78+
var self = this;
79+
self.uint8array[3] = uint32 >> 24 & 0xff;
80+
self.uint8array[2] = uint32 >> 16 & 0xff;
81+
self.uint8array[1] = uint32 >> 8 & 0xff;
82+
self.uint8array[0] = uint32 & 0xff;
83+
return data
84+
}
85+
86+
drawColumn = () => {
87+
var self = this;
88+
var value = 0;
89+
for (var y = 0; y < self.height; y++) {
90+
var x = col;
91+
self.setPixel(x, y, value, value, value, 255)
92+
}
93+
}
94+
95+
getData = () => {
96+
var self = this;
97+
var freqByteData = new Uint8Array(self.analyser.frequencyBinCount);
98+
self.analyser.getByteFrequencyData(freqByteData);
99+
100+
// Reverse the direction, making lower frequencies on the bottom.
101+
for (var i = self.analyser.frequencyBinCount - 1; i >= 0; i--) {
102+
self.dataBuffer[i] = freqByteData[(self.analyser.frequencyBinCount - 1) - i] / 255.0;
103+
}
104+
105+
return self.dataBuffer
106+
}
107+
108+
color = (value) => {
109+
var self = this;
110+
var rgb = {R: 0, G: 0, B: 0};
111+
112+
if (0 <= value && value <= 1 / 8) {
113+
rgb.R = 0;
114+
rgb.G = 0;
115+
rgb.B = 4 * value + .5; // .5 - 1 // b = 1/2
116+
} else if (1 / 8 < value && value <= 3 / 8) {
117+
rgb.R = 0;
118+
rgb.G = 4 * value - .5; // 0 - 1 // b = - 1/2
119+
rgb.B = 0;
120+
} else if (3 / 8 < value && value <= 5 / 8) {
121+
rgb.R = 4*value - 1.5; // 0 - 1 // b = - 3/2
122+
rgb.G = 1;
123+
rgb.B = -4 * value + 2.5; // 1 - 0 // b = 5/2
124+
} else if (5 / 8 < value && value <= 7 / 8) {
125+
rgb.R = 1;
126+
rgb.G = -4 * value + 3.5; // 1 - 0 // b = 7/2
127+
rgb.B = 0;
128+
} else if (7 / 8 < value && value <= 1) {
129+
rgb.R = -4*value + 4.5; // 1 - .5 // b = 9/2
130+
rgb.G = 0;
131+
rgb.B = 0;
132+
} else { // should never happen - value > 1
133+
rgb.R = .5;
134+
rgb.G = 0;
135+
rgb.B = 0;
136+
}
137+
138+
return [rgb.R, rgb.G, rgb.B, 1].map(function(d) { return parseInt(d * 255, 10)})
139+
}
140+
141+
colorizeData = (data) => {
142+
var self = this;
143+
var d;
144+
for(var i = 0, n = data.length; i < n; i++) {
145+
d = self.color(data[i]);
146+
self.colorData.set(d, i * 4);
147+
}
148+
return self.colorData;
149+
}
150+
151+
addColumn = (colorizeData) => {
152+
var self = this;
153+
for (var y = 0; y < self.height; y++) {
154+
self.setPixel(self.x, y, colorizeData[4 * y + 0], colorizeData[4 * y + 1], colorizeData[4 * y + 2], colorizeData[4 * y + 3]);
155+
}
156+
self.x++;
157+
self.x %= self.width;
158+
}
159+
160+
drawFrame = (data) => {
161+
var self = this;
162+
var data = data || self.getData();
163+
var colorData = self.colorizeData(data)
164+
165+
self.addColumn(colorData);
166+
self.imageData.data.set(self.buf8);
167+
self.ctx.putImageData(self.imageData, 0, 0);
168+
}
169+
170+
render() {
171+
const {src} = this.props
172+
return (
173+
<a>
174+
<canvas id='viewport' height="512" width="700"/>
175+
</a>)
176+
}
177+
}

assets/src/styles/app.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@
77
justify-content: center;
88
align-items: center;
99
}
10+
11+
canvas {
12+
border:2px solid white;
13+
}

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)