musicLineアプリ開発日記

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

Observation (iOS17+) の注意点

今回は監視フレームワーク・通知機構のObservationの話。
Observationを使用してメモリが圧迫する状況があったので調査しました。

Observationでメモリが圧迫



はじめに

musicLineではMIDI楽譜を編集するためのデータ構造として、トラックやフレーズ、音符、座標というように階層的にモデルを構築しています。
例えば、曲データは複数のトラック、トラックは複数のフレーズを保持しているような構造です。

https://cdn-ak.f.st-hatena.com/images/fotolife/m/musicline_developer/20230513/20230513152016.png
データ階層

詳しくはこちら


このような階層的なデータ構造だと、Viewへ変更通知を送る場合に監視は自身のプロパティだけでなく、下層モデルのプロパティまで広がるため、適切な監視体制が求められます。


Combineの問題点

CombineのObservableObjectでは@Publishedを付与したプロパティが監視対象となりますが、変更通知はインスタンスを保持している上層にしか届きません。
そのため、Viewの層(@ObservedObject@StateObject)まで通知を届けるとなると、親のモデルもObservableObjectにして、通知を繋ぎ込む(リレー的に監視する)必要がありました。

// 子を監視して、子からの変更通知があれば上へ流す
counter.objectWillChange.sink{
    self.objectWillChange.send()
}.store(in: &anyCancellable)

詳しくはこちら


Observationで解決?

iOS17でObservationを使えるようになりました。
CombineのObservableObjectはクラス毎に変更を監視することに対し、Observationの@Observableではプロパティ毎に監視しています。
そのためどれだけModel階層が深くても、Viewでプロパティを使用すれば通知が届くようになりました。

つまり、Combineのように通知を繋ぎ込むことなくViewの層まで変更通知を届けられるため、中間の煩雑なコードが削減できます。

qiita.com

またmusicLineでは、音符やフレーズの追加・削除が頻繁にされるため、その度に繋ぎこむ処理が必要なので監視対象の管理も大変でした。
この動的な監視対象の管理問題もObservationにより解決できましたが、新たな問題がありました。


Observationの問題点

@Observableにより末端モデルの音符を監視します。
移動した音符は黄色になっており、通知が適切にViewに届くことを確認できます。
しかしスクロールするたびに、メモリが増え続ける問題がありました。
試しに、1トラック300フレーズの中に音符約1000個を敷き詰めてメモリ量を確認しました。

Observationでメモリ使用量が増加

スクロールで画面を再描画する度にメモリが上がり続けることがわかります。iPhoneのメモリは8GBとか16GBなので、これは見逃せる増加量ではないですね。

そもそも、音符を配置しただけでメモリが1GBを超えているのも問題です。
ここまで音符を敷き詰めることは稀ではありますが、メモリはなるべく少なく抑えたいところです。

音符は4個のプロパティ(位置とサイズ等)があるため、120万(300 x 1000 x 4)のプロパティを監視している計算になります。
この時点で120万のプロパティを全て監視しようとする使い方が間違えているなあという感じはしました。


そして、Observationの挙動調査へ

musicLineの音符のように膨大なプロパティを監視する状況では、Observationの使用は不適切だと認識しました。(メモリ増加問題を除いても)
しかし他の場面(コミュニティの曲リスト等)で使用できるか確認したかったので、どういうパターンの時にメモリ増加問題が起こるかを調査しました。

なおmusicLineのモデル実装が悪い可能性があるので、Observationの最小限サンプルを作成しました。




Observationの挙動調査

調査では通知を上層へ繋ぐことを想定して、MVVMでCounterを実装しました。ただしメモリ増加がわかりやすくなるように、1000個のCounterViewをリストで表示しています。
また比較対象にCombineでも同様に実装しました。

CounterView

CounterViewのリスト


メモリが安定するパターン

Observation(上)とCombine(下)

30MBから始まり、下へスクロールすることで45MBまで増加しますが、その後は何度かスクロールを繰り返しても50MB前後で安定しました。
最初の下スクロールでメモリ増加する理由は、Listで表示しているので最初のスクロールでCounterViewCounterViewModelを作成していると思われます。
その後はCounterViewの作成は行われないためメモリが安定します。


Observationの実装

View

struct Page: View {
    let counters = (0..<1000).map{ _ in Counter() }
    var body: some View {
        List(0..<1000) { num in
            let counter = counters[num]
            CounterView(label: "Counter\(num)", viewModel: .init(counter: counter))
        }
    }
}
struct CounterView: View{
    let label: String
    let viewModel: CounterViewModel
    
    var body: some View {
        HStack {
            Text("\(label):")
            Text("\(viewModel.count)")
            Button("+") { viewModel.increment() }
        }
    }
}

ViewModel

class CounterViewModel {
    var count: Int {
        counter.count
    }
    private let counter: Counter

    init(counter: Counter) {
        self.counter = counter
    }
    
    func increment() {
        counter.increment()
    }
}

Model

@Observable
class Counter {
    var count = 0

    func increment() {
        self.count += 1
    }
}


Combineの実装

View

struct CounterView: View{
    ...
    @ObservedObject var viewModel: CounterViewModel
    ...
}

ViewModel

class CounterViewModel: ObservableObject {
    ...
    private var anyCancellable = Set<AnyCancellable>()
    
    init(counter: Counter) {
        self.counter = counter
        counter.objectWillChange.sink{
            self.objectWillChange.send()
        }.store(in: &anyCancellable)
    }
}

Model

class Counter: ObservableObject {
    @Published var count = 0
    ...
}



メモリ増加になるパターン

前のパターンでは下スクロール後はCounterViewの作成も再描画も行われませんでした。
次はCounterViewの再描画が頻繁に行われるパターンで検証してみます。
今回はGeometryReaderにより、スクロールに応じてアニメーションを付けることで、頻繁に再描画する状況を作りました。

struct CounterView: View{
    let label: String
    let viewModel: CounterViewModel
    
    var body: some View {
        GeometryReader{ geometry in
            let shift = sin(geometry.frame(in: .global).minY / geometry.size.height) * 10
            let x = geometry.size.width * 0.5 - shift
            let y = geometry.size.height * 0.5
                HStack {
                    Text("\(label):")
                    Text("\(viewModel.count)")
                    Button("+") { viewModel.increment() }
                }
                .position(x: x, y: y)
    }
}

Observation(上)とCombine(下)

そうすると、Observation実装の方ではスクロールごとにメモリがどんどん使われることがわかります。どうやらObservableを付与したプロパティのGetterへ頻繁にアクセスすることが原因のようです。

追加検証

試しに、Getterのアクセス(@Observableのaccessの呼び出し)を制限してみます。

@Observable
class Counter {
    var count: Int{
        get {
            if !passingAccess{
                passingAccess = true
                access(keyPath: \._count )
            }
            return _count
        }
        
        set {
            passingAccess = false
            withMutation(keyPath: \._count ) {
                _count  = newValue
            }
        }
    }

    @ObservationIgnored
    var _count = 0

    @ObservationIgnored
    var passingAccess = false
}

accessの呼び出しを制限してメモリ増加を抑える

そうすると、メモリ増加が収まりました。
ただし、この方法では適切な通知はできないようです。

スクロール後の通知はされない



Observation注意点のまとめ

GeometryReaderで座標によりアニメーションするような場合はObservationによるプロパティ監視は控えた方がいいです。そのような場合はViewの再描画によりGetterに頻繁にアクセスしますが、それがメモリ増加が止まらない問題に繋がっていると考えられます。
ObservationではプロパティのGetterで監視イベントを登録するような仕組みになっているため、このような現象が起こるかもしれません。

そもそもSwiftUIでは必要のないViewの再描画を極力抑える思想なので、再描画が頻繁に行われる設計が問題なのかもしれません。
でもスクロールによるアニメーション等で再描画が頻繁に行われるケースは結構ありそうな気もします。




監視フレームワークを再考

Observationの問題がわかりましたが、結局musicLineの音符のように膨大なプロパティを監視するための適切なフレームワークはどうするべきでしょう。もう一度Combineを使う方針に戻って考えてみます。

Combineでは次の観点で問題になることがあります。

  • メモリの圧迫
  • 監視対象の管理が大変


これは特に

  • モデル階層が深い
  • 監視するプロパティが膨大
  • 監視対象が動的に変化

といった特徴のときに起こります。


EventBus

Combineは膨大なモデルを全てObservableObjectとして監視するため、メモリが圧迫します。さらに、階層が深くなることで通知を繋ぎこむ処理が必要になります。

この問題を解消するためにObservableObjectは1個のオブジェクトのみにしました。つまり監視するオブジェクトはEventBusのみにして、EventBusを仲介して通知を送ります。

greenrobot.org

EventBusを介して通知

中間の通知を繋ぎこむコードがなくなり、メモリもCombineの実装より3MBほど軽減されました。

ただし、EventBusだと不必要なViewを再描画してしまう欠点があります。EventBusは監視を1箇所にまとめるため、カウントアップしたCounterViewのみを再描画することはできず、再描画するときは必ず画面全体になります。またEventBusは通知を飛ばせるので便利ですが、基本的にはモデルの構造を無視するシングルトンみたいなものなので、あまり多用すると処理が追えなくなるのも注意です。


実装

EventBusの実装を単純化するためにシングルトンでDIしています。

View

struct Page: View {
    ...
    @StateObject private var eventBus = EventBus.shared
    ...
}

Model

class Counter {
    var count = 0 {
        willSet {
            eventBus.send()
        }
    }
    private let eventBus = EventBus.shared
}

class EventBus: ObservableObject {
    static let shared = EventBus()
    
    func send(){
        objectWillChange.send()
    }
}


なおサンプルでは簡易的な実装ですが、Eventの種類やプロパティを送れるようにSwiftで実装しているライブラリがありました。

swiftpackageindex.com


検証用のサンプル全コードを表示

import SwiftUI

struct Page_Observation: View {
    let counters = (0..<1000).map{ _ in Counter_Observation() }
    var body: some View {
        content(CounterView_Observation.self, counters: counters)
    }
}

struct Page_Combine: View {
    let counters = (0..<1000).map{ _ in Counter_Combine() }
    var body: some View {
        content(CounterView_Combine.self, counters: counters)
    }
}

struct Page_EventBus: View {
    let counters = (0..<1000).map{ _ in Counter_EventBus() }
    @StateObject private var eventBus = EventBus.shared
    
    var body: some View {
        content(CounterView_EventBus.self, counters: counters)
    }
}

fileprivate func content<TCounterView: CounterView, TCounter: Counter>( _: TCounterView.Type, counters: [TCounter]) -> some View where  TCounter == TCounterView.TViewModel.TCounter {
    List(0..<1000) { num in
        let counter = counters[num]
        TCounterView(label: "Counter\(num)", viewModel: .init(counter: counter))
            .buttonStyle(.borderless)
    }
}
import SwiftUI

protocol CounterView: View {
    associatedtype TViewModel: CounterViewModel

    var label: String { get }
    var viewModel: TViewModel { get }
    
    init(label: String, viewModel: TViewModel)
}

struct CounterView_Observation: CounterView{
    let label: String
    let viewModel: CounterViewModel_Observation
    
    var body: some View {
        animationContent(label: label, viewModel: viewModel)
    }
}

struct CounterView_Combine: CounterView{
    let label: String
    @ObservedObject var viewModel: CounterViewModel_Combine
    
    var body: some View {
        animationContent(label: label, viewModel: viewModel)
    }
}

struct CounterView_EventBus: CounterView{
    let label: String
    let viewModel: CounterViewModel_EventBus
    
    var body: some View {
        animationContent(label: label, viewModel: viewModel)
    }
}

fileprivate func animationContent(label: String, viewModel: some CounterViewModel) -> some View{
    GeometryReader{ geometry in
        let shift = sin(geometry.frame(in: .global).minY / geometry.size.height) * 10
        let x = geometry.size.width * 0.5 - shift
        let y = geometry.size.height * 0.5
        content(label: label, viewModel: viewModel)
            .font(.body.monospacedDigit())
            .position(x: x, y: y)
    }
}

fileprivate func content(label: String, viewModel: some CounterViewModel) -> some View{
    HStack {
        Text("\(label):")
        Button("-") { viewModel.decrement() }
        Text("\(viewModel.count)")
        Button("+") { viewModel.increment() }
    }.font(.body.monospacedDigit())
}
import SwiftUI
import Combine

protocol CounterViewModel {
    associatedtype TCounter: Counter
    
    var counter: TCounter { get }
    
    init(counter: TCounter)
}

extension CounterViewModel {
    
    var count: Int {
        counter.count
    }
    
    func increment() {
        counter.increment()
    }
    func decrement() {
        counter.decrement()
    }
}

class CounterViewModel_Observation: CounterViewModel {
    
    let counter: Counter_Observation
    
    required init(counter: Counter_Observation) {
        self.counter = counter
    }
}

class CounterViewModel_Combine: ObservableObject, CounterViewModel {

    let counter: Counter_Combine
    private var anyCancellable = Set<AnyCancellable>()
    
    required init(counter: Counter_Combine) {
        self.counter = counter
        counter.objectWillChange.sink{
            self.objectWillChange.send()
        }.store(in: &anyCancellable)
    }
}

class CounterViewModel_EventBus: CounterViewModel {
    
    let counter: Counter_EventBus
    
    required init(counter: Counter_EventBus) {
        self.counter = counter
    }
}
import Foundation

protocol Counter: AnyObject {
    var count: Int { get set }
}

extension Counter {
    func increment() {
        self.count += 1
    }

    func decrement() {
        self.count -= 1
    }
}

@Observable
class Counter_Observation: Counter {
    var count = 0
}

class Counter_Combine: ObservableObject, Counter {
    @Published var count = 0
}

class Counter_EventBus: Counter {
    var count = 0 {
        willSet {
            eventBus.send()
        }
    }
    private let eventBus = EventBus.shared
}


class EventBus: ObservableObject {
    static let shared = EventBus()
    
    private init() {}
    
    func send(){
        objectWillChange.send()
    }
}




おわりに

今回はObservationの挙動について調査して注意点をまとめました。
Observationは監視フレームワークとして便利ですが、現時点ではGetterに頻繁にアクセスするような場合はメモリ増加が止まらない問題があるようです。
この問題を除いても、監視対象が膨大にある状況では全てプロパティを監視することになるのでメモリ圧迫に繋がります。 その場合は、従来通りCombineやEventBusのような方法を考えるといいと思います。

今回の結果を踏まえて、musicLineにEventBusの仕組みで通知機構を組み込んだのがこちら

改善後のメモリ

なんということでしょう。
1GB以上使用していたメモリが100MB以下に抑えられています。
フレームワークは慎重に選ばないと大変なことになることがわかりました。
ともかく問題が解決してよかった。



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週間ほど公式アカウントのアイコンとして使わせて頂きます!


次へ