カテゴリー
iOS Objective-C

CoreAudioを学ぶ(4)

パート4はRecording With Audio Queuesです。

まずは、UtilityFunctionsから見ていきましょう。見やすくフォーマットされた文字列を表示するためのカスタム関数NSPrintを定義しています。ブログの著者さんはこの記事を参考にしたようです。

void NSPrint(NSString *format, ...) { // [1]
  va_list args; // [2]

  va_start(args, format);
  NSString *string  = [[NSString alloc] initWithFormat:format arguments:args]; // [3]
  va_end(args);

  fprintf(stdout, "%s", [string UTF8String]); // [4]
    
#if !__has_feature(objc_arc)
  [string release]; // [5]
#endif
}

[1] … はC言語の文法で、この関数は「いくつかの引数を得られる」という意味です。stdarg.hに宣言されている va_list, va_start, va_endを使うことが出来ます。

[2] va_list型の変数argsを作成しています。va_listはvariable argument list(可変の変数のリスト)の略で、…の内容が格納されます。このva_listにはva_startとva_endの中でアクセスすることが出来ます。

[3] NSStringの標準のinitializerのinitWithFormatでインスタンス*stringを作り、arguments:に引数argsを渡しています。

[4] fprintfはC言語の関数で、fprintf(ストリーム、フォーマット、…(文字列))のように使います。

[5] ARC (Automatic Reference Counting)がない場合は手動でstringをリリースします。

次は、エラーの有無をチェックするカスタム関数CheckErrorです。

void CheckError(OSStatus error, const char *operation) {
  if (error == noErr) {
    return;
  }
  
  char errorString[20]; // [6]
  *(UInt32 *)(errorString + 1) = CFSwapInt32HostToBig(error); // [7] we have 4 bytes and we put them in Big-endian ordering. 1st byte the biggest
  if (isprint(errorString[1]) && isprint(errorString[2]) && // [8]
      isprint(errorString[3]) && isprint(errorString[4])) {
    errorString[0] = errorString[5] = '\'';
    errorString[6] = '\0';
  } else {
    sprintf(errorString, "%d", (int) error);
  }
  NSLog(@"Error: %s (%s)\n", operation, errorString);
  exit(1);
}

[6] 20バイトの空のcharの配列を作成しています。

[7] *(errorString + 1) のerrorString + 1 はポインタ演算です。errorString配列の2番目の要素(つまりerrorString[1]と同じ)にCFSwapInt32HostToBig(error)の戻り値unsigned intを代入しています。Big-endianは、UInt32の4byteのうち、最初のbyteが一番大きな値となるメモリ上の保存方法です。

[8] isprint( ) はC言語の関数で、そのcharがcharとして表示可能かをチェックするものです。例として’a’は表示可能、’\t’は表示可能ではない、となります。

次に、デフォルトのインプットデバイスのサンプルレートを確認するカスタム関数を見てみましょう。

void GetDefaultInputDeviceSampleRate(Float64 *oSampleRate) {
  AudioObjectPropertyAddress propertyAddress; // [9]
  
  propertyAddress.mSelector = kAudioHardwarePropertyDefaultInputDevice;
  propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
  propertyAddress.mElement = 0; // master element
  
  AudioDeviceID deviceID = 0;
  UInt32 propertySize = sizeof(AudioDeviceID);
  
  CheckError(AudioObjectGetPropertyData(kAudioObjectSystemObject,
                                        &propertyAddress,
                                        0,
                                        NULL,
                                        &propertySize,
                                        &deviceID), "Getting default input device ID from Audio System Object"); // [9]'
  
  propertyAddress.mSelector = kAudioDevicePropertyNominalSampleRate; // [10]
  propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
  propertyAddress.mElement = 0;
  propertySize = sizeof(Float64);
  
  CheckError(AudioObjectGetPropertyData(deviceID,
                                        &propertyAddress,
                                        0,
                                        NULL,
                                        &propertySize,
                                        oSampleRate), "Getting nominal sample rate for the default device"); // [10]'
}

[9] ~ [9]’ でdeviceIDを求め、[10] ~ [10]’でsampleRateを求めています。どちらもCore AudioのAudioObjectGetPropertyData( )関数を使っています。

// 以下執筆中。。。

カテゴリー
iOS Objective-C

CoreAudioを学ぶ(2)

パート2はGenerating Raw Audio Samplesを学んで行きます。方形波(square wave)、ノコギリ波(saw wave)、サイン波(sine wave)をプログラム的に生成し、ファイルを作成します。

#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h>

// CD-qualiy sample rate
#define SAMPLE_RATE 44100
#define BITS_PER_SAMPLE 16
#define BYTE_SIZE_IN_BITS 8
#define BYTES_PER_SAMPLE BITS_PER_SAMPLE / BYTE_SIZE_IN_BITS
// We use LPCM so the encoding does not use packets. Hence,
// we are going to have 1 frame per packet.
#define FRAMES_PER_PACKET 1

// number of seconds we want to capture
#define DURATION 5.0
#define FILENAME_FORMAT @"%0.3f-%@.aif"

#define NUMBER_OF_CHANNELS 1

void buildFileURL(double hz, NSString *shape, NSURL** fileURL) {
    NSString* fileName = [NSString stringWithFormat:FILENAME_FORMAT, hz, shape];
    NSString* filePath = [[[NSFileManager defaultManager] currentDirectoryPath]
                          stringByAppendingPathComponent:fileName];
    *fileURL = [NSURL fileURLWithPath:filePath];
}

void buildAudioStreamBasicDescription(AudioStreamBasicDescription* audioStreamBasicDescription) {
    memset(audioStreamBasicDescription, 0, sizeof(AudioStreamBasicDescription)); // [5]
    
    audioStreamBasicDescription->mSampleRate = SAMPLE_RATE; // [6]
    audioStreamBasicDescription->mFormatID = kAudioFormatLinearPCM;
    audioStreamBasicDescription->mFormatFlags = kAudioFormatFlagIsBigEndian | kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
    audioStreamBasicDescription->mBitsPerChannel = BITS_PER_SAMPLE;
    audioStreamBasicDescription->mChannelsPerFrame = NUMBER_OF_CHANNELS;
    audioStreamBasicDescription->mFramesPerPacket = FRAMES_PER_PACKET;
    audioStreamBasicDescription->mBytesPerFrame = BYTES_PER_SAMPLE * NUMBER_OF_CHANNELS;
    audioStreamBasicDescription->mBytesPerPacket = audioStreamBasicDescription->mFramesPerPacket * audioStreamBasicDescription->mBytesPerFrame;
} // buildAudioStreamBasicDescription

SInt16 generateSineShapeSample(int i, double waveLengthInSamples) {
    assert(i >= 1 && i <= waveLengthInSamples);
    
    return (SInt16)(SHRT_MAX * sin(2 * M_PI * (i - 1) / waveLengthInSamples));
}

SInt16 generateSquareShapeSample(int i, double waveLengthInSamples) {
    assert(i >= 1 && i <= waveLengthInSamples);
    
    if (i <= waveLengthInSamples / 2) {
        return SHRT_MAX;
    } else {
        return SHRT_MIN;
    }
}

SInt16 generateSawShapeSample(int i, double waveLengthInSamples) {
    assert(i >= 1 && i <= waveLengthInSamples);
    
    return (SInt16)(2 * SHRT_MAX / waveLengthInSamples * (i - 1) - SHRT_MAX);
}

NSString* correctShape(NSString *shape) { // [2]'
    if ([shape isEqualToString:@"square"] ||
        [shape isEqualToString:@"saw"] ||
        [shape isEqualToString:@"sine"])
    {
        return shape;
    } else {
        return @"square";
    }
}

int main(int argc, const char * argv[]) {
    if (argc < 2) {
        printf("Usage: WriteRawAudioSamples n <shape>\nwhere n is tone in Hz, shape is one of 'square' (default), 'saw', 'sine'\n");
        return -1;
    }
    
    @autoreleasepool {
        double hz = atof(argv[1]); // [1]
        assert(hz > 0);
        
        NSString *shape;
        if (argc == 2) {
            shape = @"square"; // [2]
        } else {
            shape = [NSString stringWithFormat:@"%s", argv[2]];
            shape = correctShape(shape);
        }
        
        NSLog(@"generating %f hz tone with shape %@...", hz, shape);
        
        NSURL *fileURL = NULL;
        buildFileURL(hz, shape, &fileURL);
        
        // Prepare the format
        AudioStreamBasicDescription audioStreamBasicDescription; // [3]
        buildAudioStreamBasicDescription(&audioStreamBasicDescription); // [4]
        
        // Set up the file
        AudioFileID audioFile; // [7]
        OSStatus error = noErr;
        
        error = AudioFileCreateWithURL((__bridge CFURLRef)fileURL,
                                       kAudioFileAIFFType,
                                       &audioStreamBasicDescription,
                                       kAudioFileFlags_EraseFile,
                                       &audioFile);
        assert(error == noErr);
        
        // Start writing samples;
        long maxSampleCount = SAMPLE_RATE * DURATION; // [8]
        
        long sampleCount = 1;
        UInt32 bytesToWrite = BYTES_PER_SAMPLE; // [9]
        double waveLengthInSamples = SAMPLE_RATE / hz; // [10]
        NSLog(@"wave (or cycle) length in samples: %.4f\n", waveLengthInSamples);
        
        while (sampleCount <= maxSampleCount) { // [11]
            for(int i = 1; i <= waveLengthInSamples; i++) {
                SInt16 sample = 0;
                
                if ([shape isEqualToString:@"square"]) {
                    sample = generateSquareShapeSample(i, waveLengthInSamples);
                }
                else if ([shape isEqualToString:@"saw"]) {
                    sample = generateSawShapeSample(i, waveLengthInSamples);
                } else if ([shape isEqualToString:@"sine"]) {
                    sample = generateSineShapeSample(i, waveLengthInSamples);
                }
                sample = CFSwapInt16HostToBig(sample);
                
                SInt64 offset = sampleCount * bytesToWrite;
                error = AudioFileWriteBytes(audioFile, false, offset, &bytesToWrite, &sample);
                assert(error == noErr);
                
                sampleCount++;
            }
        }
        error = AudioFileClose(audioFile);
        assert(error == noErr);
        NSLog(@"wrote %ld samples", sampleCount);
    }
    return 0;
}

[1] atof()というC言語の文字列をDouble型へ変換する関数を使って、周波数を代入しています。

[2] 実行ファイル実行時の引数が2の場合はデフォルトの”square”(方形波)が選ばれ、それ以外の場合は引数に応じて”saw”(ノコギリ波)、”sine”(サイン波)が選択されます。”square”, “saw”, “sine”以外の文字列が引数として渡された場合は”square”が選択されます[2]’。

[3] オーディオデータをファイルに書き込むにはAudioStreamBasicDescriptionのインスタンスを用いて、CoreAudio APIにどのようなフォーマットのデータを書き込みたいかを伝える必要があります。

[4] [5] まず、memset( )を使い構造体AudioStreamBasicDescriptionのサイズ分のメモリを確保し、そのメモリの全てのバイトに0(ゼロ)を書き込みます。これはセットしないパラメータに意図しないランダムな値が代入されていないようにするグッドプラクティスだそうです。

[6] 必要なパラメータを設定していきます。

[7] AudioFileCreateWithURL( )を使い、オーディオファイルを作成します。

[8] SAMPLE_RATE(一秒間に何箇所数値化するか) x DURATION(秒数)で、総サンプル数を計算します。この例では 44,100 x 5 = 220,500サンプルです。

[9] これは単純に、ビット深度(bits per sample, またはbit depth)が16の場合、byteで表すとなにか?を設定しています。1 byte == 8 bitですから、bytesToWriteは2です。

[10] サンプルレートを音の高さ(周波数、hz)で割ると、1サイクルにいくつサンプルがあるのかを計算できます。A4(440hz)の場合、44,100 / 440 = 100.2272…です。

[11] 波形タイプに応じて、サンプル毎の数値を計算し、メモリのoffsetの位置を sampleCount (何サンプル目) x bytesToWrite(サンプル毎のデータの大きさ)としてそこに書き込んでいきます。

カテゴリー
iOS Objective-C

CoreAudioを学ぶ(1)

AppleのCoreAudioを学ぶために資料を探していたところ、Learning Core Audioという本を見つけました。ただ、この本はObjective-Cで書かれていたり、少し古い本なのでコードがところどころDeprecatedになっているので困っていたところ、タイムリーにとてもいいブログを発見しました。Leaning Core Audioを読み解き、現在もコンパイルするコードを紹介してくれています。初学者がつまずきそうな箇所も丁寧に解説してくれているのがありがたいです!Objective-Cの勉強も兼ねて、このブログを読んで行こうと思います。ブログ著者さんの丁寧なティーチングスタイルがありがたかったので、僕も真似して出来るだけ端折らない記事を書いてみたいと思います。

まずは一番最初の記事、Reading Basic Info From a Local Audio Fileです。

#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h>

void GetAudioFileInformationProperty(AudioFileID audioFile, CFDictionaryRef *dictionary) {
    OSStatus theErr = noErr;
    UInt32 dictionarySize = 0;
    theErr = AudioFileGetPropertyInfo(audioFile,
                                      kAudioFilePropertyInfoDictionary,
                                      &dictionarySize,
                                      0);
    assert(theErr == noErr);
    
    theErr = AudioFileGetProperty(audioFile,
                                  kAudioFilePropertyInfoDictionary,
                                  &dictionarySize,
                                  dictionary);
    assert(theErr == noErr);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (argc < 2) { // [1]
            printf("Usage: CAMetadata fullpath/to/audiofile\n");
            return -1;
        }
        
        // [2] 
        NSString *audioFilePath = [[NSString stringWithUTF8String:argv[1]] stringByExpandingTildeInPath];
        NSURL *audioURL = [NSURL fileURLWithPath:audioFilePath];
        
        // [3]
        AudioFileID audioFile;
        OSStatus theErr = noErr;
        
        // [4]
        // [5] 
        theErr = AudioFileOpenURL((__bridge CFURLRef)audioURL, kAudioFileReadPermission, 0, &audioFile);
        
        // [6] 
        assert(theErr == noErr);
        
        CFDictionaryRef dictionary;
        
        GetAudioFileInformationProperty(audioFile, &dictionary);
        
        NSLog(@"dictionary: %@", dictionary);
        
        CFRelease(dictionary);
        
        theErr = AudioFileClose(audioFile);
        
        assert(theErr == noErr);
    }
    return 0;
}

[1] argcとは実行ファイルを実行する時の引数の数です。Argument Countの略だと思います。C言語の実行ファイル実行時は [実行ファイル名] [引数1] [引数2] … という形でプログラムを呼びます(もちろん[ ]は省いてください)。実行ファイル名そのものもカウントされるので、引数が2つの場合argcは3です。argcが2よりも小さい場合、-1をリターンしてプログラムを終了させています。

[2]まず、Objective-Cスタイルのメソッド実行の書き方に面食らいますよね?Objective-Cではクラスからインスタンスを作る時、NSString *audioFilePathというふうに「ポインタ」で作成します。そして、メソッドの実行は[クラス名 クラスメソッド名:引数]と[ ]で囲むように書きます(クラスメソッドの場合)

NSString *audioFilePath = [[NSString stringWithUTF8String:argv[1]] stringByExpandingTildeInPath];

stringWithUTF8String メソッドはノン・ラテン文字もパースさせるためのもの。stringByExpandingTildeInPath はパスの中に「~」が存在する時にパースするためのものです。

Swiftに書き直すと、以下のようになると思います。(意訳です)

let audioFilePath: NSString = NSString.stringWithUTF8String(argv[1]).stringByExpandingTildeInPath()

[3] AudioFileID audioFile; とインスタンス作成をしています。なぜポインタで作成しないのか?AudioFileIDの定義を見てみると、typedef struct OpaqueAudioFileID *AudioFileID; と、構造体OpaqueAudioFileIDを*AudioFileIDというポインタでtypedefしているため、とわかりました。Swiftのコードを見ているとよくOpaquePointerとUnsafePointerというものが出てくるのですが、いまいち違いが分かっていませんでしたが、この記事を読むと少し違いが分かりました。C言語ではプログラムがheaderファイルとsourceファイルに分かれていますが、headerファイルに構造体の内容の記述がある場合はUnsafePointer、headerファイルには宣言のみで、sourceファイルに構造体の内容の記述がある場合はOpaquePointerとなるようです。

[4] __bridgeについても分からなかったので調べたところ、CoreFoundationのオブジェクトはARCの管理対象ではないため、それを管理対象とするためのキーワードのようです。

[5] CoreAudioではこのように、メソッドの引数にポインタのアドレス(&audioFile)を渡し、そこに値を代入する場合が多いように思います。

[6] assertは( )内の条件がtrueではない場合にログに吐き出すものです。

XcodeのCommand Line Toolの実行ファイルは、~/Library/Developer/Xcode/DerivedData/プロジェクト名/Products/Debugにありました。以下のようなコマンドで呼べます。

./CAMetadata ~/Desktop/sample.mp3 
カテゴリー
iOS Swift

任意の点がMKCoordinateRegion内にあるかを調べるには

任意の点CLLocationCoordinate2DがMapViewのMKCoordinateRegionの中にあるのかを知る方法を調べていたら、このStackOverflowの投稿に辿りつきました。

+ (BOOL)coordinate:(CLLocationCoordinate2D)coord inRegion:(MKCoordinateRegion)region
{
    CLLocationCoordinate2D center = region.center;
    MKCoordinateSpan span = region.span;

    BOOL result = YES;
    result &= cos((center.latitude - coord.latitude)*M_PI/180.0) > cos(span.latitudeDelta/2.0*M_PI/180.0);
    result &= cos((center.longitude - coord.longitude)*M_PI/180.0) > cos(span.longitudeDelta/2.0*M_PI/180.0);
    return result;
}

Swiftで書くとこのようになると思います。

func checkIfCoordinateInRegion(coordinate: CLLocationCoordinate2D, region: MKCoordinateRegion) -> Bool {
        let center = region.center
        let span = region.span
        return cos((center.latitude - coordinate.latitude)*Double.pi/180.0) > cos(span.latitudeDelta/2.0*Double.pi/180.0) && cos((center.longitude - coordinate.longitude)*Double.pi/180.0) > cos(span.longitudeDelta/2.0*Double.pi/180.0)
}

なぜcosで計算すると良いのかがパッと見分からなかったので考えてみました。

まず、地球を描きます。

The Earth
Draw imaginary latitude and longtitude.

Draw an imaginary span area
Viewing the earth from the side.
Here is cosθ of your span area.
If a point is inside the span area, its cosθ’ will always be greater than cosθ.

カテゴリー
iOS Swift

iPhoneで録画したポートレート動画の描画

iPhoneでポートレート(portrait == 縦方向に長い画像)撮影した動画は、フレームを描画すると意図した角度から90°反時計回りに回転して描画されてしまいます。その理由についてAppleのこのコードを元に勉強してみました。作成したサンプルはこちら

iPhoneで撮影した動画は全てランドスケープ(landscape == 横方向に長い画像)で保存されるようです。その動画がポートレートかランドスケープかの判定にはAVAssetTrackのpreferredTransformというプロパティを用いるということのようです。

preferredTransformはCGAffineTransformというクラスのインスタンスです。Affine Transform(アフィン変換)を理解するには行列の知識が必要なのですが、この「さつき先生」の動画レクチャーがウルトラ分かりやすいのでおすすめします。

CGAffineTransformはこのような行列です。

iPhoneで撮影したランドスケープ動画のpreferredTransformの値は以下のようになっています。これはつまり単位行列(identity matrix)です。

affine transform a: 1.0
affine transform b: 0.0
affine transform c: 0.0
affine transform d: 1.0
affine transform tx: 0.0
affine transform ty: 0.0
3×3の単位行列

単位行列はベクトルや行列にかけても、そのベクトルや行列を変化させません。これは、グラフィックスでは、元の動画を変化させない(スケール、回転、並行移動をさせない)ということです。

iPhoneで撮影したポートレート動画のpreferredTransformの値は以下のようになっていました。単位行列ではなく、aとdの値が0.0, bが1.0, cは-1.0という値になっています。またtxの値が1080(画像の縦寸法)になっています。(txが画像の縦寸法の値になっている理由がいまいちよく分かっていません、、、分かり次第記事をアップデートします)

affine transform a: 0.0
affine transform b: 1.0
affine transform c: -1.0
affine transform d: 0.0
affine transform tx: 1080.0
affine transform ty: 0.0

この行列の逆行列は以下です。bとcが入れ替わり、txとtyも入れ替わっています。

affine transform inverted a: 0.0
affine transform inverted b: -1.0
affine transform inverted c: 1.0
affine transform inverted d: 0.0
affine transform inverted tx: -0.0
affine transform inverted ty: 1080.0

このa, b, c, dの部分はつまり、θ = 270°の回転行列です。回転行列式の仕組みも、このさつき先生のハイパー分かりやすい動画レクチャーをご覧ください。さつき先生の説明にある回転行列と比べるとCGAffineTransformはcとdの位置が入れ替わっています。これはこの行列を右から掛けるか左から掛けるかの違いから来ています。

つまりこの preferredTransformの値は、逆行列を用いることで、単位行列の時はそのまま単位行列を返し、回転したい場合は回転行列を返して利用するところがミソです。

var affineTransform: CGAffineTransform {
    return self.videoTrack.preferredTransform.inverted()
}

UIImageViewには以下のようにCVPixelBuffer -> CIImage -> CGImage -> UIImage と変換させ、描画させています。

func displayFrame(_ frame: CVPixelBuffer?, withAffineTransform transform: CGAffineTransform) {
    DispatchQueue.main.async { // updating the UI needs to happen on the main thread
        if let frame = frame {            
            let ciImage = CIImage(cvPixelBuffer: frame)
                .transformed(by: transform) // applying affineTransform here, otherwise a portrait image will be displayed in landscape.
            let ciContext = CIContext(options: nil)
            guard let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) else { return }
            let uiImage = UIImage(cgImage: cgImage)
            self.imageView.image = uiImage
        }
    }
}
カテゴリー
iOS Swift

CAShapeLayerをアニメーションさせる

CAShapeLayerのpositionをfor loop内でインクリメントし、アニメーションさせてみます。

import UIKit

class ViewController: UIViewController {
    
    var dotShapeLayer = CAShapeLayer()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        showPoint()
        movePoint()
    }

    func showPoint() {
        view.layer.addSublayer(dotShapeLayer)
        let dotRect = CGRect(x: 0, y: 0, width: 10, height: 10)
        let smallDotPath = UIBezierPath(rect: dotRect)
        dotShapeLayer.path = smallDotPath.cgPath
    }
    
    func movePoint() {
        DispatchQueue.global().async {
            for i in 0...100 {
                DispatchQueue.main.async {
                    self.dotShapeLayer.position = CGPoint(x: i * 2, y: i * 2)
                    self.view.layer.setNeedsDisplay()
                }
                sleep(1)
            }
        }
    }
}
カテゴリー
iOS Swift

CAShapeLayerに図形を描く

Core AnimationのCALayerを使って、Viewに図形を描くことが出来ます。

override func viewDidLoad() {
    super.viewDidLoad()
    let shapeLayer = CAShapeLayer() // CAShapeLayerインスタンスを作成
  view.layer.addSublayer(shapeLayer)
    print(shapeLayer.bounds) // (0.0, 0.0, 0.0, 0.0)をプリント
}

addSublayer(shapeLayer) とした時点では、ShapeLayerはサイズを持たないオブジェクトとしてview.layerのsublayerとして存在しています。サイズはないのですが、概念上黄色に着色して図にすると以下のようになります。

図を描くには、UIBezierPathクラスを使います。

let path = UIBezierPath()
path.move(to: CGPoint(x: 5, y: 5))      // 図形の起点を定める
path.addLine(to: CGPoint(x: 5, y: 130)) // 起点からどのポイントに線を引くか定める

shapeLayer.path = path.cgPath           // shapeLayerのpathにpath.cgPathを代入
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 6
(x: 5, y: 5)から(x: 5, y: 130)を結ぶ線が引かれました。
let path = UIBezierPath()
path.move(to: CGPoint(x: 5, y: 5))      // 図形の起点を定める
path.addLine(to: CGPoint(x: 5, y: 130)) // 起点からどのポイントに線を引くか定める
path.addLine(to: CGPoint(x: 125, y: 130)) // さらに線を伸ばす

shapeLayer.path = path.cgPath           // shapeLayerのpathにpath.cgPathを代入
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.link.cgColor // 青色に塗りつぶす
shapeLayer.lineWidth = 6
二つの線がつながり、その間を.link色で塗りつぶしました。

.addQuadCurve(to: CGPoint, controlPoint: CGPoint)を使い、曲線を描き、pathを閉じます。

let path = UIBezierPath()
path.move(to: CGPoint(x: 5, y: 5))      // 図形の起点を定める
path.addLine(to: CGPoint(x: 5, y: 130)) // 起点からどのポイントに線を引くか定める
path.addLine(to: CGPoint(x: 125, y: 130)) // さらに線を伸ばす
path.addQuadCurve(to: CGPoint(x: 5, y: 5), controlPoint: CGPoint(x: 125, y: 5)) // 曲線を描く
path.close() // pathを閉じる

shapeLayer.path = path.cgPath           // shapeLayerのpathにpath.cgPathを代入
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.link.cgColor // 青色に塗りつぶす
shapeLayer.lineWidth = 6
let path = UIBezierPath()
path.move(to: CGPoint(x: 5, y: 5))      // 図形の起点を定める
path.addLine(to: CGPoint(x: 5, y: 130)) // 起点からどのポイントに線を引くか定める
path.addLine(to: CGPoint(x: 125, y: 130)) // さらに線を伸ばす
path.addQuadCurve(to: CGPoint(x: 5, y: 5), controlPoint: CGPoint(x: 125, y: 5)) // 曲線を描く
path.close() // pathを閉じる

shapeLayer.bounds = path.bounds    // path.boundsを代入
print("shapeLayer.bounds after: \(shapeLayer.bounds)") // サイズのなかったshapeLayer.boundsにpath.boundsが設定される。shapeLayer.bounds after: (5.0, 5.0, 120.0, 125.0)
shapeLayer.path = path.cgPath           // shapeLayerのpathにpath.cgPathを代入
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.link.cgColor // 青色に塗りつぶす
shapeLayer.lineWidth = 6
shapeLayerがサイズを持ち、図形はdefault position(0.0, 0.0)へ配置されました。
let path = UIBezierPath()
path.move(to: CGPoint(x: 5, y: 5))      // 図形の起点を定める
path.addLine(to: CGPoint(x: 5, y: 130)) // 起点からどのポイントに線を引くか定める
path.addLine(to: CGPoint(x: 125, y: 130)) // さらに線を伸ばす
path.addQuadCurve(to: CGPoint(x: 5, y: 5), controlPoint: CGPoint(x: 125, y: 5)) // 曲線を描く
path.close() // pathを閉じる

shapeLayer.bounds = path.bounds    // path.boundsを代入
print("shapeLayer.bounds after: \(shapeLayer.bounds)") // サイズのなかったshapeLayer.boundsにpath.boundsが設定される。shapeLayer.bounds after: (5.0, 5.0, 120.0, 125.0)

// shapeLayerのposition.x, yをview.boundsのmidX, midYに設定
shapeLayer.position.x = view.bounds.midX 
shapeLayer.position.y = view.bounds.midY


shapeLayer.path = path.cgPath           // shapeLayerのpathにpath.cgPathを代入
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.link.cgColor // 青色に塗りつぶす
shapeLayer.lineWidth = 6
shapeLayerのポジションがview.bounds.midY, midXに設定されました。

カテゴリー
iOS Swift

UIButtonの背景色をコードで設定

UIButton.backgroundColorでボタンの背景色を設定するとボタンをプレスした時の色の反応が見えないため、UIColorからUIImageを作成し、UIButtonのバックグラウンドイメージを設定する簡単なコードを作成しました。

参考にした記事はこちら

import UIKit

class ViewController: UIViewController {

    lazy var button: UIButton = {
        let button = UIButton()
        let buttonColor = UIColor.link
        let uiImage = createUIImageFromUIColor(color: buttonColor) // UIColorからUIImageを作成
        button.setBackgroundImage(uiImage, for: .normal)

       // ボタンにアイコンイメージを指定する場合
       button.setImage(UIImage(systemsName: "heart.fill"))
       // イメージサイズを可変にする場合
       button.imageView?.contentMode = .scaleAspectFit
       button.contentHorizontalAlignment = .fill
       button.contentVerticalAlignment = .fill
       // アイコンの色を指定
       button.tintColor = UIColor.systemPink
       // ボーダーを指定する場合
       button.layer.borderColor = UIColor.black
       button.layer.borderWidth = 1
       // ボタンにテキストを指定する場合
        button.setTitle("Button", for: .normal)
        button.setTitleColor(.white, for: .normal)
        // コーナーを丸くする場合
        button.layer.cornerRadius = 20
        button.layer.masksToBounds = true
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        button.widthAnchor.constraint(equalToConstant: 100).isActive = true
        button.heightAnchor.constraint(equalToConstant: 50).isActive = true
    }
    
    private func createUIImageFromUIColor(color: UIColor) -> UIImage? {
        let size = CGSize(width: 200, height: 200)
        var colorImage: UIImage?
        
        UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
        if let context = UIGraphicsGetCurrentContext() {
            context.setFillColor(color.cgColor)
            context.fill(CGRect(origin: .zero, size: size))
            colorImage = UIGraphicsGetImageFromCurrentImageContext()
        }
        UIGraphicsEndImageContext()
        return colorImage
    }
    
}
カテゴリー
iOS Swift

Videoファイルからフレームを抽出

Vision Frameworkを使って動画解析をするための前準備として、動画ファイルから毎フレームを抽出し、UIImageViewへ描画するにはどうしたら良いのか。Appleのサンプルコードを勉強し、ミニマム実装をしてみました。

このコードのキモは、AVAssetReaderTrackOutputクラスのcopyNextSampleBuffer()メソッドを利用し、CVPixelBufferオブジェクトを得て、UIImageへ描画することです。

func nextFrame() -> CVPixelBuffer? { 
    guard let sampleBuffer = self.videoAssetReaderOutput.copyNextSampleBuffer() else {
        return nil
    }
    return CMSampleBufferGetImageBuffer(sampleBuffer)
}

while loop内で nextFrame == true を満たす場合にフレームの抽出を続け、

while true {
    guard let frame = videoReader.nextFrame() else {
        break
    }
    // Draw results
    delegate?.displayFrame(frame, withAffineTransform: videoReader.affineTransform)
    usleep(useconds_t(videoReader.frameRateInSeconds))
}

得たframe: CVPixelBufferをdelegateメソッドに送り、ViewController内のUIImageViewに描画します。

func displayFrame(_ frame: CVPixelBuffer?, withAffineTransform transform: CGAffineTransform) {
    DispatchQueue.main.async {
        if let frame = frame {
            let ciImage = CIImage(cvPixelBuffer: frame).transformed(by: transform)
            let uiImage = UIImage(ciImage: ciImage)
            self.imageView.image = uiImage
        }
    }
}
カテゴリー
iOS Swift

UIViewの理解のために

UIViewはUIを構成する基本的なビルディング・ブロックで、ひとつ以上のsubviewを持つことが出来ます。また、UIResponderのサブクラスであり、touchやその他のeventに反応することが出来たり、gesture recognizerを設定することが出来ます。

UIViewControllerのviewプロパティはRoot Viewを保持します。viewプロパティの初期値はnilです。view == nilの時にviewを呼び出すと、UIViewControllerにより自動的にloadView()メソッドが呼ばれます。

boundsとframeの違いについて

boundsは、自身のcoordinate system(0, 0)にrelativeな位置(x, y)とサイズ(width, height)で表されるrectangleです。

frameは、自身のsuperviewにrelativeな位置(x, y)とサイズ(width, height)で表されるrectangleです。

override func viewDidAppear(_ animated: Bool) {
    print(view.bounds)      // CGRect(x: y: width: height:)がプリントされる
    print(view.bounds.size) // CGSize(width: height:)がプリントされる
    print(view.frame)       // CGRect(x: y: width: height:)がプリントされる
  print(view.frame.size)  // CGSize(width: height:)がプリントされる
}
// this method happens after all the layouts has been done. このメソッドは全てのレイアウト後に呼ばれる
override func layoutSubviews() {
    super.layoutSubviews()
    let layer = sampleImageView.layer
    layer.cornerRadius = 30
}