├── src ├── vite-env.d.ts ├── features │ └── daw │ │ ├── midi-editor │ │ ├── constants │ │ │ └── index.ts │ │ ├── store │ │ │ ├── types │ │ │ │ └── types.ts │ │ │ ├── selectors │ │ │ │ └── index.ts │ │ │ └── midi-editor-slice.ts │ │ ├── midi-body │ │ │ ├── left-menu │ │ │ │ ├── left-menu.tsx │ │ │ │ ├── left-menu-scale-view │ │ │ │ │ └── left-menu-scale-view.tsx │ │ │ │ ├── left-menu-transpose │ │ │ │ │ └── left-menu-transpose.tsx │ │ │ │ ├── left-menu-velocity │ │ │ │ │ └── left-menu-velocity.tsx │ │ │ │ └── left-menu-header │ │ │ │ │ └── left-menu-header.tsx │ │ │ ├── midi-body.tsx │ │ │ └── key-editor │ │ │ │ ├── piano-roll-bar │ │ │ │ └── header │ │ │ │ │ └── piano-roll-bar-header.tsx │ │ │ │ ├── hooks │ │ │ │ └── useMidiEditorDimensions.tsx │ │ │ │ └── midi-editor-key-grid │ │ │ │ └── midi-editor-key-grid.tsx │ │ ├── hooks │ │ │ ├── useMidiEditorHorizontalScroll.tsx │ │ │ └── useMidiEditorVerticalScroll.tsx │ │ ├── midi-editor.tsx │ │ └── midi-header │ │ │ └── midi-header.tsx │ │ ├── player-bar │ │ ├── constants │ │ │ └── player-bar-constants.ts │ │ ├── undo-redo │ │ │ └── undo-redo.tsx │ │ ├── store │ │ │ ├── selectors │ │ │ │ └── index.ts │ │ │ └── playerBarSlice.ts │ │ ├── player-bar.tsx │ │ ├── master-volume │ │ │ └── master-volume.tsx │ │ ├── metronome │ │ │ └── metronome.tsx │ │ └── player │ │ │ └── player.tsx │ │ ├── common │ │ ├── components │ │ │ ├── ruler │ │ │ │ └── constants │ │ │ │ │ └── index.ts │ │ │ ├── wizard │ │ │ │ ├── wizard-context.ts │ │ │ │ ├── use-wizard-context.ts │ │ │ │ ├── types.ts │ │ │ │ └── wizard.tsx │ │ │ ├── piano-roll-key │ │ │ │ ├── readonly │ │ │ │ │ └── readonly-piano-roll-key.tsx │ │ │ │ ├── piano-roll-key.tsx │ │ │ │ ├── types.ts │ │ │ │ ├── common │ │ │ │ │ └── piano-roll-key-skeleton.tsx │ │ │ │ └── editable │ │ │ │ │ └── editable-piano-roll-key.tsx │ │ │ ├── tick-placeholder │ │ │ │ └── tick-placeholder.tsx │ │ │ ├── mix-grid │ │ │ │ ├── mix-grid-item │ │ │ │ │ └── mix-grid-item.tsx │ │ │ │ └── mix-grid.tsx │ │ │ ├── switch │ │ │ │ └── switch.tsx │ │ │ ├── loader │ │ │ │ └── loader.tsx │ │ │ ├── alert │ │ │ │ └── alert.tsx │ │ │ ├── scale-selector │ │ │ │ └── scale-selector.tsx │ │ │ ├── keyboard │ │ │ │ └── key │ │ │ │ │ ├── use-key-item-data.tsx │ │ │ │ │ └── key-item.tsx │ │ │ └── popup-menu │ │ │ │ └── popup-menu.tsx │ │ └── hooks │ │ │ ├── useDebounce.tsx │ │ │ ├── use-outside-click.tsx │ │ │ ├── use-ruler-scroll.tsx │ │ │ ├── useHorizontalResize.tsx │ │ │ └── use-preview-loop-safe-transport-position.tsx │ │ ├── bottom-bar │ │ ├── store │ │ │ ├── selectors │ │ │ │ └── index.ts │ │ │ └── bottom-bar-slice.ts │ │ ├── types │ │ │ └── bottom-up-panel.ts │ │ ├── bottom-bar-item │ │ │ └── bottom-bar-item.tsx │ │ └── bottom-bar.tsx │ │ ├── dialog │ │ ├── modal │ │ │ ├── overlay.tsx │ │ │ ├── modal.tsx │ │ │ └── content.tsx │ │ ├── store │ │ │ ├── selectors │ │ │ │ └── selectors.ts │ │ │ └── dialog-slice.ts │ │ ├── types │ │ │ └── dialog.ts │ │ ├── dialog.tsx │ │ └── hooks │ │ │ ├── useRootDialogManager.ts │ │ │ └── useDialogManager.tsx │ │ ├── playlist │ │ ├── constants │ │ │ └── index.ts │ │ ├── flatboard │ │ │ ├── track-board │ │ │ │ └── track-bar │ │ │ │ │ └── types.ts │ │ │ └── flatboard.tsx │ │ ├── store │ │ │ ├── selectors │ │ │ │ └── index.ts │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── hooks │ │ │ └── use-flatboard-scroll.tsx │ │ ├── track-list │ │ │ ├── track-item │ │ │ │ ├── track-item-solo-mute │ │ │ │ │ └── track-item-solo-muted.tsx │ │ │ │ └── track-item.tsx │ │ │ ├── track-list.tsx │ │ │ └── track-popup-menu │ │ │ │ ├── track-set-color-menu │ │ │ │ └── track-set-color-menu.tsx │ │ │ │ └── track-popup-menu.tsx │ │ └── playlist.tsx │ │ ├── menu │ │ ├── hooks │ │ │ └── import-export │ │ │ │ ├── types.ts │ │ │ │ ├── useExport.tsx │ │ │ │ └── useImport.ts │ │ ├── store │ │ │ ├── selectors │ │ │ │ └── menu-selectors.ts │ │ │ └── menu-slice.ts │ │ ├── top-right │ │ │ ├── top-right.tsx │ │ │ ├── export │ │ │ │ ├── useExportData.tsx │ │ │ │ └── export.tsx │ │ │ └── theme │ │ │ │ └── theme.tsx │ │ ├── hamburger │ │ │ ├── import │ │ │ │ ├── useImport.tsx │ │ │ │ ├── dialog │ │ │ │ │ ├── step │ │ │ │ │ │ ├── alert-dialog-step.tsx │ │ │ │ │ │ ├── loader-dialog-step.tsx │ │ │ │ │ │ └── file-dialog-step.tsx │ │ │ │ │ ├── import-wizard.tsx │ │ │ │ │ └── useImportWizardData.ts │ │ │ │ └── utils │ │ │ │ │ └── read-json-file.ts │ │ │ ├── hamburger.tsx │ │ │ └── useHamburgerData.tsx │ │ ├── menu.tsx │ │ └── title │ │ │ └── title.tsx │ │ ├── drum-machine │ │ ├── store │ │ │ ├── selectors │ │ │ │ └── drum-machine-selectors.ts │ │ │ └── drum-machine-slice.ts │ │ ├── util │ │ │ └── drum-machine-pattern-util.ts │ │ ├── config │ │ │ └── drum-machine-config.tsx │ │ ├── header │ │ │ └── drum-machine-header.tsx │ │ ├── pad │ │ │ ├── sound-selector │ │ │ │ ├── category │ │ │ │ │ ├── drum-machine-ride-icon.tsx │ │ │ │ │ ├── drum-category-icon.tsx │ │ │ │ │ ├── drum-machine-crash-icon.tsx │ │ │ │ │ ├── drum-machine-snare-icon.tsx │ │ │ │ │ ├── drum-machine-clap-icon.tsx │ │ │ │ │ ├── drum-machine-open-hh-icon.tsx │ │ │ │ │ └── drum-machine-closed-hh-icon.tsx │ │ │ │ ├── drum-machine-pad-sound-selector.tsx │ │ │ │ └── pad-sound-item │ │ │ │ │ ├── drum-machine-drum-sound-item.tsx │ │ │ │ │ └── selector-popup │ │ │ │ │ └── sound-selector-popup.tsx │ │ │ └── grid │ │ │ │ ├── pattern │ │ │ │ └── drum-machine-pad-pattern.tsx │ │ │ │ └── drum-machine-pad-grid.tsx │ │ └── drum-machine.tsx │ │ ├── playlist-header │ │ ├── store │ │ │ ├── selectors │ │ │ │ └── index.ts │ │ │ └── playlist-header-slice.ts │ │ ├── playlist-header.tsx │ │ └── playlist-commands │ │ │ └── playlist-commands.tsx │ │ ├── instrument │ │ ├── store │ │ │ ├── types │ │ │ │ └── types.ts │ │ │ └── selectors │ │ │ │ └── index.ts │ │ ├── instrument-setup.tsx │ │ ├── config │ │ │ └── instrument-config.tsx │ │ ├── header │ │ │ └── instrument-header.tsx │ │ └── keyboard │ │ │ └── instrument-keyboard.tsx │ │ └── daw.tsx ├── assets │ ├── image │ │ └── logo.webp │ ├── metronome_up.wav │ └── metronome_down.wav ├── App.tsx ├── model │ ├── note │ │ ├── key │ │ │ └── octave │ │ │ │ └── octave.ts │ │ └── note.ts │ ├── drums │ │ ├── sound │ │ │ ├── kits │ │ │ │ └── drum-kit-01 │ │ │ │ │ └── samples │ │ │ │ │ ├── KIT_01_KICK_01.wav │ │ │ │ │ ├── KIT_01_KICK_02.wav │ │ │ │ │ ├── KIT_01_KICK_03.wav │ │ │ │ │ ├── KIT_01_RIDE_01.wav │ │ │ │ │ ├── KIT_01_RIDE_02.wav │ │ │ │ │ ├── KIT_01_TOM_01.wav │ │ │ │ │ ├── KIT_01_TOM_02.wav │ │ │ │ │ ├── KIT_01_TOM_03.wav │ │ │ │ │ ├── KIT_01_TOM_04.wav │ │ │ │ │ ├── KIT_01_CRASH_01.wav │ │ │ │ │ ├── KIT_01_CRASH_02.wav │ │ │ │ │ ├── KIT_01_CRASH_03.wav │ │ │ │ │ ├── KIT_01_CRASH_04.wav │ │ │ │ │ ├── KIT_01_SNARE_01.wav │ │ │ │ │ ├── KIT_01_SNARE_02.wav │ │ │ │ │ ├── KIT_01_SNARE_03.wav │ │ │ │ │ ├── KIT_01_CLOSED_HAT.wav │ │ │ │ │ ├── KIT_01_OPEN_HAT_01.wav │ │ │ │ │ └── KIT_01_OPEN_HAT_02.wav │ │ │ └── drums-sound.ts │ │ └── category │ │ │ └── drum-category.ts │ ├── bar │ │ └── bar.ts │ ├── instrument │ │ ├── instrument.ts │ │ └── preset │ │ │ └── preset.ts │ ├── track │ │ ├── track-color.ts │ │ ├── track.ts │ │ └── drums │ │ │ ├── track-drums.ts │ │ │ └── track-drums-bar-factory.ts │ └── scale │ │ └── scale.ts ├── sequencer │ ├── channel │ │ └── instrument │ │ │ ├── channel-instrument.ts │ │ │ ├── synth │ │ │ └── synth-instrument.ts │ │ │ ├── channel-instrument-factory.ts │ │ │ └── sampler │ │ │ └── sampler-instrument.ts │ ├── time │ │ ├── utils │ │ │ └── time-utils.ts │ │ └── clock │ │ │ └── clock.ts │ ├── volume │ │ └── volume.ts │ └── metronome │ │ └── metronome.ts ├── main.tsx ├── store │ ├── observers │ │ └── index.ts │ └── index.ts └── index.css ├── public └── logo.ico ├── docs └── images │ ├── drums_dark.png │ └── screen_light.png ├── postcss.config.js ├── vite.config.ts ├── tsconfig.node.json ├── .gitignore ├── index.html ├── .eslintrc.cjs ├── tsconfig.json ├── .vscode ├── launch.json ├── qwik-city.code-snippets └── qwik.code-snippets ├── LICENSE ├── package.json └── tailwind.config.js /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/public/logo.ico -------------------------------------------------------------------------------- /src/features/daw/midi-editor/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const PIANO_ROLL_BAR_HEADER_HEIGHT = 25 2 | -------------------------------------------------------------------------------- /src/features/daw/midi-editor/store/types/types.ts: -------------------------------------------------------------------------------- 1 | export type EditorMode = 'select' | 'draw' | 'delete' 2 | -------------------------------------------------------------------------------- /docs/images/drums_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/docs/images/drums_dark.png -------------------------------------------------------------------------------- /src/assets/image/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/assets/image/logo.webp -------------------------------------------------------------------------------- /src/assets/metronome_up.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/assets/metronome_up.wav -------------------------------------------------------------------------------- /docs/images/screen_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/docs/images/screen_light.png -------------------------------------------------------------------------------- /src/assets/metronome_down.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/assets/metronome_down.wav -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { DAW } from './features/daw/daw' 2 | 3 | function App() { 4 | return 5 | } 6 | 7 | export default App 8 | -------------------------------------------------------------------------------- /src/model/note/key/octave/octave.ts: -------------------------------------------------------------------------------- 1 | export const OCTAVES = [1, 2, 3, 4, 5, 6, 7, 8] as const 2 | 3 | export type Octave = (typeof OCTAVES)[number] 4 | -------------------------------------------------------------------------------- /src/features/daw/player-bar/constants/player-bar-constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_BPM = 120 2 | export const DEFAULT_VOLUME = 100 3 | export const MAX_VOLUME = 250 4 | -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_KICK_01.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_KICK_01.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_KICK_02.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_KICK_02.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_KICK_03.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_KICK_03.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_RIDE_01.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_RIDE_01.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_RIDE_02.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_RIDE_02.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_TOM_01.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_TOM_01.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_TOM_02.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_TOM_02.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_TOM_03.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_TOM_03.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_TOM_04.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_TOM_04.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_CRASH_01.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_CRASH_01.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_CRASH_02.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_CRASH_02.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_CRASH_03.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_CRASH_03.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_CRASH_04.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_CRASH_04.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_SNARE_01.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_SNARE_01.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_SNARE_02.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_SNARE_02.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_SNARE_03.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_SNARE_03.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_CLOSED_HAT.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_CLOSED_HAT.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_OPEN_HAT_01.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_OPEN_HAT_01.wav -------------------------------------------------------------------------------- /src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_OPEN_HAT_02.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoluzzi/liberty-beats/HEAD/src/model/drums/sound/kits/drum-kit-01/samples/KIT_01_OPEN_HAT_02.wav -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /src/features/daw/common/components/ruler/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const SUB_BAR_WIDTH = 16 2 | export const SUB_BAR_NUM = 4 3 | export const RULER_BAR_WIDTH = 80 4 | export const RULER_SUB_BAR_WIDTH = 20 5 | -------------------------------------------------------------------------------- /src/features/daw/bottom-bar/store/selectors/index.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '../../../../../store' 2 | 3 | export const selectSelectedBottomUpPanel = (state: RootState) => 4 | state.bottomBar.selectedBottomUpPanel 5 | -------------------------------------------------------------------------------- /src/model/bar/bar.ts: -------------------------------------------------------------------------------- 1 | import { Note } from '../note/note' 2 | 3 | export interface Bar { 4 | id: string 5 | title: string 6 | startAtTick: number 7 | durationTicks: number 8 | notes: Note[] 9 | } 10 | -------------------------------------------------------------------------------- /src/model/instrument/instrument.ts: -------------------------------------------------------------------------------- 1 | export const INSTRUMENT_TYPES = [ 2 | 'GUITAR', 3 | 'BASS', 4 | 'DRUMS', 5 | 'KEYBOARDS', 6 | ] as const 7 | 8 | export type InstrumentType = (typeof INSTRUMENT_TYPES)[number] 9 | -------------------------------------------------------------------------------- /src/features/daw/dialog/modal/overlay.tsx: -------------------------------------------------------------------------------- 1 | export const ModalOverlay = () => { 2 | return ( 3 |
4 | ) 5 | } 6 | -------------------------------------------------------------------------------- /src/features/daw/dialog/store/selectors/selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '../../../../../store' 2 | 3 | export const selectCurrentDialog = (state: RootState) => 4 | state.dialog.queue.length > 0 ? state.dialog.queue[0] : null 5 | -------------------------------------------------------------------------------- /src/features/daw/bottom-bar/types/bottom-up-panel.ts: -------------------------------------------------------------------------------- 1 | export const BOTTOM_UP_PANELS = [ 2 | 'instrument', 3 | 'midiEditor', 4 | 'drumMachine', 5 | ] as const 6 | 7 | export type BottomUpPanel = (typeof BOTTOM_UP_PANELS)[number] 8 | -------------------------------------------------------------------------------- /src/features/daw/playlist/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const RULER_HEIGHT = 16 2 | export const TRACK_HEIGHT = 80 3 | export const FLATBOARD_BAR_HEADER_HEIGHT = 20 4 | export const TICK_WIDTH_PIXEL = 5 5 | export const MIN_FLATBOARD_KEY_HEIGHT = 4 6 | -------------------------------------------------------------------------------- /src/features/daw/menu/hooks/import-export/types.ts: -------------------------------------------------------------------------------- 1 | import { Track } from '../../../../../model/track/track' 2 | 3 | export interface ExportData { 4 | tracks: Track[] 5 | project_title: string 6 | created_at: string 7 | version: string 8 | } 9 | -------------------------------------------------------------------------------- /src/features/daw/common/components/wizard/wizard-context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { WizardContextValues } from './types' 3 | 4 | const WizardContext = React.createContext(null) 5 | 6 | export default WizardContext 7 | -------------------------------------------------------------------------------- /src/features/daw/menu/store/selectors/menu-selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '../../../../../store' 2 | 3 | export const selectProjectTitle = (state: RootState) => state.menu.projectTitle 4 | 5 | export const selectTheme = (state: RootState) => state.menu.theme 6 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /src/model/drums/category/drum-category.ts: -------------------------------------------------------------------------------- 1 | export const DRUMS_CATEGORY = [ 2 | 'OPEN_HI_HAT', 3 | 'CLOSED_HI_HAT', 4 | 'SNARE', 5 | 'CRASH', 6 | 'TOM', 7 | 'KICK', 8 | 'COWBELL', 9 | 'RIDE', 10 | 'STICK', 11 | 'HAND_CLAP', 12 | ] as const 13 | 14 | export type DrumCategory = (typeof DRUMS_CATEGORY)[number] 15 | -------------------------------------------------------------------------------- /src/features/daw/dialog/types/dialog.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export type DialogOptions = { 4 | size?: 'sm' | 'md' | 'lg' | 'xl' 5 | closeOnOverlayClick?: boolean 6 | canClose?: boolean 7 | } 8 | 9 | export type Dialog = { 10 | key: string 11 | component: React.ReactNode 12 | options?: DialogOptions 13 | } 14 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/store/selectors/drum-machine-selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '../../../../../store' 2 | 3 | export const selectMaxTrackPatterns = (state: RootState) => 4 | state.drumMachine.maxTrackPatterns 5 | 6 | export const selectSelectedPatternIndex = (state: RootState) => 7 | state.drumMachine.selectedPatternIndex 8 | -------------------------------------------------------------------------------- /src/features/daw/playlist/flatboard/track-board/track-bar/types.ts: -------------------------------------------------------------------------------- 1 | import { Bar } from '../../../../../../model/bar/bar' 2 | import { Track } from '../../../../../../model/track/track' 3 | 4 | export type TrackBarProps = { 5 | track: Track 6 | bar: Bar 7 | onSelectBar: (bar: Bar) => void 8 | onBarDetails: (bar: Bar) => void 9 | } 10 | -------------------------------------------------------------------------------- /src/sequencer/channel/instrument/channel-instrument.ts: -------------------------------------------------------------------------------- 1 | import { Key } from '../../../model/note/key/key' 2 | 3 | export interface ChannelInstrument { 4 | setVolume(volume: number): void 5 | 6 | connect(): void 7 | 8 | disconnect(): void 9 | 10 | play(note: Key, duration: string, time?: number, velocity?: number): void 11 | } 12 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/util/drum-machine-pattern-util.ts: -------------------------------------------------------------------------------- 1 | export class DrumMachinePatternUtil { 2 | /** 3 | * Given a zero-based index, returns the pattern name. 4 | * The final name is a letter from A to Z. 5 | */ 6 | static getPatternNameByIndex(index: number) { 7 | return String.fromCharCode(64 + index + 1) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/features/daw/menu/top-right/top-right.tsx: -------------------------------------------------------------------------------- 1 | import { Export } from './export/export' 2 | import { Theme } from './theme/theme' 3 | 4 | export const TopRight = () => { 5 | return ( 6 |
7 | 8 | 9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/model/track/track-color.ts: -------------------------------------------------------------------------------- 1 | // if you update this remember to also update safelist in tailwind.config.js 2 | export const TRACK_COLORS = [ 3 | 'blue', 4 | 'green', 5 | 'red', 6 | 'yellow', 7 | 'pink', 8 | 'cyan', 9 | 'purple', 10 | 'orange', 11 | ] as const 12 | 13 | export type TrackColor = (typeof TRACK_COLORS)[number] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/features/daw/common/components/wizard/use-wizard-context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import WizardContext from './wizard-context' 3 | 4 | export const useWizardContext = () => { 5 | const context = React.useContext(WizardContext) 6 | if (!context) { 7 | throw new Error('useWizard must be used within a WizardProvider') 8 | } 9 | return context 10 | } 11 | -------------------------------------------------------------------------------- /src/features/daw/player-bar/undo-redo/undo-redo.tsx: -------------------------------------------------------------------------------- 1 | import { TiArrowBack, TiArrowForward } from 'react-icons/ti' 2 | 3 | export const UndoRedo = () => { 4 | return ( 5 |
6 | 9 | 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Liberty Beats 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/features/daw/common/components/wizard/types.ts: -------------------------------------------------------------------------------- 1 | export type WizardContextValues = { 2 | goToNextStep: () => void 3 | goToPrevStep: () => void 4 | setNextStepHandler: (handler: () => void) => void 5 | 6 | isLoading: boolean 7 | activeStep: number 8 | } 9 | 10 | export type WizardProps = { 11 | onStepChange?: (step: number) => void 12 | 13 | Wrapper?: React.ReactElement 14 | } 15 | -------------------------------------------------------------------------------- /src/features/daw/playlist-header/store/selectors/index.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '../../../../../store' 2 | 3 | export const selectMaxBars = (state: RootState) => state.playlistHeader.maxBars 4 | 5 | export const selectCurrentTick = (state: RootState) => 6 | state.playlistHeader.currentTick 7 | 8 | export const selectRequestedNewTickPosition = (state: RootState) => 9 | state.playlistHeader.requestedNewTickPosition 10 | -------------------------------------------------------------------------------- /src/features/daw/common/components/piano-roll-key/readonly/readonly-piano-roll-key.tsx: -------------------------------------------------------------------------------- 1 | import { PianoRollKeyProps } from '../types' 2 | 3 | export const ReadonlyPianoRollKey = ({ 4 | note, 5 | beatWidth, 6 | }: PianoRollKeyProps) => { 7 | const noteLengthPixel = note.durationTicks * beatWidth 8 | 9 | return ( 10 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/features/daw/instrument/store/types/types.ts: -------------------------------------------------------------------------------- 1 | import { Bar } from '../../../../../model/bar/bar' 2 | import { Key } from '../../../../../model/note/key/key' 3 | 4 | export type PlayingTrackKeys = { 5 | trackId: string 6 | keys: Key[] 7 | } 8 | 9 | export type TrackPreviewLoop = { 10 | trackId: string 11 | loopBar: Bar | null 12 | } 13 | 14 | export type TogglePlayingKeyPayload = { 15 | trackId: string 16 | key: Key 17 | } 18 | -------------------------------------------------------------------------------- /src/features/daw/menu/hamburger/import/useImport.tsx: -------------------------------------------------------------------------------- 1 | import { useDialogManager } from '../../../dialog/hooks/useDialogManager' 2 | import { ImportWizard } from './dialog/import-wizard' 3 | 4 | export const useImportWizard = () => { 5 | const { show, hide } = useDialogManager('import') 6 | 7 | const showImportDialog = () => { 8 | show(, { size: 'sm' }) 9 | } 10 | 11 | return { 12 | showImportDialog, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | import { store } from './store' 6 | import { Provider } from 'react-redux' 7 | import Sequencer from './sequencer/sequencer.ts' 8 | 9 | new Sequencer(store) 10 | 11 | ReactDOM.createRoot(document.getElementById('root')!).render( 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | -------------------------------------------------------------------------------- /src/features/daw/common/hooks/useDebounce.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export const useDebounce = (value: T, delay: number) => { 4 | const [debouncedValue, setDebouncedValue] = useState(value) 5 | 6 | useEffect(() => { 7 | const handler = setTimeout(() => { 8 | setDebouncedValue(value) 9 | }, delay) 10 | 11 | return () => { 12 | clearTimeout(handler) 13 | } 14 | }, [value, delay]) 15 | 16 | return debouncedValue 17 | } 18 | -------------------------------------------------------------------------------- /src/features/daw/menu/menu.tsx: -------------------------------------------------------------------------------- 1 | import { Hamburger } from './hamburger/hamburger' 2 | import { Title } from './title/title' 3 | import { TopRight } from './top-right/top-right' 4 | 5 | export const Menu = () => { 6 | return ( 7 |
8 | 9 | 10 |
11 | 12 | </div> 13 | 14 | <TopRight /> 15 | </div> 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/features/daw/player-bar/store/selectors/index.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '../../../../../store' 2 | 3 | export const selectIsPlaying = (state: RootState) => state.playerBar.isPlaying 4 | 5 | export const selectBpm = (state: RootState) => state.playerBar.bpm 6 | 7 | export const selectTime = (state: RootState) => state.playerBar.time 8 | 9 | export const selectMetronomeActive = (state: RootState) => 10 | state.playerBar.metronomeActive 11 | 12 | export const selectVolume = (state: RootState) => state.playerBar.volume 13 | -------------------------------------------------------------------------------- /src/features/daw/dialog/modal/modal.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog } from '../types/dialog' 2 | import { ModalContent } from './content' 3 | import { ModalOverlay } from './overlay' 4 | 5 | export type ModalProps = { 6 | onClose: () => void 7 | dialog: Dialog 8 | } 9 | 10 | export const Modal = (props: ModalProps) => { 11 | return ( 12 | <div className="absolute z-40 left-0 top-0 w-screen h-screen flex justify-center items-center"> 13 | <ModalOverlay /> 14 | 15 | <ModalContent {...props} /> 16 | </div> 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/features/daw/dialog/dialog.tsx: -------------------------------------------------------------------------------- 1 | import { Modal } from './modal/modal' 2 | import { createPortal } from 'react-dom' 3 | import { useRootDialogManager } from './hooks/useRootDialogManager' 4 | 5 | export const Dialog = () => { 6 | const { firstInQueue, onClose } = useRootDialogManager() 7 | 8 | return ( 9 | <div className="relative"> 10 | {firstInQueue && 11 | createPortal( 12 | <Modal onClose={onClose} dialog={firstInQueue} />, 13 | document.body 14 | )} 15 | </div> 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/features/daw/instrument/store/selectors/index.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '../../../../../store' 2 | 3 | export const selectPlayingTrackKeys = (state: RootState) => 4 | state.instrument.playingTrackKeys 5 | 6 | export const selectSelectedOctave = (state: RootState) => 7 | state.instrument.selectedOctave 8 | 9 | export const selectTrackPreviewLoop = (state: RootState) => 10 | state.instrument.trackPreviewLoop 11 | 12 | export const selectTrackIdInPlayingPreviewloop = (state: RootState) => 13 | state.instrument.trackIdInPlayingPreviewloop 14 | -------------------------------------------------------------------------------- /src/features/daw/dialog/hooks/useRootDialogManager.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux' 2 | import { selectCurrentDialog } from '../store/selectors/selectors' 3 | import { popDialog } from '../store/dialog-slice' 4 | 5 | export const useRootDialogManager = () => { 6 | const dispatch = useDispatch() 7 | const firstInQueue = useSelector(selectCurrentDialog) 8 | return { 9 | firstInQueue, 10 | onClose: () => { 11 | if (firstInQueue) { 12 | dispatch(popDialog(firstInQueue.key)) 13 | } 14 | }, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/features/daw/menu/hamburger/import/dialog/step/alert-dialog-step.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, AlertProps } from '../../../../../common/components/alert/alert' 2 | 3 | export type AlertDialogStepProps = { 4 | status: AlertProps['status'] 5 | message: AlertProps['message'] 6 | title: AlertProps['title'] 7 | action?: AlertProps['action'] 8 | } 9 | 10 | export const AlertDialogStep = (props: AlertDialogStepProps) => { 11 | return ( 12 | <div className="flex flex-row w-full h-full p-8 justify-center"> 13 | <Alert {...props} /> 14 | </div> 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/sequencer/time/utils/time-utils.ts: -------------------------------------------------------------------------------- 1 | import * as Tone from 'tone' 2 | 3 | export class TimeUtils { 4 | static tickToToneTime(tick: number) { 5 | const measure = Math.floor(tick / 16) 6 | const quarters = Math.floor((tick % 16) / 4) 7 | const sixteenths = tick % 4 8 | return `${measure}:${quarters}:${sixteenths}` 9 | } 10 | 11 | static toneTimeToTicks(time: Tone.Unit.Time) { 12 | const [measures, quarters, sixteenths] = time 13 | .toString() 14 | .split(':') 15 | .map(Number) 16 | return measures * 16 + quarters * 4 + sixteenths 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/features/daw/common/components/piano-roll-key/piano-roll-key.tsx: -------------------------------------------------------------------------------- 1 | import { PianoRollKeyProps } from './types' 2 | import { EditablePianoRollKey } from './editable/editable-piano-roll-key' 3 | import { ReadonlyPianoRollKey } from './readonly/readonly-piano-roll-key' 4 | import { PianoRollKeySkeleton } from './common/piano-roll-key-skeleton' 5 | 6 | export const PianoRollKey = ({ editable, ...props }: PianoRollKeyProps) => { 7 | return editable ? ( 8 | <PianoRollKeySkeleton PianoRollKeyBody={EditablePianoRollKey} {...props} /> 9 | ) : ( 10 | <PianoRollKeySkeleton PianoRollKeyBody={ReadonlyPianoRollKey} {...props} /> 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/features/daw/common/components/piano-roll-key/types.ts: -------------------------------------------------------------------------------- 1 | import { Bar } from '../../../../../model/bar/bar' 2 | import { Key } from '../../../../../model/note/key/key' 3 | import { Note } from '../../../../../model/note/note' 4 | import { Track } from '../../../../../model/track/track' 5 | 6 | export type PianoRollKeyProps = { 7 | note: Note 8 | bar: Bar 9 | track: Track 10 | showedKeys: Readonly<Key[]> 11 | beatWidth: number 12 | keyHeight: number 13 | editable?: boolean 14 | selected?: boolean 15 | nonMutedColorTailwindClass?: string 16 | 17 | cursorStyle?: 'default' | 'pointer' 18 | 19 | onNoteClick?: () => void 20 | } 21 | -------------------------------------------------------------------------------- /src/features/daw/playlist/store/selectors/index.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '../../../../../store' 2 | 3 | export const selectTracks = (state: RootState) => state.playlist.tracks 4 | 5 | export const selectSelectedTrack = (state: RootState) => 6 | state.playlist.tracks.find((t) => t.id === state.playlist.selectedTrackId) 7 | 8 | export const selectSelectedBar = (state: RootState) => { 9 | const selectedTrack = selectSelectedTrack(state) 10 | if (!selectedTrack) return undefined 11 | return selectedTrack.bars.find((b) => b.id === state.playlist.selectedBarId) 12 | } 13 | 14 | export const selectToCopyBar = (state: RootState) => state.playlist.toCopyBar 15 | -------------------------------------------------------------------------------- /src/features/daw/midi-editor/midi-body/left-menu/left-menu.tsx: -------------------------------------------------------------------------------- 1 | import { LeftMenuHeader } from './left-menu-header/left-menu-header' 2 | import { LeftMenuVelocity } from './left-menu-velocity/left-menu-velocity' 3 | import { LeftMenuScaleView } from './left-menu-scale-view/left-menu-scale-view' 4 | import { LeftMenuTranspose } from './left-menu-transpose/left-menu-transpose' 5 | 6 | export const LeftMenu = () => { 7 | return ( 8 | <div className="flex flex-col w-full p-2 gap-4 select-none"> 9 | <LeftMenuHeader /> 10 | 11 | <LeftMenuVelocity /> 12 | 13 | <LeftMenuTranspose /> 14 | 15 | <LeftMenuScaleView /> 16 | </div> 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/model/note/note.ts: -------------------------------------------------------------------------------- 1 | import { Key, KeyUtils } from './key/key' 2 | 3 | export interface Note { 4 | id: string 5 | 6 | /** 7 | * starting tick, this is relative to the bar parent 8 | * */ 9 | startsAtRelativeTick: number 10 | 11 | durationTicks: number 12 | velocity: number 13 | key: Key 14 | } 15 | 16 | export class NoteUtils { 17 | static getSmallerKeySetContainingNotes = ( 18 | notes: Readonly<Note[]>, 19 | padding: number 20 | ) => { 21 | const keys = notes.map((n) => n.key) 22 | const minKey = KeyUtils.getMinKey(keys) 23 | const maxKey = KeyUtils.getMaxKey(keys) 24 | return KeyUtils.getKeySubset(minKey, maxKey, padding) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /src/features/daw/midi-editor/midi-body/midi-body.tsx: -------------------------------------------------------------------------------- 1 | import { Track } from '../../../../model/track/track' 2 | import { KeyEditor } from './key-editor/key-editor' 3 | import { LeftMenu } from './left-menu/left-menu' 4 | 5 | export type MidiBodyProps = { 6 | selectedTrack: Track 7 | } 8 | 9 | export const MidiBody = () => { 10 | return ( 11 | <div className="flex h-full w-full flex-row justify-between divide-x divide-slate-600"> 12 | <div className="flex h-full justify-between divide-x divide-slate-600 max-w-72 min-w-72"> 13 | <LeftMenu /> 14 | </div> 15 | 16 | <div className="flex flex-grow overflow-auto"> 17 | <KeyEditor /> 18 | </div> 19 | </div> 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/features/daw/dialog/hooks/useDialogManager.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch } from 'react-redux' 2 | import { DialogOptions } from '../types/dialog' 3 | import { popDialog, pushDialog } from '../store/dialog-slice' 4 | import { useCallback } from 'react' 5 | 6 | export const useDialogManager = (key: string) => { 7 | const dispatch = useDispatch() 8 | 9 | const show = useCallback( 10 | (component: React.ReactNode, options?: DialogOptions) => { 11 | dispatch(pushDialog({ key, component, options })) 12 | }, 13 | [dispatch, key] 14 | ) 15 | 16 | const hide = useCallback(() => { 17 | dispatch(popDialog(key)) 18 | }, [dispatch, key]) 19 | 20 | return { 21 | show, 22 | hide, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/features/daw/common/hooks/use-outside-click.tsx: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect } from 'react' 2 | 3 | export const useCallbackOnOutsideClick = ( 4 | ref: RefObject<HTMLElement>, 5 | callback: () => void 6 | ) => { 7 | useEffect(() => { 8 | const handleClick = (e: MouseEvent) => { 9 | if (ref.current && !ref.current.contains(e.target as Node)) { 10 | // setting a timeout to allow the click event to finish before closing the menu 11 | setTimeout(() => { 12 | callback() 13 | }, 100) 14 | } 15 | } 16 | 17 | document.addEventListener('mousedown', handleClick) 18 | return () => { 19 | document.removeEventListener('mousedown', handleClick) 20 | } 21 | }, [ref, callback]) 22 | } 23 | -------------------------------------------------------------------------------- /src/model/drums/sound/drums-sound.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InstrumentPreset, 3 | InstrumentPresetId, 4 | } from '../../instrument/preset/preset' 5 | import { Key } from '../../note/key/key' 6 | import { DrumCategory } from '../category/drum-category' 7 | import { DRUM_KIT_SOUNDS_01 } from './kits/drum-kit-01/drum-kit-01' 8 | 9 | export type DrumSound = { 10 | id: string 11 | key: Key 12 | category: DrumCategory 13 | name: string 14 | presetId: InstrumentPresetId 15 | sampleUrl: string 16 | } 17 | 18 | const DRUMS_SOUNDS: DrumSound[] = [...DRUM_KIT_SOUNDS_01] 19 | 20 | export class DrumSoundUtils { 21 | static getDrumsSoundSetByPreset(preset: InstrumentPreset) { 22 | return DRUMS_SOUNDS.filter((sound) => sound.presetId === preset.id) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/features/daw/menu/hamburger/import/dialog/step/loader-dialog-step.tsx: -------------------------------------------------------------------------------- 1 | import { Loader } from '../../../../../common/components/loader/loader' 2 | import { useWizardContext } from '../../../../../common/components/wizard/use-wizard-context' 3 | import { useEffect } from 'react' 4 | 5 | export type LoaderDialogStepProps = { 6 | isLoading: boolean 7 | } 8 | 9 | export const LoaderDialogStep = ({ isLoading }: LoaderDialogStepProps) => { 10 | const { goToNextStep } = useWizardContext() 11 | 12 | useEffect(() => { 13 | if (!isLoading) { 14 | goToNextStep() 15 | } 16 | }, [goToNextStep, isLoading]) 17 | 18 | return ( 19 | <div className="flex flex-row w-full h-full p-8 justify-center"> 20 | <Loader /> 21 | </div> 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/features/daw/common/components/tick-placeholder/tick-placeholder.tsx: -------------------------------------------------------------------------------- 1 | import { TICK_WIDTH_PIXEL } from '../../../playlist/constants' 2 | import { usePreviewLoopSafeTransportPosition } from '../../hooks/use-preview-loop-safe-transport-position' 3 | 4 | export const TickPlaceholder = () => { 5 | const { tick } = usePreviewLoopSafeTransportPosition() 6 | 7 | const barOffsetPixel = tick * TICK_WIDTH_PIXEL 8 | const barOffsetStyle = `${barOffsetPixel}px` 9 | return ( 10 | <div 11 | className="absolute top-0 h-[100%] w-[1px] z-40 bg-black dark:bg-white" 12 | style={{ left: barOffsetStyle }} 13 | > 14 | <div 15 | className={`flex flex-col h-full min-h-24 max-h-24 bg-black dark:bg-white`} 16 | ></div> 17 | </div> 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/features/daw/player-bar/player-bar.tsx: -------------------------------------------------------------------------------- 1 | // import { UndoRedo } from './undo-redo/undo-redo' 2 | import { Player } from './player/player' 3 | import { Metronome } from './metronome/metronome' 4 | import { MasterVolume } from './master-volume/master-volume' 5 | 6 | export const PlayerBar = () => { 7 | return ( 8 | <div className="flex flex-row items-center justify-between"> 9 | {/* TODO - Add UndoRedo feature */} 10 | {/* <div> 11 | <UndoRedo /> 12 | </div> */} 13 | <div className="flex-1"> 14 | <Metronome /> 15 | </div> 16 | <div className="flex-3 flex-grow"> 17 | <Player /> 18 | </div> 19 | <div className="flex-1"> 20 | <MasterVolume /> 21 | </div> 22 | </div> 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Chrome", 9 | "request": "launch", 10 | "type": "chrome", 11 | "url": "http://localhost:5173", 12 | "webRoot": "${workspaceFolder}" 13 | }, 14 | { 15 | "type": "node", 16 | "name": "dev.debug", 17 | "request": "launch", 18 | "skipFiles": ["<node_internals>/**"], 19 | "cwd": "${workspaceFolder}", 20 | "program": "${workspaceFolder}/node_modules/vite/bin/vite.js", 21 | "args": ["--mode", "ssr", "--force"] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/features/daw/menu/top-right/export/useExportData.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { VscJson } from 'react-icons/vsc' 3 | import { useExport } from '../../hooks/import-export/useExport' 4 | 5 | export const useExportData = () => { 6 | const [isOpen, setIsOpen] = useState(false) 7 | 8 | const { exportTracksToJSON } = useExport() 9 | 10 | return { 11 | onExportClick: () => setIsOpen(!isOpen), 12 | menu: { 13 | isOpen: isOpen, 14 | onClose: () => setIsOpen(false), 15 | 16 | items: [ 17 | { 18 | label: 'To JSON', 19 | icon: <VscJson />, 20 | action: () => { 21 | setIsOpen(false) 22 | exportTracksToJSON() 23 | }, 24 | }, 25 | ], 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/sequencer/volume/volume.ts: -------------------------------------------------------------------------------- 1 | import * as Tone from 'tone' 2 | import { RootStore } from '../../store' 3 | import { observeStore } from '../../store/observers' 4 | import { selectVolume } from '../../features/daw/player-bar/store/selectors' 5 | 6 | export class Volume { 7 | private _store: RootStore 8 | 9 | constructor(store: RootStore) { 10 | this._store = store 11 | 12 | this.registerStoreListeners() 13 | } 14 | 15 | registerStoreListeners() { 16 | observeStore(this._store, selectVolume, this.setVolume.bind(this)) 17 | } 18 | 19 | setVolume(volume: number) { 20 | Tone.Destination.volume.value = Volume.transformVolumeToToneVolume(volume) 21 | } 22 | 23 | static transformVolumeToToneVolume(volume: number) { 24 | return Tone.gainToDb(volume / 100) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/features/daw/menu/hamburger/import/utils/read-json-file.ts: -------------------------------------------------------------------------------- 1 | export const readJSONFile = (file: File) => { 2 | return new Promise((resolve, reject) => { 3 | const reader = new FileReader() 4 | 5 | reader.addEventListener('load', (event) => { 6 | const result = event.target?.result 7 | if (typeof result === 'string') { 8 | try { 9 | const jsonRes = JSON.parse(result) 10 | resolve(jsonRes) 11 | } catch (error) { 12 | reject(new Error('Invalid JSON content')) 13 | } 14 | } else { 15 | reject(new Error('Invalid file content')) 16 | } 17 | }) 18 | 19 | reader.addEventListener('error', () => { 20 | reject(new Error('Error reading file')) 21 | }) 22 | 23 | reader.readAsText(file) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/model/instrument/preset/preset.ts: -------------------------------------------------------------------------------- 1 | export const INSTRUMENT_PRESETS = [ 2 | { 3 | id: 'piano_grandpiano' as const, 4 | instrument: 'KEYBOARDS', 5 | name: 'Grandpiano', 6 | }, 7 | { 8 | id: 'piano_accordion' as const, 9 | instrument: 'KEYBOARDS', 10 | name: 'Accordion', 11 | }, 12 | { 13 | id: 'drums_kit_01' as const, 14 | instrument: 'DRUMS', 15 | name: 'Drums Kit 01', 16 | }, 17 | { 18 | id: 'guitar_acustic' as const, 19 | instrument: 'GUITAR', 20 | name: 'Acustic Guitar', 21 | }, 22 | { 23 | id: 'bass_electric' as const, 24 | instrument: 'BASS', 25 | name: 'Bass Standard', 26 | }, 27 | ] 28 | 29 | export type InstrumentPreset = (typeof INSTRUMENT_PRESETS)[number] 30 | export type InstrumentPresetId = InstrumentPreset['id'] 31 | -------------------------------------------------------------------------------- /src/store/observers/index.ts: -------------------------------------------------------------------------------- 1 | import { RootState, RootStore } from '..' 2 | 3 | export function observeStore<T>( 4 | store: RootStore, 5 | selectFn: (rootState: RootState) => T, 6 | onChange: (newState: T, oldState: T) => void 7 | ) { 8 | let currentState: T 9 | 10 | const handleChange = () => { 11 | let newState 12 | try { 13 | newState = selectFn(store.getState()) 14 | } catch (e) { 15 | // when listeners unsubscribe, handleChange will still run one more time, which will 16 | // sometimes throw an error, which we can ignore 17 | return 18 | } 19 | if (newState !== currentState) { 20 | const oldState = currentState 21 | currentState = newState 22 | onChange(currentState, oldState) 23 | } 24 | } 25 | 26 | handleChange() 27 | return store.subscribe(handleChange) 28 | } 29 | -------------------------------------------------------------------------------- /src/sequencer/channel/instrument/synth/synth-instrument.ts: -------------------------------------------------------------------------------- 1 | import { Key } from '../../../../model/note/key/key' 2 | import { Volume } from '../../../volume/volume' 3 | import { ChannelInstrument } from '../channel-instrument' 4 | 5 | import * as Tone from 'tone' 6 | 7 | export default class SynthInstrument implements ChannelInstrument { 8 | _synth: Tone.PolySynth = new Tone.PolySynth(Tone.FMSynth) 9 | 10 | constructor() {} 11 | 12 | play(note: Key, duration: string, time?: number, velocity?: number) { 13 | this._synth.triggerAttackRelease(note, duration, time, velocity) 14 | } 15 | connect(): void { 16 | this._synth.toDestination() 17 | } 18 | disconnect(): void { 19 | this._synth.disconnect() 20 | } 21 | setVolume(volume: number): void { 22 | this._synth.volume.value = Volume.transformVolumeToToneVolume(volume) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/features/daw/midi-editor/midi-body/key-editor/piano-roll-bar/header/piano-roll-bar-header.tsx: -------------------------------------------------------------------------------- 1 | import { Bar } from '../../../../../../../model/bar/bar' 2 | import { Track, TrackUtils } from '../../../../../../../model/track/track' 3 | import { PIANO_ROLL_BAR_HEADER_HEIGHT } from '../../../../constants' 4 | 5 | export const PianoRollBarHeader = ({ 6 | bar, 7 | track, 8 | }: { 9 | bar: Bar 10 | track: Track 11 | }) => { 12 | const barHeaderColor = TrackUtils.isTrackEffectivelyMuted(track) 13 | ? 'bg-gray-600' 14 | : `bg-${track.color}-700` 15 | return ( 16 | <div 17 | className={`sticky z-30 ${barHeaderColor} rounded-t-md pl-2 top-0`} 18 | style={{ 19 | height: PIANO_ROLL_BAR_HEADER_HEIGHT, 20 | }} 21 | > 22 | <p className="text-white text-sm font-bold select-none">{bar.title}</p> 23 | </div> 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/features/daw/dialog/store/dialog-slice.ts: -------------------------------------------------------------------------------- 1 | import { PayloadAction, createSlice } from '@reduxjs/toolkit' 2 | import { Dialog } from '../types/dialog' 3 | 4 | export type Theme = 'light' | 'dark' 5 | 6 | export interface DialogState { 7 | queue: Dialog[] 8 | } 9 | 10 | const initialState: DialogState = { 11 | queue: [], 12 | } 13 | 14 | export const dialogSlice = createSlice({ 15 | name: 'dialog', 16 | initialState, 17 | reducers: { 18 | pushDialog: (state, action: PayloadAction<Dialog>) => { 19 | state.queue.push(action.payload) 20 | }, 21 | popDialog: (state, action: PayloadAction<string>) => { 22 | state.queue = state.queue.filter( 23 | (dialog) => dialog.key !== action.payload 24 | ) 25 | }, 26 | }, 27 | }) 28 | 29 | export const { pushDialog, popDialog } = dialogSlice.actions 30 | 31 | export default dialogSlice.reducer 32 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/config/drum-machine-config.tsx: -------------------------------------------------------------------------------- 1 | import { Track } from '../../../../model/track/track' 2 | import { IoIosArrowBack, IoIosArrowForward } from 'react-icons/io' 3 | 4 | export type DrumMachineConfigProps = { 5 | selectedTrack: Track 6 | } 7 | 8 | export const DrumMachineConfig = ({ 9 | selectedTrack, 10 | }: DrumMachineConfigProps) => { 11 | return ( 12 | <div className="flex flex-row justify-between gap-4"> 13 | <div className="flex flex-row items-center gap-2"> 14 | <p>{selectedTrack.instrumentPreset.instrument} Preset</p> 15 | <div>{selectedTrack.instrumentPreset.name}</div> 16 | <div> 17 | <button> 18 | <IoIosArrowBack /> 19 | </button> 20 | <button> 21 | <IoIosArrowForward /> 22 | </button> 23 | </div> 24 | </div> 25 | </div> 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/features/daw/common/hooks/use-ruler-scroll.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux' 2 | import { RootState } from '../../../../store' 3 | import { setRulerScrollPosition } from '../../playlist-header/store/playlist-header-slice' 4 | import { useEffect } from 'react' 5 | 6 | export const useRulerScroll = (ref: React.RefObject<HTMLDivElement>) => { 7 | const rulerScroll = useSelector( 8 | (state: RootState) => state.playlistHeader.rulerScrollPosition 9 | ) 10 | const dispatch = useDispatch() 11 | 12 | useEffect(() => { 13 | if (ref.current && ref.current.scrollLeft !== rulerScroll) { 14 | ref.current.scrollLeft = rulerScroll 15 | } 16 | }, [rulerScroll, ref]) 17 | 18 | const handleRulerScroll = (e: React.UIEvent<HTMLDivElement>) => { 19 | dispatch(setRulerScrollPosition(e.currentTarget.scrollLeft)) 20 | } 21 | 22 | return handleRulerScroll 23 | } 24 | -------------------------------------------------------------------------------- /src/features/daw/playlist-header/playlist-header.tsx: -------------------------------------------------------------------------------- 1 | import { Ruler } from '../common/components/ruler/ruler' 2 | import { useRulerScroll } from '../common/hooks/use-ruler-scroll' 3 | import { PlaylistCommands } from './playlist-commands/playlist-commands' 4 | import React from 'react' 5 | 6 | export const PlaylistHeader = () => { 7 | const rulerDivRef = React.useRef<HTMLDivElement>(null) 8 | const handleRulerScroll = useRulerScroll(rulerDivRef) 9 | 10 | return ( 11 | <div className="flex flex-row justify-between gap-1 divide-x divide-slate-600 w-full"> 12 | <div className="flex my-2 max-w-72 min-w-72"> 13 | <PlaylistCommands /> 14 | </div> 15 | 16 | <div 17 | className="flex flex-grow px-2 overflow-x-scroll no-scrollbar" 18 | onScroll={handleRulerScroll} 19 | ref={rulerDivRef} 20 | > 21 | <Ruler /> 22 | </div> 23 | </div> 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/model/track/track.ts: -------------------------------------------------------------------------------- 1 | import { Bar } from '../bar/bar' 2 | import { InstrumentPreset } from '../instrument/preset/preset' 3 | import { TrackColor } from './track-color' 4 | import { TrackDrums } from './drums/track-drums' 5 | 6 | export interface Track { 7 | id: string 8 | title: string 9 | 10 | /** 11 | * Tailwind CSS color class 12 | * @example 'green' 13 | */ 14 | color: TrackColor 15 | instrumentPreset: InstrumentPreset 16 | 17 | /** 18 | * Drums track specific data (can be undefined if track is not drums track) 19 | */ 20 | trackDrums?: TrackDrums 21 | bars: Bar[] 22 | 23 | volume: number 24 | 25 | muted: boolean 26 | soloed: boolean 27 | areThereAnyOtherTrackSoloed: boolean 28 | } 29 | 30 | export class TrackUtils { 31 | static isTrackEffectivelyMuted(track: Track) { 32 | return track.muted || (track.areThereAnyOtherTrackSoloed && !track.soloed) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/features/daw/midi-editor/midi-body/key-editor/hooks/useMidiEditorDimensions.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux' 2 | import { selectWhiteKeySize } from '../../../store/selectors' 3 | import { selectMaxBars } from '../../../../playlist-header/store/selectors' 4 | import { RULER_BAR_WIDTH } from '../../../../common/components/ruler/constants' 5 | import { PIANO_ROLL_BAR_HEADER_HEIGHT } from '../../../constants' 6 | 7 | export const useMidiEditorDimensions = () => { 8 | const whiteKeySize = useSelector(selectWhiteKeySize) 9 | const maxBars = useSelector(selectMaxBars) 10 | 11 | const keyHeight = whiteKeySize * 0.599 12 | const beatWidth = RULER_BAR_WIDTH / 16 13 | const barWidth = RULER_BAR_WIDTH 14 | const barHeaderHeight = PIANO_ROLL_BAR_HEADER_HEIGHT 15 | 16 | return { 17 | whiteKeySize, 18 | keyHeight, 19 | maxBars, 20 | barWidth, 21 | beatWidth, 22 | barHeaderHeight, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/features/daw/playlist/flatboard/flatboard.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux' 2 | import { selectSelectedTrack, selectTracks } from '../store/selectors' 3 | import { Track } from '../../../../model/track/track' 4 | import { TrackBoard } from './track-board/track-board' 5 | import { TickPlaceholder } from '../../common/components/tick-placeholder/tick-placeholder' 6 | 7 | export const Flatboard = () => { 8 | const tracks = useSelector(selectTracks) 9 | const selectedTrack = useSelector(selectSelectedTrack) 10 | 11 | return ( 12 | <div className="relative h-max min-h-full"> 13 | <div className="flex flex-col gap-1"> 14 | {tracks.map((track: Track) => ( 15 | <TrackBoard 16 | key={track.id} 17 | track={track} 18 | selectedTrack={selectedTrack} 19 | /> 20 | ))} 21 | </div> 22 | <TickPlaceholder /> 23 | </div> 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/features/daw/bottom-bar/store/bottom-bar-slice.ts: -------------------------------------------------------------------------------- 1 | import { PayloadAction, createSlice } from '@reduxjs/toolkit' 2 | import { BottomUpPanel } from '../types/bottom-up-panel' 3 | 4 | export interface BottomBarState { 5 | selectedBottomUpPanel: BottomUpPanel | null 6 | } 7 | 8 | const initialState: BottomBarState = { 9 | selectedBottomUpPanel: null, 10 | } 11 | 12 | export const bottomBarSlice = createSlice({ 13 | name: 'bottomBar', 14 | initialState, 15 | reducers: { 16 | selectBottomUpPanel: ( 17 | state, 18 | action: PayloadAction<BottomUpPanel | null> 19 | ) => { 20 | state.selectedBottomUpPanel = action.payload 21 | }, 22 | closeAllBottomUpPanels: (state) => { 23 | state.selectedBottomUpPanel = null 24 | }, 25 | }, 26 | }) 27 | 28 | export const { selectBottomUpPanel, closeAllBottomUpPanels } = 29 | bottomBarSlice.actions 30 | 31 | export default bottomBarSlice.reducer 32 | -------------------------------------------------------------------------------- /src/features/daw/menu/hamburger/import/dialog/import-wizard.tsx: -------------------------------------------------------------------------------- 1 | import { Wizard } from '../../../../common/components/wizard/wizard' 2 | import { FileChooserDialogStep } from './step/file-dialog-step' 3 | import { LoaderDialogStep } from './step/loader-dialog-step' 4 | import { AlertDialogStep } from './step/alert-dialog-step' 5 | import { useImportWizardData } from './useImportWizardData' 6 | 7 | export type ImportWizardProps = { 8 | hide: () => void 9 | } 10 | 11 | export const ImportWizard = (props: ImportWizardProps) => { 12 | const { file, alert, loader } = useImportWizardData(props) 13 | 14 | return ( 15 | <Wizard> 16 | {/* Step 1: Choose file */} 17 | <FileChooserDialogStep {...file} /> 18 | 19 | {/* Step 2: Loader - executing action */} 20 | <LoaderDialogStep {...loader} /> 21 | 22 | {/* Step 3: Success alert */} 23 | <AlertDialogStep {...alert} /> 24 | </Wizard> 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/features/daw/midi-editor/store/selectors/index.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '../../../../../store' 2 | 3 | export const selectMidiEditorRulerSize = (state: RootState) => 4 | state.midiEditor.rulerSize 5 | 6 | export const selectLastKeyDuration = (state: RootState) => 7 | state.midiEditor.lastKeyDuration 8 | 9 | export const selectWhiteKeySize = (state: RootState) => 10 | state.midiEditor.whiteKeySize 11 | 12 | export const selectSelectedNoteId = (state: RootState) => 13 | state.midiEditor.selectedNoteId 14 | 15 | export const selectEditorMode = (state: RootState) => 16 | state.midiEditor.editorMode 17 | 18 | export const selectNotePreviewEnabled = (state: RootState) => 19 | state.midiEditor.notePreviewEnabled 20 | 21 | export const selectScaleViewEnabled = (state: RootState) => 22 | state.midiEditor.scaleViewEnabled 23 | 24 | export const selectSelectedScale = (state: RootState) => 25 | state.midiEditor.selectedScale 26 | -------------------------------------------------------------------------------- /src/features/daw/midi-editor/hooks/useMidiEditorHorizontalScroll.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux' 2 | import { RootState } from '../../../../store' 3 | import { useEffect } from 'react' 4 | import { setMidiEditorHorizontalScroll } from '../store/midi-editor-slice' 5 | 6 | export const useMidiEditorHorizontalScroll = ( 7 | ref: React.RefObject<HTMLDivElement> 8 | ) => { 9 | const horizontalScroll = useSelector( 10 | (state: RootState) => state.midiEditor.horizontalScroll 11 | ) 12 | const dispatch = useDispatch() 13 | 14 | useEffect(() => { 15 | if (ref.current && ref.current.scrollLeft !== horizontalScroll) { 16 | ref.current.scrollLeft = horizontalScroll 17 | } 18 | }, [horizontalScroll, ref]) 19 | 20 | const handleRulerScroll = (e: React.UIEvent<HTMLDivElement>) => { 21 | dispatch(setMidiEditorHorizontalScroll(e.currentTarget.scrollLeft)) 22 | } 23 | 24 | return handleRulerScroll 25 | } 26 | -------------------------------------------------------------------------------- /src/model/track/drums/track-drums.ts: -------------------------------------------------------------------------------- 1 | import { DrumSound } from '../../drums/sound/drums-sound' 2 | 3 | export const TRACK_DRUM_PATTERN_SOUNDS_LENGTH = 16 4 | export const TRACK_DRUM_PATTERN_SIZE = 7 5 | export const TRACK_DRUM_PATTERNS = 8 6 | 7 | export type TrackDrumPatternSound = 'on' | 'off' 8 | 9 | export type TrackDrumPattern = { 10 | patternSounds: TrackDrumPatternSound[][] 11 | } 12 | 13 | export type TrackDrums = { 14 | selectedSounds: DrumSound[] 15 | patterns: TrackDrumPattern[] 16 | } 17 | 18 | export const EMPTY_DRUM_PATTERN: TrackDrumPatternSound[][] = Array.from({ 19 | length: TRACK_DRUM_PATTERN_SIZE, 20 | }).map(() => 21 | Array.from({ 22 | length: TRACK_DRUM_PATTERN_SOUNDS_LENGTH, 23 | }).map(() => 'off') 24 | ) 25 | 26 | export const EMPTY_DRUM_TRACK_PATTERNS: TrackDrumPattern[] = Array.from({ 27 | length: TRACK_DRUM_PATTERNS, 28 | }).map(() => { 29 | return { 30 | patternSounds: EMPTY_DRUM_PATTERN, 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /src/features/daw/playlist/hooks/use-flatboard-scroll.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux' 2 | import { RootState } from '../../../../store' 3 | import { useEffect } from 'react' 4 | import { setFlatboardScroll } from '../store/playlist-slice' 5 | 6 | export const useFlatboardScroll = ( 7 | elementsToControl: React.RefObject<HTMLDivElement>[] 8 | ) => { 9 | const flatboardScroll = useSelector( 10 | (state: RootState) => state.playlist.flatboardScroll 11 | ) 12 | const dispatch = useDispatch() 13 | 14 | useEffect(() => { 15 | elementsToControl.forEach((element) => { 16 | if (element.current && element.current.scrollTop !== flatboardScroll) { 17 | element.current.scrollTop = flatboardScroll 18 | } 19 | }) 20 | }, [flatboardScroll, elementsToControl]) 21 | 22 | const handleFlatboardScroll = (e: React.UIEvent<HTMLDivElement>) => { 23 | dispatch(setFlatboardScroll(e.currentTarget.scrollTop)) 24 | } 25 | 26 | return handleFlatboardScroll 27 | } 28 | -------------------------------------------------------------------------------- /src/sequencer/channel/instrument/channel-instrument-factory.ts: -------------------------------------------------------------------------------- 1 | import { DrumSoundUtils } from '../../../model/drums/sound/drums-sound' 2 | import { InstrumentPreset } from '../../../model/instrument/preset/preset' 3 | import { ChannelInstrument } from './channel-instrument' 4 | import SamplerInstrument from './sampler/sampler-instrument' 5 | import SynthInstrument from './synth/synth-instrument' 6 | 7 | export const createChannelInstrument = ( 8 | preset: InstrumentPreset 9 | ): ChannelInstrument => { 10 | switch (preset.instrument) { 11 | case 'DRUMS': 12 | return new SamplerInstrument(getDrumsSamples(preset)) 13 | case 'KEYBOARDS': 14 | return new SynthInstrument() 15 | default: 16 | return new SynthInstrument() 17 | } 18 | } 19 | 20 | const getDrumsSamples = (preset: InstrumentPreset) => { 21 | const sounds = DrumSoundUtils.getDrumsSoundSetByPreset(preset) 22 | return sounds.map((sound) => ({ 23 | key: sound.key, 24 | sampleUrl: sound.sampleUrl, 25 | })) 26 | } 27 | -------------------------------------------------------------------------------- /src/features/daw/menu/top-right/export/export.tsx: -------------------------------------------------------------------------------- 1 | import { FaCaretDown } from 'react-icons/fa' 2 | import { FiDownload } from 'react-icons/fi' 3 | import { useExportData } from './useExportData' 4 | import { PopupMenu } from '../../../common/components/popup-menu/popup-menu' 5 | 6 | export const Export = () => { 7 | const { onExportClick, menu } = useExportData() 8 | 9 | return ( 10 | <div className="relative h-full select-none"> 11 | <div 12 | className="flex flex-row gap-2 items-center justify-center p-2 rounded-xl cursor-pointer border-[1px] border-zinc-700 hover:border-zinc-400 dark:border-zinc-600 dark:hover:border-white" 13 | onClick={onExportClick} 14 | > 15 | <FiDownload /> 16 | <span className="text-md font-bold">Export</span> 17 | <FaCaretDown /> 18 | </div> 19 | 20 | {menu.isOpen && ( 21 | <div className="fixed -ml-20 z-50 h-fit w-fit"> 22 | <PopupMenu {...menu} /> 23 | </div> 24 | )} 25 | </div> 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/features/daw/common/components/mix-grid/mix-grid-item/mix-grid-item.tsx: -------------------------------------------------------------------------------- 1 | import { SUB_BAR_NUM } from '../../ruler/constants' 2 | 3 | export type MixGridItemProps = { 4 | barIndex: number 5 | currentSubBar: number 6 | onSelectTick: (tick: number) => void 7 | onCreateBar: (startTick: number) => void 8 | onContextMenu: ( 9 | e: React.MouseEvent<HTMLDivElement, MouseEvent>, 10 | tick: number 11 | ) => void 12 | } 13 | 14 | export const MixGridItem = ({ 15 | barIndex, 16 | currentSubBar, 17 | onSelectTick, 18 | onCreateBar, 19 | onContextMenu, 20 | }: MixGridItemProps) => { 21 | const tick = barIndex * SUB_BAR_NUM * 4 + currentSubBar * 4 22 | return ( 23 | <div 24 | key={currentSubBar} 25 | className={`border-slate-500 w-[20px] ${ 26 | currentSubBar == SUB_BAR_NUM - 1 ? '' : 'border-r' 27 | }`} 28 | onClick={() => onSelectTick(tick)} 29 | onDoubleClick={() => onCreateBar(tick)} 30 | onContextMenu={(e) => onContextMenu(e, tick)} 31 | /> 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/store/drum-machine-slice.ts: -------------------------------------------------------------------------------- 1 | import { PayloadAction, createSlice } from '@reduxjs/toolkit' 2 | import { TRACK_DRUM_PATTERNS } from '../../../../model/track/drums/track-drums' 3 | 4 | export interface DrumMachineState { 5 | isPlayingPattern: boolean 6 | maxTrackPatterns: number 7 | selectedPatternIndex: number 8 | } 9 | 10 | const initialState: DrumMachineState = { 11 | isPlayingPattern: false, 12 | maxTrackPatterns: TRACK_DRUM_PATTERNS, 13 | selectedPatternIndex: 0, 14 | } 15 | 16 | export const drumMachineSlice = createSlice({ 17 | name: 'drumMachine', 18 | initialState, 19 | reducers: { 20 | togglePlayPattern: (state) => { 21 | state.isPlayingPattern = !state.isPlayingPattern 22 | }, 23 | selectPattern: (state, action: PayloadAction<number>) => { 24 | state.selectedPatternIndex = action.payload 25 | }, 26 | }, 27 | }) 28 | 29 | export const { togglePlayPattern, selectPattern } = drumMachineSlice.actions 30 | 31 | export default drumMachineSlice.reducer 32 | -------------------------------------------------------------------------------- /src/features/daw/menu/hooks/import-export/useExport.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux' 2 | import { selectProjectTitle } from '../../store/selectors/menu-selectors' 3 | import { selectTracks } from '../../../playlist/store/selectors' 4 | import { saveAs } from 'file-saver' 5 | import { ExportData } from './types' 6 | 7 | export const useExport = () => { 8 | const projectTitle = useSelector(selectProjectTitle) 9 | const tracks = useSelector(selectTracks) 10 | 11 | function exportTracksToJSON() { 12 | const fileContent: ExportData = { 13 | project_title: projectTitle, 14 | tracks, 15 | created_at: new Date().toISOString(), 16 | version: 'liberty_beats/1.0.0', // Add version to the exported JSON file, to be able to handle future changes 17 | } 18 | const fileToSave = new Blob([JSON.stringify(fileContent)], { 19 | type: 'application/json', 20 | }) 21 | saveAs(fileToSave, `${projectTitle}-${new Date().toDateString()}`) 22 | } 23 | 24 | return { 25 | exportTracksToJSON, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/features/daw/midi-editor/hooks/useMidiEditorVerticalScroll.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux' 2 | import { RootState } from '../../../../store' 3 | import { useEffect } from 'react' 4 | import { setMidiEditorVerticalScroll } from '../store/midi-editor-slice' 5 | 6 | export const useMidiEditorVerticalScroll = ( 7 | elementsToControl: React.RefObject<HTMLDivElement>[] 8 | ) => { 9 | const verticalScroll = useSelector( 10 | (state: RootState) => state.midiEditor.verticalScroll 11 | ) 12 | const dispatch = useDispatch() 13 | 14 | useEffect(() => { 15 | elementsToControl.forEach((element) => { 16 | if (element.current && element.current.scrollTop !== verticalScroll) { 17 | element.current.scrollTop = verticalScroll 18 | } 19 | }) 20 | }, [verticalScroll, elementsToControl]) 21 | 22 | const handleFlatboardScroll = (e: React.UIEvent<HTMLDivElement>) => { 23 | dispatch(setMidiEditorVerticalScroll(e.currentTarget.scrollTop)) 24 | } 25 | 26 | return handleFlatboardScroll 27 | } 28 | -------------------------------------------------------------------------------- /src/features/daw/common/hooks/useHorizontalResize.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | export const useHorizontalResize = (initialWidth: number) => { 4 | const [elementWidth, setElementWidth] = useState(initialWidth) 5 | 6 | const handleResizeMouseDown = (e: React.MouseEvent<HTMLDivElement>) => { 7 | e.preventDefault() 8 | e.stopPropagation() 9 | 10 | const xBeforeResize = e.clientX 11 | 12 | const mouseMoveHandler = (e: MouseEvent) => { 13 | const dx = e.clientX - xBeforeResize 14 | const newWidth = elementWidth + dx 15 | setElementWidth(newWidth) 16 | } 17 | 18 | const mouseUpHandler = () => { 19 | document.removeEventListener('mouseup', mouseUpHandler) 20 | document.removeEventListener('mousemove', mouseMoveHandler) 21 | } 22 | 23 | document.addEventListener('mousemove', mouseMoveHandler) 24 | document.addEventListener('mouseup', mouseUpHandler) 25 | } 26 | 27 | return { 28 | elementWidth, 29 | setElementWidth, 30 | handleResizeMouseDown, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/features/daw/playlist/track-list/track-item/track-item-solo-mute/track-item-solo-muted.tsx: -------------------------------------------------------------------------------- 1 | import { TrackItemProps } from '../track-item' 2 | import { FaHeadphones } from 'react-icons/fa6' 3 | import { FaVolumeMute } from 'react-icons/fa' 4 | 5 | export const TrackItemSoloMuted = ({ 6 | track, 7 | onToggleMute, 8 | onToggleSolo, 9 | }: Pick<TrackItemProps, 'track' | 'onToggleMute' | 'onToggleSolo'>) => { 10 | return ( 11 | <div className="flex flex-col divide-y border-r border-slate-600 divide-slate-600 min-w-8 max-w-8 w-8 cursor-pointer"> 12 | <div 13 | className={`flex flex-1 w-full justify-center items-center ${ 14 | track.soloed ? 'bg-orange-400' : '' 15 | }`} 16 | onClick={onToggleSolo} 17 | > 18 | <FaHeadphones /> 19 | </div> 20 | <div 21 | className={`flex flex-1 w-full justify-center items-center ${ 22 | track.muted && 'bg-gray-500' 23 | }`} 24 | onClick={onToggleMute} 25 | > 26 | <FaVolumeMute /> 27 | </div> 28 | </div> 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.vscode/qwik-city.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "onRequest": { 3 | "scope": "javascriptreact,typescriptreact", 4 | "prefix": "qonRequest", 5 | "description": "onRequest function for a route index", 6 | "body": [ 7 | "export const onRequest: RequestHandler = (request) => {", 8 | " $0", 9 | "};", 10 | ], 11 | }, 12 | "loader$": { 13 | "scope": "javascriptreact,typescriptreact", 14 | "prefix": "qloader$", 15 | "description": "loader$()", 16 | "body": ["export const $1 = routeLoader$(() => {", " $0", "});"], 17 | }, 18 | "action$": { 19 | "scope": "javascriptreact,typescriptreact", 20 | "prefix": "qaction$", 21 | "description": "action$()", 22 | "body": ["export const $1 = routeAction$((data) => {", " $0", "});"], 23 | }, 24 | "Full Page": { 25 | "scope": "javascriptreact,typescriptreact", 26 | "prefix": "qpage", 27 | "description": "Simple page component", 28 | "body": [ 29 | "import { component$ } from '@builder.io/qwik';", 30 | "", 31 | "export default component$(() => {", 32 | " $0", 33 | "});", 34 | ], 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /src/features/daw/menu/store/menu-slice.ts: -------------------------------------------------------------------------------- 1 | import { PayloadAction, createSlice } from '@reduxjs/toolkit' 2 | 3 | export type Theme = 'light' | 'dark' 4 | 5 | export interface MenuState { 6 | projectTitle: string 7 | theme: Theme 8 | } 9 | 10 | const initialState: MenuState = { 11 | projectTitle: 'New Project', 12 | theme: 'light', 13 | } 14 | 15 | export const menuSlice = createSlice({ 16 | name: 'menu', 17 | initialState, 18 | reducers: { 19 | setProjectTitle: (state, action: PayloadAction<string>) => { 20 | state.projectTitle = action.payload 21 | }, 22 | setTheme: (state, action: PayloadAction<Theme>) => { 23 | if (action.payload === 'dark') { 24 | document.documentElement.classList.add('dark') 25 | localStorage.theme = 'dark' 26 | } else { 27 | document.documentElement.classList.remove('dark') 28 | localStorage.theme = 'light' 29 | } 30 | 31 | state.theme = action.payload 32 | }, 33 | }, 34 | }) 35 | 36 | export const { setProjectTitle, setTheme } = menuSlice.actions 37 | 38 | export default menuSlice.reducer 39 | -------------------------------------------------------------------------------- /src/features/daw/midi-editor/midi-editor.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux' 2 | import { selectSelectedBottomUpPanel } from '../bottom-bar/store/selectors' 3 | import { selectSelectedTrack } from '../playlist/store/selectors' 4 | import { MidiHeader } from './midi-header/midi-header' 5 | import { MidiBody } from './midi-body/midi-body' 6 | 7 | export const MidiEditor = () => { 8 | const selectedBottomUpPanel = useSelector(selectSelectedBottomUpPanel) 9 | const isSelected = selectedBottomUpPanel === 'midiEditor' 10 | 11 | const selectedTrack = useSelector(selectSelectedTrack) 12 | 13 | if (!selectedTrack) return null 14 | 15 | return ( 16 | <div className={`${!isSelected && 'hidden'}`}> 17 | <div className="flex flex-col bg-stone-200 dark:bg-stone-900 h-96 divide-y divide-slate-600 border-t border-slate-600 mx-1 px-2"> 18 | <div className="flex h-[15%] w-full"> 19 | <MidiHeader selectedTrack={selectedTrack} /> 20 | </div> 21 | 22 | <div className="flex h-[85%] overflow-auto"> 23 | <MidiBody /> 24 | </div> 25 | </div> 26 | </div> 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/header/drum-machine-header.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch } from 'react-redux' 2 | import { Track } from '../../../../model/track/track' 3 | import { closeAllBottomUpPanels } from '../../bottom-bar/store/bottom-bar-slice' 4 | import { IoClose } from 'react-icons/io5' 5 | 6 | export type DrumMachineHeaderProps = { 7 | selectedTrack: Track 8 | } 9 | 10 | export const DrumMachineHeader = ({ 11 | selectedTrack, 12 | }: DrumMachineHeaderProps) => { 13 | const dispatch = useDispatch() 14 | return ( 15 | <div className="flex h-full w-full flex-row justify-between divide-x divide-slate-600"> 16 | <div className="flex flex-row h-full justify-between divide-x divide-slate-600 max-w-72 min-w-72"> 17 | <div 18 | className="flex flex-grow cursor-pointer items-center justify-center" 19 | onClick={() => dispatch(closeAllBottomUpPanels())} 20 | > 21 | <IoClose /> 22 | </div> 23 | <div className="flex w-[85%] items-center justify-center"> 24 | <p className="font-semibold text-md">{selectedTrack.title}</p> 25 | </div> 26 | </div> 27 | </div> 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/features/daw/playlist/track-list/track-list.tsx: -------------------------------------------------------------------------------- 1 | import { TrackItem } from './track-item/track-item' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { selectSelectedTrack, selectTracks } from '../store/selectors' 4 | import { Track } from '../../../../model/track/track' 5 | import { 6 | selectTrack, 7 | toggleTrackMute, 8 | toggleTrackSolo, 9 | } from '../store/playlist-slice' 10 | 11 | export const TrackList = () => { 12 | const tracks = useSelector(selectTracks) 13 | const selectedTrack = useSelector(selectSelectedTrack) 14 | const dipatch = useDispatch() 15 | const handleSelectTrack = (track: Track) => { 16 | dipatch(selectTrack(track)) 17 | } 18 | 19 | return ( 20 | <div className="flex flex-col gap-1 w-full"> 21 | {tracks.map((track: Track) => ( 22 | <TrackItem 23 | key={track.id} 24 | track={track} 25 | selectedTrack={selectedTrack} 26 | onSelectTrack={handleSelectTrack} 27 | onToggleMute={() => dipatch(toggleTrackMute(track.id))} 28 | onToggleSolo={() => dipatch(toggleTrackSolo(track.id))} 29 | /> 30 | ))} 31 | </div> 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/sequencer/channel/instrument/sampler/sampler-instrument.ts: -------------------------------------------------------------------------------- 1 | import { Key } from '../../../../model/note/key/key' 2 | import { Volume } from '../../../volume/volume' 3 | import { ChannelInstrument } from '../channel-instrument' 4 | import * as Tone from 'tone' 5 | 6 | export type SamplerInstrumentSound = { 7 | key: Key 8 | sampleUrl: string 9 | } 10 | 11 | export default class SamplerInstrument implements ChannelInstrument { 12 | _sampler: Tone.Sampler 13 | 14 | constructor(sounds: SamplerInstrumentSound[]) { 15 | this._sampler = new Tone.Sampler() 16 | this._setup(sounds) 17 | } 18 | 19 | connect(): void { 20 | this._sampler.toDestination() 21 | } 22 | disconnect(): void { 23 | this._sampler.disconnect() 24 | } 25 | setVolume(volume: number): void { 26 | this._sampler.volume.value = Volume.transformVolumeToToneVolume(volume) 27 | } 28 | 29 | play(note: Key, duration: string, time?: number, velocity?: number) { 30 | this._sampler.triggerAttackRelease(note, duration, time, velocity) 31 | } 32 | 33 | _setup(sounds: SamplerInstrumentSound[]) { 34 | sounds.forEach((sound) => { 35 | this._sampler.add(sound.key, sound.sampleUrl) 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/features/daw/menu/top-right/theme/theme.tsx: -------------------------------------------------------------------------------- 1 | import { MdLightMode, MdDarkMode } from 'react-icons/md' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { selectTheme } from '../../store/selectors/menu-selectors' 4 | import { setTheme } from '../../store/menu-slice' 5 | 6 | export const Theme = () => { 7 | const dispatch = useDispatch() 8 | const currentMode = useSelector(selectTheme) 9 | 10 | return ( 11 | <div className="flex flex-row items-center justify-end h-full select-none"> 12 | <div className="flex flex-row gap-1 rounded-full cursor-pointer border-2 border-zinc-700 dark:border-zinc-600"> 13 | <div 14 | className={`p-1 rounded-full ${ 15 | currentMode === 'light' ? 'bg-zinc-700 text-white' : '' 16 | }`} 17 | onClick={() => dispatch(setTheme('light'))} 18 | > 19 | <MdLightMode /> 20 | </div> 21 | 22 | <div 23 | className={`p-1 rounded-full ${ 24 | currentMode === 'dark' ? 'bg-zinc-400 text-zinc-800' : '' 25 | }`} 26 | onClick={() => dispatch(setTheme('dark'))} 27 | > 28 | <MdDarkMode /> 29 | </div> 30 | </div> 31 | </div> 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/features/daw/playlist-header/playlist-commands/playlist-commands.tsx: -------------------------------------------------------------------------------- 1 | import { FaPlus } from 'react-icons/fa' 2 | import { IoMdClose } from 'react-icons/io' 3 | import { useState } from 'react' 4 | import { AddTrackMenu } from './add-track-menu/add-track-menu' 5 | 6 | export const PlaylistCommands = () => { 7 | const [showAddMenu, setShowAddMenu] = useState(false) 8 | 9 | return ( 10 | <div className="flex flex-row"> 11 | <div className="relative h-full w-full"> 12 | <button 13 | onClick={() => { 14 | if (!showAddMenu) { 15 | setShowAddMenu(true) 16 | } 17 | }} 18 | > 19 | <div className="flex flex-row items-center justify-center gap-2"> 20 | {showAddMenu ? ( 21 | <IoMdClose className="animate-in spin-in" /> 22 | ) : ( 23 | <FaPlus className="animate-out spin-out" /> 24 | )} 25 | <p>Add Track</p> 26 | </div> 27 | </button> 28 | 29 | {showAddMenu && ( 30 | <div className="fixed mt-2 z-50 h-fit w-fit"> 31 | <AddTrackMenu onClose={() => setShowAddMenu(false)} /> 32 | </div> 33 | )} 34 | </div> 35 | </div> 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/features/daw/common/components/piano-roll-key/common/piano-roll-key-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { TrackUtils } from '../../../../../../model/track/track' 2 | import { PianoRollKeyProps } from '../types' 3 | 4 | export type PianoRollKeySkeletonProps = PianoRollKeyProps & { 5 | PianoRollKeyBody: React.FC<PianoRollKeyProps> 6 | } 7 | 8 | export const PianoRollKeySkeleton = ({ 9 | PianoRollKeyBody, 10 | ...props 11 | }: PianoRollKeySkeletonProps) => { 12 | const noteLeftOffsetPixel = props.note.startsAtRelativeTick * props.beatWidth 13 | const noteTopOffsetPixel = 14 | props.showedKeys.indexOf(props.note.key) * props.keyHeight 15 | const noteColor = TrackUtils.isTrackEffectivelyMuted(props.track) 16 | ? 'bg-gray-600 dark:bg-gray-400' 17 | : props.nonMutedColorTailwindClass 18 | ? props.nonMutedColorTailwindClass 19 | : `bg-${props.track.color}-500` 20 | return ( 21 | <div 22 | className={`absolute ${noteColor} z-20 w-fit ${ 23 | props.selected && 'border-[1px] border-white' 24 | }`} 25 | style={{ 26 | height: `${props.keyHeight}px`, 27 | left: `${noteLeftOffsetPixel}px`, 28 | top: `${noteTopOffsetPixel}px`, 29 | }} 30 | > 31 | <PianoRollKeyBody {...props} /> 32 | </div> 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/features/daw/common/components/switch/switch.tsx: -------------------------------------------------------------------------------- 1 | import { TrackColor } from '../../../../../model/track/track-color' 2 | 3 | export type SwitchProps = { 4 | checked: boolean 5 | mainColor?: TrackColor 6 | 7 | onChange: (checked: boolean) => void 8 | } 9 | 10 | export const Switch = ({ checked, onChange, mainColor }: SwitchProps) => { 11 | return ( 12 | <div> 13 | <label className="inline-flex items-center cursor-pointer"> 14 | <input 15 | type="checkbox" 16 | checked={checked} 17 | value="" 18 | className="sr-only peer" 19 | onChange={(e) => onChange(e.target.checked)} 20 | /> 21 | <div 22 | className={`relative w-11 h-6 bg-gray-300 dark:bg-gray-700 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-${mainColor}-800 rounded-full peer peer-checked:after:translate-x-full border-gray-900 dark:border-gray-600 rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-black dark:after:bg-white after:border-gray-900 dark:after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-${mainColor}-600`} 23 | ></div> 24 | </label> 25 | </div> 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/features/daw/playlist/playlist.tsx: -------------------------------------------------------------------------------- 1 | import { TrackList } from './track-list/track-list' 2 | import { Flatboard } from './flatboard/flatboard' 3 | import React from 'react' 4 | import { useRulerScroll } from '../common/hooks/use-ruler-scroll' 5 | import { useFlatboardScroll } from './hooks/use-flatboard-scroll' 6 | 7 | export const Playlist = () => { 8 | const flatboardRef = React.useRef<HTMLDivElement>(null) 9 | const trackListDivRef = React.useRef<HTMLDivElement>(null) 10 | 11 | const handleRulerScroll = useRulerScroll(flatboardRef) 12 | const handleTrackListScroll = useFlatboardScroll([ 13 | trackListDivRef, 14 | flatboardRef, 15 | ]) 16 | 17 | return ( 18 | <div className="flex flex-row justify-between gap-1 divide-x divide-slate-600 w-full "> 19 | <div 20 | className="flex max-w-72 min-w-72 no-scrollbar overflow-y-scroll" 21 | onScroll={handleTrackListScroll} 22 | ref={trackListDivRef} 23 | > 24 | <TrackList /> 25 | </div> 26 | 27 | <div 28 | className="flex flex-grow px-2 overflow-auto" 29 | onScroll={(e) => { 30 | handleRulerScroll(e) 31 | handleTrackListScroll(e) 32 | }} 33 | ref={flatboardRef} 34 | > 35 | <Flatboard /> 36 | </div> 37 | </div> 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liberty-beats", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@reduxjs/toolkit": "^2.2.1", 14 | "file-saver": "^2.0.5", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-icons": "^5.0.1", 18 | "react-redux": "^9.1.0", 19 | "tone": "^14.7.77", 20 | "uuid": "^9.0.1" 21 | }, 22 | "devDependencies": { 23 | "@types/file-saver": "^2.0.7", 24 | "@types/react": "^18.2.56", 25 | "@types/react-dom": "^18.2.19", 26 | "@types/uuid": "^9.0.8", 27 | "@typescript-eslint/eslint-plugin": "^7.0.2", 28 | "@typescript-eslint/parser": "^7.0.2", 29 | "@vitejs/plugin-react": "^4.2.1", 30 | "autoprefixer": "^10.4.18", 31 | "eslint": "^8.56.0", 32 | "eslint-plugin-react-hooks": "^4.6.0", 33 | "eslint-plugin-react-refresh": "^0.4.5", 34 | "postcss": "^8.4.35", 35 | "tailwindcss": "^3.4.1", 36 | "tailwindcss-animate": "^1.0.7", 37 | "typescript": "^5.2.2", 38 | "vite": "^5.1.4" 39 | }, 40 | "optionalDependencies": { 41 | "@rollup/rollup-linux-x64-gnu": "^4.17.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/features/daw/common/hooks/use-preview-loop-safe-transport-position.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { selectTrackIdInPlayingPreviewloop } from '../../instrument/store/selectors' 4 | import { selectTime } from '../../player-bar/store/selectors' 5 | import { selectCurrentTick } from '../../playlist-header/store/selectors' 6 | 7 | /** 8 | * This will return the time and tick from the store ONLY IF a preview loop is not running. if the loop is playing, we don't want to update the time from the store, because the preview loop of an active pattern will be played at the start of the track 9 | * @returns {Object} time and tick 10 | */ 11 | export const usePreviewLoopSafeTransportPosition = () => { 12 | const [time, setTime] = useState(0) 13 | const [tick, setTick] = useState(0) 14 | 15 | const timeFromStore = useSelector(selectTime) 16 | const tickFromStore = useSelector(selectCurrentTick) 17 | const previewLoopPlayingTrackId = useSelector( 18 | selectTrackIdInPlayingPreviewloop 19 | ) 20 | 21 | useEffect(() => { 22 | if (!previewLoopPlayingTrackId) { 23 | setTime(timeFromStore) 24 | setTick(tickFromStore) 25 | } 26 | }, [previewLoopPlayingTrackId, tickFromStore, timeFromStore]) 27 | 28 | return { 29 | time, 30 | tick, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/features/daw/daw.tsx: -------------------------------------------------------------------------------- 1 | import { BottomBar } from './bottom-bar/bottom-bar' 2 | import { Dialog } from './dialog/dialog' 3 | import { DrumMachine } from './drum-machine/drum-machine' 4 | import { InstrumentSetup } from './instrument/instrument-setup' 5 | import { Menu } from './menu/menu' 6 | import { MidiEditor } from './midi-editor/midi-editor' 7 | import { PlayerBar } from './player-bar/player-bar' 8 | import { PlaylistHeader } from './playlist-header/playlist-header' 9 | import { Playlist } from './playlist/playlist' 10 | 11 | export const DAW = () => { 12 | return ( 13 | <main 14 | className="flex flex-col h-screen bg-white text-black dark:bg-black dark:text-white py-2" 15 | onContextMenu={(e) => e.preventDefault()} 16 | > 17 | <div className="px-2 pb-1"> 18 | <Menu /> 19 | </div> 20 | 21 | <div className="px-2 pb-2"> 22 | <PlayerBar /> 23 | </div> 24 | 25 | <div className="px-2"> 26 | <PlaylistHeader /> 27 | </div> 28 | 29 | <div className="flex flex-grow overflow-y-auto px-2"> 30 | <Playlist /> 31 | </div> 32 | 33 | <div> 34 | <InstrumentSetup /> 35 | <MidiEditor /> 36 | <DrumMachine /> 37 | </div> 38 | 39 | <div> 40 | <BottomBar /> 41 | </div> 42 | 43 | <Dialog /> 44 | </main> 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/pad/sound-selector/category/drum-machine-ride-icon.tsx: -------------------------------------------------------------------------------- 1 | export const DrumMachineRideIcon = (props: React.SVGProps<SVGSVGElement>) => ( 2 | <svg 3 | xmlns="http://www.w3.org/2000/svg" 4 | className="fill-current" 5 | viewBox="0 0 300.000000 300.000000" 6 | {...props} 7 | > 8 | <g 9 | transform="translate(0.000000,300.000000) scale(0.100000,-0.100000)" 10 | fill="currentColor" 11 | stroke="none" 12 | > 13 | <path d="M1487 2804 c-10 -37 -17 -67 -15 -68 6 -5 173 -46 183 -46 9 0 50 130 43 135 -8 5 -168 45 -180 45 -9 0 -20 -24 -31 -66z" /> 14 | <path d="M18 2824 c-26 -8 -22 -18 18 -53 218 -192 1649 -635 2506 -776 232 -38 458 -46 458 -15 0 39 -211 149 -490 256 -587 224 -1534 480 -2110 569 -106 17 -350 28 -382 19z" /> 15 | <path d="M1386 2699 c-17 -13 -26 -30 -26 -48 0 -27 3 -28 158 -70 86 -24 164 -44 172 -45 8 0 20 11 27 26 12 23 12 32 -1 56 -13 26 -27 32 -154 65 -76 21 -141 37 -144 37 -3 0 -17 -9 -32 -21z" /> 16 | <path d="M1290 2208 c-12 -24 -36 -54 -52 -66 -76 -62 -94 -187 -38 -264 20 -27 20 -48 20 -893 l0 -865 75 0 75 0 0 835 0 835 22 0 c43 0 109 42 138 87 59 93 48 179 -32 256 -40 39 -47 51 -38 63 10 11 9 15 -1 18 -38 13 -124 36 -134 36 -6 0 -22 -19 -35 -42z m119 -182 c43 -45 0 -108 -62 -93 -25 7 -51 51 -41 70 30 52 68 61 103 23z" /> 17 | </g> 18 | </svg> 19 | ) 20 | export default DrumMachineRideIcon 21 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/pad/sound-selector/drum-machine-pad-sound-selector.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch } from 'react-redux' 2 | import { DrumSound } from '../../../../../model/drums/sound/drums-sound' 3 | import { setCurrentTrackDrumsSounds } from '../../../playlist/store/playlist-slice' 4 | import { DrumMachinePadSoundItem } from './pad-sound-item/drum-machine-drum-sound-item' 5 | import { Track } from '../../../../../model/track/track' 6 | 7 | export type DrumMachinePadSoundSelectorProps = { 8 | selectedTrack: Track 9 | selectedSounds: DrumSound[] 10 | } 11 | 12 | export const DrumMachinePadSoundSelector = ({ 13 | selectedSounds, 14 | selectedTrack, 15 | }: DrumMachinePadSoundSelectorProps) => { 16 | const dispatch = useDispatch() 17 | return ( 18 | <div className="flex flex-col gap-1"> 19 | {selectedSounds.map((sound, index) => { 20 | return ( 21 | <DrumMachinePadSoundItem 22 | key={index} 23 | selectedTrack={selectedTrack} 24 | sound={sound} 25 | index={index} 26 | onSelectedSound={(sound) => { 27 | const newSounds = selectedSounds.map((s, i) => 28 | i === index ? sound : s 29 | ) 30 | dispatch(setCurrentTrackDrumsSounds(newSounds)) 31 | }} 32 | /> 33 | ) 34 | })} 35 | </div> 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/features/daw/dialog/modal/content.tsx: -------------------------------------------------------------------------------- 1 | import { ModalProps } from './modal' 2 | import { IoClose } from 'react-icons/io5' 3 | 4 | export const ModalContent = ({ onClose, dialog }: ModalProps) => { 5 | const size = dialog.options?.size || 'md' 6 | 7 | const canClose = dialog.options?.canClose ?? true 8 | 9 | const sizesByType = { 10 | sm: { 11 | width: '350px', 12 | height: '280px', 13 | }, 14 | md: { 15 | width: '250px', 16 | height: '250px', 17 | }, 18 | lg: { 19 | width: '250px', 20 | height: '250px', 21 | }, 22 | xl: { 23 | width: '250px', 24 | height: '250px', 25 | }, 26 | } 27 | 28 | return ( 29 | <div 30 | className="flex flex-col bg-slate-200 z-50 dark:bg-stone-800 text-black dark:text-white rounded-2xl overflow-hidden drop-shadow-2xl py-2 px-4" 31 | style={{ 32 | width: sizesByType[size].width, 33 | height: sizesByType[size].height, 34 | }} 35 | > 36 | <div className="flex flex-row h-12 justify-end"> 37 | {canClose && ( 38 | <div 39 | className="cursor-pointer hover:text-slate-600 dark:hover:text-slate-400" 40 | onClick={onClose} 41 | > 42 | <IoClose size={24} /> 43 | </div> 44 | )} 45 | </div> 46 | 47 | {dialog.component} 48 | </div> 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /src/features/daw/bottom-bar/bottom-bar-item/bottom-bar-item.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch } from 'react-redux' 2 | import { BottomUpPanel } from '../types/bottom-up-panel' 3 | import { 4 | closeAllBottomUpPanels, 5 | selectBottomUpPanel, 6 | } from '../store/bottom-bar-slice' 7 | import { Track } from '../../../../model/track/track' 8 | 9 | export type BottomBarItemProps = { 10 | label: string 11 | bottomUpPanel: BottomUpPanel 12 | selectedTrack?: Track 13 | selectedBottomUpPanel: BottomUpPanel | null 14 | } 15 | 16 | export const BottomBarItem = ({ 17 | selectedTrack, 18 | selectedBottomUpPanel, 19 | bottomUpPanel, 20 | label, 21 | }: BottomBarItemProps) => { 22 | const dispatch = useDispatch() 23 | const isSelected = selectedBottomUpPanel === bottomUpPanel 24 | 25 | const handleInstrumentSelection = () => { 26 | if (isSelected) { 27 | dispatch(closeAllBottomUpPanels()) 28 | } else { 29 | dispatch(selectBottomUpPanel(bottomUpPanel)) 30 | } 31 | } 32 | 33 | return ( 34 | <div className="flex"> 35 | <button 36 | disabled={!selectedTrack} 37 | onClick={handleInstrumentSelection} 38 | className={`text-xs font-bold ${ 39 | !selectedTrack && 'bg-gray-300 dark:bg-gray-600' 40 | } ${isSelected && 'bg-black text-white dark:bg-white dark:text-black'}`} 41 | > 42 | {label} 43 | </button> 44 | </div> 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/drum-machine.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux' 2 | import { selectSelectedBottomUpPanel } from '../bottom-bar/store/selectors' 3 | import { selectSelectedTrack } from '../playlist/store/selectors' 4 | import { DrumMachineHeader } from './header/drum-machine-header' 5 | import { DrumMachineConfig } from './config/drum-machine-config' 6 | import { DrumMachinePad } from './pad/drum-machine-pad' 7 | 8 | export const DrumMachine = () => { 9 | const selectedBottomUpPanel = useSelector(selectSelectedBottomUpPanel) 10 | const isSelected = selectedBottomUpPanel === 'drumMachine' 11 | 12 | const selectedTrack = useSelector(selectSelectedTrack) 13 | 14 | if (!selectedTrack) return null 15 | 16 | return ( 17 | <div className={`${!isSelected && 'hidden'}`}> 18 | <div className="flex flex-col bg-stone-200 dark:bg-stone-900 h-96 divide-y divide-slate-600 border-t border-slate-600 mx-1 px-2"> 19 | <div className="flex h-[15%] w-full"> 20 | <DrumMachineHeader selectedTrack={selectedTrack} /> 21 | </div> 22 | 23 | <div className="flex h-[15%] w-full"> 24 | <DrumMachineConfig selectedTrack={selectedTrack} /> 25 | </div> 26 | 27 | <div className="flex w-full flex-grow overflow-auto bg-zinc-200 dark:bg-zinc-800"> 28 | <DrumMachinePad selectedTrack={selectedTrack} /> 29 | </div> 30 | </div> 31 | </div> 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer utilities { 6 | /* Hide scrollbar for Chrome, Safari, and Opera */ 7 | .no-scrollbar::-webkit-scrollbar { 8 | display: none; 9 | } 10 | 11 | /* Hide scrollbar for IE, Edge, and Firefox */ 12 | .no-scrollbar { 13 | -ms-overflow-style: none; /* IE and Edge */ 14 | scrollbar-width: none; /* Firefox */ 15 | } 16 | } 17 | 18 | @layer base { 19 | select { 20 | @apply bg-gray-300 dark:bg-gray-700 p-2 border border-gray-400 dark:border-gray-600 dark:text-white text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full placeholder-gray-400; 21 | } 22 | 23 | button { 24 | @apply dark:bg-stone-800 bg-stone-200 rounded-xl text-lg border-[1px] border-stone-800 dark:border-stone-200 py-2 px-4 font-medium cursor-pointer transition-colors hover:border-blue-600 dark:hover:border-blue-400; 25 | } 26 | 27 | h2 { 28 | @apply text-2xl font-semibold text-center; 29 | } 30 | } 31 | 32 | :root { 33 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 34 | line-height: 1.5; 35 | font-weight: 400; 36 | 37 | color-scheme: light dark; 38 | 39 | font-synthesis: none; 40 | text-rendering: optimizeLegibility; 41 | -webkit-font-smoothing: antialiased; 42 | -moz-osx-font-smoothing: grayscale; 43 | } 44 | 45 | body { 46 | margin: 0; 47 | } 48 | 49 | h1 { 50 | font-size: 3.2em; 51 | line-height: 1.1; 52 | } 53 | -------------------------------------------------------------------------------- /src/features/daw/player-bar/master-volume/master-volume.tsx: -------------------------------------------------------------------------------- 1 | import { FaVolumeHigh } from 'react-icons/fa6' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { selectVolume } from '../store/selectors' 4 | import { setVolume } from '../store/playerBarSlice' 5 | import { MAX_VOLUME } from '../constants/player-bar-constants' 6 | import { Volume } from '../../../../sequencer/volume/volume' 7 | 8 | export const MasterVolume = () => { 9 | const volume = useSelector(selectVolume) 10 | const dispatch = useDispatch() 11 | 12 | const volumeInDb = Volume.transformVolumeToToneVolume(volume).toFixed(1) 13 | return ( 14 | <div className="flex flex-row justify-start items-center gap-2"> 15 | <div className="flex flex-row items-center gap-2"> 16 | <FaVolumeHigh /> 17 | <input 18 | id="volume-range" 19 | type="range" 20 | min="0" 21 | max={MAX_VOLUME} 22 | value={volume} 23 | onChange={(e) => { 24 | dispatch(setVolume(e.target.valueAsNumber)) 25 | }} 26 | className="h-2 w-full cursor-ew-resize appearance-none rounded-lg dark:bg-gray-700 bg-gray-300 accent-blue-500 dark:accent-blue-400" 27 | ></input> 28 | </div> 29 | 30 | <div className="flex flex-row items-center gap-1"> 31 | <span className="font-bold text-sm">{volumeInDb}</span> 32 | <span className="font-light text-xs">db</span> 33 | </div> 34 | </div> 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/features/daw/menu/hamburger/hamburger.tsx: -------------------------------------------------------------------------------- 1 | import { GiHamburgerMenu } from 'react-icons/gi' 2 | 3 | import logo from '../../../../assets/image/logo.webp' 4 | import { useHamburgerData } from './useHamburgerData' 5 | import { PopupMenu } from '../../common/components/popup-menu/popup-menu' 6 | 7 | export const Hamburger = () => { 8 | const { onHamburgerClick, menu } = useHamburgerData() 9 | 10 | return ( 11 | <div className="relative h-full select-none"> 12 | <div 13 | className="flex h-full items-center justify-center cursor-pointer gap-2" 14 | onClick={onHamburgerClick} 15 | > 16 | <GiHamburgerMenu size={28} /> 17 | 18 | <img className="h-full" src={logo} alt="Logo" /> 19 | 20 | <div className="flex items-center justify-center"> 21 | <span className="absolute flex bg-gradient-to-r blur-lg from-blue-500 via-teal-500 to-pink-500 bg-clip-text text-3xl box-content font-extrabold text-transparent text-center"> 22 | Liberty Beats 23 | </span> 24 | <h1 className="relative top-0 w-fit h-auto justify-center flex bg-gradient-to-r items-center from-blue-500 via-teal-500 to-pink-500 bg-clip-text text-3xl font-extrabold text-transparent text-center"> 25 | Liberty Beats 26 | </h1> 27 | </div> 28 | </div> 29 | 30 | {menu.isOpen && ( 31 | <div className="fixed z-50 h-fit w-fit"> 32 | <PopupMenu {...menu} /> 33 | </div> 34 | )} 35 | </div> 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/sequencer/metronome/metronome.ts: -------------------------------------------------------------------------------- 1 | import * as Tone from 'tone' 2 | import metronomeDown from '../../assets/metronome_down.wav' 3 | import metronomeUp from '../../assets/metronome_up.wav' 4 | import { RootStore } from '../../store' 5 | import { TimeUtils } from '../time/utils/time-utils' 6 | import { observeStore } from '../../store/observers' 7 | import { selectMetronomeActive } from '../../features/daw/player-bar/store/selectors' 8 | 9 | export class Metronome { 10 | private _playerUp: Tone.Player 11 | private _playerDown: Tone.Player 12 | private _store: RootStore 13 | private _loop!: Tone.Loop 14 | 15 | constructor(store: RootStore) { 16 | this._playerUp = new Tone.Player(metronomeUp).toDestination() 17 | this._playerDown = new Tone.Player(metronomeDown).toDestination() 18 | this._store = store 19 | 20 | this.setup() 21 | this.registerStoreListeners() 22 | } 23 | 24 | setup() { 25 | this._loop = new Tone.Loop(() => { 26 | const tick = TimeUtils.toneTimeToTicks(Tone.Transport.position) 27 | if (Math.floor(tick) % 16 === 0) { 28 | this._playerUp.start() 29 | } else { 30 | this._playerDown.start() 31 | } 32 | }, '4n') 33 | this._loop.start(0) 34 | } 35 | 36 | registerStoreListeners() { 37 | observeStore( 38 | this._store, 39 | selectMetronomeActive, 40 | this.changeMetronomeState.bind(this) 41 | ) 42 | } 43 | 44 | changeMetronomeState(isActive: boolean) { 45 | this._loop.mute = !isActive 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/features/daw/midi-editor/midi-body/left-menu/left-menu-scale-view/left-menu-scale-view.tsx: -------------------------------------------------------------------------------- 1 | import { FaItunesNote } from 'react-icons/fa6' 2 | import { Scale } from '../../../../../../model/scale/scale' 3 | import { selectSelectedTrack } from '../../../../playlist/store/selectors' 4 | import { useDispatch, useSelector } from 'react-redux' 5 | import { 6 | selectScaleViewEnabled, 7 | selectSelectedScale, 8 | } from '../../../store/selectors' 9 | import { 10 | selectScale, 11 | toggleScaleViewEnabled, 12 | } from '../../../store/midi-editor-slice' 13 | import { ScaleSelector } from '../../../../common/components/scale-selector/scale-selector' 14 | 15 | export const LeftMenuScaleView = () => { 16 | const selectedTrack = useSelector(selectSelectedTrack) 17 | const scaleViewEnabled = useSelector(selectScaleViewEnabled) 18 | const selectedScale = useSelector(selectSelectedScale) 19 | const dispatch = useDispatch() 20 | 21 | return ( 22 | <div className="flex flex-col gap-2"> 23 | <div className="flex flex-row gap-2"> 24 | <FaItunesNote className={`text-${selectedTrack?.color}-500`} /> 25 | <p className="text-sm font-bold">Scale View</p> 26 | </div> 27 | 28 | <ScaleSelector 29 | scaleViewEnabled={scaleViewEnabled} 30 | selectedTrack={selectedTrack} 31 | selectedScale={selectedScale} 32 | onToggleScaleViewEnabled={() => dispatch(toggleScaleViewEnabled())} 33 | onSelectScale={(scale: Scale) => dispatch(selectScale(scale))} 34 | /> 35 | </div> 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/pad/sound-selector/category/drum-category-icon.tsx: -------------------------------------------------------------------------------- 1 | import { DrumCategory } from '../../../../../../model/drums/category/drum-category' 2 | import DrumMachineClapIcon from './drum-machine-clap-icon' 3 | import DrumMachineClosedHiHatIcon from './drum-machine-closed-hh-icon' 4 | import DrumMachineCrashIcon from './drum-machine-crash-icon' 5 | import DrumMachineKickIcon from './drum-machine-kick-icon' 6 | import DrumMachineOpenHiHatIcon from './drum-machine-open-hh-icon' 7 | import DrumMachineRideIcon from './drum-machine-ride-icon' 8 | import DrumMachineSnareIcon from './drum-machine-snare-icon' 9 | import DrumMachineTomIcon from './drum-machine-tom-icon' 10 | 11 | export type DrumCategoryIconProps = React.SVGProps<SVGSVGElement> & { 12 | category: DrumCategory 13 | } 14 | 15 | export const DrumMachineCategoryIcon = ({ 16 | category, 17 | ...svgProps 18 | }: DrumCategoryIconProps) => { 19 | switch (category) { 20 | case 'HAND_CLAP': 21 | return <DrumMachineClapIcon {...svgProps} /> 22 | case 'CLOSED_HI_HAT': 23 | return <DrumMachineClosedHiHatIcon {...svgProps} /> 24 | case 'OPEN_HI_HAT': 25 | return <DrumMachineOpenHiHatIcon {...svgProps} /> 26 | case 'CRASH': 27 | return <DrumMachineCrashIcon {...svgProps} /> 28 | case 'KICK': 29 | return <DrumMachineKickIcon {...svgProps} /> 30 | case 'RIDE': 31 | return <DrumMachineRideIcon {...svgProps} /> 32 | case 'SNARE': 33 | return <DrumMachineSnareIcon {...svgProps} /> 34 | case 'TOM': 35 | return <DrumMachineTomIcon {...svgProps} /> 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | const colorNames = [ 3 | 'red', 4 | 'blue', 5 | 'green', 6 | 'yellow', 7 | 'orange', 8 | 'purple', 9 | 'cyan', 10 | 'pink', 11 | ] 12 | const safelist = colorNames 13 | .map((color) => [ 14 | `bg-${color}-50`, 15 | `bg-${color}-100`, 16 | `bg-${color}-200`, 17 | `bg-${color}-300`, 18 | `bg-${color}-400`, 19 | `bg-${color}-500`, 20 | `bg-${color}-600`, 21 | `bg-${color}-700`, 22 | `bg-${color}-800`, 23 | `bg-${color}-900`, 24 | 25 | `hover:bg-${color}-100`, 26 | `hover:bg-${color}-600`, 27 | 28 | `group-hover:bg-${color}-500`, 29 | 30 | `dark:bg-${color}-100`, 31 | `dark:bg-${color}-200`, 32 | `dark:bg-${color}-500`, 33 | `dark:bg-${color}-700`, 34 | `dark:bg-${color}-800`, 35 | `dark:bg-${color}-900`, 36 | 37 | `dark:hover:bg-${color}-900`, 38 | 39 | `peer-checked:bg-${color}-600`, 40 | 41 | `border-${color}-300`, 42 | 43 | `dark:border-${color}-800`, 44 | 45 | `text-${color}-200`, 46 | `text-${color}-400`, 47 | `text-${color}-500`, 48 | `text-${color}-800`, 49 | 50 | `dark:text-${color}-400`, 51 | `dark:text-${color}-800`, 52 | 53 | `accent-${color}-600`, 54 | 55 | `ring-${color}-800`, 56 | 57 | `peer-focus:ring-${color}-800`, 58 | ]) 59 | .flat() 60 | 61 | export default { 62 | darkMode: 'class', 63 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 64 | theme: { 65 | extend: {}, 66 | }, 67 | // eslint-disable-next-line no-undef 68 | plugins: [require('tailwindcss-animate')], 69 | safelist: safelist, 70 | } 71 | -------------------------------------------------------------------------------- /src/features/daw/common/components/loader/loader.tsx: -------------------------------------------------------------------------------- 1 | export const Loader = () => { 2 | return ( 3 | <div className="flex flex-row w-full h-full justify-center" role="status"> 4 | <svg 5 | aria-hidden="true" 6 | className="w-full h-full text-gray-400 fill-blue-400 animate-spin dark:text-gray-600 dark:fill-blue-400" 7 | viewBox="0 0 100 101" 8 | fill="none" 9 | xmlns="http://www.w3.org/2000/svg" 10 | > 11 | <path 12 | d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" 13 | fill="currentColor" 14 | /> 15 | <path 16 | d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" 17 | fill="currentFill" 18 | /> 19 | </svg> 20 | <span className="sr-only">Loading...</span> 21 | </div> 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/features/daw/playlist/store/types/index.ts: -------------------------------------------------------------------------------- 1 | import { Key } from '../../../../../model/note/key/key' 2 | import { TrackColor } from '../../../../../model/track/track-color' 3 | import { TrackDrumPattern } from '../../../../../model/track/drums/track-drums' 4 | 5 | export type AddKeyToCurrentTrackPayload = { 6 | key: Key 7 | startAtTick: number 8 | duration: number 9 | } 10 | 11 | export type AddKeyToCurrentBarPayload = { 12 | key: Key 13 | barId: string 14 | startAtRelativeTick: number 15 | duration: number 16 | } 17 | 18 | export type SetTrackVolumePayload = { 19 | trackId: string 20 | volume: number 21 | } 22 | 23 | export type MoveBarPayload = { 24 | fromTrackId: string 25 | toTrackId: string 26 | barId: string 27 | newStartAtTick: number 28 | } 29 | export type ResizeBarPayload = { 30 | trackId: string 31 | barId: string 32 | newDurationTicks: number 33 | } 34 | 35 | export type SetTrackColorPayload = { 36 | trackId: string 37 | color: TrackColor 38 | } 39 | 40 | export type RenameTrackPayload = { 41 | trackId: string 42 | newTitle: string 43 | } 44 | 45 | export type ResizeNotePayload = { 46 | trackId: string 47 | barId: string 48 | noteId: string 49 | newDurationTicks: number 50 | } 51 | 52 | export type MoveNotePayload = { 53 | trackId: string 54 | fromBarId: string 55 | noteId: string 56 | newStartAtTick: number 57 | newKey: Key 58 | } 59 | 60 | export type setCurrentTrackDrumsPatternPayload = { 61 | patternIndex: number 62 | pattern: TrackDrumPattern 63 | } 64 | 65 | export type TrackBarIdentifier = { 66 | trackId: string 67 | barId: string 68 | } 69 | -------------------------------------------------------------------------------- /src/features/daw/player-bar/store/playerBarSlice.ts: -------------------------------------------------------------------------------- 1 | import { PayloadAction, createSlice } from '@reduxjs/toolkit' 2 | import { DEFAULT_BPM, DEFAULT_VOLUME } from '../constants/player-bar-constants' 3 | 4 | export interface PlayerState { 5 | isPlaying: boolean 6 | bpm: number 7 | time: number 8 | metronomeActive: boolean 9 | volume: number 10 | } 11 | 12 | const initialState: PlayerState = { 13 | isPlaying: false, 14 | bpm: DEFAULT_BPM, 15 | time: 0, 16 | metronomeActive: false, 17 | volume: DEFAULT_VOLUME, 18 | } 19 | 20 | export const playerBarSlice = createSlice({ 21 | name: 'playerBar', 22 | initialState, 23 | reducers: { 24 | togglePlay: (state) => { 25 | state.isPlaying = !state.isPlaying 26 | }, 27 | stop: (state) => { 28 | state.isPlaying = false 29 | }, 30 | setBpm: (state, action: PayloadAction<number>) => { 31 | state.bpm = action.payload 32 | }, 33 | decreaseBpm: (state) => { 34 | state.bpm = state.bpm - 1 35 | }, 36 | increaseBpm: (state) => { 37 | state.bpm = state.bpm + 1 38 | }, 39 | setTime(state, action: PayloadAction<number>) { 40 | state.time = action.payload 41 | }, 42 | toggleMetronome: (state) => { 43 | state.metronomeActive = !state.metronomeActive 44 | }, 45 | setVolume: (state, action: PayloadAction<number>) => { 46 | state.volume = action.payload 47 | }, 48 | }, 49 | }) 50 | 51 | export const { 52 | togglePlay, 53 | setTime, 54 | stop, 55 | setBpm, 56 | increaseBpm, 57 | toggleMetronome, 58 | decreaseBpm, 59 | setVolume, 60 | } = playerBarSlice.actions 61 | 62 | export default playerBarSlice.reducer 63 | -------------------------------------------------------------------------------- /src/model/track/drums/track-drums-bar-factory.ts: -------------------------------------------------------------------------------- 1 | import { DrumMachinePatternUtil } from '../../../features/daw/drum-machine/util/drum-machine-pattern-util' 2 | import { Bar } from '../../bar/bar' 3 | import { DrumSound } from '../../drums/sound/drums-sound' 4 | import { Note } from '../../note/note' 5 | import { TrackDrumPatternSound } from './track-drums' 6 | import { v4 as uuidv4 } from 'uuid' 7 | 8 | export const createTrackDrumsBar = ( 9 | patternSounds: TrackDrumPatternSound[][], 10 | selectedSounds: DrumSound[], 11 | patternIndex: number, 12 | startAtTick: number = 0 13 | ): Bar => { 14 | return { 15 | id: uuidv4(), 16 | title: DrumMachinePatternUtil.getPatternNameByIndex(patternIndex), // TODO - include track name 17 | startAtTick, 18 | durationTicks: 16, 19 | notes: createTrackDrumsBarNotes(patternSounds, selectedSounds), 20 | } 21 | } 22 | 23 | const createTrackDrumsBarNotes = ( 24 | patternSounds: TrackDrumPatternSound[][], 25 | selectedSounds: DrumSound[] 26 | ): Note[] => { 27 | if (patternSounds.length === 0 || patternSounds.length === 0) return [] 28 | 29 | const notes = [] 30 | 31 | for (let soundIndex = 0; soundIndex < patternSounds.length; soundIndex++) { 32 | const patternSoundTicks = patternSounds[soundIndex] 33 | 34 | for (let tickIndex = 0; tickIndex < patternSoundTicks.length; tickIndex++) { 35 | if (patternSoundTicks[tickIndex] === 'on') { 36 | notes.push({ 37 | id: uuidv4(), 38 | startsAtRelativeTick: tickIndex, 39 | durationTicks: 1, 40 | key: selectedSounds[soundIndex].key, 41 | velocity: 100, 42 | }) 43 | } 44 | } 45 | } 46 | 47 | return notes 48 | } 49 | -------------------------------------------------------------------------------- /src/features/daw/instrument/instrument-setup.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux' 2 | import { selectSelectedBottomUpPanel } from '../bottom-bar/store/selectors' 3 | import { InstrumentHeader } from './header/instrument-header' 4 | import { InstrumentConfig } from './config/instrument-config' 5 | import { selectSelectedTrack } from '../playlist/store/selectors' 6 | import { selectSelectedOctave } from './store/selectors' 7 | import { InstrumentKeyboard } from './keyboard/instrument-keyboard' 8 | 9 | export const InstrumentSetup = () => { 10 | const selectedBottomUpPanel = useSelector(selectSelectedBottomUpPanel) 11 | const isSelected = selectedBottomUpPanel === 'instrument' 12 | 13 | const selectedTrack = useSelector(selectSelectedTrack) 14 | const selectedOctave = useSelector(selectSelectedOctave) 15 | 16 | if (!selectedTrack) return null 17 | 18 | return ( 19 | <div className={`${!isSelected && 'hidden'}`}> 20 | <div className="flex flex-col bg-stone-200 dark:bg-stone-900 h-96 divide-y divide-slate-600 border-t border-slate-600 mx-1 px-2"> 21 | <div className="flex h-[15%] w-full"> 22 | <InstrumentHeader 23 | selectedTrack={selectedTrack} 24 | selectedOctave={selectedOctave} 25 | /> 26 | </div> 27 | 28 | <div className="flex h-[15%] w-full"> 29 | <InstrumentConfig selectedTrack={selectedTrack} /> 30 | </div> 31 | 32 | <div className="flex w-full flex-grow overflow-auto bg-zinc-800"> 33 | <InstrumentKeyboard 34 | selectedTrack={selectedTrack} 35 | selectedOctave={selectedOctave} 36 | /> 37 | </div> 38 | </div> 39 | </div> 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/features/daw/player-bar/metronome/metronome.tsx: -------------------------------------------------------------------------------- 1 | import { FaPlus, FaMinus } from 'react-icons/fa' 2 | import { PiMetronomeBold } from 'react-icons/pi' 3 | import { useDispatch, useSelector } from 'react-redux' 4 | import { selectBpm, selectMetronomeActive } from '../store/selectors' 5 | import { 6 | decreaseBpm, 7 | increaseBpm, 8 | toggleMetronome, 9 | } from '../store/playerBarSlice' 10 | // import { MdKeyboardArrowDown } from 'react-icons/md' 11 | 12 | export const Metronome = () => { 13 | const bpm = useSelector(selectBpm) 14 | const metronomeActive = useSelector(selectMetronomeActive) 15 | const dispatch = useDispatch() 16 | 17 | return ( 18 | <div className="flex flex-row justify-start items-center gap-4"> 19 | <button 20 | className={metronomeActive ? 'text-blue-600 dark:text-blue-400' : ''} 21 | onClick={() => dispatch(toggleMetronome())} 22 | > 23 | <PiMetronomeBold /> 24 | </button> 25 | 26 | {/* <button> 27 | <MdKeyboardArrowDown /> 28 | </button> */} 29 | 30 | <div className="flex flex-row justify-center items-center gap-2"> 31 | <button 32 | onClick={() => { 33 | dispatch(decreaseBpm()) 34 | }} 35 | > 36 | <FaMinus /> 37 | </button> 38 | <div className="flex items-baseline gap-1 select-none"> 39 | <span className="font-bold text-lg">{bpm}</span> 40 | <span className="font-light text-xs">bpm</span> 41 | </div> 42 | 43 | <button 44 | onClick={() => { 45 | dispatch(increaseBpm()) 46 | }} 47 | > 48 | <FaPlus /> 49 | </button> 50 | </div> 51 | </div> 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /src/features/daw/midi-editor/midi-header/midi-header.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch } from 'react-redux' 2 | import { Track } from '../../../../model/track/track' 3 | import { closeAllBottomUpPanels } from '../../bottom-bar/store/bottom-bar-slice' 4 | import { Ruler } from '../../common/components/ruler/ruler' 5 | import { IoClose } from 'react-icons/io5' 6 | import { useMidiEditorHorizontalScroll } from '../hooks/useMidiEditorHorizontalScroll' 7 | import React from 'react' 8 | 9 | export type MidiHeaderProps = { 10 | selectedTrack: Track 11 | } 12 | 13 | export const MidiHeader = ({ selectedTrack }: MidiHeaderProps) => { 14 | const dispatch = useDispatch() 15 | const rulerRef = React.useRef<HTMLDivElement>(null) 16 | 17 | const handleRulerScroll = useMidiEditorHorizontalScroll(rulerRef) 18 | 19 | return ( 20 | <div className="flex h-full w-full flex-row justify-between divide-x divide-slate-600 select-none"> 21 | <div className="flex flex-row h-full justify-between divide-x divide-slate-600 max-w-72 min-w-72"> 22 | <div 23 | className="flex flex-grow cursor-pointer items-center justify-center" 24 | onClick={() => dispatch(closeAllBottomUpPanels())} 25 | > 26 | <IoClose /> 27 | </div> 28 | <div className="flex w-[85%] items-center justify-center"> 29 | <p className="font-semibold text-md">{selectedTrack.title}</p> 30 | </div> 31 | </div> 32 | 33 | {/* Just used to fill space & align with underlying keyboard */} 34 | <div className="w-20 min-w-20" /> 35 | 36 | <div 37 | onScroll={handleRulerScroll} 38 | ref={rulerRef} 39 | className="flex flex-grow overflow-x-scroll no-scrollbar pl-2" 40 | > 41 | <Ruler /> 42 | </div> 43 | </div> 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /src/features/daw/instrument/config/instrument-config.tsx: -------------------------------------------------------------------------------- 1 | import { Track } from '../../../../model/track/track' 2 | import { IoIosArrowBack, IoIosArrowForward } from 'react-icons/io' 3 | import { ScaleSelector } from '../../common/components/scale-selector/scale-selector' 4 | import { useDispatch, useSelector } from 'react-redux' 5 | import { 6 | selectScaleViewEnabled, 7 | selectSelectedScale, 8 | } from '../../midi-editor/store/selectors' 9 | import { 10 | selectScale, 11 | toggleScaleViewEnabled, 12 | } from '../../midi-editor/store/midi-editor-slice' 13 | import { Scale } from '../../../../model/scale/scale' 14 | 15 | export type InstrumentConfigProps = { 16 | selectedTrack: Track 17 | } 18 | 19 | export const InstrumentConfig = ({ selectedTrack }: InstrumentConfigProps) => { 20 | const scaleViewEnabled = useSelector(selectScaleViewEnabled) 21 | const selectedScale = useSelector(selectSelectedScale) 22 | const dispatch = useDispatch() 23 | 24 | return ( 25 | <div className="flex flex-row justify-between gap-4"> 26 | <div className="flex flex-row items-center gap-2"> 27 | <p>{selectedTrack.instrumentPreset.instrument} Preset</p> 28 | <div>{selectedTrack.instrumentPreset.name}</div> 29 | <div> 30 | <button> 31 | <IoIosArrowBack /> 32 | </button> 33 | <button> 34 | <IoIosArrowForward /> 35 | </button> 36 | </div> 37 | </div> 38 | 39 | <ScaleSelector 40 | scaleViewEnabled={scaleViewEnabled} 41 | selectedTrack={selectedTrack} 42 | selectedScale={selectedScale} 43 | onToggleScaleViewEnabled={() => dispatch(toggleScaleViewEnabled())} 44 | onSelectScale={(scale: Scale) => dispatch(selectScale(scale))} 45 | /> 46 | </div> 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/features/daw/menu/title/title.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux' 2 | import { selectProjectTitle } from '../store/selectors/menu-selectors' 3 | import { BsPencilFill } from 'react-icons/bs' 4 | import { useEffect, useState } from 'react' 5 | import { useDebounce } from '../../common/hooks/useDebounce' 6 | import { setProjectTitle } from '../store/menu-slice' 7 | 8 | export const Title = () => { 9 | const projectTitle = useSelector(selectProjectTitle) 10 | const dispatch = useDispatch() 11 | 12 | const [localProjectTitle, setLocalProjectTitle] = 13 | useState<string>(projectTitle) 14 | 15 | const debouncedLocalProjectTitle = useDebounce(localProjectTitle, 500) 16 | 17 | useEffect(() => { 18 | if (localProjectTitle !== projectTitle) { 19 | dispatch(setProjectTitle(localProjectTitle)) 20 | } 21 | }, [debouncedLocalProjectTitle, dispatch, localProjectTitle, projectTitle]) 22 | 23 | return ( 24 | <div className="flex flex-row h-12 max-h-12 px-2 py-2 gap-4"> 25 | <div className="relative flex items-center gap-2 w-80 justify-center rounded-3xl overflow-hidden "> 26 | <input 27 | type="text" 28 | className="w-full z-10 h-full peer bg-transparent font-semibold text-center border-none focus:outline-none " 29 | value={localProjectTitle} 30 | onChange={(e) => setLocalProjectTitle(e.target.value)} 31 | /> 32 | 33 | <div className="z-10 mr-2 transition-all opacity-0 peer-hover:opacity-100 peer-focus:opacity-0"> 34 | <BsPencilFill /> 35 | </div> 36 | 37 | <div className="absolute opacity-60 w-full h-full transition-colors peer-focus:bg-slate-300 peer-hover:bg-slate-300 dark:peer-focus:bg-slate-700 dark:peer-hover:bg-slate-700"></div> 38 | </div> 39 | </div> 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/model/scale/scale.ts: -------------------------------------------------------------------------------- 1 | import { Key, KeyUtils } from '../note/key/key' 2 | 3 | export const SCALE_TYPES = [ 4 | 'MAJOR', 5 | 'MINOR', 6 | 'PENTATONIC MINOR', 7 | 'PENTATONIC MAJOR', 8 | 'BLUES', 9 | ] as const 10 | 11 | export type ScaleType = (typeof SCALE_TYPES)[number] 12 | 13 | export const SCALE_KEYS = [ 14 | 'C', 15 | 'C#', 16 | 'D♭', 17 | 'D', 18 | 'D#', 19 | 'E♭', 20 | 'E', 21 | 'F', 22 | 'F#', 23 | 'G♭', 24 | 'G', 25 | 'G#', 26 | 'A♭', 27 | 'A', 28 | 'A#', 29 | 'B♭', 30 | 'B', 31 | ] as const 32 | 33 | export type ScaleKey = (typeof SCALE_KEYS)[number] 34 | 35 | export type Scale = { 36 | key: ScaleKey 37 | type: ScaleType 38 | } 39 | 40 | export class ScaleUtils { 41 | static getScaleKeys(scale: Scale): Key[] { 42 | const scaleKeys: Key[] = [] 43 | let currentKey: Key | null = KeyUtils.getFirstKeyMatchingScaleKey(scale.key) 44 | const scaleIntervals = ScaleUtils.getScaleIntervals(scale.type) 45 | let intervalIndex = 0 46 | 47 | do { 48 | if (!currentKey) break 49 | scaleKeys.push(currentKey) 50 | 51 | const interval = scaleIntervals[intervalIndex] 52 | intervalIndex = (intervalIndex + 1) % scaleIntervals.length 53 | currentKey = KeyUtils.applyIntervalToKey(currentKey, interval) 54 | } while (currentKey) 55 | 56 | return scaleKeys 57 | } 58 | 59 | static getScaleIntervals(scaleType: ScaleType): number[] { 60 | switch (scaleType) { 61 | case 'MAJOR': 62 | return [2, 2, 1, 2, 2, 2, 1] 63 | case 'MINOR': 64 | return [2, 1, 2, 2, 1, 2, 2] 65 | case 'PENTATONIC MINOR': 66 | return [3, 2, 2, 3, 2] 67 | case 'PENTATONIC MAJOR': 68 | return [2, 2, 3, 2, 3] 69 | case 'BLUES': 70 | return [3, 2, 1, 1, 3, 2] 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit' 2 | import playerBarReducer from '../features/daw/player-bar/store/playerBarSlice' 3 | import playlistSlice from '../features/daw/playlist/store/playlist-slice' 4 | import playlistHeaderSlice from '../features/daw/playlist-header/store/playlist-header-slice' 5 | import bottomBarSlice from '../features/daw/bottom-bar/store/bottom-bar-slice' 6 | import instrumentSlice from '../features/daw/instrument/store/instrument-slice' 7 | import midiEditorSlice from '../features/daw/midi-editor/store/midi-editor-slice' 8 | import drumMachineSlice from '../features/daw/drum-machine/store/drum-machine-slice' 9 | import menuSlice from '../features/daw/menu/store/menu-slice' 10 | import dialogSlice from '../features/daw/dialog/store/dialog-slice' 11 | 12 | export const store = configureStore({ 13 | reducer: { 14 | playerBar: playerBarReducer, 15 | playlist: playlistSlice, 16 | playlistHeader: playlistHeaderSlice, 17 | bottomBar: bottomBarSlice, 18 | instrument: instrumentSlice, 19 | midiEditor: midiEditorSlice, 20 | drumMachine: drumMachineSlice, 21 | menu: menuSlice, 22 | dialog: dialogSlice, 23 | }, 24 | middleware: (getDefaultMiddleware) => 25 | getDefaultMiddleware({ 26 | serializableCheck: { 27 | // Ignore warnings, see https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data 28 | ignoredActions: ['dialog/pushDialog'], 29 | ignoredPaths: ['dialog.queue'], 30 | }, 31 | }), 32 | }) 33 | 34 | export type RootStore = typeof store 35 | // Infer the `RootState` and `AppDispatch` types from the store itself 36 | export type RootState = ReturnType<typeof store.getState> 37 | // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} 38 | export type AppDispatch = typeof store.dispatch 39 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/pad/sound-selector/category/drum-machine-crash-icon.tsx: -------------------------------------------------------------------------------- 1 | export const DrumMachineCrashIcon = (props: React.SVGProps<SVGSVGElement>) => ( 2 | <svg 3 | xmlns="http://www.w3.org/2000/svg" 4 | className="fill-current" 5 | viewBox="0 0 300.000000 300.000000" 6 | {...props} 7 | > 8 | <g 9 | transform="translate(0.000000,300.000000) scale(0.100000,-0.100000)" 10 | fill="currentColor" 11 | stroke="none" 12 | > 13 | <path d="M2804 2975 c-179 -68 -477 -270 -798 -540 -65 -55 -128 -108 -140 -117 -21 -17 -21 -17 -1 -24 11 -4 40 -27 64 -51 33 -32 41 -46 33 -55 -5 -7 -15 -33 -22 -58 -23 -88 -119 -226 -293 -418 -262 -291 -563 -536 -702 -573 -22 -6 -46 -16 -53 -22 -10 -8 -26 3 -66 47 l-54 57 -99 -118 c-344 -412 -583 -784 -583 -909 0 -76 219 27 535 253 826 589 1925 1754 2227 2360 29 59 53 122 56 148 4 42 3 45 -19 45 -13 -1 -52 -12 -85 -25z" /> 14 | <path d="M770 2305 l-85 -85 48 -47 47 -48 85 85 85 85 -48 48 -48 47 -84 -85z" /> 15 | <path d="M922 2147 l-82 -83 48 -47 48 -47 84 85 84 85 -44 45 c-24 25 -47 45 -50 45 -3 0 -43 -37 -88 -83z" /> 16 | <path d="M1554 2163 c-89 -31 -216 -104 -288 -165 -36 -31 -45 -44 -41 -61 8 -30 -67 -106 -88 -89 -11 9 -19 6 -38 -16 -106 -117 -178 -287 -194 -462 -5 -56 -3 -80 7 -93 12 -17 17 -15 73 18 140 85 229 159 425 355 199 198 277 292 366 436 52 86 47 94 -62 100 -68 4 -90 1 -160 -23z" /> 17 | <path d="M1847 1232 l-47 -47 200 -200 200 -200 47 48 48 47 -200 200 -200 200 -48 -48z" /> 18 | <path d="M2385 812 c-221 -106 -151 -433 92 -432 71 0 127 24 170 73 41 47 56 96 51 172 -5 76 -37 132 -100 171 -57 36 -155 43 -213 16z m139 -123 c32 -15 60 -72 51 -106 -24 -97 -172 -97 -191 1 -6 37 14 88 43 105 24 14 66 14 97 0z" /> 19 | <path d="M2410 150 l0 -150 65 0 65 0 0 150 0 150 -65 0 -65 0 0 -150z" /> 20 | </g> 21 | </svg> 22 | ) 23 | export default DrumMachineCrashIcon 24 | -------------------------------------------------------------------------------- /src/features/daw/midi-editor/midi-body/left-menu/left-menu-transpose/left-menu-transpose.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux' 2 | import { selectSelectedNoteId } from '../../../store/selectors' 3 | import { transposeCurrentTrackNoteKey } from '../../../../playlist/store/playlist-slice' 4 | 5 | export const LeftMenuTranspose = () => { 6 | const selectedNoteId = useSelector(selectSelectedNoteId) 7 | const dispatch = useDispatch() 8 | 9 | const transposeItems = [ 10 | { 11 | id: 1, 12 | name: '+ 1', 13 | value: 1, 14 | }, 15 | { 16 | id: 2, 17 | name: '- 1', 18 | value: -1, 19 | }, 20 | { 21 | id: 3, 22 | name: '+ 12', 23 | value: 12, 24 | }, 25 | { 26 | id: 4, 27 | name: '- 12', 28 | value: -12, 29 | }, 30 | ] 31 | 32 | return ( 33 | <div className="flex flex-col gap-2"> 34 | <div className="flex flex-row"> 35 | <p className="text-xs font-bold">Transpose</p> 36 | </div> 37 | 38 | <div className="flex flex-row gap-4"> 39 | {transposeItems.map((item) => ( 40 | <button 41 | key={item.id} 42 | className={`${ 43 | selectedNoteId === null ? 'cursor-not-allowed' : 'cursor-pointer' 44 | } p-2 rounded-md font-bold text-sm`} 45 | onClick={() => { 46 | if (selectedNoteId === null) return 47 | 48 | dispatch( 49 | transposeCurrentTrackNoteKey({ 50 | noteId: selectedNoteId, 51 | keyOffset: item.value, 52 | }) 53 | ) 54 | }} 55 | disabled={selectedNoteId === null} 56 | > 57 | {item.name} 58 | </button> 59 | ))} 60 | </div> 61 | </div> 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/pad/grid/pattern/drum-machine-pad-pattern.tsx: -------------------------------------------------------------------------------- 1 | import { TrackDrumPatternSound } from '../../../../../../model/track/drums/track-drums' 2 | import { TRACK_COLORS } from '../../../../../../model/track/track-color' 3 | 4 | export type DrumMachinePadPatternProps = { 5 | patternSounds: TrackDrumPatternSound[] 6 | soundIndex: number 7 | activeTickBar: number | null 8 | onSoundChange: ( 9 | toChangeSoundIndex: number, 10 | toChangeBeatIndex: number, 11 | newValue: TrackDrumPatternSound 12 | ) => void 13 | } 14 | 15 | export const DrumMachinePadPattern = ({ 16 | patternSounds, 17 | soundIndex, 18 | onSoundChange, 19 | activeTickBar, 20 | }: DrumMachinePadPatternProps) => { 21 | const getItemBackgroundClasses = (indexCol: number) => { 22 | const hoverClasses = `hover:bg-${TRACK_COLORS[soundIndex]}-100 dark:hover:bg-${TRACK_COLORS[soundIndex]}-900` 23 | if (indexCol === activeTickBar) { 24 | return `bg-black dark:bg-white ${hoverClasses}` 25 | } 26 | return Math.floor(indexCol / 4) % 2 === 0 27 | ? `bg-zinc-300 dark:bg-zinc-600 ${hoverClasses}` 28 | : `bg-zinc-400 dark:bg-zinc-700 ${hoverClasses}` 29 | } 30 | 31 | return ( 32 | <div className="flex flex-row gap-1"> 33 | {patternSounds.map((sound, indexCol) => ( 34 | <div 35 | className={`cursor-pointer h-8 w-8 p-[2px] ${getItemBackgroundClasses( 36 | indexCol 37 | )}`} 38 | key={indexCol} 39 | onClick={() => 40 | onSoundChange(soundIndex, indexCol, sound === 'on' ? 'off' : 'on') 41 | } 42 | > 43 | {sound === 'on' && ( 44 | <div 45 | className={`w-full h-full bg-${TRACK_COLORS[soundIndex]}-500 rounded-full`} 46 | /> 47 | )} 48 | </div> 49 | ))} 50 | </div> 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/features/daw/playlist-header/store/playlist-header-slice.ts: -------------------------------------------------------------------------------- 1 | import { PayloadAction, createSlice } from '@reduxjs/toolkit' 2 | 3 | export interface PlaylistHeaderSlice { 4 | maxBars: number 5 | rulerScrollPosition: number 6 | currentTick: number 7 | requestedNewTickPosition: number | null 8 | } 9 | 10 | const DEFAULT_MAX_BARS = 40 11 | const TICKS_PER_MEASURE = 16 12 | 13 | const initialState: PlaylistHeaderSlice = { 14 | maxBars: DEFAULT_MAX_BARS, 15 | rulerScrollPosition: 0, 16 | currentTick: 0, 17 | requestedNewTickPosition: null, 18 | } 19 | 20 | export const playlistHeaderSlice = createSlice({ 21 | name: 'playlistHeader', 22 | initialState, 23 | reducers: { 24 | setRulerScrollPosition: (state, action: PayloadAction<number>) => { 25 | state.rulerScrollPosition = action.payload 26 | }, 27 | setCurrentTickFromSequencer: (state, action: PayloadAction<number>) => { 28 | state.currentTick = Math.floor(action.payload) 29 | state.requestedNewTickPosition = null 30 | }, 31 | requestNewTickPosition: (state, action: PayloadAction<number>) => { 32 | state.requestedNewTickPosition = action.payload 33 | }, 34 | requestForwardTickPosition: (state) => { 35 | const newTick = Math.min( 36 | state.currentTick + 1, 37 | state.maxBars * TICKS_PER_MEASURE 38 | ) 39 | state.requestedNewTickPosition = newTick 40 | }, 41 | requestBackwardTickPosition: (state) => { 42 | const newTick = Math.max(state.currentTick - 1, 0) 43 | state.requestedNewTickPosition = newTick 44 | }, 45 | }, 46 | }) 47 | 48 | export const { 49 | setRulerScrollPosition, 50 | setCurrentTickFromSequencer, 51 | requestNewTickPosition, 52 | requestForwardTickPosition, 53 | requestBackwardTickPosition, 54 | } = playlistHeaderSlice.actions 55 | 56 | export default playlistHeaderSlice.reducer 57 | -------------------------------------------------------------------------------- /src/features/daw/common/components/alert/alert.tsx: -------------------------------------------------------------------------------- 1 | export type AlertProps = { 2 | status: 'success' | 'error' | 'warning' | 'info' 3 | title: string 4 | message: string 5 | 6 | action?: { 7 | label: string 8 | onClick: () => void 9 | } 10 | } 11 | 12 | export const Alert = ({ status, message, title, action }: AlertProps) => { 13 | const statusToColor = { 14 | success: 'green', 15 | error: 'red', 16 | warning: 'yellow', 17 | info: 'blue', 18 | } 19 | const color = statusToColor[status] 20 | return ( 21 | <div 22 | className={`p-4 text-${color}-800 border border-${color}-300 rounded-lg bg-${color}-50 dark:bg-gray-800 dark:text-${color}-400 dark:border-${color}-800`} 23 | role="alert" 24 | > 25 | <div className="flex items-center"> 26 | <svg 27 | className="flex-shrink-0 w-4 h-4 me-2" 28 | aria-hidden="true" 29 | xmlns="http://www.w3.org/2000/svg" 30 | fill="currentColor" 31 | viewBox="0 0 20 20" 32 | > 33 | <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z" /> 34 | </svg> 35 | <span className="sr-only">Info</span> 36 | <h3 className="text-lg font-medium">{title}</h3> 37 | </div> 38 | <div className="mt-2 mb-4 text-sm">{message}</div> 39 | 40 | {action?.label && action?.onClick && ( 41 | <div className="w-full flex flex-row items-center justify-center"> 42 | <button 43 | onClick={action.onClick} 44 | className={`text-${color}-800 bg-${color}-200 border border-${color}-300 dark:bg-${color}-200 rounded-md px-2 py-1`} 45 | > 46 | {action.label} 47 | </button> 48 | </div> 49 | )} 50 | </div> 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/pad/grid/drum-machine-pad-grid.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux' 2 | import { TrackDrumPatternSound } from '../../../../../model/track/drums/track-drums' 3 | import { selectCurrentTick } from '../../../playlist-header/store/selectors' 4 | import { DrumMachinePadPattern } from './pattern/drum-machine-pad-pattern' 5 | import { DrumSound } from '../../../../../model/drums/sound/drums-sound' 6 | 7 | export type DrumMachinePadGridProps = { 8 | selectedSounds: DrumSound[] 9 | selectedPatternBeats: TrackDrumPatternSound[][] 10 | 11 | previewLoopPlayingTrackId: string | null 12 | 13 | onUpdateCurrentPattern: (newPattern: TrackDrumPatternSound[][]) => void 14 | } 15 | 16 | export const DrumMachinePadGrid = ({ 17 | selectedPatternBeats, 18 | onUpdateCurrentPattern, 19 | previewLoopPlayingTrackId, 20 | }: DrumMachinePadGridProps) => { 21 | const currentTick = useSelector(selectCurrentTick) 22 | 23 | const activeTickBar = previewLoopPlayingTrackId ? currentTick : null 24 | 25 | return ( 26 | <div className="flex flex-col gap-1"> 27 | {selectedPatternBeats.map((patternSounds, soundIndex) => ( 28 | <DrumMachinePadPattern 29 | key={soundIndex} 30 | patternSounds={patternSounds} 31 | activeTickBar={activeTickBar} 32 | soundIndex={soundIndex} 33 | onSoundChange={(toChangeSoundIndex, toChangeBeatIndex, newValue) => { 34 | const newPattern = selectedPatternBeats.map( 35 | (soundBeats, soundIndex) => 36 | soundIndex === toChangeSoundIndex 37 | ? soundBeats.map((sound, beatIndex) => 38 | beatIndex === toChangeBeatIndex ? newValue : sound 39 | ) 40 | : soundBeats 41 | ) 42 | onUpdateCurrentPattern(newPattern) 43 | }} 44 | /> 45 | ))} 46 | </div> 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/features/daw/instrument/header/instrument-header.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch } from 'react-redux' 2 | import { Track } from '../../../../model/track/track' 3 | import { closeAllBottomUpPanels } from '../../bottom-bar/store/bottom-bar-slice' 4 | import { 5 | selectNextOctave, 6 | selectPreviousOctave, 7 | } from '../store/instrument-slice' 8 | import { FaPlus, FaMinus } from 'react-icons/fa' 9 | import { IoClose } from 'react-icons/io5' 10 | import { OCTAVES, Octave } from '../../../../model/note/key/octave/octave' 11 | 12 | export type InstrumentHeaderProps = { 13 | selectedTrack: Track 14 | selectedOctave: Octave 15 | } 16 | 17 | export const InstrumentHeader = ({ 18 | selectedTrack, 19 | selectedOctave, 20 | }: InstrumentHeaderProps) => { 21 | const dispatch = useDispatch() 22 | return ( 23 | <div className="flex h-full w-full flex-row justify-between divide-x divide-slate-600"> 24 | <div className="flex flex-row h-full justify-between divide-x divide-slate-600 max-w-72 min-w-72"> 25 | <div 26 | className="flex flex-grow cursor-pointer items-center justify-center" 27 | onClick={() => dispatch(closeAllBottomUpPanels())} 28 | > 29 | <IoClose /> 30 | </div> 31 | <div className="flex w-[85%] items-center justify-center"> 32 | <p className="font-semibold text-md">{selectedTrack.title}</p> 33 | </div> 34 | </div> 35 | 36 | <div className="flex flex-grow items-center justify-end pl-2 gap-2"> 37 | <button 38 | disabled={selectedOctave === OCTAVES[0]} 39 | className="disabled:cursor-not-allowed" 40 | onClick={() => dispatch(selectPreviousOctave())} 41 | > 42 | <FaMinus /> 43 | </button> 44 | <span>{'Octave ' + selectedOctave}</span> 45 | <button 46 | disabled={selectedOctave === OCTAVES[OCTAVES.length - 1]} 47 | className="disabled:cursor-not-allowed" 48 | onClick={() => dispatch(selectNextOctave())} 49 | > 50 | <FaPlus /> 51 | </button> 52 | </div> 53 | </div> 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/features/daw/common/components/scale-selector/scale-selector.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | SCALE_KEYS, 3 | SCALE_TYPES, 4 | Scale, 5 | ScaleKey, 6 | ScaleType, 7 | } from '../../../../../model/scale/scale' 8 | import { Track } from '../../../../../model/track/track' 9 | import { Switch } from '../switch/switch' 10 | 11 | export type ScaleSelectorProps = { 12 | scaleViewEnabled: boolean 13 | selectedTrack?: Track | null 14 | selectedScale?: Scale | null 15 | 16 | onToggleScaleViewEnabled: () => void 17 | onSelectScale: (scale: Scale) => void 18 | } 19 | 20 | export const ScaleSelector = ({ 21 | scaleViewEnabled, 22 | selectedTrack, 23 | selectedScale, 24 | onToggleScaleViewEnabled, 25 | onSelectScale, 26 | }: ScaleSelectorProps) => { 27 | return ( 28 | <div className="flex flex-row items-center gap-2"> 29 | <Switch 30 | checked={scaleViewEnabled} 31 | onChange={onToggleScaleViewEnabled} 32 | mainColor={selectedTrack?.color} 33 | /> 34 | 35 | <div className="flex flex-row gap-1"> 36 | <select 37 | className="text-sm" 38 | value={selectedScale?.key} 39 | onChange={(e) => { 40 | onSelectScale({ 41 | key: e.currentTarget.value as ScaleKey, 42 | type: selectedScale?.type as ScaleType, 43 | }) 44 | }} 45 | > 46 | {SCALE_KEYS.map((scaleKey) => ( 47 | <option key={scaleKey} value={scaleKey}> 48 | {scaleKey} 49 | </option> 50 | ))} 51 | </select> 52 | <select 53 | className="text-sm" 54 | onChange={(e) => { 55 | onSelectScale({ 56 | key: selectedScale?.key as ScaleKey, 57 | type: e.currentTarget.value as ScaleType, 58 | }) 59 | }} 60 | value={selectedScale?.type} 61 | > 62 | {SCALE_TYPES.map((scaleType) => ( 63 | <option key={scaleType} value={scaleType}> 64 | {scaleType} 65 | </option> 66 | ))} 67 | </select> 68 | </div> 69 | </div> 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /src/features/daw/playlist/track-list/track-popup-menu/track-set-color-menu/track-set-color-menu.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { IoIosArrowForward } from 'react-icons/io' 3 | import { 4 | TRACK_COLORS, 5 | TrackColor, 6 | } from '../../../../../../model/track/track-color' 7 | 8 | export type TrackSetColorMenuProps = { 9 | currentColor: TrackColor 10 | onSetColor: (color: TrackColor) => void 11 | } 12 | 13 | const ColorGrid = (props: TrackSetColorMenuProps) => { 14 | return ( 15 | <div className="grid grid-cols-3 gap-8 p-2"> 16 | {TRACK_COLORS.map((item, index) => ( 17 | <div 18 | key={index} 19 | onClick={() => props.onSetColor(item)} 20 | className={`rounded-full cursor-pointer border-2 w-6 h-6 bg-${item}-500 ${ 21 | props.currentColor === item 22 | ? 'border-slate-200' 23 | : 'border-slate-600' 24 | }`} 25 | /> 26 | ))} 27 | </div> 28 | ) 29 | } 30 | 31 | export const TrackSetColorMenu = ({ 32 | currentColor, 33 | onSetColor, 34 | }: TrackSetColorMenuProps) => { 35 | const [showColorMenu, setShowColorMenu] = useState(false) 36 | return ( 37 | <div 38 | className="p-2 hover:bg-zinc-300 dark:hover:bg-zinc-600 select-none flex flex-row gap-2 items-center" 39 | onMouseEnter={() => setShowColorMenu(true)} 40 | onMouseLeave={() => setShowColorMenu(false)} 41 | > 42 | <span className={`rounded-full w-4 h-4 bg-${currentColor}-500`} /> 43 | <p className="font-bold text-sm">Set Color</p> 44 | <div className="flex-grow flex justify-end"> 45 | <div className="relative h-full"> 46 | <IoIosArrowForward /> 47 | 48 | <div 49 | hidden={!showColorMenu} 50 | className="fixed ml-4 -mt-4 z-50 h-fit w-fit" 51 | > 52 | <div className="w-fit flex flex-col bg-zinc-200 dark:bg-zinc-900 shadow-sm shadow-zinc-600 rounded-md overflow-hidden p-2"> 53 | <ColorGrid currentColor={currentColor} onSetColor={onSetColor} /> 54 | </div> 55 | </div> 56 | </div> 57 | </div> 58 | </div> 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/features/daw/menu/hamburger/import/dialog/step/file-dialog-step.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { useWizardContext } from '../../../../../common/components/wizard/use-wizard-context' 3 | 4 | export type FileChooserDialogStepProps = { 5 | handleFileImport: (file: File) => void 6 | } 7 | 8 | export const FileChooserDialogStep = ({ 9 | handleFileImport, 10 | }: FileChooserDialogStepProps) => { 11 | const [fileToImport, setFileToImport] = useState<File | null>(null) 12 | 13 | const { goToNextStep, setNextStepHandler } = useWizardContext() 14 | 15 | setNextStepHandler(() => { 16 | // Here we would execute the import action 17 | fileToImport && handleFileImport(fileToImport) 18 | }) 19 | 20 | return ( 21 | <div className="flex flex-col w-full h-full justify-start items-center mt-4 mx-2 gap-4"> 22 | <h2>Select file to import</h2> 23 | 24 | <div className="flex flex-col gap-2 justify-center items-center"> 25 | <input 26 | className="text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 dark:text-gray-400 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400" 27 | aria-describedby="file_input_help" 28 | id="file_input" 29 | accept=".json" 30 | type="file" 31 | onChange={(e) => { 32 | if (e?.target?.files && e.target.files.length > 0) { 33 | setFileToImport(e?.target?.files[0]) 34 | } 35 | }} 36 | /> 37 | <p 38 | className="text-sm text-gray-500 dark:text-gray-300" 39 | id="file_input_help" 40 | > 41 | JSON File previously exported. 42 | </p> 43 | </div> 44 | 45 | <div className="flex flex-row gap-4"> 46 | <button 47 | className="bg-green-500 hover:bg-green-700 dark:bg-green-800 dark:hover:bg-green-600 dark:hover:border-slate-400 text-white rounded-lg px-4 py-2 disabled:opacity-50 disabled:cursor-not-allowed" 48 | onClick={goToNextStep} 49 | disabled={!fileToImport} 50 | > 51 | Import 52 | </button> 53 | </div> 54 | </div> 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /src/features/daw/instrument/keyboard/instrument-keyboard.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux' 2 | import { Track } from '../../../../model/track/track' 3 | import { Keyboard } from '../../common/components/keyboard/keyboard' 4 | import React from 'react' 5 | import { selectPlayingTrackKeys } from '../store/selectors' 6 | import { Octave } from '../../../../model/note/key/octave/octave' 7 | import { KEYS } from '../../../../model/note/key/key' 8 | import { 9 | selectScaleViewEnabled, 10 | selectSelectedScale, 11 | } from '../../midi-editor/store/selectors' 12 | import { ScaleUtils } from '../../../../model/scale/scale' 13 | 14 | export type InstrumentKeyboardProps = { 15 | selectedTrack: Track 16 | selectedOctave: Octave 17 | } 18 | 19 | const SHOWED_KEYS_NUMBER = 32 20 | const SHOWED_WHITE_KEYS = 19 21 | 22 | const getShowedKeys = (selectedOctave: Octave) => { 23 | const startingKey = KEYS.find((k) => 24 | k.endsWith(Number(selectedOctave).toString()) 25 | ) 26 | if (!startingKey) return [] 27 | const startingKeyIndex = KEYS.indexOf(startingKey) 28 | return KEYS.slice(startingKeyIndex, startingKeyIndex + SHOWED_KEYS_NUMBER) 29 | } 30 | 31 | export const InstrumentKeyboard = (props: InstrumentKeyboardProps) => { 32 | const scaleViewEnabled = useSelector(selectScaleViewEnabled) 33 | const selectedScale = useSelector(selectSelectedScale) 34 | const showedKeys = getShowedKeys(props.selectedOctave) 35 | const playingTrackKeys = useSelector(selectPlayingTrackKeys) 36 | 37 | const currentTrackPlayingKeys = 38 | playingTrackKeys.find((item) => item.trackId === props.selectedTrack.id) 39 | ?.keys || [] 40 | 41 | const selectedScaleKeys = ScaleUtils.getScaleKeys(selectedScale) 42 | const [keySize, setKeySize] = React.useState(0) 43 | 44 | const handleResize = (keyboardSize: number) => { 45 | setKeySize(keyboardSize / SHOWED_WHITE_KEYS) 46 | } 47 | 48 | return ( 49 | <> 50 | <Keyboard 51 | selectedTrack={props.selectedTrack} 52 | showedKeys={showedKeys} 53 | playingKeys={currentTrackPlayingKeys} 54 | whiteKeySize={keySize} 55 | minWhiteKeySize={50} 56 | onResize={handleResize} 57 | highlightedKeys={scaleViewEnabled ? selectedScaleKeys : []} 58 | /> 59 | </> 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/pad/sound-selector/pad-sound-item/drum-machine-drum-sound-item.tsx: -------------------------------------------------------------------------------- 1 | import { RiArrowDropDownLine } from 'react-icons/ri' 2 | import { DrumMachineCategoryIcon } from '../category/drum-category-icon' 3 | import { SoundSelectorPopup } from './selector-popup/sound-selector-popup' 4 | import { TRACK_COLORS } from '../../../../../../model/track/track-color' 5 | import { useState } from 'react' 6 | import { DrumSound } from '../../../../../../model/drums/sound/drums-sound' 7 | import { Track } from '../../../../../../model/track/track' 8 | 9 | export type DrumMachinePadSoundItemProps = { 10 | selectedTrack: Track 11 | sound: DrumSound 12 | index: number 13 | 14 | onSelectedSound: (sound: DrumSound) => void 15 | } 16 | 17 | export const DrumMachinePadSoundItem = ({ 18 | selectedTrack, 19 | sound, 20 | index, 21 | onSelectedSound, 22 | }: DrumMachinePadSoundItemProps) => { 23 | const [showPopup, setShowPopup] = useState(false) 24 | return ( 25 | <div className="relative"> 26 | <div 27 | key={index} 28 | className="flex flex-row group items-center justify-start h-8 gap-2 select-none cursor-pointer bg-slate-100 hover:bg-slate-300 dark:bg-slate-900 dark:hover:bg-slate-700" 29 | onClick={() => { 30 | setShowPopup(!showPopup) 31 | }} 32 | > 33 | <div 34 | className={`w-[2px] h-full group-hover:bg-${TRACK_COLORS[index]}-500`} 35 | /> 36 | <div className={`h-full py-1 w-fit text-${TRACK_COLORS[index]}-500 `}> 37 | <DrumMachineCategoryIcon 38 | category={sound.category} 39 | className="w-6 h-full" 40 | /> 41 | </div> 42 | <div className="text-sm font-bold flex-grow">{sound.name}</div> 43 | 44 | <RiArrowDropDownLine /> 45 | </div> 46 | 47 | {showPopup && ( 48 | <div 49 | className="fixed z-30 h-fit w-fit" 50 | style={{ 51 | marginTop: -40 - index * 30, 52 | }} 53 | > 54 | <SoundSelectorPopup 55 | onClose={() => setShowPopup(false)} 56 | currentSoundIndex={index} 57 | selectedTrack={selectedTrack} 58 | onSelectedSound={onSelectedSound} 59 | /> 60 | </div> 61 | )} 62 | </div> 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/pad/sound-selector/category/drum-machine-snare-icon.tsx: -------------------------------------------------------------------------------- 1 | export const DrumMachineSnareIcon = (props: React.SVGProps<SVGSVGElement>) => ( 2 | <svg 3 | xmlns="http://www.w3.org/2000/svg" 4 | className="fill-current" 5 | viewBox="0 0 300.000000 300.000000" 6 | {...props} 7 | > 8 | <g 9 | transform="translate(0.000000,300.000000) scale(0.100000,-0.100000)" 10 | fill="currentColor" 11 | stroke="none" 12 | > 13 | <path d="M2306 2748 c-14 -13 -154 -265 -311 -560 -264 -495 -289 -539 -323 -555 -51 -26 -77 -65 -76 -115 1 -35 8 -48 38 -75 84 -77 210 -7 191 106 -6 35 12 69 304 580 171 299 311 550 311 558 0 20 -20 55 -39 70 -25 19 -68 15 -95 -9z" /> 14 | <path d="M272 2727 c-14 -14 -22 -36 -22 -58 -1 -37 -21 -16 654 -669 219 -212 243 -239 249 -274 8 -53 36 -88 84 -105 36 -13 43 -12 79 5 110 53 84 201 -38 221 -36 5 -76 43 -470 449 -237 244 -437 445 -445 448 -32 13 -70 5 -91 -17z" /> 15 | <path d="M267 1338 c-24 -19 -38 -64 -27 -92 22 -60 -59 -56 1262 -56 l1207 0 25 23 c36 32 39 84 6 117 -20 20 -33 20 -1238 20 -959 0 -1222 -3 -1235 -12z" /> 16 | <path d="M450 1088 c0 -18 222 -558 230 -558 7 0 230 541 230 558 0 9 -62 12 -230 12 -177 0 -230 -3 -230 -12z" /> 17 | <path d="M1104 1083 c4 -10 57 -141 118 -290 75 -183 115 -268 120 -259 14 24 228 549 228 558 0 4 -106 8 -236 8 -221 0 -236 -1 -230 -17z" /> 18 | <path d="M1760 1090 c0 -9 219 -549 227 -559 2 -2 5 -2 8 1 5 5 235 557 235 564 0 2 -106 4 -235 4 -145 0 -235 -4 -235 -10z" /> 19 | <path d="M2413 1088 c59 -151 201 -485 208 -493 6 -6 8 86 7 245 l-3 255 -109 3 c-85 2 -107 0 -103 -10z" /> 20 | <path d="M888 777 l-116 -282 230 -3 c127 -1 233 0 235 2 5 5 -214 545 -226 558 -4 4 -59 -119 -123 -275z" /> 21 | <path d="M1658 1049 c-3 -8 -54 -131 -113 -274 -59 -143 -110 -266 -112 -272 -4 -10 46 -13 232 -13 186 0 236 3 232 13 -31 77 -221 535 -227 546 -6 11 -8 11 -12 0z" /> 22 | <path d="M2200 775 c-62 -152 -111 -278 -108 -280 3 -3 108 -4 235 -3 l229 3 -114 278 c-63 152 -118 277 -122 277 -4 0 -58 -124 -120 -275z" /> 23 | <path d="M370 745 c0 -194 3 -247 13 -251 7 -2 56 -3 109 -2 l95 3 -101 247 c-55 136 -104 248 -108 248 -5 0 -8 -110 -8 -245z" /> 24 | <path d="M262 367 c-32 -34 -30 -89 4 -116 27 -21 28 -21 1234 -21 1206 0 1207 0 1234 21 35 27 36 86 3 117 l-23 22 -1215 0 -1216 0 -21 -23z" /> 25 | </g> 26 | </svg> 27 | ) 28 | export default DrumMachineSnareIcon 29 | -------------------------------------------------------------------------------- /src/features/daw/common/components/piano-roll-key/editable/editable-piano-roll-key.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch } from 'react-redux' 2 | 3 | import { PianoRollKeyProps } from '../types' 4 | import { useHorizontalResize } from '../../../hooks/useHorizontalResize' 5 | import { useDebounce } from '../../../hooks/useDebounce' 6 | import { TICK_WIDTH_PIXEL } from '../../../../playlist/constants' 7 | import { resizeNote } from '../../../../playlist/store/playlist-slice' 8 | import { useEffect } from 'react' 9 | import { useDragAndDrop } from '../../../hooks/useDragAndDrop' 10 | 11 | export const EditablePianoRollKey = ({ 12 | note, 13 | bar, 14 | track, 15 | beatWidth, 16 | cursorStyle, 17 | onNoteClick, 18 | }: PianoRollKeyProps) => { 19 | const { 20 | elementWidth: noteLengthPixel, 21 | setElementWidth, 22 | handleResizeMouseDown, 23 | } = useHorizontalResize(note.durationTicks * beatWidth) 24 | 25 | const dispatch = useDispatch() 26 | const debouncedNoteLengthPixel = useDebounce(noteLengthPixel, 500) 27 | 28 | const { handleDragStart } = useDragAndDrop({ 29 | type: 'drag_note', 30 | bar, 31 | track, 32 | note, 33 | }) 34 | 35 | useEffect(() => { 36 | const newDurationTicks = Math.floor(noteLengthPixel / TICK_WIDTH_PIXEL) 37 | if (newDurationTicks === note.durationTicks) return 38 | 39 | dispatch( 40 | resizeNote({ 41 | trackId: track.id, 42 | barId: bar.id, 43 | noteId: note.id, 44 | newDurationTicks, 45 | }) 46 | ) 47 | setElementWidth(newDurationTicks * TICK_WIDTH_PIXEL) 48 | // eslint-disable-next-line react-hooks/exhaustive-deps 49 | }, [debouncedNoteLengthPixel, dispatch, track.id]) 50 | 51 | return ( 52 | <div 53 | className="h-full" 54 | onDragStart={handleDragStart} 55 | draggable 56 | style={{ 57 | width: `${noteLengthPixel}px`, 58 | }} 59 | > 60 | <div className="relative w-full h-full"> 61 | <div 62 | className={`w-full h-full ${ 63 | cursorStyle === 'pointer' ? 'cursor-pointer' : 'cursor-default' 64 | }`} 65 | onClick={onNoteClick} 66 | /> 67 | <div 68 | className="absolute z-30 top-0 right-0 h-full w-2 cursor-ew-resize" 69 | onMouseDown={handleResizeMouseDown} 70 | /> 71 | </div> 72 | </div> 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /src/features/daw/menu/hamburger/import/dialog/useImportWizardData.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from 'react' 2 | import { FileChooserDialogStepProps } from './step/file-dialog-step' 3 | import { LoaderDialogStepProps } from './step/loader-dialog-step' 4 | import { AlertDialogStepProps } from './step/alert-dialog-step' 5 | import { ImportWizardProps } from './import-wizard' 6 | import { useImport } from '../../../hooks/import-export/useImport' 7 | 8 | export type ImportWizardData = { 9 | file: FileChooserDialogStepProps 10 | loader: LoaderDialogStepProps 11 | alert: AlertDialogStepProps 12 | } 13 | 14 | export const useImportWizardData = ({ hide }: ImportWizardProps) => { 15 | const [status, setStatus] = useState<'idle' | 'success' | 'error'>('idle') 16 | 17 | const [projectTitle, setProjectTitle] = useState<string>('') 18 | const [trackCount, setTrackCount] = useState<number>(0) 19 | 20 | const { executeImportProject, exportData, isLoading, isError, errorMessage } = 21 | useImport() 22 | 23 | useEffect(() => { 24 | if (isLoading) { 25 | return 26 | } 27 | 28 | if (isError) { 29 | setStatus('error') 30 | return 31 | } 32 | 33 | if (exportData) { 34 | setStatus('success') 35 | setProjectTitle(exportData.project_title) 36 | setTrackCount(exportData.tracks.length) 37 | } 38 | }, [exportData, isError, isLoading]) 39 | 40 | const alert: AlertDialogStepProps = useMemo(() => { 41 | if (status === 'idle') { 42 | // should not be rendered 43 | return { status: 'info', title: '', message: '' } 44 | } 45 | 46 | if (status === 'error') { 47 | return { 48 | status: 'error', 49 | title: 'Error', 50 | message: 'Error importing file: ' + errorMessage, 51 | action: { 52 | label: 'OK', 53 | onClick: hide, 54 | }, 55 | } 56 | } 57 | 58 | return { 59 | status: 'success', 60 | title: 'Success', 61 | message: `Project '${projectTitle}' imported successfully. It contains ${trackCount} tracks.`, 62 | action: { 63 | label: 'OK', 64 | onClick: hide, 65 | }, 66 | } 67 | }, [errorMessage, hide, projectTitle, status, trackCount]) 68 | 69 | return { 70 | file: { 71 | handleFileImport: executeImportProject, 72 | }, 73 | 74 | loader: { 75 | isLoading, 76 | }, 77 | 78 | alert, 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /.vscode/qwik.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Qwik component (simple)": { 3 | "scope": "javascriptreact,typescriptreact", 4 | "prefix": "qcomponent$", 5 | "description": "Simple Qwik component", 6 | "body": [ 7 | "export const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = component$(() => {", 8 | " return <${2:div}>$4</$2>", 9 | "});", 10 | ], 11 | }, 12 | "Qwik component (props)": { 13 | "scope": "typescriptreact", 14 | "prefix": "qcomponent$ + props", 15 | "description": "Qwik component w/ props", 16 | "body": [ 17 | "export interface ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}Props {", 18 | " $2", 19 | "}", 20 | "", 21 | "export const $1 = component$<$1Props>((props) => {", 22 | " const ${2:count} = useSignal(0);", 23 | " return (", 24 | " <${3:div} on${4:Click}$={(ev) => {$5}}>", 25 | " $6", 26 | " </${3}>", 27 | " );", 28 | "});", 29 | ], 30 | }, 31 | "Qwik signal": { 32 | "scope": "javascriptreact,typescriptreact", 33 | "prefix": "quseSignal", 34 | "description": "useSignal() declaration", 35 | "body": ["const ${1:foo} = useSignal($2);", "$0"], 36 | }, 37 | "Qwik store": { 38 | "scope": "javascriptreact,typescriptreact", 39 | "prefix": "quseStore", 40 | "description": "useStore() declaration", 41 | "body": ["const ${1:state} = useStore({", " $2", "});", "$0"], 42 | }, 43 | "$ hook": { 44 | "scope": "javascriptreact,typescriptreact", 45 | "prefix": "q$", 46 | "description": "$() function hook", 47 | "body": ["$(() => {", " $0", "});", ""], 48 | }, 49 | "useVisibleTask": { 50 | "scope": "javascriptreact,typescriptreact", 51 | "prefix": "quseVisibleTask", 52 | "description": "useVisibleTask$() function hook", 53 | "body": ["useVisibleTask$(({ track }) => {", " $0", "});", ""], 54 | }, 55 | "useTask": { 56 | "scope": "javascriptreact,typescriptreact", 57 | "prefix": "quseTask$", 58 | "description": "useTask$() function hook", 59 | "body": [ 60 | "useTask$(({ track }) => {", 61 | " track(() => $1);", 62 | " $0", 63 | "});", 64 | "", 65 | ], 66 | }, 67 | "useResource": { 68 | "scope": "javascriptreact,typescriptreact", 69 | "prefix": "quseResource$", 70 | "description": "useResource$() declaration", 71 | "body": [ 72 | "const $1 = useResource$(({ track, cleanup }) => {", 73 | " $0", 74 | "});", 75 | "", 76 | ], 77 | }, 78 | } 79 | -------------------------------------------------------------------------------- /src/features/daw/common/components/wizard/wizard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import WizardContext from './wizard-context' 3 | import { WizardProps } from './types' 4 | 5 | export const Wizard: React.FC<React.PropsWithChildren<WizardProps>> = ({ 6 | children, 7 | onStepChange, 8 | Wrapper, 9 | }) => { 10 | const [activeStep, setActiveStep] = React.useState(0) 11 | const [isLoading, setIsLoading] = React.useState(false) 12 | 13 | const hasNextStep = React.useRef(true) 14 | const hasPrevStep = React.useRef(false) 15 | const nextStepHandler = React.useRef<(() => void) | null>(() => {}) 16 | const stepCount = React.Children.count(children) 17 | 18 | hasPrevStep.current = activeStep > 0 19 | hasNextStep.current = activeStep < stepCount - 1 20 | 21 | const goToNextStep = React.useCallback(() => { 22 | if (hasNextStep.current) { 23 | if (nextStepHandler.current) { 24 | setIsLoading(true) 25 | nextStepHandler.current() 26 | setIsLoading(false) 27 | 28 | nextStepHandler.current = null 29 | } 30 | 31 | const nextStep = activeStep + 1 32 | setActiveStep(nextStep) 33 | onStepChange?.(nextStep) 34 | } 35 | }, [activeStep, onStepChange]) 36 | 37 | const goToPrevStep = React.useCallback(() => { 38 | if (hasPrevStep.current) { 39 | const prevStep = activeStep - 1 40 | setActiveStep(prevStep) 41 | onStepChange?.(prevStep) 42 | } 43 | }, [activeStep, onStepChange]) 44 | 45 | const setNextStepHandler = React.useCallback((handler: () => void) => { 46 | nextStepHandler.current = handler 47 | }, []) 48 | 49 | const value = React.useMemo( 50 | () => ({ 51 | goToNextStep, 52 | goToPrevStep, 53 | setNextStepHandler, 54 | 55 | isLoading, 56 | activeStep, 57 | }), 58 | [activeStep, goToNextStep, goToPrevStep, isLoading, setNextStepHandler] 59 | ) 60 | 61 | const activeStepContent = React.useMemo(() => { 62 | const reactChildren = React.Children.toArray(children) 63 | return reactChildren[activeStep] 64 | }, [activeStep, children]) 65 | 66 | const wrappedActiveStepContent = React.useMemo( 67 | () => 68 | Wrapper 69 | ? React.cloneElement(Wrapper, { children: activeStepContent }) 70 | : activeStepContent, 71 | [Wrapper, activeStepContent] 72 | ) 73 | 74 | return ( 75 | <WizardContext.Provider value={value}> 76 | {wrappedActiveStepContent} 77 | </WizardContext.Provider> 78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /src/features/daw/midi-editor/midi-body/left-menu/left-menu-velocity/left-menu-velocity.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux' 2 | import { selectSelectedNoteId } from '../../../store/selectors' 3 | import { selectSelectedTrack } from '../../../../playlist/store/selectors' 4 | import { useEffect, useState } from 'react' 5 | import { useDebounce } from '../../../../common/hooks/useDebounce' 6 | import { Bar } from '../../../../../../model/bar/bar' 7 | import { setCurrentTrackNoteVelocity } from '../../../../playlist/store/playlist-slice' 8 | 9 | export const LeftMenuVelocity = () => { 10 | const selectedNoteId = useSelector(selectSelectedNoteId) 11 | const selectedTrack = useSelector(selectSelectedTrack) 12 | const dispatch = useDispatch() 13 | 14 | const [noteVelocity, setNoteVelocity] = useState(100) 15 | 16 | const debouncedNoteVelocity = useDebounce(noteVelocity, 100) 17 | 18 | useEffect(() => { 19 | if (selectedNoteId === null) return 20 | dispatch( 21 | setCurrentTrackNoteVelocity({ 22 | noteId: selectedNoteId, 23 | velocity: noteVelocity, 24 | }) 25 | ) 26 | // eslint-disable-next-line react-hooks/exhaustive-deps 27 | }, [debouncedNoteVelocity, dispatch]) 28 | 29 | useEffect(() => { 30 | if (selectedNoteId === null) { 31 | setNoteVelocity(100) 32 | return 33 | } 34 | const bar = selectedTrack?.bars.find((bar: Bar) => 35 | bar.notes.find((note) => note.id === selectedNoteId) 36 | ) 37 | 38 | const note = bar?.notes.find((n) => n.id === selectedNoteId) 39 | setNoteVelocity(note ? note.velocity : 100) 40 | // eslint-disable-next-line react-hooks/exhaustive-deps 41 | }, [selectedNoteId]) 42 | 43 | return ( 44 | <div className="flex flex-col gap-2"> 45 | <div className="flex flex-col gap-2"> 46 | <div className="flex flex-row gap-2 items-center justify-start"> 47 | <p className="font-light text-sm">Velocity</p> 48 | <p className="font-bold">{noteVelocity}</p> 49 | </div> 50 | 51 | <input 52 | disabled={selectedNoteId === null} 53 | id="minmax-range" 54 | type="range" 55 | min="0" 56 | max="100" 57 | value={noteVelocity} 58 | onChange={(e) => { 59 | setNoteVelocity(parseInt(e.target.value)) 60 | }} 61 | className={`h-1 w-full cursor-ew-resize rounded-lg accent-${selectedTrack?.color}-600`} 62 | ></input> 63 | </div> 64 | </div> 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/pad/sound-selector/category/drum-machine-clap-icon.tsx: -------------------------------------------------------------------------------- 1 | export const DrumMachineClapIcon = (props: React.SVGProps<SVGSVGElement>) => ( 2 | <svg 3 | xmlns="http://www.w3.org/2000/svg" 4 | className="fill-current" 5 | viewBox="0 0 300 300" 6 | {...props} 7 | > 8 | <g 9 | transform="translate(0.000000,300.000000) scale(0.100000,-0.100000)" 10 | fill="currentColor" 11 | stroke="none" 12 | > 13 | <path d="M2488 2973 c-9 -10 -33 -65 -54 -122 -35 -96 -37 -105 -23 -130 15 -27 61 -39 87 -23 18 12 92 199 92 232 0 52 -69 81 -102 43z" /> 14 | <path d="M1470 2848 c-27 -7 -112 -87 -362 -336 -181 -179 -328 -329 -328 -333 0 -4 13 -21 29 -38 47 -51 71 -113 71 -186 0 -36 4 -65 9 -65 14 0 732 734 749 765 9 19 13 45 9 78 -4 42 -10 56 -41 84 -19 18 -44 33 -55 33 -11 0 -25 2 -33 4 -7 2 -29 0 -48 -6z" /> 15 | <path d="M2712 2704 c-105 -74 -124 -100 -99 -138 21 -31 50 -40 78 -25 12 6 60 38 106 71 76 54 83 62 83 93 0 35 -28 65 -60 65 -8 0 -57 -30 -108 -66z" /> 16 | <path d="M1895 2691 c-22 -9 -121 -99 -114 -104 2 -2 20 -10 39 -17 51 -21 110 -78 135 -132 l22 -47 46 47 c70 72 86 136 47 199 -33 54 -113 79 -175 54z" /> 17 | <path d="M364 2476 c-34 -15 -72 -58 -79 -90 -3 -12 -36 -244 -73 -517 l-68 -495 -72 -82 c-44 -50 -72 -91 -72 -105 0 -15 29 -51 88 -110 l89 -89 25 30 c18 22 27 47 32 94 4 34 16 124 26 198 11 74 35 257 55 405 20 149 43 293 51 321 19 68 78 135 145 163 l52 23 -7 85 c-8 104 -28 144 -86 167 -48 19 -65 19 -106 2z" /> 18 | <path d="M1684 2460 c-18 -4 -45 -16 -60 -28 -16 -11 -195 -187 -399 -391 -388 -389 -392 -392 -441 -357 -17 13 -20 31 -24 163 -5 119 -9 154 -24 179 -54 91 -185 90 -237 -2 -12 -20 -39 -190 -85 -532 l-68 -503 -73 -86 c-40 -48 -73 -96 -73 -107 0 -13 130 -151 382 -403 303 -303 388 -383 408 -383 16 0 61 27 124 75 54 41 105 75 112 75 29 0 143 44 237 92 222 114 432 286 811 666 l259 260 4 49 c11 113 -86 184 -190 139 -18 -8 -104 -87 -192 -176 -177 -178 -201 -196 -239 -179 -34 16 -51 60 -36 91 7 12 133 145 280 293 177 178 271 280 276 298 19 79 -26 160 -97 176 -76 16 -84 11 -383 -288 -218 -217 -288 -281 -307 -281 -36 0 -69 35 -69 73 0 28 37 68 344 377 189 190 349 355 355 367 29 58 0 147 -58 178 -39 20 -110 19 -139 -1 -13 -9 -176 -169 -363 -355 -293 -293 -344 -339 -369 -339 -17 0 -39 9 -50 20 -46 46 -39 55 259 354 185 186 282 291 287 309 26 107 -58 199 -162 177z" /> 19 | <path d="M2699 2389 c-15 -15 -19 -28 -14 -47 11 -45 30 -52 151 -52 97 0 115 3 138 21 30 24 34 59 8 82 -15 14 -41 17 -140 17 -110 0 -124 -2 -143 -21z" /> 20 | </g> 21 | </svg> 22 | ) 23 | export default DrumMachineClapIcon 24 | -------------------------------------------------------------------------------- /src/sequencer/time/clock/clock.ts: -------------------------------------------------------------------------------- 1 | import * as Tone from 'tone' 2 | import { RootStore } from '../../../store' 3 | import { TimeUtils } from '../utils/time-utils' 4 | import { setCurrentTickFromSequencer } from '../../../features/daw/playlist-header/store/playlist-header-slice' 5 | import { setTime } from '../../../features/daw/player-bar/store/playerBarSlice' 6 | import { observeStore } from '../../../store/observers' 7 | import { selectRequestedNewTickPosition } from '../../../features/daw/playlist-header/store/selectors' 8 | import { selectBpm } from '../../../features/daw/player-bar/store/selectors' 9 | 10 | export class Clock { 11 | currentTick: number 12 | 13 | private _bpm: number 14 | private _time: number 15 | private _store: RootStore 16 | 17 | constructor(store: RootStore) { 18 | this._store = store 19 | this._bpm = store.getState().playerBar.bpm 20 | this._time = 0 21 | this.currentTick = 0 22 | 23 | Tone.Transport.bpm.value = this._bpm 24 | 25 | Tone.Transport.scheduleRepeat(() => { 26 | this.handleTick() 27 | }, '16n') 28 | 29 | this.registerStoreListeners() 30 | } 31 | 32 | requestNewTickPosition(newTick: number | null) { 33 | if (newTick === null) return 34 | 35 | Tone.Transport.position = TimeUtils.tickToToneTime(newTick) 36 | this.getTickAndTimeFromToneTransport() 37 | 38 | if (Tone.Transport.state !== 'started') { 39 | // will trigger store update ONLY if transport is not playing so to not collide with the handleTick method 40 | this.notifyStore() 41 | } 42 | } 43 | 44 | private registerStoreListeners() { 45 | observeStore( 46 | this._store, 47 | selectRequestedNewTickPosition, 48 | this.requestNewTickPosition.bind(this) 49 | ) 50 | 51 | observeStore(this._store, selectBpm, this.handleRequestedNewBpm.bind(this)) 52 | } 53 | 54 | private handleRequestedNewBpm(newBpm: number) { 55 | this._bpm = newBpm 56 | Tone.Transport.bpm.value = this._bpm 57 | } 58 | 59 | private handleTick() { 60 | this.getTickAndTimeFromToneTransport() 61 | this.notifyStore() 62 | } 63 | 64 | private getTickAndTimeFromToneTransport() { 65 | this.currentTick = TimeUtils.toneTimeToTicks(Tone.Transport.position) 66 | // sometimes the transport position is negative, so we need to clamp it to 0 67 | this._time = Math.max(Tone.Transport.seconds, 0) 68 | } 69 | 70 | private notifyStore() { 71 | this._store.dispatch(setTime(this._time)) 72 | this._store.dispatch(setCurrentTickFromSequencer(this.currentTick)) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/features/daw/midi-editor/midi-body/left-menu/left-menu-header/left-menu-header.tsx: -------------------------------------------------------------------------------- 1 | import { BsFillCursorFill } from 'react-icons/bs' 2 | import { FaHeadphones, FaTrash } from 'react-icons/fa' 3 | import { RiPencilFill } from 'react-icons/ri' 4 | import { useDispatch, useSelector } from 'react-redux' 5 | import { 6 | selectEditorMode, 7 | selectNotePreviewEnabled, 8 | } from '../../../store/selectors' 9 | import { 10 | selectNote, 11 | setEditorMode, 12 | toggleNotePreviewEnabled, 13 | } from '../../../store/midi-editor-slice' 14 | import { EditorMode } from '../../../store/types/types' 15 | import { selectSelectedTrack } from '../../../../playlist/store/selectors' 16 | 17 | export const LeftMenuHeader = () => { 18 | const editorMode = useSelector(selectEditorMode) 19 | const notePreviewEnabled = useSelector(selectNotePreviewEnabled) 20 | const selectedTrack = useSelector(selectSelectedTrack) 21 | const dispatch = useDispatch() 22 | 23 | const modes: { 24 | id: EditorMode 25 | icon: JSX.Element 26 | }[] = [ 27 | { 28 | id: 'select', 29 | icon: <BsFillCursorFill size={16} />, 30 | }, 31 | { 32 | id: 'draw', 33 | icon: <RiPencilFill size={16} />, 34 | }, 35 | { 36 | id: 'delete', 37 | icon: <FaTrash size={16} />, 38 | }, 39 | ] 40 | 41 | return ( 42 | <div className="flex flex-row justify-between items-center"> 43 | <div className="flex flex-row gap-1"> 44 | {modes.map((mode) => ( 45 | <button 46 | key={mode.id} 47 | className={`flex items-center justify-center text-xs ${ 48 | editorMode === mode.id 49 | ? 'dark:bg-white dark:text-zinc-800 bg-zinc-800 text-white' 50 | : 'dark:bg-zinc-800 dark:text-white bg-white zinc-800' 51 | }`} 52 | onClick={() => { 53 | dispatch(selectNote(null)) 54 | dispatch(setEditorMode(mode.id)) 55 | }} 56 | > 57 | {mode.icon} 58 | </button> 59 | ))} 60 | </div> 61 | 62 | <div> 63 | <button 64 | className={`rounded-full ${ 65 | notePreviewEnabled 66 | ? `bg-${selectedTrack?.color}-900 text-${selectedTrack?.color}-200 dark:bg-${selectedTrack?.color}-100 dark:text-${selectedTrack?.color}-800` 67 | : '' 68 | }`} 69 | onClick={() => { 70 | dispatch(toggleNotePreviewEnabled()) 71 | }} 72 | > 73 | <FaHeadphones /> 74 | </button> 75 | </div> 76 | </div> 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /src/features/daw/bottom-bar/bottom-bar.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux' 2 | import { selectSelectedBottomUpPanel } from './store/selectors' 3 | import { selectSelectedTrack } from '../playlist/store/selectors' 4 | import { BottomUpPanel } from './types/bottom-up-panel' 5 | import { BottomBarItem } from './bottom-bar-item/bottom-bar-item' 6 | import { useEffect } from 'react' 7 | import { selectBottomUpPanel } from './store/bottom-bar-slice' 8 | import { Track } from '../../../model/track/track' 9 | 10 | const bottomUpPanelItems: { label: string; bottomUpPanel: BottomUpPanel }[] = [ 11 | { 12 | label: 'Instrument', 13 | bottomUpPanel: 'instrument', 14 | }, 15 | { 16 | label: 'MIDI Editor', 17 | bottomUpPanel: 'midiEditor', 18 | }, 19 | { 20 | label: 'Drum Machine', 21 | bottomUpPanel: 'drumMachine', 22 | }, 23 | ] 24 | 25 | const getAllowedBottomUpPanelForTrack = (selectedTrack?: Track) => { 26 | const selectedTrackType = selectedTrack?.instrumentPreset.instrument 27 | if (selectedTrackType && selectedTrackType !== 'DRUMS') { 28 | return ['instrument', 'midiEditor'] 29 | } 30 | 31 | if (selectedTrackType && selectedTrackType === 'DRUMS') { 32 | return ['drumMachine'] 33 | } 34 | 35 | return [] 36 | } 37 | 38 | export const BottomBar = () => { 39 | const selectedBottomUpPanel = useSelector(selectSelectedBottomUpPanel) 40 | const dipatch = useDispatch() 41 | const selectedTrack = useSelector(selectSelectedTrack) 42 | const items = getAllowedBottomUpPanelForTrack(selectedTrack) 43 | .map((panel) => { 44 | return bottomUpPanelItems.find((item) => item.bottomUpPanel === panel) 45 | }) 46 | .filter(Boolean) as { label: string; bottomUpPanel: BottomUpPanel }[] 47 | 48 | useEffect(() => { 49 | const allowedBottomUpPanels = getAllowedBottomUpPanelForTrack(selectedTrack) 50 | if ( 51 | selectedBottomUpPanel && 52 | !allowedBottomUpPanels.includes(selectedBottomUpPanel) 53 | ) { 54 | if (allowedBottomUpPanels.length === 0) { 55 | dipatch(selectBottomUpPanel(null)) 56 | } else { 57 | dipatch(selectBottomUpPanel(allowedBottomUpPanels[0] as BottomUpPanel)) 58 | } 59 | } 60 | }, [dipatch, selectedBottomUpPanel, selectedTrack]) 61 | 62 | return ( 63 | <div className="flex flex-row py-1 px-2 gap-2"> 64 | {items.map((item) => ( 65 | <BottomBarItem 66 | key={item.label} 67 | label={item.label} 68 | bottomUpPanel={item.bottomUpPanel} 69 | selectedTrack={selectedTrack} 70 | selectedBottomUpPanel={selectedBottomUpPanel} 71 | /> 72 | ))} 73 | </div> 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /src/features/daw/menu/hooks/import-export/useImport.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | import { ExportData } from './types' 3 | import { useDispatch, useSelector } from 'react-redux' 4 | import { setProjectTitle } from '../../store/menu-slice' 5 | import { selectTracks } from '../../../playlist/store/selectors' 6 | import { addTrack, deleteTrack } from '../../../playlist/store/playlist-slice' 7 | import { readJSONFile } from '../../hamburger/import/utils/read-json-file' 8 | 9 | const importVersion1 = (data: ExportData) => { 10 | return data 11 | } 12 | 13 | const importByVersion = { 14 | 'liberty_beats/1.0.0': importVersion1, 15 | } 16 | 17 | const parseData = (json: unknown) => { 18 | if (!json) { 19 | throw new Error('Invalid project file') 20 | } 21 | 22 | const data = json as ExportData 23 | const allowedVersions = Object.keys(importByVersion) 24 | if (!allowedVersions.includes(data.version)) { 25 | throw new Error( 26 | `Invalid version of the project file, it should be one of ${allowedVersions 27 | .map((v) => `"${v}"`) 28 | .join(',')}` 29 | ) 30 | } 31 | 32 | const dataVersion = data.version as keyof typeof importByVersion 33 | 34 | return importByVersion[dataVersion](data) 35 | } 36 | 37 | export const useImport = () => { 38 | const dispatch = useDispatch() 39 | 40 | const tracks = useSelector(selectTracks) 41 | const [isLoading, setIsLoading] = useState(false) 42 | const [isError, setIsError] = useState(false) 43 | const [errorMessage, setErrorMessage] = useState<string>('') 44 | const [exportData, setExportData] = useState<ExportData | null>(null) 45 | 46 | const executeImportProject = useCallback( 47 | async (file: File) => { 48 | setIsLoading(true) 49 | 50 | try { 51 | const fileContent = await readJSONFile(file) 52 | const data = parseData(fileContent) 53 | setExportData(data) 54 | 55 | dispatch(setProjectTitle(data.project_title)) 56 | 57 | tracks.forEach((track) => { 58 | // remove all tracks 59 | dispatch(deleteTrack(track.id)) 60 | }) 61 | 62 | data.tracks.forEach((track) => { 63 | // add imported tracks 64 | dispatch(addTrack(track)) 65 | }) 66 | } catch (error) { 67 | setIsError(true) 68 | if (error instanceof Error) { 69 | setErrorMessage(error.message) 70 | } 71 | } finally { 72 | setIsLoading(false) 73 | } 74 | }, 75 | [dispatch, tracks] 76 | ) 77 | 78 | return { 79 | isLoading, 80 | executeImportProject, 81 | exportData, 82 | isError, 83 | errorMessage, 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/pad/sound-selector/category/drum-machine-open-hh-icon.tsx: -------------------------------------------------------------------------------- 1 | export const DrumMachineOpenHiHatIcon = ( 2 | props: React.SVGProps<SVGSVGElement> 3 | ) => ( 4 | <svg 5 | xmlns="http://www.w3.org/2000/svg" 6 | className="fill-current" 7 | viewBox="0 0 300.000000 300.000000" 8 | {...props} 9 | > 10 | <g 11 | transform="translate(0.000000,300.000000) scale(0.050000,-0.050000)" 12 | fill="currentColor" 13 | stroke="none" 14 | > 15 | <path d="M2900 5740 c0 -220 -5 -260 -31 -260 -39 0 -234 -98 -254 -128 -8 -12 -15 -226 -15 -475 l0 -453 -126 -70 c-109 -60 -152 -71 -315 -83 -1513 -113 -2366 -485 -2110 -921 94 -161 621 -430 841 -430 74 0 17 -34 -125 -73 -833 -233 -1013 -641 -415 -942 353 -178 1146 -338 1850 -375 127 -6 268 -16 315 -22 l85 -10 0 -677 0 -677 62 -52 c141 -120 535 -120 676 0 l62 52 0 677 0 677 85 9 c47 6 235 20 419 32 717 48 1398 191 1746 366 598 301 418 709 -415 942 -142 39 -199 73 -125 73 220 0 747 269 841 430 256 436 -597 808 -2110 921 -276 20 -441 110 -441 241 l0 88 300 0 300 0 0 -100 0 -100 100 0 100 0 0 100 0 100 67 0 c115 0 133 41 133 305 0 262 -15 295 -131 295 l-69 0 0 100 0 100 -100 0 -100 0 0 -100 0 -100 -300 0 -300 0 0 65 c-1 92 -154 215 -269 215 -26 0 -31 40 -31 260 l0 260 -100 0 -100 0 0 -260z m231 -469 l69 -29 0 -458 0 -459 -62 -12 c-35 -7 -97 -13 -138 -13 -41 0 -103 6 -137 13 l-63 12 0 458 0 459 65 28 c84 37 179 37 266 1z m469 -371 l0 -100 -100 0 -100 0 0 100 0 100 100 0 100 0 0 -100z m600 0 l0 -100 -200 0 -200 0 0 100 0 100 200 0 200 0 0 -100z m-1486 -745 c116 -68 459 -66 581 3 l84 49 72 -39 c107 -57 275 -213 320 -297 l39 -74 -47 -38 c-204 -167 -931 -208 -1353 -77 -244 75 -260 145 -73 324 191 183 268 213 377 149z m-614 -100 c0 -3 -23 -49 -50 -103 -168 -332 199 -541 950 -541 750 0 1118 209 951 540 -64 126 -79 122 281 81 1046 -119 1734 -401 1513 -621 -553 -554 -3683 -692 -5063 -223 -625 212 -626 412 -2 624 344 117 1421 301 1420 243z m-353 -1284 c193 -24 644 -59 923 -73 22 -1 -41 -19 -140 -40 -933 -197 -587 -747 470 -747 1057 0 1403 550 470 747 -99 21 -157 39 -130 40 249 12 698 48 913 73 l263 31 297 -60 c681 -138 1062 -333 964 -495 -347 -572 -3681 -740 -5105 -256 -615 209 -605 426 28 629 463 149 737 188 1047 151z m1353 -371 l0 -300 -100 0 -100 0 0 300 0 300 100 0 100 0 0 -300z m-400 -102 l0 -182 -85 15 c-230 38 -439 134 -407 186 24 38 125 83 278 123 227 60 214 68 214 -142z m893 119 c68 -20 144 -55 170 -77 l47 -40 -47 -40 c-50 -42 -236 -106 -378 -129 l-85 -15 0 184 0 184 85 -15 c47 -7 140 -31 208 -52z m-393 -1154 l0 -238 -62 -12 c-80 -16 -196 -16 -275 0 l-63 12 0 238 0 237 200 0 200 0 0 -237z m0 -545 c0 -103 -26 -118 -200 -118 -174 0 -200 15 -200 118 l0 92 200 0 200 0 0 -92z m0 -400 c0 -103 -26 -118 -200 -118 -174 0 -200 15 -200 118 l0 92 200 0 200 0 0 -92z" /> 16 | </g> 17 | </svg> 18 | ) 19 | export default DrumMachineOpenHiHatIcon 20 | -------------------------------------------------------------------------------- /src/features/daw/playlist/track-list/track-popup-menu/track-popup-menu.tsx: -------------------------------------------------------------------------------- 1 | import { BiRename } from 'react-icons/bi' 2 | import { MdDelete } from 'react-icons/md' 3 | import { FaArrowUp, FaArrowDown, FaRegCopy } from 'react-icons/fa' 4 | import { TrackSetColorMenu } from './track-set-color-menu/track-set-color-menu' 5 | import { Track } from '../../../../../model/track/track' 6 | import { useDispatch } from 'react-redux' 7 | import { 8 | deleteTrack, 9 | duplicateTrack, 10 | moveTrackDown, 11 | moveTrackUp, 12 | setTrackColor, 13 | } from '../../store/playlist-slice' 14 | import { useRef } from 'react' 15 | import { useCallbackOnOutsideClick } from '../../../common/hooks/use-outside-click' 16 | 17 | export type TrackPopupMenuProps = { 18 | track: Track 19 | onClose: () => void 20 | onRename: () => void 21 | } 22 | 23 | export const TrackPopupMenu = ({ 24 | track, 25 | onClose, 26 | onRename, 27 | }: TrackPopupMenuProps) => { 28 | const dispatch = useDispatch() 29 | const popupMenuRef = useRef<HTMLDivElement>(null) 30 | 31 | useCallbackOnOutsideClick(popupMenuRef, onClose) 32 | 33 | const menuItems = [ 34 | { 35 | label: 'Rename', 36 | icon: <BiRename />, 37 | action: () => { 38 | onRename() 39 | }, 40 | }, 41 | { 42 | label: 'Move Up', 43 | icon: <FaArrowUp />, 44 | action: () => { 45 | dispatch(moveTrackUp(track.id)) 46 | }, 47 | }, 48 | { 49 | label: 'Move Down', 50 | icon: <FaArrowDown />, 51 | action: () => { 52 | dispatch(moveTrackDown(track.id)) 53 | }, 54 | }, 55 | { 56 | label: 'Duplicate', 57 | icon: <FaRegCopy />, 58 | action: () => { 59 | dispatch(duplicateTrack(track.id)) 60 | }, 61 | }, 62 | { 63 | label: 'Delete', 64 | icon: <MdDelete />, 65 | action: () => { 66 | dispatch(deleteTrack(track.id)) 67 | }, 68 | }, 69 | ] 70 | return ( 71 | <div 72 | className="w-52 flex flex-col bg-zinc-200 dark:bg-zinc-900 shadow-md shadow-zinc-600 rounded-xl overflow-hidden" 73 | ref={popupMenuRef} 74 | > 75 | <TrackSetColorMenu 76 | currentColor={track.color} 77 | onSetColor={(color) => 78 | dispatch(setTrackColor({ trackId: track.id, color })) 79 | } 80 | /> 81 | 82 | {menuItems.map((item, index) => ( 83 | <div 84 | key={index} 85 | className="p-2 hover:bg-zinc-300 dark:hover:bg-zinc-600 select-none cursor-pointer flex flex-row gap-2 items-center" 86 | onClick={item.action} 87 | > 88 | {item.icon} 89 | <p className="font-bold text-sm">{item.label}</p> 90 | </div> 91 | ))} 92 | </div> 93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /src/features/daw/midi-editor/store/midi-editor-slice.ts: -------------------------------------------------------------------------------- 1 | import { PayloadAction, createSlice } from '@reduxjs/toolkit' 2 | import { EditorMode } from './types/types' 3 | import { SCALE_KEYS, SCALE_TYPES, Scale } from '../../../../model/scale/scale' 4 | 5 | export interface MidiEditorSlice { 6 | rulerSize: number 7 | horizontalScroll: number 8 | verticalScroll: number 9 | whiteKeySize: number 10 | 11 | lastKeyDuration: number 12 | editorMode: EditorMode 13 | selectedNoteId: string | null 14 | notePreviewEnabled: boolean 15 | scaleViewEnabled: boolean 16 | selectedScale: Scale 17 | } 18 | 19 | const initialState: MidiEditorSlice = { 20 | rulerSize: 20, 21 | horizontalScroll: 0, 22 | verticalScroll: 0, 23 | lastKeyDuration: 2, 24 | whiteKeySize: 20, 25 | selectedNoteId: null, 26 | editorMode: 'select', 27 | notePreviewEnabled: false, 28 | scaleViewEnabled: false, 29 | selectedScale: { 30 | key: SCALE_KEYS[0], 31 | type: SCALE_TYPES[0], 32 | }, 33 | } 34 | 35 | export const midiEditorSlice = createSlice({ 36 | name: 'midiEditor', 37 | initialState, 38 | reducers: { 39 | setMidiEditorRulerSize: (state, action: PayloadAction<number>) => { 40 | state.rulerSize = action.payload 41 | }, 42 | setMidiEditorWhiteKeySize: (state, action: PayloadAction<number>) => { 43 | state.whiteKeySize = action.payload 44 | }, 45 | setMidiEditorHorizontalScroll: (state, action: PayloadAction<number>) => { 46 | state.horizontalScroll = action.payload 47 | }, 48 | setMidiEditorVerticalScroll: (state, action: PayloadAction<number>) => { 49 | state.verticalScroll = action.payload 50 | }, 51 | setLastKeyDuration: (state, action: PayloadAction<number>) => { 52 | state.lastKeyDuration = action.payload 53 | }, 54 | selectNote: (state, action: PayloadAction<string | null>) => { 55 | state.selectedNoteId = action.payload 56 | }, 57 | setEditorMode: (state, action: PayloadAction<EditorMode>) => { 58 | state.editorMode = action.payload 59 | }, 60 | toggleNotePreviewEnabled: (state) => { 61 | state.notePreviewEnabled = !state.notePreviewEnabled 62 | }, 63 | toggleScaleViewEnabled: (state) => { 64 | state.scaleViewEnabled = !state.scaleViewEnabled 65 | }, 66 | selectScale: (state, action: PayloadAction<Scale>) => { 67 | state.selectedScale = action.payload 68 | }, 69 | }, 70 | }) 71 | 72 | export const { 73 | setMidiEditorRulerSize, 74 | setMidiEditorWhiteKeySize, 75 | setMidiEditorHorizontalScroll, 76 | setMidiEditorVerticalScroll, 77 | setLastKeyDuration, 78 | selectNote, 79 | setEditorMode, 80 | toggleNotePreviewEnabled, 81 | toggleScaleViewEnabled, 82 | selectScale, 83 | } = midiEditorSlice.actions 84 | 85 | export default midiEditorSlice.reducer 86 | -------------------------------------------------------------------------------- /src/features/daw/common/components/mix-grid/mix-grid.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { PopupMenu } from '../popup-menu/popup-menu' 3 | import { RULER_SUB_BAR_WIDTH, SUB_BAR_NUM } from '../ruler/constants' 4 | import { MixGridItem } from './mix-grid-item/mix-grid-item' 5 | import { FaPaste } from 'react-icons/fa6' 6 | 7 | export type MixGridProps = { 8 | maxBars: number 9 | 10 | evenColumnsColor: string 11 | oddColumnsColor: string 12 | 13 | onSelectTick: (tick: number) => void 14 | onCreateBar: (startTick: number) => void 15 | onPasteBar: (startTick: number) => void 16 | } 17 | 18 | export const MixGrid = (props: MixGridProps) => { 19 | const [menuIsOpen, setMenuIsOpen] = useState(false) 20 | const [menuTick, setMenuTick] = useState<number>(0) 21 | 22 | const getTrackColorClass = (barIndex: number) => { 23 | return barIndex % 2 == 0 ? props.evenColumnsColor : props.oddColumnsColor 24 | } 25 | return ( 26 | <div className="flex flex-row relative"> 27 | {Array.from({ length: props.maxBars }).map((_, barIndex) => ( 28 | <div 29 | key={barIndex} 30 | className={`flex flex-col w-[80px] justify-end opacity-50 h-full border-l border-slate-400 ${ 31 | barIndex == props.maxBars - 1 ? 'border-r' : '' 32 | } ${getTrackColorClass(barIndex)}`} 33 | > 34 | <div className="flex flex-row h-full"> 35 | {Array.from({ length: SUB_BAR_NUM }).map((_, subBarIndex) => ( 36 | <MixGridItem 37 | key={subBarIndex} 38 | barIndex={barIndex} 39 | currentSubBar={subBarIndex} 40 | onSelectTick={props.onSelectTick} 41 | onCreateBar={props.onCreateBar} 42 | onContextMenu={(e, tick) => { 43 | e.preventDefault() 44 | props.onSelectTick(tick) 45 | setMenuTick(tick) 46 | setMenuIsOpen(true) 47 | }} 48 | /> 49 | ))} 50 | </div> 51 | </div> 52 | ))} 53 | {menuIsOpen && ( 54 | <div 55 | className="fixed mt-12 z-50 h-fit w-fit" 56 | style={{ marginLeft: (menuTick / 4) * RULER_SUB_BAR_WIDTH + 16 }} // calculating the offset of the menu based on the tick (plus some padding to the left) 57 | > 58 | <PopupMenu 59 | onClose={() => setMenuIsOpen(false)} 60 | items={[ 61 | { 62 | label: 'Paste', 63 | icon: <FaPaste />, 64 | action: () => { 65 | props.onPasteBar(menuTick) 66 | setMenuIsOpen(false) 67 | }, 68 | }, 69 | ]} 70 | /> 71 | </div> 72 | )} 73 | </div> 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /src/features/daw/common/components/keyboard/key/use-key-item-data.tsx: -------------------------------------------------------------------------------- 1 | import { Key } from '../../../../../../model/note/key/key' 2 | import { KeyItemProps } from './key-item' 3 | 4 | export const useKeyItemData = (props: KeyItemProps) => { 5 | const MIN_WHITE_KEY_SIZE = 8 6 | 7 | const whiteKeySize = Math.max( 8 | props.whiteKeySize, 9 | props.minWhiteKeySize || MIN_WHITE_KEY_SIZE 10 | ) 11 | const isBlackKey = props.keyToRender.includes('#') 12 | 13 | const getWhiteKeySize = () => whiteKeySize 14 | const getBlackKeySize = () => whiteKeySize * 0.75 15 | const getBlackKeyOffset = () => (whiteKeySize * 2 - getBlackKeySize()) / 2 16 | const getOffsetByKeyMap = () => { 17 | const blackKeyOffset = getBlackKeyOffset() 18 | 19 | return { 20 | C: 0, 21 | 'C#': blackKeyOffset, 22 | D: whiteKeySize, 23 | 'D#': whiteKeySize + blackKeyOffset, 24 | E: whiteKeySize * 2, 25 | F: whiteKeySize * 3, 26 | 'F#': whiteKeySize * 3 + blackKeyOffset, 27 | G: whiteKeySize * 4, 28 | 'G#': whiteKeySize * 4 + blackKeyOffset, 29 | A: whiteKeySize * 5, 30 | 'A#': whiteKeySize * 5 + blackKeyOffset, 31 | B: whiteKeySize * 6, 32 | } 33 | } 34 | 35 | const getOctaveWidth = () => whiteKeySize * 7 36 | 37 | const getOffsetByKey = (key: Key) => { 38 | const baseKey = key.slice(0, key.length - 1) 39 | const octave = Number(key.slice(-1)) 40 | const offsetByKeyMap = getOffsetByKeyMap() 41 | const offset = offsetByKeyMap[baseKey as keyof typeof offsetByKeyMap] 42 | return offset + octave * getOctaveWidth() 43 | } 44 | 45 | const getRelativeOffset = (key: Key, startingKey: Key) => { 46 | const startingOffset = getOffsetByKey(startingKey) 47 | const keyOffset = getOffsetByKey(key) 48 | return keyOffset - startingOffset 49 | } 50 | 51 | const keySize = isBlackKey ? getBlackKeySize() : getWhiteKeySize() 52 | const relativeOffset = getRelativeOffset(props.keyToRender, props.startingKey) 53 | const keyInvertedSize = isBlackKey ? '55%' : '100%' 54 | 55 | const leftOffset = props.orientation === 'horizontal' ? relativeOffset : 0 56 | const bottomOffset = props.orientation === 'vertical' ? relativeOffset : 0 57 | const width = 58 | props.orientation === 'horizontal' ? `${keySize}px` : keyInvertedSize 59 | const height = 60 | props.orientation === 'vertical' ? `${keySize}px` : keyInvertedSize 61 | 62 | const shadowWidth = props.orientation === 'horizontal' ? '100%' : '5%' 63 | const shadowHeight = props.orientation === 'vertical' ? '100%' : '5%' 64 | 65 | return { 66 | isBlackKey, 67 | leftOffset, 68 | bottomOffset, 69 | width, 70 | height, 71 | shadowWidth, 72 | shadowHeight, 73 | needsLabel: !isBlackKey, 74 | isHorizontal: props.orientation === 'horizontal', 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/features/daw/midi-editor/midi-body/key-editor/midi-editor-key-grid/midi-editor-key-grid.tsx: -------------------------------------------------------------------------------- 1 | import { useMidiEditorDimensions } from '../hooks/useMidiEditorDimensions' 2 | import { Key } from '../../../../../../model/note/key/key' 3 | import { useDragAndDrop } from '../../../../common/hooks/useDragAndDrop' 4 | import { PIANO_ROLL_BAR_HEADER_HEIGHT } from '../../../constants' 5 | import { useDispatch } from 'react-redux' 6 | import { moveNote } from '../../../../playlist/store/playlist-slice' 7 | import { GridCanvas } from './grid-canvas/grid-canvas' 8 | 9 | export type MidiEditorKeyGridProps = { 10 | showedKeys: Readonly<Key[]> 11 | 12 | cursorStyle?: 'default' | 'add' 13 | 14 | onKeyClick?: (key: Key, beat: number) => void 15 | onKeyDoubleClick?: (key: Key, beat: number) => void 16 | } 17 | 18 | export const MidiEditorKeyGrid = ({ 19 | showedKeys, 20 | onKeyClick, 21 | cursorStyle, 22 | onKeyDoubleClick, 23 | }: MidiEditorKeyGridProps) => { 24 | const midiEditorDimensions = useMidiEditorDimensions() 25 | const dispatch = useDispatch() 26 | 27 | const { handleOnDrop } = useDragAndDrop({ 28 | type: 'drop_note', 29 | singleKeyHeight: midiEditorDimensions.keyHeight, 30 | gridPaddingTop: PIANO_ROLL_BAR_HEADER_HEIGHT, 31 | onDropNote: (noteId, fromBarId, trackId, newStartAtTick, newKey) => { 32 | dispatch( 33 | moveNote({ 34 | noteId, 35 | fromBarId, 36 | trackId, 37 | newStartAtTick, 38 | newKey, 39 | }) 40 | ) 41 | }, 42 | }) 43 | 44 | const getKeyFromClick = (e: MouseEvent, boundigRect: DOMRect) => { 45 | const gridClickY = 46 | e.clientY - boundigRect.top - midiEditorDimensions.barHeaderHeight 47 | 48 | const keyIndex = Math.floor(gridClickY / midiEditorDimensions.keyHeight) 49 | return showedKeys[keyIndex] 50 | } 51 | 52 | const getTickFromClick = (e: MouseEvent, boundigRect: DOMRect) => { 53 | const gridClickX = e.clientX - boundigRect.left 54 | return Math.floor(gridClickX / midiEditorDimensions.beatWidth) 55 | } 56 | 57 | const onDoubleClick = (e: MouseEvent, boundigRect: DOMRect) => { 58 | onKeyDoubleClick && 59 | onKeyDoubleClick( 60 | getKeyFromClick(e, boundigRect), 61 | getTickFromClick(e, boundigRect) 62 | ) 63 | } 64 | 65 | const onClick = (e: MouseEvent, boundigRect: DOMRect) => { 66 | onKeyClick && 67 | onKeyClick( 68 | getKeyFromClick(e, boundigRect), 69 | getTickFromClick(e, boundigRect) 70 | ) 71 | } 72 | 73 | return ( 74 | <GridCanvas 75 | onDrop={handleOnDrop} 76 | showedKeys={showedKeys} 77 | onClick={onClick} 78 | onDoubleClick={onDoubleClick} 79 | cursorStyle={cursorStyle} 80 | midiEditorDimensions={midiEditorDimensions} 81 | /> 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /src/features/daw/menu/hamburger/useHamburgerData.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { FaCircleInfo } from 'react-icons/fa6' 3 | import { FiDownload, FiUpload } from 'react-icons/fi' 4 | import { IoIosHelpBuoy } from 'react-icons/io' 5 | import { VscJson } from 'react-icons/vsc' 6 | 7 | import { useExport } from '../hooks/import-export/useExport' 8 | import { useImportWizard } from './import/useImport' 9 | import { MdDarkMode, MdLightMode, MdOutlineColorLens } from 'react-icons/md' 10 | import { useDispatch } from 'react-redux' 11 | import { setTheme } from '../store/menu-slice' 12 | 13 | export const useHamburgerData = () => { 14 | const [isOpen, setIsOpen] = useState(false) 15 | 16 | const dispatch = useDispatch() 17 | 18 | const { exportTracksToJSON } = useExport() 19 | const { showImportDialog } = useImportWizard() 20 | 21 | return { 22 | onHamburgerClick: () => setIsOpen(!isOpen), 23 | menu: { 24 | isOpen: isOpen, 25 | onClose: () => setIsOpen(false), 26 | 27 | items: [ 28 | { 29 | label: 'Import', 30 | icon: <FiUpload />, 31 | children: [ 32 | { 33 | label: 'From JSON', 34 | icon: <VscJson />, 35 | action: () => { 36 | setIsOpen(false) 37 | showImportDialog() 38 | }, 39 | }, 40 | ], 41 | }, 42 | 43 | { 44 | label: 'Export', 45 | icon: <FiDownload />, 46 | children: [ 47 | { 48 | label: 'To JSON', 49 | icon: <VscJson />, 50 | action: () => { 51 | setIsOpen(false) 52 | exportTracksToJSON() 53 | }, 54 | }, 55 | ], 56 | }, 57 | 58 | { 59 | label: 'Theme', 60 | icon: <MdOutlineColorLens />, 61 | children: [ 62 | { 63 | label: 'Light', 64 | icon: <MdLightMode />, 65 | action: () => { 66 | setIsOpen(false) 67 | dispatch(setTheme('light')) 68 | }, 69 | }, 70 | { 71 | label: 'Dark', 72 | icon: <MdDarkMode />, 73 | action: () => { 74 | setIsOpen(false) 75 | dispatch(setTheme('dark')) 76 | }, 77 | }, 78 | ], 79 | }, 80 | 81 | { 82 | label: 'Help', 83 | icon: <IoIosHelpBuoy />, 84 | children: [ 85 | { 86 | label: 'About', 87 | icon: <FaCircleInfo />, 88 | action: () => { 89 | setIsOpen(false) 90 | }, 91 | }, 92 | ], 93 | }, 94 | ], 95 | }, 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/features/daw/common/components/keyboard/key/key-item.tsx: -------------------------------------------------------------------------------- 1 | import { Key } from '../../../../../../model/note/key/key' 2 | import { Track } from '../../../../../../model/track/track' 3 | import { useKeyItemData } from './use-key-item-data' 4 | 5 | export type KeyItemProps = { 6 | selectedTrack: Track 7 | keyToRender: Key 8 | startingKey: Key 9 | whiteKeySize: number 10 | minWhiteKeySize?: number 11 | isSelected: boolean 12 | isHighlighted?: boolean 13 | orientation: 'horizontal' | 'vertical' 14 | onMouseDown: (key: Key) => void 15 | onMouseUp: (key: Key) => void 16 | onMouseEnter: (key: Key) => void 17 | onMouseLeave: (key: Key) => void 18 | } 19 | 20 | export const KeyItem = (props: KeyItemProps) => { 21 | const itemData = useKeyItemData(props) 22 | 23 | const selectedTrackBgColor = `bg-${props.selectedTrack.color}-400` 24 | 25 | const tailwindClasses = itemData.isBlackKey 26 | ? `z-10 ${props.isSelected ? selectedTrackBgColor : 'bg-black'}` 27 | : `${itemData.isHorizontal ? 'border-r' : 'border-b'} border-black ${ 28 | props.isSelected ? selectedTrackBgColor : 'bg-white' 29 | }` 30 | 31 | const verticalStyle = { 32 | bottom: `${itemData.bottomOffset}px`, 33 | } 34 | 35 | const horizontalStyle = { 36 | left: `${itemData.leftOffset}px`, 37 | } 38 | 39 | return ( 40 | <div 41 | className={`h-full ${tailwindClasses} select-none absolute flex items-center justify-end gap-1 cursor-pointer ${ 42 | itemData.isHorizontal ? 'flex-col rounded-b-md' : 'flex-row' 43 | }`} 44 | style={{ 45 | ...{ 46 | ...(props.orientation === 'horizontal' 47 | ? horizontalStyle 48 | : verticalStyle), 49 | }, 50 | width: itemData.width, 51 | height: itemData.height, 52 | }} 53 | onMouseLeave={() => props.onMouseLeave(props.keyToRender)} 54 | onMouseEnter={() => props.onMouseEnter(props.keyToRender)} 55 | onMouseDown={() => props.onMouseDown(props.keyToRender)} 56 | onMouseUp={() => props.onMouseUp(props.keyToRender)} 57 | > 58 | {itemData.needsLabel && ( 59 | <p 60 | className={` ${ 61 | props.isSelected ? 'border-white text-white' : 'text-slate-400' 62 | } ${ 63 | itemData.isHorizontal 64 | ? 'text-sm border-slate-300 border-[1px] rounded-md px-1' 65 | : 'text-xs font-bold' 66 | }`} 67 | > 68 | {props.keyToRender} 69 | </p> 70 | )} 71 | 72 | <span 73 | className={`w-1 h-1 rounded-full ${ 74 | props.isHighlighted ? selectedTrackBgColor : '' 75 | }`} 76 | /> 77 | 78 | <div 79 | className={`place-self-stretch rounded-b-md ${ 80 | props.isSelected 81 | ? '' 82 | : itemData.isBlackKey 83 | ? 'bg-slate-800' 84 | : 'bg-slate-200' 85 | }`} 86 | style={{ 87 | height: itemData.shadowHeight, 88 | width: itemData.shadowWidth, 89 | }} 90 | /> 91 | </div> 92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /src/features/daw/playlist/track-list/track-item/track-item.tsx: -------------------------------------------------------------------------------- 1 | import { Track } from '../../../../../model/track/track' 2 | 3 | import { TRACK_HEIGHT } from '../../constants' 4 | import { useState } from 'react' 5 | import { TrackPopupMenu } from '../track-popup-menu/track-popup-menu' 6 | import { IoMdSettings } from 'react-icons/io' 7 | import { TrackItemSoloMuted } from './track-item-solo-mute/track-item-solo-muted' 8 | import { TrackItemNameVolume } from './track-item-name-volume/track-item-name-volume' 9 | 10 | export type TrackItemProps = { 11 | track: Track 12 | selectedTrack?: Track | null 13 | onSelectTrack: (track: Track) => void 14 | onToggleMute: () => void 15 | onToggleSolo: () => void 16 | } 17 | 18 | export const TrackItem = ({ 19 | track, 20 | selectedTrack, 21 | onSelectTrack, 22 | onToggleMute, 23 | onToggleSolo, 24 | }: TrackItemProps) => { 25 | const isSelected = selectedTrack?.id === track.id 26 | 27 | const [showTrackMenu, setShowTrackMenu] = useState(false) 28 | const [isRenaming, setIsRenaming] = useState(false) 29 | 30 | const effectivelyMuted = 31 | track.muted || (track.areThereAnyOtherTrackSoloed && !track.soloed) 32 | 33 | const selectedHighlightColor = effectivelyMuted 34 | ? 'bg-gray-800 dark:bg-gray-500' 35 | : `bg-${track.color}-500` 36 | 37 | return ( 38 | <div 39 | className={`flex flex-row justify-between w-full ${ 40 | isSelected 41 | ? 'bg-zinc-300 dark:bg-zinc-800' 42 | : 'bg-zinc-100 dark:bg-zinc-900' 43 | }`} 44 | style={{ height: `${TRACK_HEIGHT}px`, minHeight: `${TRACK_HEIGHT}px` }} 45 | onClick={() => onSelectTrack(track)} 46 | onContextMenu={(e) => { 47 | e.preventDefault() 48 | onSelectTrack(track) 49 | if (!showTrackMenu) { 50 | setShowTrackMenu(true) 51 | } 52 | }} 53 | > 54 | <TrackItemSoloMuted 55 | track={track} 56 | onToggleMute={onToggleMute} 57 | onToggleSolo={onToggleSolo} 58 | /> 59 | 60 | <TrackItemNameVolume 61 | track={track} 62 | effectivelyMuted={effectivelyMuted} 63 | isRenaming={isRenaming} 64 | setIsRenaming={setIsRenaming} 65 | /> 66 | 67 | <div className="relative h-full w-8"> 68 | <div 69 | className="mt-4 cursor-pointer self-center" 70 | onClick={() => { 71 | if (!showTrackMenu) { 72 | setShowTrackMenu(true) 73 | } 74 | }} 75 | > 76 | <IoMdSettings /> 77 | </div> 78 | {showTrackMenu && ( 79 | <div className="fixed mt-2 z-50 h-fit w-fit"> 80 | <TrackPopupMenu 81 | track={track} 82 | onClose={() => setShowTrackMenu(false)} 83 | onRename={() => { 84 | setShowTrackMenu(false) 85 | setIsRenaming(true) 86 | }} 87 | /> 88 | </div> 89 | )} 90 | </div> 91 | 92 | <div className={`h-full w-1 ${isSelected && selectedHighlightColor}`} /> 93 | </div> 94 | ) 95 | } 96 | -------------------------------------------------------------------------------- /src/features/daw/common/components/popup-menu/popup-menu.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react' 2 | import { useCallbackOnOutsideClick } from '../../hooks/use-outside-click' 3 | import { IoIosArrowForward } from 'react-icons/io' 4 | 5 | export type PopupMenuItem = { 6 | label: string 7 | icon: JSX.Element 8 | action?: () => void 9 | children?: PopupMenuItem[] 10 | } 11 | 12 | export type PopupMenuProps = { 13 | items: PopupMenuItem[] 14 | onClose: () => void 15 | } 16 | 17 | export const PopupMenu = ({ items, onClose }: PopupMenuProps) => { 18 | const [showChildrenMenu, setShowChildrenMenu] = useState(false) 19 | const [childMenuIndex, setChildMenuIndex] = useState(0) 20 | 21 | const popupMenuRef = useRef<HTMLDivElement>(null) 22 | useCallbackOnOutsideClick(popupMenuRef, onClose) 23 | 24 | return ( 25 | <div 26 | className={`w-52 flex flex-col bg-zinc-200 dark:bg-zinc-900 shadow-md shadow-zinc-600 rounded-tl-xl rounded-bl-xl ${ 27 | showChildrenMenu ? '' : 'rounded-tr-xl rounded-br-xl' 28 | } overflow-hidden`} 29 | ref={popupMenuRef} 30 | > 31 | {items.map((item, index) => ( 32 | <div 33 | key={index} 34 | className="p-2 h-10 hover:bg-zinc-300 dark:hover:bg-zinc-600 select-none cursor-pointer flex flex-row gap-2 items-center" 35 | onClick={item.action ? item.action : () => {}} 36 | onMouseEnter={() => { 37 | if (!item.children) { 38 | return 39 | } 40 | setShowChildrenMenu(true) 41 | setChildMenuIndex(index) 42 | }} 43 | onMouseLeave={() => { 44 | if (!item.children) { 45 | return 46 | } 47 | setShowChildrenMenu(false) 48 | }} 49 | > 50 | {item.icon} 51 | <p className="font-bold text-sm">{item.label}</p> 52 | 53 | {item.children && ( 54 | <div className="flex-grow flex justify-end"> 55 | <div className="h-full relative"> 56 | <IoIosArrowForward /> 57 | <div 58 | hidden={!(showChildrenMenu && childMenuIndex === index)} 59 | className="fixed -mt-7 ml-6 z-50 h-fit w-fit" 60 | > 61 | <div className="w-52 flex flex-col bg-zinc-200 dark:bg-zinc-900 shadow-md shadow-zinc-600 rounded-tr-xl rounded-br-xl overflow-hidden"> 62 | {item.children.map((item, index) => ( 63 | <div 64 | key={index} 65 | className="p-2 h-10 hover:bg-zinc-300 dark:hover:bg-zinc-600 select-none cursor-pointer flex flex-row gap-2 items-center " 66 | onClick={item.action} 67 | > 68 | {item.icon} 69 | <p className="font-bold text-sm">{item.label}</p> 70 | </div> 71 | ))} 72 | </div> 73 | </div> 74 | </div> 75 | </div> 76 | )} 77 | </div> 78 | ))} 79 | </div> 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/pad/sound-selector/category/drum-machine-closed-hh-icon.tsx: -------------------------------------------------------------------------------- 1 | export const DrumMachineClosedHiHatIcon = ( 2 | props: React.SVGProps<SVGSVGElement> 3 | ) => ( 4 | <svg 5 | xmlns="http://www.w3.org/2000/svg" 6 | className="fill-current" 7 | viewBox="0 0 236.5489 191" 8 | {...props} 9 | > 10 | <defs id="defs8"> 11 | <clipPath clipPathUnits="userSpaceOnUse" id="clipPath15"> 12 | <path 13 | style={{ 14 | fill: '#00ff00', 15 | strokeWidth: 107.462, 16 | strokeLinecap: 'round', 17 | strokeLinejoin: 'bevel', 18 | paintOrder: 'stroke fill markers', 19 | }} 20 | d="m 354.02305,1061.6186 287.64374,84.818 549.47331,73.7548 217.5766,51.6283 7.3755,213.889 70.0671,-3.6877 v 435.1533 l 44.2529,-11.0632 v -420.4024 l 62.6915,-14.751 v -73.7548 l 33.1897,51.6284 55.3161,-25.8142 7.3755,-55.3161 -55.3161,-81.1303 -33.1897,-3.6877 944.0615,-129.0709 L 2710.489,1043.1799 2533.4775,906.73348 1655.7953,777.6626 1600.4792,10.612625 1401.3412,-0.450625 1353.4006,755.53618 313.45791,962.04958 Z" 21 | id="path16" 22 | /> 23 | </clipPath> 24 | </defs> 25 | <g 26 | transform="matrix(0.1,0,0,-0.1,-34.5,191)" 27 | fill="currentColor" 28 | stroke="none" 29 | id="g8" 30 | clipPath="url(#clipPath15)" 31 | > 32 | <path 33 | d="m 1480,1695 v -215 h -30 -30 v -104 c 0,-117 6,-109 -85,-121 -27,-4 -72,-15 -100,-25 -27,-10 -90,-21 -140,-25 -151,-10 -334,-34 -462,-60 -116,-24 -258,-67 -277,-86 -6,-5 465,-9 1144,-9 679,0 1150,4 1145,9 -20,19 -162,62 -278,86 -128,26 -311,50 -462,60 -49,4 -113,15 -140,25 -28,10 -73,21 -100,25 -86,12 -86,12 -83,58 4,49 31,60 36,15 5,-38 32,-37 32,1 0,16 6,31 13,34 10,4 10,7 0,18 -7,7 -13,23 -13,36 0,14 -6,23 -15,23 -9,0 -15,-9 -15,-25 0,-18 -5,-25 -20,-25 -17,0 -20,7 -20,45 0,43 -1,45 -30,45 h -30 v 215 c 0,208 -1,215 -20,215 -19,0 -20,-7 -20,-215 z" 34 | id="path1" 35 | /> 36 | <path 37 | d="m 382,989 c 104,-54 418,-112 728,-135 52,-3 107,-12 122,-20 14,-7 56,-19 92,-26 l 66,-13 V 683 c 0,-69 4,-113 10,-113 6,0 10,-102 10,-285 V 0 h 90 90 v 285 c 0,183 4,285 10,285 6,0 10,45 10,114 v 114 l 48,7 c 26,4 72,15 102,25 30,10 93,21 140,25 252,19 482,57 625,101 183,56 236,53 -1025,53 H 345 Z" 38 | id="path2" 39 | /> 40 | <path 41 | d="m 2250,772 c 0,-4 18,-26 40,-47 38,-38 40,-38 40,-15 0,55 -10,70 -46,70 -19,0 -34,-4 -34,-8 z" 42 | id="path3" 43 | /> 44 | <path 45 | d="m 2785,760 c 3,-5 11,-10 16,-10 6,0 7,5 4,10 -3,6 -11,10 -16,10 -6,0 -7,-4 -4,-10 z" 46 | id="path4" 47 | /> 48 | <path 49 | d="m 2780,699 c 0,-5 5,-7 10,-4 6,3 10,8 10,11 0,2 -4,4 -10,4 -5,0 -10,-5 -10,-11 z" 50 | id="path5" 51 | /> 52 | <path 53 | d="m 2166,675 c 4,-8 10,-12 15,-9 11,6 2,24 -11,24 -5,0 -7,-7 -4,-15 z" 54 | id="path6" 55 | /> 56 | <path 57 | d="m 2217,599 c 7,-7 15,-10 18,-7 3,3 -2,9 -12,12 -14,6 -15,5 -6,-5 z" 58 | id="path7" 59 | /> 60 | <path 61 | d="m 2203,573 c 4,-3 10,-3 14,0 3,4 0,7 -7,7 -7,0 -10,-3 -7,-7 z" 62 | id="path8" 63 | /> 64 | </g> 65 | </svg> 66 | ) 67 | export default DrumMachineClosedHiHatIcon 68 | -------------------------------------------------------------------------------- /src/features/daw/player-bar/player/player.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux' 2 | import { selectIsPlaying } from '../store/selectors' 3 | import { togglePlay, stop } from '../store/playerBarSlice' 4 | import { 5 | FaPlay, 6 | FaPause, 7 | FaBackward, 8 | FaForward, 9 | FaStepBackward, 10 | FaStop, 11 | // , FaCircle 12 | } from 'react-icons/fa' 13 | import { 14 | requestBackwardTickPosition, 15 | requestForwardTickPosition, 16 | requestNewTickPosition, 17 | } from '../../playlist-header/store/playlist-header-slice' 18 | import { selectTrackIdInPlayingPreviewloop } from '../../instrument/store/selectors' 19 | import { usePreviewLoopSafeTransportPosition } from '../../common/hooks/use-preview-loop-safe-transport-position' 20 | // import { TfiLoop } from 'react-icons/tfi' 21 | 22 | export const Player = () => { 23 | const { time } = usePreviewLoopSafeTransportPosition() 24 | 25 | const isPlaying = useSelector(selectIsPlaying) 26 | const previewLoopPlayingTrackId = useSelector( 27 | selectTrackIdInPlayingPreviewloop 28 | ) 29 | 30 | const dispatch = useDispatch() 31 | 32 | const handleStop = () => { 33 | dispatch(stop()) 34 | dispatch(requestNewTickPosition(0)) 35 | } 36 | 37 | const minutes = Math.floor(time / 60) 38 | const seconds = Math.floor(time - minutes * 60) 39 | const milliseconds = Math.floor((time - seconds) * 10) 40 | const padTimePart = (timePart: number) => { 41 | return String(timePart).padStart(2, '0') 42 | } 43 | 44 | const timeString = `${padTimePart(minutes)}:${padTimePart( 45 | seconds 46 | )}:${milliseconds}` 47 | 48 | return ( 49 | <div className="flex flex-row gap-1 items-center justify-center"> 50 | {isPlaying ? ( 51 | <button onClick={handleStop} disabled={!!previewLoopPlayingTrackId}> 52 | <FaStop /> 53 | </button> 54 | ) : ( 55 | <button 56 | onClick={() => dispatch(requestNewTickPosition(0))} 57 | disabled={!!previewLoopPlayingTrackId} 58 | > 59 | <FaStepBackward /> 60 | </button> 61 | )} 62 | 63 | <button 64 | onClick={() => dispatch(requestBackwardTickPosition())} 65 | disabled={!!previewLoopPlayingTrackId} 66 | > 67 | <FaBackward /> 68 | </button> 69 | {isPlaying ? ( 70 | <button 71 | onClick={() => dispatch(togglePlay())} 72 | disabled={!!previewLoopPlayingTrackId} 73 | > 74 | <FaPause /> 75 | </button> 76 | ) : ( 77 | <button 78 | onClick={() => dispatch(togglePlay())} 79 | disabled={!!previewLoopPlayingTrackId} 80 | > 81 | <FaPlay /> 82 | </button> 83 | )} 84 | <button 85 | onClick={() => dispatch(requestForwardTickPosition())} 86 | disabled={!!previewLoopPlayingTrackId} 87 | > 88 | <FaForward /> 89 | </button> 90 | {/* TODO - Record & Loop buttons */} 91 | {/* <button className="text-red-500"> 92 | <FaCircle /> 93 | </button> */} 94 | {/* <button> 95 | <TfiLoop /> 96 | </button> */} 97 | <span className="select-none font-bold text-lg">{timeString}</span> 98 | </div> 99 | ) 100 | } 101 | -------------------------------------------------------------------------------- /src/features/daw/drum-machine/pad/sound-selector/pad-sound-item/selector-popup/sound-selector-popup.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | import { TRACK_DRUM_PATTERN_SIZE } from '../../../../../../../model/track/drums/track-drums' 3 | import { 4 | DrumSound, 5 | DrumSoundUtils, 6 | } from '../../../../../../../model/drums/sound/drums-sound' 7 | import { Track } from '../../../../../../../model/track/track' 8 | import { DrumMachineCategoryIcon } from '../../category/drum-category-icon' 9 | import { useCallbackOnOutsideClick } from '../../../../../common/hooks/use-outside-click' 10 | import { useDispatch } from 'react-redux' 11 | import { 12 | addPlayingKey, 13 | removeAllPlayingKeys, 14 | removePlayingKey, 15 | } from '../../../../../instrument/store/instrument-slice' 16 | 17 | export type SoundSelectorPopupProps = { 18 | selectedTrack: Track 19 | currentSoundIndex: number 20 | 21 | onSelectedSound: (sound: DrumSound) => void 22 | onClose: () => void 23 | } 24 | 25 | export const SoundSelectorPopup = ({ 26 | onClose, 27 | currentSoundIndex, 28 | selectedTrack, 29 | onSelectedSound, 30 | }: SoundSelectorPopupProps) => { 31 | const dispatch = useDispatch() 32 | const popupMenuRef = useRef<HTMLDivElement>(null) 33 | 34 | const onClosePopup = () => { 35 | onClose() 36 | dispatch(removeAllPlayingKeys(selectedTrack.id)) 37 | } 38 | 39 | useCallbackOnOutsideClick(popupMenuRef, onClosePopup) 40 | 41 | const sounds = DrumSoundUtils.getDrumsSoundSetByPreset( 42 | selectedTrack.instrumentPreset 43 | ) 44 | const defaultSounds = sounds.slice(0, TRACK_DRUM_PATTERN_SIZE) 45 | const selectedSoundsFromStore = selectedTrack.trackDrums?.selectedSounds 46 | const selectedSounds = 47 | selectedSoundsFromStore === undefined || 48 | selectedSoundsFromStore?.length === 0 49 | ? defaultSounds 50 | : selectedSoundsFromStore 51 | 52 | const currentSound = selectedSounds[currentSoundIndex] 53 | 54 | const selectable = [ 55 | currentSound, 56 | ...sounds.filter((sound) => !selectedSounds.includes(sound)), 57 | ] 58 | 59 | return ( 60 | <div 61 | className="w-52 max-h-60 overflow-y-auto flex flex-col bg-zinc-100 dark:bg-zinc-900 shadow-md shadow-zinc-600 rounded-xl " 62 | ref={popupMenuRef} 63 | > 64 | {selectable.map((item, index) => ( 65 | <div 66 | key={index} 67 | className="p-2 hover:bg-zinc-300 dark:hover:bg-zinc-600 select-none cursor-pointer flex flex-row gap-2 items-center" 68 | onClick={() => { 69 | onClosePopup() 70 | onSelectedSound(item) 71 | }} 72 | onMouseEnter={() => { 73 | dispatch( 74 | addPlayingKey({ 75 | key: item.key, 76 | trackId: selectedTrack.id, 77 | }) 78 | ) 79 | }} 80 | onMouseLeave={() => { 81 | dispatch( 82 | removePlayingKey({ 83 | key: item.key, 84 | trackId: selectedTrack.id, 85 | }) 86 | ) 87 | }} 88 | > 89 | <DrumMachineCategoryIcon 90 | category={item.category} 91 | className="w-6 h-full" 92 | /> 93 | <p className="font-bold text-sm">{item.name}</p> 94 | </div> 95 | ))} 96 | </div> 97 | ) 98 | } 99 | --------------------------------------------------------------------------------