├── apps ├── .gitkeep └── waveform │ ├── src │ ├── assets │ │ ├── .gitkeep │ │ └── examples │ │ │ ├── bass.wav │ │ │ ├── kick.ogg │ │ │ ├── square.mp3 │ │ │ └── random-noise-processor.js │ ├── app │ │ ├── synth │ │ │ ├── services │ │ │ │ ├── index.ts │ │ │ │ └── note.ts │ │ │ ├── voicing │ │ │ │ ├── index.ts │ │ │ │ └── components │ │ │ │ │ ├── index.ts │ │ │ │ │ └── voicing-section.tsx │ │ │ ├── settings │ │ │ │ ├── components │ │ │ │ │ ├── index.ts │ │ │ │ │ └── midi-input.tsx │ │ │ │ └── index.tsx │ │ │ ├── filter │ │ │ │ ├── components │ │ │ │ │ ├── index.ts │ │ │ │ │ └── filter-section.tsx │ │ │ │ ├── modules │ │ │ │ │ └── index.ts │ │ │ │ ├── constants │ │ │ │ │ ├── index.ts │ │ │ │ │ └── ranges.ts │ │ │ │ └── index.ts │ │ │ ├── synth │ │ │ │ └── components │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oscillators-container.tsx │ │ │ ├── wave-table-editor │ │ │ │ ├── audiofile-wavetable │ │ │ │ │ ├── components │ │ │ │ │ │ ├── load-file │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── load-file.tsx │ │ │ │ │ │ │ └── examples.tsx │ │ │ │ │ │ ├── audiofile-wave-picker │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── pickers.tsx │ │ │ │ │ │ │ └── audiofile-wave-picker.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── audiofile-wavetable.tsx │ │ │ │ │ │ └── wave-preview.tsx │ │ │ │ │ ├── modules │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── index.tsx │ │ │ │ ├── manual-wavetable │ │ │ │ │ ├── components │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── manual-wavetable.tsx │ │ │ │ │ ├── modules │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── manual-wavetable.ts │ │ │ │ │ └── index.tsx │ │ │ │ ├── common │ │ │ │ │ └── components │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── wave-editor.tsx │ │ │ │ │ │ └── wave-selector │ │ │ │ │ │ ├── waves-preview.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── oscillator │ │ │ │ ├── components │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── wave │ │ │ │ │ │ ├── chart.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── wave-selector.tsx │ │ │ │ └── hooks │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── use-oscillator-context.ts │ │ │ │ │ ├── use-wavetables.ts │ │ │ │ │ └── use-wavetable.ts │ │ │ └── common │ │ │ │ └── modules │ │ │ │ ├── index.ts │ │ │ │ ├── adsr-envelope.ts │ │ │ │ ├── input-controller.ts │ │ │ │ ├── keyboard-controller.ts │ │ │ │ └── synth-core.ts │ │ ├── common │ │ │ ├── constants │ │ │ │ ├── index.ts │ │ │ │ └── note-frequency.ts │ │ │ └── components │ │ │ │ ├── index.ts │ │ │ │ ├── rx-handle.tsx │ │ │ │ └── header.tsx │ │ ├── wave-table-editor │ │ │ ├── manual-wavetable │ │ │ │ ├── modules │ │ │ │ │ ├── index.ts │ │ │ │ │ └── manual-wavetable.ts │ │ │ │ ├── components │ │ │ │ │ ├── index.ts │ │ │ │ │ └── manual-wavetable.tsx │ │ │ │ ├── wave-upscale │ │ │ │ │ ├── components │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── output-wave.tsx │ │ │ │ │ │ └── controls.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── modules │ │ │ │ │ │ └── index.ts │ │ │ │ └── index.tsx │ │ │ ├── audiofile-wavetable │ │ │ │ ├── components │ │ │ │ │ ├── load-file │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── load-file.tsx │ │ │ │ │ │ └── examples.tsx │ │ │ │ │ ├── audiofile-wave-picker │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── pickers.tsx │ │ │ │ │ │ └── audiofile-wave-picker.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── audiofile-wavetable.tsx │ │ │ │ │ └── wave-preview.tsx │ │ │ │ ├── modules │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── audiofile-wavetable.ts │ │ │ │ │ └── audiofile.ts │ │ │ │ └── index.tsx │ │ │ ├── common │ │ │ │ ├── components │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── wave-selector │ │ │ │ │ │ ├── wave.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── wave-editor.tsx │ │ │ │ └── modules │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── wavetable.ts │ │ │ │ │ └── audio-processor-module.ts │ │ │ ├── index.tsx │ │ │ ├── wave-table-editor │ │ │ │ └── index.tsx │ │ │ └── header │ │ │ │ ├── play-button │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ ├── app │ │ │ ├── modules │ │ │ │ ├── index.ts │ │ │ │ └── app-module.tsx │ │ │ ├── plugins │ │ │ │ └── snapshot.ts │ │ │ └── index.tsx │ │ └── index.tsx │ ├── favicon.ico │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── main.tsx │ ├── polyfills.ts │ └── index.html │ ├── .babelrc │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── .browserslistrc │ ├── tsconfig.spec.json │ ├── tsconfig.json │ ├── tsconfig.app.json │ └── project.json ├── libs ├── .gitkeep ├── math │ ├── src │ │ ├── number │ │ │ ├── index.ts │ │ │ └── number.ts │ │ ├── polyfill │ │ │ ├── index.ts │ │ │ └── request-idle-callback.ts │ │ ├── series │ │ │ ├── index.ts │ │ │ ├── typed-array.ts │ │ │ └── wave.ts │ │ ├── vector │ │ │ ├── index.ts │ │ │ └── vector2d.ts │ │ ├── string │ │ │ └── index.ts │ │ └── index.ts │ ├── .babelrc │ ├── tsconfig.lib.json │ ├── README.md │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── project.json ├── ui-kit │ ├── src │ │ ├── common │ │ │ ├── constants │ │ │ │ ├── index.ts │ │ │ │ └── theme.ts │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ └── use-screen-size.ts │ │ │ ├── styles │ │ │ │ ├── index.ts │ │ │ │ ├── absolute.ts │ │ │ │ ├── global-style.ts │ │ │ │ ├── typography.ts │ │ │ │ └── states.ts │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── line-chart │ │ │ ├── constants │ │ │ │ ├── index.ts │ │ │ │ └── context.ts │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ └── use-line-chart-context.ts │ │ │ ├── index.tsx │ │ │ └── components │ │ │ │ ├── index.ts │ │ │ │ ├── square-dots.tsx │ │ │ │ ├── y-axis.tsx │ │ │ │ ├── line.tsx │ │ │ │ ├── x-axis.tsx │ │ │ │ └── line-chart.tsx │ │ ├── filter │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ └── filter-linechart.tsx │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ ├── index.tsx │ │ │ └── services │ │ │ │ └── index.ts │ │ ├── wave-drawer │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ └── mouse-interactive.tsx │ │ │ └── index.tsx │ │ ├── label │ │ │ └── index.tsx │ │ ├── icon-button │ │ │ └── index.tsx │ │ ├── piano-keyboard │ │ │ ├── index.tsx │ │ │ └── octave.tsx │ │ ├── section │ │ │ └── index.tsx │ │ ├── index.ts │ │ ├── select │ │ │ ├── cascade-select.tsx │ │ │ ├── index.tsx │ │ │ └── select-dumb.tsx │ │ ├── modal │ │ │ └── index.tsx │ │ ├── file-drop │ │ │ └── index.tsx │ │ ├── checkbox │ │ │ └── index.tsx │ │ ├── tabs │ │ │ └── index.tsx │ │ ├── analyser │ │ │ └── index.tsx │ │ └── draggable-number │ │ │ └── index.tsx │ ├── README.md │ ├── .babelrc │ ├── jest.config.ts │ ├── .eslintrc.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── tsconfig.json │ └── project.json └── rxjs-react │ ├── src │ ├── types │ │ ├── index.ts │ │ ├── observables.ts │ │ └── rx-model.ts │ ├── plugins │ │ ├── snapshot │ │ │ ├── index.ts │ │ │ ├── services.ts │ │ │ └── plugin.ts │ │ ├── index.ts │ │ └── search-params-snapshot │ │ │ └── index.ts │ ├── services │ │ ├── index.ts │ │ ├── rx-model-react.tsx │ │ ├── rx-model.ts │ │ └── __tests__ │ │ │ └── rx-model.spec.tsx │ ├── observables │ │ ├── index.ts │ │ ├── primitive-bs.ts │ │ ├── array-bs.ts │ │ └── object-bs.ts │ ├── utils │ │ └── index.ts │ ├── hooks │ │ ├── index.ts │ │ ├── use-behavior-subject.ts │ │ ├── use-nullable-context.ts │ │ ├── use-observable.ts │ │ └── use-rx-model.ts │ └── index.ts │ ├── README.md │ ├── .babelrc │ ├── jest.config.ts │ ├── .eslintrc.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── tsconfig.json │ └── project.json ├── tools ├── generators │ └── .gitkeep └── tsconfig.tools.json ├── babel.config.json ├── screenshot.png ├── .prettierignore ├── .prettierrc ├── jest.preset.js ├── proxy.conf.json ├── jest.config.ts ├── .vscode └── extensions.json ├── .editorconfig ├── README.md ├── netlify ├── utils │ └── index.ts └── functions │ ├── get-wavetable.ts │ └── get-wavetables-list.ts ├── .gitignore ├── tsconfig.base.json ├── .eslintrc.json ├── nx.json └── package.json /apps/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/generators/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/waveform/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "babelrcRoots": ["*"] 3 | } 4 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './note'; 2 | -------------------------------------------------------------------------------- /libs/math/src/number/index.ts: -------------------------------------------------------------------------------- 1 | export * as number from './number'; 2 | -------------------------------------------------------------------------------- /libs/ui-kit/src/common/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './theme'; 2 | -------------------------------------------------------------------------------- /libs/math/src/polyfill/index.ts: -------------------------------------------------------------------------------- 1 | export * from './request-idle-callback'; 2 | -------------------------------------------------------------------------------- /libs/ui-kit/src/line-chart/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context'; 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qvantor/waveform/HEAD/screenshot.png -------------------------------------------------------------------------------- /libs/ui-kit/src/common/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useScreenSize } from './use-screen-size'; 2 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/voicing/index.ts: -------------------------------------------------------------------------------- 1 | export { VoicingSection } from './components'; 2 | -------------------------------------------------------------------------------- /libs/math/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] 3 | } 4 | -------------------------------------------------------------------------------- /libs/ui-kit/src/filter/components/index.ts: -------------------------------------------------------------------------------- 1 | export { FilterRouter } from './filter-router'; 2 | -------------------------------------------------------------------------------- /apps/waveform/src/app/common/constants/index.ts: -------------------------------------------------------------------------------- 1 | export { noteFrequency } from './note-frequency'; 2 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/settings/components/index.ts: -------------------------------------------------------------------------------- 1 | export { MidiInput } from './midi-input'; 2 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rx-model'; 2 | export * from './observables'; 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 110, 4 | "jsxSingleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/filter/components/index.ts: -------------------------------------------------------------------------------- 1 | export { FilterSection } from './filter-section'; 2 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/plugins/snapshot/index.ts: -------------------------------------------------------------------------------- 1 | export * from './services'; 2 | export * from './plugin'; 3 | -------------------------------------------------------------------------------- /libs/ui-kit/src/wave-drawer/components/index.ts: -------------------------------------------------------------------------------- 1 | export { MouseInteractive } from './mouse-interactive'; 2 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/filter/modules/index.ts: -------------------------------------------------------------------------------- 1 | export { FilterProvider, useFilter } from './filter'; 2 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/voicing/components/index.ts: -------------------------------------------------------------------------------- 1 | export { VoicingSection } from './voicing-section'; 2 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/manual-wavetable/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './manual-wavetable'; 2 | -------------------------------------------------------------------------------- /apps/waveform/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qvantor/waveform/HEAD/apps/waveform/src/favicon.ico -------------------------------------------------------------------------------- /libs/rxjs-react/src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export { searchParamsSnapshotPlugin } from './search-params-snapshot'; 2 | -------------------------------------------------------------------------------- /libs/ui-kit/src/line-chart/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useLineChartContext } from './use-line-chart-context'; 2 | -------------------------------------------------------------------------------- /apps/waveform/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/math/src/series/index.ts: -------------------------------------------------------------------------------- 1 | export * as wave from './wave'; 2 | export * as typedArray from './typed-array'; 3 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/filter/constants/index.ts: -------------------------------------------------------------------------------- 1 | export { defaultFilterRanges, filterRanges } from './ranges'; 2 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/synth/components/index.ts: -------------------------------------------------------------------------------- 1 | export { OscillatorsContainer } from './oscillators-container'; 2 | -------------------------------------------------------------------------------- /libs/math/src/vector/index.ts: -------------------------------------------------------------------------------- 1 | export type { Vector2D } from './vector2d'; 2 | export { vector2d } from './vector2d'; 3 | -------------------------------------------------------------------------------- /libs/ui-kit/src/line-chart/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | export { useLineChartContext } from './hooks'; 3 | -------------------------------------------------------------------------------- /libs/math/src/string/index.ts: -------------------------------------------------------------------------------- 1 | export const generateId = () => Date.now().toString(36) + Math.random().toString(36).slice(2); 2 | -------------------------------------------------------------------------------- /proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/.netlify": { 3 | "target": "http://localhost:8888/", 4 | "secure": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/waveform/src/app/common/components/index.ts: -------------------------------------------------------------------------------- 1 | export { RxHandle } from './rx-handle'; 2 | export { Header } from './header'; 3 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/audiofile-wavetable/components/load-file/index.ts: -------------------------------------------------------------------------------- 1 | export { LoadFile } from './load-file'; 2 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjects } from '@nrwl/jest'; 2 | 3 | export default { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export { rxModelReact } from './rx-model-react'; 2 | export { rxModel } from './rx-model'; 3 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/manual-wavetable/components/index.ts: -------------------------------------------------------------------------------- 1 | export { ManualWavetable } from './manual-wavetable'; 2 | -------------------------------------------------------------------------------- /apps/waveform/src/assets/examples/bass.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qvantor/waveform/HEAD/apps/waveform/src/assets/examples/bass.wav -------------------------------------------------------------------------------- /apps/waveform/src/assets/examples/kick.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qvantor/waveform/HEAD/apps/waveform/src/assets/examples/kick.ogg -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/filter/index.ts: -------------------------------------------------------------------------------- 1 | export { FilterProvider } from './modules'; 2 | export { FilterSection } from './components'; 3 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/wave-table-editor/audiofile-wavetable/components/load-file/index.ts: -------------------------------------------------------------------------------- 1 | export { LoadFile } from './load-file'; 2 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/wave-table-editor/manual-wavetable/components/index.ts: -------------------------------------------------------------------------------- 1 | export { ManualWavetable } from './manual-wavetable'; 2 | -------------------------------------------------------------------------------- /apps/waveform/src/assets/examples/square.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qvantor/waveform/HEAD/apps/waveform/src/assets/examples/square.mp3 -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/oscillator/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Wave } from './wave'; 2 | export { WaveSelector } from './wave-selector'; 3 | -------------------------------------------------------------------------------- /apps/waveform/src/app/app/modules/index.ts: -------------------------------------------------------------------------------- 1 | export { AppProvider, useApp } from './app-module'; 2 | export type { AppModule } from './app-module'; 3 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/wave-table-editor/audiofile-wavetable/modules/index.ts: -------------------------------------------------------------------------------- 1 | export { AudiofileProvider, useAudiofile } from './audiofile'; 2 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/audiofile-wavetable/components/audiofile-wave-picker/index.ts: -------------------------------------------------------------------------------- 1 | export { AudiofileWavePicker } from './audiofile-wave-picker'; 2 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/common/components/index.ts: -------------------------------------------------------------------------------- 1 | export { WaveSelector } from './wave-selector'; 2 | export { WaveEditor } from './wave-editor'; 3 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/wave-table-editor/common/components/index.ts: -------------------------------------------------------------------------------- 1 | export { WaveSelector } from './wave-selector'; 2 | export { WaveEditor } from './wave-editor'; 3 | -------------------------------------------------------------------------------- /libs/ui-kit/src/common/styles/index.ts: -------------------------------------------------------------------------------- 1 | export * from './global-style'; 2 | export * from './typography'; 3 | export * from './absolute'; 4 | export * from './states'; 5 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/wave-table-editor/audiofile-wavetable/components/audiofile-wave-picker/index.ts: -------------------------------------------------------------------------------- 1 | export { AudiofileWavePicker } from './audiofile-wave-picker'; 2 | -------------------------------------------------------------------------------- /libs/math/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './vector'; 2 | export * from './number'; 3 | export * from './series'; 4 | export * from './string'; 5 | export * from './polyfill'; 6 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/observables/index.ts: -------------------------------------------------------------------------------- 1 | export { PrimitiveBS } from './primitive-bs'; 2 | export { ArrayBS } from './array-bs'; 3 | export { ObjectBS } from './object-bs'; 4 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/manual-wavetable/wave-upscale/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Controls } from './controls'; 2 | export { OutputWave } from './output-wave'; 3 | -------------------------------------------------------------------------------- /libs/ui-kit/src/common/types/index.ts: -------------------------------------------------------------------------------- 1 | export type Notes = 'C' | 'C#' | 'D' | 'D#' | 'E' | 'F' | 'F#' | 'G' | 'G#' | 'A' | 'A#' | 'B'; 2 | 3 | export type Note = [number, Notes]; 4 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const isRecord = (value: unknown): value is Record => 2 | typeof value === 'object' && value !== null && !Array.isArray(value); 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "firsttris.vscode-jest-runner", 6 | "dbaeumer.vscode-eslint" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /apps/waveform/src/app/app/plugins/snapshot.ts: -------------------------------------------------------------------------------- 1 | import { searchParamsSnapshotPlugin } from '@waveform/rxjs-react'; 2 | 3 | export const { plugin: urlSnapshotPlugin, saveUrlSnapshot } = searchParamsSnapshotPlugin(); 4 | -------------------------------------------------------------------------------- /libs/ui-kit/README.md: -------------------------------------------------------------------------------- 1 | # ui-kit 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test ui-kit` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /libs/rxjs-react/README.md: -------------------------------------------------------------------------------- 1 | # rxjs-react 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test rxjs-react` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /libs/math/src/series/typed-array.ts: -------------------------------------------------------------------------------- 1 | export const slice = (array: Float32Array, from: number, count: number) => { 2 | const slice = []; 3 | for (let i = from; i < from + count; i++) slice.push(array[i]); 4 | return slice; 5 | }; 6 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/oscillator/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useOscillatorContext, OscillatorContext } from './use-oscillator-context'; 2 | export { useWavetables } from './use-wavetables'; 3 | export { useWavetable } from './use-wavetable'; 4 | -------------------------------------------------------------------------------- /libs/ui-kit/src/line-chart/components/index.ts: -------------------------------------------------------------------------------- 1 | export { LineChart } from './line-chart'; 2 | export { XAxis } from './x-axis'; 3 | export { YAxis } from './y-axis'; 4 | export { Line } from './line'; 5 | export { SquareDots } from './square-dots'; 6 | -------------------------------------------------------------------------------- /apps/waveform/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nrwl/react/babel", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ] 9 | ], 10 | "plugins": [["styled-components", { "pure": true, "ssr": true }]] 11 | } 12 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/audiofile-wavetable/modules/index.ts: -------------------------------------------------------------------------------- 1 | export { AudiofileWavePickerProvider, useAudiofileWavePicker } from './audiofile'; 2 | export { AudiofileWavetableProvider, useAudiofileWavetable } from './audiofile-wavetable'; 3 | -------------------------------------------------------------------------------- /apps/waveform/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as ReactDOM from 'react-dom/client'; 3 | import { Core } from './app'; 4 | 5 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); 6 | root.render(); 7 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useNullableContext } from './use-nullable-context'; 2 | export { useObservable } from './use-observable'; 3 | export { useBehaviorSubject } from './use-behavior-subject'; 4 | export { useRxModel } from './use-rx-model'; 5 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './services'; 2 | export * from './hooks'; 3 | export { searchParamsSnapshotPlugin } from './plugins'; 4 | export { PrimitiveBS, ArrayBS, ObjectBS } from './observables'; 5 | 6 | export type { Model } from './types'; 7 | -------------------------------------------------------------------------------- /apps/waveform/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Polyfill stable language features. These imports will be optimized by `@babel/preset-env`. 3 | * 4 | * See: https://github.com/zloirock/core-js#babel 5 | */ 6 | import 'core-js/stable'; 7 | import 'regenerator-runtime/runtime'; 8 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/wave-table-editor/manual-wavetable/modules/index.ts: -------------------------------------------------------------------------------- 1 | export { ManualWavetableProvider, useManualWavetable } from './manual-wavetable'; 2 | export type { ManualWavetableModule, ManualWavetableModel, ManualWavetableActions } from './manual-wavetable'; 3 | -------------------------------------------------------------------------------- /apps/waveform/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // When building for production, this file is replaced with `environment.prod.ts`. 3 | 4 | export const environment = { 5 | production: false, 6 | }; 7 | -------------------------------------------------------------------------------- /libs/math/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": [] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["jest.config.ts", "**/*.spec.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/ui-kit/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nrwl/react/babel", 5 | { 6 | "runtime": "automatic", 7 | "useBuiltIns": "usage" 8 | } 9 | ] 10 | ], 11 | "plugins": [["styled-components", { "pure": true, "ssr": true }]] 12 | } 13 | -------------------------------------------------------------------------------- /libs/rxjs-react/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nrwl/react/babel", 5 | { 6 | "runtime": "automatic", 7 | "useBuiltIns": "usage" 8 | } 9 | ] 10 | ], 11 | "plugins": [["styled-components", { "pure": true, "ssr": true }]] 12 | } 13 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/hooks/use-behavior-subject.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from 'rxjs'; 2 | import { useObservable } from './use-observable'; 3 | 4 | export const useBehaviorSubject = (behaviorSubject: BehaviorSubject): T => 5 | useObservable(behaviorSubject, behaviorSubject.value); 6 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/audiofile-wavetable/components/index.ts: -------------------------------------------------------------------------------- 1 | export { LoadFile } from './load-file/load-file'; 2 | export { AudiofileWavePicker } from './audiofile-wave-picker'; 3 | export { AudiofileWavetable } from './audiofile-wavetable'; 4 | export { WavePreview } from './wave-preview'; 5 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/wave-table-editor/audiofile-wavetable/components/index.ts: -------------------------------------------------------------------------------- 1 | export { LoadFile } from './load-file/load-file'; 2 | export { AudiofileWavePicker } from './audiofile-wave-picker'; 3 | export { AudiofileWavetable } from './audiofile-wavetable'; 4 | export { WavePreview } from './wave-preview'; 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /libs/math/README.md: -------------------------------------------------------------------------------- 1 | # math 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test math` to execute the unit tests via [Jest](https://jestjs.io). 8 | 9 | ## Running lint 10 | 11 | Run `nx lint math` to execute the lint via [ESLint](https://eslint.org/). 12 | -------------------------------------------------------------------------------- /libs/ui-kit/src/common/styles/absolute.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | export const absoluteCenterXY = css` 4 | top: 50%; 5 | left: 50%; 6 | transform: translate(-50%, -50%); 7 | `; 8 | 9 | export const absoluteCenterX = css` 10 | left: 50%; 11 | transform: translate(-50%, 0); 12 | `; 13 | -------------------------------------------------------------------------------- /libs/ui-kit/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'ui-kit', 4 | preset: '../../jest.preset.js', 5 | transform: { 6 | '^.+\\.[tj]sx?$': 'babel-jest', 7 | }, 8 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 9 | coverageDirectory: '../../coverage/libs/ui-kit', 10 | }; 11 | -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /libs/rxjs-react/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'rxjs-react', 4 | preset: '../../jest.preset.js', 5 | transform: { 6 | '^.+\\.[tj]sx?$': 'babel-jest', 7 | }, 8 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 9 | coverageDirectory: '../../coverage/libs/rxjs-react', 10 | }; 11 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/common/modules/index.ts: -------------------------------------------------------------------------------- 1 | export { wavetable } from './wavetable'; 2 | export type { WavetableModel, WavetableActions } from './wavetable'; 3 | 4 | export { AudioProcessorProvider, useAudioProcessor } from './audio-processor-module'; 5 | export type { AudioProcessorModule } from './audio-processor-module'; 6 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/manual-wavetable/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ManualWavetableProvider } from './modules'; 3 | import { ManualWavetable } from './components'; 4 | 5 | export default () => ( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/types/observables.ts: -------------------------------------------------------------------------------- 1 | export type SnapshotType = 'PrimitiveBS' | 'ArrayBS' | 'ObjectBS'; 2 | export type SnapshotValue = { _type_: SnapshotType; value: V }; 3 | 4 | export interface Snapshotable { 5 | readonly __snapshotable: true; 6 | getSnapshot: () => unknown; 7 | setSnapshot: (value: SnapshotValue) => void; 8 | } 9 | -------------------------------------------------------------------------------- /libs/ui-kit/src/line-chart/hooks/use-line-chart-context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LineChartContext } from '../constants'; 3 | 4 | export const useLineChartContext = () => { 5 | const context = React.useContext(LineChartContext); 6 | if (context === null) throw new Error('useLineChartContext should be used inside of LineChart component'); 7 | return context; 8 | }; 9 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/hooks/use-nullable-context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const useNullableContext = (context: React.Context, name?: string): T => { 4 | const notNullContext = React.useContext(context); 5 | if (notNullContext === null) 6 | throw new Error(`Do not use ${name ?? 'useNullableContext'} outside of it Provider`); 7 | return notNullContext; 8 | }; 9 | -------------------------------------------------------------------------------- /libs/math/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/oscillator/hooks/use-oscillator-context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNullableContext } from '@waveform/rxjs-react'; 3 | import { useOscillator1 } from '../../common/modules'; 4 | 5 | export const OscillatorContext = React.createContext | null>(null); 6 | 7 | export const useOscillatorContext = () => useNullableContext(OscillatorContext); 8 | -------------------------------------------------------------------------------- /libs/math/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'math', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]sx?$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 14 | coverageDirectory: '../../coverage/libs/math', 15 | }; 16 | -------------------------------------------------------------------------------- /libs/math/src/polyfill/request-idle-callback.ts: -------------------------------------------------------------------------------- 1 | export const requestIdleCallback = (callback: () => void) => { 2 | return window.requestIdleCallback 3 | ? window.requestIdleCallback(callback) 4 | : window.requestAnimationFrame(callback); 5 | }; 6 | 7 | export const cancelIdleCallback = (handle: number) => { 8 | window.cancelIdleCallback ? window.cancelIdleCallback(handle) : window.cancelAnimationFrame(handle); 9 | }; 10 | -------------------------------------------------------------------------------- /libs/ui-kit/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/waveform/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Header } from './header'; 3 | import { AudioProcessorProvider } from './common/modules'; 4 | import { WaveTableEditor } from './wave-table-editor'; 5 | 6 | export default () => { 7 | return ( 8 | 9 |
10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /libs/rxjs-react/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/waveform/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'waveform', 4 | preset: '../../jest.preset.js', 5 | transform: { 6 | '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest', 7 | '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nrwl/react/babel'] }], 8 | }, 9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 10 | coverageDirectory: '../../coverage/apps/waveform', 11 | }; 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Screenshot](screenshot.png) 2 | 3 | # Waveform - experimental synth in browser 4 | 5 | [Demo](https://waveformaudio.netlify.app/) 6 | 7 | Experimental wavetable synth written on TS and Web Audio. 8 | 9 | ## Features 10 | * up to 12 voices with portamento and legato 11 | * 2 panoramic osc with detune, unison up to 8 voices in each 12 | * 7 basic audio filters 13 | * input from onscreen, qwerty or midi keyboard 14 | * envelope support 15 | -------------------------------------------------------------------------------- /libs/ui-kit/src/common/styles/global-style.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | export const GlobalStyle = createGlobalStyle` 4 | * { 5 | margin: 0; 6 | padding: 0; 7 | font-family: 'Inter Tight', sans-serif; 8 | } 9 | 10 | body { 11 | user-select: none; 12 | } 13 | 14 | h1 { 15 | margin: 0; 16 | } 17 | 18 | button { 19 | &:focus { 20 | outline: none; 21 | } 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /libs/math/src/series/wave.ts: -------------------------------------------------------------------------------- 1 | import FFT from 'fft.js'; 2 | 3 | export const realWithImag = (real: number[]): [number[], number[]] => { 4 | const f = new FFT(real.length); 5 | const imag = new Array(real.length); 6 | f.realTransform(imag, real); 7 | 8 | const imagOutput: number[] = []; 9 | for (let i = 0; i < imag.length; i++) { 10 | if (i % 2 === 0) continue; 11 | imagOutput.push(imag[i]); 12 | } 13 | return [real, imagOutput]; 14 | }; 15 | -------------------------------------------------------------------------------- /apps/waveform/src/assets/examples/random-noise-processor.js: -------------------------------------------------------------------------------- 1 | class WhiteNoiseProcessor extends AudioWorkletProcessor { 2 | process(inputs, outputs) { 3 | const output = outputs[0]; 4 | output.forEach((channel) => { 5 | for (let i = 0; i < channel.length; i++) { 6 | channel[i] = Math.random() * 2 - 1; 7 | } 8 | }); 9 | return true; 10 | } 11 | } 12 | 13 | 14 | registerProcessor('white-noise-processor', WhiteNoiseProcessor); 15 | -------------------------------------------------------------------------------- /libs/math/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /libs/ui-kit/src/label/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { textBold12 } from '../common/styles'; 3 | import { theme } from '../common/constants'; 4 | import styled from 'styled-components'; 5 | 6 | const Root = styled.label` 7 | ${textBold12}; 8 | color: ${theme.colors.primaryDarkMediumContrast}; 9 | display: block; 10 | text-align: center; 11 | `; 12 | 13 | export const Label = ({ children }: React.PropsWithChildren) => {children}; 14 | -------------------------------------------------------------------------------- /libs/math/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.ts", 12 | "**/*.test.tsx", 13 | "**/*.spec.tsx", 14 | "**/*.test.js", 15 | "**/*.spec.js", 16 | "**/*.test.jsx", 17 | "**/*.spec.jsx", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /libs/ui-kit/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.ts", 12 | "**/*.test.tsx", 13 | "**/*.spec.tsx", 14 | "**/*.test.js", 15 | "**/*.spec.js", 16 | "**/*.test.jsx", 17 | "**/*.spec.jsx", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/hooks/use-observable.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Observable } from 'rxjs'; 3 | 4 | export const useObservable = (observable: Observable, initialValue: T): T => { 5 | const [value, setValue] = React.useState(initialValue); 6 | React.useEffect(() => { 7 | const subscription = observable.subscribe(setValue); 8 | return () => { 9 | subscription.unsubscribe(); 10 | }; 11 | }, [observable, setValue]); 12 | return value; 13 | }; 14 | -------------------------------------------------------------------------------- /libs/rxjs-react/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.ts", 12 | "**/*.test.tsx", 13 | "**/*.spec.tsx", 14 | "**/*.test.js", 15 | "**/*.spec.js", 16 | "**/*.test.jsx", 17 | "**/*.spec.jsx", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /apps/waveform/src/app/common/constants/note-frequency.ts: -------------------------------------------------------------------------------- 1 | import { Notes } from '@waveform/ui-kit'; 2 | 3 | export const noteFrequency: Record = { 4 | C: 32.70319566257482, 5 | 'C#': 34.64782887210901, 6 | D: 36.70809598967594, 7 | 'D#': 38.89087296526011, 8 | E: 41.20344461410874, 9 | F: 43.65352892912548, 10 | 'F#': 46.24930283895429, 11 | G: 48.99942949771866, 12 | 'G#': 51.91308719749314, 13 | A: 55.0, 14 | 'A#': 58.27047018976123, 15 | B: 61.73541265701551, 16 | }; 17 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/services/note.ts: -------------------------------------------------------------------------------- 1 | import { Note, Notes } from '@waveform/ui-kit'; 2 | import { noteFrequency } from '../../common/constants'; 3 | 4 | export const getFq = ([octave, note]: Note) => noteFrequency[note] * 2 ** (octave - 1); 5 | export const noteToString = ([octave, note]: Note) => `${note}${octave}`; 6 | export const stringToNote = (value: string): Note => { 7 | const octave = value.slice(-1); 8 | const note = value.replace(octave, ''); 9 | return [Number(octave), note as Notes]; 10 | }; 11 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/wave-table-editor/manual-wavetable/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useOscillatorContext } from '../../oscillator'; 3 | import { ManualWavetableProvider } from './modules'; 4 | import { ManualWavetable } from './components'; 5 | 6 | export default () => { 7 | const oscillator = useOscillatorContext(); 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /netlify/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { S3Client } from '@aws-sdk/client-s3'; 2 | 3 | export const client = new S3Client({ 4 | region: 'eu-central-1', 5 | credentials: { 6 | accessKeyId: process.env.ACCESS_KEY_ID, 7 | secretAccessKey: process.env.SECRET_ACCESS_KEY, 8 | }, 9 | }); 10 | 11 | export const getHeaders = () => { 12 | return { 13 | 'Access-Control-Allow-Origin': '*', 14 | 'Access-Control-Allow-Headers': 'Content-Type', 15 | 'Access-Control-Allow-Methods': 'GET, OPTION', 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /libs/ui-kit/src/common/styles/typography.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | export const text10 = css` 4 | font-size: 10px; 5 | line-height: 10px; 6 | `; 7 | 8 | export const textLight14 = css` 9 | font-size: 14px; 10 | font-weight: 200; 11 | `; 12 | 13 | export const text14 = css` 14 | font-size: 14px; 15 | `; 16 | 17 | export const text12 = css` 18 | font-size: 12px; 19 | line-height: 14px; 20 | `; 21 | 22 | export const textBold12 = css` 23 | font-size: 12px; 24 | font-weight: 600; 25 | `; 26 | -------------------------------------------------------------------------------- /libs/math/src/number/number.ts: -------------------------------------------------------------------------------- 1 | export const thresholds = (value: number, min: number, max: number) => Math.max(Math.min(value, max), min); 2 | 3 | export const getBaseLog = (x: number, y: number) => Math.log(y) / Math.log(x); 4 | 5 | export const getLogOfTwo = (val: number) => getBaseLog(2, val); 6 | 7 | export const round = (value: number, precision = 1000) => Math.round(value * precision) / precision; 8 | 9 | export const percent = (value: number) => `${Math.round(value * 100)}%`; 10 | 11 | export const powerOfTwo = (val: number) => 2 ** val; 12 | -------------------------------------------------------------------------------- /libs/ui-kit/src/line-chart/constants/context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ScaleLinear } from 'd3-scale'; 3 | import { Line } from 'd3-shape'; 4 | import { Vector2D } from '@waveform/math'; 5 | 6 | export interface LineChartContextType { 7 | width: number; 8 | height: number; 9 | scaleX: ScaleLinear; 10 | scaleY: ScaleLinear; 11 | lineFn: Line; 12 | padding: Vector2D; 13 | ref: React.RefObject; 14 | } 15 | 16 | export const LineChartContext = React.createContext(null); 17 | -------------------------------------------------------------------------------- /apps/waveform/src/app/common/components/rx-handle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Handle, HandleProps } from '@waveform/ui-kit'; 3 | import { PrimitiveBS, useBehaviorSubject } from '@waveform/rxjs-react'; 4 | 5 | type Props = Omit & { 6 | $value: PrimitiveBS; 7 | }; 8 | 9 | export const RxHandle = ({ $value, onChange, ...props }: Props) => { 10 | const value = useBehaviorSubject($value); 11 | const onChangeInternal = React.useCallback((newValue: number) => onChange?.(newValue), [onChange]); 12 | return ; 13 | }; 14 | -------------------------------------------------------------------------------- /apps/waveform/src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigProvider } from 'antd'; 2 | import { App } from './app'; 3 | import Synth from './synth'; 4 | 5 | export function Core() { 6 | return ( 7 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default Core; 26 | -------------------------------------------------------------------------------- /apps/waveform/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by: 2 | # 1. autoprefixer to adjust CSS to support the below specified browsers 3 | # 2. babel preset-env to adjust included polyfills 4 | # 5 | # For additional information regarding the format and rule options, please see: 6 | # https://github.com/browserslist/browserslist#queries 7 | # 8 | # If you need to support different browsers in production, you may tweak the list below. 9 | 10 | last 1 Chrome version 11 | last 1 Firefox version 12 | last 2 Edge major versions 13 | last 2 Safari major version 14 | last 2 iOS major versions 15 | Firefox ESR 16 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/settings/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { IconButton } from '@waveform/ui-kit'; 3 | import { AiOutlineSave } from 'react-icons/ai'; 4 | import { useApp } from '../../app'; 5 | import { MidiInput } from './components'; 6 | 7 | const Root = styled.div` 8 | display: flex; 9 | flex-direction: column; 10 | gap: 10px; 11 | `; 12 | 13 | export const Settings = () => { 14 | const [, { save }] = useApp(); 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /apps/waveform/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.ts", 12 | "**/*.test.tsx", 13 | "**/*.spec.tsx", 14 | "**/*.test.js", 15 | "**/*.spec.js", 16 | "**/*.test.jsx", 17 | "**/*.spec.jsx", 18 | "**/*.d.ts" 19 | ], 20 | "files": [ 21 | "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", 22 | "../../node_modules/@nrwl/react/typings/image.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/hooks/use-rx-model.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Model, ModelFactory } from '../types'; 3 | 4 | // function useRxModel(): Model; 5 | 6 | export const useRxModel = , A, I>( 7 | name: string, 8 | rxModelFactory: (deps: D) => ModelFactory, 9 | deps: D, 10 | initial: I 11 | ): Model => { 12 | const [state, actions, lifecycle] = React.useMemo(() => rxModelFactory(deps).init(name, initial), []); 13 | React.useEffect(() => { 14 | return () => { 15 | lifecycle.stop(); 16 | }; 17 | }, [lifecycle]); 18 | return [state, actions]; 19 | }; 20 | -------------------------------------------------------------------------------- /libs/ui-kit/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "files": [ 8 | "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", 9 | "../../node_modules/@nrwl/react/typings/image.d.ts" 10 | ], 11 | "exclude": [ 12 | "jest.config.ts", 13 | "**/*.spec.ts", 14 | "**/*.test.ts", 15 | "**/*.spec.tsx", 16 | "**/*.test.tsx", 17 | "**/*.spec.js", 18 | "**/*.test.js", 19 | "**/*.spec.jsx", 20 | "**/*.test.jsx" 21 | ], 22 | "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] 23 | } 24 | -------------------------------------------------------------------------------- /libs/rxjs-react/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "files": [ 8 | "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", 9 | "../../node_modules/@nrwl/react/typings/image.d.ts" 10 | ], 11 | "exclude": [ 12 | "jest.config.ts", 13 | "**/*.spec.ts", 14 | "**/*.test.ts", 15 | "**/*.spec.tsx", 16 | "**/*.test.tsx", 17 | "**/*.spec.js", 18 | "**/*.test.js", 19 | "**/*.spec.jsx", 20 | "**/*.test.jsx" 21 | ], 22 | "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] 23 | } 24 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/observables/primitive-bs.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from 'rxjs'; 2 | import { Snapshotable, SnapshotValue } from '../types'; 3 | import { getSnapshotValue, snapshotToValue } from '../plugins/snapshot'; 4 | 5 | export class PrimitiveBS 6 | extends BehaviorSubject 7 | implements Snapshotable 8 | { 9 | readonly __snapshotable = true as const; 10 | getSnapshot = (): SnapshotValue => { 11 | return getSnapshotValue('PrimitiveBS', this.value); 12 | }; 13 | setSnapshot = (value: SnapshotValue) => { 14 | this.next(snapshotToValue(value)); 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /apps/waveform/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Waveform 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /libs/ui-kit/src/common/constants/theme.ts: -------------------------------------------------------------------------------- 1 | const colors = { 2 | white: '#fff', 3 | primary: '#ecf0f1', 4 | primaryLowContrast: '#bdc3c7', 5 | primaryMediumContrast: '#95a5a6', 6 | primaryHighContrast: '#7f8c8d', 7 | 8 | primaryDark: '#121A22', 9 | primaryDarkMediumContrast: '#2c3e50', 10 | primaryDarkHighContrast: '#5A7FA4', 11 | 12 | accent: '#2ecc71', 13 | secondAccent: '#3498db', 14 | thirdAccent: '#f1c40f', 15 | 16 | red: '#e74c3c', 17 | }; 18 | 19 | const borderRadius = { 20 | s: '2px', 21 | m: '4px', 22 | l: '6px', 23 | }; 24 | 25 | export const theme = { 26 | colors, 27 | borderRadius, 28 | }; 29 | 30 | export type Theme = typeof theme; 31 | -------------------------------------------------------------------------------- /libs/ui-kit/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "noImplicitOverride": true, 11 | "noPropertyAccessFromIndexSignature": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true 14 | }, 15 | "files": [], 16 | "include": [], 17 | "references": [ 18 | { 19 | "path": "./tsconfig.lib.json" 20 | }, 21 | { 22 | "path": "./tsconfig.spec.json" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /apps/waveform/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "noImplicitOverride": true, 11 | "noPropertyAccessFromIndexSignature": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true 14 | }, 15 | "files": [], 16 | "include": [], 17 | "references": [ 18 | { 19 | "path": "./tsconfig.app.json" 20 | }, 21 | { 22 | "path": "./tsconfig.spec.json" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /libs/rxjs-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "noImplicitOverride": true, 11 | "noPropertyAccessFromIndexSignature": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true 14 | }, 15 | "files": [], 16 | "include": [], 17 | "references": [ 18 | { 19 | "path": "./tsconfig.lib.json" 20 | }, 21 | { 22 | "path": "./tsconfig.spec.json" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /libs/math/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "math", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/math/src", 5 | "projectType": "library", 6 | "targets": { 7 | "lint": { 8 | "executor": "@nrwl/linter:eslint", 9 | "outputs": ["{options.outputFile}"], 10 | "options": { 11 | "lintFilePatterns": ["libs/math/**/*.ts"] 12 | } 13 | }, 14 | "test": { 15 | "executor": "@nrwl/jest:jest", 16 | "outputs": ["coverage/libs/math"], 17 | "options": { 18 | "jestConfig": "libs/math/jest.config.ts", 19 | "passWithNoTests": true 20 | } 21 | } 22 | }, 23 | "tags": [] 24 | } 25 | -------------------------------------------------------------------------------- /libs/ui-kit/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-kit", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/ui-kit/src", 5 | "projectType": "library", 6 | "tags": [], 7 | "targets": { 8 | "lint": { 9 | "executor": "@nrwl/linter:eslint", 10 | "outputs": ["{options.outputFile}"], 11 | "options": { 12 | "lintFilePatterns": ["libs/ui-kit/**/*.{ts,tsx,js,jsx}"] 13 | } 14 | }, 15 | "test": { 16 | "executor": "@nrwl/jest:jest", 17 | "outputs": ["coverage/libs/ui-kit"], 18 | "options": { 19 | "jestConfig": "libs/ui-kit/jest.config.ts", 20 | "passWithNoTests": true 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | # Local Netlify folder 42 | .netlify 43 | -------------------------------------------------------------------------------- /libs/rxjs-react/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rxjs-react", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/rxjs-react/src", 5 | "projectType": "library", 6 | "tags": [], 7 | "targets": { 8 | "lint": { 9 | "executor": "@nrwl/linter:eslint", 10 | "outputs": ["{options.outputFile}"], 11 | "options": { 12 | "lintFilePatterns": ["libs/rxjs-react/**/*.{ts,tsx,js,jsx}"] 13 | } 14 | }, 15 | "test": { 16 | "executor": "@nrwl/jest:jest", 17 | "outputs": ["coverage/libs/rxjs-react"], 18 | "options": { 19 | "jestConfig": "libs/rxjs-react/jest.config.ts", 20 | "passWithNoTests": true 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/oscillator/components/wave/chart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { LineChart, Line, theme } from '@waveform/ui-kit'; 4 | import { useObservable } from '@waveform/rxjs-react'; 5 | import { useOscillatorContext } from '../../hooks'; 6 | 7 | const Root = styled(LineChart)` 8 | border-radius: 3px; 9 | `; 10 | 11 | export const Chart = () => { 12 | const [{ $wave }] = useOscillatorContext(); 13 | const wave = useObservable($wave, []); 14 | return ( 15 | 16 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /libs/ui-kit/src/common/styles/states.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { theme } from '../constants'; 3 | 4 | export const Deactivated = styled.div` 5 | position: absolute; 6 | width: 100%; 7 | height: 100%; 8 | background-image: linear-gradient( 9 | 135deg, 10 | ${theme.colors.primaryDark} 25%, 11 | ${theme.colors.primaryDarkMediumContrast} 25%, 12 | ${theme.colors.primaryDarkMediumContrast} 50%, 13 | ${theme.colors.primaryDark} 50%, 14 | ${theme.colors.primaryDark} 75%, 15 | ${theme.colors.primaryDarkMediumContrast} 75%, 16 | ${theme.colors.primaryDarkMediumContrast} 100% 17 | ); 18 | background-size: 10px 10px; 19 | opacity: 0.8; 20 | z-index: 1; 21 | border-radius: 3px; 22 | `; 23 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/audiofile-wavetable/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAudioProcessor } from '../common/modules'; 3 | import { AudiofileWavetableProvider } from './modules'; 4 | import { LoadFile, AudiofileWavetable } from './components'; 5 | 6 | export default () => { 7 | const audioProcessor = useAudioProcessor(); 8 | const [audioBuffer, setAudioBuffer] = React.useState(new Float32Array(0)); 9 | 10 | if (audioBuffer.length === 0) return ; 11 | 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/waveform/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [ 6 | "node" 7 | ] 8 | }, 9 | "files": [ 10 | "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", 11 | "../../node_modules/@nrwl/react/typings/image.d.ts" 12 | ], 13 | "exclude": [ 14 | "jest.config.ts", 15 | "**/*.spec.ts", 16 | "**/*.test.ts", 17 | "**/*.spec.tsx", 18 | "**/*.test.tsx", 19 | "**/*.spec.js", 20 | "**/*.test.js", 21 | "**/*.spec.jsx", 22 | "**/*.test.jsx" 23 | ], 24 | "include": [ 25 | "**/*.js", 26 | "**/*.jsx", 27 | "**/*.ts", 28 | "**/*.tsx", 29 | "../../node_modules/@types/webmidi" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2017", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@waveform/math": ["libs/math/src/index.ts"], 19 | "@waveform/rxjs-react": ["libs/rxjs-react/src/index.ts"], 20 | "@waveform/ui-kit": ["libs/ui-kit/src/index.ts"] 21 | } 22 | }, 23 | "exclude": ["node_modules", "tmp"] 24 | } 25 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/manual-wavetable/wave-upscale/components/output-wave.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useBehaviorSubject } from '@waveform/rxjs-react'; 3 | import { Line, LineChart, theme, XAxis, YAxis } from '@waveform/ui-kit'; 4 | import { useWaveUpscale } from '../modules'; 5 | 6 | export const OutputWave = () => { 7 | const [{ $outputWave }] = useWaveUpscale(); 8 | const outputWave = useBehaviorSubject($outputWave); 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/manual-wavetable/components/manual-wavetable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { WaveEditor, WaveSelector } from '../../common/components'; 4 | import { useManualWavetable } from '../modules'; 5 | import { WaveUpscale } from '../wave-upscale'; 6 | 7 | const Root = styled.div` 8 | display: grid; 9 | grid-template-rows: 65px 1fr 1fr; 10 | gap: 20px; 11 | padding: 20px 20px 20px 0; 12 | `; 13 | 14 | export const ManualWavetable = () => { 15 | const [model, actions] = useManualWavetable(); 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/common/modules/index.ts: -------------------------------------------------------------------------------- 1 | export { MidiControllerProvider, useMidiController } from './midi-controller'; 2 | export { InputControllerProvider, useInputController } from './input-controller'; 3 | export { KeyboardControllerProvider, useKeyboardController } from './keyboard-controller'; 4 | export { AdsrEnvelopeProvider, useAdsrEnvelope } from './adsr-envelope'; 5 | export { Oscillator1Provider, Oscillator2Provider, useOscillator1, useOscillator2 } from './oscillator'; 6 | export { SynthProvider, useSynth } from './synth'; 7 | export { SynthCoreProvider, useSynthCore } from './synth-core'; 8 | export type { AdsrEnvelopeModule, AdsrEnvelopeModel } from './adsr-envelope'; 9 | export type { OscillatorModule, OscillatorModel, OscillatorActions } from './oscillator'; 10 | -------------------------------------------------------------------------------- /libs/ui-kit/src/line-chart/components/square-dots.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { theme } from '@waveform/ui-kit'; 3 | import { useLineChartContext } from '../hooks'; 4 | 5 | interface Props { 6 | data: number[]; 7 | size?: number; 8 | style?: React.CSSProperties; 9 | } 10 | 11 | export const SquareDots = ({ data, size = 4, style }: Props) => { 12 | const { scaleX, scaleY } = useLineChartContext(); 13 | return ( 14 | 15 | {data.map((value, index) => ( 16 | 24 | ))} 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /libs/ui-kit/src/line-chart/components/y-axis.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { line } from 'd3-shape'; 3 | import { theme } from '@waveform/ui-kit'; 4 | import { useLineChartContext } from '../hooks'; 5 | 6 | interface Props { 7 | ticks?: number; 8 | style?: React.CSSProperties; 9 | } 10 | 11 | export const YAxis = ({ ticks, style }: Props) => { 12 | const { scaleY, width, padding } = useLineChartContext(); 13 | return ( 14 | 15 | {scaleY.ticks(ticks).map((y) => ( 16 | 23 | ))} 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/observables/array-bs.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from 'rxjs'; 2 | import { Snapshotable, SnapshotValue } from '../types'; 3 | import { getSnapshotValue, isSnapshotable, snapshotToValue, isSnapshotValue } from '../plugins/snapshot'; 4 | 5 | export class ArrayBS> extends BehaviorSubject implements Snapshotable { 6 | readonly __snapshotable = true as const; 7 | 8 | getSnapshot = (): SnapshotValue> => { 9 | return getSnapshotValue( 10 | 'ArrayBS', 11 | this.value.map((value) => (isSnapshotable(value) ? value.getSnapshot() : value)) 12 | ); 13 | }; 14 | setSnapshot = (snap: SnapshotValue) => { 15 | this.next(snap.value.map((value) => (isSnapshotValue(value) ? snapshotToValue(value) : value)) as T); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /netlify/functions/get-wavetable.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from '@netlify/functions'; 2 | import { getHeaders, client } from '../utils'; 3 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 4 | import { GetObjectCommand } from '@aws-sdk/client-s3'; 5 | 6 | const handler: Handler = async (event) => { 7 | if (typeof event.queryStringParameters.id !== 'string') return { statusCode: 400 }; 8 | const { id: Key } = event.queryStringParameters; 9 | const url = await getSignedUrl( 10 | client, 11 | new GetObjectCommand({ 12 | Bucket: process.env.BUCKET, 13 | Key, 14 | }), 15 | { expiresIn: 21600 } 16 | ); 17 | return { 18 | statusCode: 200, 19 | httpMethod: 'POST', 20 | headers: { 21 | ...getHeaders(), 22 | }, 23 | body: url, 24 | }; 25 | }; 26 | 27 | export { handler }; 28 | -------------------------------------------------------------------------------- /libs/ui-kit/src/common/hooks/use-screen-size.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Vector2D } from '@waveform/math'; 3 | 4 | const debounce = (fn: () => void, time = 300) => { 5 | let timer: ReturnType | undefined = undefined; 6 | return () => { 7 | if (timer) clearTimeout(timer); 8 | timer = setTimeout(fn, time); 9 | }; 10 | }; 11 | 12 | export const useScreenSize = () => { 13 | const [size, setSize] = React.useState([window.innerWidth, window.innerHeight]); 14 | React.useEffect(() => { 15 | const onResize = debounce(() => { 16 | setSize([window.innerWidth, window.innerHeight]); 17 | }); 18 | window.addEventListener('resize', onResize); 19 | return () => { 20 | window.removeEventListener('resize', onResize); 21 | }; 22 | }, []); 23 | return size; 24 | }; 25 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/common/components/wave-selector/wave.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Line, LineChart, theme } from '@waveform/ui-kit'; 4 | 5 | const Root = styled.div<{ selected: boolean }>` 6 | width: 59px; 7 | height: 59px; 8 | border: 1px solid 9 | ${({ selected }) => (selected ? theme.colors.accent : theme.colors.primaryDarkHighContrast)}; 10 | `; 11 | 12 | interface Props { 13 | wave: number[]; 14 | selected: boolean; 15 | onClick?: () => void; 16 | } 17 | 18 | export const Wave = ({ wave, selected, onClick }: Props) => { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /libs/math/src/vector/vector2d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export type Vector2D = [number, number]; 4 | 5 | const fromValues = (x: number, y?: number): Vector2D => (y !== undefined ? [x, y] : [x, x]); 6 | 7 | const fromMouseEvent = (e: MouseEvent | React.MouseEvent): Vector2D => [e.clientX, e.clientY]; 8 | 9 | const subtract = (a: Vector2D, b: Vector2D): Vector2D => [a[0] - b[0], a[1] - b[1]]; 10 | 11 | const invertY = (a: Vector2D): Vector2D => [a[0], -a[1]]; 12 | 13 | const abs = (a: Vector2D): Vector2D => [Math.abs(a[0]), Math.abs(a[1])]; 14 | 15 | const addition = (a: Vector2D): number => a[0] + a[1]; 16 | 17 | const isEqual = (a: Vector2D, b: Vector2D) => a[0] === b[0] && a[1] === b[1]; 18 | 19 | export const vector2d = { 20 | fromMouseEvent, 21 | subtract, 22 | addition, 23 | abs, 24 | invertY, 25 | fromValues, 26 | isEqual, 27 | }; 28 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/manual-wavetable/wave-upscale/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { useAudioProcessor } from '../../common/modules'; 4 | import { useManualWavetable } from '../modules'; 5 | import { WaveUpscaleProvider } from './modules'; 6 | import { Controls, OutputWave } from './components'; 7 | 8 | const Root = styled.div` 9 | display: grid; 10 | grid-template-columns: 70px 1fr; 11 | `; 12 | 13 | export const WaveUpscale = () => { 14 | const audioProcessor = useAudioProcessor(); 15 | const manualWavetable = useManualWavetable(); 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/wave-table-editor/audiofile-wavetable/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useOscillatorContext } from '../../oscillator'; 3 | import { AudiofileProvider } from './modules'; 4 | import { LoadFile, AudiofileWavetable } from './components'; 5 | 6 | let initialBuffer = new Float32Array(0); 7 | 8 | export default () => { 9 | const oscillator = useOscillatorContext(); 10 | const [audioBuffer, setAudioBuffer] = React.useState(initialBuffer); 11 | const setAudioBufferInternal = (buffer: Float32Array) => { 12 | initialBuffer = buffer; 13 | setAudioBuffer(buffer); 14 | }; 15 | 16 | if (audioBuffer.length === 0) return ; 17 | 18 | return ( 19 | 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /libs/ui-kit/src/filter/types/index.ts: -------------------------------------------------------------------------------- 1 | import { HandleProps } from '../../handle'; 2 | 3 | export type FilterNumerics = 'cutoff' | 'resonance' | 'gain'; 4 | 5 | export type FilterParams = { 6 | type: BiquadFilterType; 7 | cutoff: number; 8 | resonance: number; 9 | gain: number; 10 | }; 11 | 12 | export type FilterRange = Required> & 13 | Pick; 14 | export type FilterRanges = Record; 15 | 16 | export type CommonFilterProps = FilterParams & { 17 | active?: boolean; 18 | setType: (value: BiquadFilterType) => void; 19 | setNumericValue: (key: FilterNumerics, value: number) => void; 20 | ranges: FilterRanges; 21 | }; 22 | 23 | export type FilterRouterProps = Pick; 24 | 25 | export type FilterProps = Pick; 26 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/services/rx-model-react.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ModelFactory, Model } from '../types'; 3 | import { useNullableContext, useRxModel } from '../hooks'; 4 | 5 | export const rxModelReact = < 6 | D extends Record, // dependencies 7 | M extends Record, // model 8 | A, // actions 9 | I = undefined // initial values 10 | >( 11 | name: string, 12 | model: (deps: D) => ModelFactory 13 | ) => { 14 | const Context = React.createContext | null>(null); 15 | const useModel = () => useNullableContext(Context); 16 | const ModelProvider = ({ children, initial, ...rest }: { children: React.ReactNode; initial: I } & D) => { 17 | const value = useRxModel(name, model, rest as unknown as D, initial); 18 | return {children}; 19 | }; 20 | return { ModelProvider, useModel, Context }; 21 | }; 22 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/wave-table-editor/audiofile-wavetable/components/audiofile-wavetable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { useOscillatorContext } from '../../../oscillator'; 4 | import { WaveSelector } from '../../common/components'; 5 | import { AudiofileWavePicker } from './audiofile-wave-picker'; 6 | import { WavePreview } from './wave-preview'; 7 | 8 | const Root = styled.div` 9 | display: grid; 10 | grid-template-rows: 65px 1fr 1fr; 11 | gap: 20px; 12 | padding: 20px 20px 20px 0; 13 | `; 14 | 15 | export const AudiofileWavetable = () => { 16 | const [{ $waveTable, $current, $wave }, { setCurrent }] = useOscillatorContext(); 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/wave-table-editor/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { HighlightOutlined, SoundOutlined } from '@ant-design/icons'; 4 | import { Tabs } from '@waveform/ui-kit'; 5 | import ManualWavetable from './manual-wavetable'; 6 | import AudiofileWavetable from './audiofile-wavetable'; 7 | 8 | const Root = styled.div` 9 | display: grid; 10 | grid-template-columns: 50px 1fr; 11 | height: calc(100vh - 100px); 12 | `; 13 | 14 | export default () => { 15 | const [tab, setTab] = React.useState('Manual'); 16 | return ( 17 | 18 | 19 | }> 20 | 21 | 22 | }> 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/wave-table-editor/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { HighlightOutlined, SoundOutlined } from '@ant-design/icons'; 4 | import { Tabs } from '@waveform/ui-kit'; 5 | import AudiofileWavetable from '../audiofile-wavetable'; 6 | import ManualWavetable from '../manual-wavetable'; 7 | 8 | const Root = styled.div` 9 | display: grid; 10 | grid-template-columns: 50px 1fr; 11 | `; 12 | 13 | export const WaveTableEditor = () => { 14 | const [tab, setTab] = React.useState('Audio file'); 15 | return ( 16 | 17 | 18 | }> 19 | 20 | 21 | }> 22 | 23 | 24 | 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/filter/components/filter-section.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useBehaviorSubject } from '@waveform/rxjs-react'; 3 | import { Filter, Section } from '@waveform/ui-kit'; 4 | import { useFilter } from '../modules'; 5 | 6 | export const FilterSection = () => { 7 | const [{ $filter, $filterType, $active, $ranges }, { toggleActive, setType, setNumericValue }] = 8 | useFilter(); 9 | const active = useBehaviorSubject($active); 10 | const filter = useBehaviorSubject($filter); 11 | const filterType = useBehaviorSubject($filterType); 12 | const ranges = useBehaviorSubject($ranges); 13 | 14 | return ( 15 |
16 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /libs/ui-kit/src/line-chart/components/line.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Line as LineType } from 'd3-shape'; 3 | import { theme } from '../../common/constants'; 4 | import { useLineChartContext } from '../hooks'; 5 | 6 | interface Props { 7 | data: number[]; 8 | style?: React.CSSProperties; 9 | } 10 | 11 | interface CustomLineProps { 12 | data: T[]; 13 | customLineFn: LineType; 14 | style?: React.CSSProperties; 15 | } 16 | 17 | export function Line(props: Props): JSX.Element; 18 | export function Line(props: CustomLineProps): JSX.Element; 19 | 20 | export function Line(props: Props | CustomLineProps) { 21 | const { data, style } = props; 22 | const { lineFn } = useLineChartContext(); 23 | const d = 'customLineFn' in props ? props.customLineFn(data) : lineFn(props.data); 24 | return ( 25 | 26 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /apps/waveform/src/app/app/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 4 | import { Toaster } from 'react-hot-toast'; 5 | import { GlobalStyle, theme } from '@waveform/ui-kit'; 6 | import { AppProvider, useApp } from './modules'; 7 | import { urlSnapshotPlugin, saveUrlSnapshot } from './plugins/snapshot'; 8 | 9 | export { AppProvider, useApp, urlSnapshotPlugin, saveUrlSnapshot }; 10 | 11 | const queryClient = new QueryClient(); 12 | 13 | const Root = styled.div` 14 | background: ${theme.colors.primary}; 15 | height: 100vh; 16 | display: grid; 17 | grid-template-rows: 80px 1fr; 18 | `; 19 | 20 | export const App = ({ children }: React.PropsWithChildren) => { 21 | return ( 22 | 23 | 24 | 25 | {children} 26 | 27 | 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/observables/object-bs.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from 'rxjs'; 2 | import { Snapshotable, SnapshotValue } from '../types'; 3 | import { isSnapshotable, getSnapshotValue, snapshotToValue, isSnapshotValue } from '../plugins/snapshot'; 4 | 5 | export class ObjectBS> extends BehaviorSubject implements Snapshotable { 6 | readonly __snapshotable = true as const; 7 | 8 | getSnapshot = (): SnapshotValue> => { 9 | const resultObject: Record = {}; 10 | for (const key in this.value) { 11 | const value = this.value[key]; 12 | resultObject[key] = isSnapshotable(value) ? value.getSnapshot() : value; 13 | } 14 | return getSnapshotValue('ObjectBS', resultObject); 15 | }; 16 | setSnapshot = (value: SnapshotValue) => { 17 | const newValue: Record = {}; 18 | for (const key in value.value) { 19 | const valueItem = value.value[key]; 20 | newValue[key] = isSnapshotValue(valueItem) ? snapshotToValue(valueItem) : value.value[key]; 21 | } 22 | this.next(newValue as T); 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/manual-wavetable/modules/manual-wavetable.ts: -------------------------------------------------------------------------------- 1 | import { ArrayBS, rxModelReact } from '@waveform/rxjs-react'; 2 | import { wavetable } from '../../common/modules'; 3 | 4 | const manualWavetable = () => 5 | wavetable.actions(({ $waveTable, $current }) => ({ 6 | updateCurrentWave: ([i, value]: [number, number]) => { 7 | const $wave = $waveTable.value[$current.value]; 8 | const newWave = [...$wave.value]; 9 | newWave[i] = value; 10 | $wave.next(newWave); 11 | }, 12 | cloneCurrent: () => { 13 | $waveTable.next([...$waveTable.value, new ArrayBS([...$waveTable.value[$current.value].value])]); 14 | }, 15 | })); 16 | 17 | export const { ModelProvider: ManualWavetableProvider, useModel: useManualWavetable } = rxModelReact( 18 | 'manualWavetable', 19 | manualWavetable 20 | ); 21 | 22 | export type ManualWavetableModule = ReturnType; 23 | export type ManualWavetableModel = ReturnType['init']>[0]; 24 | export type ManualWavetableActions = ReturnType['init']>[1]; 25 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/common/modules/wavetable.ts: -------------------------------------------------------------------------------- 1 | import { mergeMap, mergeWith } from 'rxjs'; 2 | import { ArrayBS, PrimitiveBS, rxModel } from '@waveform/rxjs-react'; 3 | import { number } from '@waveform/math'; 4 | 5 | export const wavetable = rxModel(() => { 6 | const rateRange: [number, number] = [2, 8]; 7 | const $rate = new PrimitiveBS(4); 8 | const $waveTable = new ArrayBS[]>([ 9 | new ArrayBS(Array(number.powerOfTwo(rateRange[1])).fill(0)), 10 | new ArrayBS(Array(number.powerOfTwo(rateRange[1])).fill(0)), 11 | ]); 12 | const $current = new PrimitiveBS(0); 13 | 14 | const $wave = $current.pipe( 15 | mergeWith($waveTable), 16 | mergeMap(() => $waveTable.value[$current.value]) 17 | ); 18 | 19 | return { $waveTable, $wave, $current, $rate, rateRange }; 20 | }).actions(({ $current, $rate }) => ({ 21 | setCurrent: (i: number) => $current.next(i), 22 | setRate: (value: number) => $rate.next(value), 23 | })); 24 | 25 | export type WavetableModel = ReturnType[0]; 26 | export type WavetableActions = ReturnType[1]; 27 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/audiofile-wavetable/modules/audiofile-wavetable.ts: -------------------------------------------------------------------------------- 1 | import { map } from 'rxjs'; 2 | import { ArrayBS, rxModelReact } from '@waveform/rxjs-react'; 3 | import { wave } from '@waveform/math'; 4 | import { wavetable, AudioProcessorModule } from '../../common/modules'; 5 | 6 | interface Dependencies { 7 | audioProcessor: AudioProcessorModule; 8 | } 9 | 10 | const audiofileWavetable = ({ audioProcessor: [, { setWave }] }: Dependencies) => 11 | wavetable 12 | .actions(({ $waveTable, $current }) => ({ 13 | setWaveTable: (waves: number[][]) => { 14 | if (waves.length - 1 < $current.value) { 15 | $current.next(waves.length - 1); 16 | } 17 | $waveTable.next(waves.map((wave) => new ArrayBS(wave))); 18 | }, 19 | })) 20 | .subscriptions(({ $wave }) => $wave.pipe(map(wave.realWithImag)).subscribe(setWave)); 21 | 22 | export const { ModelProvider: AudiofileWavetableProvider, useModel: useAudiofileWavetable } = rxModelReact( 23 | 'audiofileWavetable', 24 | audiofileWavetable 25 | ); 26 | 27 | export type AudiofileWavetableModule = ReturnType; 28 | -------------------------------------------------------------------------------- /apps/waveform/src/app/app/modules/app-module.tsx: -------------------------------------------------------------------------------- 1 | import { fromEvent, filter, tap } from 'rxjs'; 2 | import toast from 'react-hot-toast'; 3 | import { rxModel, rxModelReact } from '@waveform/rxjs-react'; 4 | import { saveUrlSnapshot } from '../plugins/snapshot'; 5 | 6 | const appModel = () => { 7 | return rxModel(() => ({ 8 | $keyDown: fromEvent(document, 'keydown'), 9 | $keyUp: fromEvent(document, 'keyup'), 10 | })) 11 | .actions(() => ({ 12 | save: () => { 13 | toast.success('Preset has been saved and the URL has been copied to the clipboard.'); 14 | saveUrlSnapshot(); 15 | navigator.clipboard.writeText(window.location.href); 16 | }, 17 | })) 18 | .subscriptions(({ $keyUp, $keyDown }, { save }) => [ 19 | $keyDown 20 | .pipe( 21 | filter((e) => (e.metaKey || e.ctrlKey) && e.code === 'KeyS'), 22 | tap((e) => e.preventDefault()) 23 | ) 24 | .subscribe(save), 25 | ]); 26 | }; 27 | 28 | export const { ModelProvider: AppProvider, useModel: useApp } = rxModelReact('appModel', appModel); 29 | 30 | export type AppModule = ReturnType; 31 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/oscillator/hooks/use-wavetables.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { CascadeOption } from '@waveform/ui-kit'; 3 | import prettyBytes from 'pretty-bytes'; 4 | 5 | interface Wavetable { 6 | Key: string; 7 | Size: number; 8 | } 9 | 10 | type Element = Folder | Wavetable; 11 | 12 | interface Folder { 13 | name: string; 14 | children: Element[]; 15 | } 16 | 17 | const wavetablesToOptions = (data: Element[]): CascadeOption[] => 18 | data.map((wave) => { 19 | if ('Key' in wave) { 20 | const path = wave.Key.split('/'); 21 | const [filename] = path[path.length - 1].split('.'); 22 | return { value: wave.Key, label: `${filename} (${prettyBytes(wave.Size)})` }; 23 | } 24 | return { value: wave.name, label: wave.name, children: wavetablesToOptions(wave.children) }; 25 | }); 26 | 27 | export const useWavetables = () => { 28 | const { data } = useQuery({ 29 | queryKey: ['wavetables'], 30 | queryFn: () => fetch('/.netlify/functions/get-wavetables-list').then((res) => res.json()), 31 | staleTime: 24 * 60 * 60 * 1000, 32 | }); 33 | return wavetablesToOptions(data ?? []); 34 | }; 35 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/audiofile-wavetable/components/audiofile-wavetable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { AudiofileWavePickerProvider, useAudiofileWavetable } from '../modules'; 4 | import { WaveSelector } from '../../common/components'; 5 | import { AudiofileWavePicker } from './audiofile-wave-picker'; 6 | import { WavePreview } from './wave-preview'; 7 | 8 | const Root = styled.div` 9 | display: grid; 10 | grid-template-rows: 65px 1fr 1fr; 11 | gap: 20px; 12 | padding: 20px 20px 20px 0; 13 | `; 14 | 15 | interface Props { 16 | audioBuffer: Float32Array; 17 | } 18 | 19 | export const AudiofileWavetable = ({ audioBuffer }: Props) => { 20 | const audiofileWavetable = useAudiofileWavetable(); 21 | const [{ rateRange, ...model }, { setCurrent }] = audiofileWavetable; 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/wave-table-editor/manual-wavetable/components/manual-wavetable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { useOscillatorContext } from '../../../oscillator'; 4 | import { WaveSelector, WaveEditor } from '../../common/components'; 5 | import { useManualWavetable } from '../modules'; 6 | 7 | const Root = styled.div` 8 | display: grid; 9 | grid-template-rows: 65px 1fr; 10 | gap: 20px; 11 | padding: 20px 20px 20px 0; 12 | `; 13 | 14 | export const ManualWavetable = () => { 15 | const [{ $waveTable, $wave, $current }, { setCurrent }] = useOscillatorContext(); 16 | const [{ $rate, rateRange }, { cloneCurrent, setRate, updateCurrentWave }] = useManualWavetable(); 17 | return ( 18 | 19 | 26 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /libs/ui-kit/src/icon-button/index.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { PropsWithChildren, ButtonHTMLAttributes } from 'react'; 3 | import { theme } from '../common/constants'; 4 | 5 | const ActiveStyle = css` 6 | background: ${theme.colors.secondAccent}; 7 | box-shadow: inset 0 0 0 1px #99cbec, 0 0 3px 2px rgba(153, 203, 236, 0.5); 8 | `; 9 | const Root = styled.button<{ active: boolean }>` 10 | border: 1px solid ${theme.colors.primaryDarkMediumContrast}; 11 | border-radius: 3px; 12 | padding: 3px; 13 | display: flex; 14 | align-items: center; 15 | color: ${theme.colors.primary}; 16 | cursor: pointer; 17 | 18 | &:active { 19 | ${ActiveStyle} 20 | } 21 | 22 | ${({ active }) => 23 | active 24 | ? ActiveStyle 25 | : css` 26 | background: ${theme.colors.primaryHighContrast}; 27 | `} 28 | svg { 29 | font-size: 15px; 30 | } 31 | `; 32 | 33 | type Props = PropsWithChildren<{ active?: boolean } & ButtonHTMLAttributes>; 34 | 35 | export const IconButton = ({ children, active, ...rest }: Props) => { 36 | return ( 37 | 38 | {children} 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /libs/ui-kit/src/line-chart/components/x-axis.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { theme } from '../../common/constants'; 3 | import { useLineChartContext } from '../hooks'; 4 | import { ScaleLinear } from 'd3-scale'; 5 | 6 | interface Props { 7 | ticks?: number; 8 | style?: React.CSSProperties; 9 | formatText?: (value: number) => string | number; 10 | customScaleX?: ScaleLinear; 11 | } 12 | 13 | export const XAxis = ({ ticks, style, formatText, customScaleX }: Props) => { 14 | const { scaleX, height, padding } = useLineChartContext(); 15 | const scaleXInternal = customScaleX ?? scaleX; 16 | return ( 17 | 18 | {scaleXInternal.ticks(ticks).map((x) => ( 19 | 20 | 25 | {formatText && ( 26 | 27 | {formatText(x)} 28 | 29 | )} 30 | 31 | ))} 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /libs/ui-kit/src/piano-keyboard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { useScreenSize } from '../common/hooks'; 4 | import { Notes, Note } from '../common/types'; 5 | import { Octave } from './octave'; 6 | 7 | const Root = styled.div` 8 | display: flex; 9 | `; 10 | 11 | interface Props { 12 | pressed: Record; 13 | octaves?: [number, number]; 14 | onPress?: (note: Note) => void; 15 | onRelease?: (note: Note) => void; 16 | } 17 | 18 | export const PianoKeyboard = ({ octaves = [1, 5], onPress, onRelease, pressed }: Props) => { 19 | const [width] = useScreenSize(); 20 | const octavesCount = Math.min(octaves[1], Math.floor(width / 280)) - octaves[0] + 1; 21 | const onPressInternal = (octave: number) => (note: Notes) => onPress?.([octave, note]); 22 | const onReleaseInternal = (octave: number) => (note: Notes) => onRelease?.([octave, note]); 23 | return ( 24 | 25 | {[...Array(octavesCount)].map((_, i) => ( 26 | 32 | ))} 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /libs/ui-kit/src/section/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Checkbox } from '../checkbox'; 3 | import styled from 'styled-components'; 4 | import { theme } from '../common/constants'; 5 | import { textBold12 } from '../common/styles'; 6 | 7 | interface Props { 8 | name?: React.ReactNode; 9 | active?: boolean; 10 | onClick?: () => void; 11 | } 12 | 13 | const Root = styled.div` 14 | padding: 10px; 15 | display: flex; 16 | flex-direction: column; 17 | background: ${theme.colors.primary}; 18 | `; 19 | 20 | const Header = styled.div` 21 | display: flex; 22 | align-items: center; 23 | cursor: pointer; 24 | gap: 10px; 25 | ${textBold12}; 26 | padding: 3px 5px; 27 | background: ${theme.colors.primaryLowContrast}; 28 | color: ${theme.colors.white}; 29 | margin-bottom: 5px; 30 | border-radius: 3px; 31 | `; 32 | 33 | const Content = styled.div` 34 | flex: 1 1; 35 | `; 36 | 37 | export const Section = ({ name, children, active, onClick }: React.PropsWithChildren) => { 38 | return ( 39 | 40 |
41 | {active !== undefined && } 42 | {name} 43 |
44 | {children} 45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/plugins/snapshot/services.ts: -------------------------------------------------------------------------------- 1 | import { isRecord } from '../../utils'; 2 | import { Snapshotable, SnapshotType, SnapshotValue } from '../../types'; 3 | import { ArrayBS, ObjectBS } from '../../observables'; 4 | 5 | export const isSnapshotable = (value: unknown): value is Snapshotable => 6 | isRecord(value) && '__snapshotable' in value; 7 | 8 | export const getSnapshotValue = (type: SnapshotType, value: T): SnapshotValue => ({ 9 | _type_: type, 10 | value, 11 | }); 12 | export const isSnapshotValue = (value: unknown): value is SnapshotValue => 13 | isRecord(value) && '_type_' in value; 14 | 15 | export const snapshotToValue = (value: SnapshotValue): any => { 16 | switch (value._type_) { 17 | case 'PrimitiveBS': 18 | return value.value; 19 | case 'ArrayBS': 20 | return new ArrayBS( 21 | value.value.map((value: unknown) => (isSnapshotValue(value) ? snapshotToValue(value) : value)) 22 | ); 23 | case 'ObjectBS': 24 | return new ObjectBS( 25 | Object.keys(value.value).reduce((sum, key) => { 26 | const result = value.value[key]; 27 | return { ...sum, [key]: isSnapshotValue(result) ? snapshotToValue(result) : result }; 28 | }, {}) 29 | ); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/filter/constants/ranges.ts: -------------------------------------------------------------------------------- 1 | import { FilterNumerics, FilterRange, FilterRanges } from '@waveform/ui-kit'; 2 | 3 | export const defaultFilterRanges: FilterRanges = { 4 | cutoff: { min: 10, max: 22050, precision: 1, mode: 'log', plotSize: 200 }, 5 | resonance: { min: 0, max: 20, precision: 10 }, 6 | gain: { min: 0, max: 1, precision: 100 }, 7 | }; 8 | 9 | const disabled: Partial = { min: -1000, max: 1000, disabled: true }; 10 | 11 | export const filterRanges: Record>>> = { 12 | lowpass: { 13 | gain: disabled, 14 | }, 15 | highpass: { 16 | gain: disabled, 17 | }, 18 | bandpass: { 19 | resonance: { min: 0.01, max: 1000, precision: 100, mode: 'log' }, 20 | gain: disabled, 21 | }, 22 | lowshelf: { 23 | resonance: disabled, 24 | gain: { min: -40, max: 40 }, 25 | }, 26 | highshelf: { 27 | resonance: disabled, 28 | gain: { min: -40, max: 40 }, 29 | }, 30 | peaking: { 31 | gain: { min: 0, max: 40 }, 32 | resonance: { min: 0, max: 10, precision: 100 }, 33 | }, 34 | notch: { 35 | gain: disabled, 36 | resonance: { min: 0.001, max: 10, precision: 10000, plotSize: 200, mode: 'log' }, 37 | }, 38 | allpass: {}, 39 | }; 40 | -------------------------------------------------------------------------------- /libs/ui-kit/src/index.ts: -------------------------------------------------------------------------------- 1 | export { LineChart, Line, SquareDots, XAxis, YAxis, useLineChartContext } from './line-chart'; 2 | export { Handle } from './handle'; 3 | export { Tooltip } from './tooltip'; 4 | export { WaveDrawer } from './wave-drawer'; 5 | export { Label } from './label'; 6 | export { Analyser } from './analyser'; 7 | export { FileDrop } from './file-drop'; 8 | export { Tabs } from './tabs'; 9 | export { PianoKeyboard } from './piano-keyboard'; 10 | export { AdsrEnvelope } from './adsr-envelope'; 11 | export { Modal } from './modal'; 12 | export { Section } from './section'; 13 | export { VolumeAnalyser } from './volume-analyser'; 14 | export { FqAnalyser } from './fq-analyser'; 15 | export { Filter } from './filter'; 16 | export { Select, CascadeSelect } from './select'; 17 | export { DraggableNumber } from './draggable-number'; 18 | export { Checkbox } from './checkbox'; 19 | export { IconButton } from './icon-button'; 20 | export { theme } from './common/constants'; 21 | export * from './common/styles'; 22 | export * from './common/types'; 23 | 24 | export type { FilterNumerics, FilterRange, FilterRanges, FilterParams } from './filter'; 25 | export type { CascadeOption } from './select'; 26 | export type { HandleProps } from './handle'; 27 | export type { Theme } from './common/constants'; 28 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/audiofile-wavetable/components/wave-preview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useObservable } from '@waveform/rxjs-react'; 3 | import { LineChart, Line, XAxis, YAxis } from '@waveform/ui-kit'; 4 | import styled from 'styled-components'; 5 | import { useAudiofileWavePicker, useAudiofileWavetable } from '../modules'; 6 | import { RxHandle } from '../../../common/components'; 7 | 8 | const Root = styled.div` 9 | display: grid; 10 | grid-template-columns: 70px 1fr; 11 | `; 12 | 13 | const HandlersWrapper = styled.div` 14 | display: flex; 15 | flex-direction: column; 16 | gap: 10px; 17 | justify-content: space-between; 18 | align-items: center; 19 | `; 20 | 21 | export const WavePreview = () => { 22 | const [{ $wave }] = useAudiofileWavetable(); 23 | const [{ $phase }, { setPhase }] = useAudiofileWavePicker(); 24 | const wave = useObservable($wave, []); 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/types/rx-model.ts: -------------------------------------------------------------------------------- 1 | import { Subscription } from 'rxjs'; 2 | 3 | export type Plugin = { 4 | onInit?: (model: M, meta: Meta) => void; 5 | onStop?: (model: M, meta: Meta) => void; 6 | }; 7 | 8 | export interface Initializers { 9 | actions: Array<(model: M) => A>; 10 | subscriptions: Array<(model: M, actions: A) => Subscription | Subscription[]>; 11 | plugins: Array>; 12 | destroy: Array<(model: M) => void>; 13 | } 14 | 15 | export type Meta = { 16 | readonly name: string; 17 | readonly active: boolean; 18 | }; 19 | export type ModelLifecycle = { 20 | meta: Meta; 21 | stop: () => void; 22 | }; 23 | export type ModelInternal, A> = [M, A, ModelLifecycle]; 24 | export type Model, A> = [M, A]; 25 | 26 | export type ModelFactory, A, I> = { 27 | init: (name: string, initial: I) => ModelInternal; 28 | actions: (fn: (model: M) => NA) => ModelFactory; 29 | subscriptions: (fn: (model: M, actions: A) => Subscription | Subscription[]) => ModelFactory; 30 | plugins: (plugins: Plugin | Array>) => ModelFactory; 31 | destroy: (fn: (model: M) => void) => ModelFactory; 32 | }; 33 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/header/play-button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CaretRightOutlined, PauseOutlined } from '@ant-design/icons'; 3 | import styled from 'styled-components'; 4 | import { theme } from '@waveform/ui-kit'; 5 | import { useBehaviorSubject } from '@waveform/rxjs-react'; 6 | import { useAudioProcessor } from '../../common/modules'; 7 | 8 | const Button = styled.button` 9 | border: none; 10 | background: ${theme.colors.primaryLowContrast}; 11 | cursor: pointer; 12 | height: 80px; 13 | width: 80px; 14 | transition: all 150ms; 15 | background-image: linear-gradient(120deg, ${theme.colors.secondAccent}, ${theme.colors.accent} 100%); 16 | color: ${theme.colors.white}; 17 | 18 | &:hover { 19 | filter: brightness(1.05); 20 | } 21 | 22 | &:active { 23 | filter: hue-rotate(10deg); 24 | } 25 | 26 | &:focus-visible { 27 | filter: brightness(1.05); 28 | outline: none; 29 | } 30 | `; 31 | 32 | export const PlayButton = () => { 33 | const [{ $isPlay }, { playToggle }] = useAudioProcessor(); 34 | const play = useBehaviorSubject($isPlay); 35 | 36 | return ( 37 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/wave-table-editor/audiofile-wavetable/components/wave-preview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { useObservable } from '@waveform/rxjs-react'; 4 | import { LineChart, Line, XAxis, YAxis } from '@waveform/ui-kit'; 5 | import { RxHandle } from '../../../../common/components'; 6 | import { useOscillatorContext } from '../../../oscillator'; 7 | import { useAudiofile } from '../modules'; 8 | 9 | const Root = styled.div` 10 | display: grid; 11 | grid-template-columns: 70px 1fr; 12 | `; 13 | 14 | const HandlersWrapper = styled.div` 15 | display: flex; 16 | flex-direction: column; 17 | gap: 10px; 18 | justify-content: space-between; 19 | align-items: center; 20 | `; 21 | 22 | export const WavePreview = () => { 23 | const [{ $wave }] = useOscillatorContext(); 24 | const [{ $phase }, { setPhase }] = useAudiofile(); 25 | const wave = useObservable($wave, []); 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/manual-wavetable/wave-upscale/components/controls.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { number } from '@waveform/math'; 4 | import { useBehaviorSubject } from '@waveform/rxjs-react'; 5 | import { useWaveUpscale } from '../modules'; 6 | import { useManualWavetable } from '../../modules'; 7 | import { RxHandle } from '../../../../common/components'; 8 | 9 | const HandlersWrapper = styled.div` 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: space-between; 13 | align-items: center; 14 | `; 15 | 16 | export const Controls = () => { 17 | const [{ $rate }] = useManualWavetable(); 18 | const [{ $outputRate, $phase }, { setOutputRate, setPhase }] = useWaveUpscale(); 19 | const rate = useBehaviorSubject($rate); 20 | return ( 21 | 22 | <>Out: {number.powerOfTwo(value)}} 29 | /> 30 | Math.round(value * 100) / 100} 34 | label='Phase' 35 | /> 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /libs/ui-kit/src/select/cascade-select.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Cascader, Spin } from 'antd'; 4 | import { noop } from 'rxjs'; 5 | import { SelectDumb, ContentContainer } from './select-dumb'; 6 | 7 | const Root = styled.div` 8 | ${ContentContainer} 9 | `; 10 | 11 | const SelectDumbInternal = styled(SelectDumb)` 12 | .ant-spin-nested-loading { 13 | width: 100%; 14 | } 15 | `; 16 | 17 | type CascadeOptionWithChildren = { 18 | value: string; 19 | label: React.ReactNode; 20 | children: CascadeOption[]; 21 | }; 22 | 23 | type CascadeOptionValue = { 24 | value: string; 25 | label: React.ReactNode; 26 | }; 27 | 28 | export type CascadeOption = CascadeOptionWithChildren | CascadeOptionValue; 29 | 30 | interface Props { 31 | options: CascadeOption[]; 32 | value: (string | number)[]; 33 | setValue: (value: (string | number)[]) => void; 34 | loading: boolean; 35 | } 36 | 37 | export const CascadeSelect = ({ 38 | options, 39 | value, 40 | setValue, 41 | loading, 42 | children, 43 | }: React.PropsWithChildren) => { 44 | return ( 45 | 46 | 47 | 48 | {children} 49 | 50 | 51 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/common/modules/adsr-envelope.ts: -------------------------------------------------------------------------------- 1 | import { ObjectBS, rxModel, rxModelReact } from '@waveform/rxjs-react'; 2 | import { urlSnapshotPlugin } from '../../../app'; 3 | 4 | const adsrEnvelope = () => 5 | rxModel(() => { 6 | const $envelope = new ObjectBS({ 7 | attack: 0.005, 8 | hold: 0.1, 9 | decay: 0, 10 | sustain: 1, 11 | release: 0.005, 12 | }); 13 | return { $envelope }; 14 | }) 15 | .actions(({ $envelope }) => ({ 16 | setEnvelopeValue: (key: keyof typeof $envelope.value, value: number) => 17 | $envelope.next({ 18 | ...$envelope.value, 19 | [key]: value, 20 | }), 21 | })) 22 | .plugins( 23 | urlSnapshotPlugin({ 24 | modelToSnap: ({ $envelope }) => $envelope.value, 25 | applySnap: (snap, { $envelope }) => 26 | $envelope.next({ 27 | attack: snap.attack ?? 0.005, 28 | hold: snap.hold ?? 0.1, 29 | decay: snap.decay ?? 0, 30 | sustain: snap.sustain ?? 1, 31 | release: snap.release ?? 0.005, 32 | }), 33 | }) 34 | ); 35 | 36 | export const { ModelProvider: AdsrEnvelopeProvider, useModel: useAdsrEnvelope } = rxModelReact( 37 | 'adsr', 38 | adsrEnvelope 39 | ); 40 | 41 | export type AdsrEnvelopeModule = ReturnType; 42 | export type AdsrEnvelopeModel = AdsrEnvelopeModule[0]; 43 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/oscillator/hooks/use-wavetable.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import React from 'react'; 3 | import { useSynthCore } from '../../common/modules'; 4 | import { useOscillatorContext } from './use-oscillator-context'; 5 | 6 | const WAVE_SIZE = 2048; 7 | 8 | export const useWavetable = (id: string | null) => { 9 | const [, { setWaveTable }] = useOscillatorContext(); 10 | const [{ audioCtx }] = useSynthCore(); 11 | const { data, isLoading } = useQuery({ 12 | queryKey: ['wavetable', id], 13 | queryFn: async () => { 14 | const formLink = await fetch(`/.netlify/functions/get-wavetable?id=${id}`).then((res) => res.text()); 15 | return await fetch(formLink).then(async (res) => { 16 | const buffer = await res.arrayBuffer(); 17 | const audioBuffer = await audioCtx.decodeAudioData(buffer); 18 | const data = audioBuffer.getChannelData(0); 19 | const wavesCount = Math.floor(audioBuffer.length / WAVE_SIZE); 20 | const wavetable = []; 21 | for (let i = 0; i < wavesCount; i++) 22 | wavetable.push([...data.slice(i * WAVE_SIZE, (i + 1) * WAVE_SIZE)]); 23 | return wavetable; 24 | }); 25 | }, 26 | enabled: Boolean(id), 27 | staleTime: 24 * 60 * 60 * 1000, 28 | cacheTime: Infinity, 29 | }); 30 | React.useEffect(() => { 31 | if (data) setWaveTable(data); 32 | }, [data, setWaveTable]); 33 | return isLoading; 34 | }; 35 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/audiofile-wavetable/components/audiofile-wave-picker/pickers.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { number } from '@waveform/math'; 3 | import { theme, useLineChartContext } from '@waveform/ui-kit'; 4 | import { useBehaviorSubject } from '@waveform/rxjs-react'; 5 | import { useAudiofileWavePicker, useAudiofileWavetable } from '../../modules'; 6 | 7 | export const Pickers = () => { 8 | const { scaleX, height, padding } = useLineChartContext(); 9 | const [{ $wavePickers, $waveSize }] = useAudiofileWavePicker(); 10 | const [{ $current }] = useAudiofileWavetable(); 11 | const wavePickers = useBehaviorSubject($wavePickers); 12 | const waveSize = useBehaviorSubject($waveSize); 13 | const current = useBehaviorSubject($current); 14 | 15 | const width = scaleX(number.powerOfTwo(waveSize)); 16 | return ( 17 | 18 | {wavePickers.map((x, i) => ( 19 | 20 | 28 | 33 | 34 | ))} 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/settings/components/midi-input.tsx: -------------------------------------------------------------------------------- 1 | import { MdPiano } from 'react-icons/md'; 2 | import styled from 'styled-components'; 3 | import { Label, Select, IconButton } from '@waveform/ui-kit'; 4 | import { useState } from 'react'; 5 | import { Popover } from 'antd'; 6 | import { useBehaviorSubject } from '@waveform/rxjs-react'; 7 | import { useMidiController } from '../../common/modules'; 8 | 9 | const Content = styled.div` 10 | min-width: 300px; 11 | height: 55px; 12 | `; 13 | 14 | export const MidiInput = () => { 15 | const [open, setOpen] = useState(false); 16 | const [{ $midiInputs, $selectedInputId }, { selectMidiInput }] = useMidiController(); 17 | const midiInputs = useBehaviorSubject($midiInputs); 18 | const selected = useBehaviorSubject($selectedInputId); 19 | return ( 20 | 25 | 26 | 61 | 62 | {!active && } 63 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 76 | 77 | 78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /libs/ui-kit/src/line-chart/components/line-chart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { scaleLinear } from 'd3-scale'; 3 | import { line } from 'd3-shape'; 4 | import styled from 'styled-components'; 5 | import { Vector2D } from '@waveform/math'; 6 | import { theme } from '../../common/constants'; 7 | import { LineChartContext } from '../constants'; 8 | 9 | type Props = { 10 | padding?: Vector2D; 11 | domainX?: [number, number]; 12 | domainY?: [number, number]; 13 | } & React.DetailedHTMLProps, HTMLDivElement>; 14 | 15 | const Root = styled.div` 16 | background: ${theme.colors.primaryDark}; 17 | overflow: hidden; 18 | height: 100%; 19 | position: relative; 20 | `; 21 | 22 | const Svg = styled.svg` 23 | position: absolute; 24 | width: 100%; 25 | height: 100%; 26 | `; 27 | 28 | export const LineChart = ({ 29 | padding = [5, 2], 30 | domainX: [domainXMin, domainXMax] = [0, 1], 31 | domainY: [domainYMin, domainYMax] = [0, 1], 32 | children, 33 | ...rest 34 | }: React.PropsWithChildren) => { 35 | const ref = React.useRef(null); 36 | const [paddingX, paddingY] = padding; 37 | const [[width, height], setSize] = React.useState([0, 0]); 38 | 39 | React.useEffect(() => { 40 | const onResize = () => { 41 | if (!ref.current) return; 42 | setSize([ref.current.clientWidth, ref.current.clientHeight]); 43 | }; 44 | onResize(); 45 | window.addEventListener('resize', onResize); 46 | return () => { 47 | window.removeEventListener('resize', onResize); 48 | }; 49 | }, []); 50 | 51 | const [scaleX, scaleY, lineFn] = React.useMemo(() => { 52 | const scaleX = scaleLinear([0, width - paddingX * 2]); 53 | const scaleY = scaleLinear([0, height - paddingY * 2]); 54 | scaleX.domain([domainXMin, domainXMax]); 55 | scaleY.domain([domainYMin, domainYMax]); 56 | 57 | const lineFn = line() 58 | .x((d, i) => scaleX(i)) 59 | .y(scaleY); 60 | 61 | return [scaleX, scaleY, lineFn]; 62 | }, [width, height, paddingX, paddingY, domainXMin, domainXMax, domainYMin, domainYMax]); 63 | return ( 64 | 65 | 66 | {width !== 0 && ( 67 | 68 | {children} 69 | 70 | )} 71 | 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/services/rx-model.ts: -------------------------------------------------------------------------------- 1 | import { Subscription } from 'rxjs'; 2 | import { Initializers, ModelInternal, ModelFactory, Plugin } from '../types'; 3 | 4 | type State = M | ((initial: I) => M); 5 | 6 | // @todo improvements - make a model by types, then initial it with values 7 | export const rxModel = < 8 | M extends Record, 9 | A extends Record, 10 | I 11 | >( 12 | model: State, 13 | initializers: Initializers = { actions: [], subscriptions: [], plugins: [], destroy: [] } 14 | ): ModelFactory => { 15 | const cloneSelf = ( 16 | model: State, 17 | opts: Partial> 18 | ): ModelFactory => { 19 | const actions = [...initializers.actions, ...(opts.actions ?? [])] as ((model: M) => A & NA)[]; 20 | const subscriptions = [...initializers.subscriptions, ...(opts.subscriptions ?? [])]; 21 | const plugins = [...initializers.plugins, ...(opts.plugins ?? [])]; 22 | const destroy = [...initializers.destroy, ...(opts.destroy ?? [])]; 23 | return rxModel(model, { actions, subscriptions, plugins, destroy }); 24 | }; 25 | 26 | const actions = (fn: (model: M) => NA) => cloneSelf(model, { actions: [fn] }); 27 | const subscriptions = (fn: (model: M, actions: A) => Subscription | Subscription[]) => 28 | cloneSelf(model, { subscriptions: [fn] }); 29 | const plugins = (plugin: Plugin | Array>) => 30 | cloneSelf(model, { plugins: Array.isArray(plugin) ? plugin : [plugin] }); 31 | const destroy = (fn: (model: M) => void) => cloneSelf(model, { destroy: [fn] }); 32 | 33 | const init = (name: string, initial: I): ModelInternal => { 34 | const meta = { name, active: true }; 35 | const modelValue = typeof model === 'function' ? model(initial) : model; 36 | const actions = initializers.actions.reduce((sum, fn) => ({ ...sum, ...fn(modelValue) }), {} as A); 37 | const subscriptions = initializers.subscriptions.map((fn) => fn(modelValue, actions)).flat(); 38 | initializers.plugins.forEach((plugin) => plugin?.onInit?.(modelValue, meta)); 39 | 40 | const stop = () => { 41 | meta.active = false; 42 | initializers.destroy.forEach((fn) => fn(modelValue)); 43 | subscriptions.forEach((subscription) => subscription.unsubscribe()); 44 | initializers.plugins.forEach((plugin) => plugin?.onStop?.(modelValue, meta)); 45 | }; 46 | return [modelValue, actions, { stop, meta }]; 47 | }; 48 | return { 49 | init, 50 | actions, 51 | subscriptions, 52 | plugins, 53 | destroy, 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /libs/ui-kit/src/piano-keyboard/octave.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import cls from 'classnames'; 4 | import { theme } from '../common/constants'; 5 | import { Notes } from '../common/types'; 6 | 7 | const Root = styled.div` 8 | display: flex; 9 | `; 10 | 11 | const KeyWhite = styled.div` 12 | width: 40px; 13 | height: 100px; 14 | border-radius: 0 0 12px 12px; 15 | background: ${theme.colors.white}; 16 | box-shadow: inset 0px -2px 3px 1px rgb(127 140 141 / 20%); 17 | 18 | &.pressed { 19 | background: linear-gradient(180deg, rgba(255, 255, 255, 1) 0%, rgba(236, 240, 241, 1) 100%); 20 | box-shadow: inset 0px -2px 6px 2px rgb(127 140 141 / 20%); 21 | } 22 | `; 23 | 24 | const KeyWhiteOffset = styled(KeyWhite)` 25 | margin-left: -10px; 26 | `; 27 | 28 | const KeyBlack = styled.div` 29 | box-shadow: -1px -1px 2px rgb(255 255 255 / 20%) inset, 0 -5px 2px 3px rgb(0 0 0 / 60%) inset; 30 | background: linear-gradient(45deg, #222 0%, #555 100%); 31 | border-radius: 0 0 6px 6px; 32 | 33 | height: 50px; 34 | width: 20px; 35 | margin-left: -10px; 36 | z-index: 1; 37 | 38 | &.pressed { 39 | box-shadow: -1px -1px 2px rgb(255 255 255 / 20%) inset, 0 -2px 2px 3px rgb(0 0 0 / 60%) inset, 40 | 0 1px 2px rgb(0 0 0 / 50%); 41 | background: linear-gradient(to right, #444 0%, #222 100%); 42 | } 43 | `; 44 | 45 | interface Props { 46 | pressed: Notes[]; 47 | onPress?: (note: Notes) => void; 48 | onRelease?: (note: Notes) => void; 49 | } 50 | 51 | export const Octave = ({ onPress, onRelease, pressed }: Props) => { 52 | const onPressInternal = (note: Notes) => () => { 53 | onPress?.(note); 54 | const onMouseUp = () => { 55 | onRelease?.(note); 56 | document.removeEventListener('mouseup', onMouseUp); 57 | }; 58 | document.addEventListener('mouseup', onMouseUp); 59 | }; 60 | 61 | const keyProps = (note: Notes) => ({ 62 | onMouseDown: onPressInternal(note), 63 | className: cls({ pressed: pressed.includes(note) }), 64 | }); 65 | 66 | return ( 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /libs/ui-kit/src/analyser/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { number } from '@waveform/math'; 4 | import { theme } from '@waveform/ui-kit'; 5 | 6 | interface Props { 7 | analyser: AnalyserNode; 8 | fftSize?: number; 9 | } 10 | 11 | const Canvas = styled.canvas` 12 | position: absolute; 13 | `; 14 | 15 | export const Analyser = ({ analyser, fftSize = 9 }: Props) => { 16 | const rootRef = React.useRef(null); 17 | const ref = React.useRef(null); 18 | const [[width, height], setSize] = React.useState<[number, number]>([300, 150]); 19 | 20 | React.useEffect(() => { 21 | const onResize = () => { 22 | if (!rootRef.current) return; 23 | setSize([rootRef.current.clientWidth, Math.floor(rootRef.current.clientHeight)]); 24 | }; 25 | onResize(); 26 | window.addEventListener('resize', onResize); 27 | return () => { 28 | window.removeEventListener('resize', onResize); 29 | }; 30 | }, []); 31 | 32 | React.useEffect(() => { 33 | if (!ref.current) return; 34 | let animationFrameId: number; 35 | const ctx = ref.current.getContext('2d') as CanvasRenderingContext2D; 36 | analyser.fftSize = number.powerOfTwo(fftSize); 37 | const bufferLength = analyser.frequencyBinCount; 38 | const dataArray = new Uint8Array(bufferLength); 39 | 40 | function draw() { 41 | animationFrameId = requestAnimationFrame(draw); 42 | analyser.getByteTimeDomainData(dataArray); 43 | // analyser.getByteFrequencyData(frequencyData); 44 | ctx.fillStyle = theme.colors.primaryDark; 45 | ctx.fillRect(0, 0, width, height); 46 | 47 | ctx.lineWidth = 2; 48 | ctx.strokeStyle = theme.colors.accent; 49 | ctx.beginPath(); 50 | 51 | const sliceWidth = width / bufferLength; 52 | let x = 0; 53 | for (let i = 0; i < bufferLength; i++) { 54 | const v = dataArray[i] / 128.0; 55 | const y = v * (height / 2); 56 | 57 | if (i === 0) { 58 | ctx.moveTo(x, y); 59 | } else { 60 | ctx.lineTo(x, y); 61 | } 62 | 63 | // ctx.fillStyle = 'rgba(52, 152, 219, 0.7)'; 64 | // ctx.fillRect(x, size[1] + 10, sliceWidth, -frequencyData[i]); 65 | x += sliceWidth; 66 | } 67 | ctx.lineTo(width, height / 2); 68 | ctx.stroke(); 69 | } 70 | 71 | draw(); 72 | return () => { 73 | window.cancelAnimationFrame(animationFrameId); 74 | }; 75 | }, [width, height, analyser, fftSize]); 76 | 77 | return ( 78 |
79 | 80 |
81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/plugins/snapshot/plugin.ts: -------------------------------------------------------------------------------- 1 | import { isRecord } from '../../utils'; 2 | import { isSnapshotable, isSnapshotValue } from './services'; 3 | import { Plugin } from '../../types'; 4 | 5 | type ModelForSnapshot = { model: Record; keys: Array }; 6 | 7 | export const snapshotPlugin = () => { 8 | let initSnapshot: Record | null = null; 9 | 10 | const setInitSnapshot = (snapshot?: string) => { 11 | if (!snapshot) { 12 | initSnapshot = null; 13 | return; 14 | } 15 | const data = JSON.parse(snapshot); 16 | if (!isRecord(data)) return; 17 | initSnapshot = data; 18 | }; 19 | 20 | const setSnapshotToModel = (snapshot: unknown, model?: ModelForSnapshot['model']) => { 21 | if (!isRecord(snapshot) || !model) return; 22 | 23 | for (const key in snapshot) { 24 | const modelValue = model[key]; 25 | const snap = snapshot[key]; 26 | if (isSnapshotable(modelValue) && isSnapshotValue(snap)) { 27 | modelValue.setSnapshot(snap); 28 | } 29 | } 30 | }; 31 | 32 | const modelForSnap = new Map(); 33 | const modelPlugin = >(keys?: Array): Plugin => ({ 34 | onInit: (model, meta) => { 35 | modelForSnap.set(meta.name, { model, keys: keys ? (keys as (string | number)[]) : Object.keys(model) }); 36 | if (initSnapshot && Object.keys(initSnapshot).includes(meta.name)) { 37 | const value = initSnapshot[meta.name]; 38 | setSnapshotToModel(value, model); 39 | } 40 | }, 41 | onStop: (model, meta) => { 42 | modelForSnap.delete(meta.name); 43 | }, 44 | }); 45 | const getSnapshot = (): string => { 46 | const primitiveStore: Record = {}; 47 | for (const [name, { model, keys }] of modelForSnap) { 48 | const modelStore: Record = {}; 49 | for (const key of keys) { 50 | const value = model[key]; 51 | if (isSnapshotable(value)) { 52 | modelStore[key] = value.getSnapshot(); 53 | } 54 | } 55 | primitiveStore[name] = modelStore; 56 | } 57 | return JSON.stringify(primitiveStore); 58 | }; 59 | const loadSnapshot = (snapshot: string): void => { 60 | const data = JSON.parse(snapshot); 61 | if (!isRecord(data)) return; 62 | 63 | for (const name in data) { 64 | const modelItem = modelForSnap.get(name); 65 | const modelSnapshot = data[name]; 66 | setSnapshotToModel(modelSnapshot, modelItem?.model); 67 | } 68 | }; 69 | return { modelPlugin, getSnapshot, loadSnapshot, setInitSnapshot }; 70 | }; 71 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/manual-wavetable/wave-upscale/modules/index.ts: -------------------------------------------------------------------------------- 1 | import { distinctUntilChanged, filter, map, mergeWith } from 'rxjs'; 2 | import { rxModel, rxModelReact, ArrayBS, PrimitiveBS } from '@waveform/rxjs-react'; 3 | import { number, wave } from '@waveform/math'; 4 | import { AudioProcessorModule } from '../../../common/modules'; 5 | import { ManualWavetableModule } from '../../modules'; 6 | 7 | interface Dependencies { 8 | audioProcessor: AudioProcessorModule; 9 | manualWavetable: ManualWavetableModule; 10 | } 11 | 12 | const waveUpscale = ({ 13 | audioProcessor: [, { setWave }], 14 | manualWavetable: [{ $rate, $wave }], 15 | }: Dependencies) => 16 | rxModel(() => { 17 | const $inputWave = new ArrayBS([]); 18 | const $outputRate = new PrimitiveBS(10); 19 | const $outputWave = new ArrayBS<[number[], number[]]>([[], []]); 20 | const $phase = new PrimitiveBS(0); 21 | return { $outputRate, $outputWave, $inputWave, $phase }; 22 | }) 23 | .actions(({ $outputRate, $phase }) => ({ 24 | setOutputRate: (value: number) => $outputRate.next(value), 25 | setPhase: (value: number) => $phase.next(value), 26 | })) 27 | .subscriptions(({ $inputWave, $outputRate, $phase, $outputWave }) => [ 28 | $wave.subscribe($inputWave), 29 | $rate.pipe(filter((rate) => rate > $outputRate.value)).subscribe($outputRate), 30 | $inputWave 31 | .pipe( 32 | mergeWith($rate, $outputRate, $phase), 33 | distinctUntilChanged(), 34 | map(() => [...$inputWave.value].splice(0, number.powerOfTwo($rate.value))), 35 | map((croppedWave): number[] => { 36 | if ($rate.value >= $outputRate.value) return croppedWave; 37 | const fArray = []; 38 | for (let i = 0; i < number.powerOfTwo($rate.value); i++) { 39 | const from = croppedWave[i]; 40 | const to = croppedWave[i + 1] ?? croppedWave[0]; 41 | const stepsCount = number.powerOfTwo($outputRate.value) / number.powerOfTwo($rate.value); 42 | const diff = to - from; 43 | const stepDiff = diff / stepsCount; 44 | for (let j = 0; j < stepsCount; j++) { 45 | fArray.push(from + stepDiff * j); 46 | } 47 | } 48 | return [...fArray.splice(Math.round(($phase.value / 100) * fArray.length)), ...fArray]; 49 | }), 50 | map(wave.realWithImag) 51 | ) 52 | .subscribe($outputWave), 53 | $outputWave.subscribe(setWave), 54 | ]); 55 | 56 | export const { ModelProvider: WaveUpscaleProvider, useModel: useWaveUpscale } = rxModelReact( 57 | 'waveUpscale', 58 | waveUpscale 59 | ); 60 | -------------------------------------------------------------------------------- /libs/ui-kit/src/draggable-number/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cls from 'classnames'; 3 | import { CaretUpOutlined, CaretDownOutlined } from '@ant-design/icons'; 4 | import styled from 'styled-components'; 5 | import { number, vector2d, Vector2D } from '@waveform/math'; 6 | import { text10 } from '../common/styles'; 7 | import { theme } from '../common/constants'; 8 | 9 | interface Props { 10 | value: number; 11 | onChange?: (value: number) => void; 12 | label?: string; 13 | range?: Vector2D; 14 | } 15 | 16 | const Root = styled.div` 17 | font-size: 12px; 18 | line-height: 12px; 19 | display: inline-flex; 20 | align-items: end; 21 | justify-content: space-between; 22 | gap: 8px; 23 | background: ${theme.colors.primaryDark}; 24 | color: ${theme.colors.white}; 25 | border-radius: 3px; 26 | padding: 5px 6px; 27 | cursor: ns-resize; 28 | `; 29 | 30 | const Label = styled.div` 31 | ${text10}; 32 | font-weight: 600; 33 | `; 34 | 35 | const ValueContainer = styled.div` 36 | display: flex; 37 | gap: 3px; 38 | align-items: center; 39 | `; 40 | 41 | const Value = styled.div` 42 | width: 12px; 43 | text-align: right; 44 | `; 45 | 46 | const Controls = styled.div` 47 | display: flex; 48 | flex-direction: column; 49 | margin: -5px 0; 50 | transition: all 150ms; 51 | 52 | &.active { 53 | color: ${theme.colors.accent}; 54 | } 55 | 56 | .up { 57 | margin-bottom: -2px; 58 | } 59 | 60 | .down { 61 | margin-top: -2px; 62 | } 63 | `; 64 | 65 | export const DraggableNumber = ({ value, onChange, label, range = [0, 10] }: Props) => { 66 | const [active, setActive] = React.useState(false); 67 | const onMouseDown = (e: React.MouseEvent) => { 68 | const initial = vector2d.fromMouseEvent(e); 69 | const mouseMove = (e: MouseEvent) => { 70 | const [, diffY] = vector2d.invertY(vector2d.subtract(vector2d.fromMouseEvent(e), initial)); 71 | onChange?.(number.thresholds(value + Math.round(diffY / 10), ...range)); 72 | }; 73 | 74 | const cleanUp = () => { 75 | setActive(false); 76 | document.removeEventListener('mousemove', mouseMove); 77 | document.removeEventListener('mouseup', cleanUp); 78 | }; 79 | 80 | document.addEventListener('mousemove', mouseMove); 81 | document.addEventListener('mouseup', cleanUp); 82 | setActive(true); 83 | }; 84 | return ( 85 | 86 | 87 | 88 | {value} 89 | 90 | 91 | 92 | 93 | 94 | 95 | ); 96 | }; 97 | -------------------------------------------------------------------------------- /apps/waveform/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "waveform", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/waveform/src", 5 | "projectType": "application", 6 | "targets": { 7 | "build": { 8 | "executor": "@nrwl/webpack:webpack", 9 | "outputs": ["{options.outputPath}"], 10 | "defaultConfiguration": "production", 11 | "options": { 12 | "compiler": "babel", 13 | "outputPath": "dist/apps/waveform", 14 | "index": "apps/waveform/src/index.html", 15 | "baseHref": "/", 16 | "main": "apps/waveform/src/main.tsx", 17 | "polyfills": "apps/waveform/src/polyfills.ts", 18 | "tsConfig": "apps/waveform/tsconfig.app.json", 19 | "assets": ["apps/waveform/src/favicon.ico", "apps/waveform/src/assets"], 20 | "styles": [], 21 | "scripts": [], 22 | "webpackConfig": "@nrwl/react/plugins/webpack" 23 | }, 24 | "configurations": { 25 | "development": { 26 | "extractLicenses": false, 27 | "optimization": false, 28 | "sourceMap": true, 29 | "vendorChunk": true 30 | }, 31 | "production": { 32 | "fileReplacements": [ 33 | { 34 | "replace": "apps/waveform/src/environments/environment.ts", 35 | "with": "apps/waveform/src/environments/environment.prod.ts" 36 | } 37 | ], 38 | "optimization": true, 39 | "outputHashing": "all", 40 | "sourceMap": false, 41 | "namedChunks": false, 42 | "extractLicenses": true, 43 | "vendorChunk": false 44 | } 45 | } 46 | }, 47 | "serve": { 48 | "executor": "@nrwl/webpack:dev-server", 49 | "defaultConfiguration": "development", 50 | "options": { 51 | "buildTarget": "waveform:build", 52 | "hmr": true, 53 | "port": 4200 54 | }, 55 | "configurations": { 56 | "development": { 57 | "buildTarget": "waveform:build:development", 58 | "proxyConfig": "proxy.conf.json" 59 | }, 60 | "production": { 61 | "buildTarget": "waveform:build:production", 62 | "hmr": false 63 | } 64 | } 65 | }, 66 | "lint": { 67 | "executor": "@nrwl/linter:eslint", 68 | "outputs": ["{options.outputFile}"], 69 | "options": { 70 | "lintFilePatterns": ["apps/waveform/**/*.{ts,tsx,js,jsx}"] 71 | } 72 | }, 73 | "test": { 74 | "executor": "@nrwl/jest:jest", 75 | "outputs": ["coverage/apps/waveform"], 76 | "options": { 77 | "jestConfig": "apps/waveform/jest.config.ts", 78 | "passWithNoTests": true 79 | } 80 | } 81 | }, 82 | "tags": [] 83 | } 84 | -------------------------------------------------------------------------------- /libs/rxjs-react/src/services/__tests__/rx-model.spec.tsx: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { rxModel } from '../rx-model'; 4 | 5 | const simpleData = { value: 1, string: 'string', object: { value: 1, string: 'string' } }; 6 | const rxjsData = { $array: new BehaviorSubject([1, 2, 3]), $object: new BehaviorSubject(simpleData) }; 7 | 8 | describe('RxModel', () => { 9 | it('should return RxModelFactory', () => { 10 | const model = rxModel(simpleData); 11 | expect(typeof model.init).toBe('function'); 12 | expect(typeof model.actions).toBe('function'); 13 | expect(typeof model.subscriptions).toBe('function'); 14 | expect(typeof model.plugins).toBe('function'); 15 | }); 16 | it('should init with a initial params', () => { 17 | const [model] = rxModel((value: number) => ({ 18 | ...simpleData, 19 | value, 20 | })).init('name', 32); 21 | expect(model).toEqual({ ...simpleData, value: 32 }); 22 | }); 23 | it('should pass a model', () => { 24 | const model = rxModel(simpleData); 25 | const [modelData] = model.init('name', {}); 26 | expect(modelData).toEqual(simpleData); 27 | }); 28 | it('should attach an actions', () => { 29 | const fn = jest.fn(); 30 | const [, actions] = rxModel(simpleData) 31 | .actions(({ value }) => ({ 32 | fn, 33 | returnValue: () => value, 34 | })) 35 | .init('name', {}); 36 | expect(actions.fn).toEqual(fn); 37 | expect(fn.mock.calls.length).toEqual(0); 38 | 39 | actions.fn(); 40 | expect(fn.mock.calls.length).toEqual(1); 41 | 42 | expect(actions.returnValue()).toEqual(simpleData.value); 43 | }); 44 | 45 | it('should attach and stop rx subscriptions', () => { 46 | const modelFactory = rxModel(rxjsData).subscriptions(({ $array }) => $array.subscribe()); 47 | expect(rxjsData.$array.observed).toBeFalsy(); 48 | 49 | const [, , { stop }] = modelFactory.init('name', {}); 50 | expect(rxjsData.$array.observed).toBeTruthy(); 51 | 52 | stop(); 53 | expect(rxjsData.$array.observed).toBeFalsy(); 54 | }); 55 | 56 | it('should attach plugins', () => { 57 | const onInit = jest.fn(); 58 | const onStop = jest.fn(); 59 | const modelFactory = rxModel(simpleData).plugins({ 60 | onInit, 61 | onStop, 62 | }); 63 | expect(onInit.mock.calls.length).toBe(0); 64 | expect(onStop.mock.calls.length).toBe(0); 65 | 66 | const [, , { stop }] = modelFactory.init('name', {}); 67 | expect(onInit.mock.calls[0]).toEqual([simpleData, { active: true, name: 'name' }]); 68 | expect(onInit.mock.calls.length).toBe(1); 69 | 70 | stop(); 71 | expect(onStop.mock.calls[0]).toEqual([simpleData, { active: false, name: 'name' }]); 72 | expect(onStop.mock.calls.length).toBe(1); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "waveform", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "concurrently -k -p \"[{name}]\" -n \"api,client\" -c \"yellow.bold,green.bold\" \"netlify dev\" \"npm run client:start\"", 7 | "client:start": "nx serve", 8 | "build": "nx build", 9 | "test": "nx test", 10 | "prettier:check": "prettier -l './apps/**/*.{ts,tsx}' './libs/**/*.{ts,tsx}'", 11 | "prettier:fix": "prettier --write './apps/**/*.{ts,tsx}' './libs/**/*.{ts,tsx}'" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@ant-design/icons": "^4.7.0", 16 | "@aws-sdk/client-s3": "^3.245.0", 17 | "@aws-sdk/s3-request-presigner": "^3.245.0", 18 | "@tanstack/react-query": "^4.18.0", 19 | "@types/webmidi": "^2.0.6", 20 | "antd": "^5.0.7", 21 | "classnames": "^2.3.2", 22 | "core-js": "^3.6.5", 23 | "d3-scale": "^4.0.2", 24 | "d3-shape": "^3.1.0", 25 | "fft.js": "^4.0.4", 26 | "hex-to-rgba": "^2.0.1", 27 | "lodash": "^4.17.21", 28 | "pretty-bytes": "^6.0.0", 29 | "react": "18.2.0", 30 | "react-dom": "18.2.0", 31 | "react-hot-toast": "^2.4.0", 32 | "react-icons": "^4.8.0", 33 | "react-is": "18.2.0", 34 | "regenerator-runtime": "0.13.7", 35 | "rxjs": "^7.5.7", 36 | "styled-components": "5.3.5", 37 | "tslib": "^2.3.0" 38 | }, 39 | "devDependencies": { 40 | "@netlify/functions": "^1.3.0", 41 | "@nrwl/cli": "14.8.4", 42 | "@nrwl/cypress": "14.8.4", 43 | "@nrwl/eslint-plugin-nx": "14.8.4", 44 | "@nrwl/jest": "14.8.4", 45 | "@nrwl/linter": "14.8.4", 46 | "@nrwl/nx-cloud": "latest", 47 | "@nrwl/react": "^15.2.0", 48 | "@nrwl/web": "^15.2.0", 49 | "@nrwl/workspace": "14.8.4", 50 | "@testing-library/react": "13.4.0", 51 | "@types/d3-scale": "^4.0.2", 52 | "@types/d3-shape": "^3.1.0", 53 | "@types/jest": "28.1.1", 54 | "@types/lodash": "^4.14.188", 55 | "@types/node": "16.11.7", 56 | "@types/react": "18.0.20", 57 | "@types/react-dom": "18.0.6", 58 | "@types/react-is": "17.0.3", 59 | "@types/styled-components": "5.1.26", 60 | "@typescript-eslint/eslint-plugin": "^5.36.1", 61 | "@typescript-eslint/parser": "^5.36.1", 62 | "babel-jest": "28.1.1", 63 | "babel-plugin-styled-components": "1.10.7", 64 | "concurrently": "^7.6.0", 65 | "cypress": "^10.7.0", 66 | "eslint": "~8.15.0", 67 | "eslint-config-prettier": "8.1.0", 68 | "eslint-plugin-cypress": "^2.10.3", 69 | "eslint-plugin-import": "2.26.0", 70 | "eslint-plugin-jsx-a11y": "6.6.1", 71 | "eslint-plugin-react": "7.31.8", 72 | "eslint-plugin-react-hooks": "4.6.0", 73 | "jest": "28.1.1", 74 | "jest-environment-jsdom": "28.1.1", 75 | "nx": "14.8.4", 76 | "prettier": "^2.6.2", 77 | "react-test-renderer": "18.2.0", 78 | "ts-jest": "28.0.5", 79 | "ts-node": "10.9.1", 80 | "typescript": "~4.8.2" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/common/modules/synth-core.ts: -------------------------------------------------------------------------------- 1 | import { PrimitiveBS, ArrayBS, rxModel, rxModelReact } from '@waveform/rxjs-react'; 2 | 3 | const synthCore = () => 4 | rxModel(() => { 5 | const audioCtx = new AudioContext(); 6 | const preGain = audioCtx.createGain(); 7 | const masterLimiter = audioCtx.createDynamicsCompressor(); 8 | const masterGain = audioCtx.createGain(); 9 | 10 | // (async () => { 11 | // await audioCtx.audioWorklet.addModule("/assets/examples/random-noise-processor.js"); 12 | // const whiteNoiseNode = new AudioWorkletNode( 13 | // audioCtx, 14 | // "white-noise-processor" 15 | // ); 16 | // whiteNoiseNode.connect(preGain) 17 | // })() 18 | 19 | preGain.gain.setValueAtTime(1, audioCtx.currentTime); 20 | 21 | masterLimiter.threshold.setValueAtTime(-16.0, audioCtx.currentTime); 22 | masterLimiter.ratio.setValueAtTime(20, audioCtx.currentTime); 23 | masterLimiter.attack.setValueAtTime(0.001, audioCtx.currentTime); 24 | masterLimiter.release.setValueAtTime(0.01, audioCtx.currentTime); 25 | 26 | masterLimiter.connect(masterGain); 27 | masterGain.connect(audioCtx.destination); 28 | 29 | const $masterGain = new PrimitiveBS(0.8); 30 | const $limiter = new PrimitiveBS(false); 31 | const $midNodes = new ArrayBS([]); // nodes to connect between preGain and masterLimiter 32 | 33 | return { audioCtx, preGain, masterLimiter, masterGain, $midNodes, $masterGain, $limiter }; 34 | }) 35 | .actions(({ $masterGain, $midNodes }) => ({ 36 | setMasterGain: (value: number) => $masterGain.next(value), 37 | addMidNode: (node: AudioNode) => $midNodes.next([...$midNodes.value, node]), 38 | removeMidNode: (node: AudioNode) => { 39 | node.disconnect(); 40 | $midNodes.next($midNodes.value.filter((midNode) => midNode !== node)); 41 | }, 42 | })) 43 | .subscriptions(({ $masterGain, $midNodes, audioCtx, masterGain, preGain, masterLimiter }) => [ 44 | $masterGain.subscribe((value) => masterGain.gain.setValueAtTime(value, audioCtx.currentTime)), 45 | $midNodes.subscribe((nodes) => { 46 | preGain.disconnect(); 47 | if (nodes.length > 0) { 48 | for (let i = 0; i < nodes.length; i++) { 49 | const node = nodes[i]; 50 | const next = nodes[i + 1]; 51 | if (i === 0) preGain.connect(node); 52 | if (next) node.connect(next); 53 | else node.connect(masterLimiter); 54 | } 55 | } else { 56 | preGain.connect(masterLimiter); 57 | } 58 | }), 59 | ]); 60 | 61 | export const { ModelProvider: SynthCoreProvider, useModel: useSynthCore } = rxModelReact( 62 | 'synthCore', 63 | synthCore 64 | ); 65 | 66 | export type SynthCoreModule = ReturnType; 67 | export type SynthCoreModel = SynthCoreModule[0]; 68 | export type SynthCoreActions = SynthCoreModule[1]; 69 | -------------------------------------------------------------------------------- /apps/waveform/src/app/synth/wave-table-editor/common/components/wave-selector/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { PlusOutlined } from '@ant-design/icons'; 4 | import { useBehaviorSubject, useObservable } from '@waveform/rxjs-react'; 5 | import { theme, LineChart, Line } from '@waveform/ui-kit'; 6 | import { ManualWavetableActions } from '../../../manual-wavetable/modules'; 7 | import { OscillatorModel, OscillatorActions } from '../../../../common/modules'; 8 | import { RxHandle } from '../../../../../common/components'; 9 | 10 | const Root = styled.div` 11 | display: grid; 12 | grid-template-columns: 70px 1fr; 13 | `; 14 | 15 | const HandlersWrapper = styled.div` 16 | display: flex; 17 | flex-direction: column; 18 | gap: 10px; 19 | justify-content: space-between; 20 | align-items: center; 21 | `; 22 | 23 | const WavesPreview = styled.div` 24 | display: flex; 25 | gap: 4px; 26 | background: ${theme.colors.primaryDark}; 27 | padding: 2px; 28 | overflow-x: scroll; 29 | `; 30 | 31 | const Wave = styled.div<{ selected: boolean }>` 32 | width: 59px; 33 | height: 59px; 34 | border: 1px solid 35 | ${({ selected }) => (selected ? theme.colors.accent : theme.colors.primaryDarkHighContrast)}; 36 | `; 37 | const NewWave = styled.div` 38 | display: flex; 39 | align-items: center; 40 | justify-content: center; 41 | width: 57px; 42 | height: 57px; 43 | color: ${theme.colors.primaryDarkHighContrast}; 44 | border: 2px solid transparent; 45 | transition: all 150ms; 46 | cursor: pointer; 47 | 48 | &:hover { 49 | border: 2px solid ${theme.colors.primaryDarkHighContrast}; 50 | } 51 | `; 52 | 53 | export type Props = Pick & 54 | Pick & 55 | Partial>; 56 | 57 | export const WaveSelector = ({ $waveTable, $current, $wave, setCurrent, cloneCurrent }: Props) => { 58 | const waveTable = useBehaviorSubject($waveTable); 59 | useObservable($wave, []); 60 | return ( 61 | 62 | 63 | `${value + 1}`} 71 | /> 72 | 73 | 74 | {waveTable.map((wave, i) => ( 75 | setCurrent(i)} selected={i === $current.value}> 76 | 77 | 78 | 79 | 80 | ))} 81 | {cloneCurrent && ( 82 | 83 | 84 | 85 | )} 86 | 87 | 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /libs/ui-kit/src/filter/services/index.ts: -------------------------------------------------------------------------------- 1 | import { FilterProps } from '../types'; 2 | 3 | type ChartArray = [number, number][]; 4 | 5 | const emptyLine: ChartArray = [ 6 | [10, 0], 7 | [22050, 0], 8 | ]; 9 | 10 | const lowpass = ({ resonance, cutoff }: FilterProps): ChartArray => [ 11 | [10, 0], 12 | [cutoff - cutoff * 0.01, 0], 13 | [cutoff, resonance / 15], 14 | [cutoff + cutoff * 0.1, 0], 15 | [cutoff * 15, -1], 16 | ]; 17 | 18 | const highpass = ({ resonance, cutoff }: FilterProps): ChartArray => [ 19 | [cutoff - cutoff * 0.93, -1], 20 | [cutoff - cutoff * 0.1, 0], 21 | [cutoff + cutoff * 0.07, resonance / 15], 22 | [cutoff, 0], 23 | [22050, 0], 24 | ]; 25 | 26 | const bandpass = ({ resonance, cutoff }: FilterProps): ChartArray => { 27 | const data: [number, number][] = [ 28 | [(cutoff * resonance) / 1100, -1], 29 | [(cutoff * resonance) / 1100, -1], 30 | [cutoff - cutoff * 0.05, -resonance / 1100], 31 | [cutoff, 0], 32 | [cutoff, 0], 33 | [cutoff + cutoff * 0.05, -resonance / 1100], 34 | [cutoff / (resonance / 1100), -1], 35 | [cutoff / (resonance / 1100), -1], 36 | [22050, -1], 37 | ]; 38 | if (data[0][0] > 10) data.unshift([10, -1]); 39 | return data; 40 | }; 41 | 42 | const lowshelf = ({ gain, cutoff }: FilterProps): ChartArray => { 43 | const data: [number, number][] = [ 44 | [cutoff - cutoff * 0.5, gain / 50], 45 | [cutoff - cutoff * 0.5, gain / 50], 46 | [cutoff + cutoff * 0.9, 0], 47 | [cutoff + cutoff * 0.9, 0], 48 | [22050, 0], 49 | ]; 50 | if (data[0][0] > 10) data.unshift([10, gain / 50]); 51 | return data; 52 | }; 53 | 54 | const highshelf = ({ gain, cutoff }: FilterProps): ChartArray => { 55 | const data: [number, number][] = [ 56 | // [10, 0], 57 | [cutoff - cutoff * 0.5, 0], 58 | [cutoff - cutoff * 0.5, 0], 59 | [cutoff + cutoff * 0.9, gain / 50], 60 | [cutoff + cutoff * 0.9, gain / 50], 61 | [22050, gain / 50], 62 | ]; 63 | if (data[0][0] > 10) data.unshift([10, 0]); 64 | return data; 65 | }; 66 | 67 | const peaking = ({ gain, cutoff, resonance }: FilterProps): ChartArray => { 68 | if (resonance === 0) return emptyLine; 69 | const resNormalised = resonance / 12; 70 | return [ 71 | [10, 0], 72 | [cutoff * resNormalised, 0], 73 | [cutoff * resNormalised, 0], 74 | [cutoff, gain / 40], 75 | [cutoff, gain / 40], 76 | [cutoff / resNormalised, 0], 77 | [cutoff / resNormalised, 0], 78 | [22050, 0], 79 | ]; 80 | }; 81 | 82 | export const getChartData = (type: BiquadFilterType, props: FilterProps): ChartArray => { 83 | switch (type) { 84 | case 'lowpass': 85 | return lowpass(props); 86 | case 'highpass': 87 | return highpass(props); 88 | case 'bandpass': 89 | return bandpass(props); 90 | case 'lowshelf': 91 | return lowshelf(props); 92 | case 'highshelf': 93 | return highshelf(props); 94 | case 'peaking': 95 | return peaking(props); 96 | default: 97 | return emptyLine; 98 | } 99 | }; 100 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/audiofile-wavetable/modules/audiofile.ts: -------------------------------------------------------------------------------- 1 | import { map, mergeWith } from 'rxjs'; 2 | import { PrimitiveBS, ArrayBS, rxModel, rxModelReact } from '@waveform/rxjs-react'; 3 | import { number, typedArray } from '@waveform/math'; 4 | import { AudiofileWavetableModule } from './audiofile-wavetable'; 5 | 6 | interface Props { 7 | audioBuffer: Float32Array; 8 | } 9 | 10 | interface Dependencies { 11 | audiofileWavetable: AudiofileWavetableModule; 12 | } 13 | 14 | const audiofileWavePicker = ({ audiofileWavetable: [, { setWaveTable }] }: Dependencies) => 15 | rxModel(({ audioBuffer }: Props) => { 16 | const waveSizeRange = [2, 11]; 17 | const pickersCountRange = [1, 64]; 18 | 19 | const $phase = new PrimitiveBS(0); 20 | const $waveSize = new PrimitiveBS(9); 21 | const $wavesPickersCount = new PrimitiveBS(32); 22 | const $wavePickers = new ArrayBS([]); 23 | return { 24 | waveSizeRange, 25 | pickersCountRange, 26 | audioBuffer, 27 | $wavesPickersCount, 28 | $wavePickers, 29 | $waveSize, 30 | $phase, 31 | }; 32 | }) 33 | .actions(({ $wavePickers, $wavesPickersCount, $waveSize, $phase }) => ({ 34 | setWavesPickers: (array: number[]) => $wavePickers.next(array), 35 | setWavesPickersCount: (value: number) => $wavesPickersCount.next(value), 36 | setWaveSize: (value: number) => $waveSize.next(value), 37 | setPhase: (value: number) => $phase.next(value), 38 | })) 39 | .subscriptions( 40 | ({ audioBuffer, $wavesPickersCount, $wavePickers, $waveSize, $phase }, { setWavesPickers }) => [ 41 | // set wave-pickers on all audioBuffer length 42 | $wavesPickersCount 43 | .pipe( 44 | mergeWith($phase), 45 | map(() => { 46 | const pickersCount = $wavesPickersCount.value; 47 | const step = Math.floor(audioBuffer.length / pickersCount); 48 | 49 | const result: number[] = []; 50 | for (let i = 0; i < pickersCount; i++) { 51 | result.push(Math.round((i + $phase.value / 100) * step)); 52 | } 53 | return result; 54 | }) 55 | ) 56 | .subscribe(setWavesPickers), 57 | 58 | // set pick wave for every wave-pickers and save to wavetable 59 | $wavePickers 60 | .pipe( 61 | mergeWith($waveSize), 62 | map(() => { 63 | const result: number[][] = []; 64 | const waveSize = number.powerOfTwo($waveSize.value); 65 | for (let i = 0; i < $wavePickers.value.length; i++) { 66 | const picker = $wavePickers.value[i]; 67 | if (picker + waveSize > audioBuffer.length) continue; 68 | result[i] = typedArray.slice(audioBuffer, picker, waveSize); 69 | } 70 | return result; 71 | }) 72 | ) 73 | .subscribe(setWaveTable), 74 | ] 75 | ); 76 | export const { ModelProvider: AudiofileWavePickerProvider, useModel: useAudiofileWavePicker } = rxModelReact( 77 | 'audiofileWavePicker', 78 | audiofileWavePicker 79 | ); 80 | -------------------------------------------------------------------------------- /apps/waveform/src/app/wave-table-editor/common/components/wave-selector/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { PlusOutlined } from '@ant-design/icons'; 4 | import { useBehaviorSubject, useObservable } from '@waveform/rxjs-react'; 5 | import { theme, LineChart, Line } from '@waveform/ui-kit'; 6 | import { number } from '@waveform/math'; 7 | import { WavetableModel, WavetableActions } from '../../modules'; 8 | import { ManualWavetableActions } from '../../../manual-wavetable/modules'; 9 | import { RxHandle } from '../../../../common/components'; 10 | 11 | const Root = styled.div` 12 | display: grid; 13 | grid-template-columns: 70px 1fr; 14 | `; 15 | 16 | const HandlersWrapper = styled.div` 17 | display: flex; 18 | flex-direction: column; 19 | gap: 10px; 20 | justify-content: space-between; 21 | align-items: center; 22 | `; 23 | 24 | const WavesPreview = styled.div` 25 | display: flex; 26 | gap: 4px; 27 | background: ${theme.colors.primaryDark}; 28 | padding: 2px; 29 | overflow-x: scroll; 30 | `; 31 | 32 | const Wave = styled.div<{ selected: boolean }>` 33 | width: 59px; 34 | height: 59px; 35 | border: 1px solid 36 | ${({ selected }) => (selected ? theme.colors.accent : theme.colors.primaryDarkHighContrast)}; 37 | `; 38 | const NewWave = styled.div` 39 | display: flex; 40 | align-items: center; 41 | justify-content: center; 42 | width: 57px; 43 | height: 57px; 44 | color: ${theme.colors.primaryDarkHighContrast}; 45 | border: 2px solid transparent; 46 | transition: all 150ms; 47 | cursor: pointer; 48 | 49 | &:hover { 50 | border: 2px solid ${theme.colors.primaryDarkHighContrast}; 51 | } 52 | `; 53 | 54 | export type Props = Omit & 55 | Pick & 56 | Partial> & { 57 | withRate?: boolean; 58 | }; 59 | 60 | export const WaveSelector = ({ 61 | $waveTable, 62 | $current, 63 | $rate, 64 | $wave, 65 | setCurrent, 66 | cloneCurrent, 67 | withRate = true, 68 | }: Props) => { 69 | const waveTable = useBehaviorSubject($waveTable); 70 | const rate = useBehaviorSubject($rate); 71 | useObservable($wave, []); 72 | return ( 73 | 74 | 75 | 83 | 84 | 85 | {waveTable.map((wave, i) => { 86 | const croppedWave = withRate ? [...wave.value].splice(0, number.powerOfTwo(rate)) : wave.value; 87 | return ( 88 | setCurrent(i)} selected={i === $current.value}> 89 | 90 | 91 | 92 | 93 | ); 94 | })} 95 | {cloneCurrent && ( 96 | 97 | 98 | 99 | )} 100 | 101 | 102 | ); 103 | }; 104 | --------------------------------------------------------------------------------