You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaScript和弦移调功能异常:重复点击按钮多次移调问题

问题描述

我有7个和弦按钮(C、D、E、F、G、A、B)、一个文本域(textarea)以及原调选择器。将带和弦的《Fly me to the moon》歌词复制到文本域后,点击E按钮一次,和弦能正常升高一个调;但再次点击同一按钮时,和弦会继续升高,出现重复转调的异常。以下是基于Tonal库的实现代码:

问题原因

核心问题在于转调逻辑始终基于初始原调计算区间,而非当前文本的实际调性:

  • 每次点击转调按钮时,代码用window.originalKey(初始或选择的原调)和目标按钮的调性计算转调区间,然后直接修改文本域内容。
  • 第一次点击后,文本域的和弦已经是转调后的结果,但window.originalKey并没有更新,第二次点击时,依然用原调计算相同的区间,导致已经转调过的和弦被再次转调,出现持续升高的异常。
  • 代码没有跟踪当前文本域的实际调性,每次操作都默认从原始调性开始转调。
解决方案

需要新增变量跟踪当前文本的实际调性,转调时基于当前调性计算区间,而非固定的原调:

  1. 保存原始歌词内容,避免原调切换时无法恢复初始状态。
  2. 新增currentKey变量记录当前文本的调性,初始值与原调一致。
  3. 点击转调按钮时,计算currentKey到目标调的区间,转调后更新currentKey为目标调。
  4. 切换原调选择器时,重置currentKey为新的原调,并将文本域恢复为原始歌词。
修改后的完整代码

HTML

<script src="https://cdn.jsdelivr.net/npm/tonal/browser/tonal.min.js"></script>

<!-- 原调选择器 -->
<div>
    <label for="originalKey_id">原调:</label>
    <select id="originalKey_id">
        <option value="C">C</option>
        <option value="D">D</option>
        <option value="E">E</option>
        <option value="F">F</option>
        <option value="G">G</option>
        <option value="A">A</option>
        <option value="B">B</option>
    </select>
</div>

<!-- 和弦按钮 -->
<div class="pads_container">
    <button class="pads" data-padd="C">C</button>
    <button class="pads" data-padd="D">D</button>
    <button class="pads" data-padd="E">E</button>
    <button class="pads" data-padd="F">F</button>
    <button class="pads" data-padd="G">G</button>
    <button class="pads" data-padd="A">A</button>
    <button class="pads" data-padd="B">B</button>
</div>

<textarea value="" id="textarea" rows="5" cols="25">
 D             G             A              D
Fly me to the moon, let me play among the stars
</textarea>

CSS

body {
    font-family: Arial, sans-serif;
    background-color: #333;
    color: #ccc;
    text-align: center;
}

.pads_container {
    position:relative;
    top:2vmin;  
    display:flex;
    width:95%;
    left:1vmin;
    right:1vmin;
}

button.pads {
    padding: 1em;
    font-size: 3vmin;
    border: 1px solid #ccc;
    background: green;
      color: #fff;
    cursor: pointer;
}

button.pads.blue {
    background-color: #007bff;
    color: #fff;
}

button.pads:hover {
    background-color: #00a3d9;
}

#textarea {
    top:3em;
    position:relative;
    width:100%;
    background: #222;
    color: #ddd;
}

JavaScript

const { transpose, Interval } = Tonal;

// 保存原始歌词和当前调性
let originalLyrics = document.getElementById("textarea").value;
let currentKey = 'C'; // 初始当前调性与原调一致

// 转调函数:基于当前调性计算区间
function transposeChords(text, interval) {
    return text.replace(/\b([A-G][#b]?)(m|min|maj|dim|dim7|aug|sus|add)?([0-9]*)\b/g, (match, root, type, number) => {
        let chordType = type;

        // 处理扩展和弦
        if (number) {
            chordType = chordType || '';
            if (number.match(/\d+/)) {
                chordType += number; 
            }
        }
        
        try {
            const transposedRoot = transpose(root, interval);
            const transposedChord = transposedRoot + (chordType || '');
            return transposedChord || match;
        } catch (e) {
            console.error('转调出错', match, e);
            return match;
        }
    });
}

// 处理转调按钮点击
window.transpose = function(padd) {
    // 计算当前调性到目标调的区间
    let interval = Interval.distance(currentKey, padd);
    let lyrics = document.getElementById("textarea").value;
    document.getElementById('textarea').value = transposeChords(lyrics, interval);
    // 更新当前调性为目标调
    currentKey = padd;
    colorPads(padd);
};

// 高亮当前选中的按钮
function colorPads(padd) {
    const buttons = document.querySelectorAll('.pads');
    buttons.forEach(button => {
        if (button.dataset.padd === padd) {
            button.classList.add('blue');
        } else {
            button.classList.remove('blue');
        }
    });
}

// 切换原调时重置状态
function originalKey_function() {
    const select_OrigKey = document.getElementById('originalKey_id');
    window.originalKey = select_OrigKey.value;
    // 重置当前调性为新的原调
    currentKey = window.originalKey;
    // 恢复文本域为原始歌词
    document.getElementById("textarea").value = originalLyrics;
    colorPads(window.originalKey);
}

// 按钮点击事件监听
document.querySelectorAll('.pads').forEach(button => {
    button.addEventListener('click', () => {
        const padd = button.dataset.padd;
        window.transpose(padd);
    });
});

// 原调选择器变更事件监听
document.getElementById('originalKey_id').addEventListener('change', originalKey_function);

// 初始化
window.originalKey = 'C';
colorPads(window.originalKey);

内容的提问来源于stack exchange,提问作者Alex

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 07:32:06