This commit is contained in:
André Michelle
2026-02-25 14:10:39 +01:00
parent a511b79943
commit b2969b5f85
8 changed files with 36 additions and 24 deletions
@@ -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())
@@ -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}
@@ -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
@@ -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)
@@ -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()
@@ -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)
@@ -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)
@@ -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)
}
})
}