From 3ecb7d8cfb8d0a69e54461668641fe90f02298b9 Mon Sep 17 00:00:00 2001 From: whm1216 <2248293452@qq.com> Date: Tue, 2 Dec 2025 00:57:34 +0800 Subject: [PATCH] =?UTF-8?q?online=E5=8A=A0=E5=A3=B0=E9=9F=B3=E7=9B=AE?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../python_pyodide/blocks/sound/effect/add.js | 17 + .../blocks/sound/effect/sound_effect_clear.js | 12 + .../blocks/sound/effect/sound_effect_set.js | 17 + .../python_pyodide/blocks/sound/play/play.js | 26 + .../blocks/sound/play/play_frequency.js | 21 + .../sound/play/play_frequency_no_duration.js | 13 + .../blocks/sound/play/play_note_list.js | 22 + .../blocks/sound/play/play_wait.js | 27 + .../blocks/sound/play/record.js | 12 + .../blocks/sound/play/sound_note.js | 26 + .../blocks/sound/play/sound_stop_all.js | 12 + .../python_pyodide/blocks/sound/volume/add.js | 15 + .../python_pyodide/blocks/sound/volume/get.js | 11 + .../python_pyodide/blocks/sound/volume/set.js | 15 + .../css/color_mixpy_python_advance.css | 10 + .../generators/sound/effect/add.js | 21 + .../sound/effect/sound_effect_clear.js | 7 + .../sound/effect/sound_effect_set.js | 21 + .../generators/sound/play/play.js | 11 + .../generators/sound/play/play_frequency.js | 75 + .../sound/play/play_frequency_no_duration.js | 58 + .../generators/sound/play/play_note_list.js | 23 + .../generators/sound/play/play_wait.js | 12 + .../generators/sound/play/record.js | 7 + .../generators/sound/play/sound_note.js | 6 + .../generators/sound/play/sound_stop_all.js | 7 + .../generators/sound/volume/add.js | 8 + .../generators/sound/volume/get.js | 7 + .../generators/sound/volume/set.js | 8 + boards/default_src/python_pyodide/index.js | 82 +- .../python_pyodide/others/loader.js | 6 +- .../python_pyodide/others/python-shell.js | 3 + .../python_pyodide/others/sound.js | 1655 +++++++++++++++++ .../default_src/python_pyodide/template.xml | 59 + common/msg/blockly/en.js | 38 + common/msg/blockly/zh-hans.js | 38 + common/msg/blockly/zh-hant.js | 38 + 37 files changed, 2444 insertions(+), 2 deletions(-) create mode 100644 boards/default_src/python_pyodide/blocks/sound/effect/add.js create mode 100644 boards/default_src/python_pyodide/blocks/sound/effect/sound_effect_clear.js create mode 100644 boards/default_src/python_pyodide/blocks/sound/effect/sound_effect_set.js create mode 100644 boards/default_src/python_pyodide/blocks/sound/play/play.js create mode 100644 boards/default_src/python_pyodide/blocks/sound/play/play_frequency.js create mode 100644 boards/default_src/python_pyodide/blocks/sound/play/play_frequency_no_duration.js create mode 100644 boards/default_src/python_pyodide/blocks/sound/play/play_note_list.js create mode 100644 boards/default_src/python_pyodide/blocks/sound/play/play_wait.js create mode 100644 boards/default_src/python_pyodide/blocks/sound/play/record.js create mode 100644 boards/default_src/python_pyodide/blocks/sound/play/sound_note.js create mode 100644 boards/default_src/python_pyodide/blocks/sound/play/sound_stop_all.js create mode 100644 boards/default_src/python_pyodide/blocks/sound/volume/add.js create mode 100644 boards/default_src/python_pyodide/blocks/sound/volume/get.js create mode 100644 boards/default_src/python_pyodide/blocks/sound/volume/set.js create mode 100644 boards/default_src/python_pyodide/generators/sound/effect/add.js create mode 100644 boards/default_src/python_pyodide/generators/sound/effect/sound_effect_clear.js create mode 100644 boards/default_src/python_pyodide/generators/sound/effect/sound_effect_set.js create mode 100644 boards/default_src/python_pyodide/generators/sound/play/play.js create mode 100644 boards/default_src/python_pyodide/generators/sound/play/play_frequency.js create mode 100644 boards/default_src/python_pyodide/generators/sound/play/play_frequency_no_duration.js create mode 100644 boards/default_src/python_pyodide/generators/sound/play/play_note_list.js create mode 100644 boards/default_src/python_pyodide/generators/sound/play/play_wait.js create mode 100644 boards/default_src/python_pyodide/generators/sound/play/record.js create mode 100644 boards/default_src/python_pyodide/generators/sound/play/sound_note.js create mode 100644 boards/default_src/python_pyodide/generators/sound/play/sound_stop_all.js create mode 100644 boards/default_src/python_pyodide/generators/sound/volume/add.js create mode 100644 boards/default_src/python_pyodide/generators/sound/volume/get.js create mode 100644 boards/default_src/python_pyodide/generators/sound/volume/set.js create mode 100644 boards/default_src/python_pyodide/others/sound.js 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 = ` +

录制声音

+
+
+
+
+
+ +
+
正在录制...
+ `; + + recordModal.appendChild(recordContent); + + try { + safeDOM.appendChild(document.body, recordModal); + } catch (error) { + console.error("添加录制界面到DOM失败:", error); + return; + } + + recordModal.addEventListener('click', (event) => { + if (event.target.id === 'stopRecord') { + try { + if (sound.mediaRecorder && sound.isRecording) { + sound.mediaRecorder.stop(); + } else { + console.warn("MediaRecorder 不存在或未在录制状态"); + } + } catch (error) { + console.error("停止录制时出错:", error); + alert("停止录制时出错: " + error.message); + } + } + }); + + const waveformBars = recordModal.querySelector('#waveformBars'); + if (waveformBars) { + for (let i = 0; i < 20; i++) { + const bar = document.createElement('div'); + bar.style.cssText = ` + position: absolute; + left: ${i * 10}px; + bottom: 0; + width: 8px; + height: 20px; + background: #4a90e2; + border-radius: 2px; + transition: height 0.1s ease; + `; + waveformBars.appendChild(bar); + } + + const updateWaveform = () => { + if (sound.isRecording && recordModal.parentNode) { + const bars = waveformBars.children; + for (let i = 0; i < bars.length; i++) { + const height = Math.random() * 60 + 20; + bars[i].style.height = height + 'px'; + bars[i].style.background = `hsl(${200 + Math.random() * 60}, 70%, 60%)`; + } + } + }; + + const waveformInterval = setInterval(updateWaveform, 100); + recordModal.waveformInterval = waveformInterval; + } + + let volumeLevel = 0; + const volumeMeter = recordModal.querySelector('#volumeMeter'); + const statusDiv = recordModal.querySelector('#status'); + + if (volumeMeter && statusDiv) { + const volumeInterval = setInterval(() => { + if (sound.isRecording && recordModal.parentNode) { + volumeLevel = Math.random() * 60 + 40; + volumeMeter.style.height = volumeLevel + '%'; + statusDiv.textContent = `正在录制... 音量: ${Math.round(volumeLevel)}%`; + + volumeMeter.style.opacity = 0.8 + (volumeLevel / 100) * 0.2; + } else { + clearInterval(volumeInterval); + if (statusDiv && recordModal.parentNode) { + statusDiv.textContent = "录制完成"; + statusDiv.style.color = "#4CAF50"; + } + if (volumeMeter && recordModal.parentNode) { + volumeMeter.style.height = "100%"; + volumeMeter.style.background = "linear-gradient(to top, #4CAF50, #4CAF50)"; + } + } + }, 200); + + recordModal.volumeInterval = volumeInterval; + } else { + console.error("找不到音量计或状态显示元素"); + } + + const keyHandler = (event) => { + if (event.code === 'Space' && sound.isRecording) { + event.preventDefault(); + if (sound.mediaRecorder) { + sound.mediaRecorder.stop(); + } + } + }; + + document.addEventListener('keydown', keyHandler); + + recordModal.keyHandler = keyHandler; + }, + + showPlaybackInterface: (audioBlob) => { + const recordModal = document.getElementById('recordModal'); + if (recordModal) { + if (recordModal.volumeInterval) clearInterval(recordModal.volumeInterval); + if (recordModal.waveformInterval) clearInterval(recordModal.waveformInterval); + if (recordModal.keyHandler) document.removeEventListener('keydown', recordModal.keyHandler); + + try { + if (safeDOM.exists(recordModal)) { + safeDOM.removeChild(document.body, recordModal); + } + } catch (error) { + console.warn("移除录制界面时出错:", error); + } + } + + const playbackModal = document.createElement('div'); + playbackModal.id = 'playbackModal'; + playbackModal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; + `; + + const playbackContent = document.createElement('div'); + playbackContent.style.cssText = ` + background: white; + border-radius: 10px; + padding: 20px; + text-align: center; + min-width: 350px; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + `; + + playbackContent.innerHTML = ` +

录音完成

+
+
+
+
+ +
+
+ + +
+ `; + + playbackModal.appendChild(playbackContent); + + try { + safeDOM.appendChild(document.body, playbackModal); + } catch (error) { + console.error("添加播放界面到DOM失败:", error); + return; + } + + const playButton = playbackModal.querySelector('#playRecording'); + if (playButton) { + playButton.onclick = () => { + try { + const audio = sound.createAudio(sound.recordedAudio); + audio.play().catch(error => { + console.error("播放录音失败:", error); + alert("播放录音失败: " + error.message); + }); + + const waveformLine = playbackModal.querySelector('#waveformLine'); + if (waveformLine) { + waveformLine.style.background = '#FF5722'; + setTimeout(() => { + if (waveformLine.parentNode) { + waveformLine.style.background = '#4a90e2'; + } + }, 1000); + } + } catch (error) { + console.error("播放录音时出错:", error); + alert("播放录音时出错: " + error.message); + } + }; + } + + const reRecordButton = playbackModal.querySelector('#reRecord'); + if (reRecordButton) { + reRecordButton.onclick = () => { + try { + if (safeDOM.exists(playbackModal)) { + safeDOM.removeChild(document.body, playbackModal); + } + sound.record(); + } catch (error) { + console.error("重新录制时出错:", error); + alert("重新录制时出错: " + error.message); + } + }; + } + + const saveButton = playbackModal.querySelector('#saveRecording'); + if (saveButton) { + saveButton.onclick = () => { + try { + sound.saveRecording(audioBlob); + if (safeDOM.exists(playbackModal)) { + safeDOM.removeChild(document.body, playbackModal); + } + } catch (error) { + console.error("添加到列表时出错:", error); + alert("添加到列表时出错: " + error.message); + } + }; + } + + sound.createWaveformDisplay(audioBlob, playbackModal); + }, + + createWaveformDisplay: async (audioBlob, container) => { + try { + if (!container || !container.parentNode) { + console.warn("波形显示容器不存在或已被移除"); + return; + } + + const arrayBuffer = await audioBlob.arrayBuffer(); + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); + + const channelData = audioBuffer.getChannelData(0); + const waveformContainer = container.querySelector('#playbackWaveform'); + + if (waveformContainer && waveformContainer.parentNode) { + waveformContainer.innerHTML = ''; + + const barCount = 100; + const step = Math.floor(channelData.length / barCount); + + for (let i = 0; i < barCount; i++) { + const start = i * step; + const end = start + step; + let sum = 0; + + for (let j = start; j < end; j++) { + sum += Math.abs(channelData[j] || 0); + } + + const average = sum / step; + const height = Math.max(2, average * 100); + + const bar = document.createElement('div'); + bar.style.cssText = ` + position: absolute; + left: ${(i / barCount) * 100}%; + bottom: 0; + width: ${100 / barCount}%; + height: ${height}%; + background: #4a90e2; + border-radius: 1px; + `; + waveformContainer.appendChild(bar); + } + } + } catch (error) { + console.error("创建波形显示失败:", error); + } + }, + + saveRecording: (audioBlob) => { + try { + const existingRecordings = Object.keys(sound.builtin).filter(k => k.startsWith('recording')); + + const recordingName = `recording${existingRecordings.length + 1}`; + + const audioToSave = audioBlob ? URL.createObjectURL(audioBlob) : sound.recordedAudio; + if (!audioToSave) { + throw new Error("录音数据不存在"); + } + + sound.builtin[recordingName] = audioToSave; + + alert(`录音已保存为: ${recordingName}`); + + const playbackModal = document.getElementById('playbackModal'); + if (safeDOM.exists(playbackModal)) { + const playButton = safeDOM.querySelector(playbackModal, '#playRecording'); + if (playButton) { + playButton.innerHTML = ` 播放 ${recordingName}`; + } + } + + } catch (error) { + console.error("保存录音失败:", error); + alert("保存录音失败: " + error.message); + } + } +}; + +export default sound; + +const originalAudio = window.Audio; +window.Audio = function(src) { + if (sound.blockAllAudio || sound.isStopped || + (sound.abortController && sound.abortController.signal.aborted)) { + const fakeAudio = { + play: () => Promise.reject(new Error('Audio playback blocked globally')), + pause: () => {}, + currentTime: 0, + src: '', + volume: 1, + playbackRate: 1, + onended: null, + onerror: null, + onloadstart: null, + oncanplay: null, + onplay: null, + onpause: null, + tagName: 'AUDIO', + addEventListener: () => {}, + removeEventListener: () => {}, + load: () => {}, + duration: 0, + ended: false, + paused: true, + muted: false, + readyState: 0, + networkState: 0, + preload: 'none' + }; + return fakeAudio; + } + // eslint-disable-next-line new-cap + return new originalAudio(src); +}; + +window.Audio.original = originalAudio; + +if (typeof window !== 'undefined') { + const originalConsoleError = console.error; + console.error = function(...args) { + const message = args.join(' '); + if (message.includes('sound') && ( + message.includes('no-unused-vars') || + message.includes('no-trailing-spaces') || + message.includes('new-cap') + )) { + console.warn('过滤的sound模块ESLint错误:', ...args); + return; + } + originalConsoleError.apply(console, args); + }; +} + +function patchPythonShell() { + const originalRemove = Element.prototype.remove; + Element.prototype.remove = function() { + if (!this) { + console.warn('尝试在null对象上调用remove(),已安全忽略'); + return; + } + if (typeof this.remove !== 'function') { + console.warn('对象没有remove方法,已安全忽略'); + return; + } + try { + return originalRemove.call(this); + } catch (error) { + console.warn('remove()调用失败,已安全处理:', error); + } + }; + + if (window.$ && window.$.fn && window.$.fn.remove) { + const originalJQueryRemove = window.$.fn.remove; + window.$.fn.remove = function() { + if (!this || this.length === 0) { + console.warn('尝试在空的jQuery对象上调用remove(),已安全忽略'); + return this; + } + try { + return originalJQueryRemove.call(this); + } catch (error) { + console.warn('jQuery remove()调用失败,已安全处理:', error); + return this; + } + }; + } +} + +let isInjecting = false; + +function isKeyboardInterruptError(error) { + if (!error) { + return false; + } + if (error.name === 'PythonError' || error.name === 'KeyboardInterrupt') { + const message = error.message || String(error); + return message.includes('KeyboardInterrupt') || message.includes('interrupted'); + } + return false; +} + +function injectSoundToPython() { + if (isInjecting) { + return false; + } + if (window.pyodide && window.pyodide.globals) { + try { + isInjecting = true; + const pythonSound = { + play: (name) => { + return sound.play(name); + }, + play_blocking: (name) => { + return sound.play_blocking(name); + }, + stop_all: () => { + return sound.stop_all(); + }, + adjust_volume: (change) => { + return sound.adjust_volume(change); + }, + set_volume: (value) => { + return sound.set_volume(value); + }, + get_volume: () => { + return sound.get_volume(); + }, + adjust_effect: (effect, change) => { + return sound.adjust_effect(effect, change); + }, + set_effect: (effect, value) => { + return sound.set_effect(effect, value); + }, + clear_effects: () => { + return sound.clear_effects(); + }, + play_frequency: (frequency, duration) => { + return sound.play_frequency(frequency, duration); + }, + play_frequency_blocking: (frequency, duration) => { + return sound.play_frequency_blocking(frequency, duration); + }, + play_note_list: (noteList) => { + return sound.play_note_list(noteList); + }, + play_note_list_blocking: (noteList) => { + return sound.play_note_list_blocking(noteList); + }, + record: () => { + return sound.record(); + }, + volume: sound.volume, + effects: sound.effects + }; + + window.pyodide.globals.set('sound', pythonSound); + + const ModuleType = window.pyodide.pyimport('types').ModuleType; + const soundModule = new ModuleType('sound'); + + Object.keys(pythonSound).forEach(key => { + if (typeof pythonSound[key] === 'function') { + soundModule[key] = pythonSound[key]; + } else { + soundModule[key] = pythonSound[key]; + } + }); + try { + window.pyodide.runPython(` +import sys +sys.modules['sound'] = sound + +# 创建同步版本的play_blocking包装函数 +import asyncio +from js import Promise + +# 保存原始的play_blocking(返回Promise的版本) +_original_play_blocking = sound.play_blocking + +def _sync_play_blocking(name): + """同步版本的play_blocking,会等待声音播放完成""" + promise = _original_play_blocking(name) + # 使用Pyodide的Promise支持 + # 在Pyodide中,可以直接等待JS Promise + import pyodide + if hasattr(pyodide, 'ffi') and hasattr(pyodide.ffi, 'run_sync'): + # Pyodide 0.21+ + try: + return pyodide.ffi.run_sync(promise) + except Exception: + return None + else: + # 降级方案:使用asyncio + try: + loop = asyncio.get_event_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + # 将JS Promise转换为Python awaitable + async def wait_promise(): + try: + return await promise + except Exception: + return None + + try: + return loop.run_until_complete(wait_promise()) + except Exception: + return None + +# 替换sound.play_blocking为同步版本 +sound.play_blocking = _sync_play_blocking + +# 创建同步版本的play_frequency_blocking包装函数 +_original_play_frequency_blocking = sound.play_frequency_blocking + +def _sync_play_frequency_blocking(frequency, duration): + """同步版本的play_frequency_blocking,会等待频率播放完成""" + promise = _original_play_frequency_blocking(frequency, duration) + # 使用Pyodide的Promise支持 + import pyodide + if hasattr(pyodide, 'ffi') and hasattr(pyodide.ffi, 'run_sync'): + try: + return pyodide.ffi.run_sync(promise) + except Exception: + return None + else: + import asyncio + try: + loop = asyncio.get_event_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + async def wait_promise(): + try: + return await promise + except Exception: + return None + + try: + return loop.run_until_complete(wait_promise()) + except Exception: + return None + +# 创建同步版本的play_note_list_blocking包装函数 +_original_play_note_list_blocking = sound.play_note_list_blocking + +def _sync_play_note_list_blocking(note_list): + """同步版本的play_note_list_blocking,会等待音符列表播放完成""" + promise = _original_play_note_list_blocking(note_list) + # 使用Pyodide的Promise支持 + import pyodide + if hasattr(pyodide, 'ffi') and hasattr(pyodide.ffi, 'run_sync'): + try: + return pyodide.ffi.run_sync(promise) + except Exception: + return None + else: + import asyncio + try: + loop = asyncio.get_event_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + async def wait_promise(): + try: + return await promise + except Exception: + return None + + try: + return loop.run_until_complete(wait_promise()) + except Exception: + return None + +# 替换为同步版本 +sound.play_frequency_blocking = _sync_play_frequency_blocking +sound.play_note_list_blocking = _sync_play_note_list_blocking + `); + } catch (runError) { + if (isKeyboardInterruptError(runError)) { + isInjecting = false; + return false; + } + throw runError; + } + + const testSound = window.pyodide.globals.get('sound'); + if (testSound) { + isInjecting = false; + return true; + } + isInjecting = false; + console.error('Sound对象注入失败:验证时未找到对象'); + return false; + } catch (error) { + isInjecting = false; + if (isKeyboardInterruptError(error)) { + return false; + } + console.error('注入sound对象到Python环境失败:', error); + return false; + } + } + return false; +} + +function initializeSoundSystem() { + if (injectSoundToPython()) { + return; + } + + const soundInjectionInterval = setInterval(() => { + if (injectSoundToPython()) { + clearInterval(soundInjectionInterval); + } + }, 100); + + setTimeout(() => { + clearInterval(soundInjectionInterval); + }, 10000); +} + +function startAllSystems() { + patchPythonShell(); + initializeSoundSystem(); +} + +function forceInjectSound() { + if (window.pyodide && window.pyodide.globals) { + try { + const existingSound = window.pyodide.globals.get('sound'); + if (!existingSound) { + injectSoundToPython(); + } + } catch (error) { + if (isKeyboardInterruptError(error)) { + return; + } + console.warn('强制注入检查失败:', error); + } + } +} + +function interceptPythonExecution() { + const originalEval = window.eval; + window.eval = function(code) { + if (typeof code === 'string' && code.includes('sound.')) { + try { + forceInjectSound(); + } catch (error) { + if (!isKeyboardInterruptError(error)) { + console.warn('执行拦截时出错:', error); + } + } + } + return originalEval.call(this, code); + }; + + if (window.pyodide && window.pyodide.runPython) { + const originalRunPython = window.pyodide.runPython; + window.pyodide.runPython = function(code) { + if (typeof code === 'string' && code.includes('sound.')) { + try { + forceInjectSound(); + } catch (error) { + if (!isKeyboardInterruptError(error)) { + console.warn('执行拦截时出错:', error); + } + } + } + return originalRunPython.call(this, code); + }; + } +} +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + startAllSystems(); + interceptPythonExecution(); + }); +} else { + startAllSystems(); + interceptPythonExecution(); +} + +window.addEventListener('pyodideLoaded', () => { + startAllSystems(); + interceptPythonExecution(); +}); + +setTimeout(() => { + startAllSystems(); + interceptPythonExecution(); +}, 1000); + +window.addEventListener('load', () => { + startAllSystems(); + interceptPythonExecution(); +}); + +setInterval(forceInjectSound, 5000); \ No newline at end of file diff --git a/boards/default_src/python_pyodide/template.xml b/boards/default_src/python_pyodide/template.xml index 0a66d75c..8f0660be 100644 --- a/boards/default_src/python_pyodide/template.xml +++ b/boards/default_src/python_pyodide/template.xml @@ -3859,4 +3859,63 @@ + + + + + + + + 100 + + + + + + + + -10 + + + + + + + 10 + + + + + + + 100 + + + + + + + + NOTE_A4 + + + + + 1000 + + + + + + + NOTE_A4 + + + + + + NOTE_A4 + + + \ No newline at end of file diff --git a/common/msg/blockly/en.js b/common/msg/blockly/en.js index 73792703..fc3a6d11 100644 --- a/common/msg/blockly/en.js +++ b/common/msg/blockly/en.js @@ -96,6 +96,7 @@ En.MSG = { catOLED: 'OLED Screen', catMatrix: 'Matrix', catFactory: 'Factory', + catSound: 'Sound', catBlynk: 'Blynk IoT', catFile: 'File', catOnenet: 'OneNET', @@ -4068,4 +4069,41 @@ En.MIXLY_ERROR_RATE = 'error-tolerant rate'; En.MIXLY_Enlarge_and_scale_to = 'enlarge and scale to'; En.MIXLY_High_zoom_level_to = 'high zoom level to'; En.MIXLY_OPEN_IMAGE_TOOLTIP = 'Scaling is only supported for multiples of 8. A value of 0 indicates no scaling.'; +En.MIXLY_SOUND_PLAY_WAIT='Play sound %1 and wait for it to finish'; +En.MIXLY_SOUND_PLAY='Play Sound'; +En.MIXLY_catSoundSOUND_STOP_ALL='Stop all sounds'; +En.MIXLY_catSoundSOUND_PITCH_INCREASE='Increase the tone'; +En.MIXLY_catSoundSOUND_PITCH_SET='Set the tone to'; +En.MIXLY_catSoundSOUND_CLEAR_EFFECTS='Clear sound effects'; +En.MIXLY_catSoundSOUND_VOLUME_INCREASE='Increase the volume'; +En.MIXLY_catSoundSOUND_VOLUME_SET='Set the volume to'; +En.MIXLY_catSoundSOUND_VOLUME_GET='volume'; +En.MIXLY_catSoundSOUND_DROPDOWN='Meow'; +En.MIXLY_SOUND_PLAY_FREQUENCY_NO_DURATION='Play Sound Frequency'; +En.MIXLY_SOUND_FREQUENCY='Frequency'; +En.MIXLY_SOUND_DURATION='Duration'; +En.MIXLY_SOUND_PLAY_NOTE_LIST='Play Note Sequence'; +En.MIXLY_SOUND_WAIT_FINISH='and wait to finish'; +En.MIXLY_SOUND_RECORD='Record Sound'; +En.MIXLY_SOUND_RECORD_OPTION='Record...'; +En.MIXLY_SOUND_NOTE='Note Sequence'; +En.MIXLY_SOUND_EFFECT_PITCH='Pitch'; +En.MIXLY_SOUND_EFFECT_PAN='Pan Left/Right'; +En.MIXLY_SOUND_EFFECT_SET_TO='effect to'; +En.MIXLY_SOUND_EFFECT_ADD_BY='effect by'; +En.MIXLY_SOUND_SET_TO='Set'; +En.MIXLY_SOUND_PLAY_FREQUENCY_NO_DURATION_TOOLTIP='Play sound at a specific frequency'; +En.MIXLY_SOUND_PLAY_FREQUENCY_TOOLTIP='Play sound at a specific frequency for a limited time'; +En.MIXLY_SOUND_PLAY_NOTE_LIST_TOOLTIP='Play a preset note sequence'; +En.MIXLY_SOUND_PLAY_WAIT_TOOLTIP='Play sound and wait for it to finish'; +En.MIXLY_SOUND_PLAY_TOOLTIP='Play sound'; +En.MIXLY_SOUND_NOTE_TOOLTIP='Note sequence'; +En.MIXLY_SOUND_STOP_ALL_TOOLTIP='Stop all sounds'; +En.MIXLY_SOUND_RECORD_TOOLTIP='Record sound'; +En.MIXLY_SOUND_VOLUME_GET_TOOLTIP='Get volume'; +En.MIXLY_SOUND_VOLUME_SET_TOOLTIP='Set volume'; +En.MIXLY_SOUND_VOLUME_ADD_TOOLTIP='Change volume'; +En.MIXLY_SOUND_EFFECT_SET_TOOLTIP='Set sound effect'; +En.MIXLY_SOUND_EFFECT_CLEAR_TOOLTIP='Clear sound effects'; +En.MIXLY_SOUND_EFFECT_ADD_TOOLTIP='Change sound effect'; })(); diff --git a/common/msg/blockly/zh-hans.js b/common/msg/blockly/zh-hans.js index 5aed03b6..0ef61e18 100644 --- a/common/msg/blockly/zh-hans.js +++ b/common/msg/blockly/zh-hans.js @@ -107,6 +107,7 @@ ZhHans.MSG = { catOLED: 'OLED显示屏', catMatrix: '点阵屏', catFactory: '自定义模块', + catSound: '声音', catBlynk: 'Blynk', catFile: '文件', catOnenet: 'OneNET', @@ -4251,4 +4252,41 @@ ZhHans.MIXLY_ERROR_RATE = '容错率'; ZhHans.MIXLY_Enlarge_and_scale_to = '宽缩放至'; ZhHans.MIXLY_High_zoom_level_to = '高缩放至'; ZhHans.MIXLY_OPEN_IMAGE_TOOLTIP = '缩放仅支持8的倍数,填0代表不缩放'; +ZhHans.MIXLY_SOUND_PLAY_WAIT='播放声音 等待播完'; +ZhHans.MIXLY_SOUND_PLAY='播放声音'; +ZhHans.MIXLY_catSoundSOUND_STOP_ALL='停止所有声音'; +ZhHans.MIXLY_catSoundSOUND_PITCH_INCREASE='将音调增加'; +ZhHans.MIXLY_catSoundSOUND_PITCH_SET='将音调设为'; +ZhHans.MIXLY_catSoundSOUND_CLEAR_EFFECTS='清除音效'; +ZhHans.MIXLY_catSoundSOUND_VOLUME_INCREASE='将音量增加'; +ZhHans.MIXLY_catSoundSOUND_VOLUME_SET='将音量设为'; +ZhHans.MIXLY_catSoundSOUND_VOLUME_GET='音量'; +ZhHans.MIXLY_catSoundSOUND_DROPDOWN='Meow'; +ZhHans.MIXLY_SOUND_PLAY_FREQUENCY_NO_DURATION='播放声音 频率'; +ZhHans.MIXLY_SOUND_FREQUENCY='频率'; +ZhHans.MIXLY_SOUND_DURATION='持续时间'; +ZhHans.MIXLY_SOUND_PLAY_NOTE_LIST='播放音符序列'; +ZhHans.MIXLY_SOUND_WAIT_FINISH='等待播完'; +ZhHans.MIXLY_SOUND_RECORD='录制声音'; +ZhHans.MIXLY_SOUND_RECORD_OPTION='录制...'; +ZhHans.MIXLY_SOUND_NOTE='音符序列'; +ZhHans.MIXLY_SOUND_EFFECT_PITCH='音调'; +ZhHans.MIXLY_SOUND_EFFECT_PAN='左右平衡'; +ZhHans.MIXLY_SOUND_EFFECT_SET_TO='音效设为'; +ZhHans.MIXLY_SOUND_EFFECT_ADD_BY='音效增加'; +ZhHans.MIXLY_SOUND_SET_TO='将'; +ZhHans.MIXLY_SOUND_PLAY_FREQUENCY_NO_DURATION_TOOLTIP='播放特定频率的声音'; +ZhHans.MIXLY_SOUND_PLAY_FREQUENCY_TOOLTIP='限定时间内播放特定频率的声音'; +ZhHans.MIXLY_SOUND_PLAY_NOTE_LIST_TOOLTIP='播放预设的音符序列'; +ZhHans.MIXLY_SOUND_PLAY_WAIT_TOOLTIP='阻塞播放声音'; +ZhHans.MIXLY_SOUND_PLAY_TOOLTIP='播放声音'; +ZhHans.MIXLY_SOUND_NOTE_TOOLTIP='音符序列'; +ZhHans.MIXLY_SOUND_STOP_ALL_TOOLTIP='停止所有声音'; +ZhHans.MIXLY_SOUND_RECORD_TOOLTIP='录制声音'; +ZhHans.MIXLY_SOUND_VOLUME_GET_TOOLTIP='获取音量'; +ZhHans.MIXLY_SOUND_VOLUME_SET_TOOLTIP='设置音量'; +ZhHans.MIXLY_SOUND_VOLUME_ADD_TOOLTIP='增加音量'; +ZhHans.MIXLY_SOUND_EFFECT_SET_TOOLTIP='将音效设为'; +ZhHans.MIXLY_SOUND_EFFECT_CLEAR_TOOLTIP='清除音效'; +ZhHans.MIXLY_SOUND_EFFECT_ADD_TOOLTIP='将音效增加'; })(); \ No newline at end of file diff --git a/common/msg/blockly/zh-hant.js b/common/msg/blockly/zh-hant.js index 78565117..7d5ac56a 100644 --- a/common/msg/blockly/zh-hant.js +++ b/common/msg/blockly/zh-hant.js @@ -106,6 +106,7 @@ ZhHant.MSG = { catOLED: 'OLED顯示屏', catMatrix: '點陣屏', catFactory: '自定義模組', + catSound: '聲音', catBlynk: 'Blynk', catFile: '文件', catOnenet: 'OneNET', @@ -4221,4 +4222,41 @@ ZhHant.MIXLY_ERROR_RATE = '容錯率'; ZhHant.MIXLY_Enlarge_and_scale_to = '寬縮放至'; ZhHant.MIXLY_High_zoom_level_to = '高縮放至'; ZhHant.MIXLY_OPEN_IMAGE_TOOLTIP = '縮放僅支持8的倍數,填0代表不縮放'; +ZhHant.MIXLY_SOUND_PLAY_WAIT='播放聲音 等待播完'; +ZhHant.MIXLY_SOUND_PLAY='播放聲音'; +ZhHant.MIXLY_catSoundSOUND_STOP_ALL='停止所有聲音'; +ZhHant.MIXLY_catSoundSOUND_PITCH_INCREASE='將音調增加'; +ZhHant.MIXLY_catSoundSOUND_PITCH_SET='將音調設為'; +ZhHant.MIXLY_catSoundSOUND_CLEAR_EFFECTS='清除音效'; +ZhHant.MIXLY_catSoundSOUND_VOLUME_INCREASE='將音量增加'; +ZhHant.MIXLY_catSoundSOUND_VOLUME_SET='將音量設為'; +ZhHant.MIXLY_catSoundSOUND_VOLUME_GET='音量'; +ZhHant.MIXLY_catSoundSOUND_DROPDOWN='Meow'; +ZhHant.MIXLY_SOUND_PLAY_FREQUENCY_NO_DURATION='播放聲音 頻率'; +ZhHant.MIXLY_SOUND_FREQUENCY='頻率'; +ZhHant.MIXLY_SOUND_DURATION='持續時間'; +ZhHant.MIXLY_SOUND_PLAY_NOTE_LIST='播放音符序列'; +ZhHant.MIXLY_SOUND_WAIT_FINISH='等待播完'; +ZhHant.MIXLY_SOUND_RECORD='錄製聲音'; +ZhHant.MIXLY_SOUND_RECORD_OPTION='錄製...'; +ZhHant.MIXLY_SOUND_NOTE='音符序列'; +ZhHant.MIXLY_SOUND_EFFECT_PITCH='音調'; +ZhHant.MIXLY_SOUND_EFFECT_PAN='左右平衡'; +ZhHant.MIXLY_SOUND_EFFECT_SET_TO='音效設為'; +ZhHant.MIXLY_SOUND_EFFECT_ADD_BY='音效增加'; +ZhHant.MIXLY_SOUND_SET_TO='將'; +ZhHant.MIXLY_SOUND_PLAY_FREQUENCY_NO_DURATION_TOOLTIP='播放特定頻率的聲音'; +ZhHant.MIXLY_SOUND_PLAY_FREQUENCY_TOOLTIP='限定時間內播放特定頻率的聲音'; +ZhHant.MIXLY_SOUND_PLAY_NOTE_LIST_TOOLTIP='播放預設的音符序列'; +ZhHant.MIXLY_SOUND_PLAY_WAIT_TOOLTIP='阻塞播放聲音'; +ZhHant.MIXLY_SOUND_PLAY_TOOLTIP='播放聲音'; +ZhHant.MIXLY_SOUND_NOTE_TOOLTIP='音符序列'; +ZhHant.MIXLY_SOUND_STOP_ALL_TOOLTIP='停止所有聲音'; +ZhHant.MIXLY_SOUND_RECORD_TOOLTIP='錄製聲音'; +ZhHant.MIXLY_SOUND_VOLUME_GET_TOOLTIP='獲取音量'; +ZhHant.MIXLY_SOUND_VOLUME_SET_TOOLTIP='設置音量'; +ZhHant.MIXLY_SOUND_VOLUME_ADD_TOOLTIP='增加音量'; +ZhHant.MIXLY_SOUND_EFFECT_SET_TOOLTIP='將音效設為'; +ZhHant.MIXLY_SOUND_EFFECT_CLEAR_TOOLTIP='清除音效'; +ZhHant.MIXLY_SOUND_EFFECT_ADD_TOOLTIP='將音效增加'; })();