DrawingAreaは、デフォルトではマウスのイベントを拾えないようだ。発生するイベントの種類を増やすには、WidgetExt::add_event()というメソッドを使うらしい。
let drawingArea: gtk::DrawingArea = builder.get_object("drawingarea1").unwrap();
drawingArea.set_size_request(300, 300);
drawingArea.add_events(gdk_sys::GDK_BUTTON_PRESS_MASK.bits() as i32);
drawingArea.add_events(gdk_sys::GDK_BUTTON_RELEASE_MASK.bits() as i32);
JavaのSwingだと、clickedというイベントで同じ場所でマウスのボタンが押されて離されたという状態を取得できたが、どうやらGTK+には、そういうのが無いようだ。自分でPRESSの時の座標を覚えておいて、RELEASEの座標が同じだったらクリックと判定する必要がある模様。イベントハンドラの登録は、WidgetExt::connect_event()を使う。
drawingArea.connect_event(|_, e| {
let clone = e.clone();
match e.get_event_type() {
EventType::ButtonPress => {
let res: Result<EventButton, Event> = clone.downcast();
println!("pressed: {:?}", res.unwrap().get_position());
},
EventType::ButtonRelease => {
let res: Result<EventButton, Event> = clone.downcast();
println!("released: {:?}", res.unwrap().get_position());
},
_ => {}
}
return Inhibit(false);
});
Event型で来るため、特定のイベントであるかをEventTypeで判定して、ダウンキャストするというなんとも汚ないコード。なぜイベント自体をenumにしなかったのかな。GTK+との関係で難しいのだろうか。
クロージャの引数は&Eventで、downcastの引数は、&selfではなくてselfそのものなので、そのままイベントを渡すとmoveが起きてしまってコンパイルエラーになる。なので、一度clone()している。
コードサンプルは、こちら
作品9は3つのノクターンで構成されていて、第3曲が最後。ショパンとしては、この曲は意欲作だったのではないかと思う。ショパンのノクターンは3部構成になっているものが多いけど、この曲は、その中間部の雰囲気をがらりと変えて大きな効果を得ることを狙ったのだと思う。ただ、残念ながら一般には作品9の中での知名度は最も低くなってしまった。まぁ普通の人からすると、え、何、なんでノクターンなのに、こんなに激しいの? みたいな違和感が大きかったんだろうな。まぁ、こんな具合に作曲者が空回りしてしまうケースというのは多い。
この曲は、スタカーティシモが多用されている。面白いのはタイの後の音にスタカーティシモが付いているケースがあって、これ、どう表現するか悩みどころ。実際の演奏会なら大げさに右手を跳ね上げたりするんだろうか。
中間部。Agitatoでガンガン行く。ショパン的には「どうだ!」って感じだったのだろうな。
最後のアルペジオは、ものすごく綺麗だけど、これppで弾くのすごく難しそうで禿げそう。
楽譜引用はエキエル版
今回のお題は、2値(白黒)+alpha(透過付き)のgifファイルを読み込み、その中の黒を赤として描画したいというもの。要は「選択状態」の描画を赤色でしたい。だけど元の画像ファイルは2値(白黒)だよ。という状況。
Pixbufは、2値の画像読み込んだ場合でも、中ではRGBAそれぞれに8bitを割り当てているようだ。なので、ピクセル操作は思ったより簡単だった。
fn to_red(pixbuf: &Pixbuf) {
assert!(
pixbuf.get_colorspace() == gdk_pixbuf_sys::GDK_COLORSPACE_RGB,
"Unsupported color space: {}", pixbuf.get_colorspace()
);
assert!(pixbuf.get_has_alpha(), "This image does not have alpha channel");
let n_channels = pixbuf.get_n_channels();
let w = pixbuf.get_width();
let h = pixbuf.get_height();
let rowstride = pixbuf.get_rowstride();
let mut buf: &mut [u8] = unsafe {
pixbuf.get_pixels()
};
for y in 0..h {
for x in 0..w {
let offset = (y * rowstride + x * n_channels) as usize;
let r = buf[offset];
let g = buf[offset + 1];
let b = buf[offset + 2];
let a = buf[offset + 3];
if r == 0 && g == 0 && b == 0 {
buf[offset] = 255;
}
}
}
}
一応colorspaceと、alpha付きであることをチェック。
assert!(
pixbuf.get_colorspace() == gdk_pixbuf_sys::GDK_COLORSPACE_RGB,
"Unsupported color space: {}", pixbuf.get_colorspace()
);
assert!(pixbuf.get_has_alpha(), "This image does not have alpha channel");
あと、注意としてパディングされている可能性があるので、rowstrideにyを掛けてやる必要があるようだ。get_pixelsは、&mut [u8]が返ってくるので、大胆にもそのまま中をいじれてしまう。RGB全てが0(黒)なら、Rだけ255にしてやる。
for y in 0..h {
for x in 0..w {
let offset = (y * rowstride + x * n_channels) as usize;
let r = buf[offset];
let g = buf[offset + 1];
let b = buf[offset + 2];
let a = buf[offset + 3];
if r == 0 && g == 0 && b == 0 {
buf[offset] = 255;
}
}
}
コードは、GuiHubに置いておいた。
Rustで以下のようなJavaのenumを実現したい。
public enum SharpFlat {
SHARP(1), DOUBLE_SHARP(2), NATURAL(0), NULL(0), FLAT(-1), DOUBLE_FLAT(-2);
private final byte offset;
private SharpFlat(int offset) {
this.offset = (byte)offset;
}
public int getOffset() {
return offset;
}
}
RustのenumはJavaとかのenumとは考え方が違って直和型の定義のようだ。
structで定義してもいいけど、enumになっていればexhaustive checkされるので、やはりenumの方がありがたい。試してみると、enumもtraitを実装できるので、以下のようにすることで解決できた。
enum SharpFlat {
SHARP,
FLAT,
}
trait HasOffset {
fn offset(&self) -> i8;
}
impl HasOffset for SharpFlat {
fn offset(&self) -> i8 {
match *self {
SharpFlat::SHARP => 1,
SharpFlat::FLAT => -1,
}
}
}
fn foo(sharp_flat: Option<SharpFlat>) {
match sharp_flat {
None => {
println!("none");
},
Some(SharpFlat::SHARP) => {
println!("sharp {}", SharpFlat::SHARP.offset());
},
Some(SharpFlat::FLAT) => {
println!("flat");
},
}
}
fn main() {
foo(Some(SharpFlat::SHARP));
}
ショパンのノクターンの代名詞とも言える有名な曲。
構成は単純だけど、楽譜上、作曲者の指示が非常に多く、その1つ1つをどう表現するかが腕の見せどころとなる。第1小節目、左手にはスタカート指定。
続く小節にはスタカート無し。Sempreも無しなので、第1小節だけだ。
ただダンパーペダルを踏む指示もあるので、踏み込んでスタカートとなれば聴感上はスタカートあっても無くても変わらない。なので気にしないという考えもあるだろう。実際そう弾いているピアニストも多い。しかしわざわざ、このように楽譜上の表現を変えているのだから、やはりスタカートをきちんと生かすべきだろう。つまりスタカートの部分ではダンパーペダルは、踏まないかハーフペダルとしてスタカートを生かすべきだ。
最後のクライマックスにも同様なスタカーティシモ指定。
ここもペダルを踏み込んでスタカーティシモでは違いが分からないわけで、さりとて、全くペダルを踏まずにスタカーティシモでは興醒めなので、うまい具合にハーフペダリングして軽く残響を残しつつ、スタカーティシモを表現することになる。こんな微妙なダンパーペダルの制御をリアルなピアノでやろうとしたら、普段使っているピアノを持ち歩かないと無理だろう。
ところで、ここにStrettoがあるのは今まで知らなかった、確かにこの箇所の演奏は若干テンポを上げるケースが多いなとは思っていたが、これはちゃんと楽譜に指示があったのか。
楽譜引用はエキエル版
GTK+のGtkScrolledWindowを使っている時に、スクロールバーが表示されなかったり、無用な描画要求が多量に来て困っていたのだが、Gladeの設定にあるOverlay Scrollingというオプションをオフったら、嘘のように直った。なんなんだろう、これ。
Cairoを使ってアプリケーションのクライアント領域を描画しているのだが、再描画が必要な領域を知るため、最初は、cairo-clip-extentsを使っていた。ただ、これだとどうも、毎度全域を描画するように指示がくる。
もう1つ、cairo_copy_clip_rectangle_listというAPIもあり、こちらはより細かく、再描画が必要な複数の矩形領域を返してくれる。これら2つを表示してみると、こんな感じ。
cairo-clip-extents: (855, 91, 862, 817)
cairo_copy_clip_rectangle_list[0]: Some(cairo_rectangle_t { x: 855, y: 91, width: 7, height: 726 })
cairo_copy_clip_rectangle_listをそのまま使うか、これで返ってきた領域の和集合を計算して、そこに対して再描画するのが良さそう。
ショパン・ノクターンを出したのだけど、相変わらずSoundCloudは何でもかんでも海賊版警告が出て面倒なのでupするのやめた。日本でもSpotifyが始まったので、そちらでどうぞ(無料会員でもCMが入るけど聴けます)。
ただ、Spotifyは、かなりキツ目のダイナミック・レンジ圧縮が勝手に入ってしまって、特にクラシック音楽は音質が落ちてしまうのが難点。Apple Musicとかに入っている方は、そちらの方が良いかも(他のディストリビューションはこちら)。
今回は、作品9から37までの12曲。楽譜はエキエル版を参考にした。全般にテンポ指定がかなり速め。このテンポ通りに弾いているピアニストは、あまりいないんじゃないだろうか。でも本当は、もっとサラっと弾くべきなのかもしれない。
第1曲。全音の楽譜では、ほぼ全域でダンパーペダルを踏む指示なのだけど、エキエル版では、ごく一部にしかない。1つ目は出だし。
第1小節(弱起なので)にダンパーベダルがあるが、それっきり(Sempre指定も無し)。そして中間部も出だしにあるのみ。
特に、この中間部の右手はオクターブな上に弱音なので、ペダル無しで楽譜通りに弾くのはかなり難易度高いと思う。
楽譜引用はエキエル版
rust-jackのサンプルを見ると、
put_p.write(&RawMidi {
time: 0,
bytes: &[0b10010000 /* Note On, channel 1 */,
0b01000000 /* Key number */, 0b01111111 /* Velocity */],
})
.unwrap();
put_p.write(&RawMidi {
time: ps.n_frames() / 2,
bytes: &[0b10000000 /* Note Off, channel 1 */,
0b01000000 /* Key number */, 0b01111111 /* Velocity */],
})
.unwrap();
RawMidiの第一パラメータに時間情報が指定されているので、ここを色々変更して試してみた。すると、どうも大きな値を指定するとプログラムが異常終了する。
thread '' panicked at 'called `Result::unwrap()` on an `Err` value: UnknownError', ../src/libcore/result.rs:799
stack backtrace:
Jack: JackClient::ClientNotify ref = 4 name = rust_jack_show_midi notify = 3
Jack: JackClient::kXRunCallback
1: 0x556f8a7d8a0f - std::sys::backtrace::tracing::imp::write::h6f1d53a70916b90d
2: 0x556f8a7db50d - std::panicking::default_hook::{{closure}}::h137e876f7d3b5850
3: 0x556f8a7daa1a - std::panicking::default_hook::h0ac3811ec7cee78c
4: 0x556f8a7dafb8 - std::panicking::rust_panic_with_hook::hc303199e04562edf
5: 0x556f8a7dae52 - std::panicking::begin_panic::h6ed03353807cf54d
6: 0x556f8a7dad90 - std::panicking::begin_panic_fmt::hc321cece241bb2f5
7: 0x556f8a7dad11 - rust_begin_unwind
8: 0x556f8a80ffef - core::panicking::panic_fmt::h27224b181f9f037f
9: 0x556f8a7cadf2 - core::result::unwrap_failed::hf4a2bbe780b35bc4
at /buildslave/rust-buildbot/slave/stable-dist-rustc-linux/build/obj/../src/libcore/macros.rs:29
10: 0x556f8a7c8ad0 - >::unwrap::h089470f17859500e
at /buildslave/rust-buildbot/slave/stable-dist-rustc-linux/build/obj/../src/libcore/result.rs:737
11: 0x556f8a7ce894 - sample::main::{{closure}}::hf2f3cb9afdb66a80
at /home/shanai/rust/midi-sample/src/main.rs:34
12: 0x556f8a7cd12f - ::process::h4cdfe5f6478477da
at /home/shanai/.cargo/registry/src/github.com-1ecc6299db9ec823/jack-0.2.2/src/callbacks.rs:181
13: 0x556f8a7ccef2 - jack::callbacks::process::h6e653a5eccae8773
at /home/shanai/.cargo/registry/src/github.com-1ecc6299db9ec823/jack-0.2.2/src/callbacks.rs:215
14: 0x7f379543fc1d -
15: 0x7f379543f057 -
16: 0x7f379545767f -
17: 0x7f37950136b9 - start_thread
18: 0x7f3794b3382c - clone
19: 0x0 -
fatal runtime error: failed to initiate panic, error 5
二分法で限界を探してみると、1023までなら大丈夫で、1024以上だとエラーになる。Jackのサイトにプログラミング・モデル的な解説が無いので理解に時間がかかったが、どうやらこういうことらしい。
この設定で指定したフレーム/ピリオドに指定した値が限界らしい。rust-jackではコールバックを登録するが、このコールバックは、ここで指定したフレーム/ピリオドで指定された間隔で呼ばれ、コールバック内では1フレーム内の動作のみを指定可能なようだ。なので、時間指定にはこのフレーム/ピリオドの値未満しか指定できない。で、この値の単位なのだが、1/サンプルレート 秒になる。つまり、この例であれば1/44100秒。なので、1フレームは1024/44100秒になる。分解能は1/44100秒なので、仮にテンポ=600(1分で四分音符600回演奏、つまり1秒に四分音符10回演奏)を上限とすると、四分音符の分解能は4410となる。MIDIシーケンサとしては十分な値だろう。
最後にピリオド/バッファーだが、ここで指定した個数のフレーム用バッファーが用意されるようだ。ここの値が大きければ事前にデータを準備しておけるので、コールバックでの処理が少々遅延したとしても途切れずに再生できるが、先にデータを用意しておかないといけないので、レイテンシーが増大する。この画面の右下にあるレイテンシーがそれを表しているようだ(1024 * 3 * 1/44100 = 0.0696598639)。