开过光的序
当一个民谣小哥抱着吉他哼唱着《情非得已》时,他右手扫着音孔处的琴弦,左手变换着按着琴颈处的琴弦,一段简单的弹唱便看起来有模有样。在不看脸不看唱功的情况下,是什么原理才赋予这位小哥如此风骚的魅力呢?
这就是吉他伴奏。
而他只是一个吉他初学者,还没办法给歌曲编配伴奏,只好从网上找来吉他谱,按照里面的标识来进行弹奏。他找到了下面这样的谱子:
这是一个典型的吉他弹唱谱该有的样子,它可以被分成四个部分:
和弦指法:用于标记该小节内的和弦名以及对应的按弦指法。
六线谱:它是专门属于吉他的谱子,六条横线至上而下分别对应吉他的一弦到六弦,横线上添加各种符号来标记右手的弹奏方式。
简谱:这里是数字简谱,配以各种符号来描述歌曲的旋律与节奏。
歌词:嗯,就是歌词。
对于初学者,吉他入门的坎儿在于左手的指法,当时我记下了大多数和弦的指法图,左手指尖的磨出的茧也是起了褪,褪了起,身为乐理渣的我终有一天疑惑了,问号三连:
1. 这个和弦为什么叫这个名字?
2. 这个和弦为什么是这个指法?
3. 同一和弦在吉他上到底有多少种不同的指法?
本文将基于基本的乐理知识,用代码推导计算出以上问题的答案,并将其结果可视化。
一、从一个单音说起
心虚的声明:外行人基于自己的理解强行解释乐理,望专业人士轻喷
声音因物体振动而产生,每一个不同频率(即不同音高)的声响都可以称之为一个单音,但人耳的辨音能力有限,故将人耳能清晰分辨的最小的音高间隔称为半音;
相隔半音的两个音的频率比值为2的12次方根。
为什么是这个值,这就得提到十二平均律。
音乐界老前辈经过大量的听力实践后,发现例如do到高音do这个音程作为一个循环听起来最和谐,并且这高音do与do的频率比率刚好是2,在保证单音之间跨度和谐、而且能较清晰地辨听的情况下,将这个音程按频率比划分成了12等份,这与中国的五声音阶(宫商角徵羽)和西洋的七声音阶存在相互映照的关系,如下图(这里我暂时用数字标记十二平均律音程上的每个音):
也就是说一个音与它对应高八度的音之间的跨度便是一个音程,它们的频率比为1:2。
1(do)与2(re)之间是一个全音的跨度,而3(mi)与4(fa)、7(si)与1.(高音do)之间是一个半音的跨度,一个全音跨度就相当于两个半音跨度,可以看出1(do)与2(re)之间还夹了一个音,我们称它为#1(升do)或者说b2(降re)。
理解了这些后,便可以用代码实现一个单音类:
1. 首先来确定一种单音的书写形式
可以借用简谱的标记方式,数字1、2、3、4、5、6、7,分别代表唱名的do、re、mi、fa、sol、la、si;
当这个音升半调时,在数字的前面加上#,例如#1(升do),降半调时,在数字前面加上b,例如b1(降do);
当标记一个音的高八度音时,在数字的右侧加一个“点号”,例如1.(高音do),#2.(高音升re)(因为字符串没法像简谱那样在数字顶部加点号),当标记一个音的低八度音时,在数字的左侧加一个“点号”,例如.1(低音do),.b2(低音降re);
2. 构建单音类
// 检测数据类型的公用方法
functionis(data){
returnfunction(type){
returnObject.prototype.toString.call(data)===`[object${type}]`;
}
}
// 单音类,用于音的映射查询与音高的改变,同时可标记记录其在吉他上的位置
classTone{
constructor(toneString='1'stringfret){
// 所有唱名数组
this.syllableMap=['do''re''mi''fa''sol''la''si'];
// 音程
this.keyMap=['1'['#1''b2'],'2'['#2''b3'],'3''4'['#4''b5'],'5'['#5''b6'],'6'['#6''b7'],'7'];
//所有调名
this.intervalMap=['C'['#C''bD'],'D'['#D''bE'],'E''F'['#F''bG'],'G'['#G''bA'],'A'['#A''bB'],'B'];
// 单音的字符串表示
this.toneString=toneString;
// 单音的字符串表示(去除八度标记)
this.toneNormal=toneString.replace(/\./g'');
// 数字音
this.key=toneString.replace(/\.|b|#/g'');
// 唱名
this.syllableName=this.syllableMap[+this.key-1];
// 降半调标记
this.flat=toneString.match('b')?'b':'';
// 升半调标记
this.sharp=toneString.match('#')?'#':'';
letoctave_arr=toneString.split(this.key);
letoctave_flat=octave_arr[0].toString.match(/\./g);
letoctave_sharp=octave_arr[1].toString.match(/\./g);
// 八度度数
this.octave=(octave_sharp?octave_sharp.length:0)-(octave_flat?octave_flat.length:0);
// 吉他按弦位置
this.position={
// 第几弦
string:string
// 第几品格
fret:fret
};
}
// 获取某个音在音程上的位置
findKeyIndex(keyString){
returnthis.keyMap.findIndex((item)={
if(is(item)('Array')){
returnitem.includes(keyString);
}elseif(item===keyString){
returntrue;
}else{
returnfalse;
}
});
}
// 音高增减,num为增或减的半音数量
step(num){
letkeyString=this.flat+this.sharp+this.key;
letlen=this.keyMap.length;
letindex=this.findKeyIndex(keyString);
if(index-1){
num=+num;
// 计算改变音高后的音在音程上的位置
letnextIndex=parseInt(index+num0);
letoctave=this.octave;
if(nextIndex=len){
letindex_gap=nextIndex-len;
octave+=Math.floor(index_gap/len)+1;
nextIndex=index_gap%len;
}elseif(nextIndex0){
letindex_gap=nextIndex;
octave+=Math.floor(index_gap/len);
nextIndex=index_gap%len+len;
}
letnextKey=this.keyMap[nextIndex];
// 计算并添加高低八度的记号
letoctaveString=newArray(Math.abs(octave)).fill('.').join('');
lettoneString='';
if(!is(nextKey)('Array')){
toneString=(octave0?octaveString:'')+nextKey+(octave0?octaveString:'');
returnnewthis.constructor(toneStringthis.position.stringthis.position.fret+num);
}else{
// 可能得到两个音高一样但标记方式不一样的音
returnnextKey.map((key)={
returnnewthis.constructor((octave0?octaveString:'')+key+(octave0?octaveString:''),this.position.stringthis.position.fret+num);
});
}
}else{
returnnull;
}
}
}
有了这个单音类后,后续可以借用它来方便地对比两个音之间的跨度,并且可以通过构建吉他每根弦的初始音,通过step方法推导出吉他其他任意位置的音高。
执行示例:
创建一个1(do)的单音实例
1. 什么是和弦
先上个百度词条:
(1)由三个或三个以上的音构成;
(2)音之间有跨度关系(三度或非三度);
(3)音之间要从低到高排列。
由此我画了一张图:
一个音程上的12个音可以像时钟的刻度那样排列,顺时针方向代表音的从低到高;然后我们将“时针”、“分针”、“秒针”在不重叠且相互有一定间隔的情况下随意拨弄,把他们指向的音顺时针连起来,就可能构成了一个三个音组成的和弦(同理更多音组成的和弦就相当于再往里加指针)。
这样一看,便能发现这更像是一个排列组合问题,拿三个音的组合来说,从12个音里面任意挑3个音(不排序),会有220种情况,但这里面并不都是和弦;和弦和弦,顾名思义,听起来得和谐得不难听,这开始更像是人们的主观意识判断,但随着音乐知识体系的成熟,和弦也会有一套公认的标准,变得向数学公式那样有迹可循。
细想一下,一个和弦好不好听,带什么感情色彩,取决于组成音的相互映衬关系,也就是音之间的相互音高间隔,隔得太近会别扭,隔得太远也别扭,那就得取个适中的,这个适中就是三度;
三度又分为大三度与小三度
大三度:两个全音的跨度,即4个半音的跨度。
小三度:一个全音加一个半音的跨度,即3个半音的跨度。
C调下的C和弦组成音如下:
对照上图那个刻度盘可数出来:
1(do)与3(mi)中间还夹了#1/b2、2、#2/b3这3个音,共4个半音的跨度;
3(mi)与5(sol)中间还夹了4、#4/b5这2个音,共3个半音的跨度;
那么像这样组成的和弦就成为大三和弦。
2. 常见和弦标记规则
和弦类型 | 组成 | 标记 |
---|
大三和弦 | 大三度 + 小三度 |
|
小三和弦 | 小三度 + 大三度 | m |
增三和弦 | 大三度 + 大三度 | aug |
减三和弦 | 小三度 + 小三度 | dim |
大小七和弦(属七和弦) | 大三和弦+ 小三度 | 7或Mm7 |
大大七和弦(大七和弦) | 大三和弦+ 大三度 | maj7或M7 |
小小七和弦(小七和弦) | 小三和弦+ 小三度 | m7或mm7 |
小大七和弦 | 小三和弦+ 大三度 | mM7 |
减七和弦 | 减三和弦+ 小三度 | dim7 |
半减七和弦 | 减三和弦+ 大三度 | m7-5 |
增属七和弦 | 增三和弦+ 减三度 | 7#5或M7+5 |
增大七和弦 | 增三和弦+ 小三度 | aug7或Maj7#5 |
加音和弦与指定和弦根音相对复杂些,暂不讨论。
3. 和弦根音
和弦组成音中的第一个音为和弦的根音,也叫基础音,可以根据当前的调式和某和弦的根音来判断该和弦的初始名称,例如在C调下,根音与和弦名的对照关系如下:
通俗点说相当于,在某调下,一个和弦的根音为该调的1(do)时,那它就叫某和弦(额外标记根据音之间的三度关系再添加),例如:
C调下:
根音为1(do)构成的和弦名为C;
根音为2(re)构成的和弦名为D;
D调下:
根音为1(do)构成的和弦名为D;
根音为1(do)构成的和弦名为E;
B调下:
根音为1(do)构成的和弦名为B;
根音为2(do)构成的和弦名为C;
4. 和弦完整名称计算
基于以上的乐理规则,可以实现如下推导和弦名的类:
//和弦名称推导classChordName{constructor(chordTone){//实例化一个单音类做工具,用来计算音与各种标记的映射关系this.toneUtil=newTone;}//获取两个音的间隔跨度getToneSpace(tonePre,toneNext){lettoneSpace=this.toneUtil.findKeyIndex(toneNext)-this.toneUtil.findKeyIndex(tonePre);returntoneSpace=toneSpace0?toneSpace+12:toneSpace;}//大三度isMajorThird(tonePre,toneNext){returnthis.getToneSpace(tonePre,toneNext)===4;}//小三度isMinorThird(tonePre,toneNext){returnthis.getToneSpace(tonePre,toneNext)===3;}//增三度isMajorMajorThird(tonePre,toneNext){returnthis.getToneSpace(tonePre,toneNext)===5;}//减三度isMinorMinorThird(tonePre,toneNext){returnthis.getToneSpace(tonePre,toneNext)===2;}//大三和弦isMajorChord(chordTone){returnthis.isMajorThird(chordTone[0],chordTone[1])this.isMinorThird(chordTone[1],chordTone[2]);}//小三和弦misMinorChord(chordTone){returnthis.isMinorThird(chordTone[0],chordTone[1])this.isMajorThird(chordTone[1],chordTone[2]);}//增三和弦augisAugmentedChord(chordTone){returnthis.isMajorThird(chordTone[0],chordTone[1])this.isMajorThird(chordTone[1],chordTone[2]);}//减三和弦dimisDiminishedChord(chordTone){returnthis.isMinorThird(chordTone[0],chordTone[1])this.isMinorThird(chordTone[1],chordTone[2]);}//挂四和弦isSus4(chordTone){returnthis.isMajorMajorThird(chordTone[0],chordTone[1])this.isMinorMinorThird(chordTone[1],chordTone[2]);}//大小七和弦/属七和弦7/Mm7isMajorMinorSeventhChord(chordTone){if(chordTone.length4)returnfalse;returnthis.isMajorChord(chordTone)this.isMinorThird(chordTone[2],chordTone[3]);}//小大七和弦mM7isMinorMajorSeventhChord(chordTone){if(chordTone.length4)returnfalse;returnthis.isMinorChord(chordTone)this.isMajorThird(chordTone[2],chordTone[3]);}//大七和弦maj7/M7isMajorMajorSeventhChord(chordTone){if(chordTone.length4)returnfalse;returnthis.isMajorChord(chordTone)this.isMajorThird(chordTone[2],chordTone[3]);}//小七和弦m7/mm7isMinorMinorSeventhChord(chordTone){if(chordTone.length4)returnfalse;returnthis.isMinorChord(chordTone)this.isMinorThird(chordTone[2],chordTone[3]);}//减七和弦dim7isDiminishedSeventhChord(chordTone){if(chordTone.length4)returnfalse;returnthis.isDiminishedChord(chordTone)this.isMinorThird(chordTone[2],chordTone[3]);}//半减七和弦m7-5isHalfDiminishedSeventhChord(chordTone){if(chordTone.length4)returnfalse;returnthis.isDiminishedChord(chordTone)this.isMajorThird(chordTone[2],chordTone[3]);}//增属七和弦7#5/M7+5isHalfAugmentedSeventhChord(chordTone){if(chordTone.length4)returnfalse;returnthis.isAugmentedChord(chordTone)this.isMinorMinorThird(chordTone[2],chordTone[3]);}//增大七和弦aug7/Maj7#5isAugmentedSeventhChord(chordTone){if(chordTone.length4)returnfalse;returnthis.isAugmentedChord(chordTone)this.isMinorThird(chordTone[2],chordTone[3]);}//获取音对应的根音和弦名getKeyName(key){letkeyName=this.toneUtil.intervalMap[this.toneUtil.findKeyIndex(key)];if(is(keyName)('Array')){keyName=/b/.test(key)?keyName[1]:keyName[0];};returnkeyName;}//计算和弦名getChordName(chordTone){letrootKey=chordTone[0];//和弦的字母名letchordRootName=this.getKeyName(rootKey);//和弦字母后面的具体修饰名letsuffix='...';letsuffixArr=;//三音和弦的遍历方法及对应修饰名letchord3SuffixMap=[{fn:this.isMajorChord,suffix:''},{fn:this.isMinorChord,suffix:'m'},{fn:this.isAugmentedChord,suffix:'aug'},{fn:this.isDiminishedChord,suffix:'dim'},{fn:this.isSus4,suffix:'sus4'}];//四音和弦的遍历方法及对应修饰名letchord4SuffixMap=[{fn:this.isMajorMinorSeventhChord,suffix:'7'},{fn:this.isMinorMajorSeventhChord,suffix:'mM7'},{fn:this.isMajorMajorSeventhChord,suffix:'maj7'},{fn:this.isMinorMinorSeventhChord,suffix:'m7'},{fn:this.isDiminishedSeventhChord,suffix:'dim7'},{fn:this.isHalfDiminishedSeventhChord,suffix:'m7-5'},{fn:this.isHalfAugmentedSeventhChord,suffix:'7#5'},{fn:this.isAugmentedSeventhChord,suffix:'aug7'}];//三音和弦if(chordTone.length===3){suffixArr=chord3SuffixMap.filter((item)={returnitem.fn.bind(this,chordTone);});suffix=suffixArr.length0?suffixArr[0].suffix:suffix;}else{//四音和弦suffixArr=chord4SuffixMap.filter((item)={returnitem.fn.bind(this,chordTone);});suffix=suffixArr.length0?suffixArr[0].suffix:suffix;}//拼接起来得到完整的和弦名returnchordRootName+suffix;}}
运行示例:
1. 指法图
一个完整的吉他和弦指法图的例子如下,右边对照为真实的吉他:
2. 吉他弦上音的分布
我从网上抠来了这张带着历史气息的彩图:
比如你左手能用上的只有不超过5根手指头而弦有6根,但食指是可以使用大横按按多根弦的,但大横按只能按在该指法的最低品位上;还得考虑指法按弦后是包括了和弦里所有的音,同时相邻两弦的音不能一样...
诸如此类,想要一下子心算出来所有可能的结果,怕是为难我胖虎了。
不过这个很适合用递归算法解决。
3. 指法推导
为此专门构建一个类,在初始化的时候使用之前写的单音类,算出吉他弦上所有位置的音。之后就可以通过this.toneMap[tring][fret]的形式直接获得该位置的音,例如this.toneMap[1][3]获取1弦3品的音。
//吉他和弦指法推导类classGuitarChord{constructor{//暂定的吉他的最大品格数this.fretLength=15;//构建1到6弦的初始音this.initialTone=[newTone('3.',1,0),newTone('7',2,0),newTone('5',3,0),newTone('2',4,0),newTone('.6',5,0),newTone('.3',6,0)];//用于吉他上所有位置对应的音this.toneMap=;//从1到6弦,从品位数的低到高,依次计算每个位置的音for(letstring=1;string=this.initialTone.length;string++){this.toneMap[string]=;for(letfret=0;fret=this.fretLength;fret++){this.toneMap[string].push(this.initialTone[string-1].step(fret));}}}}
给它加上一个公用的单音位置搜寻方法:
//在指定的品格数范围内,查找某个音在某根弦的音域下所有的品格位置/**@paramkey搜寻的音(字符串形式)*@paramtoneArray音域数组,即某根弦上所有单音类按顺序组成的数组*@paramfretStart搜寻的最低品格数*@paramfretEnd搜寻的最高品格数*/findFret(key,toneArray,fretStart,fretEnd){key=key.replace(/\./g,'');letfretArray=;fretStart=fretStart?fretStart:0;fretEnd=fretEnd?(fretEnd+1):toneArray.length;for(leti=fretStart;ifretEnd;i++){if(is(toneArray[i])('Array')){lettoneStringArray=toneArray[i].map((item)={returnitem.toneNormal;});if(toneStringArray.includes(key)){fretArray.push(i);}}else{if(toneArray[i].toneString.replace(/\./g,'')===key){fretArray.push(i);}}}returnfretArray;}
接下来是核心的循环递归算法,先构思下大致的递归的流程:
(1)指定从1弦开始,启动递归。(递归入口)
(2)指定了某弦后,循环遍历和弦的组成音,计算是否有音落在该弦指定的品位范围内,如果没有,返回false;如果有,转步骤(3)。
(3)先保存该音与它的按弦位置,当前位置最终有效取决于,当且仅当在它后面的所有弦也是能找到按弦位置的有效解,如果该弦是第6弦,返回true,递归结束(递归出口),否则转步骤(4);
(4)当前结果最终的有效性=当前临时结果有效性(true)下一根弦是否存在有效解(此时已转至步骤(3))。若当前结果最终有效,返回true;若无效,回退pop出之前在该弦保存的结果。
最后实现还需考虑相邻两弦音不能相同,另外为了便于回溯整体结果,在单次的结果保存时,添加了指向上一次结果的指针pre。
//递归遍历范围内的指定和弦的所有位置组合/**@paramstringIndex当前遍历到的弦的序号*@paramtoneIndex上一根弦使用的音的序号(用于相邻的两根弦的音不重复)*@paramfretStart遍历的最低品格数*@paramfretEnd遍历的最高品格数*@parampreResult上一根弦确定的音的结果*@parampositionSave保存该轮递归的结果*/calc(stringIndex,toneIndex,fretStart,fretEnd,preResult,positionSave){lettoneArray=this.toneMap[stringIndex];letresult=false;//从和弦音的数组里逐个选出音进行试探(this.chordTone在后面提到的函数中赋值)for(leti=0;ithis.chordTone.length;i++){//相邻的上一根弦已使用的音不做本次计算if(i!==toneIndex){letresultNext=false;lettoneKey=this.chordTone[i];//在品格范围内查找当前音的位置letfret=this.findFret(toneKey,toneArray,fretStart,fretEnd);//品格范围内存在该音if(fret.length0){//记录该音的位置,几弦几品与音的数字描述letresultNow={string:stringIndex,fret:fret[0],key:toneKey}//在本次记录上保存上一根弦的结果,方便回溯resultNow.pre=preResult?preResult:null;//保存本次结果positionSave.push(resultNow);//设置该弦上的结果标记resultNext=true;//没有遍历完所有6根弦,则继续往下一根弦计算,附带上本次的结果记录if(stringIndexthis.initialTone.length){letnextStringIndex=stringIndex+1;//该弦上的结果的有效标记,取决上它后面的弦的结果均有效resultNext=resultNextthis.calc(nextStringIndex,i,fretStart,fretEnd,resultNow,positionSave);}else{//所有弦均遍历成功,代表递归结果有效resultNext=true;}//在该弦的计算结果无效,吐出之前保存的该弦结果if(!resultNext){positionSave.pop;}}else{//品格范围内不存在该音resultNext=false;}//任意一个和弦里的音,能在该弦取得有效结果,则该弦上的结果有效result=result||resultNext;}};returnresult;}
使用此递归方法,用1、3、5为和弦组成音做输入,会得到类似下面这样的结果:
递归在执行的时候,在每个节点上可能产生多个分支节点层层往下深入,以上的打印其实就是列出了每个节点的数据。而我们需要的是将这个递归结果拆分为不同指法结果的数组,就像下面这样:
为此添加一个filter函数:
//和弦指法过滤器filter(positionSave){//从6弦开始回溯记录的和弦指法结果,拆解出所有指法组合letallResult=positionSave.filter((item)={returnitem.string===this.initialTone.length}).map((item)={letresultItem=[{string:item.string,fret:item.fret,key:item.key}];while(item.pre){item=item.pre;resultItem.unshift({string:item.string,fret:item.fret,key:item.key});}returnresultItem;});if(allResult.length0){//依次调用各个过滤器returnthis.integrityFilter(this.fingerFilter(this.rootToneFilter(allResult)));}else{return;}}
可以看到回溯计算出理想的结果形式后,末尾还调用了多个过滤器,因为代码计算出的符合组成音的所有指法组合,可能并不符合真实的按弦情况,需要进行多重的过滤。
4. 指法过滤
例如以1、3、5作为和弦音,根音为1,而初步得到的结果可能如下:
而一个和弦在吉他上弹奏时,根音应该为所有发声的音中最低的音,上图中最低的音要么位于是6弦0品的3,要么是位于6弦3品的5,不符合要求,而5弦3品刚好是该和弦根音,故应该禁用第6弦(这里的禁用是将该弦的按弦品位fret标记为null)
//根音条件过滤rootToneFilter(preResult){letnextResult=newSet;preResult.forEach((item)={//允许发声的弦的总数,初始为6letrealStringLength=6;//从低音弦到高音弦遍历,不符合根音条件则禁止其发声for(vari=item.length-1;i=0;i--){if(item[i].key!==this.rootTone){item[i].fret=null;item[i].key=null;realStringLength--;}else{break;}}if(realStringLength=4){//去重复nextResult.add(JSON.stringify(item));}});//去重后的Set解析成对应数组返回return[...nextResult].map(item=JSON.parse(item));}
左手按弦的时候,一般最多只能用上4个手指(大拇指极少用到),而用递归方法算出的结果,可能包含了各种奇奇怪怪的按法,比如下面这个:
//按弦手指数量过滤fingerFilter(preResult){returnpreResult.filter((chordItem)={//按弦的最小品位letminFret=Math.min.apply(null,chordItem.map(item=item.fret).filter(fret=(fret!=null)));//记录需要的手指数量letfingerNum=minFret0?1:0;chordItem.forEach((item)={if(item.fret!=nullitem.fretminFret){fingerNum++;}});returnfingerNum=4;});}
递归计算所有可能的指法组合时,虽然保证了相邻两个音不重复,但不保证所有的和弦组成音都被使用了,而且在前一轮根音过滤时,可能禁用了部分弦的发声,这可能导致丢掉了其中唯一一个组成音,所以最后还需进行一轮完整性过滤,剔除残次品:
//和弦组成音完整性过滤integrityFilter(preResult){returnpreResult.filter((chordItem)={letkeyCount=[...newSet(chordItem.map(item=item.key).filter(key=key!=null))].length;returnkeyCount===this.chordTone.length;});}
5. 指法计算入口
由这里输入和弦的组成音,计算这些音所有可能出现的品格位置,然后从低到高,依次计算4或5个品格范围内的和弦指法,经整合过滤后得到该和弦所有的位置的正确指法。
//和弦指法计算入口chord{letchordTone;if(is(arguments[0])('Array')){chordTone=arguments[0];}else{chordTone=Array.prototype.slice.apply(arguments).map((item)={lettone=newTone(item.toString);returntone.flat+tone.sharp+tone.key;});}//和弦组成音this.chordTone=chordTone;//根音this.rootTone=chordTone[0];this.chordResult=;letfretArray=;//查找和弦里的音可能存在的品格位置,保存至fretArraychordTone.forEach((item)={for(leti=1;ithis.toneMap.length;i++){fretArray=fretArray.concat(this.findFret(item,this.toneMap[i]));}});fretArray=[...newSet(fretArray)];//品格位置从小到大排序fretArray.sort((a,b)={returna-b;});//从低把位到高把位,计算范围内的所有该和弦指法for(leti=0;ifretArray.length;i++){letfretStart=fretArray[i];//在不需要使用大横按时,即在最低的把位计算时,可把计算的品格范围扩大一格letfretEnd=fretStart0?(fretStart+4):(fretStart+5);//最高范围不能超过吉他的最高品格数if(fretEndJSON.stringify(item)))].map(item=JSON.parse(item));returnresult;}
运行示例:
特意挑选了svg作图,因为之前不会,借此机会学习了一下。
一个较为完整的和弦指法图,svg的代码示例如下(把这个扔到自己的html里打开也能直观看到结果):
CCEGCE1
显示效果如下:
简单来说,就是将指法图拆分为多个子元素,有的画网格,有的画按弦位置,有的画空弦符号,诸如此类,然后根据传入的指法结果,动态创建这些子元素加入svg即可;但需特别考虑各个元素可能会动态改变的位置,以及对于大横按的绘图处理。
此处代码及svg公共样式详见原文。
哐当当一个运行示例:
基于以上已经实现的代码,我又折腾出了一个网页工具,在数字上左右拖动来改变和弦的组成音,从而时时计算和弦指法图:
如果你不按套路出牌,给了间隔古怪的组成音,可能会这样(因为算不出完整的和弦名字了,就用省略号代替了):
上过香的尾
一边搜着基础乐理,一边填补着漫无边际的知识空白,可算是把这个东西弄出来了,涉及的还只是音乐基础的冰山一角,比如还有许多更高级的更多音组成的和弦、以及更加稀奇古怪的和弦名字,能力有限,这里就先不纳入考虑范畴了。
不得不说,我明明是来写代码的,却不知不觉给自己上起了音乐小课。
有些做事的动力就是这么奇妙。
若看官还觉得饶有意思,便胜却人间无数。