+ {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) => (
{i + 79}
))}
- 108
- 109
- 110
- 115
- 120
@@ -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 (