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 = `

录制声音

正在录制...
`; 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 = `

录音完成

`; 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 = ` 播放 ${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) => { console.log(`Python调用: sound.play_frequency(${frequency}, ${duration})`); // 自动判断是否需要阻塞 if (sound.soundQueue.length > 0 || sound.isProcessingQueue) { return sound.play_frequency_blocking(frequency, duration); } return sound.play_frequency(frequency, duration); }, play_frequency_no_duration: (frequency) => { console.log(`Python调用: sound.play_frequency_no_duration(${frequency})`); // 自动判断是否需要阻塞 if (sound.soundQueue.length > 0 || sound.isProcessingQueue) { return sound.play_frequency_continuous_blocking(frequency); } return sound.play_frequency_continuous(frequency); }, play_frequency_blocking: (frequency, duration) => { console.log(`Python调用: sound.play_frequency_blocking(${frequency}, ${duration})`); return sound.play_frequency_blocking(frequency, duration); }, play_frequency_continuous: (frequency) => { console.log(`Python调用: sound.play_frequency_continuous(${frequency})`); return sound.play_frequency_continuous(frequency); }, play_frequency_continuous_blocking: (frequency) => { console.log(`Python调用: sound.play_frequency_continuous_blocking(${frequency})`); return sound.play_frequency_continuous_blocking(frequency); }, 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包装函数 _original_play_frequency = sound.play_frequency def _sync_play_frequency(frequency, duration): """同步版本的play_frequency,会自动判断是否需要阻塞""" promise = _original_play_frequency(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_frequency_no_duration包装函数 _original_play_frequency_no_duration = sound.play_frequency_no_duration def _sync_play_frequency_no_duration(frequency): """同步版本的play_frequency_no_duration,会自动判断是否需要阻塞""" promise = _original_play_frequency_no_duration(frequency) # 使用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 = _sync_play_frequency sound.play_frequency_no_duration = _sync_play_frequency_no_duration # 创建同步版本的play_frequency_continuous_blocking包装函数 _original_play_frequency_continuous_blocking = sound.play_frequency_continuous_blocking def _sync_play_frequency_continuous_blocking(frequency): """同步版本的play_frequency_continuous_blocking,会等待频率播放完成""" promise = _original_play_frequency_continuous_blocking(frequency) # 使用Pyodide的Promise支持 import pyodide if hasattr(pyodide, 'ffi') and hasattr(pyodide.ffi, 'run_sync'): return pyodide.ffi.run_sync(promise) 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(): return await promise return loop.run_until_complete(wait_promise()) # 替换为同步版本 sound.play_frequency_continuous_blocking = _sync_play_frequency_continuous_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);