2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonista3Advent Calendar 2022

Day 15

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

Last updated at Posted at 2022-12-14

この記事は、Pythonista3 Advent Calendar 2022 の15日目の記事です。

一方的な偏った目線で、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

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

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

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

完成Demo

GIF なので音は出ません🙇

img221206_201503.gif

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

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

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

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

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

実装

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

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

どのように、音が

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

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

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

他にも参照したサイトはありますが、AVAudioSourceNode | Apple Developer Documentation を中心に上記情報で大枠は掴めました。

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

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

(前回紹介したpdbg モジュールを使っています)

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

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

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

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

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

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

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 であることがわかります:

pdbg.state(inputFormat)

img221204_190137.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

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

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

ctypes.Structure | ctypes --- Pythonのための外部関数ライブラリ — Python 3.11.0b5 ドキュメント

基本データ型 | ctypes --- Pythonのための外部関数ライブラリ — Python 3.11.0b5 ドキュメント

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

import ctypes

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

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

import ctypes

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

AudioBufferList も、同様に

AudioBufferList | Apple Developer Documentation

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

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

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 実装へ、、、

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

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

再掲しますが、【iOS】Core Audioでシンセサイザーを作る - Qiita のここの部分です(まるっと転用させてもらってます🙇):

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
}

ここの[self] は:

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

Synthesizers With AVAudioSourceNode | LarzTech の「Setup」項のコードを確認してみましょう。

renderBlock(=[self]) と読み替えることもできそうです:

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

AVAudioSourceNode | Apple Developer Documentation Documentation にも、すぐさまAVAudioSourceNodeRenderBlock と記載がありました。

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

AVAudioSourceNodeRenderBlock | Apple Developer Documentation

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

返り値がOSStatusctypes.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

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

  • 呼び出す関数(func
  • 返り値(restype
  • 引数(argtypes

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

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

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

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 のコネクト処理:

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

開始と終了の処理:

  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

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 にぶち込んでいくのです。

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

# 引数の型を忘れそうなので、アノテーション入れました
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波を生成:

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

440.0880.0 にすると、1オクターブ高いラA になります。

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

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 内で、数値をアップデートさせています。

sampleValprint で出力するとそれっぽい数値が出ていますね!

img221205_131109.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

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

View.present()

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

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

View.present(style='fullscreen')

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

参考文献

次回は

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

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

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

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

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

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

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

せんでん

Discord

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

書籍

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

その他

  • サンプルコード

Pythonista3 Advent Calendar 2022 でのコードをまとめているリポジトリがあります。

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

  • Twitter

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

  • GitHub

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

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?