musicLineアプリ開発日記

作曲を誰でも楽しく簡単に♪

SwiftUIでダイアログ

今回はSwiftUIでダイアログを表示する話。
様々なダイアログを表示してモデルのプロパティを変更できる機構を考えます。

ダイアログの動作



はじめに

musicLineではMIDI楽譜を編集するために様々なダイアログ(ポップアップでプロパティを設定するView)を表示します。

例えば、

  • 曲名やテンポの変更
  • 楽器の変更
  • 編集トラックの変更
  • フレーズの追加 等

ダイアログの使用例

AndroidではDialogFragmentの継承することでダイアログをカスタマイズしてました。
ダイアログを表示する際は、.showメソッドによりFragmentManagerに加えて表示していました。

iOSでも同じような使い方ができるようにSwiftUIで実装しました。


ちなみに、そこまでダイアログのデザインや挙動を制御する必要がないのであれば、モディファイア.confirmationDialog.sheetも使用できます。

capibara1969.com

blog.code-candy.com



ダイアログの仕様

今回の想定するダイアログの仕様です。

  1. ヘッダーに X ボタンとタイトルを表示
  2. X ボタンかダイアログ外をタップして閉じる
  3. ダイアログの中身を変更できるようにする

ダイアログの仕様

またダイアログは、どこからでも呼び出せるようにします。

myDialog.open(model: instance)



実装

ダイアログの動作

今回は各々のボタンで異なるダイアログが表示されるViewを作ってみました。


共通ダイアログ

まず、仕様を満たす抽象的なダイアログとそのモデルを定義します。

struct Dialog<TView: View, TDialogModel: DialogModelable>: View {
    
    let dialogModel: TDialogModel
    @ViewBuilder let content: () -> TView
    ...

    var body: some View {
        if dialogModel.isOpen {
            ZStack{
                // 黒背景(タップで閉じる)
                blackScreen
                
                // ダイアログの中身
                content()
                    ...
            }
            .zIndex(1)
        }
    }
    
    private var blackScreen: some View {
        Color.black
            .opacity(0.3)
            .ignoresSafeArea()
            .onTapGesture {
                dialogModel.close()
            }
            .contentShape(Rectangle())
    }
    ...

}
protocol DialogModelable{
    var title: String { get }
    var isOpen: Bool { get }
    func close()
}

dialogModel.isOpenにより、表示と非表示を切り替えます。
背景blackScreenをタップした時に、dialogModel.close()でダイアログを閉じます。 ダイアログの中身は派生ダイアログでカスタマイズできるように、contentクロージャーにしています。


また、Dialogのヘッダーの実装について、

struct Dialog<TView: View, TDialogModel: DialogModelable>: View {
    
    ...
    private let headerHeight = 35.0
    
    var body: some View {
    ...
                // ダイアログの中身
                content()
                    .padding(.top, headerHeight)
                    .overlay(titleBar)
                    ...
    }
    
    // ヘッダー
    private var titleBar: some View {
        VStack{
            ZStack(alignment: .top){
                Color.gray.frame(height: headerHeight)
                HStack{
                    Image(systemName: "multiply")
                        .onTapGesture {
                            dialogModel.close()
                        }
                        .padding(10)
                    Spacer()
                }
                Text(dialogModel.title)
                    .lineLimit(1)
                    .font(.headline)
                    .padding(6)
            }
            Spacer()
        }
    }
}

content().paddingで上に空間を作って、overlayでヘッダーを付けてます。
ヘッダーでは X ボタンとタイトルdialogModel.titleを表示しています。


ダイアログをカスタマイズ

次に、DialogDialogModelableを派生させて、独自のダイアログへカスタマイズします。
例えば、ダイアログの中身をステッパーにする場合

struct NumberStepperDialog: View {
    
    @Bindable var numberStepper: NumberStepperDialogModel
    
    var body: some View {
        Dialog(dialogModel: numberStepper){
            Stepper("\(numberStepper.number.value)",value: $numberStepper.number.value)
                .frame(width: 200)
        }
    }
}
@Observable 
class NumberStepperDialogModel: DialogModelable{
    let title = "Stepper"
    var number = Number()
    private(set) var isOpen = false
    
    func open(number: Number){
        self.number = number
        isOpen = true
    }
    
    func close(){
        isOpen = false
    }
}

なお、Viewへの通知はObservationを使っているので、iOS17以降で動作します。
それ以前でしたら、Combine等に置き換えてください。


ダイアログモデルをシングルトンで管理

ダイアログをどこからでも呼び出せるようにシングルトンでダイアログモデルを管理します。

class DialogManager {
    
    static let shared = DialogManager()
    let numberStepper = NumberStepperDialogModel()
    ...
}
@Observable 
class Number{
    var value = 0
}
let number = Number()    // 編集対象モデル

// ダイアログを呼び出す
DialogManager.shared.numberStepper.open(number: number)


ダイアログを貼り付ける

呼び出された時にダイアログを表示できるように、Viewにダイアログを貼り付けます。

struct ContentView: View {

    ...
    let dm = DialogManager.shared

    var body: some View {
        ZStack{

            NumberStepperDialog(numberStepper: dm.numberStepper)
            ...
        }
    }

}


全コードを表示

import SwiftUI

struct ContentView: View {
    
    // MARK: Property
    let number = Number() // 編集対象モデル
    let dm = DialogManager.shared

    var body: some View {
        ZStack{
            VStack{
                menu.padding()
                Text("\(number.value.description)")
            }
            
            NumberStepperDialog(numberStepper: dm.numberStepper)
            NumberPickerDialog(numberPicker: dm.numberPicker)
            NumberTextFieldDialog(numberTextField: dm.numberTextField)
        }
    }
    
    @ViewBuilder
    var menu: some View{
        Button("Dialog 1"){
            dm.numberStepper.open(number: number)
        }
        Button("Dialog 2"){
            dm.numberPicker.open(number: number)
        }
        Button("Dialog 3"){
            dm.numberTextField.open(number: number)
        }
    }
}
import SwiftUI

struct NumberStepperDialog: View {
    
    @Bindable var numberStepper: NumberStepperDialogModel
    
    var body: some View {
        Dialog(dialogModel: numberStepper){
            Stepper("\(numberStepper.number.value)",value: $numberStepper.number.value)
                .frame(width: 200)
        }
    }
}

struct NumberPickerDialog: View {
    
    @Bindable var numberPicker: NumberPickerDialogModel
    
    var body: some View {
        Dialog(dialogModel: numberPicker){
            
            Picker("\(numberPicker.number.value)", selection: $numberPicker.number.value) {
                ForEach(0...10, id: \.self) { number in
                    Text("\(number)")
                }
            }
            .pickerStyle(.wheel)
            .frame(width: 200)
        }
    }
}

struct NumberTextFieldDialog: View {
    
    @Bindable var numberTextField: NumberTextFieldDialogModel
    
    var body: some View {
        Dialog(dialogModel: numberTextField){
            TextField("\(numberTextField.number.value)", value: $numberTextField.number.value, format: .number)
                .keyboardType(.numberPad)
                .multilineTextAlignment(.center)
                .frame(width: 50, height: 50)
                .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray))
                .padding(.top, 15)
        }
    }
}
import SwiftUI

@Observable 
class NumberStepperDialogModel: DialogModelable{
    let title = "Stepper"
    var number = Number()
    private(set) var isOpen = false
    
    func open(number: Number){
        self.number = number
        isOpen = true
    }
    
    func close(){
        isOpen = false
    }
}

@Observable 
class NumberPickerDialogModel: DialogModelable{
    let title = "Picker"
    var number = Number()
    private(set) var isOpen = false
    
    func open(number: Number){
        self.number = number
        isOpen = true
    }
    
    func close(){
        isOpen = false
    }
}

@Observable 
class NumberTextFieldDialogModel: DialogModelable{
    let title = "Text Field"
    var number = Number()
    private(set) var isOpen = false
    
    func open(number: Number){
        self.number = number
        isOpen = true
    }
    
    func close(){
        isOpen = false
    }
}
import Foundation

@Observable 
class Number{
    var value = 0
}
class DialogManager {
    
    // MARK: Property
    static let shared = DialogManager()
    let numberStepper = NumberStepperDialogModel()
    let numberPicker = NumberPickerDialogModel()
    let numberTextField = NumberTextFieldDialogModel()
    
    
    // MARK: Initializer
    private init() {}
}
import SwiftUI

struct Dialog<TView: View, TDialogModel: DialogModelable>: View {
    
    // MARK: Property

    // Stored
    let dialogModel: TDialogModel
    @ViewBuilder let content: () -> TView

    private let headerHeight = 35.0
    private let padding = 10.0
    private let margin = 10.0
    private let cornerRadius = 10.0
    
    var body: some View {
        if dialogModel.isOpen {
            ZStack{
                // 黒背景(タップで閉じる)
                blackScreen
                
                // ダイアログの中身
                content()
                    .padding(padding)
                    .padding(.top, headerHeight)
                    .frame(minWidth: 300, minHeight: 100)
                    .overlay(titleBar)
                    .background(Color.white)
                    .cornerRadius(cornerRadius)
                    .padding(margin)
            }
            .zIndex(1)
        }
    }
    
    private var blackScreen: some View{
        Color.black
            .opacity(0.3)
            .ignoresSafeArea()
            .onTapGesture {
                dialogModel.close()
            }
            .contentShape(Rectangle())
    }
    
    private var titleBar: some View{
        VStack{
            ZStack(alignment: .top){
                Color.gray.frame(height: headerHeight)
                HStack{
                    Image(systemName: "multiply")
                        .onTapGesture {
                            dialogModel.close()
                        }
                        .padding(10)
                    Spacer()
                }
                Text(dialogModel.title)
                    .lineLimit(1)
                    .font(.headline)
                    .padding(6)
            }
            Spacer()
        }
    }
}
protocol DialogModelable{
    var title: String { get }
    var isOpen: Bool { get }
    func close()
}



おわりに

今回はAndroidのようにダイアログを管理して、SwiftUIでダイアログを表示する方法を模索してみました。
ダイアログの中身に色々なViewを適用できるようにするため、Dialog.titleBarがトリッキーな実装になってしまいました。もう少しスマートで適切な実装があるかも?

実際にはanimationをつけたり、キーボード表示の挙動など考慮する点があると思います。
あとシングルトンを使ってますが、@Environmentにしてライフサイクルを意識した方がいいかもしれないですね。
それとこの実装だと同じダイアログを複数開くことができないことも気になります。。
(そもそもそんな状況を作ったらいけないか?)

とりあえずこのダイアログの機構をベースに改善していきます。



【iOS】作曲画面の進捗報告 5

musicLine(iOS)についての進捗。
フレーズツールの実装が大体完了したので報告します。

フレーズツールの動作
※ 描画に不具合があり、ちょっと変

ただし現在は描画に不具合があり、フレーズツールの操作がわかりづらくなっています。

例えば、フレーズダイアログで作成ボタンをタップしても反応がありません。正確に言えば実際はフレーズを作成したんだけど、画面の描画が行われずに作成できてないような表示になります。

描画タイミングの不具合
※ スクロールすると描画される

これはモデルの通知機構に問題があり、モデルの変更を適切に監視できてないことが要因になります。今後通知機構について見直していきます。

現在は、暫定処置として画面をスクロールすることで画面を強制的に描画しています。


実装状況
カテゴリタイトル補足
View
Composition
UI配置
ガイド表示
ツールガイド(フレーズ)フレーズ移動・伸縮のバー、選択範囲テキスト(iOS独自)
フレーズボタンフレーズボタンとサブボタンを配置、ツールに応じてアイコンを変更、画面拡大率に応じて縮小
フレーズタブツールに応じてアイコンを変更、拡大率に応じて縮小、スクロール状態に応じて移動
Dialog
ダイアログ
フレーズフレーズの作成・設定・挿入、フレーズの長さやリピート回数の設定
Composition
Common
作曲共通
通知機構Modelに変更があった時に、Viewへ通知して再描画。変更状況を監視してキャッシュ
PhraseTool
フレーズツール
フレーズの編集
データ構造トラックのフレーズを取得・追加、削除、状態 通知機構の実装を見直す。現状は一部描画がおかしい
フレーズの作成・挿入ダイアログで長さとリピートを設定 リピートフレーズは未実装
フレーズの選択2点タップで範囲選択、範囲ガイドタップで選択解除(仕様変更)
フレーズの移動左右スワイプで移動、移動は選択フレーズとスワイプ中のフレーズを含む
フレーズの伸縮サブ編集領域をスワイプした時にフレーズ長さを伸縮、長さ0で削除(iOS独自)
フレーズの貼り付け選択フレーズをコピー・ペースト、サブボタンかフレーズタブタップで挿入、選択解除
周囲有無判定作成する小節の近くにフレーズがあるかの判定、設定できる長さやリピート回数の制御
フレーズボタン・タブツールに応じてフレーズボタン、タブの処理を変更


全進捗マップを表示

カテゴリタイトル補足
View
Composition
UI配置
ガイド表示
ピアノ
スクロールエリア
小節番号
小節線
分割線拡大率によって間隔を変化
フレーズフレーズがないところはフレーズ作成ボタンを表示
ツールボタン 選択中のツールはツール色にハイライトする
メロディーライン ペンツールでの音符作成時の入力線
メロディー音符 音符の先頭にチョボをつける
リズム音符 ベース音符のみ下に表示
サンプルデータ 手入力したサンプルデータの音符を配置
連符連符は数字で表示、拡大率によって省略
ツールガイド(ペン)長さ編集しているリズム音符、伸ばした時の削る範囲音符移動時、移動先の音階をわかりやすくする
ツールガイド(指)移動中の音符影、矩形選択の枠
ツールガイド(消しゴム)削除範囲、和音のみの削除範囲
ツールガイド(フレーズ)フレーズ移動・伸縮のバー、選択範囲テキスト(iOS独自)
フレーズボタンフレーズボタンとサブボタンを配置、ツールに応じてアイコンを変更、画面拡大率に応じて縮小
フレーズタブツールに応じてアイコンを変更、拡大率に応じて縮小、スクロール状態に応じて移動
音符音階メロディ音符に音階表示
Community
UI配置
Dialog
ダイアログ
フレーズフレーズの作成・設定・挿入、フレーズの長さやリピート回数の設定
Composition
Common
作曲共通
データ構造座標とサイズ、状態を保持する。レンダラーやコライダー、通知等のロジック
コンバーターDomainのデータ構造へ変換・逆変換
通知機構Modelに変更があった時に、Viewへ通知して再描画。変更状況を監視してキャッシュ
FingerTool
指ツール
音符の編集
データ構造フレーズの音符を取得、基点と移動ベクトルを保持して移動する
音符の移動和音や連符は上下のみに制御
音符の影移動中に操作できているかわかりやすいように(iOS独自)
内外判定音符の移動をフレーズ内に留める
衝突判定移動する音符が他の音符に重なったときの挙動
タップ選択タップした音符の子音符も選択・解除
矩形選択囲った音符を選択
音符の入れ替えリズム音符のスライドで重なった音符と入れ替える(iOS独自)
和音の削除和音を移動した時に重なる時に削除する
連符に切り替えリズム音符タップで切り替え
PenTool
ペンツール
音符の作成
データ構造音符を作成・分割・統合、フレーズへ音符を追加する
音符の作成タップで音符を作成。分割線に合わせて長さを決定
音符列の作成スワイプに沿って複数の音符を作成。音階変わる時に音符分割
有無判定タップやスワイプした縦ラインに既に音符があるか判定
音符の移動音符が既にある場合は、作成ではなく移動。一定距離進むと移動終了
音符の分割リズム音符をタップした時、音符を分割、メロディ線に沿ってY位置を動かす
音符の統合2つのリズム音符の間をタップした場合、音符を統合
音符の伸縮リズム音符を左右にスワイプすることで音符の長さを伸縮、始点で分割なし(仕様変更)
音符の消滅音符の伸縮をした時に長さが0になると削除する(iOS独自)
領域差演算音符伸縮で他の音符に重なる時は差演算して他の音符長さを削る
サンプリング補間素早くスワイプしても音符が移動できるようにサンプリング間を補間する
細かい音符分割線内の細かな音符を移動する挙動
EraserTool
消しゴムツール
音符の削除
データ構造和音と連符の認識、フレーズから音符を削除する
音符の削除タップで音符を削除
和音・連符の削除和音・連符をタップで和音・連符のみを削除(iOS独自)
音符種別判定タップした音符が和音・連符・ルート音符なのか判定する
音符列の削除スワイプで指定する削除範囲内の複数の音符を削除。
和音列の削除リズム音符をスワイプで複数の和音のみを削除。(iOS独自)
休符に切り替えリズム音符タップで休符に切り替える
PhraseTool
フレーズツール
フレーズの編集
データ構造トラックのフレーズを取得・追加、削除、状態 通知機構の実装を見直す。現状は一部描画がおかしい
フレーズの作成・挿入ダイアログで長さとリピートを設定 リピートフレーズは未実装
フレーズの選択2点タップで範囲選択、範囲ガイドタップで選択解除(仕様変更)
フレーズの移動左右スワイプで移動、移動は選択フレーズとスワイプ中のフレーズを含む
フレーズの伸縮サブ編集領域をスワイプした時にフレーズ長さを伸縮、長さ0で削除(iOS独自)
フレーズの貼り付け選択フレーズをコピー・ペースト、サブボタンかフレーズタブタップで挿入、選択解除
周囲有無判定作成する小節の近くにフレーズがあるかの判定、設定できる長さやリピート回数の制御
フレーズボタン・タブツールに応じてフレーズボタン、タブの処理を変更
StampTool
スタンプツール
モチーフの編集
Transform
画面移動
座標変換
画面を上下移動ピアノのスワイプにより
画面を左右移動 スクロールエリアのスワイプにより
画面を拡大・縮小ピンチアウト・インにより基点を画面中心に設定する
画面を拡大・縮小(軸指定)長押しからのドラッグ。ピアノでX軸方向、スクロールエリアでY軸方向
MIDI
Common
MIDI共通
データ構造MIDIフォーマットへ出力できる構造
コンバーターCompositionのデータ構造へ変換・逆変換
Commnity
Common
コミュニティ
共通
データ構造カテゴリ等の曲情報、いいねやお気に入り等のリスポンスを保持
Domain
データ構造
Json形式
Melody MelodyTrack, *MelodyPhrase, NoteContainer, NoteBlock, Note
Drum DrumTrack, *DrumPhrase, BeatContainer, Beat
通化 Original, Repeat, Syncの3種のPhraseをジェネリッククラスとプロトコルで抽象化
Service
サービスモデル
SongRederJsonファイルを読み込み、Domainのデータへ変換
SongWriterDomainのデータからJsonファイルへ書き出し
Common
Service
サービスモデル
MidiPlayerMidiファイルを再生する
リポジトリ保存データを管理


フレーズツールの実装

フレーズの作成・挿入

フレーズボタンをタップした時に、フレーズを作成します。
フレーズの作成時にはダイアログを表示し、フレーズの長さやリピート回数を設定します。
また、フレーズサブボタンかフレーズタブをタップすることで、フレーズを挿入します。

フレーズの作成・挿入


フレーズの選択

フレーズをタップして選択します。
フレーズ外をタップすると選択解除になります。
なお、複数フレーズの飛び飛びでの選択はできず、2点タップすると2点間の範囲選択になります。

フレーズの選択

ちなみに、右上には選択しているフレーズの小節範囲が表示されます。その選択範囲のテキストをタップすることでも選択解除になります。


フレーズの移動

編集エリアを左右にスワイプして、フレーズを移動します。
フレーズを選択すると、複数のフレーズを一気に移動できます。
なお、選択フレーズとスワイプするフレーズの間にフレーズがある場合、挟まれたフレーズも一緒に移動します。

フレーズの移動

移動中は移動範囲がわかりやすいように、移動後の小節位置とバーを表示します。


フレーズの伸縮

サブ編集領域をスワイプすることで、フレーズの長さを伸縮します。
なおフレーズを短くした時、フレーズ外の音符は削除され、フレーズを跨いでいる音符は切断されます。
長さを0にすると、フレーズを削除します。

フレーズの伸縮


フレーズの貼り付け

フレーズボタンをタップすることで、選択フレーズをコピーして貼り付けます。
フレーズサブボタンかフレーズタブをタップすることで、選択フレーズをコピーして挿入します。
貼り付けた時、既にフレーズがある場合は上書きします。

フレーズの貼り付け


周囲有無判定

フレーズを作成する時、近くにフレーズがあるかを判定します。近くにフレーズがある場合、設定できる長さやリピート回数を制限します。

周囲有無判定

選択フレーズをコピペする際にも、周囲フレーズの有無を判定して、フレーズの上書きや選択解除の制御をします。

周囲有無判定(コピペ時)


フレーズボタン・タブ

ツールに応じてフレーズボタン等のアイコンや処理を変更します。

フレーズボタン・タブ

ツール ボタン サブボタン タブ
ペン フレーズ作成 - フレーズ設定
- - フレーズ内音符
全選択/解除
消しゴム 小節削除 - フレーズ削除
フレーズ
選択時
フレーズ作成
コピーペースト
フレーズ挿入
コピー挿入
フレーズ挿入
コピー挿入



おわりに

フレーズツールはとりあえず実装できました。
描画不具合があると、本当にできているかわかりづらいですが。。

なので次に行く前に、まずはモデル通知機構の見直しをします。
無理やりしようとすれば、ジェスチャ入力時に全てのViewを再描画することでも不具合解消になります。でも、描画コスト的に良くないですね。
描画は最小限に抑えないと音符が多くなったときに動作が重くなりそうなので、この辺りはちゃんと最適化しておきたいところです。
またモデル変更を適切に監視できていると、コストが高いMIDIファイル書き出し処理も最小限に抑えられる等の利点もあります。


【iOS】作曲画面の進捗報告 4

musicLine(iOS)についての進捗。
消しゴムツールの実装が大体完了したので報告します。

消しゴムツールの動作


実装状況
カテゴリタイトル補足
View
Composition
UI配置
ガイド表示
ツールガイド(消しゴム)削除範囲、和音のみの削除範囲
Composition
EraserTool
消しゴムツール
音符の削除
データ構造和音と連符の認識、フレーズから音符を削除する
音符の削除タップで音符を削除
和音・連符の削除和音・連符をタップで和音・連符のみを削除(iOS独自)
音符種別判定タップした音符が和音・連符・ルート音符なのか判定する
音符列の削除スワイプで指定する削除範囲内の複数の音符を削除。
和音列の削除リズム音符をスワイプで複数の和音のみを削除。(iOS独自)
休符に切り替えリズム音符タップで休符に切り替える
PhraseTool
フレーズツール
フレーズの編集


全進捗マップを表示

カテゴリタイトル補足
View
Composition
UI配置
ガイド表示
ピアノ
スクロールエリア
小節番号
小節線
分割線拡大率によって間隔を変化
フレーズフレーズがないところはフレーズ作成ボタンを表示
ツールボタン 選択中のツールはツール色にハイライトする
メロディーライン ペンツールでの音符作成時の入力線
メロディー音符 音符の先頭にチョボをつける
リズム音符 ベース音符のみ下に表示
サンプルデータ 手入力したサンプルデータの音符を配置
連符連符は数字で表示、拡大率によって省略
ツールガイド(ペン)長さ編集しているリズム音符、伸ばした時の削る範囲音符移動時、移動先の音階をわかりやすくする
ツールガイド(指)移動中の音符影、矩形選択の枠
ツールガイド(消しゴム)削除範囲、和音のみの削除範囲
音符音階メロディ音符に音階表示
Community
UI配置
Composition
Common
作曲共通
データ構造座標とサイズ、状態を保持する。レンダラーやコライダー、通知等のロジック
コンバーターDomainのデータ構造へ変換・逆変換
FingerTool
指ツール
音符の編集
データ構造フレーズの音符を取得、基点と移動ベクトルを保持して移動する
音符の移動和音や連符は上下のみに制御
音符の影移動中に操作できているかわかりやすいように(iOS独自)
内外判定音符の移動をフレーズ内に留める
衝突判定移動する音符が他の音符に重なったときの挙動
タップ選択タップした音符の子音符も選択・解除
矩形選択囲った音符を選択
音符の入れ替えリズム音符のスライドで重なった音符と入れ替える(iOS独自)
和音の削除和音を移動した時に重なる時に削除する
連符に切り替えリズム音符タップで切り替え
PenTool
ペンツール
音符の作成
データ構造音符を作成・分割・統合、フレーズへ音符を追加する
音符の作成タップで音符を作成。分割線に合わせて長さを決定
音符列の作成スワイプに沿って複数の音符を作成。音階変わる時に音符分割
有無判定タップやスワイプした縦ラインに既に音符があるか判定
音符の移動音符が既にある場合は、作成ではなく移動。一定距離進むと移動終了
音符の分割リズム音符をタップした時、音符を分割、メロディ線に沿ってY位置を動かす
音符の統合2つのリズム音符の間をタップした場合、音符を統合
音符の伸縮リズム音符を左右にスワイプすることで音符の長さを伸縮、始点で分割なし(仕様変更)
音符の消滅音符の伸縮をした時に長さが0になると削除する(iOS独自)
領域差演算音符伸縮で他の音符に重なる時は差演算して他の音符長さを削る
サンプリング補間素早くスワイプしても音符が移動できるようにサンプリング間を補間する
細かい音符分割線内の細かな音符を移動する挙動
EraserTool
消しゴムツール
音符の削除
データ構造和音と連符の認識、フレーズから音符を削除する
音符の削除タップで音符を削除。
和音・連符の削除和音・連符をタップで和音・連符のみを削除(iOS独自)
音符種別判定タップした音符が和音・連符・ルート音符なのか判定する
音符列の削除スワイプで指定する削除範囲内の複数の音符を削除。
和音列の削除リズム音符をスワイプで複数の和音のみを削除。(iOS独自)
休符に切り替えリズム音符タップで休符に切り替える
PhraseTool
フレーズツール
フレーズの編集
StampTool
スタンプツール
モチーフの編集
Transform
画面移動
座標変換
画面を上下移動ピアノのスワイプにより
画面を左右移動 スクロールエリアのスワイプにより
画面を拡大・縮小ピンチアウト・インにより基点を画面中心に設定する
画面を拡大・縮小(軸指定)長押しからのドラッグ。ピアノでX軸方向、スクロールエリアでY軸方向
MIDI
Common
MIDI共通
データ構造MIDIフォーマットへ出力できる構造
コンバーターCompositionのデータ構造へ変換・逆変換
Commnity
Common
コミュニティ
共通
データ構造カテゴリ等の曲情報、いいねやお気に入り等のリスポンスを保持
Domain
データ構造
Json形式
Melody MelodyTrack, *MelodyPhrase, NoteContainer, NoteBlock, Note
Drum DrumTrack, *DrumPhrase, BeatContainer, Beat
通化 Original, Repeat, Syncの3種のPhraseをジェネリッククラスとプロトコルで抽象化
Service
サービスモデル
SongRederJsonファイルを読み込み、Domainのデータへ変換
SongWriterDomainのデータからJsonファイルへ書き出し
Common
Service
サービスモデル
MidiPlayerMidiファイルを再生する
リポジトリ保存データを管理


消しゴムツールの実装

音符の削除

音符(休符でも可)をタップした時に、音符を削除します。
なお、細かい音符も分割線は関係なく、タップ位置に近い音符を削除します。

音符の削除

Androidの仕様 Androidではタップ位置の縦ラインに音符がある場合でも削除していました。
しかし、誤タップを防止するため、iOSではタップした周辺に音符がない場合は削除しません。ただし、スワイプした場合(音符列の削除)は時間の範囲を指定するため、縦ラインの音符も削除します。


和音・連符の削除
(音符種別判定)

音符をタップした時に、和音や連符等の音符種別を識別します。
音符種別が和音の場合、和音を削除します。
音符種別が連符の場合、音符の長さは維持して連符を削除します。(分割数を減らす)

和音・連符の削除


音符列の削除

メロディ編集エリアを左右にスワイプして、削除範囲を指定します。削除範囲は時間軸のみ指定でき、範囲内の音符を削除します。
なお音符の削除と同様、範囲内の音符種別も判定して、種別に応じた削除操作をします。

音符列の削除


和音列の削除

リズム編集エリアを左右にスワイプすることで複数の和音を削除します。

和音列の削除


休符に切り替え

リズム音符をタップして、休符のON/OFFを切り替えます。
三連符の一部を消音にするような時に使用します。

休符に切り替え



おわりに

消しゴムツールはスムーズに実装できました。
次はフレーズツールの実装ですね。
なんだか苦戦する予感。。


【iOS】作曲画面の進捗報告 3

musicLine(iOS)についての進捗。
ペンツールの実装が大体完了したので報告します。

ペンツールの動作


実装状況
カテゴリタイトル補足
View
Composition
UI配置
ガイド表示
ツールガイド(ペン)リズム音符の長さ編集ガイド、長くした時の削る範囲音符移動時、移動先の音階をわかりやすくする
ツールガイド(消しゴム)削除範囲
Composition
PenTool
ペンツール
音符の作成
データ構造音符を作成・分割・統合する
音符の作成タップで音符を作成。分割線に合わせて長さを決定
音符列の作成スワイプに沿って複数の音符を作成。音階変わる時に音符分割
有無判定タップやスワイプした縦ラインに既に音符があるか判定
音符の移動音符が既にある場合は、作成ではなく移動。一定距離進むと移動終了
音符の分割リズム音符をタップした時、音符を分割、メロディ線に沿ってY位置を動かす
音符の統合2つのリズム音符の間をタップした場合、音符を統合
音符の伸縮リズム音符を左右にスワイプすることで音符の長さを伸縮、始点で分割なし(仕様変更)
音符の消滅音符の伸縮をした時に長さが0になると削除する(iOS独自)
領域差演算音符伸縮で他の音符に重なる時は差演算して他の音符長さを削る
サンプリング補間素早くスワイプしても音符が移動できるようにサンプリング間を補間する
細かい音符分割線内の細かな音符を移動する挙動
EraserTool
消しゴムツール
音符の削除
Transform
画面移動
座標変換
画面を拡大・縮小(軸指定)長押しからのドラッグ。ピアノでX軸方向、スクロールエリアでY軸方向


全進捗マップを表示

カテゴリタイトル補足
View
Composition
UI配置
ガイド表示
ピアノ
スクロールエリア
小節番号
小節線
分割線拡大率によって間隔を変化
フレーズフレーズがないところはフレーズ作成ボタンを表示
ツールボタン 選択中のツールはツール色にハイライトする
メロディーライン ペンツールでの音符作成時の入力線
メロディー音符 音符の先頭にチョボをつける
リズム音符 ベース音符のみ下に表示 画面外に音符が出ると消える不具合を修正
サンプルデータ 手入力したサンプルデータの音符を配置
連符連符は数字で表示、拡大率によって省略
ツールガイド(ペン)長さ編集しているリズム音符、伸ばした時の削る範囲音符移動時、移動先の音階をわかりやすくする
ツールガイド(指)移動中の音符影、矩形選択の枠
ツールガイド(消しゴム)削除範囲
音符音階メロディ音符に音階表示
Community
UI配置
Composition
Common
作曲共通
データ構造座標とサイズ、状態を保持する。レンダラーやコライダー、通知等のロジック
コンバーターDomainのデータ構造へ変換・逆変換
FingerTool
指ツール
音符の編集
データ構造基点と移動ベクトルを保持して移動する
音符の移動和音や連符は上下のみに制御
音符の影移動中に操作できているかわかりやすいように(iOS独自)
内外判定音符の移動をフレーズ内に留める
衝突判定移動する音符が他の音符に重なったときの挙動
タップ選択タップした音符の子音符も選択・解除
矩形選択囲った音符を選択
音符の入れ替えリズム音符のスライドで重なった音符と入れ替える(iOS独自)
和音の削除和音を移動した時に重なる時に削除する
連符に切り替えリズム音符タップで切り替え
PenTool
ペンツール
音符の作成
データ構造音符を作成・分割・統合する
音符の作成タップで音符を作成。分割線に合わせて長さを決定
音符列の作成スワイプに沿って複数の音符を作成。音階変わる時に音符分割
有無判定タップやスワイプした縦ラインに既に音符があるか判定
音符の移動音符が既にある場合は、作成ではなく移動。一定距離進むと移動終了
音符の分割リズム音符をタップした時、音符を分割、メロディ線に沿ってY位置を動かす
音符の統合2つのリズム音符の間をタップした場合、音符を統合
音符の伸縮リズム音符を左右にスワイプすることで音符の長さを伸縮、始点で分割なし(仕様変更)
音符の消滅音符の伸縮をした時に長さが0になると削除する(iOS独自)
領域差演算音符伸縮で他の音符に重なる時は差演算して他の音符長さを削る
サンプリング補間素早くスワイプしても音符が移動できるようにサンプリング間を補間する
細かい音符分割線内の細かな音符を移動する挙動
EraserTool
消しゴムツール
音符の削除
PhraseTool
フレーズツール
フレーズの編集
StampTool
スタンプツール
モチーフの編集
Transform
画面移動
座標変換
画面を上下移動ピアノのスワイプにより
画面を左右移動 スクロールエリアのスワイプにより
画面を拡大・縮小ピンチアウト・インにより基点を画面中心に設定する
画面を拡大・縮小(軸指定)長押しからのドラッグ。ピアノでX軸方向、スクロールエリアでY軸方向
MIDI
Common
MIDI共通
データ構造MIDIフォーマットへ出力できる構造
コンバーターCompositionのデータ構造へ変換・逆変換
Commnity
Common
コミュニティ
共通
データ構造カテゴリ等の曲情報、いいねやお気に入り等のリスポンスを保持
Domain
データ構造
Json形式
Melody MelodyTrack, *MelodyPhrase, NoteContainer, NoteBlock, Note
Drum DrumTrack, *DrumPhrase, BeatContainer, Beat
通化 Original, Repeat, Syncの3種のPhraseをジェネリッククラスとプロトコルで抽象化
Service
サービスモデル
SongRederJsonファイルを読み込み、Domainのデータへ変換
SongWriterDomainのデータからJsonファイルへ書き出し
Common
Service
サービスモデル
MidiPlayerMidiファイルを再生する
リポジトリ保存データを管理


ペンツールの実装

音符の作成・移動

メロディ編集エリアをタップした時に、音符を作成します。タップした縦ラインに既に音符がある時は、その音符の音階を移動します。
なお、音符の長さは分割線に合わせた長さになります。より細かい音符を作成するときは、画面を拡大して分割線のピッチ幅を細かくします。

音符の作成・移動


音符列の作成

メロディ編集エリアをスワイプすると、スワイプ線に沿って複数の音符を作成します。音階が変わるタイミングで音符を分割します。
なお、既に音符がある時は長さは変わらず音階だけ移動します。

音符列の作成


有無判定

タップやスワイプ時、縦ラインに既に音符が存在するか判定します。
音符の有無判定は分割線を基準に(正確には分割線間の縦ラインに存在しているか)判定します。分割線内に細かな音符がある場合も、音符を作成せずに、既存の音符を移動します。

有無判定


音符の分割・統合

リズム音符をタップすることで音符を分割します。
なお、音符を分割するときは、メロディ線に沿って音階を移動します。
また、2つのリズム音符の間をタップすることで音符を統合します。

音符の分割・統合


音符の伸縮・消滅
ツールガイド(編集中のリズム音符)

リズム音符を左右にスワイプすることで音符の長さを変更します。
長さを変更している際、指でリズム音符が隠れないように上部にガイドを表示します。
音符の長さが 0 になるまで短くした時、音符は削除されます。

音符の伸縮・消滅

Androidの仕様 Androidではリズムを上書きするような仕様だったため、スワイプの始点で音符を分割してました。
これは音符長さを調整する仕様ではないため、特に音符の長さを短くする操作ができない課題がありました。
iOSでは仕様を見直し、音符長さの調整を適切にできるように仕様変更しました。
なお、タップで音符を分割してからスワイプすることで、従来のAndroidの仕様と同じような操作となります。


領域差演算
ツールガイド(削る領域)

音符の伸縮で他の音符に重なる時は差演算して、他の音符長さを削ります。

領域差演算

なお削れる領域がわかるように、音符の伸縮操作をしている時に削れる領域をグレーのガイドで表示します。


サンプリング補間

素早くスワイプするとサンプリング間が広くなり、操作が抜けることがあります。
その現象を回避するため、サンプリング間を補間します。

サンプリング補間

スワイプが早くても、またデバイスの動作が重くなっても安定してメロディを作成します。


細かい音符

基本的には分割線を基準に音符を作成したり、音符を操作しますが、分割線内に細かな音符がある場合、分割線に関わらず細かい音符を操作します。

細かい音符


画面を拡大・縮小(軸指定)

スクロールエリアやピアノエリアをロングタップ+スワイプすることで横軸と縦軸を分けた拡大縮小をします。

画面を拡大・縮小(軸指定)

iOSではピンチイン・アウトのジェスチャーで方向までは検知できないようなので、ピンチイン・アウトの場合は軸指定できないようになっています。



おわりに

ペンツールが思ったより苦戦しました。
実は指ツールの細かな不具合等もあり、長引きました。
でも不具合も修正できて、2大ツール(ペン・指)の実装が完了しました!

次は消しゴムツールの実装に取り掛かります。

ハリボテさんインタビュー

mL民のインタビュー企画第6回です。
※mL民:作曲アプリmusicLineユーザーのこと
(「」タグで他も見てね☆)

今回はハリボテさんを紹介します。
DMでのやり取りをインタビュー形式にまとめました。

  


スー

ハリボテさん、よろしくお願いします。

ダラーン


お願いしゃす。


スー

。。毎回その挨拶なんだね

  


それはそうと、今回で第一回インタビュー企画のラストです。
最後はTwitter(今はX)での要望が最も多く寄せられたハリボテさんへのインタビューです。

それではまずユーザーネームの由来を教えてください。

ハリボテ

創作者としてのオリジナル性とは、自分だけの経験に基づいて発達していく物ですが、それらの人生経験や価値観は、あくまでも歴史上において他者が紡いできた物の延長線上にあります。


スー

わあ、いきなりすごい!

ダラーン

あれ、ユーザーネームの由来を聞いてるんだぞ☆


ハリボテ

自分1人でゼロから築き上げた物ではなく、この世界のあらゆる事象が折り重なり、己に影響を与え、その結果今の自分が形成されたのです。

オレは、周りの環境が育ててくれた自分という存在を大切にしていますし、自分の経験や考え方にもちゃんと自信はありますが、傲慢にはなりたくないんですよね ( 巫山戯て傲慢を演じる時はありますが ) 。


スー

なるほど
自分ひとりの力なんて微々たるもので、知らず知らずの内に助けられてるってことですね〜

でもたまに傲慢になっちゃうんだよな

ダラーン

巫山戯る(ふざける)って漢字初めて見た!


ハリボテ

このユーザーネームは自分への戒めです。

オレなどというものは、1人じゃ何も出来ない。何も成し得ない。偶々良い環境が今のオレに色んな面白い要素をペタペタ貼り付けてくれただけで、殆どハリボテのような物です。


ダラーン

ハリボテの由来きたー!!

ここでユーザーネームの由来に繋がるんだね~


ハリボテ

けどそういう意味でも、この世は常に色んな刺激で満ちてるなと思いますし、生きてて楽しいです。

そんな感じですね!


スー

いい名前ですね〜
「ハリボテ」さんってそんな戒めの意味が込められていたのですね!
すごく考えられていてびっくりです!
予想以上に深い名前だった☆

次に、ひとこと自己紹介をお願いします。


ハリボテ

異跡露店という名前で陶芸家をやっています。作曲は趣味です。学生時代は水泳と空手が特技でした。シャトルランとかもまあまあ得意でコンスタントに120回は超えていました。反面、短距離走はめちゃめちゃ苦手で、50メートル10秒位でしたね。高3時点で。

オレは決してスポーツ全般が得意だった訳ではないです。基本的に体も心も超弱くて、小さい頃は沢山入退院を繰り返していました。

けど、なんとか体を頑丈にしていきたいって事で、肺活量が鍛えられそうな水泳、心が鍛えられそうな空手を始め、大泣きしながら少しづつやれるようになっていった感じです。


スー

そうそう
ハリボテさんといえば陶芸家のイメージが強いです

musicLineのアイコンも立派な陶器ですよね~


それよりも、入退院を繰り返されていたなんて大変でしたね!
それで、水泳と空手だったんですねー

前向きな姿勢が偉い。


ダラーン

なんか意外だ
順風満帆な陶芸家の道ではなかったんですね!
体が弱くても頑張ってきたんだな~


ハリボテ

ぶっちゃけやった物だけ得意になっていきましたので、やっていないスポーツは全て苦手です。球技とか全滅してます。ソフトボール投げとか5メートルも飛ばないですね。マジで!

あと読書好きです。小学生の頃とか毎年300冊以上読みまくってて、なんかそれだけで謎の賞状授与されました。今はそんなに読めてないですが。


スー

本を300冊以上読んでいたんですか!
すごすぎ。

ダラーン

読書好きなら負けないぞー

僕は本を買い過ぎて天井まで積み上がったことあるもんね~


スー

なんでダラーンが張り合ってるんだよー

それより本棚にちゃんと収納しなよ

ダラーン

なお、全部読破したかは聞かないで☆


スー

聞いてない聞いてない


ともかく読書はいい趣味ですね~

そんな読書好きなハリボテさんですが、
作曲に興味を持ったきっかけはなんですか?


ハリボテ

自分の周りで鳴っていた音楽にちょびっと嵌っていく過程において、何かの拍子に自分でもこういうのを作れるんじゃないか?と無謀な考えが頭を過ぎるっていう、そんな感じなんですよね。

オレは高校を卒業するまで水泳や空手をやっていた訳ですが、コンマ数秒で県大会を逃したり色々あったんですよ。10年以上もやっていたのでまあまあ体力は付きましたが、記録を残せるレベルにはなれなかったんです。で、その後大学に通い始めて、一旦スポーツはもうええかなって考えてた矢先、折角の一人暮らしだし家でめちゃめちゃ打ち込める何かが欲しいなと思ったんです。

そうして、高校生の頃はバイトもしていたので多少の貯金もあって、勢いで3万円のパソコンと1万円の作曲ソフト ( アカデミック版 ) を買いましたね。

スー

思い切りましたね~
でも打ち込める何かは欲しいですよね!


ハリボテ

音楽の知識は無いです。コードやミックスという言葉すら知りませんし、なんならベースって何?みたいな感じでした。ギターはギリ分かるけど、ベースって意味不明だよね、そもそも何の音が鳴るの?って。

ただ、オレはその頃キングダムハーツを楽しくプレイしてたりしたので、幼少期に遊んだ色んなRPGの記憶も相まって、そんな感じの雰囲気が好きで、何となくDTMを弄り始めました。

勿論、何1つ上手くはいきませんでした。ですから当然、そこへさらなるお金を掛ける気なんかも全く起きません。

でも弄っていれば何かしらの音が鳴るっていう現象そのものが面白くて、全然曲が出来なくても一向に構わなかったような記憶もあります。ぶっちゃけ未知の玩具を与えられた赤子みたいな感じでしたね。1つも意味分からんのですがなんか面白いっていう。

けど流石にめちゃめちゃ安いパソコンだったからか、8年位経った頃に壊れちゃいまして、特にバックアップも取っておらず、曲っぽい物も一切ネットに公開などしておらず、全データが消失しました。

スー

ゼロからのスタートですね~
知識がなくても直感で楽しめる作曲っていいですね☆

そんな矢先に全データ消失してしまうなんて、、、!


ハリボテ

そして、そんな時運良くmLを見付けたんですよ。

因みに音楽の勉強は未だにした事がありません。丸の内進行すら名前だけしか知りません。音楽には色んなコードという物があるらしいと、最近mL民の誰かが言ってたのを見ただけです。


スー

感覚で良い曲が作れるのが羨ましい~

ダラーン

それよりmusicLineを見つけてくれてありがとう!!!

ゲーム音楽の雰囲気が好きで自分でも作ってみたい~でも音楽の知識ないし、PC買うほどでもないかな。。
って思っている人って結構いると思うんだよね!

そんなユーザーにmusicLineを使ってほしい☆

スー

なんかここぞってばかりにPRするね。。

ちゃんと話聞いてる?
ダラーンの悪い部分でてるよ


ダラーン

ぷい


聞いてるもん


スー

。。。(子供か!)

続いて、影響を受けたアーティストはいますか?


ハリボテ

音楽そのものが好きだったというより、あくまでも「 ゲームに付随している 」という前提ありきで音楽って良いよねみたいな感じが始まりだったんですよ。

勿論、その後いつの間にかメタルを聴いたりドラムンベースを聴いたりしていた中で、単体としての音楽も好きになっていったりしたんですが。


スー

ドラムンベースって。。

「ドラムとベース」を使ってる曲の事かな??


ダラーン

ドラムンベースとは、非常に速いテンポの変則的なドラムビートに、うねる様なベースラインが特徴の電子音楽のジャンルの1つです。

シンセサイザーやサンプリングを使用して宇宙的な雰囲気を生み出して、クラブやフェスで人気のダンスミュージックみたい。ですね。



ぷい


スー

。。。


ハリボテ

そういった中で、アーティストを意識した事はマジでありませんでした。好きな曲を1つ見付けたとして、試しに同じアーティストの曲を漁っても他に好きな曲が1つも見付からなかったりというのがめちゃめちゃありまして。

なので、このアーティスト!っていうのは無いです。

それに100%オレの好きな要素で構成されてる曲っていうのも存在しない訳なので、仮に90%良いなと思っても、10%惜しいなと思ってしまう。でも「 そこオレだったらこうするのに……!」っていう感覚も無いんですよね。如何せん音楽の勉強をしていないので、結局オレの好みというのは理論で説明出来ず、非常に漠然とした物をなんとなく体感している事しか出来ないし、それを自分でしっかりと形にする能力も、当然無い。

ただ、もしかして今のオレに影響を与えたんじゃなかろうか?って、漠然と推測できる単体としての曲はめちゃめちゃいくつもあります。

Slipknot「 Eyeless 」
・Zardonic「 Bitter 」
・下村 陽子「 Tension Rising 」
・トーマ「 リベラバビロン 」

それにオレの音楽観というか人生観に影響を与えてるのは決して他者の音楽だけではないです。映画プレデターのこのシーンヤバいなとか、ターミネーター2のラストが良いなあとか、或いはカンディンスキーの数ある抽象画の中のこの絵がなんか好きだなとか、そんな要素もちょいちょい入ってきてますから!


スー

さすがハリボテさんは独特な視点で音楽を聴いているんですね!
確かに、100%好みの曲って自分にしか作れないし、他の曲にはないのかもっていうのは共感できます。
ハリボテさんのルーツを知れて良かったです♪

目標はありますか?


ハリボテ

2つあります。

① 雑誌の制作

ガチめなやつを作ってみたいですね。mL内で起きた出来事や、mL民それぞれの特技を深堀りして面白い特集組めたらなと。

雑誌名こそ「 年間mL 」的な感じにしておきたいですが、その内容は音楽だけじゃなく、音楽外の活動も半分以上盛り込みたい感じあります。

鯖頭さんの電車旅とかおろんさんのプログラミングの事とか、他にもじゃじーさんの居酒屋放浪記など面白そうなテーマは沢山ありますので、ちゃんと頑張れば作れそうです。


ダラーン

雑誌の制作!?

すごいね!!
それは面白そう~
そういう雑誌大好き!

mL民はなにかと個性的で多趣味なので色々な話が聞けそう~♪
楽しみです♪


ハリボテ

ウミイヌさんやwiscaさん、あと回転饅頭さんに読み切りで小説書いてもらうのもアリですね。そうそう、mL民って小説書ける人も何人か居るんですよ!

回転饅頭さんの「 こちら聖mL学園 」や「音路町ストーリー 」シリーズを宜しくお願いします!


ダラーン

小説の執筆!?

カタカタカタ、検索


これね~♪

参照ページこちら聖ML学園(回転饅頭) - カクヨム


参照ページ音路町ストーリー(回転饅頭) - カクヨム


小説書けるってすごいな~
mL民がモデルになっていて面白い!!


スー

あらあらダラーンちゃん

どうしたの?

すねてた癖に~


本のことになると積極的だね





ハリボテ

さて、もし本当に雑誌を作るとして、どんな規模でやるかにもよりますが、実店舗への取材の許可や色んな権利関係などクリアしなきゃならない事が沢山ありそうなので、すぐにこの目標を達成させるのは結構難しそうです。

② 陶製スピーカーの制作

こちらは比較的現実感のある目標です。オレが陶芸家としての能力を発揮するだけですので!

あわよくば公式様とコラボしてmLスピーカーなるものを作ってみたいというような構想もあります。

こちらもどんな規模感でやるのか未知数ですが、取り敢えずオレ自身が少しづつでも動かないとなんにも始まらないですね!


スー

わーすごい!
陶芸家ハリボテさんとぜひコラボしてみたいです。
費用はかかりそうですが、話題になりそうですね☆

では、次にとある1日のタイムスケジュールを教えてください。


ハリボテ

丸一日以上 / 板皿の制作

最終的に36時間ぶっ通しました。食事は予め用意しておいた水と板チョコ10枚程度のみです。

制作した板皿はロンドンのとある店にどうしても出品したい物だったんですけど、基本的には轆轤を挽く業務がメインだった物ですから、どこかで纏まった時間確保して一気に作り上げるしかないなと。


スー

36時間って。。1日以上ですね!

やばすぎです、、、

ダラーン

職人魂だな~


ハリボテ

板皿は管理が難しく、とても歪みやすいので、本当は数日掛けてゆっくり丁寧に制作した方が良いんですけど、色々細かい手段を使えば短時間で仕上げられなくもない。

それで、当初は12時間位でやったろう!と意気込んでたんですが、陶歴3年目にも関わらず、油断と思慮の浅さから大幅に時間を見誤っちゃいましたね。結果、3倍時間掛かりました。しかもホントにその日ちゃんと36時間やらないとゲームオーバーする所でした!

兎に角あの時はヤバ過ぎて、眠気なんか微塵も無く覚醒し続けてましたね。

そしてこれがその完成した板皿です!

3種類ありまして、それぞれ15枚ずつ作りました。サンプルに1枚ずつ手元に残してます。


スー

ということは36時間で45枚作ったんですね。
体を壊さなくてよかったです!
あまり無理しないように頑張ってください~

次はXで募集した質問ですが、
ハリボテさんは破壊神と創造神とわかめの中から一つ選んでなるとしたらどれになりたいですか?


ハリボテ

そんなん、わかめしかないでしょ!!!

結局、なんでも出来てしまう万能性ってのはオレのユーザーネームに反するんですよ。

なんでも破壊出来る、なんでも創造出来る、けどそれってだから何?って感じありますよね。

出来て当たり前の事が出来た所で面白くないですよね。神の万能性を以てする御業なんて、最早1+1みたいな問題を解き続けるだけの作業でしょ、そんなん。

だからオレはわかめになります。


スー

説得力あるような、ないような、、、

ダラーン

いやどんな質問だよ!!

なんで選択肢の中に藻類が入ってんの!

しかもわかめになるんかい!

びっくりしたー
二人とも普通に答えるから思わず突っ込んじゃったよ


ハリボテ

わかめって、思考回路があるかは不明だけど、基本自分で動いたりは出来ないと思うんですよね。つまり何も出来ないんですよ。で、そんな自分に絶望しながら最後は人間に採取されて食べられる。逃げも出来ない。とても嫌ですね。

やっぱ破壊神になります。そして全地球のわかめを破壊して回りますわ!

そして、わかめ共の絶望を存分に堪能し切った後、破壊するしか能の無い己に抗い、何とかして地球上にわかめを復活させるため創造神に弟子入りします。

時代は創造っすよ、やっぱ!


スー

結局創造なんですね☆
わかめは僕も嫌だな〜

ダラーン

僕も嫌だな~
じゃなくて誰でも嫌だよ!

まだこの会話続いてたのー
ついていけないよー

これが、創造と破壊を繰り返す奇人の発想力か。。

スー

では今回もアイコン紹介は最後にして
みなさんに聴いてもらいたい自分のオリジナル曲名を教えてください。


ハリボテ

「 ターニング 」ですね。これはオレの大型企画でして、同じ歌詞で100人がそれぞれ別曲として作曲したらどうなるのか?っていうのが見たくてやりました。


スー

ターニング、面白い企画ですよね。
Xでターニングの話が聞きたいという声も見かけたので、お話聞けて嬉しいです!


ハリボテ

最初は30人位集まってくれたんですが、企画が始動して徐々にターニングが投稿され始めると、段々認知度も上がってきて最後は100人になりました。

で、記念すべき最初の1曲目がこれです。作曲者はムニ/ 62(そると。) さんです。なんと開幕ランキング1位からの殿堂入りを果たしました。伝説の幕開けみたいな感じで、めちゃめちゃ盛り上がりましたね!

ムニ/62(そると。)「 ターニング 」

https://3musicline.com/community/49696 (アプリリンク)

で、今この企画がどうなってるかと言うと、98曲目の_Ashさんを最後に一旦休止中なんです。

ちょっと今色々あって準備中なんですけど、99曲目が投稿されれば100曲目にオレが投稿して終わりです。最後に企画者のオレが締めます。

兎角、これが現状一番最新のターニングです。

_Ash「 ターニング 」

https://3musicline.com/community/104248 (アプリリンク)

これもランキング1位からの殿堂入りしてます。この子はオレの弟子を自称してるめちゃめちゃ変わり者なんです。可愛がってあげて下さい。

あとmLと言えばやっぱり作曲リレーですよね!

作曲リレー「 クオリア

https://3musicline.com/community/60077 (アプリリンク)

弟子の_Ashさんでスタートし、オレがトリを担当しました。全8人が参加した楽曲となっています!


ダラーン

そうそうmLと言えば作曲リレー☆
ハリボテさんわかってる~

作曲リレーは複数人で一つの曲を作るシステムです!
作曲初心者の人もそうでない人も、交流したり協力しながら1つの曲を完成させることでコミュニティが盛り上がるかなと思って始めました~

僕が考えた作曲リレーを紹介してくれて嬉しい☆

スー

クオリア」聴きました!メンバー豪華ですね。
Ashさん、夜湾さん、ずるゐさん、そると。さん、jack=kさん、鯖頭さん、メガニマさん、ハリボテさん、、、みんなよく知ってる方です!


ハリボテ

最後に、私個人のオリジナル曲に関してですが、気になる方は是非アプリをダウンロードしていただき「 レッドプランター 」と検索を掛けてみて下さい!

今日中に投稿しますので、これが現時点での最新曲となります。

ここには載せません!





スー

ガーーーーーーーン

ここは自分の曲を紹介するスペースなんだけど!?

ダラーン

くそー
もうmusicLineをダウンロードするしかないじゃないか☆


スー

またまたー
ダラーンのあからさまなPR。

ハリボテさん、ありがとうございます!
こうなったら僕もダウンロードするしかないな☆

レッドプランター | ハリボテ
https://3musicline.com/community/156075 (アプリリンク)


では最後に描いてくれたアイコンを紹介します。

実はこの企画はハリボテさんの発案でした。
ハリボテさんのインタビューが決まったときにDMを頂き、実現したアイコン企画です。

インタビュー企画の良いアクセントになったと思います。
つきあって頂いた皆様ありがとうございます。



ダラーン

なにがそんなにハリボテさんを動かしているのか。
なぞに前のめりな姿勢がハリボテさんの良いところだね☆


スー

ということで発案者のアイコンはとても気になるところです。

今回も僕の絵を描いて頂きました。

え?


てゆうか誰? 


ダラーン

どうみてもスーくん☆

公式的に大丈夫か聞かれたけどOKしちゃった☆


スー

NG!!!

こわいこわい

それ以上近づかないで


ダラーン

ハリボテさん斬新なアイコンありがとうございます!

スーのアイコンってみんな同じようなデザインにならないか心配してたけど、みんな独自の世界観でスー君を表現してくれて飽きなかったね~♪


スー

確かに。。

そういう点では今回のアイコンもありか?
まあいいか、攻めのデザインでアイコンを書いていただきました~
ハリボテさんありがとうございます!!


というわけで今回は第一回インタビュー企画ラストのハリボテさんのインタビューでした。色々と面白かったです。

回答が長文過ぎてびっくりしましたが、第一回最後ということで特別にノーカットにしてます。
ハリボテさんファンにとって神回になったと思います☆笑
ありがとうございました!

全6弾のインタビューを閲覧してくれた皆様にも感謝です!
まだまだインタビューしたいmL民の方は沢山いますので、今後も反響があればインタビュー企画を再開するかもしれません。
その時はまたご協力よろしくお願いします。

描いていただいた僕の絵(?)はしばらく公式アカウントのアイコンとして使わせて頂きます!


インタビュー インタビュー第2弾へ




ぱるぬんさんインタビュー

mL民のインタビュー企画第5回です。
※mL民:作曲アプリmusicLineユーザーのこと
(「」タグで他も見てね☆)

今回はぱるぬんさんを紹介します。
DMでのやり取りをインタビュー形式にまとめました。

  


スー

ぱるぬんさん、よろしくお願いします。

ダラーン


お願いしゃす。



スー

今日はダラーンも参加するそうです。
(一人で大丈夫なんだけどな、逆に心配。)

改めてお願いします。
それではまず、自己紹介をどうぞ~


ぱるぬん

ぱるぬんです。
趣味はもちろん作曲、 それ以外だと絵を描いたりゲームしたりしています!
お気に入りのゲームはスプラトゥーンです!✌


スー

スプラトゥーンおもしろいですよね〜!
僕もはまっていました。

ダラーン

あ、イカがインク打つやつね!
インクが飛び散る音が癖になるんだよな~


スー

ダラーンは下手くそだったけど、
音を楽しんでたんだな。

ダラーン

そだね~♪

あと、ぱるぬんさんに質問してもいいかな~
ぱるぬんさんのアイコンで気になってるんだけど、

あ、でもこれ失礼かもしれないなあ

いや、大丈夫か。


これ


右上が本体ですか?

スー

どんな質問!?

そういうデザインだから!
どれがぱるぬんさんとかないから!


ぱるぬん

そうです!


スー

いやそうなんかーーい!!


。。そうなんですね。
すみません、お騒がせしました。


ダラーン

聞いてみるもんだね~♪


スー

。。そうだね






では、そんな右上本体のぱるぬんさんに色々と聞いていきます♪
まず影響を受けたアーティストはいますか?


ぱるぬん

ゲームBGM ばかり聞いているのでアーティストに疎いです😭
最も影響を受けたのは、 Twitter上で活動されている終乃オコジョさんという方です。素晴らしい曲を作ります。


スー

いいですね!終乃オコジョさんの曲聴きました~

かっこいいゲーム音楽で素敵です!
思わずフォローしちゃった☆

ダラーン

空気感がたまらないな

ゲーム音楽ってあとで聴いたときに楽しかった記憶が蘇る感じも好きなんだよなあ~

好きなゲーム音楽とかってありますか?


ぱるぬん

色々薦めたい曲があるんですが、 特にポケモン不思議のダンジョンの「幻の大地」 という曲が好きです!
ストーリー終盤のダンジョンで流れる曲で、 寂しくて神秘的な雰囲気がとてもマッチしています。


ダラーン

なるほど。
ぽわぽわと流れる音が心地よく、ダンジョンを進んでいきたくなるような軽快さもありますね♪



それはぷにぷにだねえ。


スー

次はXで募集した質問で、
尊敬する ML民、 影響を受けたML民がいれば教えて欲しいです!


ぱるぬん

Jazzyさんという方です!
コードの組み方や展開のバリエーションが豊かで、聴いていて飽きない曲ばかりです。
もちろん Jazzyさんだけではないです! MLはすごい人だらけです。


スー

Jazzyさんのオシャレな感じが好きです〜!
飽きない工夫が絶妙なのがいいのかな♪

すごい人だらけなの、分かります!
曲を投稿してくれてるmL民やリスナーに感謝!!

ダラーン

ほんとそう

最初は3分くらいの気軽さで作曲できることがコンセプトだったんだけどね〜
何時間も試行錯誤して良い曲を追求している姿勢がすごい!


スー

作曲に興味を持ったきっかけはなんですか?


ぱるぬん

カービィ耳コピ目的で入れたミュージックラインのコミュニティに色んな方のオリジナル 曲があったので、自分も作ってみたいなと思ってはじめました! 3年くらい前のことです。
最初期は1日に2曲とか作って投稿してました...。


スー

きっかけもゲーム音楽だったんですね!
ゲーム音楽がとてもお好きなんですね~

ダラーン

カービィの音楽っていいよね~
一度聴いたら忘れられない。頭に残る音楽ですね♪

スー

それより1日に2曲!?
すごいペースで作ってたんですね〜!


今後の目標はありますか?


ぱるぬん

・曲を作って収入を得る
Twitterのフォロワー数10000人

常に何かしらで褒められる人間になりたいです。


スー

どちらも素晴らしいですね!
mL民の方から作曲がお仕事になる方が生まれたらすごく嬉しいです。

ダラーン

雲の上の存在になっちゃったりして☆

なんだか旅立っていくようで寂しいな~

スー

応援しています!

作曲活動をしていて楽しいと思う時はどんなときですか?


ぱるぬん

基本的には常に楽しさを感じながら曲を作ってますが、スムーズに作業が進んでいる時や、使いまわせそうなコード進行を思いついた時が特に楽しいです!


スー

常に楽しいか~さすがですね♪

やっぱり第一に作曲を楽しむこと、初心を大切にしたいですね☆

次はもう一つXで募集した質問で、
一瞬の内にタイムスリップして西暦3000年に降り立ちました。
さあ、ここはどこ?
これからどうする?


ぱるぬん

↑なんですかこれ💢

図書館に行って、 自分が歴史的な人物になっているか調べに行きます。


スー

まあまあ
そう怒らずに。。

ダラーン

なんこれ💢と言いつつも回答してる優しさ

スー

歴史的な人物になってたら凄いですね~
自分が歴史的な人物として歴史に残っていたら。。
考えても想像できないなー達成感とか感じるんですかね~


では、最後にみなさんに聴いてもらいたい。。

ダラーン

ほらーやっぱり忘れてるじゃないアイコン紹介。

せっかく描いてくれてるんだから~


スー

あ、いっけない☆
それにしても音符をぶん投げるなんてひどいよ。。


今回も素敵なアイコンを描いてくれました!


もういいよーー!!

アイコンを紹介させてくれー


ダラーン

スー君違うよー
もう紹介してるよ

そう、今回の描いてくれたPRアイコンはこれだよ~


スー

なんだぱるぬんさんか。。
ぱるぬんさんひどーーい

でも面白い♪
僕に対するひどい扱いは置いといて、このイラストは好きかも!
音符を投げられてずっこける僕ってありそうなシーンが笑える~
ダラーンの僕に対するひどい扱いが上手に表現できていますね☆


ダラーン

僕はそんなに暴力的じゃないよー
たぶん☆

こういうキャラの関係性がわかるイラストって良いね
てか動きが伝わるように描くって難しいのによく描けてるなあ〜


スー

素敵なイラストありがとうございます!

では気を取り直して、最後にみなさんに聴いてもらいたい自分のオリジナル曲名を教えてください。


ぱるぬん

「分解」


https://3musicline.com/community/141021 (アプリリンク)


と「かくも美しき予告状」


https://3musicline.com/community/112920 (アプリリンク)

です!
後者はMLではKirifudaという名前の別アカウントで投稿しています。
どちらも力作なのでぜひ聴いてほしいです!


スー

今回はぱるぬんさんのインタビューでした。

質問に丁寧にお答えいただき、ありがとうございます!

描いていただいた僕の絵は1週間ほど公式アカウントのアイコンとして使わせて頂きます!


インタビュー インタビュー




描画の高速化(OpenGL VBO)

今回は音符を描画するために使用しているライブラリOpenGLの高速化の話。

描画の高速化(コマ落ち解消)



はじめに

musicLineでは、コミュニティでユーザーが投稿した曲を再生できるようになっており、再生している曲のイメージを可視化できるソングビジュアライゼーションという機能があります。

ソングビジュアライゼーション


Twitter#createbymusiclineタグから引っ張ってきたツイートからソングビジュアライゼーションのテーマを紹介します。
(すみません勝手に使ってます。)

ピアノロール
スペクトラム
バブル
サークル
波形

そして、やばい曲の到来

すみません。やばいという表現は少し失礼ですね。
音符をこれでもかと敷き詰めた物凄い曲を再生する時、ソングビジュアライゼーションの機能の動作がとても重くなる状態です。

音符を敷き詰めた物凄い曲
https://3musicline.com/community/149961 (アプリリンク)


通常の曲であれば、30FPS(1秒間に30フレーム描画)程度で動作しますが、この曲で音符が物凄くあるところは3FPSになります。
3FPSだと1秒間に3フレームしか描画しないので、とてもカクカク動きます。(最低でも10FPSは欲しいところ。。。)

音符が多くなるとカクカクした動きに


今回はソングビジュアライゼーションのテーマの中でもピアノロールを高速化ができそうだったのでしてみました。



描画の高速化

OpenGLでは様々な工程がありますが、その処理をGPUで行うことになります。

OpenGLでの描画の流れについての参考ページ


そのため、アプリ側のCPUから描画する頂点情報(位置、色等)をGPUに送ることになるのですが、この転送時間をいかに減らせるかが高速化の1つのポイントとなります。


VBOへ頂点情報を一気に送る

VBO(Vertex Buffer Object)はGPU側の領域で頂点情報を格納しておく場所です。


現在は1フレーム分の描画に必要な頂点情報収集して、毎回配列でGPUへ転送しています。

毎フレームで1フレーム分の頂点情報を転送


しかし、毎フレームで情報収集とGPU転送の時間が掛かるため、非効率です。特にピアノロールのような画面が左から右へ移り変わるような単純なアニメーションであれば、予め数フレーム分の頂点情報を一気に送ることが有効です。数フレーム分の頂点情報をストックしておいて、あとは毎フレームでスクロール位置のみGPUへ転送すれば、描画するフレーム部分をGPU側で計算できます。

数フレーム分の頂点情報を一気に転送
描画部分をGPU側で計算


つまり、毎フレームCPU側で計算した頂点情報を転送するのではなく、数フレーム分の頂点情報を一気に送ってGPU側で描画部分を計算することで、転送回数を削減することができます。


IBOで重複する頂点情報を省略

IBO(Index Buffer Object)はGPU側の領域で頂点インデックス情報を格納しておく場所です。


例えば音符1つを描画するためには、三角形ポリゴンを2枚使います。頂点が各々3点なので合計6点となります。

三角形ポリゴン2枚で6頂点

しかし、2点は共有する頂点なので2点分の頂点情報が重複します。
通常は頂点配列を転送すると、前から3点ずつをグルーピングして三角形ポリゴンを描画しますが、三角形ポリゴンの頂点を指定することができます。
三角形ポリゴンの構成する頂点インデックス(頂点配列の前からの番号)を指定することで、重複する情報を省略して三角形ポリゴンを描画することができます。

頂点インデックスの使用

重複した無駄な情報を省くことで転送データ量が減り、高速化に繋がります。



実装

AndroidOpenGLのVBOとIBOを使用して、効率良く描画をしてみます。

参考ページ

処理の流れは

  1. バッファを作成
  2. 頂点情報をバッファへ転送
  3. 頂点情報をLocationにバインド
  4. 頂点インデックスを指定して描画

となります。


1. バッファを作成

val bufferIds: IntArray // VBO, IBO の確保領域のIDリスト

bufferIds = IntArray(6).also {
    GLES20.glGenBuffers(6, it, 0)
}

この例では、6個のバッファ(VBO x 5 + IBO)を確保しています。

VBO

  • 座標
  • UV位置
  • 角丸幅
  • 音符のX範囲

IBO

  • 頂点インデックス


ちなみに、使用後必要がなくなったバッファは削除することでメモリを節約します。

GLES20.glDeleteBuffers(6, bufferIds, 0)


2. 頂点情報をバッファへ転送

val vertexBuffer: FloatBuffer = ... // 音符の4頂点座標を集めてFloatBufferに変換する

val floatByte = 4  // floatは4byte
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferIds[0]) // バッファIDの指定

vertexBuffer.position(0) // バッファのポインターを先頭へ
GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertexBuffer.capacity() * floatByte, vertexBuffer, GLES20.GL_STATIC_DRAW) // 頂点を転送

GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0) // バッファIDの解除

この例では、頂点座標を転送しています。
glBindBufferの第一引数にGLES20.GL_ARRAY_BUFFERと指定することで、VBOへ転送します。
glBindBufferの第二引数に確保したバッファIDを指定します。


また、頂点インデックスはGLES20.GL_ELEMENT_ARRAY_BUFFERと指定して、IBOへ転送します。

val indexBuffer: IntBuffer = ... // 頂点インデックスを計算してIntBufferに変換する

val intByte = 4
GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, bufferIds[5]) // バッファIDの指定

indexBuffer.position(0)
GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBuffer.capacity() * intByte, indexBuffer, GLES20.GL_STATIC_DRAW) // 頂点インデックスを転送

GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0) // バッファIDの解除


3. 頂点情報をLocationにバインド

GLES20.glEnableVertexAttribArray(noteVtPosLoc) // Location有効
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferIds[0]) // バッファIDの指定

GLES20.glVertexAttribPointer(noteVtPosLoc, 2, GLES20.GL_FLOAT, false, 0, 0) // 頂点情報をLocationにバインド

GLES20.glDisableVertexAttribArray(noteVtPosLoc) // Location解除
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0) // バッファIDの解除

2. と同様にglBindBufferでバッファIDを指定し、glVertexAttribPointerでShaderで使用する変数のLocationにバインドします。


4. 頂点インデックスを指定して描画

var faceIndexesBufferCount = // 頂点インデックスの数

GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, bufferIds[5]) // バッファIDの指定

GLES20.glDrawElements(GLES20.GL_TRIANGLES, faceIndexesBufferCount, GLES20.GL_UNSIGNED_INT, 0) // 描画

GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0) // バッファIDの解除

2. と同様にglBindBufferでバッファIDを指定し、glDrawElementsの第四引数を0に設定することで頂点インデックスを指定して描画します。



全コードを表示

class MidiNotesShader() {

    // region Property

    // Location
    private val noteVtPosLoc: Int // 頂点位置
    private val noteVtUvLoc: Int // UV位置
    private val noteColorLoc: Int // 頂点色
    private val noteRoundWidthLoc: Int // 角丸幅
    private val noteRangeLoc: Int // 音符のX範囲
    private val scrollLoc: Int  // スクロール
    private val barPosLoc: Int  // 再生バー位置
    // endregion

    private val bufferIds: IntArray // VBO, IBO の確保領域のIDリスト

    private var vertexBuffer: FloatBuffer = toFloatBufferf(listOf())
    private var uvBuffer: FloatBuffer = toFloatBufferf(listOf())
    private var colorBuffer: FloatBuffer = toFloatBufferf(listOf())
    private var roundWidthBuffer: FloatBuffer = toFloatBufferf(listOf())
    private var rangeBuffer: FloatBuffer = toFloatBufferf(listOf())

    private var faceIndexes = listOf<Int>()
    private var faceIndexesBufferCount = 0

    private val shaderProgramId: Int = GLES20.glCreateProgram()
    private val program = Program()
    private var vertexShader: Int? = null
    private var fragmentShader: Int? = null
    // endregion

    // region Initializer
    init {
        loadProgram()

        // VBO, IBO のバッファ領域確保
        bufferIds = IntArray(6).also {
            GLES20.glGenBuffers(6, it, 0)
        }

        // 頂点情報
        noteVtPosLoc = GLES20.glGetAttribLocation(shaderProgramId, program.vtPosAttr)
        noteColorLoc = GLES20.glGetAttribLocation(shaderProgramId, program.colorAttr)
        noteRoundWidthLoc = GLES20.glGetAttribLocation(shaderProgramId, program.roundWithAttr)
        noteRangeLoc = GLES20.glGetAttribLocation(shaderProgramId, program.rangeAttr)
        noteVtUvLoc = GLES20.glGetAttribLocation(shaderProgramId, program.vtUvAttr)

        // Uniform
        scrollLoc = GLES20.glGetUniformLocation(shaderProgramId, program.scrollUni)
        barPosLoc = GLES20.glGetUniformLocation(shaderProgramId, program.barPosUni)
    }

    fun dispose() {
        GLES20.glDeleteBuffers(6, bufferIds, 0)
        deleteProgram()
    }
    // endregion

    // region Method

    // 数フレーム分必要な音符の情報を計算する
    fun calcBufferData(frameCount: Int) {

        // 数フレーム分の音符を取得する
        val screenNotes = getNotesInScreen(frameCount)

        // 音符の頂点を集める
        vertexBuffer = screenNotes.getNotePointsBuffer()

        // 音符のUV座標を集める
        uvBuffer = screenNotes.getUVsBuffer()

        // 音符の色を集める
        colorBuffer = screenNotes.getColorsBuffer()

        // 音符の角丸の幅を集める
        roundWidthBuffer = screenNotes.getRoundWidthsBuffer()

        // 音符のX範囲を集める
        rangeBuffer = screenNotes.getNoteRangesBuffer()

        // 頂点インデックスを計算する
        val indexes = (0 until screenNotes.size * 4).groupBy { it / 4 }.values.flatMap { (a, b, c, d) ->
            listOf(a, b, c, d, c, b)
        }
        faceIndexes = indexes
    }

    // BufferDataをVBO, IBOへ転送する
    fun sendBufferData() {

        val floatByte = 4

        // 音符の頂点を転送
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferIds[0])
        vertexBuffer.position(0)
        GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertexBuffer.capacity() * floatByte, vertexBuffer, GLES20.GL_STATIC_DRAW)

        // 音符のUV座標を転送
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferIds[1])
        uvBuffer.position(0)
        GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, uvBuffer.capacity() * floatByte, uvBuffer, GLES20.GL_STATIC_DRAW)

        // 音符の色を転送
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferIds[2])
        colorBuffer.position(0)
        GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, colorBuffer.capacity() * floatByte, colorBuffer, GLES20.GL_STATIC_DRAW)

        // 音符の角丸の幅を転送
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferIds[3])
        roundWidthBuffer.position(0)
        GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, roundWidthBuffer.capacity() * floatByte, roundWidthBuffer, GLES20.GL_STATIC_DRAW)

        // 音符のX範囲を転送
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferIds[4])
        rangeBuffer.position(0)
        GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, rangeBuffer.capacity() * floatByte, rangeBuffer, GLES20.GL_STATIC_DRAW)

        // 頂点インデックスを転送
        val indexBuffer = toIntBuffers(faceIndexes)
        val intByte = 4
        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, bufferIds[5])
        indexBuffer.position(0)
        GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBuffer.capacity() * intByte, indexBuffer, GLES20.GL_STATIC_DRAW)
        faceIndexesBufferCount = faceIndexes.size

        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0)
        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0)
    }

    fun draw() {

        // GLSL設定
        GLES20.glUseProgram(shaderProgramId)

        // 機能有効
        GLES20.glEnable(GLES20.GL_BLEND)
        GLES20.glDisable(GLES20.GL_DEPTH_TEST)

        // 機能設定
        GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_DST_COLOR)

        // 変数有効
        GLES20.glEnableVertexAttribArray(noteVtPosLoc)
        GLES20.glEnableVertexAttribArray(noteVtUvLoc)
        GLES20.glEnableVertexAttribArray(noteColorLoc)
        GLES20.glEnableVertexAttribArray(noteRoundWidthLoc)
        GLES20.glEnableVertexAttribArray(noteRangeLoc)

        // 変数設定
        val crossTime = 6_000 // 6秒で画面を横切る
        val playBarPosition = 0.25 // 0(左端) ~ 1(右端)
        val barPos = playBarPosition * 2f - 1f // -1 ~ 1
        GLES20.glUniform1f(scrollLoc, time / crossTime * 2f - barPos)

        GLES20.glUniform1f(barPosLoc, barPos)

        // 音符の頂点をnoteVtPosLocにバインド
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferIds[0])
        GLES20.glVertexAttribPointer(noteVtPosLoc, 2, GLES20.GL_FLOAT, false, 0, 0)

        // 音符のUVをnoteVtUvLocにバインド
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferIds[1])
        GLES20.glVertexAttribPointer(noteVtUvLoc, 2, GLES20.GL_FLOAT, false, 0, 0)

        // 音符の色をnoteColorLocにバインド
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferIds[2])
        GLES20.glVertexAttribPointer(noteColorLoc, 4, GLES20.GL_FLOAT, false, 0, 0)

        // 音符の角丸幅をnoteRoundWidthLocにバインド
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferIds[3])
        GLES20.glVertexAttribPointer(noteRoundWidthLoc, 1, GLES20.GL_FLOAT, false, 0, 0)

        // 音符のX範囲をnoteRangeLocにバインド
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferIds[4])
        GLES20.glVertexAttribPointer(noteRangeLoc, 2, GLES20.GL_FLOAT, false, 0, 0)

        // 頂点インデックスをバインド
        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, bufferIds[5])
        // 描画
        GLES20.glDrawElements(GLES20.GL_TRIANGLES, faceIndexesBufferCount, GLES20.GL_UNSIGNED_INT, 0)


        // 変数解除
        GLES20.glDisableVertexAttribArray(noteVtPosLoc)
        GLES20.glDisableVertexAttribArray(noteVtUvLoc)
        GLES20.glDisableVertexAttribArray(noteColorLoc)
        GLES20.glDisableVertexAttribArray(noteRoundWidthLoc)
        GLES20.glDisableVertexAttribArray(noteRangeLoc)

        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0)
        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0)

        // 機能解除
        GLES20.glDisable(GLES20.GL_BLEND)
    }



    // Shaderプログラムを読み込む
    private fun loadProgram() {
        val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, program.vertexCode).also {
            vertexShader = it
        }
        val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, program.fragmentCode).also {
            fragmentShader = it
        }

        GLES20.glAttachShader(shaderProgramId, vertexShader)
        GLES20.glAttachShader(shaderProgramId, fragmentShader)
        GLES20.glLinkProgram(shaderProgramId)
    }

    // Shaderプログラムを削除する
    private fun deleteProgram() {
        GLES20.glDeleteProgram(shaderProgramId)
        vertexShader?.let { GLES20.glDetachShader(shaderProgramId, it) }
        fragmentShader?.let { GLES20.glDetachShader(shaderProgramId, it) }

        GLES20.glDeleteShader(GLES20.GL_VERTEX_SHADER)
        GLES20.glDeleteShader(GLES20.GL_FRAGMENT_SHADER)
    }

    // endregion

    // region InnerClass
    class Program {

        // region Property

        // uniform
        val scrollUni = "u_scroll"
        val barPosUni = "u_barPos"

        // attribute
        val vtPosAttr = "v_pos"
        val colorAttr = "v_color"
        val roundWithAttr = "v_round_width"
        val vtUvAttr = "v_uv"
        val rangeAttr = "v_range"

        // varying
        private val colorVary = "f_color"
        private val roundWithVary = "f_round"
        private val vtUvVary = "f_uv"
        private val bright = "f_bright"
        // endregion

        // region Code
        // uniform: プリミティブごとの情報(描画呼び出し全体で一定)
        // attribute: 頂点毎の情報(通常:位置、法線、色、UVなど)
        // varying: フラグメント(ピクセル)ごとの情報。頂点間で値が補完される(Vertexの入力→Fragmentで補間された出力: 1頂点=>多ピクセル)
        val vertexCode = """
            attribute vec2 $vtPosAttr;
            attribute vec4 $colorAttr;
            attribute float $roundWithAttr;
            attribute vec2 $vtUvAttr;
            attribute vec2 $rangeAttr;
            
            varying vec4 $colorVary;
            varying vec2 $vtUvVary;
            varying float $roundWithVary;
            varying float $bright;
            
            uniform float $scrollUni;
            uniform float $barPosUni;
            
            void main() {
                gl_Position = vec4($vtPosAttr - vec2($scrollUni, 0.), 0.0, 1.0);
                
                $colorVary = $colorAttr;
                $roundWithVary = $roundWithAttr;
                $vtUvVary = $vtUvAttr;
                
                float start = $rangeAttr.x;
                float end = $rangeAttr.y;
                // noteの左がスクロールバーの左の時(鳴り終わり)
                float barPos = $barPosUni + $scrollUni;
                if (end < barPos) {
                    float lengthTime = end - start; // endからの経過時間
                    float power = clamp((1.3 - lengthTime) / 1.3, 0., 1.); //1~0 4秒で0
                    float endBright = power * 0.7;

                    float elapsedScroll = barPos - end; // endからの経過時間
                    float decay = clamp(elapsedScroll / 0.2, 0., 1.); //0=1 0.5秒で1 減衰
                    
                    $bright = clamp(endBright - decay, 0., 1.);
                 } else if(barPos < start) {
                    // まだ鳴ってない
                    $bright = 0.;
                 } else {
                    // 鳴っている
                    float elapsedScroll = barPos - start;
                    float power = clamp((1.3 - elapsedScroll) / 1.3, 0., 1.); //1~0 4秒で0
                    $bright = power * 0.7;
                 }
            }
        """.trimIndent()

        val fragmentCode = """
            precision mediump float;
            varying vec4 $colorVary;
            varying float $roundWithVary;
            varying vec2 $vtUvVary;
            varying float $bright;
            
            void main() {

                vec2 pos = $vtUvVary-vec2(0.5);
                float s = -0.5 + $roundWithVary;
                float e = 0.5 - $roundWithVary;
                
                // 丸角にする
                float dis0 = length(vec2((pos.x - s) * (0.5 / $roundWithVary), pos.y));
                float dis1 = length(vec2((pos.x - e) * (0.5 / $roundWithVary), pos.y));
                if((0.5 < dis0 && pos.x < s) || (0.5 < dis1 && e < pos.x)) discard;
                      
                gl_FragColor = vec4(mix($colorVary.rgb, vec3(1.), $bright), $colorVary.a);
            }
        """.trimIndent()

        // endregion
    }
    // endregion
}



おわりに

今回はVBOとIBOを用いてOpenGLの高速化を行いました。

改善後の動作

ご覧のように音符が物凄く増えても30FPSを維持し、スムーズな動作になりました。
ピアノロール以外のテーマでも高速化できそうであれば行うかもしれません。

ちなみに作曲画面で物凄く音符がある時は、ソロボタンや画面を拡大して音符の描画量を減らすことで遅延を少なく画面移動できます。
それでも、音符が物凄く多いと少し編集するだけでも動作が重いと思いますが。。。(すみません)

とにかく作者の執念を感じられる作品でした。