Scratch Blocks自定义组件之「旋律播放」

一、背景 

看到microbit edit有旋律编辑器,就在scratch块中也写了一个,如下图所示

这是我写的

这是Micro:bit的

二、功能配置说明

支持8个音符8拍旋律控制

三、使用说明

(1)引入添加field_tone.js到core文件夹中,代码在下文

(2)添加css样式到core/css.js中,代码在下文

(3)封装音频播放组件Blockly.AudioContext,主要实现音频播放API,基于Web AudioContext实现音频播放,在core/audio_context.js,代码在下文

(4)将field_tone注册到Blockly中,这样在任意地方都可以使用,如果不想注入,直接用script标签引入也行,在core/blockly.js写如下代码:

goog.require('Blockly.FieldTone');

(5)block定义代码如下,下面代码是直接集成到一个完整block中

// ZE3P蜂鸣器 旋律播放
Blockly.Blocks['ZE3P_play_melody'] = {
  init: function () {
    this.jsonInit({
      "message0": "%1%2",
      "args0": [
        {
          "type": "field_image",
          "src": Blockly.mainWorkspace.options.pathToMedia + "/extensions/ZE3P.png",
          "width": 24,
          "height": 24
        },
        {
          "type": "field_vertical_separator"
        }
      ],
      "message1": "播放旋律%1",
      "args1": [
        {
          "type": "field_tone",
          "name": "TONE",
        }
      ],
      "category": Blockly.Categories.sounds,
      "extensions": ["colours_sounds", "shape_statement"]
    });
  }
}

(6)添加toolbox配置,代码如下:

<block type="ZE3P_play_melody" id="ZE3P_play_melody"></block>

(7)转码实现以python为例,块生成的value格式为:字符串"C5 - - G F E D C 200",以空格为分隔符,前八个为音符,每一个代表特定的频率,其中'-'为休止符,最后一个数字为BPM,和持续时间的关系为 time = 60000 / BPM,代码如下:

Blockly.Python['ZE3P_play_melody'] = function (block) {
  const toneString = block.getFieldValue('TONE') || "";
  const melody = toneString.split(" ");
  const time = parseInt(melody.pop());
  console.log(melody, time)
  // 根据实际代码而定
  return `music.play_melody("${melody.join(" ")}", ${time})\n`;
};

提示:如果采用注册方法,最好本地编译一下,使用javascript引入则不需要 

四、效果展示

 五、完整代码

完整field_tone.js代码如下:

'use strict';

goog.provide('Blockly.FieldTone');

goog.require('Blockly.DropDownDiv');
goog.require('Blockly.AudioContext');

/**
 * 构造器
 * @param music
 * @constructor
 */
Blockly.FieldTone = function (music) {
  music = "C5 B A G F E D C 200";

  // 初始化
  this.tones_ = [];
  Blockly.FieldTone.superClass_.constructor.call(this, music);
  this.addArgType('music');

  // 缩略图节点按钮
  this.buttonThumbNodes_ = [];
  // 编辑节点按钮
  this.buttonNodes_ = [];
  // 播放状态
  this.isPlaying = false;
  // 播放定时器
  this.playTimer = null;

  // 事件句柄
  this.bpmWrapper_ = null;
  this.toneWrapper_ = null;
  this.playWrapper_ = null;
  this.finishWrapper_ = null;
};
goog.inherits(Blockly.FieldTone, Blockly.Field);

/**
 * Json解析
 */
Blockly.FieldTone.fromJson = function (options) {
  return new Blockly.FieldTone(options['music']);
};

/**
 * 缩略图节点宽度
 * @type {number}
 * @const
 */
Blockly.FieldTone.THUMBNAIL_NODE_SIZE = 10;

/**
 * 缩略图节点圆角
 * @type {number}
 * @const
 */
Blockly.FieldTone.THUMBNAIL_NODE_ROUND = 2;

/**
 * 缩略图节点之间间距
 * @type {number}
 * @const
 */
Blockly.FieldTone.THUMBNAIL_NODE_GAP = 3;

/**
 * 缩略图节点父块顶部距离
 * @type {number}
 * @const
 */
Blockly.FieldTone.THUMBNAIL_NODE_TOP = 5;

/**
 * 节点宽度
 * @type {number}
 * @const
 */
Blockly.FieldTone.EDIT_NODE_SIZE = 20;

/**
 * 节点圆角
 * @type {number}
 * @const
 */
Blockly.FieldTone.EDIT_NODE_ROUND = 3;

/**
 * 节点之间间距
 * @type {number}
 * @const
 */
Blockly.FieldTone.EDIT_NODE_GAP = 6;

/**
 * 编辑音频面板内边距
 * @type {number}
 * @const
 */
Blockly.FieldTone.EDIT_DROPDOWN_PAD = 10;

/**
 * 节拍个数
 * @type {number}
 * @const
 */
Blockly.FieldTone.NODE_SIZE = 8;

/**
 * 最大节拍
 * @type {number}
 * @const
 */
Blockly.FieldTone.MAX_BPM = 1000;

/**
 * 最大节拍
 * @type {number}
 * @const
 */
Blockly.FieldTone.MIN_BPM = 1;

/**
 * 默认值
 * @type {string}
 */
Blockly.FieldTone.DEFAULT_NOTE = "- - - - - - - - " + Blockly.FieldTone.MIN_BPM;

/**
 * 初始化
 */
Blockly.FieldTone.prototype.init = function () {
  if (this.fieldGroup_) {
    return;
  }

  // 重建DOM
  this.fieldGroup_ = Blockly.utils.createSvgElement('g', {}, null);
  this.size_.width = Blockly.FieldTone.THUMBNAIL_NODE_SIZE * 11 +
    Blockly.FieldTone.THUMBNAIL_NODE_GAP * 7 + 24;
  this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_);

  if (!this.sourceBlock_.isShadow()) {
    this.box_ = Blockly.utils.createSvgElement('rect', {
      'rx': Blockly.BlockSvg.NUMBER_FIELD_CORNER_RADIUS,
      'ry': Blockly.BlockSvg.NUMBER_FIELD_CORNER_RADIUS,
      'x': 0,
      'y': 0,
      'width': this.size_.width,
      'height': this.size_.height,
      'stroke': this.sourceBlock_.getColourTertiary(),
      'fill': this.sourceBlock_.getColour(),
      'class': 'blocklyBlockBackground',
      'fill-opacity': 1
    }, null);
    this.fieldGroup_.insertBefore(this.box_, this.textElement_);
  }

  const nodeSize = Blockly.FieldTone.THUMBNAIL_NODE_SIZE;
  const nodeGap = Blockly.FieldTone.THUMBNAIL_NODE_GAP;
  const nodeHeight = this.size_.height - Blockly.FieldTone.THUMBNAIL_NODE_TOP * 2;
  const nodeRound = Blockly.FieldTone.THUMBNAIL_NODE_ROUND;

  // 创建图标
  this.iconElement_ = Blockly.utils.createSvgElement('image', {
    'height': '24px', 'width': '24px',
    'x': nodeSize, 'y': "4px",
  }, this.fieldGroup_);
  this.iconElement_.setAttributeNS('http://www.w3.org/1999/xlink',
    'xlink:href', Blockly.mainWorkspace.options.pathToMedia + '/extensions/music-block-icon.svg');

  // 创建缩略按键节点
  for (let i = 0; i < Blockly.FieldTone.NODE_SIZE; i++) {
    const x = nodeSize * 1.5 + (nodeSize + nodeGap) * i + 24;
    const attr = {
      'x': x, 'y': Blockly.FieldTone.THUMBNAIL_NODE_TOP,
      'width': nodeSize, 'height': nodeHeight,
      'rx': nodeRound, 'ry': nodeRound,
      'fill': '#dcdcdc',
      'stroke': '#f0f0f0'
    };
    this.buttonThumbNodes_.push(Blockly.utils.createSvgElement('rect', attr, this.fieldGroup_));
  }
  this.updateTone_();

  this.mouseDownWrapper_ = Blockly.bindEventWithChecks_(
    this.getClickTarget_(), 'mousedown', this, this.onMouseDown_);
};

/**
 * 设置值
 */
Blockly.FieldTone.prototype.setValue = function (newValue) {
  // 解析值
  newValue = this.parseValue(newValue);
  // 无改变
  if (newValue === null || newValue === this.value_) {
    return;
  }
  if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
    Blockly.Events.fire(new Blockly.Events.Change(this.sourceBlock_, 'field', this.name, this.value_, newValue));
  }
  // 更新值
  this.value_ = newValue;
  this.tones_ = this.valueToTones(newValue);
  // 更新缩略图
  this.updateTone_();
};

/**
 * 获取值
 */
Blockly.FieldTone.prototype.getValue = function () {
  return this.value_;
};

/**
 * 更新
 * @private
 */
Blockly.FieldTone.prototype.updateTone_ = function () {
  if (!this.buttonThumbNodes_) {
    return;
  }
  for (let i = 0; i < this.buttonThumbNodes_.length; i++) {
    // 更新缩略
    this.buttonThumbNodes_[i].setAttribute("fill", this.getToneColor(this.tones_[i]));
    // 更新按钮
    if (this.buttonNodes_ && this.buttonNodes_.length === 64) {
      for (let j = 0; j < Blockly.FieldTone.NODE_SIZE; j++) {
        const color = (this.getToneIndex(this.tones_[i]) === j) ?
          this.getToneColor(this.tones_[i]) : '#dcdcdc';
        this.buttonNodes_[j * 8 + i].setAttribute("fill", color);
      }
    }
  }
}

/**
 * 解析值
 * @param value
 */
Blockly.FieldTone.prototype.parseValue = function (value) {
  if (!value) {
    return Blockly.FieldTone.DEFAULT_NOTE;
  }
  // 删除边界空白
  value = value.trim();
  // 分割提取值
  let tones = value.split(" ");
  if (tones.length !== 9) {
    return Blockly.FieldTone.DEFAULT_NOTE;
  }
  // 异常音符值
  for (let i = 0; i < 8; i++) {
    if (!this.isValidNote(tones[i])) {
      return Blockly.FieldTone.DEFAULT_NOTE;
    }
  }
  // 异常节拍
  const bpm = parseInt(tones[8]);
  if (isNaN(bpm) || bpm > Blockly.FieldTone.MAX_BPM || bpm < Blockly.FieldTone.MIN_BPM) {
    return Blockly.FieldTone.DEFAULT_NOTE;
  }

  return value;
}

/**
 * 数据值转换位音符值
 * @param value
 */
Blockly.FieldTone.prototype.valueToTones = function (value) {
  value = this.parseValue(value);
  let data = value.split(" ");
  const tones = [];
  for (let i = 0; i < 8; i++) {
    tones.push(data[i]);
  }
  tones.push(parseInt(data[8]));
  return tones;
}

/**
 * 数据值转换位音符值
 * @param tones
 */
Blockly.FieldTone.prototype.tonesToValue = function (tones = []) {
  if (tones.length !== 9) {
    return Blockly.FieldTone.DEFAULT_NOTE;
  }
  return this.tones_.join(" ");
}

/**
 * 判断音符是否有效
 * @param tone
 * @returns {boolean}
 */
Blockly.FieldTone.prototype.isValidNote = function (tone) {
  const tones = ["C", "D", "E", "F", "G", "A", "B", "C5", "-"];
  return tones.includes(tone);
}

/**
 * 获取音符颜色
 * @param tone
 * @returns {string}
 */
Blockly.FieldTone.prototype.getToneColor = function (tone) {
  const toneColors = {
    "C": "#A80000",
    "D": "#D83B01",
    "E": "#FFB900",
    "F": "#107C10",
    "G": "#008272",
    "A": "#bb0dd5",
    "B": "#5C2D91",
    "C5": "#f88fe9",
  }
  return toneColors[tone] || "#DCDCDC";
}

/**
 * 获取音符值
 * @param index
 * @returns {Number}
 */
Blockly.FieldTone.prototype.getToneValue = function (index) {
  const tones = ["C", "D", "E", "F", "G", "A", "B", "C5"];
  if (index >= 0 && index < 8) {
    return tones[index];
  }
  return "-";
}

/**
 * 获取颜色下标
 * @param tone
 * @returns {Number}
 */
Blockly.FieldTone.prototype.getToneIndex = function (tone) {
  const tones = ["C", "D", "E", "F", "G", "A", "B", "C5"];
  return tones.indexOf(tone);
}

/**
 * 选择音符
 * @param index
 * @returns {number}
 */
Blockly.FieldTone.prototype.getToneCore = function (index) {
  const toneValues = [523, 494, 440, 392, 349, 330, 294, 262];
  if (index >= 0 && index < 8) {
    return toneValues[index];
  }
  return 0;
}

/**
 * 获取播放时长
 * @returns {number}
 */
Blockly.FieldTone.prototype.getDuration = function () {
  return 60000 / parseInt(this.tones_[8]);
}

/**
 * 显示下拉
 */
Blockly.FieldTone.prototype.showEditor_ = function () {

  Blockly.DropDownDiv.hideWithoutAnimation();
  Blockly.DropDownDiv.clearContent();
  // 创建下拉面板内容
  const contentDiv = Blockly.DropDownDiv.getContentDiv();
  const nodeSize = Blockly.FieldTone.NODE_SIZE;
  const toneSize = Blockly.FieldTone.EDIT_NODE_SIZE * nodeSize +
    Blockly.FieldTone.EDIT_NODE_GAP * (nodeSize - 1) +
    Blockly.FieldTone.EDIT_DROPDOWN_PAD * 2;
  this.toneStage_ = Blockly.utils.createSvgElement('svg', {
    'xmlns': 'http://www.w3.org/2000/svg',
    'height': toneSize + 'px',
    'width': toneSize + 'px'
  }, contentDiv);

  // 清空面板按钮
  this.buttonNodes_ = [];
  // 创建音符按钮
  for (let i = 0; i < nodeSize; i++) {
    for (let j = 0; j < nodeSize; j++) {
      // 计算按钮位置
      const x = (Blockly.FieldTone.EDIT_NODE_SIZE * j) +
        (Blockly.FieldTone.EDIT_NODE_GAP * j) +
        Blockly.FieldTone.EDIT_DROPDOWN_PAD;
      const y = (Blockly.FieldTone.EDIT_NODE_SIZE * i) +
        (Blockly.FieldTone.EDIT_NODE_GAP * i) +
        Blockly.FieldTone.EDIT_DROPDOWN_PAD;
      // 设置显示颜色
      const color = (this.getToneIndex(this.tones_[j]) === i) ?
        this.getToneColor(this.tones_[j]) : '#dcdcdc';
      const attr = {
        'x': x, 'y': y,
        'width': Blockly.FieldTone.EDIT_NODE_SIZE,
        'height': Blockly.FieldTone.EDIT_NODE_SIZE,
        'rx': Blockly.FieldTone.EDIT_NODE_ROUND,
        'ry': Blockly.FieldTone.EDIT_NODE_ROUND,
        'fill': color, 'stroke': 'white',
        "cursor": "pointer"
      };
      const tone = Blockly.utils.createSvgElement('rect', attr, this.toneStage_);
      this.toneStage_.appendChild(tone);
      this.buttonNodes_.push(tone);
    }
  }
  // 工具栏
  const toolDiv = document.createElement('div');
  toolDiv.className = "FieldToneTool";
  contentDiv.appendChild(toolDiv);

  // 左侧布局
  const leftDiv = document.createElement('div');
  toolDiv.appendChild(leftDiv);
  this.bpmInput = document.createElement('input');
  this.bpmInput.className = "blocklyHtmlInput";
  this.bpmInput.value = this.tones_[8];
  leftDiv.appendChild(this.bpmInput);
  const bpmSpan = document.createElement('span');
  bpmSpan.innerText = "BPM";
  leftDiv.appendChild(bpmSpan);

  // 按钮组
  const rightDiv = document.createElement('div');
  toolDiv.appendChild(rightDiv);
  this.playButton = document.createElement('button');
  this.playButton.innerText = "播放";
  rightDiv.appendChild(this.playButton);
  const finishButton = document.createElement('button');
  finishButton.innerText = "完成"
  rightDiv.appendChild(finishButton);

  // 计算下拉面板位置
  const scale = this.sourceBlock_.workspace.scale;
  let bBox = {width: this.size_.width, height: this.size_.height};
  bBox.width *= scale;
  bBox.height *= scale;
  const position = this.fieldGroup_.getBoundingClientRect();
  const primaryX = position.left + bBox.width / 2;
  const primaryY = position.top + bBox.height;
  const secondaryX = primaryX;
  const secondaryY = position.top;

  // 设置颜色
  Blockly.DropDownDiv.setBoundsElement(this.sourceBlock_.workspace.getParentSvg().parentNode);
  Blockly.DropDownDiv.show(this, primaryX, primaryY, secondaryX, secondaryY, this.onHide_.bind(this));
  const primaryColour = (this.sourceBlock_.isShadow()) ?
    this.sourceBlock_.parentBlock_.getColour() : this.sourceBlock_.getColour();
  Blockly.DropDownDiv.setColour(primaryColour, this.sourceBlock_.getColourTertiary());
  const category = (this.sourceBlock_.isShadow()) ?
    this.sourceBlock_.parentBlock_.getCategory() : this.sourceBlock_.getCategory();
  Blockly.DropDownDiv.setCategory(category);

  // 事件处理
  this.bpmWrapper_ = Blockly.bindEvent_(this.bpmInput, 'input', this, this.onBpmChange);
  this.toneWrapper_ = Blockly.bindEvent_(this.toneStage_, 'mousedown', this, this.onNoteClick);
  this.playWrapper_ = Blockly.bindEvent_(this.playButton, 'click', this, this.onPlayClick);
  this.finishWrapper_ = Blockly.bindEvent_(finishButton, 'click', this, this.onFinishClick);
};

/**
 * 校验是否点击音符
 * @param e
 * @returns {number}
 * @private
 */
Blockly.FieldTone.prototype.checkForNote_ = function (e) {
  const bBox = this.toneStage_.getBoundingClientRect();
  const size = Blockly.FieldTone.NODE_SIZE;
  const nodeSize = Blockly.FieldTone.EDIT_NODE_SIZE;
  const nodePad = Blockly.FieldTone.EDIT_NODE_GAP;
  const dx = e.clientX - bBox.left;
  const dy = e.clientY - bBox.top;

  for (let i = 0; i < size; i++) {
    for (let j = 0; j < size; j++) {
      const item = this.buttonNodes_[i * size + j];
      const x = parseInt(item.getAttribute("x"));
      const y = parseInt(item.getAttribute("y"));
      if (x <= dx && dx < x + nodeSize && y <= dy && dy < y + nodeSize) {
        return i * size + j;
      }
    }
  }

  return -1;
};

/**
 * 节拍输入事件
 */
Blockly.FieldTone.prototype.onBpmChange = function () {
  this.stopMelody_();
  let value = this.bpmInput.value;
  value = Number(parseInt(value));
  if (isNaN(value) || value <= Blockly.FieldTone.MIN_BPM) {
    value = Blockly.FieldTone.MIN_BPM;
  } else if (value >= Blockly.FieldTone.MAX_BPM) {
    value = Blockly.FieldTone.MAX_BPM;
  }
  this.tones_[8] = value;
  this.bpmInput.value = value;

  // 更新值
  this.setValue(this.tonesToValue(this.tones_));
}

/**
 * 点击音符按钮
 */
Blockly.FieldTone.prototype.onNoteClick = function (e) {
  const index = this.checkForNote_(e);
  if (index >= this.buttonNodes_.length || index === -1)
    return;
  const row = Math.trunc(index / Blockly.FieldTone.NODE_SIZE);
  const col = index % Blockly.FieldTone.NODE_SIZE;
  const tone = this.getToneValue(row);
  if (this.tones_[col] === tone) {
    this.tones_[col] = "-";
  } else {
    this.tones_[col] = this.getToneValue(row);
    // 播放点击音符
    this.playTone_(this.getToneCore(row));
  }
  // 更新值
  this.setValue(this.tonesToValue(this.tones_));
}

/**
 * 播放单个音符
 * @param toneValue
 */
Blockly.FieldTone.prototype.playTone_ = function (toneValue) {
  this.stopMelody_();
  this.updateGrid_();
  Blockly.AudioContext.tone(toneValue);
  setTimeout(() => {
    Blockly.AudioContext.stop();
  }, this.getDuration());
}

/**
 * 点击播放按钮
 */
Blockly.FieldTone.prototype.onPlayClick = function () {
  this.isPlaying = !this.isPlaying;
  this.playButton.innerText = this.isPlaying ? "暂停" : "播放";
  if (this.isPlaying) {
    this.playMelody_(0);
  } else {
    this.stopMelody_();
  }
}

/**
 * 停止旋律播放
 */
Blockly.FieldTone.prototype.stopMelody_ = function () {
  clearTimeout(this.playTimer);
  Blockly.AudioContext.stop();
  this.playTimer = null;
  this.playButton.innerText = "播放";
  this.isPlaying = false;
}

/**
 * 开始播放旋律
 * @param index
 */
Blockly.FieldTone.prototype.playMelody_ = function (index) {
  if (index >= 8) {
    this.stopMelody_();
    this.updateGrid_();
  } else {
    // 更新格子,显示出当前播放的列
    this.updateGrid_(index);
    // 播放当前音符
    const tone = this.getToneCore(this.getToneIndex(this.tones_[index++]));
    if (tone) {
      Blockly.AudioContext.tone(tone);
    } else {
      Blockly.AudioContext.stop();
    }
    this.playTimer = setTimeout(() => {
      this.playMelody_(index);
    }, this.getDuration());
  }
}

/**
 * 更新格子样式
 * @param col
 */
Blockly.FieldTone.prototype.updateGrid_ = function (col = -1) {
  const nodeSize = Blockly.FieldTone.NODE_SIZE;
  for (let i = 0; i < nodeSize; i++) {
    for (let j = 0; j < nodeSize; j++) {
      const index = j * nodeSize + i;
      const fill = this.buttonNodes_[index].getAttribute("fill");
      // 只更新休止符
      if (fill === "#dcdcdc") {
        if (i === col) {
          this.buttonNodes_[index].setAttribute("fill-opacity", ".7");
        } else {
          this.buttonNodes_[index].setAttribute("fill-opacity", "1");
        }
      }
    }
  }
};

/**
 * 点击完成按钮
 */
Blockly.FieldTone.prototype.onFinishClick = function () {
  Blockly.DropDownDiv.hide();
  this.stopMelody_();
}

/**
 * 更新颜色
 * @package
 */
Blockly.FieldTone.prototype.updateColour = function () {
  if (this.fieldGroup_ && this.sourceBlock_) {
    this.fieldGroup_.setAttribute('stroke', this.sourceBlock_.getColourTertiary());
    if (this.sourceBlock_ && (this.sourceBlock_.disabled || this.sourceBlock_.getInheritedDisabled())) {
      this.fieldGroup_.setAttribute('stroke-opacity', ".25");
      this.fieldGroup_.setAttribute('fill-opacity', ".25");
    } else {
      this.fieldGroup_.setAttribute('stroke-opacity', "1");
      this.fieldGroup_.setAttribute('fill-opacity', "1");
    }
  }
};

/**
 * 隐藏时
 */
Blockly.FieldTone.prototype.onHide_ = function () {
  if (this.bpmWrapper_)
    Blockly.unbindEvent_(this.bpmWrapper_);
  if (this.toneWrapper_)
    Blockly.unbindEvent_(this.toneWrapper_);
  if (this.playWrapper_)
    Blockly.unbindEvent_(this.playWrapper_);
  if (this.finishWrapper_)
    Blockly.unbindEvent_(this.finishWrapper_);
};

Blockly.Field.register('field_tone', Blockly.FieldTone);

 完成audio_context.js代码图下:

'use strict';

goog.provide('Blockly.AudioContext');

/**
 * 音频管理
 * @constructor
 */
Blockly.AudioContext = function () {
}

/**
 * mute audio
 * @type {boolean}
 * @private
 */
Blockly.AudioContext._mute = false;

/**
 * AudioContext
 * @type {null}
 * @private
 */
Blockly.AudioContext._context = null;

/**
 * frequency
 * @type {number}
 * @private
 */
Blockly.AudioContext._frequency = 0;

/**
 * gain
 * @type {null}
 * @private
 */
Blockly.AudioContext._gain = null;

/**
 * OscillatorNode
 * @type {null}
 * @private
 */
Blockly.AudioContext._vco = null;

/**
 * Create context
 * @returns {null}
 */
Blockly.AudioContext.context = function () {
  if (!Blockly.AudioContext._context)
    Blockly.AudioContext._context = Blockly.AudioContext.freshContext();
  return Blockly.AudioContext._context;
}

/**
 * freshContext
 * @returns {undefined|AudioContext}
 */
Blockly.AudioContext.freshContext = function () {
  if (window.AudioContext) {
    try {
      return new window.AudioContext();
    } catch (e) {
    }
  }
  return undefined;
}

Blockly.AudioContext.mute = function (mute) {
  if (!Blockly.AudioContext._context)
    return;
  Blockly.AudioContext._mute = mute;
  Blockly.stop();
  if (mute && Blockly.AudioContext._vco) {
    Blockly.AudioContext._vco.disconnect();
    Blockly.AudioContext.gain.disconnect();
    Blockly.AudioContext._vco = undefined;
    Blockly.AudioContext.gain = undefined;
  }
}

/**
 * Stop
 */
Blockly.AudioContext.stop = function () {
  if (!Blockly.AudioContext._context)
    return;
  Blockly.AudioContext._gain.gain.setTargetAtTime(0, Blockly.AudioContext._context.currentTime, 0.015);
  Blockly.AudioContext._frequency = 0;
}

/**
 * frequency
 * @returns {number|*}
 */
Blockly.AudioContext.frequency = function () {
  return Blockly.AudioContext._frequency;
}

/**
 * Play
 * @param frequency
 */
Blockly.AudioContext.tone = function (frequency) {
  if (Blockly.AudioContext._mute)
    return;
  if (isNaN(frequency) || frequency < 0)
    return;
  Blockly.AudioContext._frequency = frequency;
  let ctx = Blockly.AudioContext.context();
  if (!ctx)
    return;
  try {
    if (!Blockly.AudioContext._vco) {
      Blockly.AudioContext._vco = ctx.createOscillator();
      Blockly.AudioContext._vco.type = 'triangle';
      Blockly.AudioContext._gain = ctx.createGain();
      Blockly.AudioContext._gain.gain.value = 0;
      Blockly.AudioContext._gain.connect(ctx.destination);
      Blockly.AudioContext._vco.connect(Blockly.AudioContext._gain);
      Blockly.AudioContext._vco.start(0);
    }
    Blockly.AudioContext._vco.frequency.linearRampToValueAtTime(frequency, Blockly.AudioContext._context.currentTime);
    Blockly.AudioContext._gain.gain.setTargetAtTime(.2, Blockly.AudioContext._context.currentTime, 0.015);
  } catch (e) {
    Blockly.AudioContext._vco = undefined;
  }
}

完成css样式代码,可以添加到core/css.js中,也可以写在页面的html中,我是直接写在了css.js中,直接追加到Blockly.Css.CONTENT中就行,代码如下:

'.FieldToneTool{',
  'display: flex;',
  'margin-bottom: 8px;',
  'padding: 0 10px;',
  'align-items: center;',
  'justify-content: space-between;',
  '}',

  '.FieldToneTool > div> input {',
  'width: 42px;',
  'height: 24px;',
  'border-radius: 3px;',
  "margin-right: 4px;",
  'font-size: 12px;',
  '}',

  '.FieldToneTool > div> span {',
  'color: white;',
  'font-size: 12px;',
  '}',


  '.FieldToneTool >div> button {',
  'padding: 0 8px;',
  'border-radius: 3px;',
  'cursor: pointer;',
  'height: 24px;',
  'outline: none;',
  'transition: all .1s;',
  'border: 1px solid rgba(0,0,0,.2);',
  'background-color:rgba(0,0,0,.1);',
  'font-size: 12px;',
  'color: white;',
  'margin-left: 8px;',
  '}',

  '.FieldToneTool >div> button:hover {',
  'background-color:rgba(0,0,0,.2);',
  '}',

  '.FieldToneTool >div> button:active {',
  'box-shadow: 0px 0px 0px 4px ' + Blockly.Colours.fieldShadow + ';',
  '}',

 六、关于我

作者:陆志敏

联系:761324428@qq.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/55441.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

信息系统网络安全整改方案

第1章 项目概述 1.1 项目目标 本方案将通过对公司网络信息系统的安全现状进行分析工作&#xff0c;参照国家信息系统等级保护要求&#xff0c;找出信息系统与安全等级保护要求之间的差距&#xff0c;给出相应的整改意见&#xff0c;推动 XX 企业公司网络信息系统安全整改工作的…

计算机毕设 深度学习手势识别 - yolo python opencv cnn 机器视觉

文章目录 0 前言1 课题背景2 卷积神经网络2.1卷积层2.2 池化层2.3 激活函数2.4 全连接层2.5 使用tensorflow中keras模块实现卷积神经网络 3 YOLOV53.1 网络架构图3.2 输入端3.3 基准网络3.4 Neck网络3.5 Head输出层 4 数据集准备4.1 数据标注简介4.2 数据保存 5 模型训练5.1 修…

与“云”共舞,联想凌拓的新科技与新突破

伴随着数字经济的高速发展&#xff0c;IT信息技术在数字中国建设中起到的驱动和支撑作用也愈发凸显。特别是2023年人工智能和ChatGPT在全球的持续火爆&#xff0c;更是为整个IT产业注入了澎湃动力。那么面对日新月异的IT信息技术&#xff0c;再结合疫情之后截然不同的经济环境和…

【抖音小游戏】 Unity制作抖音小游戏方案 最新完整详细教程来袭【持续更新】

前言 【抖音小游戏】 Unity制作抖音小游戏方案 最新完整详细教程来袭【持续更新】一、相关准备工作1.1 用到的相关网址1.2 注册字节开发者后台账号 二、相关集成工作2.1 下载需要的集成资源2.2 安装StarkSDK和starksdk-unity-tools工具包2.3 搭建测试场景 三、构建发布3.1 发布…

【深度学习】MAT: Mask-Aware Transformer for Large Hole Image Inpainting

论文&#xff1a;https://arxiv.org/abs/2203.15270 代码&#xff1a;https://github.com/fenglinglwb/MAT 文章目录 AbstractIntroductionRelated WorkMethod总体架构卷积头Transformer主体Adjusted Transformer Block Multi-Head Contextual Attention Style Manipulation Mo…

探索Vue组件通信的秘密:打破隔阂,实现数据共享

一、Vue组件通信 每个组件都有自己的数据, 提供在data中, 每个组件的数据是独立的, 组件数据无法互相直接访问 (合理的)但是如果需要跨组件访问数据, 就需要用到组件通信 要是有一万个商品&#xff1f;&#xff1f;&#xff1f;&#xff1f;就要写一万个吗&#xff1f;函数调用…

KubeSphere 3.4.0 发布:支持 K8s v1.26

2023 年 07 月 26 日&#xff0c;KubeSphere 开源社区激动地向大家宣布&#xff0c;KubeSphere 3.4.0 正式发布&#xff01; 让我们先简单回顾下之前三个大版本的主要变化&#xff1a; KubeSphere 3.1.0 新增了“边缘计算”、“计量计费” 等功能&#xff0c;将 Kubernetes 从…

myeclipse的Debug模式

1.表示当前实现继续运行直到下一个断点&#xff0c;快捷键为F8。 2.表示打断整个进程 3.表示进入当前方法&#xff0c;快捷键为F5。 4.表示运行下一行代码&#xff0c;快捷键为F6。 5.表示退出当前方法&#xff0c;返回到调用层&#xff0c;快捷键为F7。 6.表示当前线程的…

kotlin 编写一个简单的天气预报app(五)增加forcast接口并显示

参考资料 OpenWeatherMap提供了一个/forecast接口&#xff0c;用于获取未来几天的天气预报。你可以使用HTTP GET请求访问该接口&#xff0c;并根据你所在的城市或地理坐标获取相应的天气数据。 以下是一个示例请求的URL和一些常用的参数&#xff1a; URL: http://api.openwe…

我的创作纪念日——256天

机缘 最开始我写博客没有什么特别的原因&#xff0c;主要是因为以下几点&#xff1a; 练习自己的语言组织能力 记录自己学习生活中学到的知识 为和我同一个学习阶段的朋友提供帮助 事实上最开始我根本不指望我的博客有多少人看&#xff0c;主要是想找一个好的保存 Markdown 笔…

花费7元训练自己的GPT 2模型

在上一篇博客中&#xff0c;我介绍了用Tensorflow来重现GPT 1的模型和训练的过程。这次我打算用Pytorch来重现GPT 2的模型并从头进行训练。 GPT 2的模型相比GPT 1的改进并不多&#xff0c;主要在以下方面&#xff1a; 1. GPT 2把layer normalization放在每个decoder block的前…

PHP最简单自定义自己的框架(一)

为啥要定义自己的框架&#xff1a; 定制化需求&#xff1a;每个项目都有不同的需求和特点&#xff0c;使用通用的框架可能无法满足所有的要求。自定义框架可以根据具体需求进行定制&#xff0c;提供更加灵活和符合项目需求的解决方案。学习和成长&#xff1a;自定义框架是一个很…

STM32存储左右互搏 I2C总线读写EEPROM ZD24C1MA

STM32存储左右互搏 I2C总线读写EEPROM ZD24C1MA 在较低容量存储领域&#xff0c;EEPROM是常用的存储介质&#xff0c;不同容量的EEPROM的地址对应位数不同&#xff0c;在发送字节的格式上有所区别。EEPROM是非快速访问存储&#xff0c;因为EEPROM按页进行组织&#xff0c;在连…

一文搞懂Redis架构演化之路

目录 从最简单的开始&#xff1a;单机版 Redis 数据持久化&#xff1a;有备无患 主从复制&#xff1a;多副本 哨兵&#xff1a;故障自动切换 分片集群&#xff1a;横向扩展 总结 这篇文章我想和你聊一聊 Redis 的架构演化之路。 现如今 Redis 变得越来越流行&#xff0c;…

图为科技加入深圳市智能交通行业协会 ,打 …

图为科技加入深圳市智能交通行业协会&#xff0c;打造智能交通新生态&#xff01; 交通是国民经济发展的“大动脉”&#xff0c;交通拥堵、事故频发等问题不仅影响了人们的出行体验&#xff0c;也对经济的发展产生了负面影响。安全、高效、便捷的出行&#xff0c;一直是人们的…

【Unity实用插件篇】| 学会使用 可编程瓦片Tile Map,快速搭建2D地图

前言【Unity 实用插件篇】| 学会使用 可编程瓦片Tile Map,快速搭建2D地图一、导入 Tile Map Editor二、创建调色板 Tile Palette三、快速绘制地图四、TilePalette 调色板功能介绍五、TileMap 相关组件属性介绍GirdTilemapTilemap Renderer 瓦片地图渲染器Tile Assets 瓦片资源…

【Git】分支管理策略

文章目录 分支策略bug分支-master分支出现bug怎么办删除临时分⽀小结 分支策略 在实际开发中&#xff0c;我们应该按照⼏个基本原则进⾏分⽀管理&#xff1a; 1.master分⽀应该是⾮常稳定的&#xff0c;也就是仅⽤来发布新版本&#xff0c;平时不能在上⾯⼲活 2.⼲活都在dev…

Reinforcement Learning with Code 【Code 2. Tabular Sarsa】

Reinforcement Learning with Code 【Code 2. Tabular Sarsa】 This note records how the author begin to learn RL. Both theoretical understanding and code practice are presented. Many material are referenced such as ZhaoShiyu’s Mathematical Foundation of Rei…

Elasticsearch 全文检索 分词检索-Elasticsearch文章四

文章目录 官方文档地址refercence文档全文搜索体系match简单查询match 多词/分词单字段分词match多个词的逻辑控制match的匹配精度match_pharse_prefix分词前缀方式match_bool_prefixmulti_match多字段匹配 query string类型Interval类型DSL查询之Term详解聚合查询之Bucket聚合…

RTT(RT-Thread)线程管理(1.2W字详细讲解)

目录 RTT线程管理 线程管理特点 线程工作机制 线程控制块 线程属性 线程状态之间切换 线程相关操作 创建和删除线程 创建线程 删除线程 动态创建线程实例 启动线程 初始化和脱离线程 初始化线程 脱离线程 静态创建线程实例 线程辅助函数 获得当前线程 让出处…