Spotify Graph song audio analysis

Co-Authored-By: GabeDahl <45904434+GabeDahl@users.noreply.github.com>
This commit is contained in:
Mattallmighty 2022-01-06 21:25:51 +11:00
parent f377cda44c
commit 34980f3d79
10 changed files with 753 additions and 44 deletions

View File

@ -23,6 +23,7 @@
"history": "^4.10.1",
"material-ui-theme-state": "^1.0.30",
"moment": "^2.29.1",
"moment-duration-format": "^2.3.2",
"pkce-challenge": "^2.1.0",
"qs": "^6.10.1",
"react": "^17.0.2",
@ -43,6 +44,7 @@
"react-schema-form": "^0.9.10",
"react-spotify-web-playback": "^0.8.2",
"react-toastify": "^8.0.3",
"recharts": "^2.1.8",
"redux": "^4.0.5",
"redux-actions": "^2.6.5",
"redux-thunk": "^2.3.0",

View File

@ -0,0 +1,134 @@
import React, { PureComponent } from 'react';
import moment from 'moment';
import { ComposedChart, ResponsiveContainer, XAxis, YAxis, Tooltip, Legend, Area } from 'recharts';
var momentDurationFormatSetup = require('moment-duration-format');
momentDurationFormatSetup(moment);
export default class Chart extends PureComponent {
render() {
const pitchClasses = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const pitchColors = [
'#ff3333',
'#ff9933',
'#ffff33',
'#99ff33',
'#33ff33',
'#33ff99',
'#33ffff',
'#3399ff',
'#3333ff',
'#9933ff',
'#ff33ff',
'#ff3399',
];
const { segments, width, height, pitches } = this.props;
return (
<ResponsiveContainer width={width} height={height} debounce={5}>
<ComposedChart
barGap={1}
data={segments}
margin={{
top: 15,
right: 20,
left: 0,
bottom: 0,
}}
>
<XAxis
dataKey="start"
tickFormatter={time => {
let timeStr = time.toString().split('');
if (timeStr.includes('.')) {
const sec = timeStr
.slice(
0,
timeStr.findIndex(el => el === '.')
)
.join('');
let milsec = timeStr
.slice(timeStr.findIndex(el => el === '.') + 1)
.join('');
if (milsec.length === 2) {
milsec = milsec.concat('0');
} else {
milsec = milsec.concat('00');
}
let t = moment.duration.format(
[
moment.duration({
seconds: sec,
milliseconds: milsec,
}),
],
'm:ss.S'
)[0];
return t.substring(0, t.length - 1);
} else {
return moment.duration.format(
[
moment.duration({
seconds: time,
}),
],
'm:ss.S'
);
}
}}
/>
<YAxis
tickFormatter={value => `${value.toFixed(0)}%`}
domain={[0, 15]}
allowDataOverflow="true"
/>
<Tooltip
formatter={(value, name) =>
value > 6 ? [`${value.toFixed(0)}%`, name] : [null, null]
}
contentStyle={{ backgroundColor: '#0f0f0f', border: 'none' }}
labelStyle={{ color: '#f1f1f1' }}
itemSorter={item => -item.value}
/>
<Legend align="left" height={250} layout="vertical" content={renderLegend} />
{pitchClasses.map((p, i) => {
if (pitches[p] === true) {
return (
<Area
fillOpacity="1"
strokeOpacity="1"
name={p}
stackId="1"
stroke={pitchColors[i]}
fill={pitchColors[i]}
dataKey={s => s.pitches[i]}
/>
);
}
})}
{/* Dynamically Painted Bar Chart */}
{/* <Bar dataKey={segments}>
{data.map((entry, index) => (
<Cell fill/>
))}
</Bar> */}
</ComposedChart>
</ResponsiveContainer>
);
}
}
const renderLegend = props => {
const payload = props;
payload.payload.reverse();
return (
<ul style={{ height: 210, display: 'table', paddingLeft: 10, marginTop: 5 }}>
{payload.payload.map((entry, index) => (
<li key={`item-${index}`} style={{ color: entry.color, display: 'table-row' }}>
{entry.value}
</li>
))}
</ul>
);
};

View File

@ -0,0 +1,39 @@
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
import Slider from '@material-ui/core/Slider';
const useStyles = makeStyles({
root: {
maxWidth: 400,
},
input: {
width: 120,
},
});
export default function ChartSize(props) {
const classes = useStyles();
return (
<div className={classes.root}>
<Typography id="input-slider" gutterBottom color="primary">
Zoom
</Typography>
<Grid container spacing={2} alignItems="center">
<Grid item xs>
<Slider
value={props.value}
step={100}
max={20000}
min={300}
valueLabelDisplay="auto"
onChange={props.handleSizeSliderChange}
aria-labelledby="chart-size-slider"
/>
</Grid>
</Grid>
</div>
);
}

View File

@ -0,0 +1,114 @@
import React from 'react';
import {
makeStyles,
Typography,
Grid,
Select,
MenuItem,
InputLabel,
FormControl,
} from '@material-ui/core';
const useStyles = makeStyles(theme => ({
select: {
color: '#1ED760',
width: '90%',
'&:before': {
borderColor: '#1ED760',
},
'&:after': {
borderColor: '#1ED760',
},
},
icon: {
fill: '#1ED760',
},
}));
const chords = {
'Major Chords': {
'C Major': [0, 4, 7],
'C Sharp Major': [1, 5, 8],
'D Major': [2, 6, 9],
'E Flat Major': [3, 7, 10],
'E Major': [4, 8, 11],
'F Major': [5, 9, 0],
'F Sharp Major': [6, 10, 1],
'G Major': [7, 11, 2],
'A Flat Major': [8, 0, 3],
'A Major': [9, 1, 4],
'B Flat Major': [10, 2, 5],
'B Major': [11, 3, 6],
},
'Minor Chords': {
'C Minor': [0, 3, 7],
'C Sharp Minor': [1, 4, 8],
'D Minor': [2, 5, 9],
'E Flat Minor': [3, 6, 10],
'E Minor': [4, 7, 11],
'F Minor': [5, 8, 0],
'F Sharp Minor': [6, 9, 1],
'G Minor': [7, 10, 2],
'A Flat Minor': [8, 11, 3],
'A Minor': [9, 0, 4],
'B Flat Minor': [10, 1, 5],
'B Minor': [11, 2, 6],
},
'Diminished Chords': {
'C Diminished': [0, 3, 6],
'C Sharp Diminished': [1, 4, 7],
'D Diminished': [2, 5, 8],
'E Flat Diminished': [3, 6, 9],
'E Diminished': [4, 7, 10],
'F Diminished': [5, 8, 11],
'F Sharp Diminished': [6, 9, 0],
'G Diminished': [7, 10, 1],
'A Flat Diminished': [8, 11, 2],
'A Diminished': [9, 0, 3],
'B Flat Diminished': [10, 1, 4],
'B Diminished': [11, 2, 5],
},
'Augmented Chords': {
'C Augmented': [0, 4, 8],
'C Sharp Augmented': [1, 5, 9],
'D Augmented': [2, 6, 10],
'E Flat Augmented': [3, 7, 11],
'E Augmented': [4, 8, 0],
'F Augmented': [5, 9, 1],
'F Sharp Augmented': [6, 10, 2],
'G Augmented': [7, 11, 3],
'A Flat Augmented': [8, 0, 4],
'A Augmented': [9, 1, 5],
'B Flat Augmented': [10, 2, 6],
'B Augmented': [11, 3, 7],
},
};
export default function PitchSelect(props) {
const classes = useStyles();
return (
<Grid container spacing={1}>
{Object.keys(chords).map(group => (
<Grid item xs={6} sm={3}>
<FormControl style={{ width: '100%' }}>
<InputLabel id={group}>
<Typography color="primary">{group}</Typography>
</InputLabel>
<Select
onChange={props.handleChordClick}
labelId={group}
color="primary"
className={classes.select}
inputProps={{ classes: { icon: classes.icon } }}
>
{Object.keys(chords[group]).map(chord => {
return <MenuItem value={chords[group][chord]}>{chord}</MenuItem>;
})}
</Select>
</FormControl>
</Grid>
))}
</Grid>
);
}

View File

@ -0,0 +1,240 @@
import React, { Component } from 'react';
import Chart from './Chart';
import SectionChart from './SectionChart';
import ChartSize from './ChartSize';
import ChordButtons from './ChordButtons';
import PitchSelect from './PitchSelect';
import { connect } from 'react-redux';
import {
Card,
Grid,
Typography,
CardContent,
Avatar,
Backdrop,
CircularProgress,
LinearProgress,
} from '@material-ui/core';
import { updatePlayerState } from 'modules/spotify';
import { getAsyncIntegrations } from 'modules/integrations';
export class Layout extends Component {
state = {
width: 2000,
height: 255,
pitches: {
C: true,
'C#': true,
D: true,
'D#': true,
E: true,
F: true,
'F#': true,
G: true,
'G#': true,
A: true,
'A#': true,
B: true,
},
openFromSearch: false,
};
handleSizeSliderChange = (event, newValue) => {
this.setState({ width: newValue });
};
handleCheck = event => {
const e = event;
this.setState({
pitches: {
...this.state.pitches,
[e.target.name]: e.target.checked,
},
});
};
handleChordClick = event => {
const e = event;
const pitchClasses = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
numbers.map(item => {
this.setState(state => ({
pitches: {
...state.pitches,
[pitchClasses[item]]: false,
},
}));
});
setTimeout(() => {
e.target.value.map(item => {
this.setState((state, props) => ({
pitches: {
...this.state.pitches,
[pitchClasses[item]]: true,
},
}));
});
}, 100);
};
render() {
return (
<div className="wrapper" style={{ width: '100%' }}>
<Grid
container
direction="column"
justify="center"
alignItems="center"
spacing={2}
style={{ width: '100%' }}
>
<Grid item xs={12} sm={12}>
{/* <Header /> */}
</Grid>
<Grid container item xs={12} sm={12} justify="center">
<Card
style={{
maxWidth: '99%',
minWidth: '99%',
overflowX: 'auto',
backgroundColor: '#0f0f0f',
}}
>
<CardContent>
{Object.keys(this.props.analysis).length < 1 ? (
<Grid
container
direction="column"
justify="center"
alignItems="center"
style={{ height: this.state.height, width: '95vw' }}
>
<div style={{ height: 5, marginBottom: 40, width: '100%' }}>
<LinearProgress style={{ height: 5, width: '100%' }} />
</div>
<Typography variant="h5" align="center" color="secondary">
Audio Analysis for current song generated here
</Typography>
<div style={{ height: 5, marginTop: 40, width: '100%' }}>
<LinearProgress
variant="query"
style={{ height: 5, width: '100%' }}
/>
</div>
</Grid>
) : (
<div>
<Grid
container
spacing={3}
alignItems="center"
justify="center"
style={{ maxWidth: '95vw' }}
>
<Grid item>
<Avatar
style={{ border: '1px solid white' }}
alt="album-image"
src={this.props.albumURL}
variant="rounded"
>
A
</Avatar>
</Grid>
<Grid item>
<Typography
variant="h5"
style={{ color: '#f1f1f1' }}
>
{this.props.name}
</Typography>
</Grid>
<Grid item>
<Typography variant="h7" style={{ color: '#666' }}>
{this.props.artist}
</Typography>
</Grid>
</Grid>
<div
style={{
maxWidth: '98%',
overflowX: 'auto',
overflowY: 'hidden',
}}
>
<SectionChart
sections={this.props.sections}
segments={this.props.segments}
width={this.state.width}
height={this.state.height}
/>
<Chart
segments={this.props.segments}
width={this.state.width}
height={this.state.height}
pitches={this.state.pitches}
/>
</div>
</div>
)}
</CardContent>
</Card>
</Grid>
<Grid item container xs={12} sm={12} spacing={2}>
<Grid item id="left" xs={12} sm={4}>
<Card style={{ height: '100%', backgroundColor: '#0f0f0f' }}>
<CardContent>
<ChartSize
handleSizeSliderChange={this.handleSizeSliderChange}
value={this.state.width}
/>
</CardContent>
</Card>
</Grid>
<Grid item id="right" xs={12} sm={8}>
<Card style={{ height: '100%', backgroundColor: '#0f0f0f' }}>
<CardContent
style={{ height: '100%', paddingTop: 0, paddingBottom: 0 }}
>
<Grid
style={{ height: '100%' }}
container
alignItems="center"
justify="center"
>
<PitchSelect
pitches={this.state.pitches}
handleCheck={this.handleCheck}
/>
</Grid>
</CardContent>
</Card>
</Grid>
<Grid item id="right" xs={12} sm={12}>
<Card style={{ backgroundColor: '#0f0f0f' }}>
<CardContent>
<ChordButtons handleChordClick={this.handleChordClick} />
</CardContent>
</Card>
</Grid>
</Grid>
{/* <Backdrop style={{zIndex: 1000}} open='true'>
<CircularProgress />
</Backdrop> */}
</Grid>
</div>
);
}
}
export default connect(
state => ({
analysis: state.spotify.audioAnalysis,
segments: state.spotify.audioAnalysis.segments,
sections: state.spotify.audioAnalysis.sections,
}),
{ updatePlayerState, getAsyncIntegrations }
)(Layout);

View File

@ -0,0 +1,38 @@
import React from 'react';
import Checkbox from '@material-ui/core/Checkbox';
import { FormGroup, FormControlLabel, makeStyles } from '@material-ui/core';
const useStyles = makeStyles(theme => ({
root: {
color: '#1ED760',
'&$checked': {
color: '#1ED760',
},
},
}));
export default function PitchSelect(props) {
const pitchClasses = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const classes = useStyles();
return (
<FormGroup row="true">
{pitchClasses.map(p => {
return (
<FormControlLabel
control={
<Checkbox
color="primary"
className={classes.root}
checked={props.pitches[p]}
onClick={props.handleCheck}
name={p}
/>
}
label={p}
style={{ color: '#1ED760' }}
/>
);
})}
</FormGroup>
);
}

View File

@ -0,0 +1,87 @@
import React, { PureComponent } from 'react';
import {
ComposedChart,
ResponsiveContainer,
XAxis,
YAxis,
Tooltip,
Bar,
Cell,
Legend,
} from 'recharts';
export default class Chart extends PureComponent {
render() {
const pitchColors = [
'#ff3333',
'#ff9933',
'#ffff33',
'#99ff33',
'#33ff33',
'#33ff99',
'#33ffff',
'#3399ff',
'#3333ff',
'#9933ff',
'#ff33ff',
'#ff3399',
];
const { sections, width } = this.props;
return (
<ResponsiveContainer width={width} height={20} debounce={5}>
<ComposedChart
barGap={1}
data={sections}
margin={{
top: 15,
right: 20,
left: 0,
bottom: 0,
}}
>
<XAxis hide="true" dataKey="start" unit="s" />
<YAxis hide="true" height={10} />
<Legend align="left" height={20} layout="vertical" content={renderLegend} />
{/* <Tooltip
content={renderTooltip}
/> */}
<Bar dataKey="start" minPointSize={10} barGap={0}>
{sections
? sections.map((entry, index) => (
<Cell key={`cell-${index}`} fill={pitchColors[entry.key]} />
))
: null}
</Bar>
</ComposedChart>
</ResponsiveContainer>
);
}
}
const renderLegend = () => {
return (
<ul
style={{
height: 20,
display: 'table',
paddingLeft: 10,
marginTop: 0,
listStyleType: 'none',
}}
>
<li style={{ color: '#f1f1f1' }}>Key</li>
</ul>
);
};
// const renderTooltip = ({ active, payload, label }) => {
// const pitchClasses = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
// if (active === true) {
// return (
// <div style={{backgroundColor: '#010101', padding: 10}}>
// <div style={{color:'#f1f1f1'}}>{`${pitchClasses[payload[0].payload.key]}`}</div>
// </div>
// )
// }
// }

View File

@ -24,7 +24,7 @@ const DataRow = ({ id, name, type, data }) => {
event_filter: { scene_name: dr[1].scene_name },
},
});
//let newData = data.filter(x => x[0] !== dr[0]);
setTest(!test);
};
return data

View File

@ -1,11 +1,12 @@
import React from 'react';
import { connect } from 'react-redux';
import withStyles from '@material-ui/core/styles/withStyles';
import Moment from 'react-moment';
import moment from 'moment';
import { ToastContainer, toast } from 'react-toastify';
// import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
//import Card from '@material-ui/core/Card';
//import CardHeader from '@material-ui/core/CardHeader';
//import CardContent from '@material-ui/core/CardContent';
import {
Button,
AppBar,
Checkbox,
FormControl,
@ -14,33 +15,44 @@ import {
InputLabel,
Select,
Typography,
Accordion,
AccordionSummary,
AccordionDetails,
Link,
Slider,
Table,
TableHead,
TableRow,
TableContainer,
TableBody,
TableCell,
Paper,
} from '@material-ui/core';
import {
PlayArrow,
Pause,
SkipNext,
SkipPrevious,
ExpandMore,
Info,
} from '@material-ui/icons';
} from '@material-ui/core';
import { updatePlayerState } from 'modules/spotify';
import { getAsyncIntegrations } from 'modules/integrations';
import PlayArrow from '@material-ui/icons/PlayArrow';
import Pause from '@material-ui/icons/Pause';
import SkipNext from '@material-ui/icons/SkipNext';
import SkipPrevious from '@material-ui/icons/SkipPrevious';
import InfoIcon from '@material-ui/icons/Info';
import Link from '@material-ui/core/Link';
import { activateScene } from 'modules/scenes';
import { addTrigger } from 'proxies/spotify';
import Moment from 'react-moment';
import moment from 'moment';
import Slider from '@material-ui/core/Slider';
import { ToastContainer, toast } from 'react-toastify';
import RadarChart from 'components/SpotifyPlayer/RadarChart';
import Accordion from '@material-ui/core/Accordion';
import AccordionSummary from '@material-ui/core/AccordionSummary';
import AccordionDetails from '@material-ui/core/AccordionDetails';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableContainer from '@material-ui/core/TableContainer';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import Paper from '@material-ui/core/Paper';
import Layout from 'components/AudioAnalysis/Layout';
// const useStylesAccordin = makeStyles(theme => ({
// root: {
// width: '100%',
// },
// heading: {
// fontSize: theme.typography.pxToRem(15),
// fontWeight: theme.typography.fontWeightRegular,
// },
// }));
const data = {
datasets: [
@ -93,6 +105,18 @@ const styles = theme => ({
},
});
// const useStyles = makeStyles(theme => ({
// sceneButton: {
// size: 'large',
// margin: theme.spacing(1),
// },
// submitControls: {
// display: 'flex',
// flexWrap: 'wrap',
// width: '100%',
// height: '100%',
// },
// }));
class SpotifyPlayer extends React.Component {
constructor(props) {
@ -103,6 +127,21 @@ class SpotifyPlayer extends React.Component {
effects: '',
play: false,
player: {},
expanded: true,
pitches: {
C: true,
'C#': true,
D: true,
'D#': true,
E: true,
F: true,
'F#': true,
G: true,
'G#': true,
A: true,
'A#': true,
B: true,
},
};
}
@ -210,6 +249,9 @@ class SpotifyPlayer extends React.Component {
getTime(duration) {
var seconds = Math.floor((duration / 1000) % 60),
minutes = Math.floor((duration / (1000 * 60)) % 60);
// hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
// hours = hours < 10 ? '0' + hours : hours;
minutes = minutes < 10 ? '0' + minutes : minutes;
seconds = seconds < 10 ? '0' + seconds : seconds;
@ -277,7 +319,7 @@ class SpotifyPlayer extends React.Component {
}
render() {
const { playerState, classes, scenes, audioFeatures } = this.props;
const { playerState, classes, scenes, audioFeatures, audioAnalysis } = this.props;
const rows = [];
function capitalizeFirstLetter(string) {
@ -298,7 +340,7 @@ class SpotifyPlayer extends React.Component {
return Object.keys(playerState).length === 0 ? (
<Link target="_blank" href="https://support.spotify.com/us/article/spotify-connect/">
<Typography color="textPrimary">
Using Spotify Connect, select LedFX <Info></Info>
Using Spotify Connect, select LedFX <InfoIcon></InfoIcon>
</Typography>
</Link>
) : (
@ -495,17 +537,21 @@ class SpotifyPlayer extends React.Component {
</Grid>
</Grid>
</Grid>
<Grid md={12} container item style={{ margin: '30px 20px' }}>
<Accordion style={{ width: '100%' }}>
<AccordionSummary
expandIcon={<ExpandMore />}
aria-controls="panel1a-content"
id="panel1a-header"
style={{ width: '100%' }}
>
<Typography>Audio Features</Typography>
</AccordionSummary>
<AccordionDetails>
<Accordion
style={{ width: '100%' }}
expanded={this.state.expanded}
onChange={() => this.setState({ expanded: !this.state.expanded })}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1a-content"
id="panel1a-header"
style={{ width: '100%' }}
>
<Typography>Audio Features</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid md={12} container item style={{ margin: '30px 20px' }}>
<Grid xs={6} item>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
@ -544,7 +590,7 @@ class SpotifyPlayer extends React.Component {
</Table>
</TableContainer>
</Grid>
<Grid xs={6} item>
<Grid xs={6} item sx={{ background: 'gray !important' }}>
<RadarChart
chartData={data}
chartValues={audioFeatures}
@ -552,9 +598,12 @@ class SpotifyPlayer extends React.Component {
// positon={playerState.position}
/>
</Grid>
</AccordionDetails>
</Accordion>
</Grid>
<Grid container xs={12}>
<Layout />
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
</Grid>
<ToastContainer />
</AppBar>
@ -566,6 +615,8 @@ export default connect(
state => ({
playerState: state.spotify.playerState,
audioFeatures: state.spotify.audioFeatures,
audioAnalysis: state.spotify.audioAnalysis,
accessToken: state.spotify.accessToken,
scenes: state.scenes.list,
integrations: state.integrations.list.spotify,

4
yarn.lock Normal file
View File

@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1