図形の更新機能を追加する【実践的Macintoshプログラミング解説】

印刷用表示 |テキストサイズ 小 |中 |大 |

CoreData版 Repeating Motif Generator の開発 Repeating Motif Wonderland CoreData 実践的 Macintosh プログラミング解説

LinkIconホーム

更新日 2009-05-24

原図とミキサーのモデルを作る

図形の更新機能を追加する

synthesizeMotif.png Oscillatorの波形を元にして原図を更新する機能を追加します。

 Mixerは各発振器の出力波形を合成して出力する事が役割となります。合成といっても、単純に足し算をしているだけです。右の図で、三つの波形がつながった先に表示されているのがミキサーの波形です。

 MasterMotifは各ミキサーの出力をそれぞれ縦軸の値、横軸の値として線を繋ぎあわせ、図形を作り出します。これは第三のプロジェクトでLissajousがやっていたのと全く同じ事をやっているだけです。

 発振器の数が一つから三つに増える事で、リサージュ図形よりも多様な図形を作り出せる様になりました。

Mixerの波形更新メソッド

 波形を更新するメソッドupdateBezierPathをMixerに追加します。 各発振器出力を取り出して、Y軸の値を加算したものがMixerの出力である合成波形となります。でき上がった図形をsetBezierPath:でセットして終了です。

 コメントにも書きましたが、各発振器の出力波形が先に更新されている事が前提となっています。

 各発振器の出力波形に含まれるエレメントの数が一致する事をチェックしていますが、これをやらないとどのような不具合が発生するかについては【失敗例】誰が出しているエラーメッセージなの?に書きましたので、そちらを参照して下さい。

// 
//  Mixer.m
//

#pragma mark -
#pragma mark Update path

//  OscillatorのbezierPathが先に更新されている事
- (void)updateBezierPath
{
    int             i;
    NSPoint         p1,p2,p3;
    NSBezierPath    *path = [NSBezierPath bezierPath];
    NSBezierPath    *waveform1 = [[self osc1] bezierPath];
    NSBezierPath    *waveform2 = [[self osc2] bezierPath];
    NSBezierPath    *waveform3 = [[self osc3] bezierPath];

    if([waveform1 elementCount] == [waveform2 elementCount] && 
       [waveform2 elementCount] == [waveform3 elementCount])
    {
        for(i=0;i<[[[self masterMotif] resolution] intValue];i++)
        {
            [waveform1 elementAtIndex:i associatedPoints:&p1];
            [waveform2 elementAtIndex:i associatedPoints:&p2];
            [waveform3 elementAtIndex:i associatedPoints:&p3];
            if(i == 0)
                [path moveToPoint:NSMakePoint(i,p1.y+p2.y+p3.y)];
            else
                [path lineToPoint:NSMakePoint(i,p1.y+p2.y+p3.y)];
        }
        [self setBezierPath:path];
    }
}

発振器を更新して合成波形を更新する

 上記のメソッドは各発振器の出力波形が先に更新されている事が前提ですが、各発振器を更新して上記のメソッドを呼びだす事も必要になるので、そのためのメソッドfullUpdateを用意しました。

// 
//  Mixer.m
//

- (void)fullUpdate
{
    [[self mutableSetValueForKey:@"oscillators"] makeObjectsPerformSelector:@selector(updateBezierPath)];
    [self updateBezierPath];
}

MasterMotifの波形更新メソッド

 波形を更新するメソッドupdateBezierPathをMasterMotifに追加します。こちらはLissajousでやっていた事とほとんど変わりませんので、説明は省略します。

 ミキサーの波形を更新してから自身の波形を更新するメソッドfullUpdateはMasterMotifにも必要となりますので、追加します。

//
//  MasterMotifCD.m
//

#pragma mark -
#pragma mark Update path

//  MixerのbezierPathが先に更新されている事
- (void)updateBezierPath
{
    int             i;
    NSPoint         px,py;
    NSBezierPath    *path = [NSBezierPath bezierPath];
    NSBezierPath    *waveformX = [[self x] bezierPath];
    NSBezierPath    *waveformY = [[self y] bezierPath];

    if([waveformX elementCount] != [waveformY elementCount])  return;
    for(i=0;i<[[self resolution] intValue];i++)
    {
        [waveformX elementAtIndex:i associatedPoints:&px];
        [waveformY elementAtIndex:i associatedPoints:&py];
        if(i == 0)
            [path moveToPoint:NSMakePoint(px.y,py.y)];
        else
            [path lineToPoint:NSMakePoint(px.y,py.y)];
    }
    [path closePath];
    [self setBezierPath:path];
}

- (void)fullUpdate
{
    [[self mutableSetValueForKey:@"mixers"] makeObjectsPerformSelector:@selector(fullUpdate)];
    [self updateBezierPath];
}

波形の更新が必要になる時

 波形の更新が必要になるのはresolutionが変更された時と、各発振器の出力波形が変更された時です。前者はresolutionのSetterで、後者は発振器のSetterで波形の更新すればOKです。

 まずresolutionのSetterを修正します。resolutionに値をセットしてからfullUpdateを実行します。

// 
//  MasterMotifCD.m
//

- (void)setResolution:(NSNumber *)value 
{
    [self setValue:value forKey:@"resolution" action:@"setResolution"];
    [self fullUpdate];
}

OscillatorのupdateBezierPathを修正する

 LissajousをMasterMotifCDに置き換えます。ただし最後にLissajousのupdateBezierPathメソッドを呼びだすコードは削除します。これをMixerCDのupdateBezierPathメソッドを呼びだすコードに置き換えてしまうと、fullUpdateを実行した時に無駄なコードになってしまうためです。

 初期化コードからupdateBezierPathを実行した場合、初期化コードが呼びだされる順番が関係する為か、resolutionを取得できない場合がありました。そのような場合は波形の更新は行ないません。

 OscillatorCDのSetterにupdateBezierPathメッセージを送るコードを追加します。まず自分自身に送り、次にMixerに送り、最後にMasterMotifに送ります。この順番を守る必要があります。

// 
//  OscillatorCD.m
//  RepeatingMotifGenerator
//

- (void)updateBezierPath
{
    int             i,resolution;
    double          output,normalizedPhase;
    NSBezierPath    *path;
    MasterMotifCD   *masterMotif = [[self mixer] masterMotif];

    if(masterMotif == nil)   return;

    path = [NSBezierPath bezierPath];
    resolution = [[masterMotif resolution] intValue];
    for(i=0;i<resolution;i++)
    {
        normalizedPhase = i/(double)resolution;
        output = [self output:normalizedPhase];
        if(i == 0)
            [path moveToPoint:NSMakePoint(i,output)];
        else
            [path lineToPoint:NSMakePoint(i,output)];
    }
    [self setBezierPath:path];
}

- (void)setAmplitude:(NSNumber *)value
{
    [self setValue:value forKey:@"amplitude" action:@"setAmplitude"];
    [self updateBezierPath];
    [[self mixer] updateBezierPath];
    [[[self mixer] masterMotif] updateBezierPath];
}

- (void)setFrequency:(NSNumber *)value 
{
    [self setValue:value forKey:@"frequency" action:@"setFrequency"];
    [self updateBezierPath];
    [[self mixer] updateBezierPath];
    [[[self mixer] masterMotif] updateBezierPath];
}

- (void)setPhaseLag:(NSNumber *)value 
{
    [self setValue:value forKey:@"phaseLag" action:@"setPhaseLag"];
    [self updateBezierPath];
    [[self mixer] updateBezierPath];
    [[[self mixer] masterMotif] updateBezierPath];

【失敗例】パラメータが1だけずれる事がある?


 テスト中に、90と入力した筈のパラメータが保存後に開いてみると89になっている、という事がありました。整数を扱っているつもりでいたのに、実際には実数で処理されていた、という事が原因です。

 amplitude, frequency, phaseLagはOscillatorエンティティの属性で、タイプはInt 16です。整数型に指定したのだから、このパラメータは整数として扱ってよいと思っていました。しかしこれらの実体はNSNumberというオブジェクトです。整数を取り出す事もできますが、実数を取り出す事もできるので、内部的には実数で処理されているものと思われます。

 さて、これらのパラメータを設定するためにNSSliderを使いました。数値表示はNSNumberFormatterがセットされたラベルで行ないます。NSNumberFormatterで整数化されるので見過ごしてしまいましたが、NSSliderが送ってくる数値は整数ではなく実数です。

  •  スライダーにTick Marksを設定して"Only stop on tick marks"にチェックを入れ、Tick Marksのところの値が整数になる様にしておけば、整数値になります。小数部分がゼロの実数といった方が正確かもしれません。最初はfrequencyのスライダーだけそのような設定にしており、frequencyは数値が1だけずれる現象が発生しませんでした。

 詳しくは調べていませんが、90を入力したつもりでも実際には89.?が入力されている事が1だけずれる原因の様です。なぜ入力時と保存後に開いた時で異なる数値として表示されるのかはわかりませんが、そこは追求しませんでした。そもそも90を入力したつもりでいるのに、実際には89.?が入力されている事自体が問題です。

 そこでSetterを変更し、強制的に整数化した値をセットする様に変更しました。合わせてNSNumberFormatterを外しました。これで見た目と実際の値が一致する様になり、1だけずれる現象がなくなりました。

// 
//  MasterMotifCD.m
//

- (void)setResolution:(NSNumber *)value 
{
    [self setValue:[NSNumber numberWithInt:[value intValue]] forKey:@"resolution" action:@"setResolution"];
    [self fullUpdate];
}

// 
//  OscillatorCD.m
//

- (void)setAmplitude:(NSNumber *)value
{
    [self setValue:[NSNumber numberWithInt:[value intValue]] forKey:@"amplitude" action:@"setAmplitude"];
    [self updateBezierPath];
    [[self mixer] updateBezierPath];
    [[[self mixer] masterMotif] updateBezierPath];
}

- (void)setFrequency:(NSNumber *)value 
{
    [self setValue:[NSNumber numberWithInt:[value intValue]] forKey:@"frequency" action:@"setFrequency"];
    [self updateBezierPath];
    [[self mixer] updateBezierPath];
    [[[self mixer] masterMotif] updateBezierPath];
}

- (void)setPhaseLag:(NSNumber *)value 
{
    [self setValue:[NSNumber numberWithInt:[value intValue]] forKey:@"phaseLag" action:@"setPhaseLag"];
    [self updateBezierPath];
    [[self mixer] updateBezierPath];
    [[[self mixer] masterMotif] updateBezierPath];
}