朋友换一个音区唱熟悉的歌,起音与录音不同,你却往往马上认出来。到底是什么保留了下来?
旋律不只有一个个音高,也有音与音之间的关系。 这一课比较整体移动与局部改音。无需凭耳朵报出音名,可以把声音、图形与文字放在一起检查。
建议留十分钟,包含反复试听。先记住第一课的区分:声音何时开始,与它有多高,是可以分别改变的条件。这里固定时间,只动音高。
先听起伏,再量距离
先播「原版」,再播「全部 +5」。可以轻轻哼唱,也可以用手跟随起伏。第二版像不像同一个想法搬到了更高的位置?接着比较「单音 +1」与原版,留意最高点附近。
正在呈现知识画面
把整条旋律移高三个版本都是 100 BPM、八个等距音。第二版全部升五个半音;第三版只把第四个音升一个半音。
{"id":"music-pitch-shift","title":"把整条旋律移高","summary":"三个版本都是 100 BPM、八个等距音。第二版全部升五个半音;第三版只把第四个音升一个半音。","height":760,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"mf-intro\" data-i18n=\"intro\"></p>\n<div id=\"variants\" class=\"mf-options\" role=\"group\" data-i18n-label=\"choose\"></div>\n<p id=\"facts\" class=\"mf-facts\" aria-live=\"polite\"></p>\n<svg id=\"plot\" class=\"mf-plot\" viewBox=\"0 0 540 200\" aria-hidden=\"true\"></svg>\n<div class=\"mf-axis\"><span data-i18n=\"axis\"></span><span id=\"duration\"></span></div>\n<div id=\"groups\" class=\"mf-groups\"></div><p id=\"notation\" class=\"mf-notation\"></p><p class=\"mf-key\" data-i18n=\"key\"></p>\n<div id=\"envelope-panel\" hidden><svg id=\"envelope-plot\" class=\"mf-plot\" viewBox=\"0 0 540 120\" aria-hidden=\"true\"></svg><p class=\"mf-key\" data-i18n=\"envelope\"></p></div>\n<div class=\"mf-actions\"><button id=\"play\" data-i18n=\"play\"></button><button id=\"stop\" data-i18n=\"stop\" disabled></button></div>\n<div class=\"mf-progress\" aria-hidden=\"true\"><span id=\"progress\"></span></div><p id=\"status\" class=\"mf-status\" role=\"status\" data-i18n=\"idle\"></p>\n<div class=\"mf-volume\"><label for=\"volume\" data-i18n=\"volume\"></label><input id=\"volume\" type=\"range\" min=\"0\" max=\"100\" value=\"65\"></div>\n","css":"body{padding:24px}h2{font-size:22px;line-height:1.4}.mf-intro{color:var(--muted);margin:10px 0 18px;line-height:1.7}.mf-options{display:flex;gap:8px;flex-wrap:wrap}.mf-options button{flex:1 1 125px;min-height:46px;font-size:16px;white-space:normal;line-height:1.45;text-align:left;padding:10px 12px}.mf-options button[aria-pressed=true]{background:var(--accent-soft);border-color:var(--accent);color:var(--accent)}.mf-facts{margin:16px 0;padding:14px;border-left:3px solid var(--accent);background:var(--surface);font-size:16px;line-height:1.75;min-height:4em}.mf-plot{width:100%;height:auto;display:block;margin:12px 0 0;background:var(--surface);border:1px solid var(--rule);border-radius:8px}.mf-axis{display:flex;justify-content:space-between;font-size:13px;color:var(--muted);gap:16px;margin:6px 0}.mf-notation{font-size:15px;line-height:1.8;overflow-wrap:anywhere;margin:10px 0;color:var(--ink)}.mf-groups{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0}.mf-groups span{padding:6px 10px;border:1px solid var(--rule);border-radius:5px;font-size:15px;white-space:pre}.mf-actions{display:flex;gap:8px;margin:18px 0 12px}.mf-actions button{min-height:46px;font-size:16px;flex:1}.mf-progress{height:3px;background:var(--rule);overflow:hidden}.mf-progress span{display:block;height:100%;background:var(--accent);width:0}.mf-status{font-size:14px;line-height:1.7;margin:10px 0;min-height:3.4em;color:var(--muted)}.mf-volume{display:flex;gap:12px;align-items:center;margin-top:12px;border-top:1px solid var(--rule);padding-top:12px;font-size:14px}.mf-volume input{min-width:60px;max-width:180px}.mf-volume label{white-space:nowrap}.mf-key{color:var(--muted);font-size:14px;line-height:1.6;margin:12px 0}@media(max-width:420px){body{padding:20px 14px}h2{font-size:20px}.mf-options button{flex-basis:105px}.mf-facts{padding:12px}}\n\n.mf-groups span.is-current{border-color:var(--accent);background:var(--accent-soft);box-shadow:inset 0 -2px 0 var(--accent)}\n\n[hidden]{display:none!important}\n#blind-answers{flex-wrap:wrap}#blind-answers button{flex:1 1 100px;white-space:normal;line-height:1.5;min-width:0;padding:10px}\n","js":"const MODEL = {\"mode\":\"notes\",\"min\":60,\"max\":74,\"blind\":false,\"variants\":[{\"score\":{\"bpm\":100,\"beats\":8,\"events\":[{\"midi\":60,\"hz\":261.625565,\"beat\":0,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":62,\"hz\":293.664768,\"beat\":1,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":64,\"hz\":329.627557,\"beat\":2,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":67,\"hz\":391.995436,\"beat\":3,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":64,\"hz\":329.627557,\"beat\":4,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":62,\"hz\":293.664768,\"beat\":5,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":60,\"hz\":261.625565,\"beat\":6,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":60,\"hz\":261.625565,\"beat\":7,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"}]},\"notation\":\"C4 · D4 · E4 · G4 · E4 · D4 · C4 · C4\",\"groups\":[]},{\"score\":{\"bpm\":100,\"beats\":8,\"events\":[{\"midi\":65,\"hz\":349.228231,\"beat\":0,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":67,\"hz\":391.995436,\"beat\":1,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":69,\"hz\":440.0,\"beat\":2,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":72,\"hz\":523.251131,\"beat\":3,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":69,\"hz\":440.0,\"beat\":4,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":67,\"hz\":391.995436,\"beat\":5,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":65,\"hz\":349.228231,\"beat\":6,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":65,\"hz\":349.228231,\"beat\":7,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"}]},\"notation\":\"F4 · G4 · A4 · C5 · A4 · G4 · F4 · F4\",\"groups\":[]},{\"score\":{\"bpm\":100,\"beats\":8,\"events\":[{\"midi\":60,\"hz\":261.625565,\"beat\":0,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":62,\"hz\":293.664768,\"beat\":1,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":64,\"hz\":329.627557,\"beat\":2,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":68,\"hz\":415.304698,\"beat\":3,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":64,\"hz\":329.627557,\"beat\":4,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":62,\"hz\":293.664768,\"beat\":5,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":60,\"hz\":261.625565,\"beat\":6,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":60,\"hz\":261.625565,\"beat\":7,\"duration\":0.48,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"}]},\"notation\":\"C4 · D4 · E4 · G♯4 · E4 · D4 · C4 · C4\",\"groups\":[]}]};\nconst $ = s => document.querySelector(s);\nconst variants = MODEL.variants;\nlet selected = 0;\nlet quizRevealed = !MODEL.blind;\nfunction visibility(){for(const q of ['#facts','#plot','.mf-axis','#groups','#notation','.mf-key'])$(q).toggleAttribute('hidden',!quizRevealed);document.body.dataset.quizRevealed=String(quizRevealed);}\nconst svgNS = 'http://www.w3.org/2000/svg';\nfunction node(tag, attrs = {}) { const el = document.createElementNS(svgNS, tag); for (const [k,v] of Object.entries(attrs)) el.setAttribute(k,v); return el; }\nfunction draw() {\n const v = variants[selected], plot = $('#plot'); plot.replaceChildren();\n $('#facts').textContent = viz.t('fact'+selected);\n $('#duration').textContent = viz.t('clip') + ' ' + (v.score.beats * 60 / v.score.bpm).toFixed(1) + ' ' + viz.t('seconds');\n $('#notation').textContent = v.notation;\n if (MODEL.mode === 'partials') {\n const e=v.score.events[0], p=e.partials;\n $('#envelope-panel').hidden=false;\n const envelope=$('#envelope-plot');envelope.replaceChildren();\n const attack=Math.min(e.attack,e.duration/2),points=[[12,108],[12+516*attack/e.duration,12]];\n for(let i=1;i<=60;i++){const t=attack+(e.duration-attack)*i/60,amplitude=Math.pow(.0001/(.05*e.gain),(t-attack)/(e.duration-attack));points.push([12+516*t/e.duration,108-amplitude*96]);}\n points.push([528,108]);envelope.append(node('polyline',{points:points.map(p=>p.join(',')).join(' '),fill:'none',stroke:'var(--accent)','stroke-width':3}));\n for(let i=0;i<4;i++) {\n plot.append(node('rect',{x:40+i*125,y:160-(p[i]||0)*125,width:58,height:(p[i]||0)*125,fill:'var(--accent)'}));\n const t=node('text',{x:69+i*125,y:185,'text-anchor':'middle',fill:'var(--ink)','font-size':18}); t.textContent=(i+1)+'×';plot.append(t);\n }\n } else {\n const events=v.score.events, min=MODEL.min??Math.min(...events.map(e=>e.midi)), max=MODEL.max??Math.max(...events.map(e=>e.midi));\n for(let i=0;i<=v.score.beats;i++) plot.append(node('line',{x1:12+i/v.score.beats*516,x2:12+i/v.score.beats*516,y1:16,y2:170,stroke:'var(--rule)'}));\n for(const e of events){\n const x=12+e.beat/v.score.beats*516;\n if(MODEL.mode==='pulse') plot.append(node('rect',{x,y:166-e.gain*140,width:Math.max(4,516/v.score.beats*.38),height:e.gain*140,rx:3,fill:'var(--accent)'}));\n else { const y=160-(e.midi-min)/Math.max(1,max-min)*130; plot.append(node('rect',{x,y:y-4,width:Math.max(4,Math.min(528-x,e.duration*v.score.bpm/60/v.score.beats*516)),height:8,rx:3,fill:e.layer==='bass'?'var(--ink)':'var(--accent)',opacity:e.layer==='chord'?'.45':'1'})); }\n }\n }\n if(MODEL.mode!=='partials') plot.append(node('line',{id:'cursor',x1:12,x2:12,y1:10,y2:177,stroke:'var(--ink)','stroke-width':2,visibility:'hidden'}));\n const groups=$('#groups'); groups.replaceChildren();\n for (const label of v.groups || []) {const chip=document.createElement('span');chip.textContent=label;groups.append(chip);}\n if(MODEL.blind)visibility();\n [...$('#variants').children].forEach((b,i)=>b.setAttribute('aria-pressed',String(i===selected)));\n}\nconst player=viz.audio({onState(state){document.body.dataset.audioState=state;$('#play').disabled=state!=='idle';$('#stop').disabled=state==='idle';$('#status').textContent=viz.t(state==='idle'?'idle':state==='starting'?'starting':'playing');},onTick(beat){const fraction=beat<0?0:Math.min(1,beat/variants[selected].score.beats);$('#progress').style.width=(fraction*100)+'%';const cursor=$('#cursor');if(cursor){cursor.setAttribute('x1',12+fraction*516);cursor.setAttribute('x2',12+fraction*516);cursor.setAttribute('visibility',beat<0?'hidden':'visible');}const chips=[...$('#groups').children];chips.forEach((el,i)=>el.classList.toggle('is-current',beat>=0&&i===Math.min(chips.length-1,Math.floor(fraction*chips.length))));},onError(){$('#status').textContent=viz.t('error');}});\nvariants.forEach((v,i)=>{const b=document.createElement('button');b.type='button';b.dataset.variant=i;b.textContent=viz.t('option'+i);b.onclick=()=>{player.stop();selected=i;draw();};$('#variants').append(b);});\n$('#play').onclick=()=>player.play(variants[selected].score);\n$('#stop').onclick=()=>player.stop();\n$('#volume').oninput=e=>player.setVolume(Number(e.target.value)/100);\ndocument.body.dataset.audioState='idle';draw();\n\nif(MODEL.blind){\n const panel=document.createElement('div');panel.className='mf-actions';panel.id='blind-answers';\n const result=document.createElement('p');result.className='mf-facts';result.id='blind-feedback';result.setAttribute('role','status');result.textContent=viz.t('before');\n for(const [key,correct]of [['choose_p',true],['choose_q',false],['reveal',null]]){const b=document.createElement('button');b.type='button';b.textContent=viz.t(key);b.dataset.blindAnswer=key;b.onclick=()=>{player.stop();quizRevealed=true;visibility();result.textContent=(correct===null?'':viz.t(correct?'correct':'wrong')+' ')+viz.t('comparison_correct');};panel.append(b);}\n $('#variants').after(panel,result);\n}\n","audio":true,"strings":{"axis":"时间 → · 越高表示音越高","choose":"选择版本","clip":"段长:","envelope":"振幅随时间 →(1.4 秒)。曲线升到峰值后衰减,比较起音阶段占用的时间。","error":"声音未能启动,请重试,或阅读图示与文字说明。","fact0":"C4 D4 E4 G4 E4 D4 C4 C4。注意上行和回落。","fact1":"F4 G4 A4 C5 A4 G4 F4 F4。音程和起音位置全部保留。","fact2":"只有 G4 变成 G♯4。大致起伏方向没变,但相邻的两个音程变了。","heading":"把整条旋律移高","idle":"选择版本后播放。修改会停止播放,每段播完自动停止。","intro":"比较原版、整体移高和只改一个音。保持同样的打拍速度。","key":"从左向右读。横条表示起音与预设时长,上下叠放表示同时发声。也可直接读音名与说明。","option0":"原版","option1":"全部 +5","option2":"单音 +1","play":"播放此版","playing":"正在播放,可随时停止。","seconds":"秒","starting":"正在准备声音…","stop":"停止","volume":"音量"}}
图从左到右表示时间,越高的位置表示越高的音;三个版本使用同一纵轴范围。横条宽度表示预设发声时长,不是响度。下方音名可作为不看图的替代信息。
原版以 C–D–E 开始,每次都上行两个半音。整体移高版以 F–G–A 开始,距离完全相同。整句每个音都高了五个半音,这叫移调:起音变了,音程关系与节奏保留。
局部修改中,G 变成 G♯。原来的 E 到 G 是三个半音,改后 E 到 G♯ 是四个;回到 E 的距离也一起变化。两条线仍大致先上后下。因此,旋律轮廓比精确的音程序列更粗略,方向相同不代表距离相同。
用少量词汇描述音高
| 概念 | 本实验中的意思 | 例子 |
| 音高 | 听到的高低 | C5 高于 C4 |
| 半音 | 钢琴相邻琴键之间的一步,包含黑键 | E 到 F;C 到 C♯ |
| 音程 | 两个音高之间的距离 | C 到 E 是四个半音 |
| 八度 | 这里跨十二个半音 | C4 到 C5 |
| 移调 | 每个音移动相同音程 | 全部升高五个半音 |
音名字母 A 到 G 循环使用。升号 ♯ 把所写音升高半音,降号 ♭ 把它降低半音。数字标记八度,本系列用 C4 表示中央 C。E–F、B–C 本来就相邻一个半音,不需要凭空补一枚黑键。半音与全音
实验采用十二平均律:十二个等分半音构成八度,升高八度对应频率翻倍。完成练习不需要先会算频率。其他调律和音乐传统可能以不同方式组织音高,这里从常见西方调性音乐的实用词汇入门。
换一条陌生旋律,检查关系
先预测:准确移调必须保留什么?只保留头尾够不够?只保留每步上行还是下行够不够?
分别将参照与 P、Q 比较。先选答案,再用揭示后的图示核对音名与距离。这是在练习核对关系,不是在测试你能否瞬间认音。
正在呈现知识画面
新旋律:哪版是移调?比较参照、P 和 Q,先判断哪版是移调,再显示图示与解释。
{"id":"music-pitch-check","title":"新旋律:哪版是移调?","summary":"比较参照、P 和 Q,先判断哪版是移调,再显示图示与解释。","height":760,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"mf-intro\" data-i18n=\"intro\"></p>\n<div id=\"variants\" class=\"mf-options\" role=\"group\" data-i18n-label=\"choose\"></div>\n<p id=\"facts\" class=\"mf-facts\" aria-live=\"polite\"></p>\n<svg id=\"plot\" class=\"mf-plot\" viewBox=\"0 0 540 200\" aria-hidden=\"true\"></svg>\n<div class=\"mf-axis\"><span data-i18n=\"axis\"></span><span id=\"duration\"></span></div>\n<div id=\"groups\" class=\"mf-groups\"></div><p id=\"notation\" class=\"mf-notation\"></p><p class=\"mf-key\" data-i18n=\"key\"></p>\n<div id=\"envelope-panel\" hidden><svg id=\"envelope-plot\" class=\"mf-plot\" viewBox=\"0 0 540 120\" aria-hidden=\"true\"></svg><p class=\"mf-key\" data-i18n=\"envelope\"></p></div>\n<div class=\"mf-actions\"><button id=\"play\" data-i18n=\"play\"></button><button id=\"stop\" data-i18n=\"stop\" disabled></button></div>\n<div class=\"mf-progress\" aria-hidden=\"true\"><span id=\"progress\"></span></div><p id=\"status\" class=\"mf-status\" role=\"status\" data-i18n=\"idle\"></p>\n<div class=\"mf-volume\"><label for=\"volume\" data-i18n=\"volume\"></label><input id=\"volume\" type=\"range\" min=\"0\" max=\"100\" value=\"65\"></div>\n","css":"body{padding:24px}h2{font-size:22px;line-height:1.4}.mf-intro{color:var(--muted);margin:10px 0 18px;line-height:1.7}.mf-options{display:flex;gap:8px;flex-wrap:wrap}.mf-options button{flex:1 1 125px;min-height:46px;font-size:16px;white-space:normal;line-height:1.45;text-align:left;padding:10px 12px}.mf-options button[aria-pressed=true]{background:var(--accent-soft);border-color:var(--accent);color:var(--accent)}.mf-facts{margin:16px 0;padding:14px;border-left:3px solid var(--accent);background:var(--surface);font-size:16px;line-height:1.75;min-height:4em}.mf-plot{width:100%;height:auto;display:block;margin:12px 0 0;background:var(--surface);border:1px solid var(--rule);border-radius:8px}.mf-axis{display:flex;justify-content:space-between;font-size:13px;color:var(--muted);gap:16px;margin:6px 0}.mf-notation{font-size:15px;line-height:1.8;overflow-wrap:anywhere;margin:10px 0;color:var(--ink)}.mf-groups{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0}.mf-groups span{padding:6px 10px;border:1px solid var(--rule);border-radius:5px;font-size:15px;white-space:pre}.mf-actions{display:flex;gap:8px;margin:18px 0 12px}.mf-actions button{min-height:46px;font-size:16px;flex:1}.mf-progress{height:3px;background:var(--rule);overflow:hidden}.mf-progress span{display:block;height:100%;background:var(--accent);width:0}.mf-status{font-size:14px;line-height:1.7;margin:10px 0;min-height:3.4em;color:var(--muted)}.mf-volume{display:flex;gap:12px;align-items:center;margin-top:12px;border-top:1px solid var(--rule);padding-top:12px;font-size:14px}.mf-volume input{min-width:60px;max-width:180px}.mf-volume label{white-space:nowrap}.mf-key{color:var(--muted);font-size:14px;line-height:1.6;margin:12px 0}@media(max-width:420px){body{padding:20px 14px}h2{font-size:20px}.mf-options button{flex-basis:105px}.mf-facts{padding:12px}}\n\n.mf-groups span.is-current{border-color:var(--accent);background:var(--accent-soft);box-shadow:inset 0 -2px 0 var(--accent)}\n\n[hidden]{display:none!important}\n#blind-answers{flex-wrap:wrap}#blind-answers button{flex:1 1 100px;white-space:normal;line-height:1.5;min-width:0;padding:10px}\n","js":"const MODEL = {\"mode\":\"notes\",\"min\":65,\"max\":74,\"blind\":true,\"variants\":[{\"score\":{\"bpm\":90,\"beats\":4,\"events\":[{\"midi\":65,\"hz\":349.228231,\"beat\":0,\"duration\":0.5,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":69,\"hz\":440.0,\"beat\":1,\"duration\":0.5,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":67,\"hz\":391.995436,\"beat\":2,\"duration\":0.5,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":72,\"hz\":523.251131,\"beat\":3,\"duration\":0.5,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"}]},\"notation\":\"F4 · A4 · G4 · C5\",\"groups\":[]},{\"score\":{\"bpm\":90,\"beats\":4,\"events\":[{\"midi\":67,\"hz\":391.995436,\"beat\":0,\"duration\":0.5,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":71,\"hz\":493.883301,\"beat\":1,\"duration\":0.5,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":69,\"hz\":440.0,\"beat\":2,\"duration\":0.5,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":74,\"hz\":587.329536,\"beat\":3,\"duration\":0.5,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"}]},\"notation\":\"G4 · B4 · A4 · D5\",\"groups\":[]},{\"score\":{\"bpm\":90,\"beats\":4,\"events\":[{\"midi\":67,\"hz\":391.995436,\"beat\":0,\"duration\":0.5,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":70,\"hz\":466.163762,\"beat\":1,\"duration\":0.5,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":69,\"hz\":440.0,\"beat\":2,\"duration\":0.5,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"},{\"midi\":74,\"hz\":587.329536,\"beat\":3,\"duration\":0.5,\"gain\":0.8,\"wave\":\"triangle\",\"layer\":\"melody\"}]},\"notation\":\"G4 · B♭4 · A4 · D5\",\"groups\":[]}]};\nconst $ = s => document.querySelector(s);\nconst variants = MODEL.variants;\nlet selected = 0;\nlet quizRevealed = !MODEL.blind;\nfunction visibility(){for(const q of ['#facts','#plot','.mf-axis','#groups','#notation','.mf-key'])$(q).toggleAttribute('hidden',!quizRevealed);document.body.dataset.quizRevealed=String(quizRevealed);}\nconst svgNS = 'http://www.w3.org/2000/svg';\nfunction node(tag, attrs = {}) { const el = document.createElementNS(svgNS, tag); for (const [k,v] of Object.entries(attrs)) el.setAttribute(k,v); return el; }\nfunction draw() {\n const v = variants[selected], plot = $('#plot'); plot.replaceChildren();\n $('#facts').textContent = viz.t('fact'+selected);\n $('#duration').textContent = viz.t('clip') + ' ' + (v.score.beats * 60 / v.score.bpm).toFixed(1) + ' ' + viz.t('seconds');\n $('#notation').textContent = v.notation;\n if (MODEL.mode === 'partials') {\n const e=v.score.events[0], p=e.partials;\n $('#envelope-panel').hidden=false;\n const envelope=$('#envelope-plot');envelope.replaceChildren();\n const attack=Math.min(e.attack,e.duration/2),points=[[12,108],[12+516*attack/e.duration,12]];\n for(let i=1;i<=60;i++){const t=attack+(e.duration-attack)*i/60,amplitude=Math.pow(.0001/(.05*e.gain),(t-attack)/(e.duration-attack));points.push([12+516*t/e.duration,108-amplitude*96]);}\n points.push([528,108]);envelope.append(node('polyline',{points:points.map(p=>p.join(',')).join(' '),fill:'none',stroke:'var(--accent)','stroke-width':3}));\n for(let i=0;i<4;i++) {\n plot.append(node('rect',{x:40+i*125,y:160-(p[i]||0)*125,width:58,height:(p[i]||0)*125,fill:'var(--accent)'}));\n const t=node('text',{x:69+i*125,y:185,'text-anchor':'middle',fill:'var(--ink)','font-size':18}); t.textContent=(i+1)+'×';plot.append(t);\n }\n } else {\n const events=v.score.events, min=MODEL.min??Math.min(...events.map(e=>e.midi)), max=MODEL.max??Math.max(...events.map(e=>e.midi));\n for(let i=0;i<=v.score.beats;i++) plot.append(node('line',{x1:12+i/v.score.beats*516,x2:12+i/v.score.beats*516,y1:16,y2:170,stroke:'var(--rule)'}));\n for(const e of events){\n const x=12+e.beat/v.score.beats*516;\n if(MODEL.mode==='pulse') plot.append(node('rect',{x,y:166-e.gain*140,width:Math.max(4,516/v.score.beats*.38),height:e.gain*140,rx:3,fill:'var(--accent)'}));\n else { const y=160-(e.midi-min)/Math.max(1,max-min)*130; plot.append(node('rect',{x,y:y-4,width:Math.max(4,Math.min(528-x,e.duration*v.score.bpm/60/v.score.beats*516)),height:8,rx:3,fill:e.layer==='bass'?'var(--ink)':'var(--accent)',opacity:e.layer==='chord'?'.45':'1'})); }\n }\n }\n if(MODEL.mode!=='partials') plot.append(node('line',{id:'cursor',x1:12,x2:12,y1:10,y2:177,stroke:'var(--ink)','stroke-width':2,visibility:'hidden'}));\n const groups=$('#groups'); groups.replaceChildren();\n for (const label of v.groups || []) {const chip=document.createElement('span');chip.textContent=label;groups.append(chip);}\n if(MODEL.blind)visibility();\n [...$('#variants').children].forEach((b,i)=>b.setAttribute('aria-pressed',String(i===selected)));\n}\nconst player=viz.audio({onState(state){document.body.dataset.audioState=state;$('#play').disabled=state!=='idle';$('#stop').disabled=state==='idle';$('#status').textContent=viz.t(state==='idle'?'idle':state==='starting'?'starting':'playing');},onTick(beat){const fraction=beat<0?0:Math.min(1,beat/variants[selected].score.beats);$('#progress').style.width=(fraction*100)+'%';const cursor=$('#cursor');if(cursor){cursor.setAttribute('x1',12+fraction*516);cursor.setAttribute('x2',12+fraction*516);cursor.setAttribute('visibility',beat<0?'hidden':'visible');}const chips=[...$('#groups').children];chips.forEach((el,i)=>el.classList.toggle('is-current',beat>=0&&i===Math.min(chips.length-1,Math.floor(fraction*chips.length))));},onError(){$('#status').textContent=viz.t('error');}});\nvariants.forEach((v,i)=>{const b=document.createElement('button');b.type='button';b.dataset.variant=i;b.textContent=viz.t('option'+i);b.onclick=()=>{player.stop();selected=i;draw();};$('#variants').append(b);});\n$('#play').onclick=()=>player.play(variants[selected].score);\n$('#stop').onclick=()=>player.stop();\n$('#volume').oninput=e=>player.setVolume(Number(e.target.value)/100);\ndocument.body.dataset.audioState='idle';draw();\n\nif(MODEL.blind){\n const panel=document.createElement('div');panel.className='mf-actions';panel.id='blind-answers';\n const result=document.createElement('p');result.className='mf-facts';result.id='blind-feedback';result.setAttribute('role','status');result.textContent=viz.t('before');\n for(const [key,correct]of [['choose_p',true],['choose_q',false],['reveal',null]]){const b=document.createElement('button');b.type='button';b.textContent=viz.t(key);b.dataset.blindAnswer=key;b.onclick=()=>{player.stop();quizRevealed=true;visibility();result.textContent=(correct===null?'':viz.t(correct?'correct':'wrong')+' ')+viz.t('comparison_correct');};panel.append(b);}\n $('#variants').after(panel,result);\n}\n","audio":true,"strings":{"axis":"时间 → · 越高表示音越高","before":"选好答案,再核对解释。","choose":"选择版本","choose_p":"P 是移调","choose_q":"Q 是移调","clip":"段长:","compare_title":"先作答,再显示图示","comparison_correct":"P 保留全部音程,Q 改了内部一个音。请核对揭示后的距离。","correct":"正确。","envelope":"振幅随时间 →(1.4 秒)。曲线升到峰值后衰减,比较起音阶段占用的时间。","error":"声音未能启动,请重试,或阅读图示与文字说明。","fact0":"比较距离:+4、−2、+5 个半音。","fact1":"G4 B4 A4 D5。各音移动距离一致。","fact2":"G4 B♭4 A4 D5。第二个音移动得少一点。","heading":"新旋律:哪版是移调?","idle":"选择版本后播放。修改会停止播放,每段播完自动停止。","intro":"先听参照、P 和 Q,再读实验后面的答案。","key":"从左向右读。横条表示起音与预设时长,上下叠放表示同时发声。也可直接读音名与说明。","option0":"参照","option1":"版本 P","option2":"版本 Q","play":"播放此版","playing":"正在播放,可随时停止。","reveal":"显示解释","seconds":"秒","starting":"正在准备声音…","stop":"停止","volume":"音量","wrong":"还不对,请对照解释检查所选答案。"}}
查看答案与音程计算
P 是准确移调。 参照 F–A–G–C 的相邻距离为 +4、−2、+5 个半音。P 的 G–B–A–D 保留同样序列,每个音都比对应参照高两个半音。
Q 的 G–B♭–A–D 则为 +3、−1、+5。它仍然上、下、上,首尾也与 P 相同,但内部距离不同。只检查端点会漏掉这次修改。
无法播放时,可把参照写成 65、69、67、72,把 P 写成 67、71、69、74。对应相减,四个答案都是 +2。这些数字只是方便计数,不是额外要求背诵的记谱法。
认出旋律,不代表所有效果相同
移调会不会改变音乐的感觉?
会。高音区可能更突出、更明亮,也可能更难唱。乐器在不同音区的音色不同,歌手也有音域限制。即使这里固定合成音色,改变音区仍会改变听感。保留的是音程结构,并非全部表现效果。
认歌还依赖节奏、重复、歌词和记忆。改过一个音的旋律也可能认得出来,因此「我认得这首歌」不能精确证明所有音程都没变。
选一条熟悉旋律,从两个舒适的起音各哼几音。观察哪些跳进保留,哪里为了迁就音域而把距离唱小。试着说清「整句移动了」还是「这个音程改变了」。
下一课把这些音高关系写到五线谱上,学习怎样读出它们。