1655 lines
60 KiB
JavaScript
1655 lines
60 KiB
JavaScript
window.addEventListener('error', (event) => {
|
||
if (event.error && event.error.message && event.error.message.includes('openOrClosedShadowRoot')) {
|
||
console.warn('捕获到Blockly DOM操作错误,这通常是无害的:', event.error.message);
|
||
event.preventDefault();
|
||
return false;
|
||
}
|
||
|
||
if (event.error && event.error.message && (
|
||
event.error.message.includes('DOM') ||
|
||
event.error.message.includes('Element') ||
|
||
event.error.message.includes('Node')
|
||
)) {
|
||
console.warn('捕获到DOM操作错误:', event.error.message);
|
||
event.preventDefault();
|
||
return false;
|
||
}
|
||
});
|
||
|
||
window.addEventListener('unhandledrejection', (event) => {
|
||
if (event.reason && event.reason.message && event.reason.message.includes('openOrClosedShadowRoot')) {
|
||
console.warn('捕获到未处理的Promise拒绝(Blockly DOM错误):', event.reason.message);
|
||
event.preventDefault();
|
||
return false;
|
||
}
|
||
});
|
||
|
||
const originalConsoleError = console.error;
|
||
console.error = function(...args) {
|
||
const message = args.join(' ');
|
||
if (message.includes('openOrClosedShadowRoot') ||
|
||
message.includes('DOM') ||
|
||
message.includes('Element') ||
|
||
message.includes('Node')) {
|
||
console.warn('过滤的DOM错误:', ...args);
|
||
return;
|
||
}
|
||
originalConsoleError.apply(console, args);
|
||
};
|
||
|
||
const safeDOM = {
|
||
appendChild: (parent, child) => {
|
||
try {
|
||
if (parent && child && parent.appendChild) {
|
||
return parent.appendChild(child);
|
||
}
|
||
} catch (error) {
|
||
console.warn('安全DOM添加失败:', error.message);
|
||
}
|
||
return null;
|
||
},
|
||
|
||
removeChild: (parent, child) => {
|
||
try {
|
||
if (parent && child && parent.removeChild && child.parentNode === parent) {
|
||
return parent.removeChild(child);
|
||
}
|
||
} catch (error) {
|
||
console.warn('安全DOM移除失败:', error.message);
|
||
}
|
||
return null;
|
||
},
|
||
|
||
querySelector: (container, selector) => {
|
||
try {
|
||
if (container && container.querySelector) {
|
||
return container.querySelector(selector);
|
||
}
|
||
} catch (error) {
|
||
console.warn('安全DOM查询失败:', error.message);
|
||
}
|
||
return null;
|
||
},
|
||
|
||
exists: (element) => {
|
||
return element && element.parentNode && document.contains(element);
|
||
}
|
||
};
|
||
|
||
const sound = {
|
||
|
||
volume: 100,
|
||
|
||
effects: {
|
||
pitch: 0,
|
||
pan: 0
|
||
},
|
||
|
||
builtin: {
|
||
"Meow": "meow"
|
||
},
|
||
|
||
isRecording: false,
|
||
mediaRecorder: null,
|
||
recordedChunks: [],
|
||
recordedAudio: null,
|
||
|
||
activeAudios: [],
|
||
|
||
isStopped: false,
|
||
abortController: null,
|
||
|
||
blockAllAudio: false,
|
||
|
||
audioHistory: [],
|
||
|
||
soundQueue: [],
|
||
isProcessingQueue: false,
|
||
currentlyPlaying: null,
|
||
|
||
initAudioContext: () => {
|
||
if (!sound.audioContext) {
|
||
try {
|
||
sound.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||
} catch (error) {
|
||
console.error("Error initializing audio context:", error);
|
||
}
|
||
}
|
||
},
|
||
|
||
createAudio: (src) => {
|
||
if (sound.blockAllAudio || sound.isStopped ||
|
||
(sound.abortController && sound.abortController.signal.aborted)) {
|
||
const fakeAudio = {
|
||
play: () => Promise.reject(new Error('Audio playback blocked')),
|
||
pause: () => {},
|
||
currentTime: 0,
|
||
src: '',
|
||
volume: 1,
|
||
playbackRate: 1,
|
||
onended: null,
|
||
onerror: null,
|
||
onloadstart: null,
|
||
oncanplay: null,
|
||
onplay: null,
|
||
onpause: null,
|
||
tagName: 'AUDIO',
|
||
addEventListener: () => {},
|
||
removeEventListener: () => {},
|
||
load: () => {},
|
||
duration: 0,
|
||
ended: false,
|
||
paused: true,
|
||
muted: false,
|
||
readyState: 0,
|
||
networkState: 0,
|
||
preload: 'none'
|
||
};
|
||
return fakeAudio;
|
||
}
|
||
return new Audio(src);
|
||
},
|
||
|
||
play: async (name) => {
|
||
try {
|
||
if (sound.isStopped) {
|
||
return;
|
||
}
|
||
|
||
if (sound.abortController && sound.abortController.signal.aborted) {
|
||
return;
|
||
}
|
||
|
||
if (sound.blockAllAudio) {
|
||
return;
|
||
}
|
||
|
||
if (sound.soundQueue.length > 0 || sound.isProcessingQueue) {
|
||
sound.soundQueue.push({
|
||
name,
|
||
resolve: () => {},
|
||
reject: () => {}
|
||
});
|
||
sound.processQueue();
|
||
return;
|
||
}
|
||
|
||
if (sound.activeAudios.length > 0) {
|
||
|
||
sound.activeAudios.forEach(audio => {
|
||
try {
|
||
if (audio && audio.tagName === 'AUDIO') {
|
||
audio.pause();
|
||
audio.currentTime = 0;
|
||
audio.src = '';
|
||
}
|
||
} catch (error) {
|
||
console.warn("停止现有音频时出错:", error);
|
||
}
|
||
});
|
||
sound.activeAudios = [];
|
||
}
|
||
|
||
if (name.startsWith('recording') && sound.builtin[name]) {
|
||
if (sound.blockAllAudio || sound.isStopped) {
|
||
return;
|
||
}
|
||
|
||
const audio = sound.createAudio(sound.builtin[name]);
|
||
audio.volume = sound.volume / 100;
|
||
|
||
const currentPitch = sound.effects.pitch;
|
||
const currentPan = sound.effects.pan;
|
||
|
||
if (currentPitch !== 0 || currentPan !== 0) {
|
||
sound.initAudioContext();
|
||
if (sound.audioContext) {
|
||
try {
|
||
const source = sound.audioContext.createMediaElementSource(audio);
|
||
const gainNode = sound.audioContext.createGain();
|
||
|
||
if (currentPitch !== 0) {
|
||
const pitchShift = sound.audioContext.createBiquadFilter();
|
||
pitchShift.type = 'peaking';
|
||
pitchShift.frequency.setValueAtTime(1000, sound.audioContext.currentTime);
|
||
pitchShift.Q.setValueAtTime(1, sound.audioContext.currentTime);
|
||
|
||
const pitchGain = Math.max(-20, Math.min(20, currentPitch * 0.8));
|
||
pitchShift.gain.setValueAtTime(pitchGain, sound.audioContext.currentTime);
|
||
|
||
source.connect(pitchShift);
|
||
pitchShift.connect(gainNode);
|
||
} else {
|
||
source.connect(gainNode);
|
||
}
|
||
|
||
if (currentPan !== 0) {
|
||
const panNode = sound.audioContext.createStereoPanner();
|
||
const panValue = Math.max(-1, Math.min(1, (currentPan / 100) * 1.5));
|
||
panNode.pan.setValueAtTime(panValue, sound.audioContext.currentTime);
|
||
|
||
gainNode.connect(panNode);
|
||
panNode.connect(sound.audioContext.destination);
|
||
} else {
|
||
gainNode.connect(sound.audioContext.destination);
|
||
}
|
||
|
||
gainNode.gain.setValueAtTime(sound.volume / 100, sound.audioContext.currentTime);
|
||
} catch (error) {
|
||
console.warn("应用增强音效失败,使用默认播放:", error);
|
||
audio.play().catch(error => {
|
||
if (error.name !== 'AbortError') {
|
||
console.error("Error playing recorded audio:", error);
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
} else {
|
||
console.warn("音频上下文初始化失败,无法应用增强音效");
|
||
audio.play().catch(error => {
|
||
if (error.name !== 'AbortError') {
|
||
console.error("Error playing recorded audio:", error);
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
} else {
|
||
audio.play().catch(error => {
|
||
if (error.name !== 'AbortError') {
|
||
console.error("Error playing recorded audio:", error);
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
|
||
sound.activeAudios.push(audio);
|
||
|
||
audio.onended = () => {
|
||
const index = sound.activeAudios.indexOf(audio);
|
||
if (index > -1) {
|
||
sound.activeAudios.splice(index, 1);
|
||
}
|
||
};
|
||
|
||
audio.play().catch(error => {
|
||
if (error.name !== 'AbortError') {
|
||
console.error("Error playing recorded audio:", error);
|
||
}
|
||
const index = sound.activeAudios.indexOf(audio);
|
||
if (index > -1) {
|
||
sound.activeAudios.splice(index, 1);
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (name === "Meow") {
|
||
sound.initAudioContext();
|
||
|
||
if (sound.audioContext) {
|
||
const oscillator = sound.audioContext.createOscillator();
|
||
const gainNode = sound.audioContext.createGain();
|
||
|
||
let frequency = 440;
|
||
|
||
if (sound.effects.pitch !== 0) {
|
||
frequency *= Math.pow(2, sound.effects.pitch / 12);
|
||
}
|
||
|
||
oscillator.frequency.setValueAtTime(frequency, sound.audioContext.currentTime);
|
||
oscillator.type = 'sine';
|
||
|
||
gainNode.gain.setValueAtTime(sound.volume / 100, sound.audioContext.currentTime);
|
||
|
||
if (sound.effects.pan !== 0) {
|
||
const panNode = sound.audioContext.createStereoPanner();
|
||
const panValue = sound.effects.pan / 100;
|
||
panNode.pan.setValueAtTime(panValue, sound.audioContext.currentTime);
|
||
|
||
oscillator.connect(panNode);
|
||
panNode.connect(gainNode);
|
||
} else {
|
||
oscillator.connect(gainNode);
|
||
}
|
||
|
||
gainNode.connect(sound.audioContext.destination);
|
||
|
||
oscillator.start();
|
||
oscillator.stop(sound.audioContext.currentTime + 0.5);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("Error in sound.play:", error);
|
||
}
|
||
},
|
||
|
||
_playAudioInternal: async (name) => {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
if (sound.isStopped || sound.blockAllAudio) {
|
||
reject(new Error('Playback blocked'));
|
||
return;
|
||
}
|
||
|
||
if (name.startsWith('recording') && sound.builtin[name]) {
|
||
const audio = sound.createAudio(sound.builtin[name]);
|
||
audio.volume = sound.volume / 100;
|
||
|
||
const currentPitch = sound.effects.pitch;
|
||
const currentPan = sound.effects.pan;
|
||
|
||
if (currentPitch !== 0 || currentPan !== 0) {
|
||
sound.initAudioContext();
|
||
if (sound.audioContext) {
|
||
try {
|
||
const source = sound.audioContext.createMediaElementSource(audio);
|
||
const gainNode = sound.audioContext.createGain();
|
||
|
||
if (currentPitch !== 0) {
|
||
const pitchShift = sound.audioContext.createBiquadFilter();
|
||
pitchShift.type = 'peaking';
|
||
pitchShift.frequency.setValueAtTime(1000, sound.audioContext.currentTime);
|
||
pitchShift.Q.setValueAtTime(1, sound.audioContext.currentTime);
|
||
const pitchGain = Math.max(-20, Math.min(20, currentPitch * 0.8));
|
||
pitchShift.gain.setValueAtTime(pitchGain, sound.audioContext.currentTime);
|
||
source.connect(pitchShift);
|
||
pitchShift.connect(gainNode);
|
||
} else {
|
||
source.connect(gainNode);
|
||
}
|
||
|
||
if (currentPan !== 0) {
|
||
const panNode = sound.audioContext.createStereoPanner();
|
||
const panValue = Math.max(-1, Math.min(1, (currentPan / 100) * 1.5));
|
||
panNode.pan.setValueAtTime(panValue, sound.audioContext.currentTime);
|
||
gainNode.connect(panNode);
|
||
panNode.connect(sound.audioContext.destination);
|
||
} else {
|
||
gainNode.connect(sound.audioContext.destination);
|
||
}
|
||
|
||
gainNode.gain.setValueAtTime(sound.volume / 100, sound.audioContext.currentTime);
|
||
} catch (error) {
|
||
console.warn("应用音效失败:", error);
|
||
}
|
||
}
|
||
}
|
||
|
||
audio.onended = () => {
|
||
const index = sound.activeAudios.indexOf(audio);
|
||
if (index > -1) {
|
||
sound.activeAudios.splice(index, 1);
|
||
}
|
||
resolve();
|
||
};
|
||
|
||
audio.onerror = (error) => {
|
||
console.error(`音频播放失败: ${name}`, error);
|
||
const index = sound.activeAudios.indexOf(audio);
|
||
if (index > -1) {
|
||
sound.activeAudios.splice(index, 1);
|
||
}
|
||
reject(error);
|
||
};
|
||
|
||
sound.activeAudios.push(audio);
|
||
audio.play().catch(error => {
|
||
if (error.name === 'AbortError') {
|
||
resolve();
|
||
} else {
|
||
console.error("播放音频失败:", error);
|
||
reject(error);
|
||
}
|
||
});
|
||
|
||
} else if (name === "Meow") {
|
||
sound.initAudioContext();
|
||
if (sound.audioContext) {
|
||
const oscillator = sound.audioContext.createOscillator();
|
||
const gainNode = sound.audioContext.createGain();
|
||
|
||
let frequency = 440;
|
||
if (sound.effects.pitch !== 0) {
|
||
frequency *= Math.pow(2, sound.effects.pitch / 12);
|
||
}
|
||
|
||
oscillator.frequency.setValueAtTime(frequency, sound.audioContext.currentTime);
|
||
oscillator.type = 'sine';
|
||
gainNode.gain.setValueAtTime(sound.volume / 100, sound.audioContext.currentTime);
|
||
|
||
if (sound.effects.pan !== 0) {
|
||
const panNode = sound.audioContext.createStereoPanner();
|
||
const panValue = sound.effects.pan / 100;
|
||
panNode.pan.setValueAtTime(panValue, sound.audioContext.currentTime);
|
||
oscillator.connect(panNode);
|
||
panNode.connect(gainNode);
|
||
} else {
|
||
oscillator.connect(gainNode);
|
||
}
|
||
|
||
gainNode.connect(sound.audioContext.destination);
|
||
|
||
oscillator.start();
|
||
oscillator.stop(sound.audioContext.currentTime + 0.5);
|
||
|
||
setTimeout(() => {
|
||
resolve();
|
||
}, 500);
|
||
} else {
|
||
reject(new Error('AudioContext not available'));
|
||
}
|
||
} else {
|
||
reject(new Error(`Unknown sound: ${name}`));
|
||
}
|
||
} catch (error) {
|
||
console.error("内部播放错误:", error);
|
||
reject(error);
|
||
}
|
||
});
|
||
},
|
||
|
||
processQueue: async () => {
|
||
if (sound.isProcessingQueue || sound.soundQueue.length === 0) {
|
||
return;
|
||
}
|
||
|
||
sound.isProcessingQueue = true;
|
||
|
||
while (sound.soundQueue.length > 0) {
|
||
const queueItem = sound.soundQueue.shift();
|
||
|
||
try {
|
||
if (queueItem.type === 'frequency') {
|
||
const { frequency, duration, resolve } = queueItem;
|
||
|
||
sound.currentlyPlaying = { type: 'frequency', frequency, startTime: Date.now() };
|
||
|
||
await new Promise((freqResolve) => {
|
||
sound.initAudioContext();
|
||
|
||
if (sound.audioContext) {
|
||
const oscillator = sound.audioContext.createOscillator();
|
||
const gainNode = sound.audioContext.createGain();
|
||
|
||
oscillator.frequency.setValueAtTime(frequency, sound.audioContext.currentTime);
|
||
oscillator.type = 'sine';
|
||
|
||
const currentPitch = sound.effects.pitch;
|
||
if (currentPitch !== 0) {
|
||
oscillator.frequency.setValueAtTime(
|
||
frequency * Math.pow(2, currentPitch / 12),
|
||
sound.audioContext.currentTime
|
||
);
|
||
}
|
||
|
||
gainNode.gain.setValueAtTime(sound.volume / 100, sound.audioContext.currentTime);
|
||
oscillator.connect(gainNode);
|
||
gainNode.connect(sound.audioContext.destination);
|
||
|
||
oscillator.onended = () => {
|
||
sound.currentlyPlaying = null;
|
||
freqResolve();
|
||
};
|
||
|
||
oscillator.start();
|
||
oscillator.stop(sound.audioContext.currentTime + duration / 1000);
|
||
} else {
|
||
freqResolve();
|
||
}
|
||
});
|
||
|
||
sound.currentlyPlaying = null;
|
||
resolve();
|
||
} else {
|
||
const { name, resolve } = queueItem;
|
||
|
||
sound.currentlyPlaying = { name, startTime: Date.now() };
|
||
await sound._playAudioInternal(name);
|
||
sound.currentlyPlaying = null;
|
||
resolve();
|
||
}
|
||
} catch (error) {
|
||
console.error(`队列播放失败:`, error);
|
||
sound.currentlyPlaying = null;
|
||
if (queueItem.reject) {
|
||
queueItem.reject(error);
|
||
}
|
||
}
|
||
}
|
||
|
||
sound.isProcessingQueue = false;
|
||
},
|
||
|
||
play_blocking: (name) => {
|
||
return new Promise((resolve, reject) => {
|
||
sound.soundQueue.push({ name, resolve, reject });
|
||
sound.processQueue();
|
||
});
|
||
},
|
||
|
||
stop_all: () => {
|
||
try {
|
||
sound.isStopped = true;
|
||
sound.blockAllAudio = true;
|
||
|
||
if (sound.soundQueue.length > 0) {
|
||
sound.soundQueue.forEach(({ reject }) => {
|
||
reject(new Error('Playback stopped'));
|
||
});
|
||
sound.soundQueue = [];
|
||
}
|
||
sound.isProcessingQueue = false;
|
||
sound.currentlyPlaying = null;
|
||
|
||
if (sound.abortController) {
|
||
sound.abortController.abort();
|
||
}
|
||
sound.abortController = new AbortController();
|
||
|
||
if (sound.activeAudios.length > 0) {
|
||
sound.activeAudios.forEach((audio) => {
|
||
try {
|
||
if (audio && audio.tagName === 'AUDIO') {
|
||
audio.pause();
|
||
audio.currentTime = 0;
|
||
audio.src = '';
|
||
audio.load();
|
||
audio.onended = null;
|
||
audio.onerror = null;
|
||
audio.onloadstart = null;
|
||
audio.oncanplay = null;
|
||
audio.onplay = null;
|
||
audio.onpause = null;
|
||
}
|
||
} catch (error) {
|
||
console.warn(`停止音频时出错:`, error);
|
||
}
|
||
});
|
||
|
||
sound.activeAudios = [];
|
||
}
|
||
|
||
if (sound.audioContext) {
|
||
try {
|
||
sound.audioContext.close();
|
||
sound.audioContext = null;
|
||
} catch (error) {
|
||
console.warn("关闭音频上下文时出错:", error);
|
||
}
|
||
}
|
||
|
||
sound.effects.pitch = 0;
|
||
sound.effects.pan = 0;
|
||
|
||
const allAudioElements = document.querySelectorAll('audio');
|
||
if (allAudioElements.length > 0) {
|
||
allAudioElements.forEach((audio) => {
|
||
try {
|
||
audio.pause();
|
||
audio.currentTime = 0;
|
||
audio.src = '';
|
||
} catch (error) {
|
||
console.warn(`停止页面音频元素时出错:`, error);
|
||
}
|
||
});
|
||
}
|
||
|
||
setTimeout(() => {
|
||
sound.isStopped = false;
|
||
sound.blockAllAudio = false;
|
||
}, 100);
|
||
} catch (error) {
|
||
console.error("Error in sound.stop_all:", error);
|
||
}
|
||
},
|
||
|
||
adjust_volume: (change) => {
|
||
const newVolume = Math.max(0, Math.min(100, sound.volume + change));
|
||
sound.volume = newVolume;
|
||
},
|
||
|
||
set_volume: (value) => {
|
||
sound.volume = Math.max(0, Math.min(100, value));
|
||
},
|
||
|
||
get_volume: () => {
|
||
return sound.volume;
|
||
},
|
||
|
||
noteFrequencies: {
|
||
"NOTE_B3": 247,
|
||
"NOTE_C4": 262,
|
||
"NOTE_D4": 294,
|
||
"NOTE_E4": 330,
|
||
"NOTE_F4": 349,
|
||
"NOTE_G4": 392,
|
||
"NOTE_A4": 440,
|
||
"NOTE_B4": 494,
|
||
"NOTE_C5": 523,
|
||
"NOTE_D5": 587,
|
||
"NOTE_E5": 659,
|
||
"NOTE_F5": 698,
|
||
"NOTE_G5": 784
|
||
},
|
||
|
||
play_frequency: (frequency, duration = 1000) => {
|
||
try {
|
||
sound.initAudioContext();
|
||
|
||
if (sound.audioContext) {
|
||
const oscillator = sound.audioContext.createOscillator();
|
||
const gainNode = sound.audioContext.createGain();
|
||
|
||
oscillator.frequency.setValueAtTime(frequency, sound.audioContext.currentTime);
|
||
oscillator.type = 'sine';
|
||
|
||
const currentPitch = sound.effects.pitch;
|
||
if (currentPitch !== 0) {
|
||
oscillator.frequency.setValueAtTime(
|
||
frequency * Math.pow(2, currentPitch / 12),
|
||
sound.audioContext.currentTime
|
||
);
|
||
}
|
||
|
||
gainNode.gain.setValueAtTime(sound.volume / 100, sound.audioContext.currentTime);
|
||
|
||
oscillator.connect(gainNode);
|
||
gainNode.connect(sound.audioContext.destination);
|
||
|
||
oscillator.start();
|
||
|
||
if (duration > 0) {
|
||
oscillator.stop(sound.audioContext.currentTime + duration / 1000);
|
||
} else {
|
||
oscillator.stop(sound.audioContext.currentTime + 2);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("播放频率声音失败:", error);
|
||
}
|
||
},
|
||
|
||
play_frequency_blocking: (frequency, duration = 1000) => {
|
||
return new Promise((resolve, reject) => {
|
||
const actualDuration = duration > 0 ? duration : 2000;
|
||
|
||
const queueItem = {
|
||
type: 'frequency',
|
||
frequency,
|
||
duration: actualDuration,
|
||
resolve,
|
||
reject
|
||
};
|
||
|
||
sound.soundQueue.push(queueItem);
|
||
sound.processQueue();
|
||
});
|
||
},
|
||
|
||
play_note_list: (noteList) => {
|
||
try {
|
||
const noteSequences = {
|
||
"DADADADUM": [
|
||
{ note: "NOTE_D4", duration: 500 },
|
||
{ note: "NOTE_A4", duration: 500 },
|
||
{ note: "NOTE_D4", duration: 500 },
|
||
{ note: "NOTE_A4", duration: 500 },
|
||
{ note: "NOTE_D4", duration: 500 },
|
||
{ note: "NOTE_A4", duration: 500 },
|
||
{ note: "NOTE_D4", duration: 500 },
|
||
{ note: "NOTE_A4", duration: 500 }
|
||
],
|
||
"BIRTHDAY": [
|
||
{ note: "NOTE_C4", duration: 400 },
|
||
{ note: "NOTE_C4", duration: 400 },
|
||
{ note: "NOTE_D4", duration: 800 },
|
||
{ note: "NOTE_C4", duration: 800 },
|
||
{ note: "NOTE_F4", duration: 800 },
|
||
{ note: "NOTE_E4", duration: 1600 }
|
||
],
|
||
"BA_DING": [
|
||
{ note: "NOTE_C5", duration: 200 },
|
||
{ note: "NOTE_E5", duration: 200 },
|
||
{ note: "NOTE_G5", duration: 400 }
|
||
],
|
||
"JUMP_UP": [
|
||
{ note: "NOTE_C5", duration: 100 },
|
||
{ note: "NOTE_E5", duration: 100 },
|
||
{ note: "NOTE_G5", duration: 100 }
|
||
],
|
||
"JUMP_DOWN": [
|
||
{ note: "NOTE_G5", duration: 100 },
|
||
{ note: "NOTE_E5", duration: 100 },
|
||
{ note: "NOTE_C5", duration: 100 }
|
||
],
|
||
"POWER_UP": [
|
||
{ note: "NOTE_C4", duration: 150 },
|
||
{ note: "NOTE_E4", duration: 150 },
|
||
{ note: "NOTE_G4", duration: 150 },
|
||
{ note: "NOTE_C5", duration: 300 }
|
||
],
|
||
"POWER_DOWN": [
|
||
{ note: "NOTE_C5", duration: 150 },
|
||
{ note: "NOTE_G4", duration: 150 },
|
||
{ note: "NOTE_E4", duration: 150 },
|
||
{ note: "NOTE_C4", duration: 300 }
|
||
]
|
||
};
|
||
|
||
const sequence = noteSequences[noteList];
|
||
if (sequence) {
|
||
let currentTime = 0;
|
||
|
||
sequence.forEach((item) => {
|
||
const frequency = sound.noteFrequencies[item.note] || 440;
|
||
const duration = item.duration;
|
||
|
||
setTimeout(() => {
|
||
sound.play_frequency(frequency, duration);
|
||
}, currentTime);
|
||
|
||
currentTime += duration;
|
||
});
|
||
} else {
|
||
console.warn(`未知的音符列表: ${noteList}`);
|
||
}
|
||
} catch (error) {
|
||
console.error("播放音符列表失败:", error);
|
||
}
|
||
},
|
||
|
||
play_note_list_blocking: (noteList) => {
|
||
return new Promise((resolve, reject) => {
|
||
(async () => {
|
||
try {
|
||
const noteSequences = {
|
||
"DADADADUM": [
|
||
{ note: "NOTE_D4", duration: 500 },
|
||
{ note: "NOTE_A4", duration: 500 },
|
||
{ note: "NOTE_D4", duration: 500 },
|
||
{ note: "NOTE_A4", duration: 500 },
|
||
{ note: "NOTE_D4", duration: 500 },
|
||
{ note: "NOTE_A4", duration: 500 },
|
||
{ note: "NOTE_D4", duration: 500 },
|
||
{ note: "NOTE_A4", duration: 500 }
|
||
],
|
||
"BIRTHDAY": [
|
||
{ note: "NOTE_C4", duration: 400 },
|
||
{ note: "NOTE_C4", duration: 400 },
|
||
{ note: "NOTE_D4", duration: 800 },
|
||
{ note: "NOTE_C4", duration: 800 },
|
||
{ note: "NOTE_F4", duration: 800 },
|
||
{ note: "NOTE_E4", duration: 1600 }
|
||
],
|
||
"BA_DING": [
|
||
{ note: "NOTE_C5", duration: 200 },
|
||
{ note: "NOTE_E5", duration: 200 },
|
||
{ note: "NOTE_G5", duration: 400 }
|
||
],
|
||
"JUMP_UP": [
|
||
{ note: "NOTE_C5", duration: 100 },
|
||
{ note: "NOTE_E5", duration: 100 },
|
||
{ note: "NOTE_G5", duration: 100 }
|
||
],
|
||
"JUMP_DOWN": [
|
||
{ note: "NOTE_G5", duration: 100 },
|
||
{ note: "NOTE_E5", duration: 100 },
|
||
{ note: "NOTE_C5", duration: 100 }
|
||
],
|
||
"POWER_UP": [
|
||
{ note: "NOTE_C4", duration: 150 },
|
||
{ note: "NOTE_E4", duration: 150 },
|
||
{ note: "NOTE_G4", duration: 150 },
|
||
{ note: "NOTE_C5", duration: 300 }
|
||
],
|
||
"POWER_DOWN": [
|
||
{ note: "NOTE_C5", duration: 150 },
|
||
{ note: "NOTE_G4", duration: 150 },
|
||
{ note: "NOTE_E4", duration: 150 },
|
||
{ note: "NOTE_C4", duration: 300 }
|
||
]
|
||
};
|
||
|
||
const sequence = noteSequences[noteList];
|
||
if (sequence) {
|
||
for (const item of sequence) {
|
||
const frequency = sound.noteFrequencies[item.note] || 440;
|
||
await sound.play_frequency_blocking(frequency, item.duration);
|
||
}
|
||
|
||
resolve();
|
||
} else {
|
||
console.warn(`未知的音符列表: ${noteList}`);
|
||
reject(new Error(`未知的音符列表: ${noteList}`));
|
||
}
|
||
} catch (error) {
|
||
console.error("播放音符列表失败:", error);
|
||
reject(error);
|
||
}
|
||
})();
|
||
});
|
||
},
|
||
|
||
adjust_effect: (effect, change) => {
|
||
if (effect === "pitch") {
|
||
sound.effects.pitch = Math.max(-24, Math.min(24, sound.effects.pitch + change));
|
||
} else if (effect === "pan") {
|
||
sound.effects.pan = Math.max(-100, Math.min(100, sound.effects.pan + change));
|
||
}
|
||
},
|
||
|
||
set_effect: (effect, value) => {
|
||
if (effect === "pitch") {
|
||
sound.effects.pitch = Math.max(-24, Math.min(24, value));
|
||
} else if (effect === "pan") {
|
||
sound.effects.pan = Math.max(-100, Math.min(100, value));
|
||
}
|
||
},
|
||
|
||
clear_effects: () => {
|
||
sound.effects.pitch = 0;
|
||
sound.effects.pan = 0;
|
||
},
|
||
|
||
record: () => {
|
||
if (sound.isRecording) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
navigator.mediaDevices.getUserMedia({ audio: true })
|
||
.then(stream => {
|
||
sound.mediaRecorder = new MediaRecorder(stream);
|
||
sound.recordedChunks = [];
|
||
|
||
sound.mediaRecorder.ondataavailable = (event) => {
|
||
if (event.data.size > 0) {
|
||
sound.recordedChunks.push(event.data);
|
||
}
|
||
};
|
||
|
||
sound.mediaRecorder.onstart = () => {
|
||
sound.isRecording = true;
|
||
};
|
||
|
||
sound.mediaRecorder.onstop = () => {
|
||
sound.isRecording = false;
|
||
|
||
const audioBlob = new Blob(sound.recordedChunks, { type: 'audio/wav' });
|
||
sound.recordedAudio = URL.createObjectURL(audioBlob);
|
||
|
||
stream.getTracks().forEach(track => {
|
||
track.stop();
|
||
});
|
||
|
||
sound.showPlaybackInterface(audioBlob);
|
||
};
|
||
|
||
sound.mediaRecorder.onerror = (event) => {
|
||
console.error("MediaRecorder 错误:", event.error);
|
||
sound.isRecording = false;
|
||
};
|
||
|
||
sound.mediaRecorder.start(100);
|
||
|
||
sound.showRecordInterface();
|
||
})
|
||
.catch(error => {
|
||
console.error("获取麦克风权限失败:", error);
|
||
alert("无法访问麦克风,请检查权限设置。错误: " + error.message);
|
||
});
|
||
} catch (error) {
|
||
console.error("录制功能初始化失败:", error);
|
||
alert("录制功能初始化失败: " + error.message);
|
||
}
|
||
},
|
||
|
||
showRecordInterface: () => {
|
||
const recordModal = document.createElement('div');
|
||
recordModal.id = 'recordModal';
|
||
recordModal.style.cssText = `
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 10000;
|
||
`;
|
||
|
||
const recordContent = document.createElement('div');
|
||
recordContent.style.cssText = `
|
||
background: white;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
text-align: center;
|
||
min-width: 300px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
`;
|
||
|
||
recordContent.innerHTML = `
|
||
<h3 style="color: #4a90e2; margin-bottom: 20px;">录制声音</h3>
|
||
<div id="waveform" style="width: 250px; height: 100px; background: #f0f0f0; margin: 20px auto; border-radius: 5px; position: relative; overflow: hidden;">
|
||
<div id="volumeMeter" style="position: absolute; left: 10px; top: 10px; width: 20px; height: 80px; background: linear-gradient(to top, #4CAF50, #FFC107, #F44336); border-radius: 3px;"></div>
|
||
<div id="waveformBars" style="position: absolute; left: 40px; top: 10px; width: 200px; height: 80px;"></div>
|
||
</div>
|
||
<div style="margin: 20px 0;">
|
||
<button id="stopRecord" style="background: #f44336; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; margin: 0 10px;">停止录制</button>
|
||
</div>
|
||
<div id="status" style="color: #666; font-size: 14px; margin-top: 10px;">正在录制...</div>
|
||
`;
|
||
|
||
recordModal.appendChild(recordContent);
|
||
|
||
try {
|
||
safeDOM.appendChild(document.body, recordModal);
|
||
} catch (error) {
|
||
console.error("添加录制界面到DOM失败:", error);
|
||
return;
|
||
}
|
||
|
||
recordModal.addEventListener('click', (event) => {
|
||
if (event.target.id === 'stopRecord') {
|
||
try {
|
||
if (sound.mediaRecorder && sound.isRecording) {
|
||
sound.mediaRecorder.stop();
|
||
} else {
|
||
console.warn("MediaRecorder 不存在或未在录制状态");
|
||
}
|
||
} catch (error) {
|
||
console.error("停止录制时出错:", error);
|
||
alert("停止录制时出错: " + error.message);
|
||
}
|
||
}
|
||
});
|
||
|
||
const waveformBars = recordModal.querySelector('#waveformBars');
|
||
if (waveformBars) {
|
||
for (let i = 0; i < 20; i++) {
|
||
const bar = document.createElement('div');
|
||
bar.style.cssText = `
|
||
position: absolute;
|
||
left: ${i * 10}px;
|
||
bottom: 0;
|
||
width: 8px;
|
||
height: 20px;
|
||
background: #4a90e2;
|
||
border-radius: 2px;
|
||
transition: height 0.1s ease;
|
||
`;
|
||
waveformBars.appendChild(bar);
|
||
}
|
||
|
||
const updateWaveform = () => {
|
||
if (sound.isRecording && recordModal.parentNode) {
|
||
const bars = waveformBars.children;
|
||
for (let i = 0; i < bars.length; i++) {
|
||
const height = Math.random() * 60 + 20;
|
||
bars[i].style.height = height + 'px';
|
||
bars[i].style.background = `hsl(${200 + Math.random() * 60}, 70%, 60%)`;
|
||
}
|
||
}
|
||
};
|
||
|
||
const waveformInterval = setInterval(updateWaveform, 100);
|
||
recordModal.waveformInterval = waveformInterval;
|
||
}
|
||
|
||
let volumeLevel = 0;
|
||
const volumeMeter = recordModal.querySelector('#volumeMeter');
|
||
const statusDiv = recordModal.querySelector('#status');
|
||
|
||
if (volumeMeter && statusDiv) {
|
||
const volumeInterval = setInterval(() => {
|
||
if (sound.isRecording && recordModal.parentNode) {
|
||
volumeLevel = Math.random() * 60 + 40;
|
||
volumeMeter.style.height = volumeLevel + '%';
|
||
statusDiv.textContent = `正在录制... 音量: ${Math.round(volumeLevel)}%`;
|
||
|
||
volumeMeter.style.opacity = 0.8 + (volumeLevel / 100) * 0.2;
|
||
} else {
|
||
clearInterval(volumeInterval);
|
||
if (statusDiv && recordModal.parentNode) {
|
||
statusDiv.textContent = "录制完成";
|
||
statusDiv.style.color = "#4CAF50";
|
||
}
|
||
if (volumeMeter && recordModal.parentNode) {
|
||
volumeMeter.style.height = "100%";
|
||
volumeMeter.style.background = "linear-gradient(to top, #4CAF50, #4CAF50)";
|
||
}
|
||
}
|
||
}, 200);
|
||
|
||
recordModal.volumeInterval = volumeInterval;
|
||
} else {
|
||
console.error("找不到音量计或状态显示元素");
|
||
}
|
||
|
||
const keyHandler = (event) => {
|
||
if (event.code === 'Space' && sound.isRecording) {
|
||
event.preventDefault();
|
||
if (sound.mediaRecorder) {
|
||
sound.mediaRecorder.stop();
|
||
}
|
||
}
|
||
};
|
||
|
||
document.addEventListener('keydown', keyHandler);
|
||
|
||
recordModal.keyHandler = keyHandler;
|
||
},
|
||
|
||
showPlaybackInterface: (audioBlob) => {
|
||
const recordModal = document.getElementById('recordModal');
|
||
if (recordModal) {
|
||
if (recordModal.volumeInterval) clearInterval(recordModal.volumeInterval);
|
||
if (recordModal.waveformInterval) clearInterval(recordModal.waveformInterval);
|
||
if (recordModal.keyHandler) document.removeEventListener('keydown', recordModal.keyHandler);
|
||
|
||
try {
|
||
if (safeDOM.exists(recordModal)) {
|
||
safeDOM.removeChild(document.body, recordModal);
|
||
}
|
||
} catch (error) {
|
||
console.warn("移除录制界面时出错:", error);
|
||
}
|
||
}
|
||
|
||
const playbackModal = document.createElement('div');
|
||
playbackModal.id = 'playbackModal';
|
||
playbackModal.style.cssText = `
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 10000;
|
||
`;
|
||
|
||
const playbackContent = document.createElement('div');
|
||
playbackContent.style.cssText = `
|
||
background: white;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
text-align: center;
|
||
min-width: 350px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
`;
|
||
|
||
playbackContent.innerHTML = `
|
||
<h3 style="color: #4a90e2; margin-bottom: 20px;">录音完成</h3>
|
||
<div id="playbackWaveform" style="width: 300px; height: 120px; background: linear-gradient(45deg, #f0f0f0 25%, transparent 25%), linear-gradient(-45deg, #f0f0f0 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #f0f0f0 75%), linear-gradient(-45deg, transparent 75%, #f0f0f0 75%); background-size: 20px 20px; margin: 20px auto; border-radius: 5px; position: relative; overflow: hidden;">
|
||
<div id="waveformLine" style="position: absolute; left: 0; top: 50%; width: 100%; height: 2px; background: #4a90e2; transform: translateY(-50%);"></div>
|
||
</div>
|
||
<div style="margin: 20px 0;">
|
||
<button id="playRecording" style="background: #4CAF50; color: white; border: none; padding: 15px 25px; border-radius: 5px; cursor: pointer; margin: 0 10px; font-size: 16px;">
|
||
<span style="font-size: 20px;">▶</span> 播放
|
||
</button>
|
||
</div>
|
||
<div style="margin: 20px 0;">
|
||
<button id="reRecord" style="background: #FF9800; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; margin: 0 10px;">🔄 重新录制</button>
|
||
<button id="saveRecording" style="background: #9C27B0; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; margin: 0 10px;">💾 添加到列表</button>
|
||
</div>
|
||
`;
|
||
|
||
playbackModal.appendChild(playbackContent);
|
||
|
||
try {
|
||
safeDOM.appendChild(document.body, playbackModal);
|
||
} catch (error) {
|
||
console.error("添加播放界面到DOM失败:", error);
|
||
return;
|
||
}
|
||
|
||
const playButton = playbackModal.querySelector('#playRecording');
|
||
if (playButton) {
|
||
playButton.onclick = () => {
|
||
try {
|
||
const audio = sound.createAudio(sound.recordedAudio);
|
||
audio.play().catch(error => {
|
||
console.error("播放录音失败:", error);
|
||
alert("播放录音失败: " + error.message);
|
||
});
|
||
|
||
const waveformLine = playbackModal.querySelector('#waveformLine');
|
||
if (waveformLine) {
|
||
waveformLine.style.background = '#FF5722';
|
||
setTimeout(() => {
|
||
if (waveformLine.parentNode) {
|
||
waveformLine.style.background = '#4a90e2';
|
||
}
|
||
}, 1000);
|
||
}
|
||
} catch (error) {
|
||
console.error("播放录音时出错:", error);
|
||
alert("播放录音时出错: " + error.message);
|
||
}
|
||
};
|
||
}
|
||
|
||
const reRecordButton = playbackModal.querySelector('#reRecord');
|
||
if (reRecordButton) {
|
||
reRecordButton.onclick = () => {
|
||
try {
|
||
if (safeDOM.exists(playbackModal)) {
|
||
safeDOM.removeChild(document.body, playbackModal);
|
||
}
|
||
sound.record();
|
||
} catch (error) {
|
||
console.error("重新录制时出错:", error);
|
||
alert("重新录制时出错: " + error.message);
|
||
}
|
||
};
|
||
}
|
||
|
||
const saveButton = playbackModal.querySelector('#saveRecording');
|
||
if (saveButton) {
|
||
saveButton.onclick = () => {
|
||
try {
|
||
sound.saveRecording(audioBlob);
|
||
if (safeDOM.exists(playbackModal)) {
|
||
safeDOM.removeChild(document.body, playbackModal);
|
||
}
|
||
} catch (error) {
|
||
console.error("添加到列表时出错:", error);
|
||
alert("添加到列表时出错: " + error.message);
|
||
}
|
||
};
|
||
}
|
||
|
||
sound.createWaveformDisplay(audioBlob, playbackModal);
|
||
},
|
||
|
||
createWaveformDisplay: async (audioBlob, container) => {
|
||
try {
|
||
if (!container || !container.parentNode) {
|
||
console.warn("波形显示容器不存在或已被移除");
|
||
return;
|
||
}
|
||
|
||
const arrayBuffer = await audioBlob.arrayBuffer();
|
||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||
|
||
const channelData = audioBuffer.getChannelData(0);
|
||
const waveformContainer = container.querySelector('#playbackWaveform');
|
||
|
||
if (waveformContainer && waveformContainer.parentNode) {
|
||
waveformContainer.innerHTML = '';
|
||
|
||
const barCount = 100;
|
||
const step = Math.floor(channelData.length / barCount);
|
||
|
||
for (let i = 0; i < barCount; i++) {
|
||
const start = i * step;
|
||
const end = start + step;
|
||
let sum = 0;
|
||
|
||
for (let j = start; j < end; j++) {
|
||
sum += Math.abs(channelData[j] || 0);
|
||
}
|
||
|
||
const average = sum / step;
|
||
const height = Math.max(2, average * 100);
|
||
|
||
const bar = document.createElement('div');
|
||
bar.style.cssText = `
|
||
position: absolute;
|
||
left: ${(i / barCount) * 100}%;
|
||
bottom: 0;
|
||
width: ${100 / barCount}%;
|
||
height: ${height}%;
|
||
background: #4a90e2;
|
||
border-radius: 1px;
|
||
`;
|
||
waveformContainer.appendChild(bar);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("创建波形显示失败:", error);
|
||
}
|
||
},
|
||
|
||
saveRecording: (audioBlob) => {
|
||
try {
|
||
const existingRecordings = Object.keys(sound.builtin).filter(k => k.startsWith('recording'));
|
||
|
||
const recordingName = `recording${existingRecordings.length + 1}`;
|
||
|
||
const audioToSave = audioBlob ? URL.createObjectURL(audioBlob) : sound.recordedAudio;
|
||
if (!audioToSave) {
|
||
throw new Error("录音数据不存在");
|
||
}
|
||
|
||
sound.builtin[recordingName] = audioToSave;
|
||
|
||
alert(`录音已保存为: ${recordingName}`);
|
||
|
||
const playbackModal = document.getElementById('playbackModal');
|
||
if (safeDOM.exists(playbackModal)) {
|
||
const playButton = safeDOM.querySelector(playbackModal, '#playRecording');
|
||
if (playButton) {
|
||
playButton.innerHTML = `<span style="font-size: 20px;">▶</span> 播放 ${recordingName}`;
|
||
}
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error("保存录音失败:", error);
|
||
alert("保存录音失败: " + error.message);
|
||
}
|
||
}
|
||
};
|
||
|
||
export default sound;
|
||
|
||
const originalAudio = window.Audio;
|
||
window.Audio = function(src) {
|
||
if (sound.blockAllAudio || sound.isStopped ||
|
||
(sound.abortController && sound.abortController.signal.aborted)) {
|
||
const fakeAudio = {
|
||
play: () => Promise.reject(new Error('Audio playback blocked globally')),
|
||
pause: () => {},
|
||
currentTime: 0,
|
||
src: '',
|
||
volume: 1,
|
||
playbackRate: 1,
|
||
onended: null,
|
||
onerror: null,
|
||
onloadstart: null,
|
||
oncanplay: null,
|
||
onplay: null,
|
||
onpause: null,
|
||
tagName: 'AUDIO',
|
||
addEventListener: () => {},
|
||
removeEventListener: () => {},
|
||
load: () => {},
|
||
duration: 0,
|
||
ended: false,
|
||
paused: true,
|
||
muted: false,
|
||
readyState: 0,
|
||
networkState: 0,
|
||
preload: 'none'
|
||
};
|
||
return fakeAudio;
|
||
}
|
||
// eslint-disable-next-line new-cap
|
||
return new originalAudio(src);
|
||
};
|
||
|
||
window.Audio.original = originalAudio;
|
||
|
||
if (typeof window !== 'undefined') {
|
||
const originalConsoleError = console.error;
|
||
console.error = function(...args) {
|
||
const message = args.join(' ');
|
||
if (message.includes('sound') && (
|
||
message.includes('no-unused-vars') ||
|
||
message.includes('no-trailing-spaces') ||
|
||
message.includes('new-cap')
|
||
)) {
|
||
console.warn('过滤的sound模块ESLint错误:', ...args);
|
||
return;
|
||
}
|
||
originalConsoleError.apply(console, args);
|
||
};
|
||
}
|
||
|
||
function patchPythonShell() {
|
||
const originalRemove = Element.prototype.remove;
|
||
Element.prototype.remove = function() {
|
||
if (!this) {
|
||
console.warn('尝试在null对象上调用remove(),已安全忽略');
|
||
return;
|
||
}
|
||
if (typeof this.remove !== 'function') {
|
||
console.warn('对象没有remove方法,已安全忽略');
|
||
return;
|
||
}
|
||
try {
|
||
return originalRemove.call(this);
|
||
} catch (error) {
|
||
console.warn('remove()调用失败,已安全处理:', error);
|
||
}
|
||
};
|
||
|
||
if (window.$ && window.$.fn && window.$.fn.remove) {
|
||
const originalJQueryRemove = window.$.fn.remove;
|
||
window.$.fn.remove = function() {
|
||
if (!this || this.length === 0) {
|
||
console.warn('尝试在空的jQuery对象上调用remove(),已安全忽略');
|
||
return this;
|
||
}
|
||
try {
|
||
return originalJQueryRemove.call(this);
|
||
} catch (error) {
|
||
console.warn('jQuery remove()调用失败,已安全处理:', error);
|
||
return this;
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
let isInjecting = false;
|
||
|
||
function isKeyboardInterruptError(error) {
|
||
if (!error) {
|
||
return false;
|
||
}
|
||
if (error.name === 'PythonError' || error.name === 'KeyboardInterrupt') {
|
||
const message = error.message || String(error);
|
||
return message.includes('KeyboardInterrupt') || message.includes('interrupted');
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function injectSoundToPython() {
|
||
if (isInjecting) {
|
||
return false;
|
||
}
|
||
if (window.pyodide && window.pyodide.globals) {
|
||
try {
|
||
isInjecting = true;
|
||
const pythonSound = {
|
||
play: (name) => {
|
||
return sound.play(name);
|
||
},
|
||
play_blocking: (name) => {
|
||
return sound.play_blocking(name);
|
||
},
|
||
stop_all: () => {
|
||
return sound.stop_all();
|
||
},
|
||
adjust_volume: (change) => {
|
||
return sound.adjust_volume(change);
|
||
},
|
||
set_volume: (value) => {
|
||
return sound.set_volume(value);
|
||
},
|
||
get_volume: () => {
|
||
return sound.get_volume();
|
||
},
|
||
adjust_effect: (effect, change) => {
|
||
return sound.adjust_effect(effect, change);
|
||
},
|
||
set_effect: (effect, value) => {
|
||
return sound.set_effect(effect, value);
|
||
},
|
||
clear_effects: () => {
|
||
return sound.clear_effects();
|
||
},
|
||
play_frequency: (frequency, duration) => {
|
||
return sound.play_frequency(frequency, duration);
|
||
},
|
||
play_frequency_blocking: (frequency, duration) => {
|
||
return sound.play_frequency_blocking(frequency, duration);
|
||
},
|
||
play_note_list: (noteList) => {
|
||
return sound.play_note_list(noteList);
|
||
},
|
||
play_note_list_blocking: (noteList) => {
|
||
return sound.play_note_list_blocking(noteList);
|
||
},
|
||
record: () => {
|
||
return sound.record();
|
||
},
|
||
volume: sound.volume,
|
||
effects: sound.effects
|
||
};
|
||
|
||
window.pyodide.globals.set('sound', pythonSound);
|
||
|
||
const ModuleType = window.pyodide.pyimport('types').ModuleType;
|
||
const soundModule = new ModuleType('sound');
|
||
|
||
Object.keys(pythonSound).forEach(key => {
|
||
if (typeof pythonSound[key] === 'function') {
|
||
soundModule[key] = pythonSound[key];
|
||
} else {
|
||
soundModule[key] = pythonSound[key];
|
||
}
|
||
});
|
||
try {
|
||
window.pyodide.runPython(`
|
||
import sys
|
||
sys.modules['sound'] = sound
|
||
|
||
# 创建同步版本的play_blocking包装函数
|
||
import asyncio
|
||
from js import Promise
|
||
|
||
# 保存原始的play_blocking(返回Promise的版本)
|
||
_original_play_blocking = sound.play_blocking
|
||
|
||
def _sync_play_blocking(name):
|
||
"""同步版本的play_blocking,会等待声音播放完成"""
|
||
promise = _original_play_blocking(name)
|
||
# 使用Pyodide的Promise支持
|
||
# 在Pyodide中,可以直接等待JS Promise
|
||
import pyodide
|
||
if hasattr(pyodide, 'ffi') and hasattr(pyodide.ffi, 'run_sync'):
|
||
# Pyodide 0.21+
|
||
try:
|
||
return pyodide.ffi.run_sync(promise)
|
||
except Exception:
|
||
return None
|
||
else:
|
||
# 降级方案:使用asyncio
|
||
try:
|
||
loop = asyncio.get_event_loop()
|
||
except RuntimeError:
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
|
||
# 将JS Promise转换为Python awaitable
|
||
async def wait_promise():
|
||
try:
|
||
return await promise
|
||
except Exception:
|
||
return None
|
||
|
||
try:
|
||
return loop.run_until_complete(wait_promise())
|
||
except Exception:
|
||
return None
|
||
|
||
# 替换sound.play_blocking为同步版本
|
||
sound.play_blocking = _sync_play_blocking
|
||
|
||
# 创建同步版本的play_frequency_blocking包装函数
|
||
_original_play_frequency_blocking = sound.play_frequency_blocking
|
||
|
||
def _sync_play_frequency_blocking(frequency, duration):
|
||
"""同步版本的play_frequency_blocking,会等待频率播放完成"""
|
||
promise = _original_play_frequency_blocking(frequency, duration)
|
||
# 使用Pyodide的Promise支持
|
||
import pyodide
|
||
if hasattr(pyodide, 'ffi') and hasattr(pyodide.ffi, 'run_sync'):
|
||
try:
|
||
return pyodide.ffi.run_sync(promise)
|
||
except Exception:
|
||
return None
|
||
else:
|
||
import asyncio
|
||
try:
|
||
loop = asyncio.get_event_loop()
|
||
except RuntimeError:
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
|
||
async def wait_promise():
|
||
try:
|
||
return await promise
|
||
except Exception:
|
||
return None
|
||
|
||
try:
|
||
return loop.run_until_complete(wait_promise())
|
||
except Exception:
|
||
return None
|
||
|
||
# 创建同步版本的play_note_list_blocking包装函数
|
||
_original_play_note_list_blocking = sound.play_note_list_blocking
|
||
|
||
def _sync_play_note_list_blocking(note_list):
|
||
"""同步版本的play_note_list_blocking,会等待音符列表播放完成"""
|
||
promise = _original_play_note_list_blocking(note_list)
|
||
# 使用Pyodide的Promise支持
|
||
import pyodide
|
||
if hasattr(pyodide, 'ffi') and hasattr(pyodide.ffi, 'run_sync'):
|
||
try:
|
||
return pyodide.ffi.run_sync(promise)
|
||
except Exception:
|
||
return None
|
||
else:
|
||
import asyncio
|
||
try:
|
||
loop = asyncio.get_event_loop()
|
||
except RuntimeError:
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
|
||
async def wait_promise():
|
||
try:
|
||
return await promise
|
||
except Exception:
|
||
return None
|
||
|
||
try:
|
||
return loop.run_until_complete(wait_promise())
|
||
except Exception:
|
||
return None
|
||
|
||
# 替换为同步版本
|
||
sound.play_frequency_blocking = _sync_play_frequency_blocking
|
||
sound.play_note_list_blocking = _sync_play_note_list_blocking
|
||
`);
|
||
} catch (runError) {
|
||
if (isKeyboardInterruptError(runError)) {
|
||
isInjecting = false;
|
||
return false;
|
||
}
|
||
throw runError;
|
||
}
|
||
|
||
const testSound = window.pyodide.globals.get('sound');
|
||
if (testSound) {
|
||
isInjecting = false;
|
||
return true;
|
||
}
|
||
isInjecting = false;
|
||
console.error('Sound对象注入失败:验证时未找到对象');
|
||
return false;
|
||
} catch (error) {
|
||
isInjecting = false;
|
||
if (isKeyboardInterruptError(error)) {
|
||
return false;
|
||
}
|
||
console.error('注入sound对象到Python环境失败:', error);
|
||
return false;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function initializeSoundSystem() {
|
||
if (injectSoundToPython()) {
|
||
return;
|
||
}
|
||
|
||
const soundInjectionInterval = setInterval(() => {
|
||
if (injectSoundToPython()) {
|
||
clearInterval(soundInjectionInterval);
|
||
}
|
||
}, 100);
|
||
|
||
setTimeout(() => {
|
||
clearInterval(soundInjectionInterval);
|
||
}, 10000);
|
||
}
|
||
|
||
function startAllSystems() {
|
||
patchPythonShell();
|
||
initializeSoundSystem();
|
||
}
|
||
|
||
function forceInjectSound() {
|
||
if (window.pyodide && window.pyodide.globals) {
|
||
try {
|
||
const existingSound = window.pyodide.globals.get('sound');
|
||
if (!existingSound) {
|
||
injectSoundToPython();
|
||
}
|
||
} catch (error) {
|
||
if (isKeyboardInterruptError(error)) {
|
||
return;
|
||
}
|
||
console.warn('强制注入检查失败:', error);
|
||
}
|
||
}
|
||
}
|
||
|
||
function interceptPythonExecution() {
|
||
const originalEval = window.eval;
|
||
window.eval = function(code) {
|
||
if (typeof code === 'string' && code.includes('sound.')) {
|
||
try {
|
||
forceInjectSound();
|
||
} catch (error) {
|
||
if (!isKeyboardInterruptError(error)) {
|
||
console.warn('执行拦截时出错:', error);
|
||
}
|
||
}
|
||
}
|
||
return originalEval.call(this, code);
|
||
};
|
||
|
||
if (window.pyodide && window.pyodide.runPython) {
|
||
const originalRunPython = window.pyodide.runPython;
|
||
window.pyodide.runPython = function(code) {
|
||
if (typeof code === 'string' && code.includes('sound.')) {
|
||
try {
|
||
forceInjectSound();
|
||
} catch (error) {
|
||
if (!isKeyboardInterruptError(error)) {
|
||
console.warn('执行拦截时出错:', error);
|
||
}
|
||
}
|
||
}
|
||
return originalRunPython.call(this, code);
|
||
};
|
||
}
|
||
}
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
startAllSystems();
|
||
interceptPythonExecution();
|
||
});
|
||
} else {
|
||
startAllSystems();
|
||
interceptPythonExecution();
|
||
}
|
||
|
||
window.addEventListener('pyodideLoaded', () => {
|
||
startAllSystems();
|
||
interceptPythonExecution();
|
||
});
|
||
|
||
setTimeout(() => {
|
||
startAllSystems();
|
||
interceptPythonExecution();
|
||
}, 1000);
|
||
|
||
window.addEventListener('load', () => {
|
||
startAllSystems();
|
||
interceptPythonExecution();
|
||
});
|
||
|
||
setInterval(forceInjectSound, 5000); |