feat: thêm soundfont và VSTi cho MIDI
This commit is contained in:
@@ -46,6 +46,43 @@ const AIGateway = (function() {
|
||||
name: 'export_audio', description: 'Xuất file WAV/MP3/OGG và tải về', parameters: { type: 'object', properties: { track_id: { type: 'string' }, format: { type: 'string', enum: ['wav', 'mp3', 'ogg'] }, sample_rate: { type: 'string', enum: ['22500', '44100'] }, bit_depth: { type: 'string', enum: ['8', '16', '24'] }, quality: { type: 'string', enum: ['44khz', 'lossless'] }, channels: { type: 'string', enum: ['mono', 'stereo'] }, start_time: { type: 'number' }, end_time: { type: 'number' }, start_bar: { type: 'number' }, length_bars: { type: 'number' } }, required: ['format'] }
|
||||
}, {
|
||||
name: 'fade_out', description: 'Fade-out clip (0.5s đến max)', parameters: { type: 'object', properties: { track_id: { type: 'string' }, duration_seconds: { type: 'number' }, clip_index: { type: 'number', description: 'Chỉ số của clip trên track (1-based, ví dụ: 1 cho clip 1, 2 cho clip 2)' }, clip_id: { type: 'string', description: 'ID của clip cụ thể' } } }
|
||||
}, {
|
||||
name: 'generate_multitrack_midi',
|
||||
description: 'Generates multi-track MIDI data based on genre, bar duration, and requested instruments list.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
composition_title: { type: 'string', description: 'Title of the musical piece (e.g., Epic Orchestra Intro 8-Bars)' },
|
||||
bpm: { type: 'integer' },
|
||||
total_bars: { type: 'integer' },
|
||||
tracks: {
|
||||
type: 'array',
|
||||
description: 'Array of instrument tracks along with their corresponding MIDI notes',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
track_name: { type: 'string', description: 'Track name (e.g., String Ensemble, Epic Brass, Taiko Drums)' },
|
||||
instrument_type: { type: 'string', enum: ['STRINGS', 'BRASS', 'SYNTH', 'PERCUSSION', 'DRUMS'] },
|
||||
notes: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
pitch: { type: 'integer', description: 'MIDI note pitch from 0 to 127 (e.g., C4 = 60, C3 = 48)' },
|
||||
start_beat: { type: 'number', description: 'Note start position in beats (from 0.0 to total_bars * 4.0)' },
|
||||
duration_beats: { type: 'number', description: 'Note length in beats (e.g., Quarter note = 1.0, Eighth note = 0.5)' },
|
||||
velocity: { type: 'number', description: 'Keypress velocity intensity from 0.0 to 1.0' }
|
||||
},
|
||||
required: ['pitch', 'start_beat', 'duration_beats', 'velocity']
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['track_name', 'instrument_type', 'notes']
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['composition_title', 'bpm', 'total_bars', 'tracks']
|
||||
}
|
||||
}];
|
||||
|
||||
function parseOrigin(urlStr) {
|
||||
@@ -139,12 +176,13 @@ const AIGateway = (function() {
|
||||
return calls;
|
||||
}
|
||||
|
||||
function buildUserMessage(prompt, context) {
|
||||
function buildUserMessage(prompt, context, systemInstruction = '') {
|
||||
const contextStr = JSON.stringify(context, null, 2);
|
||||
const toolNames = DEFAULT_TOOLS.map(t => ` - ${t.name}: ${t.description}`).join('\n');
|
||||
return [
|
||||
{ role: 'system', content: `Bạn là trợ lý điều khiển DAW chuyên nghiệp.
|
||||
Nhiệm vụ của bạn là phân tích yêu cầu của người dùng và chuyển đổi thành danh sách các function calls tương ứng.
|
||||
${systemInstruction ? `\nHướng dẫn tạo nhạc đặc biệt từ Preset:\n${systemInstruction}\n` : ''}
|
||||
QUAN TRỌNG:
|
||||
1. Bạn đang hoạt động ở chế độ một lượt (one-shot). Hãy trả về TẤT CẢ các function calls cần thiết để thực hiện toàn bộ các bước trong yêu cầu của người dùng trong một phản hồi duy nhất. Đừng thực hiện từng bước qua nhiều lượt chat.
|
||||
2. Có thể gọi nhiều function cùng một lúc (gọi song song/nối tiếp). Chúng sẽ được thực thi theo thứ tự bạn trả về.
|
||||
@@ -187,8 +225,8 @@ Ví dụ: "Hãy chọn và copy từ bar 4 đến bar 12 của track 1 sau đó
|
||||
};
|
||||
}
|
||||
|
||||
async function executeAIPrompt({ prompt, provider, model, apiKey, baseUrl, dawContext, tools }) {
|
||||
const messages = buildUserMessage(prompt, dawContext);
|
||||
async function executeAIPrompt({ prompt, provider, model, apiKey, baseUrl, dawContext, tools, systemInstruction }) {
|
||||
const messages = buildUserMessage(prompt, dawContext, systemInstruction);
|
||||
const toolList = tools || DEFAULT_TOOLS;
|
||||
|
||||
const completion = await callLLM({
|
||||
|
||||
@@ -60,6 +60,25 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
|
||||
saveAIConfigs: (providers) => apiRequest('/api/v1/user/config/ai', { method: 'POST', body: JSON.stringify({ providers }) }),
|
||||
|
||||
getPreferences: () => apiRequest('/api/v1/user/preferences', { method: 'GET' }),
|
||||
savePreferences: (prefs) => apiRequest('/api/v1/user/preferences', { method: 'POST', body: JSON.stringify({ preferences: prefs }) })
|
||||
savePreferences: (prefs) => apiRequest('/api/v1/user/preferences', { method: 'POST', body: JSON.stringify({ preferences: prefs }) }),
|
||||
|
||||
listPlugins: () => apiRequest('/api/v1/plugins/available', { method: 'GET' }),
|
||||
listDefaultSoundfonts: () => apiRequest('/api/v1/plugins/default-soundfonts', { method: 'GET' }),
|
||||
renderProject: (projectJson, outputFilename) => apiRequest('/api/v1/plugins/render', { method: 'POST', body: JSON.stringify({ project_json: projectJson, output_filename: outputFilename }) }),
|
||||
uploadSoundFont: async (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const token = localStorage.getItem('sonic_token') || '';
|
||||
const resp = await fetch(`${window.API_BASE_URL}/api/v1/plugins/upload-soundfont`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
body: formData
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Upload failed');
|
||||
}
|
||||
return resp.json();
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -71,6 +71,7 @@ const DAWCommandDispatcher = (function() {
|
||||
register('CREATE_MIDI_ITEM', (args) => AIGateway.createMidiItem(args));
|
||||
register('MODIFY_MIDI_NOTES', (args) => AIGateway.modifyMidiNotes(args));
|
||||
register('PROCESS_AI_DSP', (args) => AIGateway.processAIDSP(args));
|
||||
register('GENERATE_MULTITRACK_MIDI', (args) => api.generateMultitrackMidi(args));
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// SonicForge Studio SoundFont Player Service
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// Web Audio API fallback synth
|
||||
let audioCtx = null;
|
||||
let gainNode = null;
|
||||
const activeOscillators = {};
|
||||
|
||||
function getCtx() {
|
||||
if (!audioCtx) {
|
||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
gainNode = audioCtx.createGain();
|
||||
gainNode.gain.value = 0.3;
|
||||
gainNode.connect(audioCtx.destination);
|
||||
}
|
||||
return audioCtx;
|
||||
}
|
||||
|
||||
const SonicSF = {
|
||||
loadedFonts: {},
|
||||
|
||||
// Load SoundFont from URL → ArrayBuffer → store in memory
|
||||
loadSoundFont: async function (url) {
|
||||
if (this.loadedFonts[url]) return this.loadedFonts[url];
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) throw new Error('Failed to load SoundFont: ' + url);
|
||||
const buffer = await resp.arrayBuffer();
|
||||
this.loadedFonts[url] = buffer;
|
||||
return buffer;
|
||||
},
|
||||
|
||||
// Play a MIDI note using Web Audio fallback
|
||||
playNote: function (note, velocity, durationMs) {
|
||||
const ctx = getCtx();
|
||||
const freq = 440 * Math.pow(2, (note - 69) / 12);
|
||||
const osc = ctx.createOscillator();
|
||||
const noteGain = ctx.createGain();
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.value = freq;
|
||||
noteGain.gain.setValueAtTime(velocity / 127 * 0.3, ctx.currentTime);
|
||||
noteGain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + durationMs / 1000);
|
||||
osc.connect(noteGain);
|
||||
noteGain.connect(ctx.destination);
|
||||
osc.start(ctx.currentTime);
|
||||
osc.stop(ctx.currentTime + durationMs / 1000 + 0.05);
|
||||
activeOscillators[note] = osc;
|
||||
return osc;
|
||||
},
|
||||
|
||||
stopAll: function () {
|
||||
Object.values(activeOscillators).forEach(osc => {
|
||||
try { osc.stop(); } catch (e) { }
|
||||
});
|
||||
Object.keys(activeOscillators).forEach(k => delete activeOscillators[k]);
|
||||
},
|
||||
|
||||
// Save user SoundFont to IndexedDB via window.SonicStorage
|
||||
saveToIndexedDB: async function (name, arrayBuffer) {
|
||||
if (window.SonicStorage && window.SonicStorage.saveToIndexedDB) {
|
||||
await window.SonicStorage.saveToIndexedDB('soundfont_' + name, arrayBuffer);
|
||||
}
|
||||
},
|
||||
|
||||
// Load user SoundFont from IndexedDB
|
||||
loadFromIndexedDB: async function (name) {
|
||||
if (window.SonicStorage && window.SonicStorage.loadFromIndexedDB) {
|
||||
return await window.SonicStorage.loadFromIndexedDB('soundfont_' + name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
window.SonicSF = SonicSF;
|
||||
})();
|
||||
Reference in New Issue
Block a user