fix: audio clip items lost after reload — missing serverFileId on clip/track
- set serverFileId on clip + track when upload succeeds - serialize/deserialize server_file_id in source_data - guard clip.buffer null in WaveformLane canvas draw
This commit is contained in:
+13
-6
@@ -1376,6 +1376,7 @@ const WaveformLane = ({
|
|||||||
|
|
||||||
if (clips.length > 0) {
|
if (clips.length > 0) {
|
||||||
clips.forEach(clip => {
|
clips.forEach(clip => {
|
||||||
|
if (!clip.buffer) return;
|
||||||
const numChannels = clip.buffer.numberOfChannels || 1;
|
const numChannels = clip.buffer.numberOfChannels || 1;
|
||||||
const dataL = clip.buffer.getChannelData(0);
|
const dataL = clip.buffer.getChannelData(0);
|
||||||
const dataR = numChannels >= 2 ? clip.buffer.getChannelData(1) : dataL;
|
const dataR = numChannels >= 2 ? clip.buffer.getChannelData(1) : dataL;
|
||||||
@@ -7548,6 +7549,7 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
|
|||||||
if (trackType === "AUDIO" && t.clips) {
|
if (trackType === "AUDIO" && t.clips) {
|
||||||
t.clips.forEach(c => {
|
t.clips.forEach(c => {
|
||||||
const durationSec = c.buffer ? c.buffer.duration : 4.0;
|
const durationSec = c.buffer ? c.buffer.duration : 4.0;
|
||||||
|
const clipFileId = c.serverFileId || t.serverFileId;
|
||||||
items.push({
|
items.push({
|
||||||
id: c.id,
|
id: c.id,
|
||||||
name: c.name || "Audio Clip",
|
name: c.name || "Audio Clip",
|
||||||
@@ -7556,10 +7558,11 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
|
|||||||
duration_bars: (durationSec / (c.speed || 1.0)) / secondsPerBar,
|
duration_bars: (durationSec / (c.speed || 1.0)) / secondsPerBar,
|
||||||
clip_start_offset_bars: 0.0,
|
clip_start_offset_bars: 0.0,
|
||||||
source_data: {
|
source_data: {
|
||||||
audio_file_url: t.serverFileId ? `/static/audio/uploads/${t.serverFileId}` : "",
|
audio_file_url: clipFileId ? `/static/audio/uploads/${clipFileId}` : "",
|
||||||
sample_rate: c.buffer ? c.buffer.sampleRate : 44100,
|
sample_rate: c.buffer ? c.buffer.sampleRate : 44100,
|
||||||
channels: c.buffer ? c.buffer.numberOfChannels : 2,
|
channels: c.buffer ? c.buffer.numberOfChannels : 2,
|
||||||
gain: 1.0
|
gain: 1.0,
|
||||||
|
server_file_id: clipFileId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -7631,11 +7634,13 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore) => {
|
|||||||
(t.items || []).forEach(item => {
|
(t.items || []).forEach(item => {
|
||||||
if (item.type === "AUDIO_ITEM") {
|
if (item.type === "AUDIO_ITEM") {
|
||||||
const src = item.source_data || {};
|
const src = item.source_data || {};
|
||||||
|
const clipFileId = src.server_file_id || null;
|
||||||
clips.push({
|
clips.push({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
startTime: item.start_bar * secondsPerBar,
|
startTime: item.start_bar * secondsPerBar,
|
||||||
speed: 1.0
|
speed: 1.0,
|
||||||
|
serverFileId: clipFileId
|
||||||
});
|
});
|
||||||
} else if (item.type === "MIDI_ITEM") {
|
} else if (item.type === "MIDI_ITEM") {
|
||||||
const src = item.source_data || {};
|
const src = item.source_data || {};
|
||||||
@@ -7683,7 +7688,7 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore) => {
|
|||||||
startTime: t.start_time || 0,
|
startTime: t.start_time || 0,
|
||||||
height: t.height || 140,
|
height: t.height || 140,
|
||||||
markers: t.markers || [],
|
markers: t.markers || [],
|
||||||
serverFileId: t.server_file_id || (t.items && t.items.find(function(i) { return i.type === 'AUDIO_ITEM'; })?.source_data?.audio_file_url?.split('/').pop()) || null,
|
serverFileId: t.server_file_id || (t.items && t.items.find(function(i) { return i.type === 'AUDIO_ITEM'; })?.source_data?.server_file_id) || (t.items && t.items.find(function(i) { return i.type === 'AUDIO_ITEM'; })?.source_data?.audio_file_url?.split('/').pop()) || null,
|
||||||
channelInfo: t.channel_info || null,
|
channelInfo: t.channel_info || null,
|
||||||
isArmed: t.is_armed || false,
|
isArmed: t.is_armed || false,
|
||||||
monitoringEnabled: t.monitoring_enabled !== false,
|
monitoringEnabled: t.monitoring_enabled !== false,
|
||||||
@@ -15775,13 +15780,15 @@ const App = () => {
|
|||||||
let count = 0;
|
let count = 0;
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
try {
|
try {
|
||||||
uploadToServer(file, track.id);
|
const uploadResult = await uploadToServer(file, track.id);
|
||||||
|
const fileId = uploadResult?.file_id || null;
|
||||||
const { audioBuffer: decodedBuffer, channelInfo } = await window.SonicAudio.decodeAudioFile(file);
|
const { audioBuffer: decodedBuffer, channelInfo } = await window.SonicAudio.decodeAudioFile(file);
|
||||||
const clipId = `clip_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
const clipId = `clip_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
||||||
const newClip = { id: clipId, buffer: decodedBuffer, startTime: offset, name: file.name, speed: 1.0 };
|
const newClip = { id: clipId, buffer: decodedBuffer, startTime: offset, name: file.name, speed: 1.0, serverFileId: fileId };
|
||||||
createClipWithUndo(selectedTrackId, newClip);
|
createClipWithUndo(selectedTrackId, newClip);
|
||||||
updateActiveTracks(prev => prev.map(t => t.id === selectedTrackId ? {
|
updateActiveTracks(prev => prev.map(t => t.id === selectedTrackId ? {
|
||||||
...t,
|
...t,
|
||||||
|
serverFileId: t.serverFileId || fileId,
|
||||||
clips: [...(t.clips || []), newClip]
|
clips: [...(t.clips || []), newClip]
|
||||||
} : t));
|
} : t));
|
||||||
offset += decodedBuffer.duration;
|
offset += decodedBuffer.duration;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -849,6 +849,12 @@
|
|||||||
- **Các file ảnh hưởng:** `app/templates/index.html`, `app/static/js/app.jsx`
|
- **Các file ảnh hưởng:** `app/templates/index.html`, `app/static/js/app.jsx`
|
||||||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||||||
|
|
||||||
|
### [2026-07-30 20:52] Task: Fix audio clip items lost after reload
|
||||||
|
- **Tóm tắt thay đổi:** 3 root causes: (1) `serverFileId` không được lưu vào clip/track khi upload audio — `insertSoundClipAtCursor` gọi `uploadToServer` nhưng chỉ lưu vào global `serverFileIdMap`, không set `t.serverFileId`; (2) Serialize dùng `t.serverFileId` (null) → `audio_file_url` rỗng → Deserialize không lấy được fileId → `loadAudioBuffersForTracks` không load được buffer; (3) WaveformLane canvas `clip.buffer.numberOfChannels` crash khi buffer null → clip items không vẽ được. Fix: set `serverFileId` trên clip + track khi upload; serialize/deserialize `server_file_id` trong `source_data`; guard `if (!clip.buffer) return` trong WaveformLane.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||||||
|
- **Ghi chú/Test (nếu có):** `npm run build` pass. Chèn audio clip → lưu project → reload → clip hiển thị + waveform load lại.
|
||||||
|
---
|
||||||
|
|
||||||
### [2026-07-30 20:46] Task: Auto-save 5min + Backup 30min + Backup management UI
|
### [2026-07-30 20:46] Task: Auto-save 5min + Backup 30min + Backup management UI
|
||||||
- **Tóm tắt thay đổi:** (1) DB: thêm bảng `project_backups` + migration cột `is_backup`/`original_id`; (2) Backend: `POST/POST /cloud/{id}/backup`, `GET /cloud/{id}/backups`, `DELETE /cloud/backups/{id}`, `POST /cloud/backups/cleanup` — tự động giới hạn retention (mặc định 10, cấu hình 5-20); (3) Frontend API: 4 methods backup; (4) App: `setInterval` 5 phút auto-save + 30 phút backup; (5) ProfileModal: nút Backup (hiện số lượng) mở rộng danh sách, xóa từng bản, thanh cấu hình retention slider 5-20, nút dọn dẹp.
|
- **Tóm tắt thay đổi:** (1) DB: thêm bảng `project_backups` + migration cột `is_backup`/`original_id`; (2) Backend: `POST/POST /cloud/{id}/backup`, `GET /cloud/{id}/backups`, `DELETE /cloud/backups/{id}`, `POST /cloud/backups/cleanup` — tự động giới hạn retention (mặc định 10, cấu hình 5-20); (3) Frontend API: 4 methods backup; (4) App: `setInterval` 5 phút auto-save + 30 phút backup; (5) ProfileModal: nút Backup (hiện số lượng) mở rộng danh sách, xóa từng bản, thanh cấu hình retention slider 5-20, nút dọn dẹp.
|
||||||
- **Các file ảnh hưởng:** `app/models/user.py`, `app/api/v1/projects.py`, `app/static/js/services/api.js`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
- **Các file ảnh hưởng:** `app/models/user.py`, `app/api/v1/projects.py`, `app/static/js/services/api.js`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||||||
|
|||||||
Reference in New Issue
Block a user