feat: Sound
This commit is contained in:
parent
c14cc581f1
commit
60a308a0ed
|
@ -41,6 +41,7 @@
|
||||||
"@persea/core": "^1.0.0",
|
"@persea/core": "^1.0.0",
|
||||||
"@persea/entity": "^1.0.0",
|
"@persea/entity": "^1.0.0",
|
||||||
"@persea/json": "^1.0.0",
|
"@persea/json": "^1.0.0",
|
||||||
|
"@persea/sound": "^1.0.0",
|
||||||
"@persea/timing": "^1.0.0",
|
"@persea/timing": "^1.0.0",
|
||||||
"connected-react-router": "^6.8.0",
|
"connected-react-router": "^6.8.0",
|
||||||
"dotenv": "8.2.0",
|
"dotenv": "8.2.0",
|
||||||
|
|
|
@ -1485,6 +1485,16 @@
|
||||||
fast-json-patch "^3.0.0-1"
|
fast-json-patch "^3.0.0-1"
|
||||||
react-syntax-highlighter "^15.4.3"
|
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":
|
"@persea/timing@^1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "http://npm.cha0sdev/@persea%2ftiming/-/timing-1.0.0.tgz#6163db519682e69f51e914fc7c447961963c7b12"
|
resolved "http://npm.cha0sdev/@persea%2ftiming/-/timing-1.0.0.tgz#6163db519682e69f51e914fc7c447961963c7b12"
|
||||||
|
|
1
packages/sound/.eslintrc.js
Normal file
1
packages/sound/.eslintrc.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('../../config/.eslintrc');
|
7
packages/sound/.gitignore
vendored
Normal file
7
packages/sound/.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
**/*.js
|
||||||
|
**/*.map
|
||||||
|
!/.*
|
||||||
|
!/postcss.config.js
|
||||||
|
!/webpack.config.js
|
||||||
|
!src/**/*.js
|
||||||
|
!/test/**/*.js
|
1
packages/sound/.neutrinorc.js
Normal file
1
packages/sound/.neutrinorc.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('../../config/.neutrinorc');
|
45
packages/sound/package.json
Normal file
45
packages/sound/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
6
packages/sound/postcss.config.js
Normal file
6
packages/sound/postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/* eslint-disable global-require */
|
||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
require('autoprefixer'),
|
||||||
|
],
|
||||||
|
};
|
27
packages/sound/src/index.js
Normal file
27
packages/sound/src/index.js
Normal 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,
|
||||||
|
}), {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
42
packages/sound/src/resource-controllers/audio/index.jsx
Normal file
42
packages/sound/src/resource-controllers/audio/index.jsx
Normal 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)$/;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
5
packages/sound/src/resource-controllers/audio/index.scss
Normal file
5
packages/sound/src/resource-controllers/audio/index.scss
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.audio {
|
||||||
|
height: 100%;
|
||||||
|
padding: 1em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
112
packages/sound/src/resource-controllers/sound/index.jsx
Normal file
112
packages/sound/src/resource-controllers/sound/index.jsx
Normal 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$/;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
38
packages/sound/src/resource-controllers/sound/index.scss
Normal file
38
packages/sound/src/resource-controllers/sound/index.scss
Normal 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;
|
||||||
|
}
|
65
packages/sound/src/resource-controllers/sound/src.jsx
Normal file
65
packages/sound/src/resource-controllers/sound/src.jsx
Normal 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;
|
106
packages/sound/src/trait-components/audible.jsx
Normal file
106
packages/sound/src/trait-components/audible.jsx
Normal 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;
|
16
packages/sound/src/trait-components/audible.scss
Normal file
16
packages/sound/src/trait-components/audible.scss
Normal 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;
|
||||||
|
}
|
5
packages/sound/test/exists.js
Normal file
5
packages/sound/test/exists.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import {expect} from 'chai';
|
||||||
|
|
||||||
|
it('exists', () => {
|
||||||
|
expect(true).to.be.true;
|
||||||
|
});
|
3
packages/sound/webpack.config.js
Normal file
3
packages/sound/webpack.config.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
const neutrino = require('neutrino');
|
||||||
|
|
||||||
|
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).webpack();
|
8951
packages/sound/yarn.lock
Normal file
8951
packages/sound/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user