diff --git a/boards/default_src/python_pyodide/blocks/sound/effect/add.js b/boards/default_src/python_pyodide/blocks/sound/effect/add.js new file mode 100644 index 00000000..1355045a --- /dev/null +++ b/boards/default_src/python_pyodide/blocks/sound/effect/add.js @@ -0,0 +1,17 @@ +import * as Blockly from 'blockly/core'; + +export const sound_effect_add = { + init: function() { + this.setColour('#acc159'); + this.appendDummyInput() + .appendField(Blockly.Msg.MIXLY_SOUND_SET_TO) + .appendField(new Blockly.FieldDropdown([[Blockly.Msg.MIXLY_SOUND_EFFECT_PITCH, "pitch"], [Blockly.Msg.MIXLY_SOUND_EFFECT_PAN, "pan"]]), "EFFECT") + .appendField(Blockly.Msg.MIXLY_SOUND_EFFECT_ADD_BY); + this.appendValueInput("VALUE") + .setCheck(null) + this.appendDummyInput(); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Blockly.Msg.MIXLY_SOUND_EFFECT_ADD_TOOLTIP); + } +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/blocks/sound/effect/sound_effect_clear.js b/boards/default_src/python_pyodide/blocks/sound/effect/sound_effect_clear.js new file mode 100644 index 00000000..00472678 --- /dev/null +++ b/boards/default_src/python_pyodide/blocks/sound/effect/sound_effect_clear.js @@ -0,0 +1,12 @@ +import * as Blockly from 'blockly/core'; + +export const sound_effect_clear = { + init: function() { + this.setColour('#acc159'); + this.appendDummyInput() + .appendField(Blockly.Msg.MIXLY_catSoundSOUND_CLEAR_EFFECTS); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Blockly.Msg.MIXLY_SOUND_EFFECT_CLEAR_TOOLTIP); + } +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/blocks/sound/effect/sound_effect_set.js b/boards/default_src/python_pyodide/blocks/sound/effect/sound_effect_set.js new file mode 100644 index 00000000..d49a74b3 --- /dev/null +++ b/boards/default_src/python_pyodide/blocks/sound/effect/sound_effect_set.js @@ -0,0 +1,17 @@ +import * as Blockly from 'blockly/core'; + +export const sound_effect_set = { + init: function() { + this.setColour('#acc159'); + this.appendDummyInput() + .appendField(Blockly.Msg.MIXLY_SOUND_SET_TO) + .appendField(new Blockly.FieldDropdown([[Blockly.Msg.MIXLY_SOUND_EFFECT_PITCH, "pitch"], [Blockly.Msg.MIXLY_SOUND_EFFECT_PAN, "pan"]]), "EFFECT") + .appendField(Blockly.Msg.MIXLY_SOUND_EFFECT_SET_TO); + this.appendValueInput("VALUE") + .setCheck(null) + this.appendDummyInput(); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Blockly.Msg.MIXLY_SOUND_EFFECT_SET_TOOLTIP); + } +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/blocks/sound/play/play.js b/boards/default_src/python_pyodide/blocks/sound/play/play.js new file mode 100644 index 00000000..7f51de7a --- /dev/null +++ b/boards/default_src/python_pyodide/blocks/sound/play/play.js @@ -0,0 +1,26 @@ +import * as Blockly from 'blockly/core'; + +export const sound_play = { + init: function() { + this.setColour('#acc159'); + this.appendDummyInput() + .appendField(Blockly.Msg.MIXLY_SOUND_PLAY) + .appendField(new Blockly.FieldDropdown(this.getSoundOptions), "SOUND"); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Blockly.Msg.MIXLY_SOUND_PLAY_TOOLTIP); + }, + + getSoundOptions: function() { + const options = [["Meow", "Meow"], [Blockly.Msg.MIXLY_SOUND_RECORD_OPTION, "record"]]; + + if (window.sound && window.sound.builtin) { + const recordings = Object.keys(window.sound.builtin).filter(k => k.startsWith('recording')); + recordings.forEach(recording => { + options.push([recording, recording]); + }); + } + + return options; + } +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/blocks/sound/play/play_frequency.js b/boards/default_src/python_pyodide/blocks/sound/play/play_frequency.js new file mode 100644 index 00000000..db7fec4d --- /dev/null +++ b/boards/default_src/python_pyodide/blocks/sound/play/play_frequency.js @@ -0,0 +1,21 @@ +import * as Blockly from 'blockly/core'; + +export const sound_play_frequency = { + init: function() { + this.setColour('#acc159'); + this.appendDummyInput() + .appendField(Blockly.Msg.MIXLY_SOUND_PLAY) + .appendField(Blockly.Msg.MIXLY_SOUND_FREQUENCY); + this.appendValueInput("FREQUENCY") + .setCheck(null) + .setAlign(Blockly.ALIGN_RIGHT); + this.appendDummyInput() + .appendField(Blockly.Msg.MIXLY_SOUND_DURATION); + this.appendValueInput("DURATION") + .setCheck(null) + .setAlign(Blockly.ALIGN_RIGHT); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Blockly.Msg.MIXLY_SOUND_PLAY_FREQUENCY_TOOLTIP); + } +}; diff --git a/boards/default_src/python_pyodide/blocks/sound/play/play_frequency_no_duration.js b/boards/default_src/python_pyodide/blocks/sound/play/play_frequency_no_duration.js new file mode 100644 index 00000000..eb9c43fc --- /dev/null +++ b/boards/default_src/python_pyodide/blocks/sound/play/play_frequency_no_duration.js @@ -0,0 +1,13 @@ +import * as Blockly from 'blockly/core'; + +export const sound_play_frequency_no_duration = { + init: function() { + this.setColour('#acc159'); + this.appendValueInput("FREQUENCY") + .setCheck(null) + .appendField(Blockly.Msg.MIXLY_SOUND_PLAY_FREQUENCY_NO_DURATION); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Blockly.Msg.MIXLY_SOUND_PLAY_FREQUENCY_NO_DURATION_TOOLTIP); + } +}; diff --git a/boards/default_src/python_pyodide/blocks/sound/play/play_note_list.js b/boards/default_src/python_pyodide/blocks/sound/play/play_note_list.js new file mode 100644 index 00000000..48cf24c8 --- /dev/null +++ b/boards/default_src/python_pyodide/blocks/sound/play/play_note_list.js @@ -0,0 +1,22 @@ +import * as Blockly from 'blockly/core'; + +export const sound_play_note_list = { + init: function() { + this.setColour('#acc159'); + this.appendDummyInput() + .appendField(Blockly.Msg.MIXLY_SOUND_PLAY_NOTE_LIST); + this.appendDummyInput() + .appendField(new Blockly.FieldDropdown([ + ["DADADADUM", "DADADADUM"], + ["BIRTHDAY", "BIRTHDAY"], + ["BA_DING", "BA_DING"], + ["JUMP_UP", "JUMP_UP"], + ["JUMP_DOWN", "JUMP_DOWN"], + ["POWER_UP", "POWER_UP"], + ["POWER_DOWN", "POWER_DOWN"] + ]), "NOTE_LIST"); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Blockly.Msg.MIXLY_SOUND_PLAY_NOTE_LIST_TOOLTIP); + } +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/blocks/sound/play/play_wait.js b/boards/default_src/python_pyodide/blocks/sound/play/play_wait.js new file mode 100644 index 00000000..fd6ac720 --- /dev/null +++ b/boards/default_src/python_pyodide/blocks/sound/play/play_wait.js @@ -0,0 +1,27 @@ +import * as Blockly from 'blockly/core'; + +export const sound_play_wait = { + init: function() { + this.setColour('#acc159'); + this.appendDummyInput() + .appendField(Blockly.Msg.MIXLY_SOUND_PLAY) + .appendField(new Blockly.FieldDropdown(this.getSoundOptions), "SOUND") + .appendField(Blockly.Msg.MIXLY_SOUND_WAIT_FINISH); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Blockly.Msg.MIXLY_SOUND_PLAY_WAIT_TOOLTIP); + }, + + getSoundOptions: function() { + const options = [["Meow", "Meow"], [Blockly.Msg.MIXLY_SOUND_RECORD_OPTION, "record"]]; + + if (window.sound && window.sound.builtin) { + const recordings = Object.keys(window.sound.builtin).filter(k => k.startsWith('recording')); + recordings.forEach(recording => { + options.push([recording, recording]); + }); + } + + return options; + } +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/blocks/sound/play/record.js b/boards/default_src/python_pyodide/blocks/sound/play/record.js new file mode 100644 index 00000000..162fc681 --- /dev/null +++ b/boards/default_src/python_pyodide/blocks/sound/play/record.js @@ -0,0 +1,12 @@ +import * as Blockly from 'blockly/core'; + +export const sound_record = { + init: function() { + this.setColour('#acc159'); + this.appendDummyInput() + .appendField(Blockly.Msg.MIXLY_SOUND_RECORD); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Blockly.Msg.MIXLY_SOUND_RECORD_TOOLTIP); + } +}; diff --git a/boards/default_src/python_pyodide/blocks/sound/play/sound_note.js b/boards/default_src/python_pyodide/blocks/sound/play/sound_note.js new file mode 100644 index 00000000..82cc6a47 --- /dev/null +++ b/boards/default_src/python_pyodide/blocks/sound/play/sound_note.js @@ -0,0 +1,26 @@ +import * as Blockly from 'blockly/core'; + +export const sound_note = { + init: function() { + this.setColour('#acc159'); + this.appendDummyInput() + .appendField(new Blockly.FieldDropdown([ + ["NOTE_B3", "NOTE_B3"], + ["NOTE_C4", "NOTE_C4"], + ["NOTE_D4", "NOTE_D4"], + ["NOTE_E4", "NOTE_E4"], + ["NOTE_F4", "NOTE_F4"], + ["NOTE_G4", "NOTE_G4"], + ["NOTE_A4", "NOTE_A4"], + ["NOTE_B4", "NOTE_B4"], + ["NOTE_C5", "NOTE_C5"], + ["NOTE_D5", "NOTE_D5"], + ["NOTE_E5", "NOTE_E5"], + ["NOTE_F5", "NOTE_F5"], + ["NOTE_G5", "NOTE_G5"] + ]), "NOTE"); + this.setOutput(true, null); + this.setOutputShape(Blockly.OUTPUT_SHAPE_ROUND); + this.setTooltip(Blockly.Msg.MIXLY_SOUND_NOTE_TOOLTIP); + } +}; diff --git a/boards/default_src/python_pyodide/blocks/sound/play/sound_stop_all.js b/boards/default_src/python_pyodide/blocks/sound/play/sound_stop_all.js new file mode 100644 index 00000000..67015769 --- /dev/null +++ b/boards/default_src/python_pyodide/blocks/sound/play/sound_stop_all.js @@ -0,0 +1,12 @@ +import * as Blockly from 'blockly/core'; + +export const sound_stop_all = { + init: function() { + this.setColour('#acc159'); + this.appendDummyInput() + .appendField(Blockly.Msg.MIXLY_catSoundSOUND_STOP_ALL); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Blockly.Msg.MIXLY_SOUND_STOP_ALL_TOOLTIP); + } +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/blocks/sound/volume/add.js b/boards/default_src/python_pyodide/blocks/sound/volume/add.js new file mode 100644 index 00000000..4ffe4df4 --- /dev/null +++ b/boards/default_src/python_pyodide/blocks/sound/volume/add.js @@ -0,0 +1,15 @@ +import * as Blockly from 'blockly/core'; + +export const sound_volume_add = { + init: function() { + this.setColour('#acc159'); + this.appendDummyInput() + .appendField(Blockly.Msg.MIXLY_catSoundSOUND_VOLUME_INCREASE); + this.appendValueInput("VALUE") + .setCheck(null); + this.appendDummyInput(); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Blockly.Msg.MIXLY_SOUND_VOLUME_ADD_TOOLTIP); + } +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/blocks/sound/volume/get.js b/boards/default_src/python_pyodide/blocks/sound/volume/get.js new file mode 100644 index 00000000..11cd0cfa --- /dev/null +++ b/boards/default_src/python_pyodide/blocks/sound/volume/get.js @@ -0,0 +1,11 @@ +import * as Blockly from 'blockly/core'; + +export const sound_volume_get = { + init: function() { + this.setColour('#acc159'); + this.appendDummyInput() + .appendField(Blockly.Msg.MIXLY_catSoundSOUND_VOLUME_GET); + this.setOutput(true, "Volume"); + this.setTooltip(Blockly.Msg.MIXLY_SOUND_VOLUME_GET_TOOLTIP); + } +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/blocks/sound/volume/set.js b/boards/default_src/python_pyodide/blocks/sound/volume/set.js new file mode 100644 index 00000000..a290ed3a --- /dev/null +++ b/boards/default_src/python_pyodide/blocks/sound/volume/set.js @@ -0,0 +1,15 @@ +import * as Blockly from 'blockly/core'; + +export const sound_volume_set = { + init: function() { + this.setColour('#acc159'); + this.appendDummyInput() + .appendField(Blockly.Msg.MIXLY_catSoundSOUND_VOLUME_SET); + this.appendValueInput("VALUE") + .setCheck(null); + this.appendDummyInput(); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip(Blockly.Msg.MIXLY_SOUND_VOLUME_SET_TOOLTIP); + } +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/css/color_mixpy_python_advance.css b/boards/default_src/python_pyodide/css/color_mixpy_python_advance.css index a8bf8f20..382849b4 100644 --- a/boards/default_src/python_pyodide/css/color_mixpy_python_advance.css +++ b/boards/default_src/python_pyodide/css/color_mixpy_python_advance.css @@ -203,4 +203,14 @@ div.blocklyToolboxDiv>div.blocklyToolboxContents>div:nth-child(17)>div.blocklyTr #catFactory.blocklyTreeRow.blocklyTreeSelected>div.blocklyTreeRowContentContainer>span.blocklyTreeIcon { background: url('../../../../common/media/mark/factory4.png') no-repeat; background-size: 100% auto; +} + +#catSound.blocklyTreeRow>div.blocklyTreeRowContentContainer>span.blocklyTreeIcon { + background: url('../../../../common/media/mark/voice.png') no-repeat; + background-size: 100% auto; +} + +#catSound.blocklyTreeRow.blocklyTreeSelected>div.blocklyTreeRowContentContainer>span.blocklyTreeIcon { + background: url('../../../../common/media/mark/voice2.png') no-repeat; + background-size: 100% auto; } \ No newline at end of file diff --git a/boards/default_src/python_pyodide/generators/sound/effect/add.js b/boards/default_src/python_pyodide/generators/sound/effect/add.js new file mode 100644 index 00000000..c99fd156 --- /dev/null +++ b/boards/default_src/python_pyodide/generators/sound/effect/add.js @@ -0,0 +1,21 @@ +export const sound_effect_add = function(_block, _generator) { + if (!_generator.definitions_['import_sound']) { + _generator.definitions_['import_sound'] = 'import sound'; + } + + const effect = _block.getFieldValue("EFFECT"); + const valueInput = _block.getInputTargetBlock("VALUE"); + let val; + + if (valueInput) { + if (valueInput.type === "math_number") { + val = valueInput.getFieldValue("NUM") || "10"; + } else { + val = _generator.valueToCode(valueInput, "VALUE", _generator.ORDER_NONE) || "10"; + } + } else { + val = "10"; + } + + return `sound.adjust_effect("${effect}", ${val})\n`; +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/generators/sound/effect/sound_effect_clear.js b/boards/default_src/python_pyodide/generators/sound/effect/sound_effect_clear.js new file mode 100644 index 00000000..ecc7d053 --- /dev/null +++ b/boards/default_src/python_pyodide/generators/sound/effect/sound_effect_clear.js @@ -0,0 +1,7 @@ +export const sound_effect_clear = function(_block, _generator) { + if (!_generator.definitions_['import_sound']) { + _generator.definitions_['import_sound'] = 'import sound'; + } + + return "sound.clear_effects()\n"; +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/generators/sound/effect/sound_effect_set.js b/boards/default_src/python_pyodide/generators/sound/effect/sound_effect_set.js new file mode 100644 index 00000000..e5e466a4 --- /dev/null +++ b/boards/default_src/python_pyodide/generators/sound/effect/sound_effect_set.js @@ -0,0 +1,21 @@ +export const sound_effect_set = function(_block, _generator) { + if (!_generator.definitions_['import_sound']) { + _generator.definitions_['import_sound'] = 'import sound'; + } + + const effect = _block.getFieldValue("EFFECT"); + const valueInput = _block.getInputTargetBlock("VALUE"); + let val; + + if (valueInput) { + if (valueInput.type === "math_number") { + val = valueInput.getFieldValue("NUM") || "100"; + } else { + val = _generator.valueToCode(valueInput, "VALUE", _generator.ORDER_NONE) || "100"; + } + } else { + val = "100"; + } + + return `sound.set_effect("${effect}", ${val})\n`; +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/generators/sound/play/play.js b/boards/default_src/python_pyodide/generators/sound/play/play.js new file mode 100644 index 00000000..6fb9dac0 --- /dev/null +++ b/boards/default_src/python_pyodide/generators/sound/play/play.js @@ -0,0 +1,11 @@ +export const sound_play = function(_block, _generator) { + if (!_generator.definitions_['import_sound']) { + _generator.definitions_['import_sound'] = 'import sound'; + } + + const sound = _block.getFieldValue("SOUND"); + if (sound === "record") { + return `sound.record()\n`; + } + return `sound.play("${sound}")\n`; +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/generators/sound/play/play_frequency.js b/boards/default_src/python_pyodide/generators/sound/play/play_frequency.js new file mode 100644 index 00000000..5e7cfc0d --- /dev/null +++ b/boards/default_src/python_pyodide/generators/sound/play/play_frequency.js @@ -0,0 +1,75 @@ +function hasPlayWaitBefore(block) { + let currentBlock = block.getPreviousBlock(); + while (currentBlock) { + if (currentBlock.type === 'sound_play_wait') { + return true; + } + currentBlock = currentBlock.getPreviousBlock(); + } + return false; +} + +export const sound_play_frequency = function(_block, generator) { + if (!generator.definitions_['import_sound']) { + generator.definitions_['import_sound'] = 'import sound'; + } + + const frequencyInput = _block.getInputTargetBlock("FREQUENCY"); + const durationInput = _block.getInputTargetBlock("DURATION"); + let frequencyCode, durationCode; + + if (frequencyInput) { + try { + if (frequencyInput.type === "sound_note") { + const note = frequencyInput.getFieldValue("NOTE") || "NOTE_A4"; + const 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 + }; + frequencyCode = noteFrequencies[note] || 440; + } else if (frequencyInput.type === "math_number") { + const numValue = frequencyInput.getFieldValue("NUM"); + frequencyCode = numValue || "440"; + } else { + frequencyCode = generator.valueToCode(frequencyInput, "FREQUENCY", generator.ORDER_ATOMIC); + } + } catch (error) { + console.warn("生成频率代码时出错:", error); + frequencyCode = "440"; + } + } else { + frequencyCode = "440"; + } + + if (durationInput) { + try { + if (durationInput.type === "math_number") { + const numValue = durationInput.getFieldValue("NUM"); + durationCode = numValue || "1000"; + } else { + durationCode = generator.valueToCode(durationInput, "DURATION", generator.ORDER_ATOMIC); + } + } catch (error) { + console.warn("生成持续时间代码时出错:", error); + durationCode = "1000"; + } + } else { + durationCode = "1000"; + } + + const useBlocking = hasPlayWaitBefore(_block); + const methodName = useBlocking ? 'play_frequency_blocking' : 'play_frequency'; + + return `sound.${methodName}(${frequencyCode}, ${durationCode})\n`; +}; diff --git a/boards/default_src/python_pyodide/generators/sound/play/play_frequency_no_duration.js b/boards/default_src/python_pyodide/generators/sound/play/play_frequency_no_duration.js new file mode 100644 index 00000000..4c47f1c1 --- /dev/null +++ b/boards/default_src/python_pyodide/generators/sound/play/play_frequency_no_duration.js @@ -0,0 +1,58 @@ +function hasPlayWaitBefore(block) { + let currentBlock = block.getPreviousBlock(); + while (currentBlock) { + if (currentBlock.type === 'sound_play_wait') { + return true; + } + currentBlock = currentBlock.getPreviousBlock(); + } + return false; +} + +export const sound_play_frequency_no_duration = function(_block, generator) { + if (!generator.definitions_['import_sound']) { + generator.definitions_['import_sound'] = 'import sound'; + } + + const frequencyInput = _block.getInputTargetBlock("FREQUENCY"); + let frequencyCode; + + if (frequencyInput) { + try { + if (frequencyInput.type === "sound_note") { + const note = frequencyInput.getFieldValue("NOTE") || "NOTE_A4"; + const 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 + }; + frequencyCode = noteFrequencies[note] || 440; + } else if (frequencyInput.type === "math_number") { + const numValue = frequencyInput.getFieldValue("NUM"); + frequencyCode = numValue || "440"; + } else { + frequencyCode = generator.valueToCode(frequencyInput, "FREQUENCY", generator.ORDER_ATOMIC); + } + } catch (error) { + console.warn("生成频率代码时出错:", error); + frequencyCode = "440"; + } + } else { + frequencyCode = "440"; + } + + const useBlocking = hasPlayWaitBefore(_block); + const methodName = useBlocking ? 'play_frequency_blocking' : 'play_frequency'; + + return `sound.${methodName}(${frequencyCode}, 0)\n`; +}; diff --git a/boards/default_src/python_pyodide/generators/sound/play/play_note_list.js b/boards/default_src/python_pyodide/generators/sound/play/play_note_list.js new file mode 100644 index 00000000..2336e091 --- /dev/null +++ b/boards/default_src/python_pyodide/generators/sound/play/play_note_list.js @@ -0,0 +1,23 @@ +function hasPlayWaitBefore(block) { + let currentBlock = block.getPreviousBlock(); + while (currentBlock) { + if (currentBlock.type === 'sound_play_wait') { + return true; + } + currentBlock = currentBlock.getPreviousBlock(); + } + return false; +} + +export const sound_play_note_list = function(_block, _generator) { + if (!_generator.definitions_['import_sound']) { + _generator.definitions_['import_sound'] = 'import sound'; + } + + const noteList = _block.getFieldValue("NOTE_LIST") || "DADADADUM"; + + const useBlocking = hasPlayWaitBefore(_block); + const methodName = useBlocking ? 'play_note_list_blocking' : 'play_note_list'; + + return `sound.${methodName}("${noteList}")\n`; +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/generators/sound/play/play_wait.js b/boards/default_src/python_pyodide/generators/sound/play/play_wait.js new file mode 100644 index 00000000..3db6d510 --- /dev/null +++ b/boards/default_src/python_pyodide/generators/sound/play/play_wait.js @@ -0,0 +1,12 @@ +export const sound_play_wait = function(_block, _generator) { + if (!_generator.definitions_['import_sound']) { + _generator.definitions_['import_sound'] = 'import sound'; + } + + const sound = _block.getFieldValue("SOUND"); + if (sound === "record") { + return `sound.record()\n`; + } + + return `sound.play_blocking("${sound}")\n`; +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/generators/sound/play/record.js b/boards/default_src/python_pyodide/generators/sound/play/record.js new file mode 100644 index 00000000..1e4d04ba --- /dev/null +++ b/boards/default_src/python_pyodide/generators/sound/play/record.js @@ -0,0 +1,7 @@ +export const sound_record = function(_block, _generator) { + if (!_generator.definitions_['import_sound']) { + _generator.definitions_['import_sound'] = 'import sound'; + } + + return `sound.record()\n`; +}; diff --git a/boards/default_src/python_pyodide/generators/sound/play/sound_note.js b/boards/default_src/python_pyodide/generators/sound/play/sound_note.js new file mode 100644 index 00000000..4241293f --- /dev/null +++ b/boards/default_src/python_pyodide/generators/sound/play/sound_note.js @@ -0,0 +1,6 @@ +export const sound_note = function(_block, generator) { + const note = _block.getFieldValue("NOTE") || "NOTE_A4"; + return [`"${note}"`, generator.ORDER_ATOMIC]; +}; + + diff --git a/boards/default_src/python_pyodide/generators/sound/play/sound_stop_all.js b/boards/default_src/python_pyodide/generators/sound/play/sound_stop_all.js new file mode 100644 index 00000000..27f5817b --- /dev/null +++ b/boards/default_src/python_pyodide/generators/sound/play/sound_stop_all.js @@ -0,0 +1,7 @@ +export const sound_stop_all = function(_block, _generator) { + if (!_generator.definitions_['import_sound']) { + _generator.definitions_['import_sound'] = 'import sound'; + } + + return "sound.stop_all()\n"; +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/generators/sound/volume/add.js b/boards/default_src/python_pyodide/generators/sound/volume/add.js new file mode 100644 index 00000000..c22529b1 --- /dev/null +++ b/boards/default_src/python_pyodide/generators/sound/volume/add.js @@ -0,0 +1,8 @@ +export const sound_volume_add = function(_block, generator) { + if (!generator.definitions_['import_sound']) { + generator.definitions_['import_sound'] = 'import sound'; + } + + const val = generator.valueToCode(_block, "VALUE", generator.ORDER_NONE) || "0"; + return `sound.adjust_volume(${val})\n`; +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/generators/sound/volume/get.js b/boards/default_src/python_pyodide/generators/sound/volume/get.js new file mode 100644 index 00000000..e6bc88b1 --- /dev/null +++ b/boards/default_src/python_pyodide/generators/sound/volume/get.js @@ -0,0 +1,7 @@ +export const sound_volume_get = function(_block, generator) { + if (!generator.definitions_['import_sound']) { + generator.definitions_['import_sound'] = 'import sound'; + } + + return ['sound.get_volume()', generator.ORDER_ATOMIC]; +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/generators/sound/volume/set.js b/boards/default_src/python_pyodide/generators/sound/volume/set.js new file mode 100644 index 00000000..42649773 --- /dev/null +++ b/boards/default_src/python_pyodide/generators/sound/volume/set.js @@ -0,0 +1,8 @@ +export const sound_volume_set = function(_block, generator) { + if (!generator.definitions_['import_sound']) { + generator.definitions_['import_sound'] = 'import sound'; + } + + const val = generator.valueToCode(_block, "VALUE", generator.ORDER_NONE) || "100"; + return `sound.set_volume(${val})\n`; +}; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/index.js b/boards/default_src/python_pyodide/index.js index 72f9bfd4..02c85a14 100644 --- a/boards/default_src/python_pyodide/index.js +++ b/boards/default_src/python_pyodide/index.js @@ -75,6 +75,7 @@ import { } from './'; import './others/loader'; +import sound from './others/sound.js'; import './css/color_mixpy_python_advance.css'; @@ -151,4 +152,83 @@ Object.assign( PythonMixpyTurtleGenerators, PythonPyodideSKLearnGenerators, PythonTensorflowGenerators -); \ No newline at end of file +); +import { sound_play } from './blocks/sound/play/play.js'; +import { sound_play_wait } from './blocks/sound/play/play_wait.js'; +import { sound_stop_all } from './blocks/sound/play/sound_stop_all.js'; + +import { sound_effect_add } from './blocks/sound/effect/add.js'; +import { sound_effect_set } from './blocks/sound/effect/sound_effect_set.js'; +import { sound_effect_clear } from './blocks/sound/effect/sound_effect_clear.js'; + +import { sound_volume_add } from './blocks/sound/volume/add.js'; +import { sound_volume_set } from './blocks/sound/volume/set.js'; +import { sound_volume_get } from './blocks/sound/volume/get.js'; + +import { sound_record } from './blocks/sound/play/record.js'; + +import { sound_play_frequency } from './blocks/sound/play/play_frequency.js'; +import { sound_play_frequency_no_duration } from './blocks/sound/play/play_frequency_no_duration.js'; +import { sound_play_note_list } from './blocks/sound/play/play_note_list.js'; +import { sound_note } from './blocks/sound/play/sound_note.js'; + +import { sound_play as sound_play_gen } from './generators/sound/play/play.js'; +import { sound_play_wait as sound_play_wait_gen } from './generators/sound/play/play_wait.js'; +import { sound_stop_all as sound_stop_all_gen } from './generators/sound/play/sound_stop_all.js'; + +import { sound_effect_add as sound_effect_add_gen } from './generators/sound/effect/add.js'; +import { sound_effect_set as sound_effect_set_gen } from './generators/sound/effect/sound_effect_set.js'; +import { sound_effect_clear as sound_effect_clear_gen } from './generators/sound/effect/sound_effect_clear.js'; + +import { sound_volume_add as sound_volume_add_gen } from './generators/sound/volume/add.js'; +import { sound_volume_set as sound_volume_set_gen } from './generators/sound/volume/set.js'; +import { sound_volume_get as sound_volume_get_gen } from './generators/sound/volume/get.js'; + +import { sound_record as sound_record_gen } from './generators/sound/play/record.js'; + +import { sound_play_frequency as sound_play_frequency_gen } from './generators/sound/play/play_frequency.js'; +import { sound_play_frequency_no_duration as sound_play_frequency_no_duration_gen } from './generators/sound/play/play_frequency_no_duration.js'; +import { sound_play_note_list as sound_play_note_list_gen } from './generators/sound/play/play_note_list.js'; +import { sound_note as sound_note_gen } from './generators/sound/play/sound_note.js'; + +Object.assign(Blockly.Blocks, { + sound_play, + sound_play_wait, + sound_stop_all, + sound_effect_add, + sound_effect_set, + sound_effect_clear, + sound_volume_add, + sound_volume_set, + sound_volume_get, + sound_record, + + sound_play_frequency, + sound_play_frequency_no_duration, + sound_play_note_list, + sound_note, +}); + + +Object.assign(Blockly.Python.forBlock, { + sound_play: sound_play_gen, + sound_play_wait: sound_play_wait_gen, + sound_stop_all: sound_stop_all_gen, + + sound_effect_add: sound_effect_add_gen, + sound_effect_set: sound_effect_set_gen, + sound_effect_clear: sound_effect_clear_gen, + + sound_volume_add: sound_volume_add_gen, + sound_volume_set: sound_volume_set_gen, + sound_volume_get: sound_volume_get_gen, + + sound_record: sound_record_gen, + + sound_play_frequency: sound_play_frequency_gen, + sound_play_frequency_no_duration: sound_play_frequency_no_duration_gen, + sound_play_note_list: sound_play_note_list_gen, + sound_note: sound_note_gen, +}); + +window.sound = sound; \ No newline at end of file diff --git a/boards/default_src/python_pyodide/others/loader.js b/boards/default_src/python_pyodide/others/loader.js index 296ea8ad..17f70cb7 100644 --- a/boards/default_src/python_pyodide/others/loader.js +++ b/boards/default_src/python_pyodide/others/loader.js @@ -1,6 +1,7 @@ import NavExt from './nav-ext'; import * as tf from '@tensorflow/tfjs'; import './tensorflow'; +import './sound.js'; import * as Blockly from 'blockly/core'; NavExt.init(); @@ -292,7 +293,10 @@ async function createModal() { createModal(); -await loadAndDisplayAllModels(); +// 使用立即执行的异步函数,避免 top-level await 导致模块异步加载 +(async () => { + await loadAndDisplayAllModels(); +})(); function openModal() { loadAndDisplayAllModels(); diff --git a/boards/default_src/python_pyodide/others/python-shell.js b/boards/default_src/python_pyodide/others/python-shell.js index 8ff8a1af..ee766d1e 100644 --- a/boards/default_src/python_pyodide/others/python-shell.js +++ b/boards/default_src/python_pyodide/others/python-shell.js @@ -71,6 +71,9 @@ export default class PythonShell { this.pyodide.setInterruptBuffer(this.interruptBuffer); this.kernelLoaded = true; this.$loader.remove(); + if (this.$loader && this.$loader.remove) { + this.$loader.remove(); + } this.$loader = null; } diff --git a/boards/default_src/python_pyodide/others/sound.js b/boards/default_src/python_pyodide/others/sound.js new file mode 100644 index 00000000..436b0e1d --- /dev/null +++ b/boards/default_src/python_pyodide/others/sound.js @@ -0,0 +1,1655 @@ +window.addEventListener('error', (event) => { + if (event.error && event.error.message && event.error.message.includes('openOrClosedShadowRoot')) { + console.warn('捕获到Blockly DOM操作错误,这通常是无害的:', event.error.message); + event.preventDefault(); + return false; + } + + if (event.error && event.error.message && ( + event.error.message.includes('DOM') || + event.error.message.includes('Element') || + event.error.message.includes('Node') + )) { + console.warn('捕获到DOM操作错误:', event.error.message); + event.preventDefault(); + return false; + } +}); + +window.addEventListener('unhandledrejection', (event) => { + if (event.reason && event.reason.message && event.reason.message.includes('openOrClosedShadowRoot')) { + console.warn('捕获到未处理的Promise拒绝(Blockly DOM错误):', event.reason.message); + event.preventDefault(); + return false; + } +}); + +const originalConsoleError = console.error; +console.error = function(...args) { + const message = args.join(' '); + if (message.includes('openOrClosedShadowRoot') || + message.includes('DOM') || + message.includes('Element') || + message.includes('Node')) { + console.warn('过滤的DOM错误:', ...args); + return; + } + originalConsoleError.apply(console, args); +}; + +const safeDOM = { + appendChild: (parent, child) => { + try { + if (parent && child && parent.appendChild) { + return parent.appendChild(child); + } + } catch (error) { + console.warn('安全DOM添加失败:', error.message); + } + return null; + }, + + removeChild: (parent, child) => { + try { + if (parent && child && parent.removeChild && child.parentNode === parent) { + return parent.removeChild(child); + } + } catch (error) { + console.warn('安全DOM移除失败:', error.message); + } + return null; + }, + + querySelector: (container, selector) => { + try { + if (container && container.querySelector) { + return container.querySelector(selector); + } + } catch (error) { + console.warn('安全DOM查询失败:', error.message); + } + return null; + }, + + exists: (element) => { + return element && element.parentNode && document.contains(element); + } +}; + +const sound = { + + volume: 100, + + effects: { + pitch: 0, + pan: 0 + }, + + builtin: { + "Meow": "meow" + }, + + isRecording: false, + mediaRecorder: null, + recordedChunks: [], + recordedAudio: null, + + activeAudios: [], + + isStopped: false, + abortController: null, + + blockAllAudio: false, + + audioHistory: [], + + soundQueue: [], + isProcessingQueue: false, + currentlyPlaying: null, + + initAudioContext: () => { + if (!sound.audioContext) { + try { + sound.audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } catch (error) { + console.error("Error initializing audio context:", error); + } + } + }, + + createAudio: (src) => { + if (sound.blockAllAudio || sound.isStopped || + (sound.abortController && sound.abortController.signal.aborted)) { + const fakeAudio = { + play: () => Promise.reject(new Error('Audio playback blocked')), + pause: () => {}, + currentTime: 0, + src: '', + volume: 1, + playbackRate: 1, + onended: null, + onerror: null, + onloadstart: null, + oncanplay: null, + onplay: null, + onpause: null, + tagName: 'AUDIO', + addEventListener: () => {}, + removeEventListener: () => {}, + load: () => {}, + duration: 0, + ended: false, + paused: true, + muted: false, + readyState: 0, + networkState: 0, + preload: 'none' + }; + return fakeAudio; + } + return new Audio(src); + }, + + play: async (name) => { + try { + if (sound.isStopped) { + return; + } + + if (sound.abortController && sound.abortController.signal.aborted) { + return; + } + + if (sound.blockAllAudio) { + return; + } + + if (sound.soundQueue.length > 0 || sound.isProcessingQueue) { + sound.soundQueue.push({ + name, + resolve: () => {}, + reject: () => {} + }); + sound.processQueue(); + return; + } + + if (sound.activeAudios.length > 0) { + + sound.activeAudios.forEach(audio => { + try { + if (audio && audio.tagName === 'AUDIO') { + audio.pause(); + audio.currentTime = 0; + audio.src = ''; + } + } catch (error) { + console.warn("停止现有音频时出错:", error); + } + }); + sound.activeAudios = []; + } + + if (name.startsWith('recording') && sound.builtin[name]) { + if (sound.blockAllAudio || sound.isStopped) { + return; + } + + const audio = sound.createAudio(sound.builtin[name]); + audio.volume = sound.volume / 100; + + const currentPitch = sound.effects.pitch; + const currentPan = sound.effects.pan; + + if (currentPitch !== 0 || currentPan !== 0) { + sound.initAudioContext(); + if (sound.audioContext) { + try { + const source = sound.audioContext.createMediaElementSource(audio); + const gainNode = sound.audioContext.createGain(); + + if (currentPitch !== 0) { + const pitchShift = sound.audioContext.createBiquadFilter(); + pitchShift.type = 'peaking'; + pitchShift.frequency.setValueAtTime(1000, sound.audioContext.currentTime); + pitchShift.Q.setValueAtTime(1, sound.audioContext.currentTime); + + const pitchGain = Math.max(-20, Math.min(20, currentPitch * 0.8)); + pitchShift.gain.setValueAtTime(pitchGain, sound.audioContext.currentTime); + + source.connect(pitchShift); + pitchShift.connect(gainNode); + } else { + source.connect(gainNode); + } + + if (currentPan !== 0) { + const panNode = sound.audioContext.createStereoPanner(); + const panValue = Math.max(-1, Math.min(1, (currentPan / 100) * 1.5)); + panNode.pan.setValueAtTime(panValue, sound.audioContext.currentTime); + + gainNode.connect(panNode); + panNode.connect(sound.audioContext.destination); + } else { + gainNode.connect(sound.audioContext.destination); + } + + gainNode.gain.setValueAtTime(sound.volume / 100, sound.audioContext.currentTime); + } catch (error) { + console.warn("应用增强音效失败,使用默认播放:", error); + audio.play().catch(error => { + if (error.name !== 'AbortError') { + console.error("Error playing recorded audio:", error); + } + }); + return; + } + } else { + console.warn("音频上下文初始化失败,无法应用增强音效"); + audio.play().catch(error => { + if (error.name !== 'AbortError') { + console.error("Error playing recorded audio:", error); + } + }); + return; + } + } else { + audio.play().catch(error => { + if (error.name !== 'AbortError') { + console.error("Error playing recorded audio:", error); + } + }); + return; + } + + sound.activeAudios.push(audio); + + audio.onended = () => { + const index = sound.activeAudios.indexOf(audio); + if (index > -1) { + sound.activeAudios.splice(index, 1); + } + }; + + audio.play().catch(error => { + if (error.name !== 'AbortError') { + console.error("Error playing recorded audio:", error); + } + const index = sound.activeAudios.indexOf(audio); + if (index > -1) { + sound.activeAudios.splice(index, 1); + } + }); + return; + } + + if (name === "Meow") { + sound.initAudioContext(); + + if (sound.audioContext) { + const oscillator = sound.audioContext.createOscillator(); + const gainNode = sound.audioContext.createGain(); + + let frequency = 440; + + if (sound.effects.pitch !== 0) { + frequency *= Math.pow(2, sound.effects.pitch / 12); + } + + oscillator.frequency.setValueAtTime(frequency, sound.audioContext.currentTime); + oscillator.type = 'sine'; + + gainNode.gain.setValueAtTime(sound.volume / 100, sound.audioContext.currentTime); + + if (sound.effects.pan !== 0) { + const panNode = sound.audioContext.createStereoPanner(); + const panValue = sound.effects.pan / 100; + panNode.pan.setValueAtTime(panValue, sound.audioContext.currentTime); + + oscillator.connect(panNode); + panNode.connect(gainNode); + } else { + oscillator.connect(gainNode); + } + + gainNode.connect(sound.audioContext.destination); + + oscillator.start(); + oscillator.stop(sound.audioContext.currentTime + 0.5); + } + } + } catch (error) { + console.error("Error in sound.play:", error); + } + }, + + _playAudioInternal: async (name) => { + return new Promise((resolve, reject) => { + try { + if (sound.isStopped || sound.blockAllAudio) { + reject(new Error('Playback blocked')); + return; + } + + if (name.startsWith('recording') && sound.builtin[name]) { + const audio = sound.createAudio(sound.builtin[name]); + audio.volume = sound.volume / 100; + + const currentPitch = sound.effects.pitch; + const currentPan = sound.effects.pan; + + if (currentPitch !== 0 || currentPan !== 0) { + sound.initAudioContext(); + if (sound.audioContext) { + try { + const source = sound.audioContext.createMediaElementSource(audio); + const gainNode = sound.audioContext.createGain(); + + if (currentPitch !== 0) { + const pitchShift = sound.audioContext.createBiquadFilter(); + pitchShift.type = 'peaking'; + pitchShift.frequency.setValueAtTime(1000, sound.audioContext.currentTime); + pitchShift.Q.setValueAtTime(1, sound.audioContext.currentTime); + const pitchGain = Math.max(-20, Math.min(20, currentPitch * 0.8)); + pitchShift.gain.setValueAtTime(pitchGain, sound.audioContext.currentTime); + source.connect(pitchShift); + pitchShift.connect(gainNode); + } else { + source.connect(gainNode); + } + + if (currentPan !== 0) { + const panNode = sound.audioContext.createStereoPanner(); + const panValue = Math.max(-1, Math.min(1, (currentPan / 100) * 1.5)); + panNode.pan.setValueAtTime(panValue, sound.audioContext.currentTime); + gainNode.connect(panNode); + panNode.connect(sound.audioContext.destination); + } else { + gainNode.connect(sound.audioContext.destination); + } + + gainNode.gain.setValueAtTime(sound.volume / 100, sound.audioContext.currentTime); + } catch (error) { + console.warn("应用音效失败:", error); + } + } + } + + audio.onended = () => { + const index = sound.activeAudios.indexOf(audio); + if (index > -1) { + sound.activeAudios.splice(index, 1); + } + resolve(); + }; + + audio.onerror = (error) => { + console.error(`音频播放失败: ${name}`, error); + const index = sound.activeAudios.indexOf(audio); + if (index > -1) { + sound.activeAudios.splice(index, 1); + } + reject(error); + }; + + sound.activeAudios.push(audio); + audio.play().catch(error => { + if (error.name === 'AbortError') { + resolve(); + } else { + console.error("播放音频失败:", error); + reject(error); + } + }); + + } else if (name === "Meow") { + sound.initAudioContext(); + if (sound.audioContext) { + const oscillator = sound.audioContext.createOscillator(); + const gainNode = sound.audioContext.createGain(); + + let frequency = 440; + if (sound.effects.pitch !== 0) { + frequency *= Math.pow(2, sound.effects.pitch / 12); + } + + oscillator.frequency.setValueAtTime(frequency, sound.audioContext.currentTime); + oscillator.type = 'sine'; + gainNode.gain.setValueAtTime(sound.volume / 100, sound.audioContext.currentTime); + + if (sound.effects.pan !== 0) { + const panNode = sound.audioContext.createStereoPanner(); + const panValue = sound.effects.pan / 100; + panNode.pan.setValueAtTime(panValue, sound.audioContext.currentTime); + oscillator.connect(panNode); + panNode.connect(gainNode); + } else { + oscillator.connect(gainNode); + } + + gainNode.connect(sound.audioContext.destination); + + oscillator.start(); + oscillator.stop(sound.audioContext.currentTime + 0.5); + + setTimeout(() => { + resolve(); + }, 500); + } else { + reject(new Error('AudioContext not available')); + } + } else { + reject(new Error(`Unknown sound: ${name}`)); + } + } catch (error) { + console.error("内部播放错误:", error); + reject(error); + } + }); + }, + + processQueue: async () => { + if (sound.isProcessingQueue || sound.soundQueue.length === 0) { + return; + } + + sound.isProcessingQueue = true; + + while (sound.soundQueue.length > 0) { + const queueItem = sound.soundQueue.shift(); + + try { + if (queueItem.type === 'frequency') { + const { frequency, duration, resolve } = queueItem; + + sound.currentlyPlaying = { type: 'frequency', frequency, startTime: Date.now() }; + + await new Promise((freqResolve) => { + sound.initAudioContext(); + + if (sound.audioContext) { + const oscillator = sound.audioContext.createOscillator(); + const gainNode = sound.audioContext.createGain(); + + oscillator.frequency.setValueAtTime(frequency, sound.audioContext.currentTime); + oscillator.type = 'sine'; + + const currentPitch = sound.effects.pitch; + if (currentPitch !== 0) { + oscillator.frequency.setValueAtTime( + frequency * Math.pow(2, currentPitch / 12), + sound.audioContext.currentTime + ); + } + + gainNode.gain.setValueAtTime(sound.volume / 100, sound.audioContext.currentTime); + oscillator.connect(gainNode); + gainNode.connect(sound.audioContext.destination); + + oscillator.onended = () => { + sound.currentlyPlaying = null; + freqResolve(); + }; + + oscillator.start(); + oscillator.stop(sound.audioContext.currentTime + duration / 1000); + } else { + freqResolve(); + } + }); + + sound.currentlyPlaying = null; + resolve(); + } else { + const { name, resolve } = queueItem; + + sound.currentlyPlaying = { name, startTime: Date.now() }; + await sound._playAudioInternal(name); + sound.currentlyPlaying = null; + resolve(); + } + } catch (error) { + console.error(`队列播放失败:`, error); + sound.currentlyPlaying = null; + if (queueItem.reject) { + queueItem.reject(error); + } + } + } + + sound.isProcessingQueue = false; + }, + + play_blocking: (name) => { + return new Promise((resolve, reject) => { + sound.soundQueue.push({ name, resolve, reject }); + sound.processQueue(); + }); + }, + + stop_all: () => { + try { + sound.isStopped = true; + sound.blockAllAudio = true; + + if (sound.soundQueue.length > 0) { + sound.soundQueue.forEach(({ reject }) => { + reject(new Error('Playback stopped')); + }); + sound.soundQueue = []; + } + sound.isProcessingQueue = false; + sound.currentlyPlaying = null; + + if (sound.abortController) { + sound.abortController.abort(); + } + sound.abortController = new AbortController(); + + if (sound.activeAudios.length > 0) { + sound.activeAudios.forEach((audio) => { + try { + if (audio && audio.tagName === 'AUDIO') { + audio.pause(); + audio.currentTime = 0; + audio.src = ''; + audio.load(); + audio.onended = null; + audio.onerror = null; + audio.onloadstart = null; + audio.oncanplay = null; + audio.onplay = null; + audio.onpause = null; + } + } catch (error) { + console.warn(`停止音频时出错:`, error); + } + }); + + sound.activeAudios = []; + } + + if (sound.audioContext) { + try { + sound.audioContext.close(); + sound.audioContext = null; + } catch (error) { + console.warn("关闭音频上下文时出错:", error); + } + } + + sound.effects.pitch = 0; + sound.effects.pan = 0; + + const allAudioElements = document.querySelectorAll('audio'); + if (allAudioElements.length > 0) { + allAudioElements.forEach((audio) => { + try { + audio.pause(); + audio.currentTime = 0; + audio.src = ''; + } catch (error) { + console.warn(`停止页面音频元素时出错:`, error); + } + }); + } + + setTimeout(() => { + sound.isStopped = false; + sound.blockAllAudio = false; + }, 100); + } catch (error) { + console.error("Error in sound.stop_all:", error); + } + }, + + adjust_volume: (change) => { + const newVolume = Math.max(0, Math.min(100, sound.volume + change)); + sound.volume = newVolume; + }, + + set_volume: (value) => { + sound.volume = Math.max(0, Math.min(100, value)); + }, + + get_volume: () => { + return sound.volume; + }, + + noteFrequencies: { + "NOTE_B3": 247, + "NOTE_C4": 262, + "NOTE_D4": 294, + "NOTE_E4": 330, + "NOTE_F4": 349, + "NOTE_G4": 392, + "NOTE_A4": 440, + "NOTE_B4": 494, + "NOTE_C5": 523, + "NOTE_D5": 587, + "NOTE_E5": 659, + "NOTE_F5": 698, + "NOTE_G5": 784 + }, + + play_frequency: (frequency, duration = 1000) => { + try { + sound.initAudioContext(); + + if (sound.audioContext) { + const oscillator = sound.audioContext.createOscillator(); + const gainNode = sound.audioContext.createGain(); + + oscillator.frequency.setValueAtTime(frequency, sound.audioContext.currentTime); + oscillator.type = 'sine'; + + const currentPitch = sound.effects.pitch; + if (currentPitch !== 0) { + oscillator.frequency.setValueAtTime( + frequency * Math.pow(2, currentPitch / 12), + sound.audioContext.currentTime + ); + } + + gainNode.gain.setValueAtTime(sound.volume / 100, sound.audioContext.currentTime); + + oscillator.connect(gainNode); + gainNode.connect(sound.audioContext.destination); + + oscillator.start(); + + if (duration > 0) { + oscillator.stop(sound.audioContext.currentTime + duration / 1000); + } else { + oscillator.stop(sound.audioContext.currentTime + 2); + } + } + } catch (error) { + console.error("播放频率声音失败:", error); + } + }, + + play_frequency_blocking: (frequency, duration = 1000) => { + return new Promise((resolve, reject) => { + const actualDuration = duration > 0 ? duration : 2000; + + const queueItem = { + type: 'frequency', + frequency, + duration: actualDuration, + resolve, + reject + }; + + sound.soundQueue.push(queueItem); + sound.processQueue(); + }); + }, + + play_note_list: (noteList) => { + try { + const noteSequences = { + "DADADADUM": [ + { note: "NOTE_D4", duration: 500 }, + { note: "NOTE_A4", duration: 500 }, + { note: "NOTE_D4", duration: 500 }, + { note: "NOTE_A4", duration: 500 }, + { note: "NOTE_D4", duration: 500 }, + { note: "NOTE_A4", duration: 500 }, + { note: "NOTE_D4", duration: 500 }, + { note: "NOTE_A4", duration: 500 } + ], + "BIRTHDAY": [ + { note: "NOTE_C4", duration: 400 }, + { note: "NOTE_C4", duration: 400 }, + { note: "NOTE_D4", duration: 800 }, + { note: "NOTE_C4", duration: 800 }, + { note: "NOTE_F4", duration: 800 }, + { note: "NOTE_E4", duration: 1600 } + ], + "BA_DING": [ + { note: "NOTE_C5", duration: 200 }, + { note: "NOTE_E5", duration: 200 }, + { note: "NOTE_G5", duration: 400 } + ], + "JUMP_UP": [ + { note: "NOTE_C5", duration: 100 }, + { note: "NOTE_E5", duration: 100 }, + { note: "NOTE_G5", duration: 100 } + ], + "JUMP_DOWN": [ + { note: "NOTE_G5", duration: 100 }, + { note: "NOTE_E5", duration: 100 }, + { note: "NOTE_C5", duration: 100 } + ], + "POWER_UP": [ + { note: "NOTE_C4", duration: 150 }, + { note: "NOTE_E4", duration: 150 }, + { note: "NOTE_G4", duration: 150 }, + { note: "NOTE_C5", duration: 300 } + ], + "POWER_DOWN": [ + { note: "NOTE_C5", duration: 150 }, + { note: "NOTE_G4", duration: 150 }, + { note: "NOTE_E4", duration: 150 }, + { note: "NOTE_C4", duration: 300 } + ] + }; + + const sequence = noteSequences[noteList]; + if (sequence) { + let currentTime = 0; + + sequence.forEach((item) => { + const frequency = sound.noteFrequencies[item.note] || 440; + const duration = item.duration; + + setTimeout(() => { + sound.play_frequency(frequency, duration); + }, currentTime); + + currentTime += duration; + }); + } else { + console.warn(`未知的音符列表: ${noteList}`); + } + } catch (error) { + console.error("播放音符列表失败:", error); + } + }, + + play_note_list_blocking: (noteList) => { + return new Promise((resolve, reject) => { + (async () => { + try { + const noteSequences = { + "DADADADUM": [ + { note: "NOTE_D4", duration: 500 }, + { note: "NOTE_A4", duration: 500 }, + { note: "NOTE_D4", duration: 500 }, + { note: "NOTE_A4", duration: 500 }, + { note: "NOTE_D4", duration: 500 }, + { note: "NOTE_A4", duration: 500 }, + { note: "NOTE_D4", duration: 500 }, + { note: "NOTE_A4", duration: 500 } + ], + "BIRTHDAY": [ + { note: "NOTE_C4", duration: 400 }, + { note: "NOTE_C4", duration: 400 }, + { note: "NOTE_D4", duration: 800 }, + { note: "NOTE_C4", duration: 800 }, + { note: "NOTE_F4", duration: 800 }, + { note: "NOTE_E4", duration: 1600 } + ], + "BA_DING": [ + { note: "NOTE_C5", duration: 200 }, + { note: "NOTE_E5", duration: 200 }, + { note: "NOTE_G5", duration: 400 } + ], + "JUMP_UP": [ + { note: "NOTE_C5", duration: 100 }, + { note: "NOTE_E5", duration: 100 }, + { note: "NOTE_G5", duration: 100 } + ], + "JUMP_DOWN": [ + { note: "NOTE_G5", duration: 100 }, + { note: "NOTE_E5", duration: 100 }, + { note: "NOTE_C5", duration: 100 } + ], + "POWER_UP": [ + { note: "NOTE_C4", duration: 150 }, + { note: "NOTE_E4", duration: 150 }, + { note: "NOTE_G4", duration: 150 }, + { note: "NOTE_C5", duration: 300 } + ], + "POWER_DOWN": [ + { note: "NOTE_C5", duration: 150 }, + { note: "NOTE_G4", duration: 150 }, + { note: "NOTE_E4", duration: 150 }, + { note: "NOTE_C4", duration: 300 } + ] + }; + + const sequence = noteSequences[noteList]; + if (sequence) { + for (const item of sequence) { + const frequency = sound.noteFrequencies[item.note] || 440; + await sound.play_frequency_blocking(frequency, item.duration); + } + + resolve(); + } else { + console.warn(`未知的音符列表: ${noteList}`); + reject(new Error(`未知的音符列表: ${noteList}`)); + } + } catch (error) { + console.error("播放音符列表失败:", error); + reject(error); + } + })(); + }); + }, + + adjust_effect: (effect, change) => { + if (effect === "pitch") { + sound.effects.pitch = Math.max(-24, Math.min(24, sound.effects.pitch + change)); + } else if (effect === "pan") { + sound.effects.pan = Math.max(-100, Math.min(100, sound.effects.pan + change)); + } + }, + + set_effect: (effect, value) => { + if (effect === "pitch") { + sound.effects.pitch = Math.max(-24, Math.min(24, value)); + } else if (effect === "pan") { + sound.effects.pan = Math.max(-100, Math.min(100, value)); + } + }, + + clear_effects: () => { + sound.effects.pitch = 0; + sound.effects.pan = 0; + }, + + record: () => { + if (sound.isRecording) { + return; + } + + try { + navigator.mediaDevices.getUserMedia({ audio: true }) + .then(stream => { + sound.mediaRecorder = new MediaRecorder(stream); + sound.recordedChunks = []; + + sound.mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + sound.recordedChunks.push(event.data); + } + }; + + sound.mediaRecorder.onstart = () => { + sound.isRecording = true; + }; + + sound.mediaRecorder.onstop = () => { + sound.isRecording = false; + + const audioBlob = new Blob(sound.recordedChunks, { type: 'audio/wav' }); + sound.recordedAudio = URL.createObjectURL(audioBlob); + + stream.getTracks().forEach(track => { + track.stop(); + }); + + sound.showPlaybackInterface(audioBlob); + }; + + sound.mediaRecorder.onerror = (event) => { + console.error("MediaRecorder 错误:", event.error); + sound.isRecording = false; + }; + + sound.mediaRecorder.start(100); + + sound.showRecordInterface(); + }) + .catch(error => { + console.error("获取麦克风权限失败:", error); + alert("无法访问麦克风,请检查权限设置。错误: " + error.message); + }); + } catch (error) { + console.error("录制功能初始化失败:", error); + alert("录制功能初始化失败: " + error.message); + } + }, + + showRecordInterface: () => { + const recordModal = document.createElement('div'); + recordModal.id = 'recordModal'; + recordModal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; + `; + + const recordContent = document.createElement('div'); + recordContent.style.cssText = ` + background: white; + border-radius: 10px; + padding: 20px; + text-align: center; + min-width: 300px; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + `; + + recordContent.innerHTML = ` +