Pythonista3 でAVAudioSourceNode を使って音を生成して鳴らそう！ 前編


この記事は、[Pythonista3 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/pythonista3) の15日目の記事です。

https://qiita.com/advent-calendar/2022/pythonista3

一方的な偏った目線で、Pythonista3 を紹介していきます。

ほぼ毎日iPhone（Pythonista3）で、コーディングをしている者です。よろしくお願いします。

以下、私の2022年12月時点の環境です。

```sysInfo.log
--- SYSTEM INFORMATION ---
* Pythonista 3.3 (330025), Default interpreter 3.6.1
* iOS 16.1.1, model iPhone12,1, resolution (portrait) 828.0 x 1792.0 @ 2.0
```

他の環境(iPad や端末の種類、iOS のバージョン違い)では、意図としない挙動(エラーになる)なる場合もあります。ご了承ください。

ちなみに、`model iPhone12,1` は、iPhone11 です。

## 音を出したい

「プログラミングにて、コードのみで音を出す」ということは全人類の夢です（n=1）。

ちょうどAudio 関係のAPI があるので、Pythonista3 で実装しましょう。

### `AVAudioSourceNode` Class

[AVAudioSourceNode | Apple Developer Documentation](https://developer.apple.com/documentation/avfaudio/avaudiosourcenode?language=objc)

https://developer.apple.com/documentation/avfaudio/avaudiosourcenode?language=objc


> AVAudioSourceNode クラスは、AVAudioSourceNodeRenderBlock を通してレンダリング用のオーディオデータを供給することができます。kAudioUnitProperty_SetRenderCallback でオーディオユニットの入力コールバックを設定する代わりに、オーディオデータを配信するための便利な方法です。

iOS13 より追加された`AVAudioSourceNode` により、以前より簡単にオーディオデータを生成できるようになりました。

今回は`AVAudioSourceNode` の力を借りて実装していきます。

### 完成Demo

GIF なので音は出ません🙇


![img221206_201503.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/a26d6085-268b-fe90-d687-5b0f0c91ccb8.gif)




`ui` モジュールの`ui.View` を使って、Pythonista3 上でアプリのような実装にしました。

波形関係の選択スライダーも、実装負担軽減のため`ui` モジュールの`ui.Slider` を使っています。

結果のグラフィックは、生成した波形情報を（無理矢理）画像情報と見立てて、画像変化させています。

`matplotlib.image.imsave` で波形情報からイメージ変換。変換情報を`ui.Image` として、`ui.View` のView に随時反映させる流れです。

今回は「音の情報を作り、音を出す」がメインなので、メイン以外の実装はありもの（の力技）で済ませます。`objc_util` から、他API を呼び出して実装も面白そうですね。

## 実装

毎回毎回手探りで実装をしています。今回はそんな実際の流れに沿って進めていきたいと思います。

### とりあえず音が出る仕組みを知る

どのように、音が

- 作られるのか
- 処理されるのか
- 出るのか

といった点を掴めるように、`AVAudioSourceNode` をキーワードに調べていきます。

- [【iOS】Core Audioでシンセサイザーを作る - Qiita](https://qiita.com/TokyoYoshida/items/df60ea8585a0223e868b)
  - ざっくりとした、設計イメージが持てる
    - 派生した使い方を知れる
  - `AVAudioEngine` の使い方を整理できる
    - 「Node をコネクトする」「連結して音の流れを掴む」
    - Web Audio API に似ている。と知れる

https://qiita.com/TokyoYoshida/items/df60ea8585a0223e868b

- [AVAudioEngineでリアルタイムレンダリング - Speaker Deck](https://speakerdeck.com/objectiveaudio/avaudioenginederiarutaimurendaringu)
  - オーディオデータの流れを知れる
  - 永続的に処理をする大変さやシビアさ
    - 「ポインタ」？何それ美味しいの？な自分の状態の把握
  - `RenderBlock` として`Block` 処理が出てきた。と知れる

https://speakerdeck.com/objectiveaudio/avaudioenginederiarutaimurendaringu

他にも参照したサイトはありますが、[AVAudioSourceNode | Apple Developer Documentation](https://developer.apple.com/documentation/avfaudio/avaudiosourcenode?language=objc) を中心に上記情報で大枠は掴めました。

これより（今回は）Swift で書かれたコードから、Pythonista3 への書き換えを始めます。

### Pythonista3 で動くように書き換える

（前回紹介した`pdbg` モジュールを使っています）

`objc_util` の呼び出しが必要なものを用意しておきます。先に宣言しておくことで、何を使うかの把握も行いやすくなると思います。

`self.set_up` メソッドで、`objc_util` 関係をインスタンス化しています。

参考にしているSwift のコードを見ながら、ポチポチと実装です。

「こんなメソッドあるのかしら？」と不安になったら`pdbg.state` で、メソッドを確認しconsole よりコピペして実行確認をしていきます。

随時実行をしエラーを確認すると、どこでコケているか原因が早く判明できます。とにかく、実行しエラーが出ていないか確認の連続です。

なんなら、class 化せずにグローバルな変数で処理をしたり、`self` を付けてinspector で確認するのも手です。

```py
from objc_util import ObjCClass

import pdbg

AVAudioEngine = ObjCClass('AVAudioEngine')
AVAudioSourceNode = ObjCClass('AVAudioSourceNode')
AVAudioFormat = ObjCClass('AVAudioFormat')


class Synth:
  def __init__(self):
    self.audioEngine: AVAudioEngine
    self.sampleRate: float = 44100.0  # set_up メソッド: outputNode より確定
    self.deltaTime: float = 0.0  # 1/sampleRate 時間間隔

    self.set_up()

  def set_up(self):
    audioEngine = AVAudioEngine.new()
    sourceNode = AVAudioSourceNode.alloc()
    mainMixer = audioEngine.mainMixerNode()
    outputNode = audioEngine.outputNode()
    format = outputNode.inputFormatForBus_(0)

    self.sampleRate = format.sampleRate()
    self.deltaTime = 1 / self.sampleRate

    inputFormat = AVAudioFormat.alloc(
    ).initWithCommonFormat_sampleRate_channels_interleaved_(
      format.commonFormat(), self.sampleRate, 1, format.isInterleaved())


if __name__ == '__main__':
  synth = Synth()

```

`inputFormat` の内容を見てみると、`1 ch,  44100 Hz, Float32` であることがわかります:

```py
pdbg.state(inputFormat)
```

![img221204_190137.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/a7e70923-f067-1efe-ba73-a9fb424d9b53.png)


```
# --- name______
<b'AVAudioFormat': <AVAudioFormat 0x281dd0be0:  1 ch,  44100 Hz, Float32>>
# --- vars( )______
{'_as_parameter_': 10768681952,
 '_cached_methods': {'initWithCommonFormat_sampleRate_channels_interleaved_': <objc_util.ObjCInstanceMethod object at 0x123d54f98>,
                     'retain': <objc_util.ObjCInstanceMethodProxy object at 0x123d54438>},
 'ptr': 10768681952,
 'weakrefs': <WeakValueDictionary at 0x123d54358>}
# --- dir( )______
['channelCount',
 'channelLayout',
 'commonFormat',
 'copy',
 'dealloc',
 'description',
 'encodeWithCoder_',
 'formatDescription',
 'hash',
 'init',
 'initStandardFormatWithSampleRate_channelLayout_',
 'initStandardFormatWithSampleRate_channels_',
 'initWithCMAudioFormatDescription_',
 'initWithCoder_',
〜 以下略 〜

```

### `AudioBuffer` と、`ObjCBlock`

いよいよ`AVAudioSourceNode` の処理に入りそうですね。

`Block` 処理が必要そうで、`ObjCBlock` を使うと思いきや。。。

まだ、下準備があります😇

`Block` 処理に流し込むための、`AudioBuffer` （そして、その配列の`AudioBufferList`）の構造体を定義する必要があります。

#### `ctypes.Structure` で構造体を用意する

[AudioBuffer | Apple Developer Documentation](https://developer.apple.com/documentation/coreaudiotypes/audiobuffer?language=objc)

https://developer.apple.com/documentation/coreaudiotypes/audiobuffer?language=objc

Structure である`AudioBuffer` 。Pythonista3 では用意されていません。ですので、Python の`ctypes.Structure` を用いて自力で指定していきます。

AudioBuffer のDocumentation より判明したことは

- [mNumberChannels | Apple Developer Documentation](https://developer.apple.com/documentation/coreaudiotypes/audiobuffer/1421973-mnumberchannels?language=objc)
  - Objective-C
    - `UInt32 mNumberChannels;`
  - Python
  - `ctypes.c_uint32`
- [mDataByteSize | Apple Developer Documentation](https://developer.apple.com/documentation/coreaudiotypes/audiobuffer/1422352-mdatabytesize?language=objc)
  - Objective-C
    - `UInt32 mDataByteSize;`
  - Python
    - `ctypes.c_uint32`
- [mData | Apple Developer Documentation](https://developer.apple.com/documentation/coreaudiotypes/audiobuffer/1422625-mdata?language=objc)
  - Objective-C
    - `void *mData;`
  - Python
    - `ctypes.c_void_p`

[ctypes.Structure | ctypes --- Pythonのための外部関数ライブラリ — Python 3.11.0b5 ドキュメント](https://docs.python.org/ja/3/library/ctypes.html#ctypes.Structure)

https://docs.python.org/ja/3/library/ctypes.html#ctypes.Structure

[基本データ型 | ctypes --- Pythonのための外部関数ライブラリ — Python 3.11.0b5 ドキュメント](https://docs.python.org/ja/3/library/ctypes.html#ctypes-fundamental-data-types-2)

https://docs.python.org/ja/3/library/ctypes.html#ctypes-fundamental-data-types-2

`ctypes` の型で対応できることに感謝し、構造体を定義します:

```py
import ctypes

class AudioBuffer(ctypes.Structure):
  _fields_ = [
    ('mNumberChannels', ctypes.c_uint32),
    ('mDataByteSize', ctypes.c_uint32),
    ('mData', ctypes.c_void_p),
  ]
```

`_fields_` へ、tuple で定義したものを配列で格納します。

```py
import ctypes
```

としている理由は、`objc_util` でも`ctypes` モジュールを呼び出しているので「これはPython 側の処理」と、私が理解しやすくするためです。

`AudioBufferList` も、同様に

[AudioBufferList | Apple Developer Documentation](https://developer.apple.com/documentation/coreaudiotypes/audiobufferlist?language=objc)

https://developer.apple.com/documentation/coreaudiotypes/audiobufferlist?language=objc

- [mNumberBuffers | Apple Developer Documentation](https://developer.apple.com/documentation/coreaudiotypes/audiobufferlist/1421934-mnumberbuffers?language=objc)
  - Objective-C
    - `UInt32 mNumberBuffers;`
  - Python
    - `ctypes.c_uint32`
- [mBuffers | Apple Developer Documentation](https://developer.apple.com/documentation/coreaudiotypes/audiobufferlist/1422090-mbuffers?language=objc)
  - Objective-C
    - `AudioBuffer mBuffers[1];`
  - Python
    - `AudioBuffer * チャンネル数`

`mBuffers` は、モノラルであれば`1`、ステレオであれば`2` で指定します。

チャンネル数は、`AudioBufferList` 以外でも（`AVAudioFormat` 等）で使用するので、`CHANNEL` として変数で持たせます。

```py
class AudioBufferList(ctypes.Structure):
  _fields_ = [
    ('mNumberBuffers', ctypes.c_uint32),
    ('mBuffers', AudioBuffer * CHANNEL),
  ]
```

静的型付け言語に慣れてないと、なかなか難しいところですね。。。

```途中経過.py
import ctypes
from objc_util import ObjCClass

import pdbg

CHANNEL = 1

AVAudioEngine = ObjCClass('AVAudioEngine')
AVAudioSourceNode = ObjCClass('AVAudioSourceNode')
AVAudioFormat = ObjCClass('AVAudioFormat')


class AudioBuffer(ctypes.Structure):
  _fields_ = [
    ('mNumberChannels', ctypes.c_uint32),
    ('mDataByteSize', ctypes.c_uint32),
    ('mData', ctypes.c_void_p),
  ]


class AudioBufferList(ctypes.Structure):
  _fields_ = [
    ('mNumberBuffers', ctypes.c_uint32),
    ('mBuffers', AudioBuffer * CHANNEL),
  ]


class Synth:
  def __init__(self):
    self.audioEngine: AVAudioEngine
    self.sampleRate: float = 44100.0  # set_up メソッド: outputNode より確定
    self.deltaTime: float = 0.0  # 1/sampleRate 時間間隔

    self.set_up()

  def set_up(self):
    audioEngine = AVAudioEngine.new()
    sourceNode = AVAudioSourceNode.alloc()
    mainMixer = audioEngine.mainMixerNode()
    outputNode = audioEngine.outputNode()
    format = outputNode.inputFormatForBus_(0)

    self.sampleRate = format.sampleRate()
    self.deltaTime = 1 / self.sampleRate

    inputFormat = AVAudioFormat.alloc(
    ).initWithCommonFormat_sampleRate_channels_interleaved_(
      format.commonFormat(), self.sampleRate, CHANNEL, format.isInterleaved())


if __name__ == '__main__':
  synth = Synth()
```

（とりあえず、実行してエラーがないか確認してみましょう）

#### `ObjCBlock` 実装へ、、、

`AudioBuffer` 、`AudioBufferList` の準備も終えたので、いよいよ`ObjCBlock` の実装に入りましょうか、、、

え？「参考にしているコードだと`Block` 処理がない」ですと？

再掲しますが、[【iOS】Core Audioでシンセサイザーを作る - Qiita](https://qiita.com/TokyoYoshida/items/df60ea8585a0223e868b) のここの部分です（まるっと転用させてもらってます🙇）:

```Synthesizer.swift
// class Synthesizerの一部
private var audioSource: AudioSource?

lazy var sourceNode = AVAudioSourceNode { [self] (_, _, frameCount, audioBufferList) -> OSStatus in
    let abl = UnsafeMutableAudioBufferListPointer(audioBufferList)
    guard let oscillator = self.audioSource else {fatalError("Oscillator is nil")}
    for frame in 0..<Int(frameCount) {
        let sampleVal: Float = oscillator.signal(time: self.time)　// この部分
        self.time += self.deltaTime
        for buffer in abl {
            let buf: UnsafeMutableBufferPointer<Float> = UnsafeMutableBufferPointer(buffer)
            buf[frame] = sampleVal
        }
    }
    return noErr
}
```

https://qiita.com/TokyoYoshida/items/df60ea8585a0223e868b

ここの`[self]` は:

```swift
lazy var sourceNode = AVAudioSourceNode { [self] (_, _, frameCount, audioBufferList) -> OSStatus in
```

[Synthesizers With AVAudioSourceNode | LarzTech](https://larztech.com/posts/2020/05/synthesizers-avaudioengine/) の「Setup」項のコードを確認してみましょう。

`renderBlock`（=`[self]`） と読み替えることもできそうです:

```swift
sourceNode = AVAudioSourceNode(renderBlock: { (_, _, frameCount, bufferList) -> OSStatus in
    // TODO: Audio generation
    return noErr
})
```

https://larztech.com/posts/2020/05/synthesizers-avaudioengine/

[AVAudioSourceNode | Apple Developer Documentation](https://developer.apple.com/documentation/avfaudio/avaudiosourcenode?language=objc) Documentation にも、すぐさま`AVAudioSourceNodeRenderBlock` と記載がありました。

https://developer.apple.com/documentation/avfaudio/avaudiosourcenode?language=objc

では、`AVAudioSourceNodeRenderBlock` の構成をみてみることにしましょう。

[AVAudioSourceNodeRenderBlock | Apple Developer Documentation](https://developer.apple.com/documentation/avfaudio/avaudiosourcenoderenderblock?language=objc)

https://developer.apple.com/documentation/avfaudio/avaudiosourcenoderenderblock?language=objc


うーんType Alias 。。。またもや、自作実装が必要そうですね😇

返り値が`OSStatus`（`ctypes.c_int32`）で、それぞれパラメータが

- `isSilence`
- `timestamp`
- `frameCount`
- `outputData`

と、なっており参考にしているコードでは、前半2つが`_` となっているので

- `isSilence`
  - `_`
- `timestamp`
  - `_`
- `frameCount`
  - 使う
- `outputData`
  - 使う

このようになりそうですね！！

必要な情報は判明したので（行ったり来たりで忙しいですが）、Pythonista3 の`ObjCBlock` を確認しましょう。

[objc_util.ObjCBlock | objc_util — Utilities for bridging Objective-C APIs — Python 3.6.1 documentation](https://omz-software.com/pythonista/docs/ios/objc_util.html#objc_util.ObjCBlock)

https://omz-software.com/pythonista/docs/ios/objc_util.html#objc_util.ObjCBlock

`class objc_util.ObjCBlock(func, restype=None, argtypes=None)` とあるで、

- 呼び出す関数（`func`）
- 返り値（`restype`）
- 引数（`argtypes`）

これらを準備するですね！

`source_node_render` 関数の第一引数`_cmd` は、 Pythonista3 で必要な必須の引数です（型は、`ctypes.c_void_p`）。

`return` を`0` としています。Pythonista3 は、エラーであると基本的に落ちるので決め打ちの成功として`0`（`OSStatus = ctypes.c_int32`） を返すようにしてます

```py
import ctypes
from objc_util import ObjCBlock

CHANNEL = 1
OSStatus = ctypes.c_int32


class AudioBuffer(ctypes.Structure):
  _fields_ = [
    ('mNumberChannels', ctypes.c_uint32),
    ('mDataByteSize', ctypes.c_uint32),
    ('mData', ctypes.c_void_p),
  ]


class AudioBufferList(ctypes.Structure):
  _fields_ = [
    ('mNumberBuffers', ctypes.c_uint32),
    ('mBuffers', AudioBuffer * CHANNEL),
  ]


def source_node_render(_cmd, _isSilence_ptr, _timestamp_ptr, frameCount,
                       outputData_ptr) -> OSStatus:
  # todo: ここに処理を書く
  return 0


render_block = ObjCBlock(
  func=source_node_render,
  restype=OSStatus,
  # コード上`_` となっている箇所は、とりあえずの`ctypes.c_void_p`
  argtypes=[
    ctypes.c_void_p,  # _cmd の部分
    ctypes.c_void_p,  # _ -> isSilence
    ctypes.c_void_p,  # _ -> timestamp
    ctypes.c_void_p,  # frameCount
    ctypes.POINTER(AudioBufferList)  # outputData
  ])

```

実行をしても、何も起きませんが逆にエラーも出ていないので、無事に`ObjCBlock` が完了しました。

#### 現在の状況を組み上げる

`class Synth` の中に、`render_block` を入れて整えていきます。

class 内に入ったことで`self.` とつけたり引数に`self` を追加するなど、気をつけて書き換えていきましょう。

```途中経過.py
import ctypes
from objc_util import ObjCClass, ObjCBlock

import pdbg

CHANNEL = 1

OSStatus = ctypes.c_int32

AVAudioEngine = ObjCClass('AVAudioEngine')
AVAudioSourceNode = ObjCClass('AVAudioSourceNode')
AVAudioFormat = ObjCClass('AVAudioFormat')


class AudioBuffer(ctypes.Structure):
  _fields_ = [
    ('mNumberChannels', ctypes.c_uint32),
    ('mDataByteSize', ctypes.c_uint32),
    ('mData', ctypes.c_void_p),
  ]


class AudioBufferList(ctypes.Structure):
  _fields_ = [
    ('mNumberBuffers', ctypes.c_uint32),
    ('mBuffers', AudioBuffer * CHANNEL),
  ]


class Synth:
  def __init__(self):
    self.audioEngine: AVAudioEngine
    self.sampleRate: float = 44100.0  # set_up メソッド: outputNode より確定
    self.deltaTime: float = 0.0  # 1/sampleRate 時間間隔

    self.render_block = ObjCBlock(
      self.source_node_render,
      restype=OSStatus,
      argtypes=[
        ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p,
        ctypes.POINTER(AudioBufferList)
      ])

    self.set_up()

  def set_up(self):
    audioEngine = AVAudioEngine.new()
    sourceNode = AVAudioSourceNode.alloc()
    mainMixer = audioEngine.mainMixerNode()
    outputNode = audioEngine.outputNode()
    format = outputNode.inputFormatForBus_(0)

    self.sampleRate = format.sampleRate()
    self.deltaTime = 1 / self.sampleRate

    inputFormat = AVAudioFormat.alloc(
    ).initWithCommonFormat_sampleRate_channels_interleaved_(
      format.commonFormat(), self.sampleRate, CHANNEL, format.isInterleaved())

    sourceNode.initWithFormat_renderBlock_(inputFormat, self.render_block)

  def source_node_render(self, _cmd, _isSilence_ptr, _timestamp_ptr,
                         frameCount, outputData_ptr) -> OSStatus:
    # todo: ここに処理を書く
    return 0


if __name__ == '__main__':
  synth = Synth()


```

### 先に出力を繋いでおきますか

`Block` 実装という一つの山場を越えたので、`sourceNode.initWithFormat_renderBlock_` 以降の処理もゴリゴリ実装していきましょう。

比較的に参考コードを素直に実装していけば、なんとかなりそうです。

```
audioEngine にsourceNode をattachNode

sourceNode（音のデータを処理） -> mainMixer（全体の調整役） -> outputNode（ここから音が出る）
```

node のコネクト処理:

```py
audioEngine.attachNode_(sourceNode)
sourceNode.volume = 0.5  # ボリュームを抑えている

audioEngine.connect_to_format_(sourceNode, mainMixer, inputFormat)
audioEngine.connect_to_format_(mainMixer, outputNode, inputFormat)

audioEngine.prepare()
self.audioEngine = audioEngine
```

開始と終了の処理:

```py
  def start(self):
    self.audioEngine.startAndReturnError_(None)

  def stop(self):
    self.audioEngine.stop()
```

これらを`Synth` class に取り込み、`.start()` を呼び出すと、`Block` 処理が走ることが確認できます。

```途中経過.py
import ctypes
from objc_util import ObjCClass, ObjCBlock

import pdbg

CHANNEL = 1

OSStatus = ctypes.c_int32

AVAudioEngine = ObjCClass('AVAudioEngine')
AVAudioSourceNode = ObjCClass('AVAudioSourceNode')
AVAudioFormat = ObjCClass('AVAudioFormat')


class AudioBuffer(ctypes.Structure):
  _fields_ = [
    ('mNumberChannels', ctypes.c_uint32),
    ('mDataByteSize', ctypes.c_uint32),
    ('mData', ctypes.c_void_p),
  ]


class AudioBufferList(ctypes.Structure):
  _fields_ = [
    ('mNumberBuffers', ctypes.c_uint32),
    ('mBuffers', AudioBuffer * CHANNEL),
  ]


class Synth:
  def __init__(self):
    self.audioEngine: AVAudioEngine
    self.sampleRate: float = 44100.0  # set_up メソッド: outputNode より確定
    self.deltaTime: float = 0.0  # 1/sampleRate 時間間隔

    self.render_block = ObjCBlock(
      self.source_node_render,
      restype=OSStatus,
      argtypes=[
        ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p,
        ctypes.POINTER(AudioBufferList)
      ])

    self.set_up()

  def set_up(self):
    audioEngine = AVAudioEngine.new()
    sourceNode = AVAudioSourceNode.alloc()
    mainMixer = audioEngine.mainMixerNode()
    outputNode = audioEngine.outputNode()
    format = outputNode.inputFormatForBus_(0)

    self.sampleRate = format.sampleRate()
    self.deltaTime = 1 / self.sampleRate

    inputFormat = AVAudioFormat.alloc(
    ).initWithCommonFormat_sampleRate_channels_interleaved_(
      format.commonFormat(), self.sampleRate, CHANNEL, format.isInterleaved())

    sourceNode.initWithFormat_renderBlock_(inputFormat, self.render_block)

    audioEngine.attachNode_(sourceNode)
    sourceNode.volume = 0.2

    audioEngine.connect_to_format_(sourceNode, mainMixer, inputFormat)
    audioEngine.connect_to_format_(mainMixer, outputNode, inputFormat)

    audioEngine.prepare()
    self.audioEngine = audioEngine

  def source_node_render(self, _cmd, _isSilence_ptr, _timestamp_ptr,
                         frameCount, outputData_ptr) -> OSStatus:
    # todo: ここに処理を書く
    print('test')
    return 0

  def start(self):
    self.audioEngine.startAndReturnError_(None)

  def stop(self):
    self.audioEngine.stop()


if __name__ == '__main__':
  synth = Synth()
  synth.start()

```

![img221205_002838.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/0010648d-8c10-dcaf-4bb7-2d4da9585457.gif)


`source_node_render` メソッド内に`print('test')` と出力指示をしました。

結果console に`test` が乱立しています（まだ「音」の設定はできていませんので、無音です）。

また、恐ろしい話ではあるのですが、`stop` の処理を書いたのに関わらず、現在止める方法が、一度アプリを強制終了です。。。（`time` モジュールなど使って、`n` 秒後に`stop` を呼び出し、終了させるなど）

今回の終わりまでには、終了指示だせるようになりますので、いまはそのままにしておきます。

（はやくsine波 出したい）

### sine波を生成する処理

みんな大好きsine波ちゃんです！！

`ObjCBlock` の、`func` にて定義した`source_node_render` の中に、sine波を生成する計算式を書き上げます。

計算した数値の結果を、いい感じに`for` でぶん回しながら

`ctypes.POINTER(AudioBufferList)` = `outputData_ptr` の各buffer にぶち込んでいくのです。

`AVAudioSourceNode` へ`renderBlock` を格納するまでは終えているので、あとは式を書き込むだけですねぇ☺️

```py
# 引数の型を忘れそうなので、アノテーション入れました
def source_node_render(self,
                       _cmd: ctypes.c_void_p,
                       _isSilence_ptr: ctypes.c_void_p,
                       _timestamp_ptr: ctypes.c_void_p,
                       frameCount: ctypes.c_void_p,
                       outputData_ptr: ctypes.POINTER) -> OSStatus:
  # todo: ここに処理を書く
  ablPointer = outputData_ptr.contents
  for frame in range(frameCount):
    sampleVal = sin(440.0 * 2.0 * pi * self.timex)
    self.timex += self.deltaTime

    for bufferr in range(ablPointer.mNumberBuffers):
      _mData = ablPointer.mBuffers[bufferr].mData
      _pointer = ctypes.POINTER(ctypes.c_float * frameCount)
      buffer = ctypes.cast(_mData, _pointer).contents
      buffer[frame] = sampleVal
    #print('test')
  return 0

```

`math` モジュールで`sin`, `pi` を呼び出しています。この処理により、sine波を生成:

```py
sampleVal = sin(440.0 * 2.0 * pi * self.timex)
```

`440.0` を`880.0` にすると、1オクターブ高いラ`A` になります。

`AudioBufferList` のポインタへ`for` で回したものをしかるべき場所に格納している（みたいです）:

```py
for bufferr in range(ablPointer.mNumberBuffers):
  _mData = ablPointer.mBuffers[bufferr].mData
  _pointer = ctypes.POINTER(ctypes.c_float * frameCount)
  buffer = ctypes.cast(_mData, _pointer).contents
  buffer[frame] = sampleVal
```

正直なところ、ポインタの理解が追いついていません。

Objective-C のDocumentation で、引数に`*` が先頭についている場合にはポインタを意識して、試行錯誤を繰り返しています。

コードの全体としては:

```音を止めるには強制終了.py
from math import sin, pi
import ctypes

from objc_util import ObjCClass, ObjCBlock

import pdbg

CHANNEL = 1

OSStatus = ctypes.c_int32

AVAudioEngine = ObjCClass('AVAudioEngine')
AVAudioSourceNode = ObjCClass('AVAudioSourceNode')
AVAudioFormat = ObjCClass('AVAudioFormat')


class AudioBuffer(ctypes.Structure):
  _fields_ = [
    ('mNumberChannels', ctypes.c_uint32),
    ('mDataByteSize', ctypes.c_uint32),
    ('mData', ctypes.c_void_p),
  ]


class AudioBufferList(ctypes.Structure):
  _fields_ = [
    ('mNumberBuffers', ctypes.c_uint32),
    ('mBuffers', AudioBuffer * CHANNEL),
  ]


class Synth:
  def __init__(self):
    self.audioEngine: AVAudioEngine
    self.sampleRate: float = 44100.0  # set_up メソッド: outputNode より確定
    self.deltaTime: float = 0.0  # 1/sampleRate 時間間隔
    self.timex: float = 0.0  # Render の間隔カウンター

    self.render_block = ObjCBlock(
      self.source_node_render,
      restype=OSStatus,
      argtypes=[
        ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p,
        ctypes.POINTER(AudioBufferList)
      ])

    self.set_up()

  def set_up(self):
    audioEngine = AVAudioEngine.new()
    sourceNode = AVAudioSourceNode.alloc()
    mainMixer = audioEngine.mainMixerNode()
    outputNode = audioEngine.outputNode()
    format = outputNode.inputFormatForBus_(0)

    self.sampleRate = format.sampleRate()
    self.deltaTime = 1 / self.sampleRate

    inputFormat = AVAudioFormat.alloc(
    ).initWithCommonFormat_sampleRate_channels_interleaved_(
      format.commonFormat(), self.sampleRate, CHANNEL, format.isInterleaved())

    sourceNode.initWithFormat_renderBlock_(inputFormat, self.render_block)

    audioEngine.attachNode_(sourceNode)
    sourceNode.volume = 0.1

    audioEngine.connect_to_format_(sourceNode, mainMixer, inputFormat)
    audioEngine.connect_to_format_(mainMixer, outputNode, inputFormat)

    audioEngine.prepare()
    self.audioEngine = audioEngine

  def source_node_render(self,
                         _cmd: ctypes.c_void_p,
                         _isSilence_ptr: ctypes.c_void_p,
                         _timestamp_ptr: ctypes.c_void_p,
                         frameCount: ctypes.c_void_p,
                         outputData_ptr: ctypes.POINTER) -> OSStatus:
    # todo: ここに処理を書く
    ablPointer = outputData_ptr.contents
    for frame in range(frameCount):
      sampleVal = sin(440.0 * 2.0 * pi * self.timex)
      #print(sampleVal)
      self.timex += self.deltaTime

      for bufferr in range(ablPointer.mNumberBuffers):
        _mData = ablPointer.mBuffers[bufferr].mData
        _pointer = ctypes.POINTER(ctypes.c_float * frameCount)
        buffer = ctypes.cast(_mData, _pointer).contents
        buffer[frame] = sampleVal
    return 0

  def start(self):
    self.audioEngine.startAndReturnError_(None)

  def stop(self):
    self.audioEngine.stop()


if __name__ == '__main__':
  synth = Synth()
  synth.start()

```

sine波生成のために、`__init__` に`self.timex` を定義してRender 内で、数値をアップデートさせています。

`sampleVal` を`print` で出力するとそれっぽい数値が出ていますね！



![img221205_131109.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/8506e225-6d77-b9fe-6ce1-973f462de744.gif)



`print` 出力中は音が出なくなってしまうので、コメントアウトすると、sine波が「ポーーー」と鳴りつづけています！！

もし音が出ない場合はマナーモードの可能性があるので、解除し再実行してください（音量注意）。

### 音を止めたい

「音を出したい」衝動が達成すると、次は「音を止めたい」ですね。

人間の欲の深さは恐ろしいものです。自分で始めた物語、しっかり物語を終えましょう。

`ui` モジュールでアプリライクな形状にして、View を閉じ（終了し）たら音が止まるようにします。

```fix.py
from math import sin, pi
import ctypes

from objc_util import ObjCClass, ObjCBlock
import ui

import pdbg

CHANNEL = 1

OSStatus = ctypes.c_int32

AVAudioEngine = ObjCClass('AVAudioEngine')
AVAudioSourceNode = ObjCClass('AVAudioSourceNode')
AVAudioFormat = ObjCClass('AVAudioFormat')


class AudioBuffer(ctypes.Structure):
  _fields_ = [
    ('mNumberChannels', ctypes.c_uint32),
    ('mDataByteSize', ctypes.c_uint32),
    ('mData', ctypes.c_void_p),
  ]


class AudioBufferList(ctypes.Structure):
  _fields_ = [
    ('mNumberBuffers', ctypes.c_uint32),
    ('mBuffers', AudioBuffer * CHANNEL),
  ]


class Synth:
  def __init__(self):
    self.audioEngine: AVAudioEngine
    self.sampleRate: float = 44100.0  # set_up メソッド: outputNode より確定
    self.deltaTime: float = 0.0  # 1/sampleRate 時間間隔
    self.timex: float = 0.0  # Render の間隔カウンター

    self.render_block = ObjCBlock(
      self.source_node_render,
      restype=OSStatus,
      argtypes=[
        ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p,
        ctypes.POINTER(AudioBufferList)
      ])

    self.set_up()

  def set_up(self):
    audioEngine = AVAudioEngine.new()
    sourceNode = AVAudioSourceNode.alloc()
    mainMixer = audioEngine.mainMixerNode()
    outputNode = audioEngine.outputNode()
    format = outputNode.inputFormatForBus_(0)

    self.sampleRate = format.sampleRate()
    self.deltaTime = 1 / self.sampleRate

    inputFormat = AVAudioFormat.alloc(
    ).initWithCommonFormat_sampleRate_channels_interleaved_(
      format.commonFormat(), self.sampleRate, CHANNEL, format.isInterleaved())

    sourceNode.initWithFormat_renderBlock_(inputFormat, self.render_block)

    audioEngine.attachNode_(sourceNode)
    sourceNode.volume = 0.1

    audioEngine.connect_to_format_(sourceNode, mainMixer, inputFormat)
    audioEngine.connect_to_format_(mainMixer, outputNode, inputFormat)

    audioEngine.prepare()
    self.audioEngine = audioEngine

  def source_node_render(self,
                         _cmd: ctypes.c_void_p,
                         _isSilence_ptr: ctypes.c_void_p,
                         _timestamp_ptr: ctypes.c_void_p,
                         frameCount: ctypes.c_void_p,
                         outputData_ptr: ctypes.POINTER) -> OSStatus:
    # todo: ここに処理を書く
    ablPointer = outputData_ptr.contents
    for frame in range(frameCount):
      sampleVal = sin(440.0 * 2.0 * pi * self.timex)
      #self._outlog(sampleVal)
      self.timex += self.deltaTime

      for bufferr in range(ablPointer.mNumberBuffers):
        _mData = ablPointer.mBuffers[bufferr].mData
        _pointer = ctypes.POINTER(ctypes.c_float * frameCount)
        buffer = ctypes.cast(_mData, _pointer).contents
        buffer[frame] = sampleVal
    return 0

  @ui.in_background
  def _outlog(self, value):
    """ 確認用で基本呼び出さない """
    print(value)

  def start(self):
    self.audioEngine.startAndReturnError_(None)

  def stop(self):
    self.audioEngine.stop()


class View(ui.View):
  def __init__(self, *args, **kwargs):
    ui.View.__init__(self, *args, **kwargs)
    self.synth = Synth()
    self.synth.start()

  def will_close(self):
    self.synth.stop()


if __name__ == '__main__':
  view = View()
  view.present()
  #view.present(style='fullscreen', orientations=['portrait'])

```


![img221205_140152.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953777/6989226f-d3c0-09c6-77b3-f538c7e9d144.gif)


`def will_close(self):` の「閉じる指示をした時」に、`Synth` の`.stop` メソッドを呼ぶようにしています。

#### tips その1（`ui.in_background`）

GIF だと、音が出ないので無理やり`print` で数値を出すようにしています（`Synth._outlog`）。

処理の関係で、出力音にノイズが乗る可能性がありますが。。。

デコレーション`@ui.in_background` で、音が出つつconsole へ出力できるように無理やり実装しています。無理やりなので、View が閉じても裏側で走り続けてしまっています。

普段`Synth._outlog` を呼ばなくていいですし、書かなくてもいいです。

#### tips その2（`View.present`）

```py
View.present()
```

と引数なしに呼び出すと、`sheet` のView が出てきます。

また背景色を指定しないと、透過状態で出現するので、console が見える状態になります。

```py
View.present(style='fullscreen')
```

`fullscreen` 指定にすると、背景色を指定しない場合には背景が黒になります（想定するに、実際は黒ではなく透過で`fullscreen` の背面に何も無いので事実黒く見える）。

## 参考文献

- [【iOS】Core Audioでシンセサイザーを作る - Qiita](https://qiita.com/TokyoYoshida/items/df60ea8585a0223e868b)
- [AVAudioEngineでリアルタイムレンダリング by 八十嶋祐樹 | トーク | iOSDC Japan 2020 - fortee.jp](https://fortee.jp/iosdc-japan-2020/proposal/2aecd4ec-e173-45fe-8d49-aec1aabc5526)
- [AVAudioEngineでリアルタイムレンダリング - Speaker Deck](https://speakerdeck.com/objectiveaudio/avaudioenginederiarutaimurendaringu)
- [Building a Synthesizer in Swift. Making audio waveforms with… | by SwiftMoji | Better Programming](https://betterprogramming.pub/building-a-synthesizer-in-swift-866cd15b731)
- [Building a Signal Generator | Apple Developer Documentation](https://developer.apple.com/documentation/avfaudio/audio_engine/building_a_signal_generator)
- [Synthesizers With AVAudioSourceNode | LarzTech](https://larztech.com/posts/2020/05/synthesizers-avaudioengine/)

## 次回は

今回で「音を出す」「音を止める」まで達成しました。音声ファイルを用意せずにコードのみで、音を作れるのですね☺️

`source_node_render` 関数内の数式を置き換えると、色々な音になりますので、ガチャガチャ書き換えて楽しむのもいいかもしれません。

冒頭でお見せした、完成Demo に近づくため次回は

- 出力音の種類を変更する
- 出ている音の可視化

の実装を進めていきます。

sine波（正弦波）が登場しましたが、矩形波などシンセサイザーで聞いたことのある波を出しましょう。

ここまで、読んでいただきありがとうございました。

## せんでん

### Discord

Pythonista3 の日本語コミュニティーがあります。みなさん優しくて、わからないところも親身に教えてくれるのでこの機会に覗いてみてください。

https://t.co/0IOW2hn1VC

### 書籍

iPhone/iPad でプログラミングする最強の本。

- [Python や Jupyter で iPhone/iPad 先端機能を簡単･自由にプログラミング！「活用篇」：hirax](https://techbookfest.org/product/1WiyV4LEev3f4YrzesRKWV?productVariantID=axX32srsSgtLtZWUtjkj99)

https://techbookfest.org/product/1WiyV4LEev3f4YrzesRKWV?productVariantID=axX32srsSgtLtZWUtjkj99

- [Python や Jupyter で iPhone/iPad 先端機能を簡単･自由にプログラミング！「土台篇」：hirax](https://techbookfest.org/product/wTZTyeibm5GQ5XgdfMrEBV?productVariantID=kRDmN1udbEYZUWbETdwL8r)

https://techbookfest.org/product/wTZTyeibm5GQ5XgdfMrEBV?productVariantID=kRDmN1udbEYZUWbETdwL8r

### その他

- サンプルコード

[Pythonista3 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/pythonista3) でのコードをまとめているリポジトリがあります。

コードのエラーや変なところや改善点など。ご指摘やPR お待ちしておりますー

https://github.com/pome-ta/Pythonista3AdventCalendar2022sampleCode

- Twitter

なんしかガチャガチャしていますが、お気兼ねなくお声がけくださいませー

https://twitter.com/pome_ta93

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">やれるか、やれないか。ではなく、やるんだけども、紹介説明することは尽きないと思うけど、締め切り守れるか？って話よ！（クズ）<br><br>Pythonista3 Advent Calendar 2022 <a href="https://t.co/JKUxA525Pt">https://t.co/JKUxA525Pt</a> <a href="https://twitter.com/hashtag/Qiita?src=hash&amp;ref_src=twsrc%5Etfw">#Qiita</a></p>&mdash; pome-ta (@pome_ta93) <a href="https://twitter.com/pome_ta93/status/1588570697777152006?ref_src=twsrc%5Etfw">November 4, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

- GitHub

基本的にGitHub にコードをあげているので、何にハマって何を実装しているのか観測できると思います。

https://github.com/pome-ta