絶対音感と譜読みの早さ: (1)特殊な認知について
私は合唱を長い間やっていて、譜読み(楽譜の読解)が他の人と比べて高速にできるということがわかっています。特に初見の楽譜を見て歌うのが早く、音取り(ピアノやキーボードなどで音を出してどのような感じの旋律か理解すること)なしで大体の曲を歌うことができます。*1
そのようになる原因を探っていたところ、自分の音楽や楽譜の認識のやり方が他の大多数の人と大きく違っているはずであることが主要因であるだろう、ということがわかってきていました。
そこで、以下のためにこの文章を書きます。
(i) 他の人の音楽の聴き方、楽譜の読み方などを知りたいと思う方のために、自分のやり方を書いておく。他の人のやり方も知りたいのでコメントなどいただけるとありがたい。また当事者からは当たり前だが当事者以外から誤解されがちな点を正す。
(ii) 自分の音楽の聴き方、楽譜の読み方を、少しでも役に立たせるために、訓練でどうにかなりそうなところについては訓練のしかたを書いておく。非常に稀になぜそんなに譜読みが早いのか聞かれることがあるが、そのたびに答えを用意しておらずしどろもどろになるのに飽きたため。
この記事では(i)について書き、(ii)については別の記事に分けたいと思います。
執筆者について
執筆者は子供の頃ピアノを練習しており、10年程度やっていました。また合唱は15歳くらいのころから10年以上続けています。
絶対音感について
突然何らかの楽器の音が鳴ったとき、その音の高さを当てられる能力のことを絶対音感、あるいは絶対的音感・絶対的音高感などと呼びます。
ここでは一般的な絶対音感について詳しく述べることはしませんが、例えば相対音感と絶対音感の違いとは? | 椿音楽教室などは自分の考えと合っているので、読んでみると良いでしょう。
私の絶対音感について
少なくとも半音単位では音の高さが分かるタイプの絶対音感を持っています。相対音感はほとんどないように思います(相対音感の未熟さを絶対音感でカバーしている)。
得意
音楽を聴いてメロディーがどの音か、コードはどうなっているかを把握するのが得意です。
苦手
(1) 楽譜に書かれている音から移調して歌うのが非常に苦手です。楽譜通りに歌うのであれば「楽譜を読む」->「歌う」の2ステップで済みますが、例えば楽譜から半音下げて歌う場合には「楽譜を読む」->「読んだ範囲の音を半音下げる計算をする」->「歌う」の3ステップかかり、なおかつ計算がまあまあ遅いからです。*2
(2) 絶対音感を持っていない人が音楽を認識する方法を想像することが難しいです。以下のようなことが観測事実から予想できていますが、実際に共感なり体感なりができないです。間違ってたら教えてください
- ある程度喉の感覚で次の音を覚える。(私も喉の感覚がどうこうというのは多少はありますが、最終的には自分の耳を使って調整します。)*3
- 複数曲歌うとき、前の曲の終わりから次の曲の入り(開始時)の音を取るのが難しい。(私は前の曲は関係なく直接音取りしています。)
- 途中で音の高さが基準音(ピアノの鍵盤を叩いた時に鳴る音)からじわじわ下がって行っても気付かない。(私は30〜50セントくらい下がったら気付きます。)
- 楽譜から移調して歌うときに苦労しない。(私は苦労します。)
- 歌うとき、横の流れ(前の音から何度上下するか)を気にする。(私の場合、同時に鳴っている他のパートの音との比較はしますが、さっき発した音との比較はしません。)*4
(3) 合唱で響きが良さそうか、楽譜通りかをなんとなく判断するのが苦手です。練習中、どのパートの音がズレているかを調べる時も、他のパートとの比較ではなく基準音との比較でやっている気がします。
(4) 複数音を同時に認識するのが苦手です。
自分の音の認知について
ピアノをやっていたので、耳で聞く音とピアノの鍵盤の上の位置が密接にリンクしています。例えば「ファ」の音が鳴ったとき、脳内で鍵盤上の「ファ」の位置が発光するというイメージです。
音楽を聴く時
メロディーについては、歌くらいの速度であれば鍵盤で正確にどの位置にあるのかが自然に想起されます。(オクターブ単位の誤差は頻繁に発生します。)
コードは雑にどのあたりの音が使われているか分かる程度です。特に複雑な場合、わからないこと、間違って推測することも多々あります。
例えば菅田将暉 『虹』 - YouTubeの歌い出し、「泣いていいんだよ」は以下のように認識します。上から下に、このようにイメージが変化していくものとして見てください。(この例はあくまで例示のためであり、正しさは保証しません。)
メロディー | コード | 歌詞 |
---|---|---|
F3 | (なし) | な |
G3 | (なし) | い |
A3 | F | て |
A3 | (F続き) | い |
G3 | (F続き) | いん |
F3 | (F続き) | だ |
G3 | (F続き) | よ |
(なし) | C | (なし) |
重要なのは、これだけでは一度聴いた曲をいきなりピアノで弾けるようにはならないということです。それをするためには少なくとも以下のものが必要です。
(1) 記憶力、特に短期記憶。私は短期記憶力が悪く、この手のタスクが極めて苦手です。(少なくとも数回〜数十回くらいは聴いて覚えないと話になりません。)
(2) 同時に複数音を理解する能力。メロディラインが複数あるときに同時に聴くのも、メロディと伴奏を同時に聴くのもできません。
(3) 指の割り当ての判断。音を聴いたとき、すぐに思い浮かぶのはあくまでも「鍵盤の位置」であって「その音を手を使って弾いている姿」ではないので、指の割り当ては考えないとわかりません。
あと、語りが入っているときはそこの音の高さはわかりません。同様にしゃべる声の音の高さ判定もできません。
楽譜を読む時
楽譜を読むときは、調号・音符・同じ小節内の臨時記号を読んで鍵盤の位置に変換します。
私は楽譜をネイティブレベルで流暢に読めるわけではないので、この作業には結構な時間がかかります。
プログラミングの人向け注: 機械語にあたるもの(私がネイティブに理解できるもの)は鍵盤の位置や音です。楽譜は一旦コンパイルが必要で、歌う場合は往々にしてそこがボトルネックです。
生物系の人向け注: 楽譜がイントロンなどのタンパク質に翻訳されない部分も含むDNA配列とすれば、脳内イメージはそこからエクソンのみを取り出して翻訳した後のタンパク質に相当する…はずです。必要なところしか読まない・場合によって読むところが変わる・一旦変換したら少量なら覚えておけるなどが共通点です。
この辺りは人によって様々な方法があるはずなので、コメントなどで教えていただけるとありがたいです。特に初心者の方・大人になってから音楽を始めた方がどうされているのか知りたいです。
初見の楽譜を見て歌うとき
私がやっているステップは次の2つです。
(1) 楽譜を読み、何の音が書かれているか理解する。 (全体の 80%〜90% くらいの時間がかかる)
(2) 理解した通りに音を歌う。(全体の 10%〜20% くらいの時間がかかる)
(1)の「理解」は以下のステップに分割できます。体感で要する時間を表記しておきます。普段は雑に上から順番にやっていき間に合わなかったところは飛ばしています。
(1-i) 自分のパートの音・リズムを拾う。(1〜3)
(1-ii) 他のパートのリズムを拾う。(2〜6くらい? パート数に比例)
(1-iii) 他のパートとタイミングが合うところや、それがない場合はどのタイミングで入れば良いかを見つける。(1〜4)
(1-iv) 他のパートの音を合わせてすべて鍵盤の上に投影し、コードがどうなっているか把握する (3〜10くらい? コードが複雑であればかなり時間がかかり、隣も見て有名な進行である場合は素早くできる)
また、リアルタイムに楽譜を読みながら歌う必要がある場合は、これらの作業を2小節くらい先読みして行うことで、楽譜を読む時の遅さに対処しています。
楽譜を読むときの速度はある程度鍛えることができると思っています。その方法についてはこの次の記事で詳しく書きたいと思います。
他パートと合わせるとき
「なんとなく響きが綺麗」や3度とか5度くらいならわかりますが、7度などに対する感覚が弱いです。
よくありそう(?)な誤解
Q1. どんな楽譜でもすぐ読めるんでしょ?
A1. 直感に反するかもしれませんが楽譜読むのが多分一番苦手です あまりやらないけど聴いて覚える方が得意かもしれません 楽譜読むのは非自明なので当然難易度によって時間が変わります
Q2. 自慢か?
A2. 絶対音感は努力して身につけたわけではないので自慢する資格はないと思います 自慢するなら譜読みの早さの方ですがそちらは次の記事でテクニックの紹介とともにやります
Q3. 自虐風自慢か?
A3. 主観的にはできることよりもできないことの方が多いです*5 あれもできない、これもできないという愚痴だと思って読んでください
Q4. 曲を聴いたら楽譜が思い浮かぶ?
A4. いいえ 楽譜を想像するのも負荷が大きいです 訓練で楽譜を想像することはたまにあります
Q5. 絶対音感は努力で身につく?
A5. わかりません 今まで調べてきたことから判断すると不可能か難易度が高いと判断するのが良心的な気がします コスパもいいかわからないですね
Q6. 絶対音感を持たない人間にこの記事の内容がどうやって役に立つの?
A6. 一応何個かは思いついています(次の記事で紹介します) この記事を書いた目的の一つに「他の人の認知方法を教えてもらう」というのがあるので、教えてもらえればさらに役に立たせる方法を見つけられるかもしれません あと単純に興味あるんで教えてください
まとめ
絶対音感を持っているだけで何かができるようになるということは基本的にはありません。私の場合、絶対音感の精度や頭の良さに制約があるので運用上やりたいけれどもできないことが目立ちます。
絶対音感を持ってない人のことは想像に限界があるので、教えてもらえると嬉しいです。
音楽を聴いたときの脳内イメージは鍵盤 + 雑なコードです。そこから実際にピアノで演奏するまでには結構なギャップがあります。
楽譜は認知コストが高いので扱いが比較的苦手です。
この記事の内容は以上です。「(2)譜読みの高速化」に続きます。
参考
論理的思考の放棄 - 登 大遊 (Daiyuu Nobori) の個人日記: インスピレーションをもらいました。
ピアノが上手になる超簡単ヒント集 | 楽譜を読む スピードアップのコツ!: 楽譜を読むときのテクニックを調べているうちに見つけました。こういった感じのことを積み重ねて譜読みの高速化に繋げることができるので、これも参考に次のブログ記事の内容を拡充したいと思っています。
*1:合唱で音楽を始めた人も含めて比較するのは公平ではないので、本当であればピアノを10年程度やっていた人の中での速度を比較すべきですが、残念ながら比較に必要なデータを持っていません。多分その比較だと遅い方だと思います。
*2:半音くらいなら臨時記号がなければなんとかできなくはないですが、臨時記号のところとかで詰まってしまいリアルタイムに楽譜を読みながら歌うのは難易度が高いです。
*3:このツイートの事例で、喉の感覚から音を逆算する能力を鍛えたという話を聞いて心底驚いた覚えがあります。そういう方向に進化することと、そんなことが可能だということにです。
*4:念のため、ここでは絶対音感と相対音感のどちらが優れているか議論したいわけではありません。どちらにもメリットとデメリットがあります。(個人的には、歌には相対音感の方が向いているのでは?と思うことは何度もあります。)
*5:他人と比べてよりできるらしいのは自覚してますがどうでもいいです。絶対音感を運用する上では自分に課せられた制限を把握することが必要で、他人との比較は無意味です。
ABC246 G - Game on Tree 3
かなり手間取った。
G - Game on Tree 3
問題
頂点 1 を根とした N 頂点の根付き木があり、頂点 1 以外には正の整数が書かれている。木の頂点 1 に駒を置き、高橋君と青木君は以下のようなターン制のゲームを行う。
1. 青木君は頂点 1 以外の頂点を選び、書かれている整数を 0 にする
2. 高橋君は駒の置かれている頂点の子のどれか一つに駒を動かす
3. 駒の置かれている頂点が葉であればゲーム終了。そうでなくても高橋君は自分の判断でゲームを終了できる。
ゲーム終了時に駒の置かれている頂点に書かれている整数が得点である。高橋君の目標は得点の最大化、青木君の目標は得点の最小化である。
両者が最適にプレイする時の得点を求めよ。
解法
二分探索により、各頂点に 0 か 1 が書いてあり、高橋君は 1 に到達できれば勝ち、青木君は阻止できれば勝ち、というゲームになる。
各頂点に対し評価値を、その頂点を根とした部分木で青木君が勝つために必要な手数 (1 を 0 にする回数) とする。各ターン、青木君には子孫のうちどれかを 0 にするという 1 手の余裕があり、高橋君は評価値最大の子を選べばよいので、子の評価値の和が 2 以上であれば勝つことができ、またそれより大きければそれだけ青木君が必要な手数も増える。また、頂点に書かれている数が 1 であれば即座に高橋君が勝つので、青木君はこれも 0 にしなければならない。よって 評価値 = max(子の評価値の和 - 1, 0) + 自分に書かれている値 である。
登場する典型
- 二分探索
- 木 DP
- 二人有限確定完全情報ゲーム
実装上の注意点
特になし
提出:
#30672562 (Rust)
// Does Takahashi win? fn dfs(v: usize, par: usize, g: &[Vec<usize>], b: &[bool]) -> i32 { let mut num = 0; if b[v] { num += 1; } let mut ma = 0; for &w in &g[v] { if w == par { continue; } let sub = dfs(w, v, g, b); num += sub; ma = max(ma, sub); } if ma > 0 { num -= 1; } num } fn solve() { input! { n: usize, a: [i64; n - 1], uv: [(usize1, usize1); n - 1], } let mut a = a; a.insert(0, 0); let mut g = vec![vec![]; n]; for &(u, v) in &uv { g[u].push(v); g[v].push(u); } let mut pass = 0; let mut fail = 1 << 30; while fail - pass > 1 { let mid = (fail + pass) / 2; let b: Vec<bool> = a.iter().map(|&a| a >= mid).collect(); let ans = dfs(0, n, &g, &b); if ans > 0 { pass = mid; } else { fail = mid; } } println!("{}", pass); }
まとめ
適切な評価値を思いつくのが難しく、[i64; 2] (部分木に対して、そこで 0 にする操作をするかどうかで場合分け) などでうまくいくかどうかを無駄に考えてしまっていた。
CODE FESTIVAL 2018 Final (Parallel) H - Pothunter
難しいけど定跡なので、この手の問題は解けるようになるべき。
H - Pothunter
問題
N頂点の木の各頂点上に街がある。木にはN - 1本の辺があり、i番目の辺はA[i], B[i]を繋ぐ辺で、この上を移動するにはD[i]だけ時間がかかる。
M個のコンテストが開催される。コンテストiは、街C[i]で時刻S[i]からE[i]の間に行われ、優勝賞金はX[i]である。
このコンテストの一部に参加して、最大の賞金を得たい。参加するコンテストには以下の条件がある。
- 同時に参加できるコンテストは最大1個。コンテストiに参加するときは、時刻S[i]からE[i]まで他のコンテストには参加できない。
- 参加したコンテストでは100%の確率で優勝できる。
最適戦略における賞金総額を求めよ。
解法
重心分解を使う。
登場する典型
- 重心分解(重心木)
- LCAを使って木の2点間の距離をO(log N)で求めるテクニック
実装上の注意点
- 重心分解を使う都合上かなり手数が多いので、デバッグプリントなどを駆使して絶対に間違いがないようにすること
提出: Submission #4339696 - CODE FESTIVAL 2018 Final (Parallel) (Rust)
// Treap省略 fn centroid_decompose(g: &[Vec<(usize, i64)>]) -> Vec<(usize, usize, i64)> { fn find_subtree_sizes(g: &[Vec<(usize, i64)>], v: usize, par: usize, dp: &mut [usize], vis: &[bool]) { let mut sum = 1; for &(w, _) in &g[v] { if par == w || vis[w] { continue; } find_subtree_sizes(g, w, v, dp, vis); sum += dp[w]; } dp[v] = sum; } fn centroid_decompose_inner(g: &[Vec<(usize, i64)>], v: usize, par: usize, cost: i64, edges: &mut Vec<(usize, usize, i64)>, dp: &mut [usize], vis: &mut [bool]) { let n = g.len(); find_subtree_sizes(g, v, n, dp, vis); let (cent, dist) = { let sz = dp[v]; let find_centroid = |mut v: usize, mut par: usize| { let mut dist = 0; loop { let mut has_majority = false; for &(w, c) in &g[v] { if par == w || vis[w] { continue; } if dp[w] > sz / 2 { dist += c; par = v; v = w; has_majority = true; break; } } if !has_majority { return (v, dist); } } }; find_centroid(v, n) }; let g_cent = g[cent].clone(); if par < n { edges.push((par, cent, dist + cost)); } // v was selected as a centroid // and will be ignored in the following decomposition procedure vis[cent] = true; for &(w, c) in &g_cent { if !vis[w] { centroid_decompose_inner(g, w, cent, c, edges, dp, vis); } } } let n = g.len(); let mut edges = vec![]; // This Vec is reused many times let mut dp = vec![0; n]; let mut vis = vec![false; n]; centroid_decompose_inner(&g, 0, n, 0, &mut edges, &mut dp, &mut vis); edges } const B: usize = 17; fn init_lca_dfs(g: &[Vec<(usize, i64)>], v: usize, par: &mut [usize], dep: &mut [usize], dep_dist: &mut [i64]) { for &(w, c) in &g[v] { if w == par[v] { continue; } par[w] = v; dep[w] = dep[v] + 1; dep_dist[w] = dep_dist[v] + c; init_lca_dfs(g, w, par, dep, dep_dist); } } fn init_lca(g: &[Vec<(usize, i64)>]) -> (Vec<usize>, Vec<i64>, Vec<Vec<usize>>) { let n = g.len(); let mut lca = vec![vec![n; n]; B]; let mut dep = vec![0; n]; let mut dep_dist = vec![0; n]; let mut par = vec![n; n]; init_lca_dfs(g, 0, &mut par, &mut dep, &mut dep_dist); for v in 0..n { lca[0][v] = par[v]; } for i in 0..B - 1 { for v in 0..n { let w = lca[i][v]; lca[i + 1][v] = if w >= n { n } else { lca[i][w] }; } } (dep, dep_dist, lca) } fn solve() { let out = std::io::stdout(); let mut out = BufWriter::new(out.lock()); macro_rules! puts { ($format:expr) => (write!(out,$format).unwrap()); ($format:expr, $($args:expr),+) => (write!(out,$format,$($args),*).unwrap()) } let mut x = 0x15262627i64; let a = 0x245711; let b = 0x13331; let mut next = || { x = x.wrapping_mul(a).wrapping_add(b); x }; input! { n: usize, m: usize, abd: [(usize1, usize1, i64); n - 1], secx: [(i64, i64, usize1, i64); m], } let mut g = vec![vec![]; n]; for &(a, b, d) in &abd { g[a].push((b, d)); g[b].push((a, d)); } // Construct centroid tree let edges = centroid_decompose(&g); let mut tree = vec![vec![]; n]; let mut par = vec![n; n]; for &(p, child, cost) in &edges { tree[p].push((child, cost)); par[child] = p; } let (dep, dep_dist, lca_aux) = init_lca(&g); let lca = |mut x: usize, mut y: usize| { if dep[x] > dep[y] { std::mem::swap(&mut x, &mut y); } for i in (0..B).rev() { if dep[y] >= dep[x] + (1 << i) { y = lca_aux[i][y]; } } assert_eq!(dep[x], dep[y]); if x == y { return x; } for i in (0..B).rev() { if dep[x] <= 1 << i { continue; } if lca_aux[i][x] == lca_aux[i][y] { continue; } x = lca_aux[i][x]; y = lca_aux[i][y]; assert_ne!(x, y); } x = lca_aux[0][x]; y = lca_aux[0][y]; assert_eq!(x, y); x }; let gdist = |x: usize, y: usize| { let l = lca(x, y); let v = dep_dist[x] + dep_dist[y] - 2 * dep_dist[l]; v }; let mut secx = secx; secx.sort(); let mut dp = vec![0; m]; let mut pool = vec![Treap::new(); n]; // set should be, and will be, strictly increasing. fn update_element<F>(set: &mut Treap<(i64, i64)>, a: i64, b: i64, next: &mut F) where F: FnMut() -> i64 { let (idx, _) = set.find_index(&(a, b)); if idx >= 1 { let &(_, pro) = set.at(idx - 1).unwrap(); if pro >= b { return; } } set.insert_mut((a, b), next()); while let Some(&(_cost, pro)) = set.at(idx + 1) { if b >= pro { set.erase_at_mut(idx + 1); } else { break; } } } for i in 0..m { let (s, e, c, x) = secx[i]; let mut cur = c; loop { let dist = gdist(cur, c); let (idx, _) = pool[cur].find_index(&(s - dist + 1, 0)); if idx >= 1 { let &(last, profit) = pool[cur].at(idx - 1).unwrap(); assert!(last + dist <= s); dp[i] = max(dp[i], profit); } if par[cur] >= n { break; } cur = par[cur]; } dp[i] += x; let mut cur = c; loop { let dist = gdist(cur, c); update_element(&mut pool[cur], dist + e, dp[i], &mut next); if par[cur] >= n { break; } cur = par[cur]; } } let ma: i64 = dp.into_iter().max().unwrap(); puts!("{}\n", ma); }
まとめ
今まで重心分解(重心木)を書いたことが一度もなかった(は?)ので、書けてよかった。
2022-01-17 (月) - 2022-01-23 (日) 目標・進捗
今週の目標
最低やりたいこと
- CSP solver の使い方を学ぶ (ARC131 E - Christmas Wreath をやってみる)
できたらやりたいこと
- AtCoder 埋め
最低目標: 1 時間
やったこと・勉強時間
01-17 (月)
0.0h
01-18 (火)
0.0h
01-19 (水)
0.0h
01-20 (木)
CSP solver の使い方を学ぶ (ARC131 E - Christmas Wreath をやってみる)
0.7h
01-21 (金)
0.0h
01-22 (土)
ARC131, CF767
0.0h
01-23 (日)
0.0h
振り返り
目標
- CSP solver の使い方を学ぶ (ARC131 E - Christmas Wreath をやってみる) partially done
それ以外
なし
合計時間: 0.7 時間
反省点
2021-12-13 (月) - 2021-12-19 (日) 目標・進捗
今週の目標
最低やりたいこと
- CSP solver の使い方を学ぶ (ARC131 E - Christmas Wreath をやってみる)
できたらやりたいこと
- AtCoder 埋め
最低目標: 0 時間
やったこと・勉強時間
12-13 (月)
0.0h
12-14 (火)
0.0h
12-15 (水)
0.0h
12-16 (木)
0.0h
12-17 (金)
0.0h
12-18 (土)
0.0h
12-19 (日)
yukicoder 1056
1.0h
振り返り
目標
- CSP solver の使い方を学ぶ (ARC131 E - Christmas Wreath をやってみる) untouched
それ以外
なし
合計時間: 1.0 時間
反省点
2021-12-06 (月) - 2021-12-12 (日) 目標・進捗
やったこと・勉強時間
12-06 (月)
非再帰遅延セグメント木 整備
0.8h
12-07 (火)
0.0h
12-08 (水)
yukicoder 1670
0.9h
12-09 (木)
yukicoder 1780, 860, 856
1.7h
12-10 (金)
yukicoder 1138
0.2h
12-11 (土)
0.0h
12-12 (日)
ABC231 H
4.5h
反省点
2021-11-22 (月) - 2021-11-28 (日) 目標・進捗
やったこと・勉強時間
11-22 (月)
yukicoder 1755
0.9h
11-23 (火)
0.0h
11-24 (水)
0.0h
11-25 (木)
0.0h
11-26 (金)
0.0h
11-27 (土)
0.0h
11-28 (日)
0.0h
反省点
yukicoder 埋めが楽しすぎる