FIX: sửa lỗi parse file midi có track hoặc chunk phụ trợ
This commit is contained in:
+135
-4
@@ -14852,11 +14852,142 @@ const App = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
// ── MIDI .mid file parser ──
|
||||
const parseMidiFile = (arrayBuffer) => {
|
||||
const data = new Uint8Array(arrayBuffer);
|
||||
if (data.length < 14) return null;
|
||||
var pos = 0;
|
||||
var read32 = function() { var v = (data[pos] << 24) | (data[pos+1] << 16) | (data[pos+2] << 8) | data[pos+3]; pos += 4; return v; };
|
||||
var read16 = function() { var v = (data[pos] << 8) | data[pos+1]; pos += 2; return v; };
|
||||
var readVLQ = function() { var v = 0, b; do { b = data[pos++]; v = (v << 7) | (b & 0x7f); } while (b & 0x80); return v; };
|
||||
|
||||
var header = String.fromCharCode(data[0], data[1], data[2], data[3]);
|
||||
if (header !== 'MThd') return null;
|
||||
read32(); var fmt = read16(); var numTracks = read16(); var division = read16();
|
||||
var ticksPerBeat = (division & 0x8000) ? 480 : (division || 480);
|
||||
var bpm = 120;
|
||||
var result = [];
|
||||
for (var t = 0; t < numTracks; t++) {
|
||||
if (pos + 8 > data.length) break;
|
||||
var trkId = String.fromCharCode(data[pos], data[pos+1], data[pos+2], data[pos+3]);
|
||||
pos += 4; var trkLen = read32(); var endPos = Math.min(pos + trkLen, data.length);
|
||||
if (trkId !== 'MTrk') {
|
||||
pos = endPos;
|
||||
continue;
|
||||
}
|
||||
var absTicks = 0; var runningStatus = 0; var trackName = 'MIDI Track ' + (t + 1);
|
||||
var midiNotes = []; var pendingNotes = {};
|
||||
var maxAbsTick = 0;
|
||||
while (pos < endPos) {
|
||||
try {
|
||||
var delta = readVLQ(); absTicks += delta;
|
||||
var status = data[pos];
|
||||
if (status >= 0x80) {
|
||||
if (status < 0xf0) {
|
||||
runningStatus = status;
|
||||
}
|
||||
pos++;
|
||||
} else {
|
||||
status = runningStatus;
|
||||
}
|
||||
var cmd = status >> 4;
|
||||
if (cmd === 0x9 || cmd === 0x8) {
|
||||
var chan = status & 0x0F;
|
||||
var pitch = data[pos++]; var vel = pos < endPos ? data[pos++] : 0;
|
||||
var noteKey = chan + '_' + pitch;
|
||||
if (cmd === 0x9 && vel > 0) {
|
||||
pendingNotes[noteKey] = { tick: absTicks, vel: vel };
|
||||
if (absTicks > maxAbsTick) maxAbsTick = absTicks;
|
||||
} else {
|
||||
var pn = pendingNotes[noteKey];
|
||||
if (pn) {
|
||||
var durTicks = absTicks - pn.tick;
|
||||
if (durTicks <= 0) durTicks = 240;
|
||||
midiNotes.push({ id: 'mn_' + t + '_' + pitch + '_' + pn.tick, pitch: pitch, start_beat: pn.tick / ticksPerBeat, duration_beats: durTicks / ticksPerBeat, velocity: Math.min(1, pn.vel / 127) });
|
||||
delete pendingNotes[noteKey];
|
||||
}
|
||||
}
|
||||
} else if (status >= 0xc0 && status < 0xe0) { if (pos < endPos) pos += 1; }
|
||||
else if (status >= 0xe0 && status < 0xf0) { if (pos + 2 <= endPos) pos += 2; }
|
||||
else if (status >= 0xa0 && status < 0xc0) { if (pos + 2 <= endPos) pos += 2; }
|
||||
else if (status >= 0xf0 && status < 0xf8) { if (pos < endPos) { var sl = readVLQ(); pos += sl; } }
|
||||
else if (status === 0xff) {
|
||||
if (pos >= endPos) break;
|
||||
var metaType = data[pos++]; var metaLen = readVLQ();
|
||||
if (metaType === 0x03) { try { trackName = String.fromCharCode.apply(null, Array.from(data.subarray(pos, pos + metaLen))); } catch(e) {} }
|
||||
else if (metaType === 0x51 && pos + 3 <= endPos) { bpm = Math.round(60000000 / ((data[pos] << 16) | (data[pos+1] << 8) | data[pos+2])); }
|
||||
pos += Math.min(metaLen, endPos - pos);
|
||||
}
|
||||
else { if (pos + 2 <= endPos) pos += 2; }
|
||||
} catch(e) { pos = endPos; }
|
||||
}
|
||||
Object.keys(pendingNotes).forEach(function(k) {
|
||||
var pn = pendingNotes[k];
|
||||
var parts = k.split('_');
|
||||
var p = parseInt(parts[1]);
|
||||
var dur = Math.max(240, maxAbsTick - pn.tick);
|
||||
midiNotes.push({ id: 'mn_' + t + '_' + p + '_' + pn.tick, pitch: p, start_beat: pn.tick / ticksPerBeat, duration_beats: dur / ticksPerBeat, velocity: Math.min(1, pn.vel / 127) });
|
||||
});
|
||||
if (midiNotes.length > 0) {
|
||||
var lastEnd = 0;
|
||||
midiNotes.forEach(function(n) { var e = (n.start_beat + n.duration_beats) * 60 / bpm; if (e > lastEnd) lastEnd = e; });
|
||||
result.push({ name: trackName, notes: midiNotes, duration: lastEnd || 4, startTime: 0, id: 'midi_' + t + '_' + Date.now() });
|
||||
}
|
||||
pos = endPos;
|
||||
}
|
||||
return result.length > 0 ? result : null;
|
||||
};
|
||||
|
||||
// ── Load File on Track (with server upload) ──
|
||||
const loadFileOnTrack = async (trackId, file) => {
|
||||
if (!file) return;
|
||||
showToast(`Đang nạp file ${file.name}...`, 'info');
|
||||
var fileName = file.name || '';
|
||||
var isMidi = /\.mid$|\.midi$/i.test(fileName);
|
||||
showToast(`Đang nạp file ${fileName}...`, 'info');
|
||||
try {
|
||||
if (isMidi) {
|
||||
var arrayBuffer = await file.arrayBuffer();
|
||||
var midiResult = parseMidiFile(arrayBuffer);
|
||||
if (!midiResult || midiResult.length === 0) {
|
||||
showToast('Không tìm thấy nốt nhạc trong file MIDI.', 'error');
|
||||
return;
|
||||
}
|
||||
if (midiResult.length === 1) {
|
||||
updateActiveTracks(prev => prev.map(function(t) {
|
||||
if (t.id !== trackId) return t;
|
||||
return { ...t, name: fileName, midiItems: [midiResult[0]] };
|
||||
}));
|
||||
showToast('Đã tải MIDI: ' + fileName, 'success');
|
||||
} else {
|
||||
var colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309', '#ca8a04', '#dc2626', '#0891b2'];
|
||||
var firstDone = false;
|
||||
midiResult.forEach(function(midiItem, idx) {
|
||||
if (!firstDone) {
|
||||
updateActiveTracks(prev => prev.map(function(t) {
|
||||
if (t.id !== trackId) return t;
|
||||
return { ...t, name: midiItem.name || fileName, midiItems: [midiItem] };
|
||||
}));
|
||||
firstDone = true;
|
||||
} else {
|
||||
var newId = 'midi_track_' + Date.now() + '_' + idx;
|
||||
updateActiveTracks(function(prev) {
|
||||
var curLen = prev.length;
|
||||
return prev.concat([{
|
||||
id: newId, name: midiItem.name || 'MIDI Track ' + (idx + 1),
|
||||
buffer: null, startTime: 0, volumeDb: 0, pan: 0,
|
||||
muted: false, solo: false, color: colors[idx % colors.length],
|
||||
markers: [], serverFileId: null, clips: [], sections: [],
|
||||
midiItems: [midiItem],
|
||||
isArmed: false, monitoringEnabled: true,
|
||||
inputSource: { deviceType: 'NONE', deviceId: '' }
|
||||
}]);
|
||||
});
|
||||
}
|
||||
});
|
||||
showToast('Đã tải MIDI: ' + midiResult.length + ' tracks from ' + fileName, 'success');
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Upload to server
|
||||
uploadToServer(file, trackId);
|
||||
|
||||
@@ -14867,13 +14998,13 @@ const App = () => {
|
||||
} = await window.SonicAudio.decodeAudioFile(file);
|
||||
setTracks(prev => prev.map(t => t.id === trackId ? {
|
||||
...t,
|
||||
name: file.name,
|
||||
name: fileName,
|
||||
buffer: decodedBuffer,
|
||||
channelInfo: channelInfo
|
||||
} : t));
|
||||
showToast(`Nạp file thành công: ${file.name} (${channelInfo.label})`, 'success');
|
||||
showToast(`Nạp file thành công: ${fileName} (${channelInfo.label})`, 'success');
|
||||
} catch (err) {
|
||||
showToast("Lỗi giải mã âm thanh. Định dạng file không tương thích.", 'error');
|
||||
showToast(isMidi ? "Lỗi giải mã MIDI." : "Lỗi giải mã âm thanh. Định dạng file không tương thích.", 'error');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user