From b2969b5f85fdec8909e7d9975cf1fd99ead804d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Michelle?= Date: Wed, 25 Feb 2026 14:10:39 +0100 Subject: [PATCH] fixes #190 --- .../src/ui/header/MusicalUnitDisplay.tsx | 8 +++----- .../app/studio/src/ui/timeline/TimeAxis.tsx | 2 +- packages/app/studio/src/video/VideoOverlay.ts | 9 +++++---- .../studio/src/video/VideoOverlayPreview.tsx | 3 ++- .../app/studio/src/video/VideoRenderer.ts | 7 +++++-- .../src/timeline/SignatureTrackAdapter.ts | 6 ++++++ .../src/project/audio/AudioContentHelpers.ts | 6 +++--- .../src/project/audio/AudioContentModifier.ts | 19 +++++++++++-------- 8 files changed, 36 insertions(+), 24 deletions(-) diff --git a/packages/app/studio/src/ui/header/MusicalUnitDisplay.tsx b/packages/app/studio/src/ui/header/MusicalUnitDisplay.tsx index 5df4bdea..293d6cab 100644 --- a/packages/app/studio/src/ui/header/MusicalUnitDisplay.tsx +++ b/packages/app/studio/src/ui/header/MusicalUnitDisplay.tsx @@ -2,7 +2,6 @@ import css from "./MusicalUnitDisplay.sass?inline" import {Html} from "@opendaw/lib-dom" import {DefaultObservableValue, Lifecycle} from "@opendaw/lib-std" import {createElement} from "@opendaw/lib-jsx" -import {PPQN} from "@opendaw/lib-dsp" import {UnitDisplay} from "@/ui/header/UnitDisplay" import {StudioService} from "@/service/StudioService" import {StudioPreferences} from "@opendaw/studio-core" @@ -30,11 +29,10 @@ export const MusicalUnitDisplay = ({lifecycle, service}: Construct) => { lifecycle.ownAll( service.engine.position.catchupAndSubscribe(owner => { const position = owner.getValue() - const [nominator, denominator] = service.projectProfileService.getValue().match({ - some: ({project}) => project.timelineBoxAdapter.signatureTrack.signatureAt(position), - none: () => [4, 4] + const {bars, beats, semiquavers, ticks} = service.projectProfileService.getValue().match({ + some: ({project}) => project.timelineBoxAdapter.signatureTrack.toParts(position), + none: () => ({bars: 0, beats: 0, semiquavers: 0, ticks: 0}) }) - const {bars, beats, semiquavers, ticks} = PPQN.toParts(Math.abs(position), nominator, denominator) barUnitString.setValue((bars + 1).toString().padStart(3, "0")) beatUnitString.setValue((beats + 1).toString()) semiquaverUnitString.setValue((semiquavers + 1).toString()) diff --git a/packages/app/studio/src/ui/timeline/TimeAxis.tsx b/packages/app/studio/src/ui/timeline/TimeAxis.tsx index 828935a2..8c6d0cce 100644 --- a/packages/app/studio/src/ui/timeline/TimeAxis.tsx +++ b/packages/app/studio/src/ui/timeline/TimeAxis.tsx @@ -131,7 +131,7 @@ export const TimeAxis = ({lifecycle, service, snapping, range, mapper}: Construc return resolvers }} provider={() => ({ unit: "bars", - value: (PPQN.toParts(durationInPulses.getValue()).bars + 1).toString() + value: (signatureTrack.toParts(durationInPulses.getValue()).bars + 1).toString() })} location={() => { const rect = endMarkerElement.getBoundingClientRect() return {x: rect.left - 32, y: rect.top} diff --git a/packages/app/studio/src/video/VideoOverlay.ts b/packages/app/studio/src/video/VideoOverlay.ts index fbc3335b..482d1e38 100644 --- a/packages/app/studio/src/video/VideoOverlay.ts +++ b/packages/app/studio/src/video/VideoOverlay.ts @@ -1,10 +1,11 @@ -import {Terminable} from "@opendaw/lib-std" -import {PPQN, ppqn} from "@opendaw/lib-dsp" +import {int, Terminable} from "@opendaw/lib-std" +import {ppqn} from "@opendaw/lib-dsp" export interface VideoOverlayConfig { readonly width: number readonly height: number readonly projectName: string + readonly toParts: (position: ppqn) => { bars: int, beats: int, semiquavers: int, ticks: int } } export class VideoOverlay implements Terminable { @@ -66,8 +67,8 @@ export class VideoOverlay implements Terminable { #renderPosition(position: ppqn, fontSize: number): void { const ctx = this.#ctx - const {width, height, projectName} = this.#config - const {bars, beats, semiquavers, ticks} = PPQN.toParts(Math.abs(position) | 0) + const {width, height, projectName, toParts} = this.#config + const {bars, beats, semiquavers, ticks} = toParts(position) const positionFontSize = Math.round(fontSize * 0.7) const charWidth = positionFontSize * 0.65 const separatorWidth = positionFontSize * 0.4 diff --git a/packages/app/studio/src/video/VideoOverlayPreview.tsx b/packages/app/studio/src/video/VideoOverlayPreview.tsx index 40be8315..cfc65fac 100644 --- a/packages/app/studio/src/video/VideoOverlayPreview.tsx +++ b/packages/app/studio/src/video/VideoOverlayPreview.tsx @@ -34,7 +34,8 @@ export const VideoOverlayPreview = ({lifecycle}: Construct) => { const overlay = await VideoOverlay.create({ width: WIDTH, height: HEIGHT, - projectName: "Dub Techno" + projectName: "Dub Techno", + toParts: (position) => PPQN.toParts(Math.abs(position) | 0) }) lifecycle.own(overlay) diff --git a/packages/app/studio/src/video/VideoRenderer.ts b/packages/app/studio/src/video/VideoRenderer.ts index cf776deb..937a460e 100644 --- a/packages/app/studio/src/video/VideoRenderer.ts +++ b/packages/app/studio/src/video/VideoRenderer.ts @@ -1,5 +1,5 @@ import {asInstanceOf, DefaultObservableValue, Option, panic, RuntimeNotifier, TimeSpan} from "@opendaw/lib-std" -import {dbToGain, RenderQuantum} from "@opendaw/lib-dsp" +import {dbToGain, ppqn, RenderQuantum} from "@opendaw/lib-dsp" import {OfflineEngineRenderer, Project} from "@opendaw/studio-core" import {Files} from "@opendaw/lib-dom" import {ShadertoyState} from "@/ui/shadertoy/ShadertoyState" @@ -60,7 +60,10 @@ export namespace VideoRenderer { } const compositionCanvas = new OffscreenCanvas(width, height) const compositionCtx = compositionCanvas.getContext("2d")! - const overlay = await VideoOverlay.create({width, height, projectName}) + const overlay = await VideoOverlay.create({ + width, height, projectName, + toParts: (position: ppqn) => project.timelineBoxAdapter.signatureTrack.toParts(position) + }) const renderer = await OfflineEngineRenderer.create(project, Option.None, sampleRate) renderer.play() diff --git a/packages/studio/adapters/src/timeline/SignatureTrackAdapter.ts b/packages/studio/adapters/src/timeline/SignatureTrackAdapter.ts index 2606217b..85868e16 100644 --- a/packages/studio/adapters/src/timeline/SignatureTrackAdapter.ts +++ b/packages/studio/adapters/src/timeline/SignatureTrackAdapter.ts @@ -279,6 +279,12 @@ export class SignatureTrackAdapter implements Terminable { return event.accumulatedPpqn + barsFromEvent * barPpqn } + toParts(position: ppqn): { bars: int, beats: int, semiquavers: int, ticks: int } { + const {event} = this.#findSignatureEventAt(position) + const parts = PPQN.toParts(position - event.accumulatedPpqn, event.nominator, event.denominator) + return {...parts, bars: parts.bars + event.accumulatedBars} + } + barLengthAt(position: ppqn): ppqn { const [nom, denom] = this.signatureAt(position) return PPQN.fromSignature(nom, denom) diff --git a/packages/studio/core/src/project/audio/AudioContentHelpers.ts b/packages/studio/core/src/project/audio/AudioContentHelpers.ts index a2665c57..31352fc6 100644 --- a/packages/studio/core/src/project/audio/AudioContentHelpers.ts +++ b/packages/studio/core/src/project/audio/AudioContentHelpers.ts @@ -1,6 +1,6 @@ import {BoxGraph} from "@opendaw/lib-box" import {AudioPitchStretchBox, AudioTimeStretchBox, WarpMarkerBox} from "@opendaw/studio-boxes" -import {ppqn} from "@opendaw/lib-dsp" +import {ppqn, seconds} from "@opendaw/lib-dsp" import {UUID} from "@opendaw/lib-std" import {WarpMarkerTemplate} from "./WarpMarkerTemplate" @@ -8,11 +8,11 @@ export namespace AudioContentHelpers { export const addDefaultWarpMarkers = (boxGraph: BoxGraph, playMode: AudioPitchStretchBox | AudioTimeStretchBox, durationInPPQN: ppqn, - durationInSeconds: number) => { + durationInSeconds: seconds) => { WarpMarkerBox.create(boxGraph, UUID.generate(), box => { box.owner.refer(playMode.warpMarkers) box.position.setValue(0) - box.seconds.setValue(0) + box.seconds.setValue(0.0) }) WarpMarkerBox.create(boxGraph, UUID.generate(), box => { box.owner.refer(playMode.warpMarkers) diff --git a/packages/studio/core/src/project/audio/AudioContentModifier.ts b/packages/studio/core/src/project/audio/AudioContentModifier.ts index e1a0bd4f..5e5c3142 100644 --- a/packages/studio/core/src/project/audio/AudioContentModifier.ts +++ b/packages/studio/core/src/project/audio/AudioContentModifier.ts @@ -1,5 +1,5 @@ import {EmptyExec, Exec, isDefined, isInstanceOf, RuntimeNotifier, UUID} from "@opendaw/lib-std" -import {TimeBase} from "@opendaw/lib-dsp" +import {seconds, TimeBase} from "@opendaw/lib-dsp" import { AudioPitchStretchBox, AudioRegionBox, @@ -17,6 +17,8 @@ export namespace AudioContentModifier { const audioAdapters = adapters.filter(adapter => !adapter.isPlayModeNoStretch) if (audioAdapters.length === 0) {return EmptyExec} return () => audioAdapters.forEach((adapter) => { + const audibleDuration = adapter.optWarpMarkers + .mapOr(warpMarkers => warpMarkers.last()?.seconds ?? 0, 0) adapter.box.playMode.defer() adapter.asPlayModeTimeStretch.ifSome(({box}) => { if (box.pointerHub.filter(Pointers.AudioPlayMode).length === 0) {box.delete()} @@ -24,7 +26,7 @@ export namespace AudioContentModifier { adapter.asPlayModePitchStretch.ifSome(({box}) => { if (box.pointerHub.filter(Pointers.AudioPlayMode).length === 0) {box.delete()} }) - switchTimeBaseToSeconds(adapter) + switchTimeBaseToSeconds(adapter, audibleDuration) }) } @@ -52,7 +54,8 @@ export namespace AudioContentModifier { })) } } else { - AudioContentHelpers.addDefaultWarpMarkers(boxGraph, pitchStretch, adapter.duration, adapter.file.endInSeconds) + AudioContentHelpers.addDefaultWarpMarkers(boxGraph, pitchStretch, + adapter.duration, adapter.box.duration.getValue()) } switchTimeBaseToMusical(adapter) }) @@ -93,7 +96,8 @@ export namespace AudioContentModifier { })) } } else { - AudioContentHelpers.addDefaultWarpMarkers(boxGraph, timeStretch, adapter.duration, adapter.file.endInSeconds) + AudioContentHelpers.addDefaultWarpMarkers(boxGraph, timeStretch, + adapter.duration, adapter.box.duration.getValue()) } if (isDefined(transients) && adapter.file.transients.length() === 0) { const markersField = adapter.file.box.transientMarkers @@ -106,15 +110,14 @@ export namespace AudioContentModifier { }) } - const switchTimeBaseToSeconds = ({box, file, timeBase}: AudioContentBoxAdapter): void => { + const switchTimeBaseToSeconds = ({box, timeBase}: AudioContentBoxAdapter, audibleDuration: seconds): void => { if (timeBase === TimeBase.Seconds) {return} - // Reset to 100% playback speed (original file speed) box.timeBase.setValue(TimeBase.Seconds) - box.duration.setValue(file.endInSeconds) + box.duration.setValue(audibleDuration) box.accept({ visitAudioRegionBox: (box: AudioRegionBox) => { box.loopOffset.setValue(0) - box.loopDuration.setValue(file.endInSeconds) + box.loopDuration.setValue(audibleDuration) } }) }