Update pieces to v4

This commit is contained in:
metalex9
2020-10-13 02:12:54 -05:00
parent 807393cd15
commit d5ffe8d585
22 changed files with 508 additions and 821 deletions

748
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -65,10 +65,11 @@
"@fortawesome/fontawesome-svg-core": "^1.2.29",
"@fortawesome/free-solid-svg-icons": "^5.13.1",
"@fortawesome/react-fontawesome": "^0.1.11",
"@generative-music/pieces-alex-bainter": "^3.4.0",
"@generative-music/samples-alex-bainter": "^1.1.0",
"@generative-music/pieces-alex-bainter": "^4.1.2",
"@generative-music/samples-alex-bainter": "^1.2.0",
"@generative-music/visualizer": "^2.3.5",
"@generative-music/web-provider": "^1.1.0",
"@generative-music/web-library": "^0.2.0",
"@generative-music/web-provider": "^2.0.4",
"audiobuffer-to-wav": "^1.0.0",
"classnames": "^2.2.6",
"clone": "^2.1.2",
@@ -81,7 +82,7 @@
"react-tiny-popover": "3.4.1",
"redux": "^4.0.5",
"svg.js": "^2.7.1",
"tone": "^13.8.25",
"tone": "^14.7.58",
"uuid": "^3.4.0"
}
}

View File

@@ -5,10 +5,10 @@ const DEFAULT_VISUALIZATION_TYPE = 'squareCut';
const pieceLoader = source => {
const pieceManifest = JSON.parse(source);
const output = `import image from '${pieceManifest.image}';
import makePiece from '${pieceManifest.makePiece}';
import activate from '${pieceManifest.makePiece}';
export default {
image,
makePiece,
activate,
title: '${pieceManifest.title}',
id: '${pieceManifest.artistId}-${pieceManifest.id}',
artist: '${pieceManifest.artistId}',

View File

@@ -71,7 +71,7 @@ const AboutTabComponent = ({ version, isUpdateAvailable, isOnline }) => {
</p>
<p>
If you enjoy this project, consider supporting it:
<div className="support-methods">
<span className="support-methods">
<a
href="https://www.patreon.com/bePatron?u=2484731"
target="_blank"
@@ -87,7 +87,7 @@ const AboutTabComponent = ({ version, isUpdateAvailable, isOnline }) => {
PayPal
</a>
<span>BTC: 3DMb8BQVTtfVv59pMLmZmHr6xSoJsb3P4Z</span>
</div>
</span>
</p>
<p>{`v${version}`}</p>
<p>

View File

@@ -12,9 +12,15 @@ import HelpTabComponent from './help-tab';
import TitleNavContainer from '@containers/title-nav.container';
import AboutTabContainer from '@containers/about-tab.container';
import PiecesTabContainer from '@containers/pieces-tab.container';
import RecordTabContainer from '@containers/record-tab.container';
import './app.scss';
const RecordRedirect = () => {
useEffect(() => {
window.location = 'https://record.generative.fm';
});
return null;
};
const AppComponent = () => {
const [isHoverEnabled, setIsHoverEnabled] = useState(!isMobile);
useEffect(() => {
@@ -51,7 +57,7 @@ const AppComponent = () => {
<Route exact path="/" component={PiecesTabContainer} />
<Route exact path="/about" component={AboutTabContainer} />
<Route exact path="/help" component={HelpTabComponent} />
<Route exact path="/record" component={RecordTabContainer} />
<Route exact path="/record" component={RecordRedirect} />
<Route
path="/music/:pieceId"
render={({ match, history }) => (

View File

@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import propTypes from 'prop-types';
import { Redirect } from 'react-router';
import pieces from '@pieces';
import provider from '@pieces/provider';
import library from '@pieces/library';
import piecesById from '@pieces/by-id';
import LinkButton from '@components/shared/link-button';
import PieceFilter from './piece-filter';
@@ -72,7 +72,7 @@ const PiecesTabComponent = ({
if (!isOnline) {
Promise.all(
pieces.map(({ id, sampleNames }) =>
provider.canProvide(sampleNames).then(result => [id, result])
library.canProvide(sampleNames).then(result => [id, result])
)
).then(results => {
setIsPieceCachedMap(new Map(results));

View File

@@ -1,289 +0,0 @@
import React, { useState, useEffect, useMemo } from 'react';
import propTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { faTimes, faPlus } from '@fortawesome/free-solid-svg-icons';
import pieces from '@pieces';
import piecesById from '@pieces/by-id';
import provider from '@pieces/provider';
import ControlButtonComponent from '../controls/control-button';
import TextButton from '@components/shared/text-button';
import isSupported from '@config/is-supported';
import './record-tab.scss';
const MAX_RECORDING_LENGTH_MINUTES = 240;
const getGeneratedRecordingsQueue = generatedRecordings =>
Reflect.ownKeys(generatedRecordings)
.map(recordingId => generatedRecordings[recordingId])
.sort((a, b) => a.queuedAtTime - b.queuedAtTime);
const RecordTabComponent = ({
selectedPieceId,
selectPiece,
queueRecordingGeneration,
generatedRecordings,
lastRecordingGenerationLength,
isOnline,
removeRecordingGeneration,
startRecordingGeneration,
}) => {
if (selectedPieceId === null) {
selectPiece(pieces[0].id);
return <div />;
}
const [recordingLengthInMinutes, setRecordingLengthInMinutes] = useState(
lastRecordingGenerationLength
);
const selectedPiece = useMemo(() => piecesById[selectedPieceId], [
selectedPieceId,
]);
const [generatedRecordingsQueue, setGeneratedRecordingsQueue] = useState(
getGeneratedRecordingsQueue(generatedRecordings)
);
const [isGenerationInProgress, setIsGenerationInProgress] = useState(false);
useEffect(() => {
const queue = getGeneratedRecordingsQueue(generatedRecordings);
setGeneratedRecordingsQueue(queue);
setIsGenerationInProgress(queue.some(({ isInProgress }) => isInProgress));
}, [generatedRecordings]);
const [isPieceCachedMap, setIsPieceCachedMap] = useState(new Map());
useEffect(() => {
if (!isOnline) {
Promise.all(
pieces.map(({ id, sampleNames }) =>
provider.canProvide(sampleNames).then(result => [id, result])
)
).then(results => {
setIsPieceCachedMap(new Map(results));
});
}
}, [isOnline]);
const isPieceDisabled = piece =>
!piece.isRecordable || (!isOnline && !isPieceCachedMap.get(piece.id));
const getIsRecordingValid = () =>
!isPieceDisabled(selectedPiece) &&
recordingLengthInMinutes > 0 &&
recordingLengthInMinutes <= MAX_RECORDING_LENGTH_MINUTES;
const [isRecordingValid, setIsRecordingValid] = useState(
getIsRecordingValid()
);
useEffect(() => {
setIsRecordingValid(getIsRecordingValid());
}, [selectedPiece, recordingLengthInMinutes, isOnline]);
const handleSubmit = event => {
event.preventDefault();
};
return (
<div className="centered-tab record-tab">
<a
className="alert"
href="https://record.generative.fm"
target="_blank"
rel="noreferrer noopener"
>
Try the new{' '}
<span className="fake-link">Generative.fm recording app</span>. Add fade
ins and outs, with more consistent waiting timesoften fasterand
smaller file sizes.
</a>
This page enables you to generate and download recordings. Recording
generation may take a while, and longer recordings require more time to
generate. Music playback is not supported while recording generation is in
progress.
{isSupported && (
<form onSubmit={handleSubmit}>
<div className="form-group recording-entry">
<span>
Record
<input
type="number"
id="length-input"
min="1"
max={MAX_RECORDING_LENGTH_MINUTES}
value={recordingLengthInMinutes}
onChange={event =>
setRecordingLengthInMinutes(event.target.value)
}
title="Length of the recording to generate, in miniutes"
required
/>
minutes of
<select
id="piece-select"
value={selectedPieceId}
onChange={event => selectPiece(event.target.value)}
title="Piece to record"
>
{pieces.map(piece => (
<option
key={piece.id}
value={piece.id}
disabled={isPieceDisabled(piece)}
>
{piece.title}
</option>
))}
</select>
</span>
{isRecordingValid && (
<ControlButtonComponent
faIcon={faPlus}
title="Add to queue"
onClick={() =>
queueRecordingGeneration({
pieceId: selectedPieceId,
lengthInMinutes: recordingLengthInMinutes,
})
}
/>
)}
{!isRecordingValid && <div className="btn-spacer" />}
</div>
{isPieceDisabled(selectedPiece) && (
<div className="form-group invalid-msg">
{`${selectedPiece.title} is not recordable${
selectedPiece.isRecordable ? ' right now.' : '.'
}`}
</div>
)}
{(recordingLengthInMinutes <= 0 ||
recordingLengthInMinutes > MAX_RECORDING_LENGTH_MINUTES) && (
<div className="form-group invalid-msg">
Recording length must be between 1 and 240 minutes
</div>
)}
<div className="form-group">
{generatedRecordingsQueue.filter(({ url }) => url === '').length >
0 &&
!isGenerationInProgress && (
<TextButton
title="Resume previously queued recording generation"
onClick={() =>
startRecordingGeneration(
generatedRecordingsQueue[0].recordingId
)
}
>
Resume generation
</TextButton>
)}
</div>
<div className="form-group">
<ul className="generated-recordings-queue">
{generatedRecordingsQueue.map(generatedRecording => {
const {
recordingId,
lengthInMinutes,
url,
pieceId,
isInProgress,
} = generatedRecording;
let status = '';
if (isInProgress) {
status = ' - generating...';
} else if (url === '') {
status = ' - waiting to generate';
}
const displayText = `${lengthInMinutes} minutes of ${piecesById[pieceId].title}${status}`;
return (
<li
key={recordingId}
className="generated-recordings-queue__item"
>
{url === '' ? (
<span>{displayText} </span>
) : (
<a
href={url}
download={`${pieceId}-${lengthInMinutes}-minutes.wav`}
>
{displayText}
</a>
)}
{!isInProgress && (
<ControlButtonComponent
faIcon={faTimes}
onClick={() => removeRecordingGeneration(recordingId)}
title="Remove"
/>
)}
{isInProgress && <div className="btn-spacer" />}
</li>
);
})}
</ul>
</div>
</form>
)}
{!isSupported && (
<div>
<br />
<Link to="/help">Browser not supported</Link>
</div>
)}
{selectedPiece.isRecordable && (
<span>
<br />
<br />
<a
rel="license noreferrer noopener"
href="http://creativecommons.org/licenses/by/4.0/"
target="_blank"
className="centered-content"
>
<img
alt="Creative Commons License"
style={{ borderWidth: 0 }}
src="https://i.creativecommons.org/l/by/4.0/80x15.png"
/>
</a>
<br />
<span className="centered-content">
{selectedPiece.title} (Excerpt) by{' '}
<a
href="https://alexbainter.com"
target="_blank"
rel="noreferrer noopener"
>
Alex Bainter
</a>{' '}
is licensed under a{' '}
<a
rel="license noreferrer noopener"
href="http://creativecommons.org/licenses/by/4.0/"
target="_blank"
>
Creative Commons Attribution 4.0 International License
</a>
.
</span>
</span>
)}
</div>
);
};
RecordTabComponent.propTypes = {
selectedPieceId: propTypes.string.isRequired,
selectPiece: propTypes.func.isRequired,
queueRecordingGeneration: propTypes.func.isRequired,
generatedRecordings: propTypes.object.isRequired,
lastRecordingGenerationLength: propTypes.string.isRequired,
isOnline: propTypes.bool.isRequired,
removeRecordingGeneration: propTypes.func.isRequired,
startRecordingGeneration: propTypes.func.isRequired,
};
export default RecordTabComponent;

View File

@@ -1,57 +0,0 @@
@import '_colors';
@import '_variables';
@import '_mixins';
.record-tab {
input:invalid {
border: 1px solid $highlightColor;
}
.form-group {
margin-top: 1em;
}
.recording-entry {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 0.5em;
input,
select {
margin: 0 1em;
}
}
.generated-recordings-queue {
@include borderedList();
}
.btn-spacer {
height: $buttonWidth;
}
.invalid-msg {
font-style: italic;
color: $highlightColor;
}
.centered-content {
width: 100%;
text-align: center;
display: inline-block;
}
.alert {
padding: 1em;
background-color: $highlightColor;
margin-bottom: 1em;
display: block;
color: $primaryColor;
text-decoration: none;
.fake-link {
text-decoration: underline;
}
}
}

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import Tone from 'tone';
import * as Tone from 'tone';
import classNames from 'classnames';
import streamDestination from '@store/middleware/stream-destination';
import './air-play.scss';
@@ -25,7 +25,7 @@ const AirPlay = () => {
const makeHandleWebkitCurrentPlaybackTargetIsWirelessChanged = audio => () => {
const isWireless = Boolean(audio.webkitCurrentPlaybackTargetIsWireless);
Tone.master.mute = isWireless;
Tone.Destination.mute = isWireless;
setIsConnected(isWireless);
};

View File

@@ -95,11 +95,16 @@ const TitleNavComponent = ({
linkTo="/"
isActive={matchRootOrMusic}
/>
<TitleNavLink
text="RECORD"
parentClass="title-nav__header__tab-list"
linkTo="/record"
/>
<li className="title-nav__header__tab-list__item">
<a
href="https://record.generative.fm"
className="title-nav__header__tab-list__item__link"
target="_blank"
rel="noreferrer noopener"
>
RECORD
</a>
</li>
<TitleNavLink
text="HELP"
parentClass="title-nav__header__tab-list"

View File

@@ -1,3 +1,3 @@
import Tone from 'tone';
import * as Tone from 'tone';
export default Tone.supported;

View File

@@ -1,25 +0,0 @@
import { connect } from 'react-redux';
import recordTabComponent from '@components/app/record-tab';
import selectPiece from '@store/actions/creators/select-piece.creator';
import queueRecordingGeneration from '@store/actions/creators/queue-recording-generation.creator';
import removeRecordingGeneration from '@store/actions/creators/remove-recording-generation.creator';
import startRecordingGeneration from '@store/actions/creators/start-recording-generation.creator';
const mapStateToProps = ({
selectedPieceId,
generatedRecordings,
lastRecordingGenerationLength,
isOnline,
}) => ({
selectedPieceId,
generatedRecordings,
lastRecordingGenerationLength,
isOnline,
});
export default connect(mapStateToProps, {
selectPiece,
queueRecordingGeneration,
removeRecordingGeneration,
startRecordingGeneration,
})(recordTabComponent);

10
src/pieces/library.js Normal file
View File

@@ -0,0 +1,10 @@
import getSamplesByFormat from '@generative-music/samples-alex-bainter';
import createProvider from '@generative-music/web-provider';
import createLibrary from '@generative-music/web-library';
import sampleFormat from '@config/sample-format';
const provider = createProvider();
const sampleIndex = getSamplesByFormat()[sampleFormat];
const library = createLibrary({ sampleIndex, provider });
export default library;

View File

@@ -1,3 +1,4 @@
import piecesById from '@pieces/by-id';
import isMobile from '@config/is-mobile';
import getOnlineStatus from '@utils/get-online-status';
import objToMap from '@utils/obj-to-map';
@@ -30,6 +31,9 @@ const getInitialState = storedState => {
timer: Object.assign({}, storedState.timer, { remainingMS: 0 }),
favorites: new Set(storedState.favorites),
isInstallable: false,
selectedPieceId: piecesById[storedState.selectedPieceId]
? storedState.selectedPieceId
: null,
});
if (typeof storedState.globalPlayTime === 'object') {

View File

@@ -1,4 +1,4 @@
import Tone from 'tone';
import * as Tone from 'tone';
import castApplicationId from '@config/cast-application-id';
import piecesById from '@pieces/by-id';
import artists from '@data/artists';
@@ -41,7 +41,7 @@ const updateReceiverMetadata = (castSession, currentPieceId) => {
};
const handleCastStateConnected = (castContext, store) => {
Tone.Master.mute = true;
Tone.Destination.mute = true;
const peerConnection = new RTCPeerConnection(null);
const castSession = castContext.getCurrentSession();
castSession.addMessageListener(CUSTOM_MESSAGE_NAMESPACE, (ns, message) => {
@@ -74,7 +74,7 @@ const handleCastStateConnected = (castContext, store) => {
const { cast } = window;
const handleCastStateChanged = ({ castState }) => {
if (castState === cast.framework.CastState.NOT_CONNECTED) {
Tone.Master.mute = false;
Tone.Destination.mute = false;
castContext.removeEventListener(
cast.framework.CastContextEventType.CAST_STATE_CHANGED,
handleCastStateChanged

View File

@@ -1,4 +1,3 @@
import Tone from 'tone';
import MUTE from '../../actions/types/mute.type';
import UNMUTE from '../../actions/types/unmute.type';
import NEXT from '../../actions/types/next.type';
@@ -19,7 +18,6 @@ import updatePlayTime from '../../actions/creators/update-play-time.creator';
const UPDATE_PLAY_TIME_INTERVAL_MS = 5000;
const piecesMiddleware = store => next => {
Tone.context.latencyHint = 'balanced';
let playTimeInterval;
const startTrackingPlayTimeForPieceId = pieceId => {

View File

@@ -1,11 +1,12 @@
import Tone from 'tone';
import provider from '@pieces/provider';
import * as Tone from 'tone';
import library from '@pieces/library';
import markPieceBuildLoading from '../../actions/creators/mark-piece-build-loading.creator';
import markPieceBuildLoaded from '../../actions/creators/mark-piece-build-loaded.creator';
import performance from './performance';
import stopPerformances from './stop-performances';
import convertPctToDb from './convert-pct-to-db';
import streamDestination from '../stream-destination';
import noop from '@utils/noop';
let lastBuildId;
let isPerformanceBuilding = false;
@@ -46,33 +47,34 @@ const makePlayPiece = (store, performances) => {
// console.log(maxDb);
// }, 1000);
provider.provide(piece.sampleNames, Tone.context).then(samples => {
piece
.makePiece({
destination: pieceVol,
audioContext: Tone.context,
samples,
})
.then(cleanUp => {
piecePerformance.addCleanupFn(cleanUp);
piecePerformance.isLoaded = true;
isPerformanceBuilding = false;
const { selectedPieceId, isPlaying } = store.getState();
store.dispatch(markPieceBuildLoaded(piecePerformance));
if (
lastBuildId === piecePerformance.performanceId &&
selectedPieceId === piece.id &&
isPlaying
) {
Tone.Transport.start();
} else {
stopPerformances(performances);
}
if (isPlaying && queuedPiece !== null) {
playPiece(queuedPiece, store);
}
});
});
piece
.activate({
destination: pieceVol,
context: Tone.context,
sampleLibrary: library,
onProgress: noop,
})
.then(([deactivate, schedule]) => {
piecePerformance.addCleanupFn(deactivate);
const end = schedule();
piecePerformance.addCleanupFn(end);
piecePerformance.isLoaded = true;
isPerformanceBuilding = false;
const { selectedPieceId, isPlaying } = store.getState();
store.dispatch(markPieceBuildLoaded(piecePerformance));
if (
lastBuildId === piecePerformance.performanceId &&
selectedPieceId === piece.id &&
isPlaying
) {
Tone.Transport.start();
} else {
stopPerformances(performances);
}
if (isPlaying && queuedPiece !== null) {
playPiece(queuedPiece, store);
}
});
} else {
queuedPiece = piece;
}

View File

@@ -1,4 +1,4 @@
import Tone from 'tone';
import * as Tone from 'tone';
import uuid from 'uuid';
const performance = (piece, volumeNode) => {

View File

@@ -1,4 +1,4 @@
import Tone from 'tone';
import * as Tone from 'tone';
import toWav from 'audiobuffer-to-wav';
import piecesById from '@pieces/by-id';
import START_RECORDING_GENERATION from '@store/actions/types/start-recording-generation.type';
@@ -8,7 +8,8 @@ import RECORDING_GENERATION_COMPLETE from '@store/actions/types/recording-genera
import recordingGenerationComplete from '@store/actions/creators/recording-generation-complete.creator';
import startRecordingGeneration from '@store/actions/creators/start-recording-generation.creator';
import stop from '@store/actions/creators/stop.creator';
import provider from '@pieces/provider';
import library from '@pieces/library';
import noop from '@utils/noop';
const renderOffline = (piece, durationInSeconds) => {
const { sampleRate } = Tone.context;
@@ -21,7 +22,7 @@ const renderOffline = (piece, durationInSeconds) => {
durationInSeconds,
sampleRate
);
Tone.context = offlineContext;
Tone.setContext(offlineContext);
/*
* SUPER HACK ALERT―TRULY DISGUSTING
@@ -31,41 +32,41 @@ const renderOffline = (piece, durationInSeconds) => {
* rendered. So, delay those disconnects until after the audio has been rendered.
*/
const fnAttempts = [];
const restoreFns = [];
const hackFn = (target, fnName, returnValue) => {
const originalFn = target[fnName];
restoreFns.push(() => {
target[fnName] = originalFn;
});
target[fnName] = function hacked(...args) {
fnAttempts.push(originalFn.bind(this, ...args));
return returnValue;
};
};
// const fnAttempts = [];
// const restoreFns = [];
// const hackFn = (target, fnName, returnValue) => {
// const originalFn = target[fnName];
// restoreFns.push(() => {
// target[fnName] = originalFn;
// });
// target[fnName] = function hacked(...args) {
// fnAttempts.push(originalFn.bind(this, ...args));
// return returnValue;
// };
// };
//
// hackFn(Tone, 'disconnect', Tone);
// hackFn(window.AudioBufferSourceNode.prototype, 'disconnect');
// hackFn(Tone.ToneBufferSource.prototype, 'dispose');
hackFn(Tone, 'disconnect', Tone);
hackFn(window.AudioBufferSourceNode.prototype, 'disconnect');
hackFn(Tone.BufferSource.prototype, 'dispose');
return provider
.provide(piece.sampleNames, offlineContext)
.then(samples =>
piece.makePiece({
samples,
audioContext: offlineContext,
destination: Tone.Master,
})
)
.then(cleanup => {
return piece
.activate({
context: offlineContext,
destination: offlineContext.destination,
sampleLibrary: library,
onProgress: noop,
})
.then(([deactivate, schedule]) => {
const end = schedule();
Tone.Transport.start();
const renderPromise = offlineContext.render();
return renderPromise.then(recordingBuffer => {
fnAttempts.concat(restoreFns).forEach(fn => fn());
//fnAttempts.concat(restoreFns).forEach(fn => fn());
end();
Tone.Transport.stop();
Tone.Transport.cancel();
cleanup();
Tone.context = originalContext;
deactivate();
Tone.setContext(originalContext);
return recordingBuffer;
});
});

View File

@@ -1,4 +1,4 @@
import Tone from 'tone';
import * as Tone from 'tone';
const streamDestination = Tone.context.createMediaStreamDestination();

5
src/utils/noop.js Normal file
View File

@@ -0,0 +1,5 @@
const noop = () => {
/* do nothing */
};
export default noop;

View File

@@ -9,7 +9,7 @@ const { EnvironmentPlugin } = require('webpack');
const makeConfig = alias => ({
mode: 'development',
devtool: 'sourcemap',
devtool: 'source-map',
entry: ['@babel/polyfill', './src'],
output: {
path: path.join(__dirname, 'dist'),