听音乐时,你可能会跟着点头、打拍子。有的鼓点一下一下很整齐,有的听起来像在往前赶,还有的留了许多空隙。
这些差别,都能用“快一点、慢一点”解释吗?
音乐走得多快,以及声音落在什么时候,是两个可以分别改变的条件。 这篇就从耳朵出发,把它们分清。
这是「乐理基础」的第一篇。无需乐器,也不用先认五线谱。读完并做完练习,你应当能比较两段短节奏,说明变化发生在速度,还是敲击的位置上。
先听:哪一段让你觉得更有变化?
把设备音量调到舒适的位置,分别听下面的 A 和 B。每段都很短,播完自动停止。先记住自己的感觉:哪段更整齐,哪段更跳跃?如果觉得差别不大,也可以继续。
正在呈现知识画面
先听两个版本,再看声音落在哪里A 与 B 都以每分钟 100 拍播放两小节。每小节均敲四次:A 落在第 1、2、3、4 拍;B 落在第 1 拍、第 2 拍后半拍、第 3 拍和第 4 拍后半拍。音色、音量和段长相同,敲击位置不同。
{"id":"music-rhythm-compare","title":"先听两个版本,再看声音落在哪里","summary":"A 与 B 都以每分钟 100 拍播放两小节。每小节均敲四次:A 落在第 1、2、3、4 拍;B 落在第 1 拍、第 2 拍后半拍、第 3 拍和第 4 拍后半拍。音色、音量和段长相同,敲击位置不同。","height":440,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p>\n<div class=\"actions\"><button class=\"audio-play\" data-listen=\"a\" aria-pressed=\"false\" data-i18n=\"a\"></button><button class=\"audio-play\" data-listen=\"b\" aria-pressed=\"false\" data-i18n=\"b\"></button><button id=\"stop\" disabled data-i18n=\"stop\"></button></div>\n<div class=\"options\"><label><input id=\"reference\" type=\"checkbox\"><span data-i18n=\"reference\"></span></label><button id=\"reveal\" aria-expanded=\"false\" data-i18n=\"reveal\"></button></div>\n<div id=\"comparison\" hidden><div class=\"readout\"><strong>100 <span>BPM</span></strong><span data-i18n=\"fixed\"></span></div><p class=\"row-title\">A <span data-i18n=\"a_hint\"></span></p><div class=\"grid\" id=\"grid-a\" data-track=\"a\"></div><p class=\"row-title\">B <span data-i18n=\"b_hint\"></span></p><div class=\"grid\" id=\"grid-b\" data-track=\"b\"></div><p class=\"hint\" data-i18n=\"legend\"></p></div><p id=\"status\" class=\"status\" role=\"status\" aria-live=\"polite\" data-i18n=\"idle\"></p><div class=\"volume\"><label for=\"volume\" data-i18n=\"volume\"></label><input id=\"volume\" type=\"range\" min=\"0\" max=\"100\" value=\"65\" data-i18n-label=\"volume\"></div>","css":"\nbody {padding:24px;} h2 {font-size:21px;} .intro {color:var(--muted);margin:8px 0 18px;}\nbutton,label,.hint {font-size:16px;} button {min-height:46px;} .actions {display:flex;gap:8px;flex-wrap:wrap;margin:16px 0;}\n.actions button {flex:1 1 90px;} .audio-play::before {content:'';display:inline-block;width:10px;height:11px;background:currentColor;clip-path:polygon(0 0,100% 50%,0 100%);margin-right:5px;} .audio-play[aria-pressed=true]::before {clip-path:none;}\n.readout {display:flex;gap:12px;justify-content:space-between;align-items:baseline;padding:12px 14px;border-bottom:1px solid var(--rule);}\n.readout strong {font-size:28px;font-weight:500;} .readout span {color:var(--muted);font-size:14px;}\n.grid {display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:6px;margin:14px 0;}\n.cell {min-width:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:7px 2px;border:1px solid var(--rule);border-radius:7px;min-height:66px;font-size:14px;}\n.cell b {font-size:21px;line-height:1;} .cell small {font-size:13px;color:var(--muted);} .cell.on {background:var(--accent-soft);border-color:var(--accent);color:var(--accent);}\n.cell.current {outline:2px solid var(--ink);outline-offset:1px;} .grid button.cell {font-size:14px;}\n.row-title {margin-top:18px;font-size:16px;font-weight:600;display:flex;justify-content:space-between;}\n.row-title span {font-size:14px;font-weight:400;color:var(--muted);} .status {min-height:3.4em;margin:14px 0 0;font-size:15px;line-height:1.7;color:var(--muted);}\n.options {display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin:14px 0;} .options label {display:flex;align-items:center;gap:8px;min-height:44px;}\ninput[type=checkbox] {width:19px;height:19px;accent-color:var(--accent);} .volume {display:flex;align-items:center;gap:12px;border-top:1px solid var(--rule);padding-top:10px;margin-top:14px;}\n.volume label {font-size:14px;color:var(--muted);white-space:nowrap;} .volume input {max-width:140px;} .tempo {margin:14px 0;} .tempo label {display:flex;justify-content:space-between;}\n.feedback {padding:14px;background:var(--surface);border-radius:8px;margin-top:12px;line-height:1.7;font-size:16px;} .choices {display:flex;gap:7px;flex-wrap:wrap;} .choices button {flex:1 1 90px;}\n@media(max-width:420px) {body {padding:20px 14px;} .grid {grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;} .cell {min-height:58px;} h2 {font-size:20px;} .readout strong {font-size:25px;}}\n","js":"\nconst $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];\nconst patterns={a:[0,2,4,6],b:[0,3,4,7]};\nlet playingKey='', bpm=100;\nconst beatLabels=['1','&','2','&','3','&','4','&'];\nfunction grid(el, pattern, editable=false) {\n el.replaceChildren();\n for(let i=0;i<8;i++) {\n const cell=document.createElement(editable?'button':'div'); cell.className='cell'+(pattern.includes(i)?' on':''); cell.dataset.slot=i;\n if(editable) {cell.type='button';cell.setAttribute('aria-pressed',String(pattern.includes(i)));}\n cell.setAttribute('aria-label',viz.t('beat')+' '+(Math.floor(i/2)+1)+(i%2?viz.t('half'):'')+' · '+viz.t(pattern.includes(i)?'hit':'rest'));\n const label=document.createElement('small');label.textContent=beatLabels[i];\n const mark=document.createElement('b');mark.textContent=pattern.includes(i)?'●':'—';mark.setAttribute('aria-hidden','true');cell.append(label,mark);el.append(cell);\n }\n}\nfunction makeScore(pattern, tempo, reference=false) {\n const events=[];\n for(let bar=0;bar<2;bar++) {\n for(const step of pattern) events.push({beat:bar*4+step/2,hz:740,duration:.075,gain:1,wave:'triangle'});\n if(reference) for(let beat=0;beat<4;beat++) events.push({beat:bar*4+beat,hz:1500,duration:.035,gain:.45});\n }\n return {bpm:tempo,beats:8,events};\n}\nconst player=viz.audio({\n onState(state) {\n document.body.dataset.audioState=state;\n $$('[data-listen]').forEach(b=>b.setAttribute('aria-pressed',String(state!=='idle'&&b.dataset.listen===playingKey)));\n $('#stop').disabled=state==='idle';\n $('#status').textContent=viz.t(state==='playing'?'playing':state==='starting'?'starting':'idle');\n },\n onTick(beat) {\n const step=beat<0?-1:Math.floor(beat*2)%8;\n $$('.cell').forEach(c=>c.classList.toggle('current',beat>=0&&Number(c.dataset.slot)===step&&(!c.closest('[data-track]')||c.closest('[data-track]').dataset.track===playingKey)));\n },\n onError() {$('#status').textContent=viz.t('error');}\n});\n$('#stop').onclick=()=>player.stop();\n$('#volume').oninput=e=>player.setVolume(Number(e.target.value)/100);\nfunction listen(key,pattern,tempo,reference=false) {playingKey=key;return player.play(makeScore(pattern,tempo,reference));}\n\nfor(const key of ['a','b']) {grid($('#grid-'+key),patterns[key]);$('[data-listen='+key+']').onclick=()=>listen(key,patterns[key],100,$('#reference').checked);}\n$('#reference').onchange=()=>player.stop();\n$('#reveal').onclick=()=>{const expanded=$('#comparison').hidden;$('#comparison').hidden=!expanded;$('#reveal').setAttribute('aria-expanded',String(expanded));$('#reveal').textContent=viz.t(expanded?'hide':'reveal');};\n","audio":true,"strings":{"a":"试听 A","a_hint":"每一拍都敲一下","b":"试听 B","b_hint":"两次敲击移到半拍位置","beat":"第","error":"声音暂时无法播放。请确认设备未静音,再点一次试听;也可以先看图解。","eyebrow":"01 / 先听","fixed":"两个版本使用相同速度","half":"拍后半拍","heading":"A 和 B,听起来一样快吗?","hide":"收起图解","hit":"敲一下","idle":"每次播放两小节。可以随时停止或重新听。","intro":"先分别听 A 和 B,留意想跟着点头或拍手的感觉。暂时不用找标准答案。","legend":"● 表示敲一下,— 表示不敲;& 表示两拍之间的半拍位置。","playing":"正在播放两小节,播完会自动停止。","reference":"加入参考拍点","rest":"不敲","reveal":"显示拍点与落点","starting":"正在准备声音…","stop":"停止","volume":"音量"}}
现在显示拍点与落点,再听一次。两段的速度都是 100 BPM,每小节都敲四次,使用相同的音色和音量。B 把其中两次敲击往后移了半拍。
你可以勾选“加入参考拍点”:较轻、较高的声音始终等间隔地响起。试着跟着它数“1、2、3、4”,留意较低的敲击有时与拍点重合,有时落在两个拍点之间。
图中的一个格子表示半拍。● 是敲一下,— 是不敲;& 标记两拍之间的位置,可以读作“和”:一、和、二、和、三、和、四、和。 图案播放两遍,正好是两小节。
这里并不要求你一定把 B 听成“更快”。第一遍的感觉可以不同;需要核对的是:参考拍点没有加快,敲击相对拍点的位置变了。
分清:拍点、速度和节奏型
想象几个人一起走路。稳定的脚步可以帮助我们理解拍点;走得快慢对应速度;在第几步拍手、是否在两步之间拍手,则对应不同的节奏安排。
| 概念 | 这篇里怎样理解 | 怎样核对 |
| 拍点(beat) | 用来跟随音乐的规律时间参照 | 跟着参考声数拍 |
| 速度(tempo) | 这些拍点推进得多快 | 比较相邻拍点之间的时间 |
| 节奏型(rhythmic pattern) | 声音与空隙怎样安排在时间里 | 看敲击落在拍上,还是拍与拍之间 |
BPM 是每分钟的拍数。 在本篇约定的拍单位下,60 BPM 每秒一拍,120 BPM 每半秒一拍,100 BPM 则每 0.6 秒一拍。它不等于“每分钟响了多少声”:一拍里可以敲两下,也可以一声都不敲。拍点、速度与节拍的区分
本篇把四拍组织成一个小节,便于比较。小节是组织拍子的方式之一,音乐并非都用四拍一组。拍子和重音怎样组织,我们留到后面再听。
真实音乐里的节奏还包括声音的长短、重音和演奏细节。这次固定音色与短促的发声方式,先只观察敲击开始的位置,避免一次改变太多条件。
动手:先移动敲击,再改变速度
先做一个很小的修改:保持 100 BPM,把图案 A 的第二声从“2”移到它后面的“&”。试听后,数拍是否还能保持原来的步子?
接着恢复 A,保持格子不动,依次听 80、100、120 BPM。这次参考拍点也跟着变快,整段的时间被压短了。
正在呈现知识画面
保持一个条件,试着改变另一个这里把每小节的四拍各分为两格,图案连续播放两遍。恢复 A 或 B 后,可以保持每分钟 100 拍,只改变敲击位置;也可以保持图案不变,把速度切为 80、100、120 BPM。改动后停止播放,下次从头听。
{"id":"music-rhythm-workbench","title":"保持一个条件,试着改变另一个","summary":"这里把每小节的四拍各分为两格,图案连续播放两遍。恢复 A 或 B 后,可以保持每分钟 100 拍,只改变敲击位置;也可以保持图案不变,把速度切为 80、100、120 BPM。改动后停止播放,下次从头听。","height":650,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p>\n<div class=\"grid\" id=\"edit-grid\"></div><p class=\"hint\" data-i18n=\"legend\"></p>\n<div class=\"actions\"><button data-pattern=\"a\" data-i18n=\"restore_a\"></button><button data-pattern=\"b\" data-i18n=\"restore_b\"></button></div>\n<div class=\"tempo\"><label for=\"tempo\"><span data-i18n=\"tempo\"></span><output id=\"tempo-value\">100 BPM</output></label><input id=\"tempo\" type=\"range\" min=\"60\" max=\"140\" step=\"1\" value=\"100\" data-i18n-label=\"tempo\"></div>\n<div class=\"choices\"><button data-tempo=\"80\">80 BPM</button><button data-tempo=\"100\">100 BPM</button><button data-tempo=\"120\">120 BPM</button></div>\n<div class=\"options\"><label><input id=\"reference\" type=\"checkbox\" checked><span data-i18n=\"reference\"></span></label></div>\n<div class=\"actions\"><button class=\"audio-play primary\" data-listen=\"edit\" aria-pressed=\"false\" data-i18n=\"listen\"></button><button id=\"stop\" disabled data-i18n=\"stop\"></button></div><p class=\"hint\" id=\"configuration\"></p><p id=\"status\" class=\"status\" role=\"status\" aria-live=\"polite\" data-i18n=\"idle\"></p><div class=\"volume\"><label for=\"volume\" data-i18n=\"volume\"></label><input id=\"volume\" type=\"range\" min=\"0\" max=\"100\" value=\"65\" data-i18n-label=\"volume\"></div>","css":"\nbody {padding:24px;} h2 {font-size:21px;} .intro {color:var(--muted);margin:8px 0 18px;}\nbutton,label,.hint {font-size:16px;} button {min-height:46px;} .actions {display:flex;gap:8px;flex-wrap:wrap;margin:16px 0;}\n.actions button {flex:1 1 90px;} .audio-play::before {content:'';display:inline-block;width:10px;height:11px;background:currentColor;clip-path:polygon(0 0,100% 50%,0 100%);margin-right:5px;} .audio-play[aria-pressed=true]::before {clip-path:none;}\n.readout {display:flex;gap:12px;justify-content:space-between;align-items:baseline;padding:12px 14px;border-bottom:1px solid var(--rule);}\n.readout strong {font-size:28px;font-weight:500;} .readout span {color:var(--muted);font-size:14px;}\n.grid {display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:6px;margin:14px 0;}\n.cell {min-width:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:7px 2px;border:1px solid var(--rule);border-radius:7px;min-height:66px;font-size:14px;}\n.cell b {font-size:21px;line-height:1;} .cell small {font-size:13px;color:var(--muted);} .cell.on {background:var(--accent-soft);border-color:var(--accent);color:var(--accent);}\n.cell.current {outline:2px solid var(--ink);outline-offset:1px;} .grid button.cell {font-size:14px;}\n.row-title {margin-top:18px;font-size:16px;font-weight:600;display:flex;justify-content:space-between;}\n.row-title span {font-size:14px;font-weight:400;color:var(--muted);} .status {min-height:3.4em;margin:14px 0 0;font-size:15px;line-height:1.7;color:var(--muted);}\n.options {display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin:14px 0;} .options label {display:flex;align-items:center;gap:8px;min-height:44px;}\ninput[type=checkbox] {width:19px;height:19px;accent-color:var(--accent);} .volume {display:flex;align-items:center;gap:12px;border-top:1px solid var(--rule);padding-top:10px;margin-top:14px;}\n.volume label {font-size:14px;color:var(--muted);white-space:nowrap;} .volume input {max-width:140px;} .tempo {margin:14px 0;} .tempo label {display:flex;justify-content:space-between;}\n.feedback {padding:14px;background:var(--surface);border-radius:8px;margin-top:12px;line-height:1.7;font-size:16px;} .choices {display:flex;gap:7px;flex-wrap:wrap;} .choices button {flex:1 1 90px;}\n@media(max-width:420px) {body {padding:20px 14px;} .grid {grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;} .cell {min-height:58px;} h2 {font-size:20px;} .readout strong {font-size:25px;}}\n","js":"\nconst $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];\nconst patterns={a:[0,2,4,6],b:[0,3,4,7]};\nlet playingKey='', bpm=100;\nconst beatLabels=['1','&','2','&','3','&','4','&'];\nfunction grid(el, pattern, editable=false) {\n el.replaceChildren();\n for(let i=0;i<8;i++) {\n const cell=document.createElement(editable?'button':'div'); cell.className='cell'+(pattern.includes(i)?' on':''); cell.dataset.slot=i;\n if(editable) {cell.type='button';cell.setAttribute('aria-pressed',String(pattern.includes(i)));}\n cell.setAttribute('aria-label',viz.t('beat')+' '+(Math.floor(i/2)+1)+(i%2?viz.t('half'):'')+' · '+viz.t(pattern.includes(i)?'hit':'rest'));\n const label=document.createElement('small');label.textContent=beatLabels[i];\n const mark=document.createElement('b');mark.textContent=pattern.includes(i)?'●':'—';mark.setAttribute('aria-hidden','true');cell.append(label,mark);el.append(cell);\n }\n}\nfunction makeScore(pattern, tempo, reference=false) {\n const events=[];\n for(let bar=0;bar<2;bar++) {\n for(const step of pattern) events.push({beat:bar*4+step/2,hz:740,duration:.075,gain:1,wave:'triangle'});\n if(reference) for(let beat=0;beat<4;beat++) events.push({beat:bar*4+beat,hz:1500,duration:.035,gain:.45});\n }\n return {bpm:tempo,beats:8,events};\n}\nconst player=viz.audio({\n onState(state) {\n document.body.dataset.audioState=state;\n $$('[data-listen]').forEach(b=>b.setAttribute('aria-pressed',String(state!=='idle'&&b.dataset.listen===playingKey)));\n $('#stop').disabled=state==='idle';\n $('#status').textContent=viz.t(state==='playing'?'playing':state==='starting'?'starting':'idle');\n },\n onTick(beat) {\n const step=beat<0?-1:Math.floor(beat*2)%8;\n $$('.cell').forEach(c=>c.classList.toggle('current',beat>=0&&Number(c.dataset.slot)===step&&(!c.closest('[data-track]')||c.closest('[data-track]').dataset.track===playingKey)));\n },\n onError() {$('#status').textContent=viz.t('error');}\n});\n$('#stop').onclick=()=>player.stop();\n$('#volume').oninput=e=>player.setVolume(Number(e.target.value)/100);\nfunction listen(key,pattern,tempo,reference=false) {playingKey=key;return player.play(makeScore(pattern,tempo,reference));}\n\nlet pattern=[...patterns.a];\nfunction render() {\n grid($('#edit-grid'),pattern,true);$('#tempo-value').textContent=bpm+' BPM';\n $('#configuration').textContent=viz.t('current')+bpm+' BPM · '+pattern.length+viz.t('hits');\n $$('#edit-grid button').forEach(cell=>cell.onclick=()=>{player.stop();const step=Number(cell.dataset.slot);pattern=pattern.includes(step)?pattern.filter(x=>x!==step):[...pattern,step].sort((a,b)=>a-b);render();$('#edit-grid').children[step].focus();});\n $$('[data-tempo]').forEach(b=>b.setAttribute('aria-pressed',String(Number(b.dataset.tempo)===bpm)));\n}\nfunction tempo(value) {player.stop();bpm=Number(value);$('#tempo').value=bpm;render();}\n$('#tempo').oninput=e=>tempo(e.target.value);\n$$('[data-tempo]').forEach(b=>b.onclick=()=>tempo(b.dataset.tempo));\n$$('[data-pattern]').forEach(b=>b.onclick=()=>{player.stop();pattern=[...patterns[b.dataset.pattern]];render();});\n$('[data-listen]').onclick=()=>listen('edit',pattern,bpm,$('#reference').checked);\n$('#reference').onchange=()=>player.stop();render();\n","audio":true,"strings":{"beat":"第","current":"当前:","error":"声音暂时无法播放。请确认设备未静音,再点一次试听;也可以先看图解。","eyebrow":"02 / 自己改","half":"拍后半拍","heading":"移动一声,或改变整段的速度","hit":"敲一下","hits":" 次敲击 / 小节","idle":"每次播放两小节。可以随时停止或重新听。","intro":"先保持 100 BPM,点击格子增减敲击。再固定图案,只调速度。每次改动后,点试听从头比较。","legend":"● 表示敲一下,— 表示不敲;& 表示两拍之间的半拍位置。","listen":"试听我的节奏","playing":"正在播放两小节,播完会自动停止。","reference":"加入参考拍点","rest":"不敲","restore_a":"恢复图案 A","restore_b":"恢复图案 B","starting":"正在准备声音…","stop":"停止","tempo":"速度","volume":"音量"}}
两种操作产生的变化不同:移动格子改变了声音相对拍点的位置;调快速度则让同一套安排更快地走完。
还可以试一次“留空”:取消某一拍上的敲击,保留参考声。你仍然能继续数拍。没有敲击,不表示这一拍不存在。 如果关闭参考声,也可以在心里继续数;暂时跟不上就把它重新打开。
我总是跟着敲击走,数不稳怎么办?
先恢复 A,只听较高的参考声,慢慢数“1、2、3、4”。再换 B,尽量保持数拍的间隔。可以调到 80 BPM,或看图帮助定位。这里练的是注意哪一层声音,不是在测试反应速度。
有些音乐允许不同的拍感理解,听者也可能按一半或两倍的速度打拍。本实验事先约定了参考拍点;先在同一参照下比较,就能说清改变了什么。
换一组声音,你还能分清吗?
下面换了敲击图案。每组比较 C 和 D:是参考拍点变快了,还是敲击换了位置?可以多听几遍,先只注意参考声,再注意较低的敲击声。
正在呈现知识画面
换一组声音,检查你能否分清变化第一组的两段都为 100 BPM,敲击位置不同;第二组使用相同敲击位置,速度分别为 90 和 120 BPM。先听再选择,之后可展开文字中的参考答案。
{"id":"music-rhythm-check","title":"换一组声音,检查你能否分清变化","summary":"第一组的两段都为 100 BPM,敲击位置不同;第二组使用相同敲击位置,速度分别为 90 和 120 BPM。先听再选择,之后可展开文字中的参考答案。","height":520,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p>\n<div class=\"choices\"><button data-question=\"0\" aria-pressed=\"true\" data-i18n=\"q1\"></button><button data-question=\"1\" aria-pressed=\"false\" data-i18n=\"q2\"></button></div>\n<div class=\"actions\"><button class=\"audio-play\" data-listen=\"c\" aria-pressed=\"false\" data-i18n=\"c\"></button><button class=\"audio-play\" data-listen=\"d\" aria-pressed=\"false\" data-i18n=\"d\"></button><button id=\"stop\" disabled data-i18n=\"stop\"></button></div>\n<p class=\"intro\" data-i18n=\"prompt\"></p><div class=\"choices\"><button data-answer=\"rhythm\" data-i18n=\"rhythm\"></button><button data-answer=\"tempo\" data-i18n=\"tempo_answer\"></button><button data-answer=\"unsure\" data-i18n=\"unsure\"></button></div>\n<p id=\"feedback\" class=\"feedback\" role=\"status\" aria-live=\"polite\" data-i18n=\"initial\"></p><p id=\"status\" class=\"status\" role=\"status\" aria-live=\"polite\" data-i18n=\"idle\"></p><div class=\"volume\"><label for=\"volume\" data-i18n=\"volume\"></label><input id=\"volume\" type=\"range\" min=\"0\" max=\"100\" value=\"65\" data-i18n-label=\"volume\"></div>","css":"\nbody {padding:24px;} h2 {font-size:21px;} .intro {color:var(--muted);margin:8px 0 18px;}\nbutton,label,.hint {font-size:16px;} button {min-height:46px;} .actions {display:flex;gap:8px;flex-wrap:wrap;margin:16px 0;}\n.actions button {flex:1 1 90px;} .audio-play::before {content:'';display:inline-block;width:10px;height:11px;background:currentColor;clip-path:polygon(0 0,100% 50%,0 100%);margin-right:5px;} .audio-play[aria-pressed=true]::before {clip-path:none;}\n.readout {display:flex;gap:12px;justify-content:space-between;align-items:baseline;padding:12px 14px;border-bottom:1px solid var(--rule);}\n.readout strong {font-size:28px;font-weight:500;} .readout span {color:var(--muted);font-size:14px;}\n.grid {display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:6px;margin:14px 0;}\n.cell {min-width:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:7px 2px;border:1px solid var(--rule);border-radius:7px;min-height:66px;font-size:14px;}\n.cell b {font-size:21px;line-height:1;} .cell small {font-size:13px;color:var(--muted);} .cell.on {background:var(--accent-soft);border-color:var(--accent);color:var(--accent);}\n.cell.current {outline:2px solid var(--ink);outline-offset:1px;} .grid button.cell {font-size:14px;}\n.row-title {margin-top:18px;font-size:16px;font-weight:600;display:flex;justify-content:space-between;}\n.row-title span {font-size:14px;font-weight:400;color:var(--muted);} .status {min-height:3.4em;margin:14px 0 0;font-size:15px;line-height:1.7;color:var(--muted);}\n.options {display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin:14px 0;} .options label {display:flex;align-items:center;gap:8px;min-height:44px;}\ninput[type=checkbox] {width:19px;height:19px;accent-color:var(--accent);} .volume {display:flex;align-items:center;gap:12px;border-top:1px solid var(--rule);padding-top:10px;margin-top:14px;}\n.volume label {font-size:14px;color:var(--muted);white-space:nowrap;} .volume input {max-width:140px;} .tempo {margin:14px 0;} .tempo label {display:flex;justify-content:space-between;}\n.feedback {padding:14px;background:var(--surface);border-radius:8px;margin-top:12px;line-height:1.7;font-size:16px;} .choices {display:flex;gap:7px;flex-wrap:wrap;} .choices button {flex:1 1 90px;}\n@media(max-width:420px) {body {padding:20px 14px;} .grid {grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;} .cell {min-height:58px;} h2 {font-size:20px;} .readout strong {font-size:25px;}}\n","js":"\nconst $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];\nconst patterns={a:[0,2,4,6],b:[0,3,4,7]};\nlet playingKey='', bpm=100;\nconst beatLabels=['1','&','2','&','3','&','4','&'];\nfunction grid(el, pattern, editable=false) {\n el.replaceChildren();\n for(let i=0;i<8;i++) {\n const cell=document.createElement(editable?'button':'div'); cell.className='cell'+(pattern.includes(i)?' on':''); cell.dataset.slot=i;\n if(editable) {cell.type='button';cell.setAttribute('aria-pressed',String(pattern.includes(i)));}\n cell.setAttribute('aria-label',viz.t('beat')+' '+(Math.floor(i/2)+1)+(i%2?viz.t('half'):'')+' · '+viz.t(pattern.includes(i)?'hit':'rest'));\n const label=document.createElement('small');label.textContent=beatLabels[i];\n const mark=document.createElement('b');mark.textContent=pattern.includes(i)?'●':'—';mark.setAttribute('aria-hidden','true');cell.append(label,mark);el.append(cell);\n }\n}\nfunction makeScore(pattern, tempo, reference=false) {\n const events=[];\n for(let bar=0;bar<2;bar++) {\n for(const step of pattern) events.push({beat:bar*4+step/2,hz:740,duration:.075,gain:1,wave:'triangle'});\n if(reference) for(let beat=0;beat<4;beat++) events.push({beat:bar*4+beat,hz:1500,duration:.035,gain:.45});\n }\n return {bpm:tempo,beats:8,events};\n}\nconst player=viz.audio({\n onState(state) {\n document.body.dataset.audioState=state;\n $$('[data-listen]').forEach(b=>b.setAttribute('aria-pressed',String(state!=='idle'&&b.dataset.listen===playingKey)));\n $('#stop').disabled=state==='idle';\n $('#status').textContent=viz.t(state==='playing'?'playing':state==='starting'?'starting':'idle');\n },\n onTick(beat) {\n const step=beat<0?-1:Math.floor(beat*2)%8;\n $$('.cell').forEach(c=>c.classList.toggle('current',beat>=0&&Number(c.dataset.slot)===step&&(!c.closest('[data-track]')||c.closest('[data-track]').dataset.track===playingKey)));\n },\n onError() {$('#status').textContent=viz.t('error');}\n});\n$('#stop').onclick=()=>player.stop();\n$('#volume').oninput=e=>player.setVolume(Number(e.target.value)/100);\nfunction listen(key,pattern,tempo,reference=false) {playingKey=key;return player.play(makeScore(pattern,tempo,reference));}\n\nconst questions=[{c:[0,1,4,6],d:[0,2,5,6],tempos:[100,100],answer:'rhythm'},{c:[0,2,3,6],d:[0,2,3,6],tempos:[90,120],answer:'tempo'}];\nlet question=0;\n$$('[data-question]').forEach(b=>b.onclick=()=>{player.stop();question=Number(b.dataset.question);$$('[data-question]').forEach(x=>x.setAttribute('aria-pressed',String(x===b)));$('#feedback').textContent=viz.t('initial');});\n$$('[data-listen]').forEach(b=>b.onclick=()=>{const q=questions[question],key=b.dataset.listen;listen(key,q[key],q.tempos[key==='c'?0:1],true);});\n$$('[data-answer]').forEach(b=>b.onclick=()=>{const prefix=b.dataset.answer==='unsure'?'unsure_feedback':b.dataset.answer===questions[question].answer?'correct':'retry';$('#feedback').textContent=viz.t(prefix)+' '+viz.t('explain_'+question);});\n","audio":true,"strings":{"beat":"第","c":"试听 C","correct":"判断一致。","d":"试听 D","error":"声音暂时无法播放。请确认设备未静音,再点一次试听;也可以先看图解。","explain_0":"第一组都是 100 BPM;每小节敲击四次,但其中两次换了位置。","explain_1":"第二组的图案相同;D 从 90 BPM 加快到 120 BPM,参考拍点与敲击一起变快。","eyebrow":"03 / 换个例子","half":"拍后半拍","heading":"这次改变的是哪一项?","hit":"敲一下","idle":"每次播放两小节。可以随时停止或重新听。","initial":"可以反复比较,选好后会显示解释。","intro":"每组都加入了较轻、较高的参考拍点。听它是否变快,再留意较低的敲击落在哪里。","legend":"● 表示敲一下,— 表示不敲;& 表示两拍之间的半拍位置。","playing":"正在播放两小节,播完会自动停止。","prompt":"从 C 到 D,主要改变了什么?","q1":"第一组","q2":"第二组","reference":"加入参考拍点","rest":"不敲","retry":"再留意一下参考拍点。","rhythm":"节奏落点","starting":"正在准备声音…","stop":"停止","tempo_answer":"速度","unsure":"还不确定","unsure_feedback":"可以先读解释,再听一遍。","volume":"音量"}}
查看两组的参考答案
第一组:C 和 D 都是 100 BPM,每小节各敲四次。C 的落点为“1、1 后的半拍、3、4”;D 为“1、2、3 后的半拍、4”。速度和次数相同,位置发生变化。
第二组:两段都在“1、2、2 后的半拍、4”敲击。C 为 90 BPM,D 为 120 BPM。图案保持不变,D 的拍点和敲击一起加快,两小节也更早结束。
如果暂时不方便播放,可以对照这些落点在纸上画八个格子,检查变的是位置还是 BPM;听辨部分留到方便听音时完成。
下次听歌,试着多说一句
找一首你熟悉、拍点比较清楚的歌,先跟着它轻轻数拍,再注意鼓或人声:每次发声都正好落在拍点上吗?哪里留了空隙,哪里落在两拍之间?不必急着给整首歌算 BPM。
当你想说“这一段更有动感”时,可以再补一句具体观察:“拍点还很稳定,但敲击的位置变了”,或者“同样的节奏加快了”。这一步,就是把模糊的感受变成可以比较和讨论的音乐关系。
下一篇会把注意力从“什么时候响”,转向“响的是哪个音”:一段旋律整体升高后,为什么我们还认得出来?
参考与延伸
本篇声音图案为教学自编;试听中的敲击和参考拍点是合成示意音。