Files
mixly3/boards/default_src/python_pyodide/others/sound.js
2025-12-02 19:11:04 +08:00

1825 lines
67 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = `
<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) => {
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);