feat: Sound

This commit is contained in:
cha0s 2021-02-03 00:41:36 -06:00
parent c14cc581f1
commit 60a308a0ed
18 changed files with 9441 additions and 0 deletions

View File

@ -41,6 +41,7 @@
"@persea/core": "^1.0.0",
"@persea/entity": "^1.0.0",
"@persea/json": "^1.0.0",
"@persea/sound": "^1.0.0",
"@persea/timing": "^1.0.0",
"connected-react-router": "^6.8.0",
"dotenv": "8.2.0",

View File

@ -1485,6 +1485,16 @@
fast-json-patch "^3.0.0-1"
react-syntax-highlighter "^15.4.3"
"@persea/sound@^1.0.0":
version "1.0.0"
resolved "http://npm.cha0sdev/@persea%2fsound/-/sound-1.0.0.tgz#fde466c89f24323311145d26150f3e74bfbdbbc6"
integrity sha512-T1r7Vxt3DJljcrRv9MiVviS66GDy2BPM2Y4jDkGc0J8heleiJtxCUDr2k/qQN1lMFVWcUteR87yhLAzmzOGYKw==
dependencies:
"@latus/core" "^2.0.0"
"@latus/react" "^2.0.0"
"@persea/core" "^1.0.0"
debug "4.3.1"
"@persea/timing@^1.0.0":
version "1.0.0"
resolved "http://npm.cha0sdev/@persea%2ftiming/-/timing-1.0.0.tgz#6163db519682e69f51e914fc7c447961963c7b12"

View File

@ -0,0 +1 @@
module.exports = require('../../config/.eslintrc');

7
packages/sound/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
**/*.js
**/*.map
!/.*
!/postcss.config.js
!/webpack.config.js
!src/**/*.js
!/test/**/*.js

View File

@ -0,0 +1 @@
module.exports = require('../../config/.neutrinorc');

View File

@ -0,0 +1,45 @@
{
"name": "@persea/sound",
"version": "1.0.0",
"main": "index.js",
"author": "cha0s",
"license": "MIT",
"scripts": {
"build": "NODE_PATH=./node_modules webpack --mode production",
"clean": "rm -rf yarn.lock node_modules $(node -e \"process.stdout.write(require('./package.json').files.filter((file) => {const parts = file.split('/'); return 1 === parts.length || 'test' !== parts[0];}).join(' '));\") && yarn",
"forcepub": "npm unpublish --force $(node -e 'const {name, version} = require(`./package.json`); process.stdout.write(`${name}@${version}`)') && npm publish",
"lint": "NODE_PATH=./node_modules eslint --format codeframe --ext mjs,js .",
"test": "yarn --silent run build --display none && mocha --colors test.js"
},
"files": [
"index.js",
"index.js.map",
"test.js",
"test.js.map"
],
"dependencies": {
"@avocado/resource": "^2.0.0",
"@latus/core": "^2.0.0",
"@latus/react": "^2.0.0",
"@persea/core": "^1.0.0",
"@persea/json": "^1.0.0",
"debug": "4.3.1"
},
"devDependencies": {
"@neutrinojs/airbnb": "^9.4.0",
"@neutrinojs/banner": "^9.4.0",
"@neutrinojs/copy": "^9.4.0",
"@neutrinojs/mocha": "^9.4.0",
"@neutrinojs/react": "^9.4.0",
"autoprefixer": "^9.8.6",
"chai": "4.2.0",
"eslint": "^7",
"eslint-import-resolver-webpack": "0.13.0",
"glob": "7.1.6",
"mocha": "^8",
"neutrino": "^9.4.0",
"source-map-support": "0.5.19",
"webpack": "^4",
"webpack-cli": "^3"
}
}

View File

@ -0,0 +1,6 @@
/* eslint-disable global-require */
module.exports = {
plugins: [
require('autoprefixer'),
],
};

View File

@ -0,0 +1,27 @@
import {basename, extname} from 'path';
import {camelCase} from '@latus/core';
import AudioController from './resource-controllers/audio';
import SoundController from './resource-controllers/sound';
export {
AudioController,
SoundController,
};
export default {
hooks: {
'@persea/core/resource-controllers': () => [
AudioController,
SoundController,
],
'@persea/entity/trait-components': () => {
const context = require.context('./trait-components', false, /\.jsx$/);
return context.keys().reduce((r, key) => ({
...r,
[camelCase(basename(key, extname(key)))]: context(key).default,
}), {});
},
},
};

View File

@ -0,0 +1,42 @@
import {
PropTypes,
React,
} from '@latus/react';
import {
BinaryResourceController,
} from '@persea/core';
export const AudioComponent = ({resource}) => {
// eslint-disable-next-line no-use-before-define
const buffer = AudioController.decode(resource);
const objectUrl = URL.createObjectURL(new Blob([buffer]));
return (
<div className="audio">
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<audio
controls
src={objectUrl}
/>
</div>
);
};
AudioComponent.displayName = 'AudioComponent';
AudioComponent.propTypes = {
resource: PropTypes.string.isRequired,
};
export default class AudioController extends BinaryResourceController {
static Component({resource}) {
return (
<AudioComponent resource={resource} />
);
}
static get matcher() {
return /\.(?:wav|ogg|mp3)$/;
}
}

View File

@ -0,0 +1,5 @@
.audio {
height: 100%;
padding: 1em;
width: 100%;
}

View File

@ -0,0 +1,112 @@
import {join} from 'path';
import {
PropTypes,
React,
} from '@latus/react';
import {
Number,
} from '@persea/core';
import {
JsonResourceController,
useJsonPatcher,
} from '@persea/json';
import SoundSource from './src';
export const SoundComponent = ({
path,
resource,
}) => {
const patch = useJsonPatcher();
const sources = (resource.src || []).map((src, i) => (
<div className="sound__audio" key={src}>
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<input
className="sound__src"
onChange={(event) => {
patch({
path: join(path, 'src', i.toString()),
value: event.target.value,
});
}}
type="text"
value={src}
/>
<SoundSource src={src} volume={resource.volume} />
<button
className="sound__source-remove"
onClick={() => {
patch({
op: 'remove',
path: join(path, 'src', i.toString()),
});
}}
type="button"
>
</button>
</div>
));
return (
<div className="sound">
<div className="sound__sources-label label">
<div className="vertical">Sources</div>
<div className="sound__sources">
{sources}
<hr />
<button
className="sound__source-add"
onClick={() => {
patch({
op: 'add',
path: join(path, 'src/-'),
value: '',
});
}}
type="button"
>
Add source
</button>
</div>
</div>
<div className="label">
Volume
<Number
onChange={(event, value) => {
patch({
path: join(path, 'volume'),
value,
});
}}
value={resource.volume || 1}
/>
</div>
</div>
);
};
SoundComponent.defaultProps = {
path: '/',
};
SoundComponent.displayName = 'SoundComponent';
SoundComponent.propTypes = {
path: PropTypes.string,
resource: PropTypes.string.isRequired,
};
export default class SoundController extends JsonResourceController {
static Component({resource}) {
return (
<SoundComponent resource={resource} />
);
}
static get matcher() {
return /\.sound\.json$/;
}
}

View File

@ -0,0 +1,38 @@
.sound {
padding: 1em;
hr {
flex-basis: 100%;
height: 0;
margin: 0;
border: 0;
}
}
// .sound__sources-label.label {
// flex-wrap: wrap;
// }
.sound__audio {
align-items: center;
display: flex;
padding: 0.5em;
width: 100%;
}
.sound-source button {
margin-left: 0.5em;
}
.sound__source-remove {
background-color: transparent;
border: none;
height: 2em;
margin: 0;
margin-left: auto;
padding: 0;
width: 2em;
}
.sound__source-add {
margin: 0.5em;
}

View File

@ -0,0 +1,65 @@
import {Resource} from '@avocado/resource';
import {
PropTypes,
React,
useEffect,
useRef,
useState,
} from '@latus/react';
const SoundSource = ({
src,
volume,
}) => {
const ref = useRef();
const [url, setUrl] = useState();
useEffect(() => {
const loadAudio = async () => {
setUrl(URL.createObjectURL(new Blob([await Resource.read(src)])));
};
loadAudio();
return () => {
URL.revokeObjectURL(url);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [src]);
useEffect(() => {
if (!ref.current) {
return;
}
ref.current.volume = volume;
});
return (
<div className="sound-source">
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<audio
ref={ref}
src={url}
/>
<button
onClick={() => {
if (ref.current) {
ref.current.currentTime = 0;
ref.current.play();
}
}}
type="button"
>
</button>
</div>
);
};
SoundSource.defaultProps = {
volume: 1.0,
};
SoundSource.displayName = 'SoundSource';
SoundSource.propTypes = {
src: PropTypes.string.isRequired,
volume: PropTypes.number,
};
export default SoundSource;

View File

@ -0,0 +1,106 @@
import {join} from 'path';
import {PropTypes, React} from '@latus/react';
import {useJsonPatcher} from '@persea/json';
const Audible = ({json, path}) => {
const patch = useJsonPatcher();
const soundsPath = join(path, 'params/sounds');
const sounds = Object.entries(json.params.sounds).map(([key, sound]) => (
<div className="audible__sound" key={key}>
<label>
Key
<input
onChange={(event) => {
patch({
op: 'move',
from: join(soundsPath, key),
path: join(soundsPath, event.target.value),
});
}}
type="text"
value={key}
/>
</label>
<label>
URI
<input
onChange={(event) => {
if (event.target.value) {
patch({
path: join(soundsPath, key, 'extends'),
value: event.target.value,
});
}
}}
type="text"
value={sound.extends}
/>
</label>
<button
className="audible__sound-remove"
onClick={() => {
patch({
op: 'remove',
path: join(soundsPath, key),
});
}}
type="button"
>
</button>
</div>
));
return (
<div className="audible">
<div className="label">
<div className="vertical">Sounds</div>
<div className="audible__sounds">
{sounds}
<button
onClick={() => {
let i = 1;
let name = 'untitled';
while (json.params.sounds[name]) {
name = `untitled (${i++})`;
}
patch({
op: 'add',
path: join(soundsPath, name),
value: {
extends: '',
},
});
}}
type="button"
>
Add a sound
</button>
</div>
</div>
</div>
);
};
Audible.defaultProps = {};
Audible.displayName = 'Audible';
Audible.propTypes = {
entity: PropTypes.shape({
context: PropTypes.shape({}),
}).isRequired,
json: PropTypes.shape({
params: PropTypes.shape({
sounds: PropTypes.objectOf(PropTypes.shape({
uri: PropTypes.string,
})),
}),
state: PropTypes.shape({
}),
}).isRequired,
path: PropTypes.string.isRequired,
};
Audible.propTypes = {};
export default Audible;

View File

@ -0,0 +1,16 @@
.audible__sound {
> label {
display: inline-flex;
width: calc(50% - 1.5em);
}
}
.audible__sound-remove {
background-color: transparent;
border: none;
height: 2em;
margin: 0;
margin-left: 0.5em;
padding: 0;
width: 2em;
}

View File

@ -0,0 +1,5 @@
import {expect} from 'chai';
it('exists', () => {
expect(true).to.be.true;
});

View File

@ -0,0 +1,3 @@
const neutrino = require('neutrino');
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).webpack();

8951
packages/sound/yarn.lock Normal file

File diff suppressed because it is too large Load Diff