Home

Awesome

portaudio

module for godot

Table of contents

Why?

Pros

Cons

*1) only applies when utilizing c++ audio callback, not GDScript bindings

Setup:

via submodule

via download

Building

Edit the SCsub file and comment / uncomment the desired host apis in use_host_api = [. When building godot it will check if the host api is supported for the platform (windows, linux or osx) and available inside the use_host_api-array. Only then the host api will be available.

Driver

Driverwindowsx11osx
ASIOWorking (*1)Not TestedNot Available
DSOUNDWorkingNot AvailableNot Available
WASAPIWorking (*2)Not AvailableNot Available
WDM/KSWorkingNot AvailableNot Available
WMMEErrorNot AvailableNot Available
JACKNot AvailableNot TestedNot Available
ALSANot AvailableNot TestedNot Available
ASIHPINot AvailableNot TestedNot Available
COREAUDIONot AvailableNot AvailableWorking
OSSNot AvailableNot TestedNot Available

*1) requires that the enduser has asio drivers installed (ex. ASIO4ALL)
*2) if used godots wasapi driver will be disabled

Godot Integration

Nodes

A PortAudioTestNode exists, simply add it to a scene via the editor and it will enumerate your host apis and devices and print them out:
<img src="/doc/test_node.png" width="400">

Example GDScripts

Tone Generator:

Youtube Video:
Godot - PortAudio Module

extends Node

class_name ToneGenerator

# Member variables
var sample_rate = 8000
var frequenzy_hz = 40
var duration = 1

# Constants
const SAMPLE_SIZE = 4 # float
	
func _ready():
	start_port_audio()
	return

func start_port_audio():
	var samples = duration * sample_rate
	
	var device_index = PortAudio.get_default_output_device()
	
	var out_p = PortAudioStreamParameter.new()
	out_p.set_device_index(device_index)
	out_p.set_channel_count(2)
	
	var stream = PortAudioStream.new()
	stream.set_output_stream_parameter(out_p)
	stream.set_sample_format(PortAudioStreamParameter.FLOAT_32)
	stream.set_frames_per_buffer(samples * SAMPLE_SIZE)

	var audio_callback = funcref(self, "audio_callback")
	var user_data = samples
	var err = PortAudio.open_stream(stream, audio_callback, user_data)
	if(err != PortAudio.NO_ERROR):
		push_error("open_stream: %s" % err)
		
	err = PortAudio.start_stream(stream)	
	if(err != PortAudio.NO_ERROR):
		push_error("start_stream: %s" % err)
		return

# Audio Callback
func audio_callback(data : PortAudioCallbackData):
	var buff = data.get_output_buffer()
	var samples = data.get_user_data()
	for i in range (samples):
		var sample : float = sin(2 * PI * float(i) / ( sample_rate / frequenzy_hz))
		buff.put_float(sample)
	return PortAudio.CONTINUE

C++

This module will add PortAudio to the include path. It allows to work with PortAudio s library directly:

#include <portaudio.h>

For tutorials on how to use PortAudio have a look at the official documentation: http://portaudio.com/docs/v19-doxydocs/initializing_portaudio.html

Gotchas and Tips

Callback Tips

Please refer to https://app.assembla.com/wiki/show/portaudio/Tips_Callbacks for a better understand about the delicate callback function.

Especially:

Regarding crossing language boundaries such as calling Java or Lua:
In general it should be avoided. But, in fact, Lua has a bounded time GC so, 
like the Supercollider language, it could be used in a PortAudio callback 
so long as you make sure it doesn’t violate the points I made above: 
i.e. avoid calling the OS level memory allocator, file I/O, or doing other things 
which violate the above in your Lua user functions. . 
That said, running Lua in a PortAudio callback is definitely at the experimental end of the spectrum.

Exposing PortAudio to GDScript will have some performance overhead and introduces additional audio latency. If you are looking for low latency it would be best to utilzie the CallbackFunction in C++. To get a better understanding of how long the callback took, the duration in μs (Microsecond) can be obtained from the callback data PortAudioCallbackData::get_last_call_duration().

Frames Per Buffer

The callback provides a frames_per_buffer-variable. This does not represent bytes. Depending on the format (FLOAT_32 = 4bytes, INT_16 = 2bytes) and channels the buffer size can be calculated.

bytes_per_channel = (FLOAT_32 = 4 bytes) (INT_16 = 2byte) (INT_8 = 1byte)
buffer_size = frames_per_buffer * channels * bytes_per_channel

Ensure that each callback the buffer is filled up correctly or it could result in slow and crackling audio. The same also applies when utilizing blocking mode via write().

Time spend in Callback

If the execution time of the callback function is longer than the playback data provided to the buffer the audio might also become slow and crackling. To calculate the playback duration of the buffer the requested frames can be divided by the sample rate.

time_request_seconds = frames_per_buffer / sample_rate

The execution time of the audio loop has to be faster than this time.

Callback Result

The return value of the callback indicates if it should continue to be called or it can be signaled to stop.
C++:

enum PortAudioCallbackResult {
	CONTINUE = 0,
	COMPLETE = 1,
	ABORT = 2,
};

GDScript:

PortAudio.CONTINUE
PortAudio.COMPLETE
PortAudio.ABORT

TODO

Dependencies

Links

Godot:
https://github.com/godotengine/godot

Portaudio:
https://app.assembla.com/spaces/portaudio/git/source
http://www.portaudio.com/
https://github.com/PortAudio/portaudio

ASIO SDK 2.0:
https://www.steinberg.net/en/company/developers.html

Asio4All Driver:
http://www.asio4all.org/