Add new version 0.3.8 and archive old version 0.3.6 of Time4Midi
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
|
|||||||
|
directories:
|
||||||
|
output: dist-033-diagnostics3
|
||||||
|
buildResources: build
|
||||||
|
appId: com.jhartworks.time4midi
|
||||||
|
productName: Time4Midi
|
||||||
|
files:
|
||||||
|
- filter:
|
||||||
|
- main.js
|
||||||
|
- preload.js
|
||||||
|
- src/**/*
|
||||||
|
- JHArtworks2023_wh.png
|
||||||
|
- time4midi.png
|
||||||
|
- package.json
|
||||||
|
npmRebuild: false
|
||||||
|
win:
|
||||||
|
icon: time4midi.ico
|
||||||
|
signAndEditExecutable: false
|
||||||
|
target:
|
||||||
|
- target: portable
|
||||||
|
arch:
|
||||||
|
- x64
|
||||||
|
electronVersion: 42.4.0
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
Copyright (c) Electron contributors
|
||||||
|
Copyright (c) 2013-2020 GitHub Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"file_format_version": "1.0.0", "ICD": {"library_path": ".\\vk_swiftshader.dll", "api_version": "1.0.5"}}
|
||||||
Binary file not shown.
Vendored
BIN
Binary file not shown.
+441
-24
@@ -12,6 +12,10 @@
|
|||||||
const EXTERNAL_IDLE_MS = 1500;
|
const EXTERNAL_IDLE_MS = 1500;
|
||||||
const AUDIO_WAVEFORM_SAMPLES = 720;
|
const AUDIO_WAVEFORM_SAMPLES = 720;
|
||||||
const AUDIO_SYNC_TOLERANCE = 0.08;
|
const AUDIO_SYNC_TOLERANCE = 0.08;
|
||||||
|
const AUDIO_SYNC_TOLERANCE_PLAYING = 0.22;
|
||||||
|
const AUDIO_SYNC_THROTTLE_MS = 220;
|
||||||
|
const AUDIO_VOLUME_THROTTLE_MS = 33;
|
||||||
|
const AUDIO_VOLUME_EPSILON = 0.002;
|
||||||
const AUDIO_SNAP_GRID_OPTIONS = ["0.25", "0.5", "1", "2", "4", "8"];
|
const AUDIO_SNAP_GRID_OPTIONS = ["0.25", "0.5", "1", "2", "4", "8"];
|
||||||
const AUDIO_SNAP_LINE_WIDTH_OPTIONS = ["1", "2", "3", "4"];
|
const AUDIO_SNAP_LINE_WIDTH_OPTIONS = ["1", "2", "3", "4"];
|
||||||
const KNX_DPT_OPTIONS = [
|
const KNX_DPT_OPTIONS = [
|
||||||
@@ -338,6 +342,11 @@
|
|||||||
audioOutputDevices: [],
|
audioOutputDevices: [],
|
||||||
audioAnalysisContext: null,
|
audioAnalysisContext: null,
|
||||||
audioTracks: new Map(),
|
audioTracks: new Map(),
|
||||||
|
audioSync: {
|
||||||
|
running: false,
|
||||||
|
pending: false,
|
||||||
|
pendingForceSeek: false
|
||||||
|
},
|
||||||
audioSnapCache: {
|
audioSnapCache: {
|
||||||
key: "",
|
key: "",
|
||||||
sourceTrackId: "",
|
sourceTrackId: "",
|
||||||
@@ -1019,6 +1028,9 @@
|
|||||||
if (!timeline || !track || !point || direction === 0) {
|
if (!timeline || !track || !point || direction === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (audioPointEditingLocked(track)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const snapData = getAudioSnapData(timeline);
|
const snapData = getAudioSnapData(timeline);
|
||||||
const snapTimes = snapData.values || [];
|
const snapTimes = snapData.values || [];
|
||||||
@@ -1060,6 +1072,8 @@
|
|||||||
|
|
||||||
setPointAbsoluteTime(track, point, targetTime, timeline);
|
setPointAbsoluteTime(track, point, targetTime, timeline);
|
||||||
if (track.type === "audio-file") {
|
if (track.type === "audio-file") {
|
||||||
|
markAudioTrackRenderDirty(track);
|
||||||
|
scheduleAudioTrackRender(track);
|
||||||
void syncAudioTracks(true);
|
void syncAudioTracks(true);
|
||||||
}
|
}
|
||||||
renderAll();
|
renderAll();
|
||||||
@@ -1201,7 +1215,6 @@
|
|||||||
return quantizeTrackValue(track, fallbackValue);
|
return quantizeTrackValue(track, fallbackValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
sortTrackPoints(track);
|
|
||||||
if (time <= track.points[0].time) {
|
if (time <= track.points[0].time) {
|
||||||
return track.points[0].value;
|
return track.points[0].value;
|
||||||
}
|
}
|
||||||
@@ -1230,6 +1243,39 @@
|
|||||||
return evaluatePointCurve(track, offset, defaultGain);
|
return evaluatePointCurve(track, offset, defaultGain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateAudioTrackLevel(track, entry, offset, force = false) {
|
||||||
|
if (!track || !entry?.element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioTrackUsesRenderedFade(track) && entry.renderedUrl) {
|
||||||
|
if (force || Math.abs((entry.element.volume ?? 1) - 1) >= 0.0005) {
|
||||||
|
entry.element.volume = 1;
|
||||||
|
}
|
||||||
|
entry.lastVolume = 1;
|
||||||
|
entry.lastVolumeAt = performance.now();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = performance.now();
|
||||||
|
const nextVolume = clamp(evaluateAudioGainAtOffset(track, Math.max(0, offset)) / 2, 0, 1);
|
||||||
|
const previousVolume = Number.isFinite(entry.lastVolume) ? entry.lastVolume : Number.NaN;
|
||||||
|
const volumeChanged = !Number.isFinite(previousVolume) || Math.abs(previousVolume - nextVolume) >= AUDIO_VOLUME_EPSILON;
|
||||||
|
const throttleElapsed = (now - (entry.lastVolumeAt || 0)) >= AUDIO_VOLUME_THROTTLE_MS;
|
||||||
|
const shouldUpdate = force || !state.transport.playing || !Number.isFinite(previousVolume) || (volumeChanged && throttleElapsed);
|
||||||
|
|
||||||
|
if (!shouldUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (force || Math.abs((entry.element.volume ?? 0) - nextVolume) >= 0.0005) {
|
||||||
|
entry.element.volume = nextVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.lastVolume = nextVolume;
|
||||||
|
entry.lastVolumeAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeTrack(track) {
|
function normalizeTrack(track) {
|
||||||
const normalized = {
|
const normalized = {
|
||||||
...track,
|
...track,
|
||||||
@@ -1411,6 +1457,9 @@
|
|||||||
runtime.audioTracks.set(trackId, {
|
runtime.audioTracks.set(trackId, {
|
||||||
element: null,
|
element: null,
|
||||||
fileUrl: "",
|
fileUrl: "",
|
||||||
|
activeUrl: "",
|
||||||
|
sourceFilePath: "",
|
||||||
|
sourceBuffer: null,
|
||||||
peaks: [],
|
peaks: [],
|
||||||
beatTimes: [],
|
beatTimes: [],
|
||||||
barTimes: [],
|
barTimes: [],
|
||||||
@@ -1418,6 +1467,14 @@
|
|||||||
beatInterval: 0,
|
beatInterval: 0,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
estimatedBpm: 0,
|
estimatedBpm: 0,
|
||||||
|
lastSeekAt: 0,
|
||||||
|
lastVolume: Number.NaN,
|
||||||
|
lastVolumeAt: 0,
|
||||||
|
renderedUrl: "",
|
||||||
|
renderSignature: "",
|
||||||
|
renderDirty: false,
|
||||||
|
renderPromise: null,
|
||||||
|
renderError: "",
|
||||||
analysisReady: false,
|
analysisReady: false,
|
||||||
decodeError: ""
|
decodeError: ""
|
||||||
});
|
});
|
||||||
@@ -1433,9 +1490,50 @@
|
|||||||
entry.element.removeAttribute("src");
|
entry.element.removeAttribute("src");
|
||||||
entry.element.load();
|
entry.element.load();
|
||||||
}
|
}
|
||||||
|
if (entry.renderedUrl) {
|
||||||
|
URL.revokeObjectURL(entry.renderedUrl);
|
||||||
|
}
|
||||||
runtime.audioTracks.delete(trackId);
|
runtime.audioTracks.delete(trackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function audioTrackUsesRenderedFade(track) {
|
||||||
|
return track?.type === "audio-file" && Array.isArray(track.points) && track.points.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function audioPointEditingLocked(track) {
|
||||||
|
return track?.type === "audio-file" && state.transport.playing;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAudioRenderSignature(track) {
|
||||||
|
return JSON.stringify({
|
||||||
|
filePath: track?.filePath || "",
|
||||||
|
gain: clamp(Number(track?.gain) || 1, 0, 2),
|
||||||
|
points: Array.isArray(track?.points)
|
||||||
|
? track.points.map((point) => ({
|
||||||
|
time: Number(point.time) || 0,
|
||||||
|
value: quantizeTrackValue(track, point.value),
|
||||||
|
mode: point.mode || "hold"
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function markAudioTrackRenderDirty(track) {
|
||||||
|
if (!track || track.type !== "audio-file") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = getAudioTrackState(track.id);
|
||||||
|
entry.renderDirty = true;
|
||||||
|
entry.renderError = "";
|
||||||
|
entry.renderSignature = "";
|
||||||
|
|
||||||
|
if (!audioTrackUsesRenderedFade(track) && entry.renderedUrl) {
|
||||||
|
URL.revokeObjectURL(entry.renderedUrl);
|
||||||
|
entry.renderedUrl = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function cleanupMissingAudioTracks() {
|
function cleanupMissingAudioTracks() {
|
||||||
const activeTrackIds = new Set(
|
const activeTrackIds = new Set(
|
||||||
state.project.timelines.flatMap((timeline) => timeline.tracks)
|
state.project.timelines.flatMap((timeline) => timeline.tracks)
|
||||||
@@ -1458,6 +1556,231 @@
|
|||||||
return runtime.audioAnalysisContext;
|
return runtime.audioAnalysisContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getDecodedAudioBuffer(track, forceRefresh = false) {
|
||||||
|
if (!track?.filePath || !window.time4midi.audio?.readFile) {
|
||||||
|
throw new Error("Audio-Datei nicht verfuegbar");
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = getAudioTrackState(track.id);
|
||||||
|
if (!forceRefresh && entry.sourceBuffer && entry.sourceFilePath === track.filePath) {
|
||||||
|
return entry.sourceBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bufferData = await window.time4midi.audio.readFile(track.filePath);
|
||||||
|
const context = getAudioAnalysisContext();
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("AudioContext nicht verfuegbar");
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = await context.decodeAudioData(bufferData.slice(0));
|
||||||
|
entry.sourceBuffer = decoded;
|
||||||
|
entry.sourceFilePath = track.filePath;
|
||||||
|
entry.duration = decoded.duration;
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeAudioBufferAsWav(audioBuffer) {
|
||||||
|
const channels = Math.max(1, audioBuffer.numberOfChannels || 1);
|
||||||
|
const sampleRate = audioBuffer.sampleRate || 44100;
|
||||||
|
const frameCount = audioBuffer.length || 0;
|
||||||
|
const bytesPerSample = 2;
|
||||||
|
const blockAlign = channels * bytesPerSample;
|
||||||
|
const dataSize = frameCount * blockAlign;
|
||||||
|
const arrayBuffer = new ArrayBuffer(44 + dataSize);
|
||||||
|
const view = new DataView(arrayBuffer);
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
const writeString = (value) => {
|
||||||
|
for (let index = 0; index < value.length; index += 1) {
|
||||||
|
view.setUint8(offset + index, value.charCodeAt(index));
|
||||||
|
}
|
||||||
|
offset += value.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
writeString("RIFF");
|
||||||
|
view.setUint32(offset, 36 + dataSize, true);
|
||||||
|
offset += 4;
|
||||||
|
writeString("WAVE");
|
||||||
|
writeString("fmt ");
|
||||||
|
view.setUint32(offset, 16, true);
|
||||||
|
offset += 4;
|
||||||
|
view.setUint16(offset, 1, true);
|
||||||
|
offset += 2;
|
||||||
|
view.setUint16(offset, channels, true);
|
||||||
|
offset += 2;
|
||||||
|
view.setUint32(offset, sampleRate, true);
|
||||||
|
offset += 4;
|
||||||
|
view.setUint32(offset, sampleRate * blockAlign, true);
|
||||||
|
offset += 4;
|
||||||
|
view.setUint16(offset, blockAlign, true);
|
||||||
|
offset += 2;
|
||||||
|
view.setUint16(offset, bytesPerSample * 8, true);
|
||||||
|
offset += 2;
|
||||||
|
writeString("data");
|
||||||
|
view.setUint32(offset, dataSize, true);
|
||||||
|
offset += 4;
|
||||||
|
|
||||||
|
const channelData = Array.from({ length: channels }, (_entry, index) => audioBuffer.getChannelData(index));
|
||||||
|
for (let frame = 0; frame < frameCount; frame += 1) {
|
||||||
|
for (let channel = 0; channel < channels; channel += 1) {
|
||||||
|
const sample = clamp(channelData[channel][frame] || 0, -1, 1);
|
||||||
|
const intSample = sample < 0 ? sample * 0x8000 : sample * 0x7fff;
|
||||||
|
view.setInt16(offset, Math.round(intSample), true);
|
||||||
|
offset += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Blob([arrayBuffer], { type: "audio/wav" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAutomationToGainParam(gainNode, track, duration) {
|
||||||
|
const defaultGain = clamp(Number(track.gain) || 1, 0, 2) / 2;
|
||||||
|
const points = Array.isArray(track.points) ? track.points : [];
|
||||||
|
if (!points.length) {
|
||||||
|
gainNode.gain.setValueAtTime(defaultGain, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstValue = clamp(Number(points[0].value) || 0, 0, 2) / 2;
|
||||||
|
gainNode.gain.setValueAtTime(firstValue, 0);
|
||||||
|
gainNode.gain.setValueAtTime(firstValue, clamp(points[0].time, 0, duration));
|
||||||
|
|
||||||
|
for (let index = 0; index < points.length - 1; index += 1) {
|
||||||
|
const current = points[index];
|
||||||
|
const next = points[index + 1];
|
||||||
|
const currentTime = clamp(Number(current.time) || 0, 0, duration);
|
||||||
|
const nextTime = clamp(Number(next.time) || 0, currentTime, duration);
|
||||||
|
const currentValue = clamp(Number(current.value) || 0, 0, 2) / 2;
|
||||||
|
const nextValue = clamp(Number(next.value) || 0, 0, 2) / 2;
|
||||||
|
|
||||||
|
gainNode.gain.setValueAtTime(currentValue, currentTime);
|
||||||
|
if (current.mode === "linear" && nextTime > currentTime) {
|
||||||
|
gainNode.gain.linearRampToValueAtTime(nextValue, nextTime);
|
||||||
|
} else {
|
||||||
|
gainNode.gain.setValueAtTime(currentValue, nextTime);
|
||||||
|
gainNode.gain.setValueAtTime(nextValue, nextTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastPoint = points[points.length - 1];
|
||||||
|
const lastTime = clamp(Number(lastPoint.time) || 0, 0, duration);
|
||||||
|
const lastValue = clamp(Number(lastPoint.value) || 0, 0, 2) / 2;
|
||||||
|
gainNode.gain.setValueAtTime(lastValue, lastTime);
|
||||||
|
gainNode.gain.setValueAtTime(lastValue, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareAudioTrackRender(track) {
|
||||||
|
const entry = getAudioTrackState(track.id);
|
||||||
|
|
||||||
|
if (!audioTrackUsesRenderedFade(track)) {
|
||||||
|
entry.renderDirty = false;
|
||||||
|
entry.renderError = "";
|
||||||
|
if (entry.renderedUrl) {
|
||||||
|
URL.revokeObjectURL(entry.renderedUrl);
|
||||||
|
entry.renderedUrl = "";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSignature = buildAudioRenderSignature(track);
|
||||||
|
if (!entry.renderDirty && entry.renderedUrl && entry.renderSignature === nextSignature) {
|
||||||
|
return entry.renderedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.renderPromise) {
|
||||||
|
return entry.renderPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.renderPromise = (async () => {
|
||||||
|
refs.transportStatusLabel.textContent = `Audio Render: ${basenameFromPath(track.filePath)} wird vorbereitet...`;
|
||||||
|
const sourceBuffer = await getDecodedAudioBuffer(track);
|
||||||
|
const OfflineContextClass = window.OfflineAudioContext || window.webkitOfflineAudioContext;
|
||||||
|
if (!OfflineContextClass) {
|
||||||
|
throw new Error("OfflineAudioContext nicht verfuegbar");
|
||||||
|
}
|
||||||
|
|
||||||
|
const offline = new OfflineContextClass(
|
||||||
|
sourceBuffer.numberOfChannels,
|
||||||
|
sourceBuffer.length,
|
||||||
|
sourceBuffer.sampleRate
|
||||||
|
);
|
||||||
|
const source = offline.createBufferSource();
|
||||||
|
const gainNode = offline.createGain();
|
||||||
|
source.buffer = sourceBuffer;
|
||||||
|
applyAutomationToGainParam(gainNode, track, sourceBuffer.duration);
|
||||||
|
source.connect(gainNode);
|
||||||
|
gainNode.connect(offline.destination);
|
||||||
|
source.start(0);
|
||||||
|
|
||||||
|
const renderedBuffer = await offline.startRendering();
|
||||||
|
const wavBlob = encodeAudioBufferAsWav(renderedBuffer);
|
||||||
|
const renderedUrl = URL.createObjectURL(wavBlob);
|
||||||
|
const latestSignature = buildAudioRenderSignature(track);
|
||||||
|
const stillNeedsRenderedFade = audioTrackUsesRenderedFade(track);
|
||||||
|
|
||||||
|
if (!stillNeedsRenderedFade) {
|
||||||
|
URL.revokeObjectURL(renderedUrl);
|
||||||
|
if (entry.renderedUrl) {
|
||||||
|
URL.revokeObjectURL(entry.renderedUrl);
|
||||||
|
entry.renderedUrl = "";
|
||||||
|
}
|
||||||
|
entry.renderSignature = "";
|
||||||
|
entry.renderDirty = false;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latestSignature !== nextSignature) {
|
||||||
|
URL.revokeObjectURL(renderedUrl);
|
||||||
|
entry.renderDirty = true;
|
||||||
|
return entry.renderedUrl || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.renderedUrl) {
|
||||||
|
URL.revokeObjectURL(entry.renderedUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.renderedUrl = renderedUrl;
|
||||||
|
entry.renderSignature = nextSignature;
|
||||||
|
entry.renderDirty = false;
|
||||||
|
entry.renderError = "";
|
||||||
|
return renderedUrl;
|
||||||
|
})().catch((error) => {
|
||||||
|
entry.renderError = error.message || String(error);
|
||||||
|
refs.transportStatusLabel.textContent = `Audio Render Fehler: ${entry.renderError}`;
|
||||||
|
throw error;
|
||||||
|
}).finally(() => {
|
||||||
|
entry.renderPromise = null;
|
||||||
|
if (entry.renderDirty && !state.transport.playing) {
|
||||||
|
scheduleAudioTrackRender(track);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return entry.renderPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleAudioTrackRender(track) {
|
||||||
|
if (!track || track.type !== "audio-file" || state.transport.playing || !track.filePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void prepareAudioTrackRender(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareTimelineAudioForPlayback(timeline = getSelectedTimeline()) {
|
||||||
|
if (!timeline) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioTracks = timeline.tracks.filter((track) => track.type === "audio-file" && track.filePath && audioTrackUsesRenderedFade(track));
|
||||||
|
for (const track of audioTracks) {
|
||||||
|
try {
|
||||||
|
await prepareAudioTrackRender(track);
|
||||||
|
} catch (_error) {
|
||||||
|
// Keep transport responsive even if one render fails.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildWaveformPeaks(samples, desiredSamples = AUDIO_WAVEFORM_SAMPLES) {
|
function buildWaveformPeaks(samples, desiredSamples = AUDIO_WAVEFORM_SAMPLES) {
|
||||||
if (!samples || !samples.length) {
|
if (!samples || !samples.length) {
|
||||||
return [];
|
return [];
|
||||||
@@ -1612,13 +1935,7 @@
|
|||||||
entry.decodeError = "";
|
entry.decodeError = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const bufferData = await window.time4midi.audio.readFile(track.filePath);
|
const decoded = await getDecodedAudioBuffer(track, true);
|
||||||
const context = getAudioAnalysisContext();
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("AudioContext nicht verfuegbar");
|
|
||||||
}
|
|
||||||
|
|
||||||
const decoded = await context.decodeAudioData(bufferData.slice(0));
|
|
||||||
const channelData = decoded.numberOfChannels > 1
|
const channelData = decoded.numberOfChannels > 1
|
||||||
? decoded.getChannelData(0).map((sample, index) => (sample + decoded.getChannelData(1)[index]) * 0.5)
|
? decoded.getChannelData(0).map((sample, index) => (sample + decoded.getChannelData(1)[index]) * 0.5)
|
||||||
: decoded.getChannelData(0);
|
: decoded.getChannelData(0);
|
||||||
@@ -1640,6 +1957,10 @@
|
|||||||
ensureTimelineCoversAudioTrack(timeline, track);
|
ensureTimelineCoversAudioTrack(timeline, track);
|
||||||
}
|
}
|
||||||
syncTrackDerivedValues(track);
|
syncTrackDerivedValues(track);
|
||||||
|
if (audioTrackUsesRenderedFade(track)) {
|
||||||
|
markAudioTrackRenderDirty(track);
|
||||||
|
scheduleAudioTrackRender(track);
|
||||||
|
}
|
||||||
renderAll();
|
renderAll();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
entry.decodeError = error.message;
|
entry.decodeError = error.message;
|
||||||
@@ -1654,17 +1975,19 @@
|
|||||||
async function ensureAudioTrackReady(track) {
|
async function ensureAudioTrackReady(track) {
|
||||||
const entry = getAudioTrackState(track.id);
|
const entry = getAudioTrackState(track.id);
|
||||||
const fileUrl = window.time4midi.audio?.fileUrl ? window.time4midi.audio.fileUrl(track.filePath) : "";
|
const fileUrl = window.time4midi.audio?.fileUrl ? window.time4midi.audio.fileUrl(track.filePath) : "";
|
||||||
|
entry.fileUrl = fileUrl;
|
||||||
if (!entry.element) {
|
if (!entry.element) {
|
||||||
entry.element = new Audio();
|
entry.element = new Audio();
|
||||||
entry.element.preload = "auto";
|
entry.element.preload = "auto";
|
||||||
}
|
}
|
||||||
if (fileUrl && entry.fileUrl !== fileUrl) {
|
const preferredUrl = audioTrackUsesRenderedFade(track) && entry.renderedUrl ? entry.renderedUrl : fileUrl;
|
||||||
entry.fileUrl = fileUrl;
|
if (preferredUrl && entry.activeUrl !== preferredUrl) {
|
||||||
entry.element.src = fileUrl;
|
entry.activeUrl = preferredUrl;
|
||||||
|
entry.element.src = preferredUrl;
|
||||||
entry.duration = 0;
|
entry.duration = 0;
|
||||||
entry.element.load();
|
entry.element.load();
|
||||||
}
|
}
|
||||||
entry.element.volume = evaluateAudioGainAtOffset(track, 0) / 2;
|
updateAudioTrackLevel(track, entry, 0, true);
|
||||||
if (state.routing.audioOutputId && typeof entry.element.setSinkId === "function") {
|
if (state.routing.audioOutputId && typeof entry.element.setSinkId === "function") {
|
||||||
try {
|
try {
|
||||||
await entry.element.setSinkId(state.routing.audioOutputId);
|
await entry.element.setSinkId(state.routing.audioOutputId);
|
||||||
@@ -1675,6 +1998,9 @@
|
|||||||
if (!entry.analysisReady && !entry.decodeError && track.filePath) {
|
if (!entry.analysisReady && !entry.decodeError && track.filePath) {
|
||||||
void analyzeAudioTrack(track);
|
void analyzeAudioTrack(track);
|
||||||
}
|
}
|
||||||
|
if (audioTrackUsesRenderedFade(track) && entry.renderDirty && !state.transport.playing) {
|
||||||
|
void prepareAudioTrackRender(track);
|
||||||
|
}
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1712,43 +2038,85 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function syncAudioTracks(forceSeek = false) {
|
async function syncAudioTracks(forceSeek = false) {
|
||||||
|
if (runtime.audioSync.running) {
|
||||||
|
runtime.audioSync.pending = true;
|
||||||
|
runtime.audioSync.pendingForceSeek = runtime.audioSync.pendingForceSeek || forceSeek;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.audioSync.running = true;
|
||||||
|
runtime.audioSync.pending = false;
|
||||||
|
runtime.audioSync.pendingForceSeek = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let nextForceSeek = forceSeek;
|
||||||
|
|
||||||
|
do {
|
||||||
const timeline = getSelectedTimeline();
|
const timeline = getSelectedTimeline();
|
||||||
|
runtime.audioSync.pending = false;
|
||||||
|
runtime.audioSync.pendingForceSeek = false;
|
||||||
|
|
||||||
if (!timeline) {
|
if (!timeline) {
|
||||||
pauseAllAudioTracks();
|
pauseAllAudioTracks();
|
||||||
return;
|
nextForceSeek = false;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const audioTracks = timeline.tracks.filter((track) => track.type === "audio-file" && track.filePath);
|
const audioTracks = timeline.tracks.filter((track) => track.type === "audio-file" && track.filePath);
|
||||||
for (const track of audioTracks) {
|
for (const track of audioTracks) {
|
||||||
|
if (audioTrackUsesRenderedFade(track)) {
|
||||||
|
try {
|
||||||
|
await prepareAudioTrackRender(track);
|
||||||
|
} catch (_error) {
|
||||||
|
// Fall back to the original file if the offline render fails.
|
||||||
|
}
|
||||||
|
}
|
||||||
const entry = await ensureAudioTrackReady(track);
|
const entry = await ensureAudioTrackReady(track);
|
||||||
const offset = state.transport.currentTime - track.startTime;
|
const offset = state.transport.currentTime - track.startTime;
|
||||||
const duration = entry.duration || track.analysisDuration || Number.MAX_SAFE_INTEGER;
|
const duration = entry.duration || track.analysisDuration || Number.MAX_SAFE_INTEGER;
|
||||||
const active = offset >= 0 && offset <= duration;
|
const active = offset >= 0 && offset <= duration;
|
||||||
entry.element.volume = evaluateAudioGainAtOffset(track, Math.max(0, offset)) / 2;
|
updateAudioTrackLevel(track, entry, offset, nextForceSeek);
|
||||||
|
|
||||||
if (!active) {
|
if (!active) {
|
||||||
|
if (!entry.element.paused) {
|
||||||
entry.element.pause();
|
entry.element.pause();
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forceSeek || Math.abs((entry.element.currentTime || 0) - offset) > AUDIO_SYNC_TOLERANCE) {
|
const now = performance.now();
|
||||||
|
const drift = Math.abs((entry.element.currentTime || 0) - offset);
|
||||||
|
const seekTolerance = state.transport.playing ? AUDIO_SYNC_TOLERANCE_PLAYING : AUDIO_SYNC_TOLERANCE;
|
||||||
|
const canSeekNow = nextForceSeek || !state.transport.playing || ((now - (entry.lastSeekAt || 0)) >= AUDIO_SYNC_THROTTLE_MS);
|
||||||
|
if (canSeekNow && drift > seekTolerance) {
|
||||||
try {
|
try {
|
||||||
entry.element.currentTime = Math.max(0, offset);
|
entry.element.currentTime = Math.max(0, offset);
|
||||||
|
entry.lastSeekAt = now;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
// Ignore seek races while metadata is loading.
|
// Ignore seek races while metadata is loading.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.transport.playing) {
|
if (state.transport.playing) {
|
||||||
|
if (entry.element.paused) {
|
||||||
try {
|
try {
|
||||||
await entry.element.play();
|
await entry.element.play();
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
// Autoplay issues are non-fatal; the transport still runs.
|
// Autoplay issues are non-fatal; the transport still runs.
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
} else if (!entry.element.paused) {
|
||||||
entry.element.pause();
|
entry.element.pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextForceSeek = runtime.audioSync.pendingForceSeek;
|
||||||
|
} while (runtime.audioSync.pending);
|
||||||
|
} finally {
|
||||||
|
runtime.audioSync.running = false;
|
||||||
|
runtime.audioSync.pending = false;
|
||||||
|
runtime.audioSync.pendingForceSeek = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
@@ -2206,7 +2574,6 @@
|
|||||||
|
|
||||||
const startTime = Math.max(0, Number(track.startTime) || 0);
|
const startTime = Math.max(0, Number(track.startTime) || 0);
|
||||||
const color = getTrackColor(track);
|
const color = getTrackColor(track);
|
||||||
sortTrackPoints(track);
|
|
||||||
|
|
||||||
ctx.strokeStyle = color;
|
ctx.strokeStyle = color;
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
@@ -2248,7 +2615,6 @@
|
|||||||
drawAudioTrackAutomation(track, trackIndex, duration, metrics);
|
drawAudioTrackAutomation(track, trackIndex, duration, metrics);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sortTrackPoints(track);
|
|
||||||
const color = getTrackColor(track);
|
const color = getTrackColor(track);
|
||||||
ctx.strokeStyle = color;
|
ctx.strokeStyle = color;
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
@@ -2480,6 +2846,8 @@
|
|||||||
const range = trackRange(track);
|
const range = trackRange(track);
|
||||||
const knxDptMeta = track.type === "knx-value" ? getKnxDptMeta(track.dpt) : null;
|
const knxDptMeta = track.type === "knx-value" ? getKnxDptMeta(track.dpt) : null;
|
||||||
const mappingPicker = renderTrackMappingPicker(track);
|
const mappingPicker = renderTrackMappingPicker(track);
|
||||||
|
const audioPointLocked = track.type === "audio-file" && state.transport.playing;
|
||||||
|
const audioPointDisabled = audioPointLocked ? "disabled" : "";
|
||||||
let typeFields = "";
|
let typeFields = "";
|
||||||
|
|
||||||
if (track.type === "midi-note") {
|
if (track.type === "midi-note") {
|
||||||
@@ -2687,19 +3055,20 @@
|
|||||||
if (point) {
|
if (point) {
|
||||||
pointFields = `
|
pointFields = `
|
||||||
<div class="section-label">Punkt</div>
|
<div class="section-label">Punkt</div>
|
||||||
|
${audioPointLocked ? `<div class="info-card">Audio-Fade-Punkte sind waehrend der Wiedergabe gesperrt. Stoppen oder pausieren, dann weiter bearbeiten.</div>` : ""}
|
||||||
<div class="field-grid">
|
<div class="field-grid">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>${track.type === "audio-file" ? "Zeit im Clip" : "Zeit"}</span>
|
<span>${track.type === "audio-file" ? "Zeit im Clip" : "Zeit"}</span>
|
||||||
<input data-bind="point.time" type="number" min="0" max="${track.type === "audio-file" ? getAudioTrackDuration(track).toFixed(2) : timeline?.duration || 0}" step="0.01" value="${point.time.toFixed(2)}">
|
<input data-bind="point.time" type="number" min="0" max="${track.type === "audio-file" ? getAudioTrackDuration(track).toFixed(2) : timeline?.duration || 0}" step="0.01" value="${point.time.toFixed(2)}" ${track.type === "audio-file" ? audioPointDisabled : ""}>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>${track.type === "audio-file" ? "Gain" : "Wert"}</span>
|
<span>${track.type === "audio-file" ? "Gain" : "Wert"}</span>
|
||||||
<input data-bind="point.value" type="number" min="${range.min}" max="${range.max}" step="${range.step}" value="${point.value}">
|
<input data-bind="point.value" type="number" min="${range.min}" max="${range.max}" step="${range.step}" value="${point.value}" ${track.type === "audio-file" ? audioPointDisabled : ""}>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Interpolation zum naechsten Punkt</span>
|
<span>Interpolation zum naechsten Punkt</span>
|
||||||
<select data-bind="point.mode">
|
<select data-bind="point.mode" ${track.type === "audio-file" ? audioPointDisabled : ""}>
|
||||||
${pointModeOptions(track, point)}
|
${pointModeOptions(track, point)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -3318,6 +3687,7 @@
|
|||||||
duration: 120,
|
duration: 120,
|
||||||
beatSnapEnabled: false,
|
beatSnapEnabled: false,
|
||||||
beatSnapGrid: "1",
|
beatSnapGrid: "1",
|
||||||
|
beatSnapLineWidth: "2",
|
||||||
tracks: []
|
tracks: []
|
||||||
};
|
};
|
||||||
state.project.timelines.push(timeline);
|
state.project.timelines.push(timeline);
|
||||||
@@ -3813,6 +4183,7 @@
|
|||||||
state.transport.currentTime = 0;
|
state.transport.currentTime = 0;
|
||||||
}
|
}
|
||||||
void syncAudioTracks(true);
|
void syncAudioTracks(true);
|
||||||
|
void prepareTimelineAudioForPlayback();
|
||||||
renderTransportState();
|
renderTransportState();
|
||||||
drawCanvas();
|
drawCanvas();
|
||||||
}
|
}
|
||||||
@@ -3823,7 +4194,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startInternalTransport() {
|
async function startInternalTransport() {
|
||||||
|
const timeline = getSelectedTimeline();
|
||||||
|
if (!timeline) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await prepareTimelineAudioForPlayback(timeline);
|
||||||
state.transport.playing = true;
|
state.transport.playing = true;
|
||||||
state.transport.playStartPerf = performance.now();
|
state.transport.playStartPerf = performance.now();
|
||||||
state.transport.playStartTime = state.transport.currentTime;
|
state.transport.playStartTime = state.transport.currentTime;
|
||||||
@@ -3855,7 +4231,7 @@
|
|||||||
if (state.transport.syncSource !== "internal") {
|
if (state.transport.syncSource !== "internal") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
startInternalTransport();
|
void startInternalTransport();
|
||||||
}
|
}
|
||||||
|
|
||||||
function tickTransport() {
|
function tickTransport() {
|
||||||
@@ -4125,6 +4501,10 @@
|
|||||||
if (!timeline) return;
|
if (!timeline) return;
|
||||||
const hit = findCanvasHit(x, y);
|
const hit = findCanvasHit(x, y);
|
||||||
if (!hit || !hit.track) return;
|
if (!hit || !hit.track) return;
|
||||||
|
if (audioPointEditingLocked(hit.track)) {
|
||||||
|
refs.transportStatusLabel.textContent = "Audio-Fade-Punkte koennen waehrend der Wiedergabe nicht bearbeitet werden.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
const metrics = getPlotMetrics();
|
const metrics = getPlotMetrics();
|
||||||
let pointTime = Number(xToTime(x, timeline.duration, metrics).toFixed(2));
|
let pointTime = Number(xToTime(x, timeline.duration, metrics).toFixed(2));
|
||||||
pointTime = snapTimelineTimeToAudioGrid(pointTime, timeline, hit.track);
|
pointTime = snapTimelineTimeToAudioGrid(pointTime, timeline, hit.track);
|
||||||
@@ -4148,6 +4528,8 @@
|
|||||||
};
|
};
|
||||||
hit.track.points.push(point);
|
hit.track.points.push(point);
|
||||||
sortTrackPoints(hit.track);
|
sortTrackPoints(hit.track);
|
||||||
|
markAudioTrackRenderDirty(hit.track);
|
||||||
|
scheduleAudioTrackRender(hit.track);
|
||||||
state.selectedTrackId = hit.track.id;
|
state.selectedTrackId = hit.track.id;
|
||||||
state.selectedPointId = point.id;
|
state.selectedPointId = point.id;
|
||||||
renderAll();
|
renderAll();
|
||||||
@@ -4158,6 +4540,7 @@
|
|||||||
const track = getSelectedTrack();
|
const track = getSelectedTrack();
|
||||||
const point = getSelectedPoint();
|
const point = getSelectedPoint();
|
||||||
if (!binding) return;
|
if (!binding) return;
|
||||||
|
const pointEditLocked = audioPointEditingLocked(track);
|
||||||
|
|
||||||
if (binding === "timeline.name" && timeline) {
|
if (binding === "timeline.name" && timeline) {
|
||||||
timeline.name = rawValue;
|
timeline.name = rawValue;
|
||||||
@@ -4228,12 +4611,17 @@
|
|||||||
void syncAudioTracks(true);
|
void syncAudioTracks(true);
|
||||||
} else if (binding === "track.gain" && track) {
|
} else if (binding === "track.gain" && track) {
|
||||||
track.gain = clamp(Number(rawValue) || 1, 0, 2);
|
track.gain = clamp(Number(rawValue) || 1, 0, 2);
|
||||||
|
markAudioTrackRenderDirty(track);
|
||||||
const entry = runtime.audioTracks.get(track.id);
|
const entry = runtime.audioTracks.get(track.id);
|
||||||
if (entry?.element) {
|
if (entry?.element) {
|
||||||
const offset = Math.max(0, state.transport.currentTime - track.startTime);
|
const offset = Math.max(0, state.transport.currentTime - track.startTime);
|
||||||
entry.element.volume = evaluateAudioGainAtOffset(track, offset) / 2;
|
updateAudioTrackLevel(track, entry, offset, true);
|
||||||
}
|
}
|
||||||
|
scheduleAudioTrackRender(track);
|
||||||
} else if (binding === "point.time" && point && timeline && track) {
|
} else if (binding === "point.time" && point && timeline && track) {
|
||||||
|
if (pointEditLocked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
point.time = track.type === "audio-file"
|
point.time = track.type === "audio-file"
|
||||||
? clamp(Number(rawValue) || 0, 0, getAudioTrackDuration(track))
|
? clamp(Number(rawValue) || 0, 0, getAudioTrackDuration(track))
|
||||||
: clamp(
|
: clamp(
|
||||||
@@ -4242,10 +4630,22 @@
|
|||||||
timeline.duration
|
timeline.duration
|
||||||
);
|
);
|
||||||
sortTrackPoints(track);
|
sortTrackPoints(track);
|
||||||
|
markAudioTrackRenderDirty(track);
|
||||||
|
scheduleAudioTrackRender(track);
|
||||||
} else if (binding === "point.value" && point && track) {
|
} else if (binding === "point.value" && point && track) {
|
||||||
|
if (pointEditLocked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
point.value = quantizeTrackValue(track, rawValue);
|
point.value = quantizeTrackValue(track, rawValue);
|
||||||
|
markAudioTrackRenderDirty(track);
|
||||||
|
scheduleAudioTrackRender(track);
|
||||||
} else if (binding === "point.mode" && point) {
|
} else if (binding === "point.mode" && point) {
|
||||||
|
if (pointEditLocked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
point.mode = rawValue;
|
point.mode = rawValue;
|
||||||
|
markAudioTrackRenderDirty(track);
|
||||||
|
scheduleAudioTrackRender(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track && ["track.minValue", "track.maxValue", "track.registerType", "track.dpt"].includes(binding)) {
|
if (track && ["track.minValue", "track.maxValue", "track.registerType", "track.dpt"].includes(binding)) {
|
||||||
@@ -4703,6 +5103,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hit.point) {
|
if (hit.point) {
|
||||||
|
if (audioPointEditingLocked(hit.track)) {
|
||||||
|
renderAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const pointerOriginTime = Number(xToTime(x, Math.max(1, getSelectedTimeline()?.duration || 1), getPlotMetrics()).toFixed(3));
|
const pointerOriginTime = Number(xToTime(x, Math.max(1, getSelectedTimeline()?.duration || 1), getPlotMetrics()).toFixed(3));
|
||||||
const pointOriginTime = getCurrentPointAbsoluteTime(hit.track, hit.point);
|
const pointOriginTime = getCurrentPointAbsoluteTime(hit.track, hit.point);
|
||||||
const snapData = getAudioSnapData(getSelectedTimeline());
|
const snapData = getAudioSnapData(getSelectedTimeline());
|
||||||
@@ -4772,6 +5176,9 @@
|
|||||||
const track = timeline.tracks.find((entry) => entry.id === runtime.drag.trackId);
|
const track = timeline.tracks.find((entry) => entry.id === runtime.drag.trackId);
|
||||||
const point = track?.points.find((entry) => entry.id === runtime.drag.pointId);
|
const point = track?.points.find((entry) => entry.id === runtime.drag.pointId);
|
||||||
if (!track || !point) return;
|
if (!track || !point) return;
|
||||||
|
if (audioPointEditingLocked(track)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const metrics = getPlotMetrics();
|
const metrics = getPlotMetrics();
|
||||||
const clampedX = clamp(x, metrics.left, metrics.right);
|
const clampedX = clamp(x, metrics.left, metrics.right);
|
||||||
@@ -4800,6 +5207,10 @@
|
|||||||
|
|
||||||
const timeline = getSelectedTimeline();
|
const timeline = getSelectedTimeline();
|
||||||
const track = timeline?.tracks.find((entry) => entry.id === completedDrag.trackId) || null;
|
const track = timeline?.tracks.find((entry) => entry.id === completedDrag.trackId) || null;
|
||||||
|
if (completedDrag.type === "point" && track?.type === "audio-file") {
|
||||||
|
markAudioTrackRenderDirty(track);
|
||||||
|
scheduleAudioTrackRender(track);
|
||||||
|
}
|
||||||
if (track?.type === "audio-file") {
|
if (track?.type === "audio-file") {
|
||||||
void syncAudioTracks(true);
|
void syncAudioTracks(true);
|
||||||
}
|
}
|
||||||
@@ -4850,8 +5261,14 @@
|
|||||||
if (!track || !state.selectedPointId) {
|
if (!track || !state.selectedPointId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (audioPointEditingLocked(track)) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
track.points = track.points.filter((point) => point.id !== state.selectedPointId);
|
track.points = track.points.filter((point) => point.id !== state.selectedPointId);
|
||||||
|
markAudioTrackRenderDirty(track);
|
||||||
|
scheduleAudioTrackRender(track);
|
||||||
state.selectedPointId = "";
|
state.selectedPointId = "";
|
||||||
renderAll();
|
renderAll();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user