27 | {selectedPatternBeats.map((patternSounds, soundIndex) => (
28 | {
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 |
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 |
29 |
34 |
35 |
36 |
52 |
68 |
69 |
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 |
16 | {TRACK_COLORS.map((item, index) => (
17 |
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 |
28 | )
29 | }
30 |
31 | export const TrackSetColorMenu = ({
32 | currentColor,
33 | onSetColor,
34 | }: TrackSetColorMenuProps) => {
35 | const [showColorMenu, setShowColorMenu] = useState(false)
36 | return (
37 |
setShowColorMenu(true)}
40 | onMouseLeave={() => setShowColorMenu(false)}
41 | >
42 |
43 |
Set Color
44 |
58 |
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
(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 |
22 |
Select file to import
23 |
24 |
25 |
{
32 | if (e?.target?.files && e.target.files.length > 0) {
33 | setFileToImport(e?.target?.files[0])
34 | }
35 | }}
36 | />
37 |
41 | JSON File previously exported.
42 |
43 |
44 |
45 |
46 |
53 |
54 |
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 |
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 |
26 |
{
30 | setShowPopup(!showPopup)
31 | }}
32 | >
33 |
36 |
37 |
41 |
42 |
{sound.name}
43 |
44 |
45 |
46 |
47 | {showPopup && (
48 |
54 | setShowPopup(false)}
56 | currentSoundIndex={index}
57 | selectedTrack={selectedTrack}
58 | onSelectedSound={onSelectedSound}
59 | />
60 |
61 | )}
62 |
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/src/features/daw/drum-machine/pad/sound-selector/category/drum-machine-snare-icon.tsx:
--------------------------------------------------------------------------------
1 | export const DrumMachineSnareIcon = (props: React.SVGProps) => (
2 |
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 |
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('')
18 | const [trackCount, setTrackCount] = useState(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> = ({
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 |
76 | {wrappedActiveStepContent}
77 |
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 |
45 |
46 |
47 |
Velocity
48 |
{noteVelocity}
49 |
50 |
51 |
{
59 | setNoteVelocity(parseInt(e.target.value))
60 | }}
61 | className={`h-1 w-full cursor-ew-resize rounded-lg accent-${selectedTrack?.color}-600`}
62 | >
63 |
64 |
65 | )
66 | }
67 |
--------------------------------------------------------------------------------
/src/features/daw/drum-machine/pad/sound-selector/category/drum-machine-clap-icon.tsx:
--------------------------------------------------------------------------------
1 | export const DrumMachineClapIcon = (props: React.SVGProps) => (
2 |
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: ,
30 | },
31 | {
32 | id: 'draw',
33 | icon: ,
34 | },
35 | {
36 | id: 'delete',
37 | icon: ,
38 | },
39 | ]
40 |
41 | return (
42 |
43 |
44 | {modes.map((mode) => (
45 |
59 | ))}
60 |
61 |
62 |
63 |
75 |
76 |
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 |
64 | {items.map((item) => (
65 |
72 | ))}
73 |
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('')
44 | const [exportData, setExportData] = useState(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
3 | ) => (
4 |
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(null)
30 |
31 | useCallbackOnOutsideClick(popupMenuRef, onClose)
32 |
33 | const menuItems = [
34 | {
35 | label: 'Rename',
36 | icon: ,
37 | action: () => {
38 | onRename()
39 | },
40 | },
41 | {
42 | label: 'Move Up',
43 | icon: ,
44 | action: () => {
45 | dispatch(moveTrackUp(track.id))
46 | },
47 | },
48 | {
49 | label: 'Move Down',
50 | icon: ,
51 | action: () => {
52 | dispatch(moveTrackDown(track.id))
53 | },
54 | },
55 | {
56 | label: 'Duplicate',
57 | icon: ,
58 | action: () => {
59 | dispatch(duplicateTrack(track.id))
60 | },
61 | },
62 | {
63 | label: 'Delete',
64 | icon: ,
65 | action: () => {
66 | dispatch(deleteTrack(track.id))
67 | },
68 | },
69 | ]
70 | return (
71 |
75 |
78 | dispatch(setTrackColor({ trackId: track.id, color }))
79 | }
80 | />
81 |
82 | {menuItems.map((item, index) => (
83 |
88 | {item.icon}
89 |
{item.label}
90 |
91 | ))}
92 |
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) => {
40 | state.rulerSize = action.payload
41 | },
42 | setMidiEditorWhiteKeySize: (state, action: PayloadAction) => {
43 | state.whiteKeySize = action.payload
44 | },
45 | setMidiEditorHorizontalScroll: (state, action: PayloadAction) => {
46 | state.horizontalScroll = action.payload
47 | },
48 | setMidiEditorVerticalScroll: (state, action: PayloadAction) => {
49 | state.verticalScroll = action.payload
50 | },
51 | setLastKeyDuration: (state, action: PayloadAction) => {
52 | state.lastKeyDuration = action.payload
53 | },
54 | selectNote: (state, action: PayloadAction) => {
55 | state.selectedNoteId = action.payload
56 | },
57 | setEditorMode: (state, action: PayloadAction) => {
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) => {
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(0)
21 |
22 | const getTrackColorClass = (barIndex: number) => {
23 | return barIndex % 2 == 0 ? props.evenColumnsColor : props.oddColumnsColor
24 | }
25 | return (
26 |
27 | {Array.from({ length: props.maxBars }).map((_, barIndex) => (
28 |
34 |
35 | {Array.from({ length: SUB_BAR_NUM }).map((_, subBarIndex) => (
36 | {
43 | e.preventDefault()
44 | props.onSelectTick(tick)
45 | setMenuTick(tick)
46 | setMenuIsOpen(true)
47 | }}
48 | />
49 | ))}
50 |
51 |
52 | ))}
53 | {menuIsOpen && (
54 |
58 |
setMenuIsOpen(false)}
60 | items={[
61 | {
62 | label: 'Paste',
63 | icon: ,
64 | action: () => {
65 | props.onPasteBar(menuTick)
66 | setMenuIsOpen(false)
67 | },
68 | },
69 | ]}
70 | />
71 |
72 | )}
73 |
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
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 |
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: ,
31 | children: [
32 | {
33 | label: 'From JSON',
34 | icon: ,
35 | action: () => {
36 | setIsOpen(false)
37 | showImportDialog()
38 | },
39 | },
40 | ],
41 | },
42 |
43 | {
44 | label: 'Export',
45 | icon: ,
46 | children: [
47 | {
48 | label: 'To JSON',
49 | icon: ,
50 | action: () => {
51 | setIsOpen(false)
52 | exportTracksToJSON()
53 | },
54 | },
55 | ],
56 | },
57 |
58 | {
59 | label: 'Theme',
60 | icon: ,
61 | children: [
62 | {
63 | label: 'Light',
64 | icon: ,
65 | action: () => {
66 | setIsOpen(false)
67 | dispatch(setTheme('light'))
68 | },
69 | },
70 | {
71 | label: 'Dark',
72 | icon: ,
73 | action: () => {
74 | setIsOpen(false)
75 | dispatch(setTheme('dark'))
76 | },
77 | },
78 | ],
79 | },
80 |
81 | {
82 | label: 'Help',
83 | icon: ,
84 | children: [
85 | {
86 | label: 'About',
87 | icon: ,
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 | 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 |
68 | {props.keyToRender}
69 |
70 | )}
71 |
72 |
77 |
78 |
91 |
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 | onSelectTrack(track)}
46 | onContextMenu={(e) => {
47 | e.preventDefault()
48 | onSelectTrack(track)
49 | if (!showTrackMenu) {
50 | setShowTrackMenu(true)
51 | }
52 | }}
53 | >
54 |
59 |
60 |
66 |
67 |
68 |
{
71 | if (!showTrackMenu) {
72 | setShowTrackMenu(true)
73 | }
74 | }}
75 | >
76 |
77 |
78 | {showTrackMenu && (
79 |
80 | setShowTrackMenu(false)}
83 | onRename={() => {
84 | setShowTrackMenu(false)
85 | setIsRenaming(true)
86 | }}
87 | />
88 |
89 | )}
90 |
91 |
92 |
93 |
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(null)
22 | useCallbackOnOutsideClick(popupMenuRef, onClose)
23 |
24 | return (
25 |
31 | {items.map((item, index) => (
32 |
{}}
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 |
{item.label}
52 |
53 | {item.children && (
54 |
55 |
56 |
57 |
61 |
62 | {item.children.map((item, index) => (
63 |
68 | {item.icon}
69 |
{item.label}
70 |
71 | ))}
72 |
73 |
74 |
75 |
76 | )}
77 |
78 | ))}
79 |
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
3 | ) => (
4 |
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 |
50 | {isPlaying ? (
51 |
54 | ) : (
55 |
61 | )}
62 |
63 |
69 | {isPlaying ? (
70 |
76 | ) : (
77 |
83 | )}
84 |
90 | {/* TODO - Record & Loop buttons */}
91 | {/* */}
94 | {/* */}
97 | {timeString}
98 |
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(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 |
64 | {selectable.map((item, index) => (
65 |
{
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 |
93 |
{item.name}
94 |
95 | ))}
96 |
97 | )
98 | }
99 |
--------------------------------------------------------------------------------