From fd41a6fe982c32f415845854cfd1d1ae63e4ae06 Mon Sep 17 00:00:00 2001 From: scha-ch Date: Thu, 26 Oct 2023 04:33:03 +0200 Subject: [PATCH] DAW#85 Fix Transport.stop() & Refactoring & Sampler & Bass basics --- README.md | 10 ++ package-lock.json | 3 +- .../(routes)/project/presets/DefaultPreset.ts | 43 ++--- src/app/common/constants.tsx | 137 ++++++++++------ src/app/common/styles.ts | 4 + src/app/common/types/instrument.types.ts | 28 ++-- src/app/common/types/midi.types.ts | 4 +- src/app/common/types/utility.types.ts | 23 ++- src/app/components/App.tsx | 5 +- src/app/components/Arranger.tsx | 1 - src/app/components/Locator.tsx | 20 +-- src/app/components/Mixer.tsx | 154 +++++++++--------- src/app/components/Note.tsx | 12 +- src/app/components/PianoRoll.tsx | 17 +- src/app/components/Time.tsx | 16 +- src/app/components/Transport.tsx | 111 ++++++------- src/app/components/track/Track.tsx | 99 ++++++----- src/app/core/hooks/api/useProjectContext.ts | 40 +---- src/app/core/hooks/audio/useAudioScheduler.ts | 90 +++++++--- src/app/core/hooks/useNoteEditor.ts | 40 ++--- src/app/core/hooks/useTransport.ts | 10 +- src/app/page.tsx | 8 +- 22 files changed, 479 insertions(+), 396 deletions(-) diff --git a/README.md b/README.md index f08a4182..ab3b5d56 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,16 @@ Design will be applied soon, this is just a raw sketch! ### 🍂 October 2023 +#### October, 25th + +
+ Screenshot 2023-10-25 at 05 35 02 + +**Nice Gold Knob**
+(Design by *ykadosh*) + +
+ #### October, 24th - **Arranger** Track selection (active track) diff --git a/package-lock.json b/package-lock.json index 937c0bb1..7f36299a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9300,7 +9300,8 @@ }, "node_modules/tone": { "version": "14.7.77", - "license": "MIT", + "resolved": "https://registry.npmjs.org/tone/-/tone-14.7.77.tgz", + "integrity": "sha512-tCfK73IkLHyzoKUvGq47gyDyxiKLFvKiVCOobynGgBB9Dl0NkxTM2p+eRJXyCYrjJwy9Y0XCMqD3uOYsYt2Fdg==", "dependencies": { "standardized-audio-context": "^25.1.8", "tslib": "^2.0.1" diff --git a/src/app/api/(routes)/project/presets/DefaultPreset.ts b/src/app/api/(routes)/project/presets/DefaultPreset.ts index d6b1bf93..663f5464 100644 --- a/src/app/api/(routes)/project/presets/DefaultPreset.ts +++ b/src/app/api/(routes)/project/presets/DefaultPreset.ts @@ -1,4 +1,6 @@ import { + /*** @project */ + DEFAULT_ACTIVE_TRACK_ID, DEFAULT_STATES, DEFAULT_BPM, DEFAULT_CLEF, @@ -6,22 +8,25 @@ import { DEFAULT_NAME, DEFAULT_POSITION, DEFAULT_QUANTIZATION, + DEFAULT_SCALE, + DEFAULT_SWING, + DEFAULT_SWING_SUBDIVISION, + + /*** @track */ + DEFAULT_TRACK_SAMPLER, DEFAULT_TRACK_INSTRUMENT_BASS, // DEFAULT_TRACK_AUDIO, - // DEFAULT_TRACK_SAMPLER, // DEFAULT_TRACK_PLAYERS, + // DEFAULT_TRACK_HI_TOM, + // DEFAULT_TRACK_MI_TOM, + // DEFAULT_TRACK_LO_TOM, + // DEFAULT_TRACK_BD, + // DEFAULT_TRACK_SD, + // DEFAULT_TRACK_OH, + + /*** @tchannel */ DEFAULT_CHANNEL_DRUMS, DEFAULT_CHANNEL_MASTER, - DEFAULT_TRACK_HI_TOM, - DEFAULT_TRACK_MI_TOM, - DEFAULT_TRACK_LO_TOM, - DEFAULT_TRACK_BD, - DEFAULT_TRACK_SD, - DEFAULT_SCALE, - DEFAULT_ACTIVE_TRACK_ID, - DEFAULT_TRACK_OH, - DEFAULT_SWING, - DEFAULT_SWING_SUBDIVISION, } from '@/constants'; import type { IProjectContext } from '@/types/project.types'; @@ -40,14 +45,14 @@ const DefaultPreset: IProjectContext = { swing: DEFAULT_SWING, swingSubdivision: DEFAULT_SWING_SUBDIVISION, tracks: [ - DEFAULT_TRACK_BD, - DEFAULT_TRACK_SD, - DEFAULT_TRACK_OH, - DEFAULT_TRACK_HI_TOM, - DEFAULT_TRACK_MI_TOM, - DEFAULT_TRACK_LO_TOM, - // DEFAULT_TRACK_INSTRUMENT_BASS, - // DEFAULT_TRACK_SAMPLER, + DEFAULT_TRACK_SAMPLER, + DEFAULT_TRACK_INSTRUMENT_BASS, + // DEFAULT_TRACK_BD, + // DEFAULT_TRACK_SD, + // DEFAULT_TRACK_OH, + // DEFAULT_TRACK_HI_TOM, + // DEFAULT_TRACK_MI_TOM, + // DEFAULT_TRACK_LO_TOM, // DEFAULT_TRACK_AUDIO, // DEFAULT_TRACK_PLAYERS, ], diff --git a/src/app/common/constants.tsx b/src/app/common/constants.tsx index 5f1e1e60..eb06419e 100644 --- a/src/app/common/constants.tsx +++ b/src/app/common/constants.tsx @@ -1,4 +1,5 @@ import _ from 'lodash/fp'; +import * as Tone from 'tone'; import t from '@/core/i18n'; @@ -7,7 +8,7 @@ import type { IChannel } from './types/channel.types'; import { EInstrument } from './types/instrument.types'; // ---------- General -/*** @Progression */ +/*** @constants */ export const PROGRESSION = [ /*** @Diatonic */ // Major @@ -41,13 +42,19 @@ export const PROGRESSION = [ // Montgomery Ward bridge 'I IV ii V', ]; +export const ROMAN_NUMS = ['i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii']; -export const isRomanNum = (test: string) => - ['i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii'].includes(test.toLowerCase()); +// ---------- Utils +/*** @utils */ +export const getMeasureTime = (n: number) => Tone.Time(`${n}m`); +export const getMeasureSeconds = (n: number) => getMeasureTime(n).toSeconds(); +export const getNotationTime = (n: number) => Tone.Time(`${n}n`); +export const getNotationSeconds = (n: number) => getNotationTime(n).toSeconds(); +export const isRomanNum = (s: string) => ROMAN_NUMS.includes(_.toLower(s)); // ---------- Project const DEFAULT_ACTIVE_TRACK_ID = 'track-bd'; -const DEFAULT_BPM = 98; +const DEFAULT_BPM = 120; const DEFAULT_CLEF = 'D'; const DEFAULT_SCALE = 'minor'; const DEFAULT_MEASURE_COUNT = 2; @@ -72,7 +79,7 @@ const DEFAULT_TRACK_INSTRUMENT_BASS: ITrack = { output: 'master', input: { id: EInstrument.MonoSynth, - label: 'BA', + label: 'in-bass', options: { oscillator: { type: 'sawtooth' }, envelope: { attack: 0.01, decay: 0.4, release: 0.3, sustain: 0.2 }, @@ -80,16 +87,16 @@ const DEFAULT_TRACK_INSTRUMENT_BASS: ITrack = { attack: 0.001, baseFrequency: 180, decay: 0.2, - octaves: 0, + octaves: 2, release: 0.2, sustain: 0.25, }, - volume: -35, + volume: -31, }, parts: [ { - label: 'BA-Part1', - events: [], + label: 'p1-bass', + notes: [['D1'], ['D1'], ['D1'], ['D1'], ['F#1'], ['F#1'], [], []], }, ], }, @@ -129,11 +136,11 @@ const DEFAULT_TRACK_BD: ITrack = { parts: [ { label: 'BD', - events: [{ note: 'C2', duration: '16n', x: 0 }], + times: [0], }, { label: 'BD', - events: [{ note: 'C2', duration: '16n', x: 0 }], + times: [0], }, ], }, @@ -155,11 +162,11 @@ const DEFAULT_TRACK_SD: ITrack = { parts: [ { label: 'p1-SD', - events: [{ note: 'D2', duration: '16n', x: 8 }], + times: [8], }, { label: 'p2-SD', - events: [{ note: 'D2', duration: '16n', x: 8 }], + times: [8], }, ], }, @@ -181,27 +188,11 @@ const DEFAULT_TRACK_OH: ITrack = { parts: [ { label: 'p1-oh', - events: [ - { note: 'F#2', duration: '16n', x: 0 }, - { note: 'F#2', duration: '16n', x: 2 }, - { note: 'F#2', duration: '16n', x: 4 }, - { note: 'F#2', duration: '16n', x: 6 }, - { note: 'F#2', duration: '16n', x: 8 }, - { note: 'F#2', duration: '16n', x: 10 }, - { note: 'F#2', duration: '16n', x: 12 }, - { note: 'F#2', duration: '16n', x: 14 }, - ], + times: [0, 2, 4, 6, 8, 10, 12, 14], }, { label: 'p2-oh', - events: [ - { note: 'F#2', duration: '16n', x: 0 }, - { note: 'F#2', duration: '16n', x: 2 }, - { note: 'F#2', duration: '16n', x: 4 }, - { note: 'F#2', duration: '16n', x: 6 }, - { note: 'F#2', duration: '16n', x: 8 }, - { note: 'F#2', duration: '16n', x: 10 }, - ], + times: [0, 2, 4, 6, 8, 10], }, ], }, @@ -221,13 +212,10 @@ const DEFAULT_TRACK_HI_TOM: ITrack = { url: './samples/WaveAlchemy/wa_drm_drums/high_tom/wadrm_hitom_acc1_r2.wav', }, parts: [ - { label: 'HiTomPart1', events: [] }, + { label: 'HiTomPart1', times: [] }, { label: 'HiTomPart2', - events: [ - { note: 'C4', duration: '16n', x: 12 }, - { note: 'C4', duration: '16n', x: 13 }, - ], + times: [12, 13], }, ], }, @@ -249,11 +237,11 @@ const DEFAULT_TRACK_MI_TOM: ITrack = { parts: [ { label: 'MiTomPart', - events: [], + times: [], }, { label: 'MiTomPart', - events: [{ note: 'F4', duration: '16n', x: 14 }], + times: [14], }, ], }, @@ -275,11 +263,11 @@ const DEFAULT_TRACK_LO_TOM: ITrack = { parts: [ { label: 'LoTomPart', - events: [], + times: [], }, { label: 'LoTomPart', - events: [{ note: 'E4', duration: '16n', x: 15 }], + times: [15], }, ], }, @@ -290,12 +278,12 @@ const DEFAULT_TRACK_LO_TOM: ITrack = { // -------- Sampler /*** @Track */ const DEFAULT_TRACK_SAMPLER: ITrack = { - id: 'track-sampler', - name: 'Sampler', + id: 'sampler-drums', + name: 'Drums', routing: { input: { id: EInstrument.Sampler, - label: 'Drums', + label: 'in-drums', options: { baseUrl: './samples/WaveAlchemy/wa_808_tape/', urls: { @@ -304,13 +292,54 @@ const DEFAULT_TRACK_SAMPLER: ITrack = { E3: 'wa_808tape_closedhat_08_clean.wav', }, volume: -40, - fadeIn: 0.2, - fadeOut: 0.2, + fadeIn: 0.01, + fadeOut: 0.01, + curve: 'linear', }, parts: [ { - label: 'Drums (Sampler)', - events: [], + label: 'p1-sampler', + notes: [ + ['C3', 'E3'], + ['E3'], + [], + ['C3', 'E3'], + ['D3'], + ['E3'], + ['C3', 'E3'], + ['E3'], + ['C3', 'E3'], + ['C3', 'E3'], + [], + ['E3'], + ['D3', 'E3'], + [], + [], + ['E3'], + ], + }, + { + label: 'p2-sampler', + notes: [ + ['C3', 'E3'], + ['E3'], + [], + ['C3', 'E3'], + ['D3'], + [], + ['C3', 'E3'], + ['E3'], + ['C3', 'E3'], + ['C3', 'E3'], + [], + ['E3'], + ['D3', 'E3'], + [], + ['E3'], + [], + [], + [], + ], }, ], }, @@ -365,16 +394,16 @@ export { // DEFAULT_STATES, // + DEFAULT_TRACK_SAMPLER, // DEFAULT_TRACK_AUDIO, DEFAULT_TRACK_INSTRUMENT_BASS, // DEFAULT_TRACK_PLAYERS, - // DEFAULT_TRACK_SAMPLER, - DEFAULT_TRACK_BD, - DEFAULT_TRACK_SD, - DEFAULT_TRACK_OH, - DEFAULT_TRACK_HI_TOM, - DEFAULT_TRACK_MI_TOM, - DEFAULT_TRACK_LO_TOM, + // DEFAULT_TRACK_BD, + // DEFAULT_TRACK_SD, + // DEFAULT_TRACK_OH, + // DEFAULT_TRACK_HI_TOM, + // DEFAULT_TRACK_MI_TOM, + // DEFAULT_TRACK_LO_TOM, DEFAULT_CHANNEL_DRUMS, DEFAULT_CHANNEL_MASTER, }; diff --git a/src/app/common/styles.ts b/src/app/common/styles.ts index 36fc49fb..5660f0a1 100644 --- a/src/app/common/styles.ts +++ b/src/app/common/styles.ts @@ -38,6 +38,7 @@ const styles = { meter: 'w-full bg-[#333]', track: { main: 'pt-6 bg-[#3e4140] border border-r-[#555] border-r-[2px] justify-center text-xs items-center content-end', + channel: 'justify-end relative bg-green-700', inner: 'flex px-4 items-center gap-1 py-2 border-b border-b-[#555]', master: 'bg-[#3e4140] border border-r-[#555] border-r-[2px] h-full flex flex-col justify-end', @@ -50,6 +51,9 @@ const styles = { bg: 'bg-transparent', bgActive: 'bg-blue-900 text-white', }, + time: { + main: 'flex flex-1 items-center px-1 py-2 border-l border-r-gray-200', + }, track: { audio: { main: 'w-full flex flex-col justify-center' }, time: 'flex w-full bg-white text-gray-500 text-xs', diff --git a/src/app/common/types/instrument.types.ts b/src/app/common/types/instrument.types.ts index 3e00b26b..d3637e08 100644 --- a/src/app/common/types/instrument.types.ts +++ b/src/app/common/types/instrument.types.ts @@ -1,4 +1,4 @@ -import { FC } from "react"; +import { FC } from 'react'; import { ChannelOptions, MembraneSynth, @@ -10,21 +10,21 @@ import { PlayersOptions, Sampler, Synth, -} from "tone"; -import { RecursivePartial as TRecursivePartial } from "tone/build/esm/core/util/Interface"; +} from 'tone'; +import { RecursivePartial as TRecursivePartial } from 'tone/build/esm/core/util/Interface'; export enum EInstrument { - AmSynth = "AmSynth", - FmSynth = "FmSynth", - MembraneSynth = "MembraneSynth", - MetalSynth = "MetalSynth", - MonoSynth = "MonoSynth", - NoiseSynth = "NoiseSynth", - OmniSynth = "OmniSynth", - Player = "Player", - Players = "Players", - Sampler = "Sampler", - Synth = "Synth", + AmSynth = 'AmSynth', + FmSynth = 'FmSynth', + MembraneSynth = 'MembraneSynth', + MetalSynth = 'MetalSynth', + MonoSynth = 'MonoSynth', + NoiseSynth = 'NoiseSynth', + OmniSynth = 'OmniSynth', + Player = 'Player', + Players = 'Players', + Sampler = 'Sampler', + Synth = 'Synth', } export type TInstrument = diff --git a/src/app/common/types/midi.types.ts b/src/app/common/types/midi.types.ts index 61a0e839..5e3e324e 100644 --- a/src/app/common/types/midi.types.ts +++ b/src/app/common/types/midi.types.ts @@ -1,5 +1,4 @@ import type { Note, Note as TNote } from 'tone/build/esm/core/type/NoteUnits'; -import type { Time as TTime } from 'tone/build/esm/core/type/Units'; export interface IMidiEventPos { readonly n: TNote | null; @@ -18,5 +17,6 @@ export interface IPlayerEvent { export interface IMidiPart { readonly label: string; - readonly events: IPlayerEvent[]; + readonly times?: number[]; + readonly notes?: TNote[][]; } diff --git a/src/app/common/types/utility.types.ts b/src/app/common/types/utility.types.ts index e9b3e057..b426f523 100644 --- a/src/app/common/types/utility.types.ts +++ b/src/app/common/types/utility.types.ts @@ -1,30 +1,27 @@ /** * Enums */ -enum EUnit { - Em = "em", - Percent = "%", - Pt = "pt", - Px = "px", - Rem = "rem", +export enum EUnit { + Em = 'em', + Percent = '%', + Pt = 'pt', + Px = 'px', + Rem = 'rem', } /** * Types */ -type TEmptyFunction = () => void; -type TOptional = T | null; +export type UniqueIdentifier = string | number; +export type TEmptyFunction = () => void; +export type TOptional = T | null; /** * Interfaces */ -interface IAbsolutePosition { +export interface IAbsolutePosition { top?: number; right?: number; bottom?: number; left?: number; } - -export { EUnit }; -export type { TOptional }; -export type { IAbsolutePosition }; diff --git a/src/app/components/App.tsx b/src/app/components/App.tsx index 8be7872e..154dac2f 100644 --- a/src/app/components/App.tsx +++ b/src/app/components/App.tsx @@ -1,7 +1,6 @@ 'use client'; -import { type MouseEvent, useEffect } from 'react'; +import { type MouseEvent } from 'react'; import { CogIcon, GridIcon, HopIcon, InfinityIcon } from 'lucide-react'; -import * as Tone from 'tone'; import { DndContext, useSensor, @@ -42,11 +41,13 @@ export function App() { const { isOpen, InstrumentPortal, openInstrument, closeInstrument } = useAudioInstrument(); + /* useEffect(() => { const audioContext = new Tone.Context(); audioContext.resume(); // Your audio processing code goes here }, []); + */ // const { audioToAbc, audioToMidi } = useConverter(); const mouseSensor = useSensor(MouseSensor); diff --git a/src/app/components/Arranger.tsx b/src/app/components/Arranger.tsx index 21633ce6..9eaa4113 100644 --- a/src/app/components/Arranger.tsx +++ b/src/app/components/Arranger.tsx @@ -71,7 +71,6 @@ export default function Arranger({ className = '' }: IArranger) { tracks={tracks} track={trk} measureCount={measureCount} - quantization={quantization} mutate={mutate} patchProjectContext={patchProjectContext} /> diff --git a/src/app/components/Locator.tsx b/src/app/components/Locator.tsx index db538381..6b8d2c44 100644 --- a/src/app/components/Locator.tsx +++ b/src/app/components/Locator.tsx @@ -1,29 +1,29 @@ -"use client"; -import { useState } from "react"; -import cn from "classnames"; +'use client'; +import { useState } from 'react'; +import cn from 'classnames'; -import useTransport from "@/core/hooks/useTransport"; -import { DEFAULT_OFFSET_LEFT } from "app/common/constants"; +import useTransport from '@/core/hooks/useTransport'; +import { DEFAULT_OFFSET_LEFT } from 'app/common/constants'; -import { EUnit } from "app/common/types/utility.types"; -import type { IProjectContext } from "app/common/types/project.types"; +import { EUnit } from 'app/common/types/utility.types'; +import type { IProjectContext } from 'app/common/types/project.types'; interface ILocator { className?: string; projectContext: IProjectContext; } -const styles = { locator: "bg-black w-[1px] absolute top-0 bottom-0" }; +const styles = { locator: 'bg-black w-[1px] absolute top-0 bottom-0' }; const getMeasureWidth = (measureCount: number) => (window.innerWidth - DEFAULT_OFFSET_LEFT) / measureCount; -export default function Locator({ className = "", projectContext }: ILocator) { +export default function Locator({ className = '', projectContext }: ILocator) { const [left, setLeft] = useState(DEFAULT_OFFSET_LEFT); const [quarter, setQuarter] = useState(0); const { measureCount } = projectContext; function loopFn(position: string) { - const splitPosition = position.toString().split(":"); + const splitPosition = position.toString().split(':'); const currentMeasure = parseInt(splitPosition[0], 10); const currentQuarter = parseInt(splitPosition[1], 10); if (currentQuarter === quarter) return; diff --git a/src/app/components/Mixer.tsx b/src/app/components/Mixer.tsx index 3a34feb0..11236e61 100644 --- a/src/app/components/Mixer.tsx +++ b/src/app/components/Mixer.tsx @@ -1,27 +1,26 @@ 'use client'; import { useCallback, useMemo, type ReactNode } from 'react'; -import { Loader } from 'lucide-react'; +import { Loader, LucideIcon } from 'lucide-react'; import { useWindowWidth } from '@react-hook/window-size'; import classNames from 'classnames'; import * as Tone from 'tone'; -import { NUM_BANDS } from './Analyzer'; import t from '@/core/i18n'; import { getIconByType } from 'config/icons'; import useProjectContext from '@/core/hooks/api/useProjectContext'; import { Meter } from '@/components'; import { ButtonGroup, Knob, MuteButton, SoloButton } from 'packages/pfui'; -import { ESize, EVariant } from 'packages/pfui/constants'; -import type { UniqueIdentifier } from '@dnd-kit/core'; -import { ETrackType, type ITrack } from 'app/common/types/track.types'; +import { ESize } from 'packages/pfui/constants'; +import { ETrackType } from 'app/common/types/track.types'; +import type { UniqueIdentifier } from '@/types/utility.types'; import type { IMixer } from 'app/common/types/mixer.types'; import styles from 'app/common/styles'; import { DEFAULT_OFFSET_LEFT } from '@/common/constants'; const $ = styles.mixer; -// ("w-[calc(100%-4px)] ml-[2px] mb-2 text-center lg:w-[80%] lg:ml-[10%] lg:p-1"); +/*** @templates */ const Inner = ({ children }: { children: ReactNode }) => (
{children}
); @@ -32,24 +31,19 @@ const TplFX = () => ( ); -const getTrackData = (track: ITrack, activeTrackId: UniqueIdentifier) => { - const { id, routing, type, name } = track; - const { input, output } = routing; - const { instrument, label } = input; - const active = activeTrackId === id ? 'active' : 'inactive'; - const cn = classNames($.track.main, $.track[active]); - const Icon = getIconByType(type); - return { cn, Icon, id, instrument, label, name, output }; -}; +/*** + * @Mixer + * @description renders channels/tracks + * @todo extract routing + */ export default function Mixer({ openInstrument }: IMixer) { - const windowWidth = useWindowWidth(); - - /* 1. Master Settings */ + /*** @master */ const masterMeter = new Tone.Meter(); masterMeter.normalRange = true; - const masterGain = new Tone.Gain(0.8).toDestination().chain(masterMeter); + const masterGain = new Tone.Gain(0.5).toDestination().chain(masterMeter); + const windowWidth = useWindowWidth(); const FxChannel = useMemo( () => ({ Inserts: () => , @@ -84,20 +78,33 @@ export default function Mixer({ openInstrument }: IMixer) { return channels.find((channel) => channel.id === id)?.channel || masterGain; }; - const channelTracks = channels.map((channel, channelIndex) => { - const meter = new Tone.Meter(); - meter.normalRange = true; - const out = getChannelById(channel.routing.output); - channel.channel!.chain(meter, out); - const Icon = getIconByType(ETrackType.Channel); - + interface IMixerRender { + Icon: LucideIcon; + id: UniqueIdentifier; + index: number; + inputLabel: string; + outputLabel: string; + label: string; + meter: Tone.Meter; + } + /*** @shared render function for channels & tracks */ + function render({ + Icon, + id, + index, + inputLabel, + outputLabel, + label, + meter, + }: IMixerRender) { return (
@@ -109,8 +116,8 @@ export default function Mixer({ openInstrument }: IMixer) { {getRouting({ - label: channel.routing.input, - output: channel.routing.output, + label: inputLabel, + output: outputLabel, })} @@ -120,56 +127,57 @@ export default function Mixer({ openInstrument }: IMixer) { -

{channel.label}

+

{label}

- + - {channel.label} + {label}
); + } + /*** @channels */ + const channelTracks = channels.map((channel, index) => { + const meter = new Tone.Meter(); + meter.normalRange = true; + const out = getChannelById(channel.routing.output); + channel.channel!.fan(meter, out); + const Icon = getIconByType(ETrackType.Channel); + return render({ + Icon, + index, + meter, + id: channel.id, + inputLabel: channel.routing.input, + outputLabel: channel.routing.output, + label: channel.label, + }); }); - + /*** @tracks */ const mixerTracks = tracks.map((track, trackIndex) => { const meter = new Tone.Meter(); meter.normalRange = true; + const { + id, + name, + routing: { input, output }, + type, + } = track; + const Icon = getIconByType(type); + const isActive = $d!.activeTrackId === id ? 'active' : 'inactive'; + const cn = classNames($.track.main, $.track[isActive]); const out = getChannelById(track.routing.output); - const trackData = getTrackData(track, $d!.activeTrackId); - const { cn, Icon, instrument, label, name, output } = trackData; - const player = instrument!.instrument as Tone.Player; - player - .chain(meter, out) - .load((instrument!.options as Tone.PlayerOptions).url as string); - - return ( -
- - - - - - - - {getRouting({ label, output })} - - - - - - - -

{label}

-
- - - {name} - -
- ); + const i = input.instrument!.instrument.fan(meter, out); + // .load((instrument!.options as Tone.PlayerOptions).url as string + return render({ + Icon, + index: trackIndex, + meter, + id, + inputLabel: input.label, + outputLabel: output, + label: name, + }); }); return ( diff --git a/src/app/components/Note.tsx b/src/app/components/Note.tsx index 598c708b..791484fa 100644 --- a/src/app/components/Note.tsx +++ b/src/app/components/Note.tsx @@ -1,25 +1,23 @@ import classNames from 'classnames'; -import styles from 'app/common/styles'; - import type { AllHTMLAttributes, CSSProperties } from 'react'; -import type { Note as TNote } from 'tone/build/esm/core/type/NoteUnits'; + +import styles from 'app/common/styles'; +const $ = styles.notes; interface INote extends AllHTMLAttributes { - note: TNote; index: number; style: CSSProperties; } export default function Note({ index, - note, onClick, className = '', style, + value = ' ', }: INote) { // TODO dependency injection of styles - const $ = styles.notes; const props = { className: classNames('absolute', $.main, $.bgActive, { className }), onClick, @@ -28,7 +26,7 @@ export default function Note({ return (
- {note} + {value}
); } diff --git a/src/app/components/PianoRoll.tsx b/src/app/components/PianoRoll.tsx index 82e03801..f538890e 100644 --- a/src/app/components/PianoRoll.tsx +++ b/src/app/components/PianoRoll.tsx @@ -1,11 +1,11 @@ -import _ from "lodash/fp"; -import { Scale } from "tonal"; +import _ from 'lodash/fp'; +import { Scale } from 'tonal'; -import { DEFAULT_OFFSET_LEFT } from "app/common/constants"; -import useProjectContext from "@/core/hooks/api/useProjectContext"; -import { Grid } from "packages/pfui"; +import { DEFAULT_OFFSET_LEFT } from 'app/common/constants'; +import useProjectContext from '@/core/hooks/api/useProjectContext'; +import { Grid } from 'packages/pfui'; -import type { ITrack } from "app/common/types/track.types"; +import type { ITrack } from 'app/common/types/track.types'; export default function PianoRoll() { // Const [currentStepIndex, setCurrentStepIndex] = useState(0); @@ -16,17 +16,16 @@ export default function PianoRoll() { ({ id }) => id === activeTrackId ); if (!activeTrack) { - console.error("[PianoRoll] No active track"); + console.error('[PianoRoll] No active track'); return null; } - const events = activeTrack.routing.input.parts?.[0].events; const notes2 = Scale.get(`${clef}2 major`).notes; const notes1 = Scale.get(`${clef}1 major`).notes; const noteScale = [...notes2.reverse(), ...notes1.reverse()]; const findRowIndex = (note: string) => noteScale.findIndex((n) => n === note); const { measureCount, quantization } = projectContext; const gridColumnCount = quantization * measureCount; - const rows = new Array(noteScale.length * gridColumnCount).fill("_"); + const rows = new Array(noteScale.length * gridColumnCount).fill('_'); return (
diff --git a/src/app/components/Time.tsx b/src/app/components/Time.tsx index 42447f5b..a8a939db 100644 --- a/src/app/components/Time.tsx +++ b/src/app/components/Time.tsx @@ -1,19 +1,17 @@ -import { DEFAULT_OFFSET_LEFT } from "../common/constants"; -import { TimeIcon } from "../../config/icons"; -import styles from "../common/styles"; +import { DEFAULT_OFFSET_LEFT } from '@/common/constants'; + +import styles from '@/common/styles'; +const $ = styles.time; export default function Time({ measureCount }: { measureCount: number }) { const mc = measureCount as number; - // Support 4x'16n' notes for now (TODO '32n' at least) + /*** @issue Support multiple modes: Q, s, etc. */ return (
 
- {new Array(measureCount).fill("").map((_, measureIndex) => ( -
+ {new Array(measureCount).fill('').map((_, measureIndex) => ( +
{measureIndex + 1}
))} diff --git a/src/app/components/Transport.tsx b/src/app/components/Transport.tsx index 8ebf392b..4a0847e6 100644 --- a/src/app/components/Transport.tsx +++ b/src/app/components/Transport.tsx @@ -1,6 +1,7 @@ -"use client"; -import { type ChangeEvent, type MouseEvent, useState, useEffect } from "react"; -import { Transport as ToneTransport, start } from "tone"; +'use client'; +import { type ChangeEvent, type MouseEvent, useState, useEffect } from 'react'; +import * as Tone from 'tone'; + import { CircleIcon, FastForwardIcon, @@ -12,17 +13,17 @@ import { RewindIcon, SquareIcon, TimerIcon, -} from "lucide-react"; +} from 'lucide-react'; -import useProjectContext from "@/core/hooks/api/useProjectContext"; -import useTransport from "@/core/hooks/useTransport"; -import styles from "app/common/styles"; +import useProjectContext from '@/core/hooks/api/useProjectContext'; +import useTransport from '@/core/hooks/useTransport'; -import { EEndpoint } from "app/common/types/api.types"; +import styles from 'app/common/styles'; +import { EEndpoint } from '@/common/types/api.types'; +const $ = styles.transport; export default function Transport() { - const $ = styles.transport; - const [position, setPosition] = useState("0:0:0.000"); + const [position, setPosition] = useState('0:0:0.000'); const loopFn = (position: string) => setPosition(position); const { loop } = useTransport({ loopFn }); const { @@ -31,48 +32,24 @@ export default function Transport() { mutate, } = useProjectContext(); - useEffect(() => { - if (!$d) return; - ToneTransport.bpm.value = $d.bpm; - ToneTransport.loop = true; - ToneTransport.loopStart = 0; - ToneTransport.loopEnd = `${$d.measureCount}m`; - }, [$d]); - - const events = { - // TransportSettings - onBpmChange: (event: ChangeEvent) => { - const bpm = parseInt(event.target.value, 10); - patchProjectContext({ bpm }); - }, - onClefChange: (event: ChangeEvent) => { - const clef = event.target.value; - patchProjectContext({ clef }); - }, - onMeasureCountChange: (event: ChangeEvent) => { - const measureCount = parseInt(event.target.value, 10); - patchProjectContext({ measureCount }); - }, - onQuantizationChange: (event: ChangeEvent) => { - const quantization = parseInt(event.target.value, 10); - patchProjectContext({ quantization }); - }, - // TransportControl - onPause: (_: MouseEvent) => ToneTransport.pause(), - onRecord: (_: MouseEvent) => - console.log("🎙️ Recording soon available"), - onStart: async (_: MouseEvent) => { - ToneTransport.start(); - await start(); - }, - onStop: (_: MouseEvent) => { - console.log("stop"); - ToneTransport.stop(); - loop.stop(); - }, - }; - function TransportControl() { + const events = { + // TransportControl + onPause: (_: MouseEvent) => Tone.Transport.pause(), + onRecord: (_: MouseEvent) => + console.log('🎙️ Recording soon available'), + onStart: (_: MouseEvent) => { + Tone.Transport.bpm.value = $d!.bpm; + Tone.Transport.loop = true; + Tone.Transport.loopStart = 0; + Tone.Transport.loopEnd = `${$d!.measureCount}m`; + Tone.Transport.start(); + loop.start(); + }, + onStop: (_: MouseEvent) => { + Tone.Transport.stop(); + }, + }; return (
) => { + const clef = event.target.value; + patchProjectContext({ clef }); + mutate(EEndpoint.ProjectSettings); + }, + onMeasureCountChange: (event: ChangeEvent) => { + const measureCount = parseInt(event.target.value, 10); + patchProjectContext({ measureCount }); + mutate(EEndpoint.ProjectSettings); + }, + onQuantizationChange: (event: ChangeEvent) => { + const quantization = parseInt(event.target.value, 10); + patchProjectContext({ quantization }); + mutate(EEndpoint.ProjectSettings); + }, + // TransportSettings + onBpmChange: (event: ChangeEvent) => { + const bpm = parseInt(event.target.value, 10); + patchProjectContext({ bpm }); + mutate(EEndpoint.ProjectSettings); + }, + }; + return (
{$d ? ( @@ -181,16 +182,11 @@ export default function Transport() { id="bpm" onChange={events.onBpmChange} > - {new Array(50).fill("").map((x, i) => ( + {new Array(50).fill('').map((x, i) => ( ))} - - - - -
@@ -214,6 +210,7 @@ export default function Transport() {
); } + return (
diff --git a/src/app/components/track/Track.tsx b/src/app/components/track/Track.tsx index d8bbb1a0..82fbcb8a 100644 --- a/src/app/components/track/Track.tsx +++ b/src/app/components/track/Track.tsx @@ -7,42 +7,39 @@ import * as Tone from 'tone'; import { getIconByType } from 'config/icons'; import { SortableItem } from '@/components'; +import useNoteEditor from '@/core/hooks/useNoteEditor'; import useScheduler from '@/core/hooks/audio/useAudioScheduler'; import { DEFAULT_OFFSET_LEFT } from 'app/common/constants'; import Note from '../Note'; + +import { EEndpoint } from '@/common/types/api.types'; +import { type ITrack } from 'app/common/types/track.types'; import type { IMidiPart } from 'app/common/types/midi.types'; -import type { ITrack } from 'app/common/types/track.types'; import styles from 'app/common/styles'; -import { EEndpoint } from '@/common/types/api.types'; -import useNoteEditor from '@/core/hooks/useNoteEditor'; const $ = styles.track; interface IExtendedTrack { tracks: ITrack[]; track: ITrack; measureCount: number; - quantization: number; mutate: (endpoint: EEndpoint) => void; patchProjectContext: (patch: Record) => void; } - function Track({ tracks, track, measureCount, - quantization, mutate, patchProjectContext, }: IExtendedTrack) { const { id, name, type, routing, className = '' } = track; const windowWidth = useWindowWidth() - DEFAULT_OFFSET_LEFT; - const { setupPlayer } = useScheduler(); - const { id: inputId, instrument, parts = [] } = routing.input; + const { setupInstrument } = useScheduler(); + const { id: inputId, instrument, parts } = routing.input; const { addNote, deleteNote } = useNoteEditor({ tracks, - parts, patchProjectContext, mutate, }); @@ -73,41 +70,63 @@ function Track({ }, }; - const drawPart = (part: IMidiPart, partIndex: number) => { - const { events } = part; + const drawPart = (pIndex: number, part: IMidiPart) => { + const measureWidth = windowWidth / measureCount; + const width = measureWidth / 16; + const commonStyle = { + width, + height: '100%', + top: '0', + borderRight: '1px solid #fff', + borderBottom: '1px solid #fff', + }; - return events.map(({ note, duration, x }, eventIndex) => { - const measureWidth = windowWidth / measureCount; - const left = x * (measureWidth / 16) + partIndex * measureWidth; - const width = measureWidth / parseInt(duration, 10); + if (part.notes) + return part.notes.map((notes, notesIndex) => { + return notes.map((note, noteIndex) => { + return ( + + ); + }); + }); + else if (part.times) + return part.times.map((time, timeIndex) => { + const left = time * width + pIndex * measureWidth; - return ( - - ); - }); + return ( + + ); + }); }; + /*** @issue https://github.com/artiphishle/daw/issues/87 */ useEffect(() => { - if (!(instrument?.options as Tone.PlayerOptions).url) return; - - setupPlayer({ - player: instrument?.instrument as Tone.Player, - id: inputId, - parts, - windowWidth, - measureCount, - }); + const { instrument: i, options } = instrument! as any; + if (options.url) + i.load(options.url as string) // to useAudioScheduler + .then(() => setupInstrument({ id: inputId, instrument: i, parts })) + .catch(console.error); + else setupInstrument({ id, instrument: i, parts }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [instrument]); @@ -129,7 +148,7 @@ function Track({
{name}
- {parts.map((part, partIndex) => drawPart(part, partIndex))} + {parts.map((part, partIndex) => drawPart(partIndex, part))}
); diff --git a/src/app/core/hooks/api/useProjectContext.ts b/src/app/core/hooks/api/useProjectContext.ts index d8aacdc1..8c52e281 100644 --- a/src/app/core/hooks/api/useProjectContext.ts +++ b/src/app/core/hooks/api/useProjectContext.ts @@ -6,7 +6,7 @@ import OmniSynth from '@/core/instruments/OmniSynth'; import Sampler from '@/core/instruments/Sampler'; import { EEndpoint } from 'app/common/types/api.types'; -import { ETrackType } from 'app/common/types/track.types'; +import { ETrackType, IRoutingInput } from 'app/common/types/track.types'; import type { IProjectContext } from 'app/common/types/project.types'; import { type TInputOptions, @@ -21,52 +21,32 @@ const loadInstrument = (_instrument: EInstrument, options: TInputOptions) => { switch (_instrument) { case EInstrument.MembraneSynth: - Instrument = OmniSynth; - instrument = new Tone.MembraneSynth(options as Tone.MembraneSynthOptions); - break; case EInstrument.MetalSynth: - Instrument = OmniSynth; - instrument = new Tone.MetalSynth(options as Tone.MetalSynthOptions); - break; case EInstrument.MonoSynth: - Instrument = OmniSynth; - instrument = new Tone.MonoSynth(options as Tone.MonoSynthOptions); - break; case EInstrument.NoiseSynth: + case EInstrument.Players: + case EInstrument.Synth: Instrument = OmniSynth; - instrument = new Tone.NoiseSynth(options as Tone.NoiseSynthOptions); + instrument = new Tone[_instrument](options as Record); break; case EInstrument.Player: Instrument = OmniSynth; - instrument = new Tone.Player(options as Tone.PlayerOptions); - break; - case EInstrument.Players: - Instrument = OmniSynth; - instrument = new Tone.Players( - options as Tone.PlayersOptions - ) as Tone.Players; + instrument = new Tone.Player(); break; case EInstrument.Sampler: Instrument = Sampler; instrument = new Tone.Sampler(options as Tone.SamplerOptions); break; - case EInstrument.Synth: - Instrument = OmniSynth; - instrument = new Tone.Synth(options as Tone.SynthOptions); - break; default: - console.error( + console.warn( "[useProjectContext] Using 'Synth' for unknown instrument:", _instrument ); Instrument = OmniSynth; instrument = new Tone.Synth(options as Tone.SynthOptions); + break; } - return { - Instrument, - instrument, - options, - }; + return { Instrument, instrument, options } as IInstrument; }; export default function useProjectContext() { @@ -84,9 +64,7 @@ export default function useProjectContext() { // 2 Instrument const deselectedTracks = tracks.map((track) => { - const { type, routing } = track; - - if (type !== ETrackType.Player) return track; + const { routing } = track; const instrument: IInstrument = loadInstrument( routing.input.id as EInstrument, { ...routing.input.options } diff --git a/src/app/core/hooks/audio/useAudioScheduler.ts b/src/app/core/hooks/audio/useAudioScheduler.ts index 43103e77..a897e966 100644 --- a/src/app/core/hooks/audio/useAudioScheduler.ts +++ b/src/app/core/hooks/audio/useAudioScheduler.ts @@ -1,36 +1,82 @@ import _ from 'lodash/fp'; import * as Tone from 'tone'; -import type { IMidiPart, IPlayerEvent } from 'app/common/types/midi.types'; +import type { IMidiPart } from 'app/common/types/midi.types'; import type { UniqueIdentifier } from '@dnd-kit/core'; +import { getMeasureSeconds, getNotationSeconds } from '@/common/constants'; +const one16 = getNotationSeconds(16); +const oneM = getMeasureSeconds(1); + interface IUseScheduler { id: UniqueIdentifier; - player: Tone.Player; - measureCount: number; - windowWidth: number; + instrument: any; parts: IMidiPart[]; } export default function useAudioScheduler() { - function setupPlayer({ - windowWidth, - measureCount, - player, - parts = [], - }: IUseScheduler) { - const measureWidth = windowWidth / measureCount; - parts.forEach((part, partIndex) => { - Tone.Transport.scheduleRepeat((time) => { - part.events.forEach(({ duration, x }) => { - const t = - x * Tone.Time('16n').toSeconds() + - time + - partIndex * Tone.Time('1m').toSeconds(); - player.start(t, 0, duration); + // @issue https://github.com/artiphishle/daw/issues/87 + const setupPlayer = ({ instrument, parts = [] }: IUseScheduler) => { + parts.forEach((part, pIndex) => + part.times!.forEach((time) => { + const t = time * getNotationSeconds(16) + pIndex * getMeasureSeconds(1); + instrument.sync().start(t); + }) + ); + }; + function setupSampler({ id, instrument, parts = [] }: IUseScheduler) { + if (id === 'track-instrument-bass') { + return parts.forEach(({ notes }) => { + new Tone.Sequence((time, note) => { + (instrument as Tone.MonoSynth).triggerAttackRelease( + note, + '4n', + time, + 80 + ); + }, notes).start(0); + }); + } + const interval = setInterval(() => { + if (!(instrument as Tone.Sampler).loaded) return; + clearInterval(interval); + + parts.forEach(({ notes }, partIndex) => { + notes?.forEach((note, noteIndex) => { + (instrument as Tone.Sampler) + .triggerAttackRelease( + note, + one16, + partIndex * oneM + noteIndex * one16, + noteIndex % 2 ? 100 : 94 // velocity one/three pronounced + ) + .sync(); }); - }, `${measureCount}m`); - }); + }); + }, 50); } - return { setupPlayer }; + const setupSynth = setupSampler; + + const setupInstrument = (options: IUseScheduler) => { + console.info('[useAudioScheduler] id:', options.id); + // @issue don't force id to start with something specific + switch ((options.id as string).split('-')[0]) { + case 'track': + setupSynth(options); + break; + case 'sampler': + setupSampler(options); + break; + case 'player': + setupPlayer(options); + break; + default: + console.warn( + '[useAudioScheduler>setupInstrument]: Not supported yet:', + options.id + ); + break; + } + }; + return { setupInstrument }; } diff --git a/src/app/core/hooks/useNoteEditor.ts b/src/app/core/hooks/useNoteEditor.ts index f7d89769..df4a4695 100644 --- a/src/app/core/hooks/useNoteEditor.ts +++ b/src/app/core/hooks/useNoteEditor.ts @@ -1,51 +1,47 @@ import { EEndpoint } from '@/common/types/api.types'; -import { IMidiPart } from '@/common/types/midi.types'; -import { ITrack } from '@/common/types/track.types'; +import type { IMidiPart } from '@/common/types/midi.types'; +import type { ITrack } from '@/common/types/track.types'; +interface IAddNote { + qIndex: number; + partIndex: number; + track: ITrack; +} +interface IDeleteNote { + track: ITrack; + noteIndex: number; + partIndex: number; +} interface IUseNoteEditor { - parts: IMidiPart[]; tracks: ITrack[]; patchProjectContext: (patch: Record) => void; mutate: (endpoint: EEndpoint) => void; } export default function useNoteEditor({ - parts, tracks, patchProjectContext, mutate, }: IUseNoteEditor) { - interface IAddNote { - qIndex: number; - partIndex: number; - track: ITrack; - } const addNote = ({ track, qIndex, partIndex }: IAddNote) => { const trackIndex = tracks.findIndex((t) => t.id === track.id); const patchTracks = [...tracks.filter((t) => t.id !== track.id)]; - track.routing.input.parts[partIndex].events.push({ - note: 'C4', - duration: '16n', - x: qIndex, - }); + + track.routing.input.parts[partIndex].times.push(qIndex); patchTracks.splice(trackIndex, 0, track); + patchProjectContext({ tracks: patchTracks }); mutate(EEndpoint.ProjectSettings); }; - interface IDeleteNote { - track: ITrack; - noteIndex: number; - partIndex: number; - } const deleteNote = ({ noteIndex, partIndex, track }: IDeleteNote) => { const trackIndex = tracks.findIndex((t) => t.id === track.id); const patchTracks = [...tracks.filter((t) => t.id !== track.id)]; - const newEvents = track.routing.input.parts[partIndex].events; - newEvents.splice(noteIndex, 1); + const newTimes = track.routing.input.parts[partIndex].times; + newTimes.splice(noteIndex, 1); const newInput = { ...track.routing.input }; newInput.parts[partIndex] = { label: track.routing.input.parts[partIndex].label, - events: newEvents, + times: newTimes, }; const newTrack = { ...track }; newTrack.routing.input = newInput; diff --git a/src/app/core/hooks/useTransport.ts b/src/app/core/hooks/useTransport.ts index 73ce3a54..aa51107c 100644 --- a/src/app/core/hooks/useTransport.ts +++ b/src/app/core/hooks/useTransport.ts @@ -1,12 +1,12 @@ -import { Loop, Time, Transport } from "tone"; -import type { Time as TTime } from "tone/build/esm/core/type/Units"; +import * as Tone from 'tone'; const pos = { format: (position: string) => { - const [m, q, x] = position.split(":"); + const [m, q, x] = position.split(':'); return `${m}:${q}:${parseFloat(x).toFixed(3)}`; }, - get: (time: TTime = Transport.position) => Time(time).toBarsBeatsSixteenths(), + get: (time = Tone.Transport.position) => + Tone.Time(time).toBarsBeatsSixteenths(), }; interface IUseTransportProps { @@ -15,7 +15,7 @@ interface IUseTransportProps { export default function useTransport({ loopFn }: IUseTransportProps) { const repeat = () => loopFn && loopFn(pos.format(pos.get())); - const loop = new Loop(repeat, "16n").start(); + const loop = new Tone.Loop(repeat, '16n'); return { loop }; } diff --git a/src/app/page.tsx b/src/app/page.tsx index c1b25fb8..f2467c0f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,4 @@ 'use client'; - import * as Tone from 'tone'; import { useState } from 'react'; import { Loader } from 'lucide-react'; @@ -11,13 +10,12 @@ import { EButtonType } from 'packages/pfui/button/Button'; export default function Home() { const [toneReady, setToneReady] = useState(false); + const [isLoading, setIsLoading] = useState(false); function Start() { - const [isLoading, setIsLoading] = useState(false); - const onClick = async () => { + const onClick = () => { setIsLoading(true); - await Tone.start(); - setToneReady(true); + Tone.start().then(() => setToneReady(true)); }; return (