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(); oscillator.stop(sound.audioContext.currentTime + duration / 1000); console.log(`播放频率: ${frequency}Hz, 持续时间: ${duration}ms, 音量: ${sound.volume}%`); } } catch (error) { console.error("播放频率声音失败:", error); } }, // 播放指定频率的声音(持续播放,无持续时间限制) play_frequency_continuous: (frequency) => { 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(); oscillator.stop(sound.audioContext.currentTime + 2); // 默认播放2秒 console.log(`播放频率(持续): ${frequency}Hz, 无持续时间限制, 音量: ${sound.volume}%`); } } catch (error) { console.error("播放频率声音失败:", error); } }, // 播放指定频率的声音(阻塞版本,使用队列,带持续时间) play_frequency_blocking: (frequency, duration = 1000) => { console.log(`=== 🔒 阻塞播放频率(加入队列): ${frequency}Hz, ${duration}ms ===`); return new Promise((resolve, reject) => { const queueItem = { type: 'frequency', frequency, duration: duration, resolve, reject }; sound.soundQueue.push(queueItem); console.log(`✅ 频率已加入声音队列,当前队列长度: ${sound.soundQueue.length}`); sound.processQueue(); }); }, // 播放指定频率的声音(阻塞版本,使用队列,持续播放) play_frequency_continuous_blocking: (frequency) => { console.log(`=== 🔒 阻塞播放频率(持续,加入队列): ${frequency}Hz ===`); return new Promise((resolve, reject) => { const duration = 2000; // 持续播放默认2秒 const queueItem = { type: 'frequency', frequency, duration: duration, resolve, reject }; sound.soundQueue.push(queueItem); console.log(`✅ 频率(持续)已加入声音队列,当前队列长度: ${sound.soundQueue.length}`); 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 = `