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
5 changes: 4 additions & 1 deletion demo/src/ExampleJSONScores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,20 @@ import * as RoxanneShiftedInfinite from '../../fixtures/roxanne-30s-preview-shif
import * as TNGEngines from '../../fixtures/TNG-Crysknife007-16-899-s.wav';
import * as TNGJSON from '../../fixtures/TNG-Infinite-Idle-Engine.json';

type ExampleJSON = { name: string; score: Score };
type ExampleJSON = { slug: string; name: string; score: Score };

// Note: The double JSON.stringify/parsing is mostly for TypeScript, so it knows
// that the incoming JSON is actually a Score. It's hard to tell TS that we
// actually have a score.

const examples: ExampleJSON[] = [
{
slug: 'ratatat-forever',
name: 'Ratatat forever',
score: JSON.parse(JSON.stringify(RatatatLoop)).default,
},
{
slug: 'tng-engines',
name: 'Star Trek TNG Infinite Ambient Engine Noise',
score: JSON.parse(
JSON.stringify({
Expand All @@ -55,6 +57,7 @@ const examples: ExampleJSON[] = [
).default,
},
{
slug: 'roxanne-pitched-json',
name: 'Roxanne, but pitched on every "Roxanne" (infinite JSON version)',
score: JSON.parse(JSON.stringify(RoxanneShiftedInfinite)).default,
},
Expand Down
8 changes: 8 additions & 0 deletions demo/src/ExampleScripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ export type CompiledPlaygroundScript = (
) => Promise<void>;

export type ExampleScript = {
slug: string;
name: string;
script: PlaygroundScript;
};

export const examples: ExampleScript[] = [
{
slug: 'tng-engines-scripted',
name: 'Star Trek TNG Infinite Ambient Engine Noise (scripted)',
script: `
const filePath = '${TNGEngines.default}';
Expand Down Expand Up @@ -161,6 +163,7 @@ export const examples: ExampleScript[] = [
`,
},
{
slug: 'roxanne-pitched-scripted',
name: 'Roxanne, but pitched on every "Roxanne" (infinite scripted version)',
script: `
// These Globals are provided by this playground, and act as imported namespaces
Expand Down Expand Up @@ -216,6 +219,7 @@ export const examples: ExampleScript[] = [
`,
},
{
slug: 'play-10-to-20',
name: 'Load and play seconds 10 through 20 of a single file',
script: `
// These Globals are provided by this playground, and act as imported namespaces
Expand Down Expand Up @@ -244,6 +248,7 @@ export const examples: ExampleScript[] = [
`,
},
{
slug: 'play-then-loop',
name: 'Play the first 10 seconds of a file, then loop a single 4 beat bar forever.',
script: `
// These Globals are provided by this playground, and act as imported namespaces
Expand Down Expand Up @@ -278,6 +283,7 @@ export const examples: ExampleScript[] = [
`,
},
{
slug: 'fade-on-interaction',
name: 'Play a track, then quickly fade out on user interaction',
script: `
// These Globals are provided by this playground, and act as imported namespaces
Expand Down Expand Up @@ -337,6 +343,7 @@ export const examples: ExampleScript[] = [
`,
},
{
slug: 'swap-score-on-interaction',
name: 'Loop and Pitch Shift a track, then swap it with a modified score on User Interaction',
script: `
// These Globals are provided by this playground, and act as imported namespaces
Expand Down Expand Up @@ -420,6 +427,7 @@ export const examples: ExampleScript[] = [
`,
},
{
slug: 'pitchshift-derail',
name: 'Pitchshift a piano down, with a voice over the top. 30 seconds in slow both down (derail!).',
script: `
// These Globals are provided by this playground, and act as imported namespaces
Expand Down
86 changes: 67 additions & 19 deletions demo/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { XAudioContext } from 'nf-player';
import * as React from 'react';
import styled from 'styled-components';

import { type PanelSlug, parseHash, setRoute, subscribeRoute } from '../router';
import { CODEEditor } from './CODEEditor/CODEEditor';
import { JSONEditor } from './JSONEditor/JSONEditor';
import { DemoTheme } from './Theme';
Expand All @@ -42,11 +43,7 @@ const StyledApplication = styled.div`
height: 100%;
`;

enum Panels {
CODE,
JSON,
VISUALIZER,
}
const DEFAULT_PANEL: PanelSlug = 'code';

// https://webaudio.github.io/web-audio-api/#AnalyserNode-attributes
const defaultAnalyserOptions = {
Expand All @@ -56,8 +53,11 @@ const defaultAnalyserOptions = {
maxDecibels: -30,
};

const initialRoute = parseHash();

const initialAppState = {
panel: Panels.CODE,
panel: initialRoute.panel ?? DEFAULT_PANEL,
exampleSlug: initialRoute.exampleSlug,
player: new SmartPlayer(),
analyser: XAudioContext().createAnalyser(),
};
Expand All @@ -68,7 +68,26 @@ type AppProps = unknown;
export class App extends React.Component<AppProps, AppState> {
readonly state: AppState = initialAppState;

switchPanel(to: Panels) {
private unsubscribeRoute: (() => void) | undefined;

componentDidMount() {
// The mounted panel's editor will write its resolved slug back via
// onExampleChange, which populates the URL on first load.
this.unsubscribeRoute = subscribeRoute((route) => {
const nextPanel = route.panel ?? DEFAULT_PANEL;
if (nextPanel !== this.state.panel) {
this.switchPanel(nextPanel, route.exampleSlug);
} else if (route.exampleSlug !== this.state.exampleSlug) {
this.setState({ exampleSlug: route.exampleSlug });
}
});
}

componentWillUnmount() {
this.unsubscribeRoute?.();
}

switchPanel(to: PanelSlug, exampleSlug?: string) {
if (this.state.player.playing) {
this.state.player.playing = false;
}
Expand All @@ -78,7 +97,7 @@ export class App extends React.Component<AppProps, AppState> {

let nextRenderer;
let analyser;
if (to === Panels.VISUALIZER) {
if (to === 'visualizer') {
const context = XAudioContext();
analyser = new AnalyserNode(context, defaultAnalyserOptions);
analyser.connect(context.destination);
Expand All @@ -96,9 +115,12 @@ export class App extends React.Component<AppProps, AppState> {
const nextPlayer = new SmartPlayer(nextRenderer);
const nextState = {
panel: to,
exampleSlug,
player: nextPlayer,
};

setRoute({ panel: to, exampleSlug });

// typescript type guard
if (analyser !== undefined) {
this.setState({
Expand All @@ -110,34 +132,60 @@ export class App extends React.Component<AppProps, AppState> {
}
}

handlePanelButton = (to: PanelSlug) => {
this.switchPanel(to, undefined);
};

handleExampleChange = (slug: string | undefined) => {
this.setState({ exampleSlug: slug });
setRoute({ panel: this.state.panel, exampleSlug: slug });
};

componentDidUpdate() {
// Exposed on `window` so the playground scripts can drive the player.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).p = this.state.player;
}

render() {
const { panel, player, analyser } = this.state;
const { panel, player, analyser, exampleSlug } = this.state;

return (
<StyledApplication>
<VerticalFitArea>
<VerticalFixedSection>
<button onClick={() => this.switchPanel(Panels.CODE)}>
{panel === Panels.CODE && '>'} CODE EDITOR
<button onClick={() => this.handlePanelButton('code')}>
{panel === 'code' && '>'} CODE EDITOR
</button>
<button onClick={() => this.switchPanel(Panels.JSON)}>
{panel === Panels.JSON && '>'} JSON
<button onClick={() => this.handlePanelButton('json')}>
{panel === 'json' && '>'} JSON
</button>
<button onClick={() => this.switchPanel(Panels.VISUALIZER)}>
{panel === Panels.VISUALIZER && '>'} VISUALIZER
<button onClick={() => this.handlePanelButton('visualizer')}>
{panel === 'visualizer' && '>'} VISUALIZER
</button>
</VerticalFixedSection>
<VerticalExpandableSection>
{panel === Panels.JSON && <JSONEditor player={player} />}
{panel === Panels.CODE && <CODEEditor player={player} />}
{panel === Panels.VISUALIZER && (
<WaveVisualizer player={player} analyser={analyser} />
{panel === 'json' && (
<JSONEditor
player={player}
exampleSlug={exampleSlug}
onExampleChange={this.handleExampleChange}
/>
)}
{panel === 'code' && (
<CODEEditor
player={player}
exampleSlug={exampleSlug}
onExampleChange={this.handleExampleChange}
/>
)}
{panel === 'visualizer' && (
<WaveVisualizer
player={player}
analyser={analyser}
exampleSlug={exampleSlug}
onExampleChange={this.handleExampleChange}
/>
)}
</VerticalExpandableSection>
</VerticalFitArea>
Expand Down
42 changes: 36 additions & 6 deletions demo/src/components/CODEEditor/CODEEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,18 @@ import {

type Props = {
player: SmartPlayer;
exampleSlug: string | undefined;
onExampleChange: (slug: string | undefined) => void;
};

type State = {
example: undefined | ExampleScript;
loading: boolean;
};

const resolveExample = (slug: string | undefined): ExampleScript =>
(slug && examples.find((e) => e.slug === slug)) || examples[0];

const initialState: State = {
example: examples[0],
loading: false,
Expand All @@ -68,12 +73,35 @@ export class CODEEditor extends React.Component<Props, State> {
private getEditorValue: () => string = () => '';

componentDidMount() {
const example = resolveExample(this.props.exampleSlug);
this.setState({
example: {
name: examples[0].name,
script: this.processExample(examples[0]),
slug: example.slug,
name: example.name,
script: this.processExample(example),
},
});
if (example.slug !== this.props.exampleSlug) {
this.props.onExampleChange(example.slug);
}
}

componentDidUpdate(prevProps: Props) {
if (prevProps.exampleSlug !== this.props.exampleSlug) {
const example = resolveExample(this.props.exampleSlug);
if (example.slug !== this.state.example?.slug) {
this.setState({
example: {
slug: example.slug,
name: example.name,
script: this.processExample(example),
},
});
}
if (example.slug !== this.props.exampleSlug) {
this.props.onExampleChange(example.slug);
}
}
}

processExample(example: ExampleScript) {
Expand Down Expand Up @@ -102,12 +130,13 @@ export class CODEEditor extends React.Component<Props, State> {

onExampleSelect = async (event: React.ChangeEvent<HTMLSelectElement>) => {
const example = examples.find(
(example) => example.name === event.target.value,
(example) => example.slug === event.target.value,
);

if (example) {
this.setState({
example: {
slug: example.slug,
name: example.name,
script: this.processExample(example),
},
Expand All @@ -118,13 +147,14 @@ export class CODEEditor extends React.Component<Props, State> {
});
}

const { player } = this.props;
const { player, onExampleChange } = this.props;

if (player.playing) {
player.playing = false;
}

player.renderTime = TimeInstant.ZERO;
onExampleChange(example?.slug);
};

handlePlayPause = () => {
Expand Down Expand Up @@ -176,10 +206,10 @@ export class CODEEditor extends React.Component<Props, State> {
Examples:
<select
onChange={this.onExampleSelect}
value={example ? example.name : undefined}
value={example ? example.slug : undefined}
>
{examples.map((example) => (
<option key={example.name} value={example.name}>
<option key={example.slug} value={example.slug}>
{example.name}
</option>
))}
Expand Down
Loading