('');
+ const playStartTimeControlX = useRef(0);
+ const selectedTrack = selectedTrackId ? commonStore.tracks.find(t => t.id === selectedTrackId) : undefined;
+
+ useEffect(() => {
+ setRefreshRef(!refreshRef);
+ }, [windowSize, commonStore.tracks]);
+
+ const viewControlsContainerWidth = (toolbarRef.current && toolbarButtonRef.current && toolbarSliderRef.current) ?
+ toolbarRef.current.clientWidth - toolbarButtonRef.current.clientWidth - toolbarSliderRef.current.clientWidth - 16 // 16 = ml-2 mr-2
+ : 0;
+ const tracksWidth = viewControlsContainerWidth;
+ const timeOfTracksWidth = Math.floor(tracksWidth / snapValue) // number of moves
+ * baseMoveTime * scale;
+ const currentTimeControlWidth = (timeOfTracksWidth < commonStore.trackTotalTime)
+ ? timeOfTracksWidth / commonStore.trackTotalTime * viewControlsContainerWidth
+ : 0;
+ const playStartTimeControlPosition = {
+ x: (commonStore.trackPlayStartTime - commonStore.trackCurrentTime) / (baseMoveTime * scale) * snapValue,
+ y: 0
+ };
+
+ return (
+
+
+ {`${commonStore.trackPlayStartTime} ms / ${commonStore.trackTotalTime} ms`}
+
+
+
+ } />
+ } onClick={() => {
+ commonStore.setTracks([]);
+ }} />
+
+
+
+
{
+ setTimeout(() => {
+ let offset = 0;
+ if (currentTimeControlRef.current) {
+ const match = currentTimeControlRef.current.style.transform.match(/translate\((.+)px,/);
+ if (match)
+ offset = parseFloat(match[1]);
+ }
+ const offsetTime = commonStore.trackTotalTime / viewControlsContainerWidth * offset;
+ commonStore.setTrackCurrentTime(offsetTime);
+ }, 1);
+ }}
+ >
+
+
+
viewControlsContainerWidth)
+ && 'hidden'
+ )}>
+
{
+ playStartTimeControlX.current = data.lastX;
+ }}
+ onStop={(e, data) => {
+ const delta = data.lastX - playStartTimeControlX.current;
+ let offsetTime = Math.round(Math.round(delta / snapValue * baseMoveTime * scale) / minimalMoveTime) * minimalMoveTime;
+ offsetTime = Math.min(Math.max(
+ offsetTime,
+ -commonStore.trackPlayStartTime), commonStore.trackTotalTime - commonStore.trackPlayStartTime);
+ commonStore.setTrackPlayStartTime(commonStore.trackPlayStartTime + offsetTime);
+ }}
+ >
+
+
+
0)
+ ? tracksRef.current.clientHeight
+ : 0,
+ top: '50%',
+ left: 'calc(50% - 0.5px)'
+ }} />
+
+
+
+
+
+
+ {
+ commonStore.setTrackScale(data.value);
+ }}
+ />
+
+
+
+ {commonStore.tracks.map(track =>
+
+
+ } size="small" shape="circular"
+ appearance="subtle" />
+ } size="small" shape="circular"
+ appearance="subtle" />
+ } size="small" shape="circular"
+ appearance="subtle" onClick={() => {
+ const tracks = commonStore.tracks.slice().filter(t => t.id !== track.id);
+ commonStore.setTracks(tracks);
+ }} />
+
+
+
)}
+
+ } size="small" shape="circular"
+ appearance="subtle"
+ onClick={() => {
+ commonStore.setTracks([...commonStore.tracks, {
+ id: uuid(),
+ content: '',
+ offsetTime: 0,
+ contentTime: 0
+ }]);
+ }}>
+ {t('New Track')}
+
+
+ {t('Select a track to preview the content')}
+
+
+
+
+ {selectedTrack &&
+
+
+
{`${t('Start Time')}: ${selectedTrack.offsetTime} ms`}
+
{`${t('Content Time')}: ${selectedTrack.contentTime} ms`}
+
+ {selectedTrack.content}
+
+
+
+ }
+
} style={{ minHeight: '32px' }}>
+ {t('Save to generation area')}
+
+
+ );
+});
+
+export default AudiotrackEditor;
diff --git a/frontend/src/pages/Composition.tsx b/frontend/src/pages/Composition.tsx
index 28eee7a..150336e 100644
--- a/frontend/src/pages/Composition.tsx
+++ b/frontend/src/pages/Composition.tsx
@@ -2,7 +2,7 @@ import 'html-midi-player';
import React, { FC, useEffect, useRef } from 'react';
import { observer } from 'mobx-react-lite';
import { WorkHeader } from '../components/WorkHeader';
-import { Button, Checkbox, Textarea } from '@fluentui/react-components';
+import { Button, Checkbox, Dropdown, Option, Textarea } from '@fluentui/react-components';
import { Labeled } from '../components/Labeled';
import { ValuedSlider } from '../components/ValuedSlider';
import { useTranslation } from 'react-i18next';
@@ -20,6 +20,7 @@ import { FileExists, OpenFileFolder, OpenSaveFileDialogBytes } from '../../wails
import { getServerRoot, toastWithButton } from '../utils';
import { CompositionParams } from '../types/composition';
import { useMediaQuery } from 'usehooks-ts';
+import { AudiotrackButton } from './AudiotrackManager/AudiotrackButton';
let compositionSseController: AbortController | null = null;
@@ -267,6 +268,18 @@ const CompositionPanel: FC = observer(() => {
autoPlay: data.checked as boolean
});
}} />
+ {commonStore.platform !== 'web' &&
+
+
+
+
+
+
+ } />
+ }
} onClick={() => {
diff --git a/frontend/src/stores/commonStore.ts b/frontend/src/stores/commonStore.ts
index 229d0b8..c7f6983 100644
--- a/frontend/src/stores/commonStore.ts
+++ b/frontend/src/stores/commonStore.ts
@@ -9,7 +9,7 @@ import { Preset } from '../types/presets';
import { AboutContent } from '../types/about';
import { Attachment, ChatParams, Conversation } from '../types/chat';
import { CompletionPreset } from '../types/completion';
-import { CompositionParams } from '../types/composition';
+import { CompositionParams, Track } from '../types/composition';
import { ModelConfig } from '../types/configs';
import { DownloadStatus } from '../types/downloads';
import { IntroductionContent } from '../types/home';
@@ -90,6 +90,11 @@ class CommonStore {
};
compositionGenerating: boolean = false;
compositionSubmittedPrompt: string = defaultCompositionPrompt;
+ tracks: Track[] = [];
+ trackScale: number = 1;
+ trackTotalTime: number = 5000;
+ trackCurrentTime: number = 0;
+ trackPlayStartTime: number = 0;
// configs
currentModelConfigIndex: number = 0;
modelConfigs: ModelConfig[] = [];
@@ -380,6 +385,26 @@ class CommonStore {
setSidePanelCollapsed(value: boolean | 'auto') {
this.sidePanelCollapsed = value;
}
+
+ setTracks(value: Track[]) {
+ this.tracks = value;
+ }
+
+ setTrackScale(value: number) {
+ this.trackScale = value;
+ }
+
+ setTrackTotalTime(value: number) {
+ this.trackTotalTime = value;
+ }
+
+ setTrackCurrentTime(value: number) {
+ this.trackCurrentTime = value;
+ }
+
+ setTrackPlayStartTime(value: number) {
+ this.trackPlayStartTime = value;
+ }
}
export default new CommonStore();
\ No newline at end of file
diff --git a/frontend/src/types/composition.ts b/frontend/src/types/composition.ts
index dd3beca..45bf7d0 100644
--- a/frontend/src/types/composition.ts
+++ b/frontend/src/types/composition.ts
@@ -9,4 +9,10 @@ export type CompositionParams = {
useLocalSoundFont: boolean,
midi: ArrayBuffer | null,
ns: NoteSequence | null
-}
\ No newline at end of file
+}
+export type Track = {
+ id: string;
+ content: string;
+ offsetTime: number;
+ contentTime: number;
+};
\ No newline at end of file
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 8be9b1a..fbbffcd 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -21,6 +21,7 @@ const embedded = [
// dependencies that exist in single component
'react-beautiful-dnd',
+ 'react-draggable',
'@magenta/music', 'html-midi-player',
'react-markdown', 'rehype-highlight', 'rehype-raw', 'remark-breaks', 'remark-gfm'
];