diff --git a/web/libs/editor/src/components/Timeline/Controls/AudioControl.scss b/web/libs/editor/src/components/Timeline/Controls/AudioControl.scss index d7b863036efe..36a8c2f0cb9a 100644 --- a/web/libs/editor/src/components/Timeline/Controls/AudioControl.scss +++ b/web/libs/editor/src/components/Timeline/Controls/AudioControl.scss @@ -23,7 +23,7 @@ margin-top: 10px; padding: 2px 0; border-top: 1px solid var(--sand_200); - font-size: 16px; + font-size: 14px; cursor: pointer; } diff --git a/web/libs/editor/src/components/Timeline/Controls/ConfigControl.scss b/web/libs/editor/src/components/Timeline/Controls/ConfigControl.scss index 913ebf593589..0002257a9ca8 100644 --- a/web/libs/editor/src/components/Timeline/Controls/ConfigControl.scss +++ b/web/libs/editor/src/components/Timeline/Controls/ConfigControl.scss @@ -5,14 +5,19 @@ left: 0; top: 36px; position: absolute; - width: 232px; + width: 260px; background: var(--sand_0); border-radius: 4px; - padding: 22px 0 0; + padding: 12px 0 0; box-shadow: 0 4px 10px 0 var(--sand_300); z-index: 10; } + &__toggle { + padding: 8px 16px; + font-size: 12px; + } + &__range { margin: 0 20px; width: calc(100% - 40px); @@ -23,7 +28,7 @@ margin-top: 10px; padding: 2px 0; border-top: 1px solid var(--sand_200); - font-size: 16px; + font-size: 14px; cursor: pointer; } diff --git a/web/libs/editor/src/components/Timeline/Controls/ConfigControl.tsx b/web/libs/editor/src/components/Timeline/Controls/ConfigControl.tsx index 1f3aae1ed1fd..11efb9cf98e2 100644 --- a/web/libs/editor/src/components/Timeline/Controls/ConfigControl.tsx +++ b/web/libs/editor/src/components/Timeline/Controls/ConfigControl.tsx @@ -1,11 +1,13 @@ import type React from "react"; -import { type FC, type MouseEvent, useEffect, useState } from "react"; +import { type FC, type MouseEvent, useContext, useEffect, useState } from "react"; +import { Toggle } from "@humansignal/ui"; import { Block, Elem } from "../../../utils/bem"; -import "./ConfigControl.scss"; import { IconConfig } from "../../../assets/icons/timeline"; +import { TimelineContext } from "../Context"; import { ControlButton } from "../Controls"; import { Slider } from "./Slider"; +import "./ConfigControl.scss"; const MAX_SPEED = 2.5; const MAX_ZOOM = 150; @@ -36,6 +38,7 @@ export const ConfigControl: FC = ({ const playbackSpeed = speed ?? 1; const [isTimeline, setTimeline] = useState(true); const [isAudioWave, setAudioWave] = useState(true); + const { settings, changeSetting } = useContext(TimelineContext); useEffect(() => { if (layerVisibility) { @@ -105,6 +108,24 @@ export const ConfigControl: FC = ({ info={"Increase or decrease the appearance of amplitude"} onChange={handleChangeAmp} /> + + changeSetting?.("loopRegion", e.target.checked)} + label="Loop Regions" + // there are no "normal" size, so that's the hack to reset size + labelProps={{ size: "normal" }} + /> + + + changeSetting?.("autoPlayNewSegments", e.target.checked)} + label="Auto-play New Regions" + // there are no "normal" size, so that's the hack to reset size + labelProps={{ size: "normal" }} + /> + {renderLayerToggles()} ); diff --git a/web/libs/editor/src/components/Timeline/Controls/Info.scss b/web/libs/editor/src/components/Timeline/Controls/Info.scss deleted file mode 100644 index 0b9b20f36e59..000000000000 --- a/web/libs/editor/src/components/Timeline/Controls/Info.scss +++ /dev/null @@ -1,28 +0,0 @@ -.control-info { - position: relative; - margin-left: 7px; - height: 14px; - width: 14px; - - &__tooltip { - transition: opacity 0.2s ease-out; - background: var(--sand_900); - padding: 4px 16px; - width: 220px; - height: auto; - border-radius: 4px; - color: var(--sand_0); - font-size: 16px; - position: absolute; - left: 50%; - transform: translateX(-50%); - visibility: hidden; - opacity: 0; - z-index: 2; - } - - &:hover &__tooltip { - visibility: visible; - opacity: 1; - } -} \ No newline at end of file diff --git a/web/libs/editor/src/components/Timeline/Controls/Info.tsx b/web/libs/editor/src/components/Timeline/Controls/Info.tsx deleted file mode 100644 index 36e1323fd5da..000000000000 --- a/web/libs/editor/src/components/Timeline/Controls/Info.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { FC } from "react"; -import { Block, Elem } from "../../../utils/bem"; - -import "./Info.scss"; -import { IconInfoConfig } from "../../../assets/icons/timeline"; - -export interface InfoProps { - text: string; -} - -export const Info: FC = ({ text }) => { - return ( - - - {text} - - ); -}; diff --git a/web/libs/editor/src/components/Timeline/Controls/Slider.scss b/web/libs/editor/src/components/Timeline/Controls/Slider.scss index 41d8be5551ad..a52085372297 100644 --- a/web/libs/editor/src/components/Timeline/Controls/Slider.scss +++ b/web/libs/editor/src/components/Timeline/Controls/Slider.scss @@ -19,17 +19,22 @@ } &__control { - padding: 0 16px; - margin-top: 10px; + padding: 4px 16px 8px; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; - font-size: 11px; + font-size: 14px; } &__info { display: flex; + align-items: center; + gap: 8px; + + svg { + display: block; + } } &__input { diff --git a/web/libs/editor/src/components/Timeline/Controls/Slider.tsx b/web/libs/editor/src/components/Timeline/Controls/Slider.tsx index d2b4e93f1885..c1db9fb7c640 100644 --- a/web/libs/editor/src/components/Timeline/Controls/Slider.tsx +++ b/web/libs/editor/src/components/Timeline/Controls/Slider.tsx @@ -1,9 +1,10 @@ import type React from "react"; import { type FC, useEffect, useRef, useState } from "react"; +import { IconInfoConfig } from "../../../assets/icons/timeline"; +import { Tooltip } from "../../../common/Tooltip/Tooltip"; import { Block, Elem } from "../../../utils/bem"; import "./Slider.scss"; -import { Info } from "./Info"; export interface SliderProps { description?: string; @@ -59,7 +60,11 @@ export const Slider: FC = ({ description, info, max, min, value, st {description} - {info && } + {info && ( + + + + )} void; data?: any; } @@ -131,6 +132,8 @@ export type TimelineSettings = { fastTravelSize?: TimelineStepFunction; stepSize?: TimelineStepFunction; leftOffset?: number; + loopRegion?: boolean; + autoPlayNewSegments?: boolean; }; export type TimelineStepFunction = ( diff --git a/web/libs/editor/src/lib/AudioUltra/Controls/Player.ts b/web/libs/editor/src/lib/AudioUltra/Controls/Player.ts index 0e04d23f1505..8eca2a208e12 100644 --- a/web/libs/editor/src/lib/AudioUltra/Controls/Player.ts +++ b/web/libs/editor/src/lib/AudioUltra/Controls/Player.ts @@ -253,6 +253,7 @@ export abstract class Player extends Destructable { protected abstract playAudio(start?: number, duration?: number): void; + // This function just sets up the playing, but doesn't actually play protected playSelection(from?: number, to?: number) { const selected = this.wf.regions.selected; @@ -262,7 +263,11 @@ export abstract class Player extends Destructable { const regionsStart = Math.min(...selected.map((r) => r.start)); const regionsEnd = Math.max(...selected.map((r) => r.end)); - const start = clamp(this.currentTime, regionsStart, regionsEnd); + // if we are outside of the selected region, start at the beginning + let start = this.currentTime; + if (start < regionsStart || start >= regionsEnd) { + start = regionsStart; + } this.loop = { start: regionsStart, end: regionsEnd }; @@ -320,9 +325,13 @@ export abstract class Player extends Destructable { protected updateLoop(time: number) { if (this.isDestroyed || !this.loop) return; if (time >= this.loop.end) { - this.currentTime = this.loop.start; - this.playing = false; - this.play(); + if (this.wf.settings.loopRegion) { + this.currentTime = this.loop.start; + this.playing = false; + this.play(); + } else { + this.pause(); + } } } diff --git a/web/libs/editor/src/lib/AudioUltra/Regions/Regions.ts b/web/libs/editor/src/lib/AudioUltra/Regions/Regions.ts index fb01e21f5223..f3d648069d42 100644 --- a/web/libs/editor/src/lib/AudioUltra/Regions/Regions.ts +++ b/web/libs/editor/src/lib/AudioUltra/Regions/Regions.ts @@ -309,7 +309,10 @@ export class Regions { const addRegion = () => { const { container, zoomedWidth, fullWidth } = this.visualizer; - const { autoPlayNewSegments, duration } = this.waveform; + const { + settings: { autoPlayNewSegments }, + duration, + } = this.waveform; const scrollLeft = this.visualizer.getScrollLeftPx(); startX = clamp(getCursorPositionX(e, container) + scrollLeft, 0, fullWidth); @@ -352,7 +355,10 @@ export class Regions { }; const handleMouseUp = () => { - const { player, autoPlayNewSegments } = this.waveform; + const { + player, + settings: { autoPlayNewSegments }, + } = this.waveform; document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); diff --git a/web/libs/editor/src/lib/AudioUltra/Waveform.ts b/web/libs/editor/src/lib/AudioUltra/Waveform.ts index 410bbaa14fd8..960cc00a1e32 100644 --- a/web/libs/editor/src/lib/AudioUltra/Waveform.ts +++ b/web/libs/editor/src/lib/AudioUltra/Waveform.ts @@ -1,3 +1,4 @@ +import type { TimelineSettings } from "../../components/Timeline/Types"; import { Events } from "./Common/Events"; import { MediaLoader } from "./Media/MediaLoader"; import type { Player } from "./Controls/Player"; @@ -138,8 +139,6 @@ export interface WaveformOptions { padding?: Padding; - autoPlayNewSegments?: boolean; - // Cursor options cursor?: CursorOptions; @@ -201,7 +200,9 @@ export class Waveform extends Events { regions!: Regions; loaded = false; renderedChannels = false; - autoPlayNewSegments = false; + // for now that's just an object to store setting and access them when needed; + // but if we need to react on changes we can convert it into getter/setter. + settings: TimelineSettings = {}; constructor(params: WaveformOptions) { super(); @@ -254,8 +255,6 @@ export class Waveform extends Events { this.visualizer, ); - this.autoPlayNewSegments = this.params.autoPlayNewSegments ?? this.autoPlayNewSegments; - this.player = this.params.playerType === "html5" ? new Html5Player(this) : new WebAudioPlayer(this); this.initEvents(); diff --git a/web/libs/editor/src/lib/AudioUltra/react/index.ts b/web/libs/editor/src/lib/AudioUltra/react/index.ts index 4bbee27929fa..516355bdd6a6 100644 --- a/web/libs/editor/src/lib/AudioUltra/react/index.ts +++ b/web/libs/editor/src/lib/AudioUltra/react/index.ts @@ -1,5 +1,6 @@ -import { type MutableRefObject, useEffect, useMemo, useRef, useState } from "react"; +import { type MutableRefObject, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { TimelineContext } from "../../../components/Timeline/Context"; import { isTimeRelativelySimilar } from "../Common/Utils"; import type { Layer } from "../Visual/Layer"; import { Waveform, type WaveformFrameState, type WaveformOptions } from "../Waveform"; @@ -30,6 +31,12 @@ export const useWaveform = ( const [layers, setLayers] = useState([]); const [layerVisibility, setLayerVisibility] = useState(new Map()); + const { settings } = useContext(TimelineContext); + useEffect(() => { + if (!waveform.current || !settings) return; + waveform.current.settings = settings; + }, [settings]); + const onFrameChangedRef = useRef(options?.onFrameChanged); onFrameChangedRef.current = options?.onFrameChanged; diff --git a/web/libs/editor/src/tags/object/AudioUltra/view.tsx b/web/libs/editor/src/tags/object/AudioUltra/view.tsx index 7d86fdd6eade..76407ba3e609 100644 --- a/web/libs/editor/src/tags/object/AudioUltra/view.tsx +++ b/web/libs/editor/src/tags/object/AudioUltra/view.tsx @@ -1,5 +1,5 @@ import { observer } from "mobx-react"; -import { type FC, useEffect, useMemo, useRef } from "react"; +import { type FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { TimelineContextProvider } from "../../../components/Timeline/Context"; import { Hotkey } from "../../../core/Hotkey"; import { useWaveform } from "../../../lib/AudioUltra/react"; @@ -58,7 +58,6 @@ const AudioUltraView: FC = ({ item }) => { backgroundCompute: true, denoize: true, }, - autoPlayNewSegments: true, onFrameChanged: (frameState) => { item.setWFFrame(frameState); }, @@ -153,22 +152,6 @@ const AudioUltraView: FC = ({ item }) => { }; }, []); - const contextValue = useMemo(() => { - return { - position: 0, - length: 0, - regions: [], - step: 10, - playing: false, - visibleWidth: 0, - seekOffset: 0, - data: undefined, - settings: { - playpauseHotkey: "audio:playpause", - }, - }; - }, []); - return ( {item.errors?.map((error: any, i: any) => ( @@ -180,49 +163,80 @@ const AudioUltraView: FC = ({ item }) => { item.stageRef.current = el; }} /> - - controls.setPlaying(true)} - onPause={() => controls.setPlaying(false)} - allowFullscreen={false} - onVolumeChange={(vol) => controls.setVolume(vol)} - onStepBackward={() => { - waveform.current?.seekBackward(NORMALIZED_STEP); - waveform.current?.syncCursor(); - }} - onStepForward={() => { - waveform.current?.seekForward(NORMALIZED_STEP); - waveform.current?.syncCursor(); - }} - onPositionChange={(pos) => { - waveform.current?.seek(pos); - waveform.current?.syncCursor(); - }} - onSpeedChange={(speed) => controls.setRate(speed)} - onZoom={(zoom) => controls.setZoom(zoom)} - amp={controls.amp} - onAmpChange={(amp) => controls.setAmp(amp)} - mediaType="audio" - toggleVisibility={(layerName: string, isVisible: boolean) => { - if (waveform.current) { - const layer = waveform.current?.getLayer(layerName); - - if (layer) { - layer.setVisibility(isVisible); - } + controls.setPlaying(true)} + onPause={() => controls.setPlaying(false)} + allowFullscreen={false} + onVolumeChange={(vol) => controls.setVolume(vol)} + onStepBackward={() => { + waveform.current?.seekBackward(NORMALIZED_STEP); + waveform.current?.syncCursor(); + }} + onStepForward={() => { + waveform.current?.seekForward(NORMALIZED_STEP); + waveform.current?.syncCursor(); + }} + onPositionChange={(pos) => { + waveform.current?.seek(pos); + waveform.current?.syncCursor(); + }} + onSpeedChange={(speed) => controls.setRate(speed)} + onZoom={(zoom) => controls.setZoom(zoom)} + amp={controls.amp} + onAmpChange={(amp) => controls.setAmp(amp)} + mediaType="audio" + toggleVisibility={(layerName: string, isVisible: boolean) => { + if (waveform.current) { + const layer = waveform.current?.getLayer(layerName); + + if (layer) { + layer.setVisibility(isVisible); } - }} - layerVisibility={controls.layerVisibility} - /> - + } + }} + layerVisibility={controls.layerVisibility} + /> ); }; -export const AudioUltra = observer(AudioUltraView); +const AudioUltraWithSettings: FC = ({ item }) => { + const [settings, setSettings] = useState({ + playpauseHotkey: "audio:playpause", + loopRegion: false, + autoPlayNewSegments: true, + }); + const changeSetting = useCallback((key: string, value: any) => { + setSettings((prev) => ({ ...prev, [key]: value })); + }, []); + + // @todo seems like this context is not used at all; and its values are static; better to check and remove + const contextValue = useMemo(() => { + return { + position: 0, + length: 0, + regions: [], + step: 10, + playing: false, + visibleWidth: 0, + seekOffset: 0, + data: undefined, + settings, + changeSetting, + }; + }, [settings]); + + return ( + + + + ); +}; + +export const AudioUltra = observer(AudioUltraWithSettings); diff --git a/web/libs/ui/src/lib/label/label.module.scss b/web/libs/ui/src/lib/label/label.module.scss index dc9a6bb15196..63c8ed20f316 100644 --- a/web/libs/ui/src/lib/label/label.module.scss +++ b/web/libs/ui/src/lib/label/label.module.scss @@ -67,7 +67,6 @@ &_placement_right &__text, &_placement_left &__text { margin-bottom: 0; - font-size: 16px; line-height: 22px; height: auto; align-items: center; diff --git a/web/libs/ui/src/lib/label/label.tsx b/web/libs/ui/src/lib/label/label.tsx index 013cf840a420..1ccce9bed380 100644 --- a/web/libs/ui/src/lib/label/label.tsx +++ b/web/libs/ui/src/lib/label/label.tsx @@ -3,7 +3,7 @@ import clsx from "clsx"; import styles from "./label.module.scss"; type LabelProps = PropsWithChildren<{ text: string; - required?: false; + required?: boolean; placement?: "right" | "left"; description?: string; size?: "large" | "small"; diff --git a/web/libs/ui/src/lib/toggle/toggle.tsx b/web/libs/ui/src/lib/toggle/toggle.tsx index a7fa8bfa9501..c3d1b5a90cb3 100644 --- a/web/libs/ui/src/lib/toggle/toggle.tsx +++ b/web/libs/ui/src/lib/toggle/toggle.tsx @@ -6,18 +6,18 @@ import styles from "./toggle.module.scss"; type ToggleProps = { className?: string; label?: string; - labelProps: any; + labelProps?: Partial>; description?: string; checked?: boolean; defaultChecked?: boolean; onChange: (e: React.ChangeEvent) => void; required?: boolean; - style: any; + style?: React.CSSProperties; disabled?: boolean; alwaysBlue?: boolean; }; -export const Toggle = forwardRef( +export const Toggle = forwardRef( ( { className, @@ -31,7 +31,7 @@ export const Toggle = forwardRef( style, alwaysBlue, ...props - }: ToggleProps, + }, ref, ) => { const initialChecked = useMemo(() => defaultChecked ?? checked ?? false, [defaultChecked, checked]);