読者です 読者をやめる 読者になる 読者になる

Pebble's Diary

プログラマーの作業メモ

swift extensionで一回だけ実行されるグローバル定数宣言の仕方

こうすればいいらしい。

extension DateFormatter {
    public static let standard: DateFormatter = {
        let obj = DateFormatter()
        obj.locale = Locale(identifier:"en_US_POSIX")
        return obj
    }()
}

swiftでsingleton

swiftでSingletonを書く場合、こうすればいいらしい。

class MySingleton {
    static let shared = MySingleton()
    private init() {}
}

The Right Way To Write a Singleton — KrakenDev

詳説 AudioStreamBasicDescription / AVAudioFormat

CoreAudioでよく使われるAudioStreamBasicDescription これはC言語の構造体で使いづらいです。
macOS10.10, iOS8以降ではAVAudioFormatと相互変換できますので、これを経由するとコーディングが簡単になります。

まずは実例を見ていきます。
よく使われる.wavファイルのフォーマットを作ってみます。

let fmt = AVAudioFormat(commonFormat:.pcmFormatInt16,
                        sampleRate:44100,
                        channels:2,
                        interleaved:true)
print("\(fmt)")
// <AVAudioFormat 0x100b06650:  2 ch,  44100 Hz, Int16, inter>
let asbd:AudioStreamBasicDescription = fmt.streamDescription.pointee
// AudioStreamBasicDescription(mSampleRate: 44100.0, mFormatID: 1819304813, mFormatFlags: 12, mBytesPerPacket: 4, mFramesPerPacket: 1, mBytesPerFrame: 4, mChannelsPerFrame: 2, mBitsPerChannel: 16, mReserved: 0)
print("\(asbd)")

1819304813(=0x6c70636d)というのは kAudioFormatLinearPCM = 'lpcm'の10進値で、線形PCM形式であることを示しています。
フォーマットフラグの12は
12=8(=kAudioFormatFlagIsPacked)+4(kAudioFormatFlagIsSignedInteger)
を意味します。

例をもう一つ見て見ましょう。

import AVFoundation
let fmt = AVAudioFormat(standardFormatWithSampleRate:44100, channels:2)
//<AVAudioFormat 0x100a00bb0:  2 ch,  44100 Hz, Float32, non-inter>
let asbd:AudioStreamBasicDescription = fmt.streamDescription.pointee
print("\(asbd)")
//AudioStreamBasicDescription( mSampleRate: 44100.0, mFormatID: 1819304813, mFormatFlags: 41, mBytesPerPacket: 4, 
//mFramesPerPacket: 1, mBytesPerFrame: 4, mChannelsPerFrame: 2, mBitsPerChannel: 32, mReserved: 0)

標準フォーマットとはfloat32のnon-interleavedの事だとわかります。
non-interleaveとはステレオの場合に、
LRの音データをLLL..., RRR...
のように別領域に割り当てることを指します。
interleaveの場合LRLR...のように交互に割り当てます。
フォーマットフラグの41は
41=32(kAudioFormatFlagsIsNonInterleaved)+8(kAudioFormatFlagsIsPacked)+1(kAudioFormatFlagIsFloat)
を意味します。

クラス構造を見てきます。

open class AVAudioFormat : NSObject, NSSecureCoding {
    public init(streamDescription asbd: UnsafePointer<AudioStreamBasicDescription>)
    public init(streamDescription asbd: UnsafePointer<AudioStreamBasicDescription>, channelLayout layout: AVAudioChannelLayout?)
    public init(standardFormatWithSampleRate sampleRate: Double, channels: AVAudioChannelCount)
    public init(standardFormatWithSampleRate sampleRate: Double, channelLayout layout: AVAudioChannelLayout)
    public init(commonFormat format: AVAudioCommonFormat, sampleRate: Double, channels: AVAudioChannelCount, interleaved: Bool)
    public init(commonFormat format: AVAudioCommonFormat, sampleRate: Double, interleaved: Bool, channelLayout layout: AVAudioChannelLayout)
    public init(settings: [String : Any])
    public init(cmAudioFormatDescription formatDescription: CMAudioFormatDescription)
    open func isEqual(_ object: Any) -> Bool
    open var isStandard: Bool { get }
    open var commonFormat: AVAudioCommonFormat { get }
    open var channelCount: AVAudioChannelCount { get }
    open var sampleRate: Double { get }
    open var isInterleaved: Bool { get }
    open var streamDescription: UnsafePointer<AudioStreamBasicDescription> { get }
    open var channelLayout: AVAudioChannelLayout? { get }
    open var magicCookie: Data?
    open var settings: [String : Any] { get }
    open var formatDescription: CMAudioFormatDescription { get }
}

// モノラルなら1,ステレオなら2の値です
public typealias AVAudioChannelCount = UInt32

// よく使うのはpcmFormatFloat32とpcmFormatInt16でしょう
public enum AVAudioCommonFormat : UInt {
    case otherFormat
    case pcmFormatFloat32
    case pcmFormatFloat64
    case pcmFormatInt16
    case pcmFormatInt32
}

formatDescriptionプロパティとsettingsプロパティに何が入っているのかも見てみましょう。

let fmt = AVAudioFormat(commonFormat:.pcmFormatInt16,
                        sampleRate:44100,
                        channels:2,
                        interleaved:true)
print("\(fmt.formatDescription)")
print("\(fmt.settings)")

<CMAudioFormatDescription 0x100a075a0 [0x7fffd7221d80]> {
    mediaType:'soun' 
    mediaSubType:'lpcm' 
    mediaSpecific: {
        ASBD: {
            mSampleRate: 44100.000000 
            mFormatID: 'lpcm' 
            mFormatFlags: 0xc 
            mBytesPerPacket: 4 
            mFramesPerPacket: 1 
            mBytesPerFrame: 4 
            mChannelsPerFrame: 2 
            mBitsPerChannel: 16    } 
        cookie: {(null)} 
        ACL: {(null)}
        FormatList Array: {(null)} 
    } 
    extensions: {(null)}
}
["AVLinearPCMIsFloatKey": 0, "AVLinearPCMIsNonInterleaved": 0, "AVNumberOfChannelsKey": 2, "AVSampleRateKey": 44100, "AVLinearPCMBitDepthKey": 16, "AVLinearPCMIsBigEndianKey": 0, "AVFormatIDKey": 1819304813]

settingsには文字列キーのDictionaryが入っているようです。

iOS10のSpeechToText APIを使って日本語PodCastをテキスト化してみる

iOS10で公開されたSpeechフレームワークを使って日本語PodCastをテキスト化してみました。 結果から言うと、テキスト化は可能でしたが、品質はかなり悪く実用として厳しかったです。 某IT系PodCastのファイルを食わせて見たんですが、 「ジャバスクリプト」とか「ディープラーニング」とか一般的でない語彙はほぼ認識できませんでした。

SFSpeechiRecognizerというクラスを使います。 音声ファイルを指定すると、iOSバイスからAppleサーバへリクエストが投げられ、 結果のテキストが得られます。

受け付ける音声ファイルの長さは60sec以内にしないといけないので、 PodCastのmp3などの音声ファイルを20secで区切って順番に処理する戦略をとります。 この方法でAPI制限に引っかかることはありませんでした。

基本のソースコードAppleのSampleCodeを見るか、WWDC2016のセッションを見るか、 ググるとわかりますので端折ります。

SpeakToMe: Using Speech Recognition with AVAudioEngine

ここでは、mp3ファイルを20secのファイルに区切る処理のソースだけを示しておきます。

import Foundation
import AudioToolbox
import AVFoundation

open class AudioDivider {
    init(url:URL) {
        self.url = url
    }
    private var url:URL
    
    public final class func diskCachePath(cacheName: String) -> String {
        let dstPath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!
        return (dstPath as NSString).appendingPathComponent(cacheName)
    }
    
    public func execute() -> [URL] {
        var urls:[URL] = []
        var inref: ExtAudioFileRef?
        var outref: ExtAudioFileRef?
        var st:OSStatus
        st = ExtAudioFileOpenURL(url as CFURL, &inref)
        var index = 0
        let pcmfmt:AVAudioFormat = AVAudioFormat.init(commonFormat: AVAudioCommonFormat.pcmFormatInt16,
                                                   sampleRate: 44100.0,
                                                   channels: 1,
                                                   interleaved: false)
        // read format setting
        guard let inref_unwrap = inref else {
            assert(false)
        }
        st = ExtAudioFileSetProperty(inref_unwrap,
                                     kExtAudioFileProperty_ClientDataFormat,
                                     UInt32(MemoryLayout<AudioStreamBasicDescription>.size),
                                     pcmfmt.streamDescription)
        let bufferByteSize:UInt32 = 1024
        var srcBuffer = [UInt8](repeating:0, count: 1024)
        // file loop
        var eof:Bool = false
        while true {
            let cachepath:String = AudioDivider.diskCachePath(cacheName:"")
            let outpath:String = "\(cachepath)/divider\(index).wav"
            let outurl:URL = URL(fileURLWithPath: outpath)
            urls.append(outurl)
            st = ExtAudioFileCreateWithURL(outurl as CFURL,
                                           kAudioFileWAVEType,
                                           pcmfmt.streamDescription,
                                           nil,
                                           AudioFileFlags.eraseFile.rawValue,
                                           &outref)
            guard let outref_unwrap = outref else {
               assert(false) 
            }
            var writeFrame:UInt32 = 0
            // data loop
            while true {
                let ab = AudioBuffer(mNumberChannels: 1, mDataByteSize: bufferByteSize, mData: &srcBuffer)
                var abl = AudioBufferList(mNumberBuffers: 1, mBuffers: ab)
                var numFrames: UInt32 = 0
                if pcmfmt.streamDescription.pointee.mBytesPerFrame > 0 {
                    numFrames = bufferByteSize / pcmfmt.streamDescription.pointee.mBytesPerFrame
                }
                st = ExtAudioFileRead(inref_unwrap, &numFrames, &abl)
                if numFrames <= 0 {
                    eof = true
                    break
                }
                st = ExtAudioFileWrite(outref_unwrap, numFrames, &abl)
                writeFrame += numFrames
                // terminate the file in 20sec 
                if writeFrame > 44100 * 20 {
                    break
                }
            }
            ExtAudioFileDispose(outref_unwrap)
            index += 1
            if eof {
                break
            }
        }
        ExtAudioFileDispose(inref_unwrap)
        return urls
    }
}

GoogleのSpeechToText API

Speech API - Speech Recognition  |  Google Cloud Platform

は精度がいいらしいですが、無料枠が1ヶ月で1hourしかないため、試す気になりませんでした。 ※1ヶ月で1hourまで無料 以降、$0.006/15sec (=$1.44/1hour)

C言語のsizeofをswiftで書くにはMemoryLayout<T>.size

タイトルそのままです。

struct Hoge {
    var a: Int32
    var b: Int32
}

MemoryLayout<Hoge>.size
// 8

Arrayにおけるmap, flatMapの動作(swift3)

ググると引っかかるのか今だにswift2のドキュメントなので、swift3用のメモ

let a = [1, 2, 3]
let b = a.map { Array(repeating: $0, count: $0) }
print(b)
// [[1], [2, 2], [3, 3, 3]]

let c = [1, 2, 3]
let d = c.flatMap { Array(repeating: $0, count: $0) } 
// [1, 2, 2, 3, 3, 3]
print(d)

これはちゃんとメソッド通りの動作で安心。

enum Optional<Wrapped> の map, flatMapメソッド

swiftのソースでmapとflatMapというメソッドがよく出てくるのだが、このメソッド名からは想像できない動作をするのでメモしておく。 (メソッド名変えてくんないかな。。)

Optionalの定義を見ると次のようになっている。

public enum Optional<Wrapped> : ExpressibleByNilLiteral {
  case none
  case some(Wrapped)
  public init(_ some: Wrapped)
  public init(nilLiteral: ())

  // mapメソッドはオプショナルではない値を返すクロージャを引数に取る。
  // 戻り値はオプショナル。
  public func map<U>(_ transform: (Wrapped) throws -> U)
      rethrows -> U?

  // flatMapメソッドはオプショナルの値を返すクロージャを引数に取る。。
  // 戻り値はオプショナル。
  public func flatMap<U>(_ transform: (Wrapped) throws -> U?)
      rethrows -> U?
}

mapの例

let a: Int? = Int("3")
let b: Int? = a.map { $0 * $0 }
print(b)
// Prints "Optional(9)"

let c: Int? = nil
let d: Int? = c.map { $0 * $0 }
print(d)
// Prints "nil"

flatMapの例

let a: Int? = Int("3")
let b: Int? = a.flatMap { x -> Int? in
  let (result, overflowed) = Int.multiplyWithOverflow(x, x)
  return overflowed ? nil : result
}
print(b)
// Prints "Optional(9)"

mapもflatMapもどちらも同じような動作をするが、クロージャの戻り値がnilになる場合がある時はflatMapを使い、それ以外はmapを使うという使い分けになるだろうか。