
--libsoundio binding.
--Written by Cosmin Apreutesei. Public Domain.

if not ... then require'libsoundio_demo'; return end

local ffi = require'ffi'
require'libsoundio_h'

local C = ffi.load'soundio'
local M = {C = C}

--helpers --------------------------------------------------------------------

local function ptr(p)
	return p ~= nil and p or nil
end

local function checkptr(p)
	return assert(ptr(p), 'non-null pointer expected')
end

local function check(err)
	if err == 0 then return end
	error(ffi.string(C.soundio_strerror(err)), 3)
end

local function vprops(index_table, property_table)
	return function(self, k)
		local get = property_table[k]
		if get then return get(self) end
		local v = index_table[k]
		if not v then error('invalid property '..k, 2) end
		return v
	end
end

--soundio --------------------------------------------------------------------

function M.new()
	local self = C.soundio_create()
	assert(self ~= nil)
	ffi.gc(self, self.free)
	return self
end

local sio = {}
sio.__index = sio

function sio:free()
	ffi.gc(self, nil)
	C.soundio_destroy(self)
end

--backends -------------------------------------------------------------------

M.backends = {
	jack       = C.SoundIoBackendJack,
	pulseaudio = C.SoundIoBackendPulseAudio,
	alsa       = C.SoundIoBackendAlsa,
	coreaudio  = C.SoundIoBackendCoreAudio,
	wasapi     = C.SoundIoBackendWasapi,
	dummy      = C.SoundIoBackendDummy,
}
local backend_names = {}
for k,v in pairs(M.backends) do
	backend_names[tonumber(v)] = k
end
local function tobackend(backend)
	return (assert(M.backends[backend:lower()], 'invalid backend'))
end

function sio:connect(backend)
	check(backend
		and C.soundio_connect_backend(self, tobackend(backend))
		or C.soundio_connect(self))
	self:flush_events()
end

sio.disconnect = C.soundio_disconnect

function sio:backends(which)
	if not which then --enumerate: backend, backend_name
		local i = -1
		return function()
			i = i + 1
			local backend = C.soundio_get_backend(self, i)
			if backend == C.SoundIoBackendNone then return end
			return assert(backend_names[backend])
		end
	elseif which == '#' then --count
		return C.soundio_backend_count(self)
	else --check by name
		return C.soundio_have_backend(self, tobackend(which)) == 1
	end
end

--events ---------------------------------------------------------------------

sio.flush_events = C.soundio_flush_events
sio.wait_events = C.soundio_wait_events
sio.wakeup = C.soundio_wakeup

--device list ----------------------------------------------------------------

sio.force_device_scan = C.soundio_force_device_scan

local function devices_api(getbyindex, count, getdefaultindex)
	local function get(self, i)
		return ffi.gc(checkptr(getbyindex(self, i)), C.soundio_device_unref)
	end
	local function api(self, which, filter)
		if not which then --iterate filtered
			local i = 0
			local n = count(self)
			return function()
				while i < n do
					local dev = get(self, i)
					i = i + 1
					if filter(dev) then
						return dev
					end
				end
			end
		elseif which == '#' then --count
			if filter == nil then
				return count(self)
			end
			--count filtered
			local n = 0
			for dev in api(self, nil, filter) do
				n = n + 1
			end
			return n
		elseif which == '*' then --get default
			if filter == nil then
				local i = getdefaultindex(self)
				return i >= 0 and get(self, i) or nil
			end
			--get default filtered
			local dev = api(self, '*')
			return dev and api(self, dev.id, filter)
		else --find by id
			for dev in api(self, nil, filter) do
				if dev.id == which then
					return dev
				end
			end
		end
	end
	return api
end

local input_devices = devices_api(
	C.soundio_get_input_device,
	C.soundio_input_device_count,
	C.soundio_default_input_device_index)

local output_devices = devices_api(
	C.soundio_get_output_device,
	C.soundio_output_device_count,
	C.soundio_default_output_device_index)

local function fraw(dev) return dev.is_raw end
local function fnotraw(dev) return not dev.is_raw end
local function ftrue(dev) return true end

function sio:devices(which, raw)
	local filter =
		raw == true and fraw
		or raw == false and fnotraw
		or raw == nil and ftrue
		or raw --custom filter
	if not which then --iterate all
		local next_dev = input_devices(self, nil, filter)
		local on_input = true
		return function()
			local dev = next_dev()
			if not dev and on_input then
				next_dev = output_devices(self, nil, filter)
				on_input = false
				dev = next_dev()
			end
			return dev
		end
	elseif which == '#' then --count all
		return
			input_devices(self, '#', filter) +
			output_devices(self, '#', filter)
	elseif which == '#i' then
		return input_devices(self, '#', filter)
	elseif which == '#o' then
		return output_devices(self, '#', filter)
	elseif which == '*i' then
		return input_devices(self, '*', filter)
	elseif which == '*o' then
		return output_devices(self, '*', filter)
	else --find by id
		return
			input_devices(self, which, filter) or
			output_devices(self, which, filter)
	end
end

--devices --------------------------------------------------------------------

local dev = {}

function dev:ref() C.soundio_device_ref(self); return self; end
function dev:unref() C.soundio_device_unref(self); return self; end

dev.supports_format = C.soundio_device_supports_format
dev.supports_layout = C.soundio_device_supports_layout
dev.supports_sample_rate = C.soundio_device_supports_sample_rate
dev.nearest_sample_rate = C.soundio_device_nearest_sample_rate

local devprop = {}
function devprop:id() return ffi.string(self.id_ptr) end
function devprop:name() return ffi.string(self.name_ptr) end
function devprop:aim() return
	self.aim_enum == C.SoundIoDeviceAimInput and 'i' or 'o'
end
function devprop:probe_error()
	return self.probe_error_code ~= 0 and self.probe_error_code or nil
end

dev.__eq = C.soundio_device_equal
dev.__index = vprops(dev, devprop)

--formats --------------------------------------------------------------------

function M.format_string(format)
	return ffi.string(C.soundio_format_string(format))
end

M.bytes_per_sample = C.soundio_get_bytes_per_sample

function M.bytes_per_frame(format, channel_count)
	return M.bytes_per_sample(format) * channel_count
end

function M.bytes_per_second(format, channel_count, sample_rate)
	return M.bytes_per_frame(format, channel_count) * sample_rate
end

local sample_ranges = {
	[C.SoundIoFormatS8]        = { -2^7,  2^7-1},
	[C.SoundIoFormatU8]        = {    0,  2^8-1},
	[C.SoundIoFormatS16NE]     = {-2^15, 2^15-1},
	[C.SoundIoFormatU16NE]     = {    0, 2^16-1},
	[C.SoundIoFormatS24LE]     = {-2^23, 2^23-1},
	[C.SoundIoFormatU24LE]     = {    0, 2^24-1},
	[C.SoundIoFormatS32LE]     = {-2^31, 2^31-1},
	[C.SoundIoFormatU32LE]     = {    0, 2^32-1},
	[C.SoundIoFormatFloat32LE] = {    0, 1},
	[C.SoundIoFormatFloat64LE] = {    0, 1},
}

function M.sample_range(format)
	return unpack(sample_ranges[tonumber(format)], 1, 2)
end

--channels -------------------------------------------------------------------

function M.channel_id(name)
	local chan = C.soundio_parse_channel_id(name, #name)
	if chan == SoundIoChannelIdInvalid then return end
	return chan
end

function M.channel_name(channel)
	return ffi.string(C.soundio_get_channel_name(channel))
end

--channel layouts ------------------------------------------------------------

local layout = {}
local layoutprop = {}
layout.__eq = C.soundio_channel_layout_equal
layout.__index = vprops(layout, layoutprop)

layoutprop.name = devprop.name

function M.builtin_layouts(which, channel_count)
	if not which then --iterate
		local i = -1
		local n = C.soundio_channel_layout_builtin_count()
		return function()
			i = i + 1
			if i >= n then return end
			return C.soundio_channel_layout_get_builtin(i)
		end
	elseif which == '#' then --count
		return C.soundio_channel_layout_builtin_count()
	elseif which == '*' then --default layout per channel count
		return C.soundio_channel_layout_get_default(channel_count)
	end
end

function layout:find_channel(channel)
	if type(channel) == 'string' then
		channel = assert(M.channel_id(channel), 'invalid channel name')
	end
	local i = C.soundio_channel_layout_find_channel(self, channel)
	return i ~= -1 and i or nil
end

function layout:detect_builtin()
	return C.soundio_channel_layout_detect_builtin(self)
end

function dev:sort_layouts()
	C.soundio_sort_channel_layouts(self.layouts, self.layout_count)
end

--device info dump -----------------------------------------------------------

function dev:print(print_)
	local print = print_ or print
	print('Device Info')
	print('  id      : '..self.id)
	print('  name    : '..self.name)
	print('  aim     : '..self.aim)
	print('  layouts : ')
	for i = 0, self.layout_count-1 do
		local layout = self.layouts[i]
		print('   '..i..': ')
		print('     name     : '..layout.name..
			(self.current_layout == layout and ' (current)' or ''))
		print('     channels : ')
		for i = 0, layout.channel_count-1 do
			print('       '..i..' : '..M.channel_name(layout.channels[i]))
		end
	end
	print('  formats : ')
	for i = 0, self.format_count-1 do
		local fmt = self.formats[i]
		print('    '..i..' : '..M.format_string(fmt)..
			(fmt == self.current_format and ' (current)' or ' '))
	end
	print('  sample_rates : ')
	for i = 0, self.sample_rate_count-1 do
		local sr = self.sample_rates[i]
		print('    '..i..' : ' ..sr.min..' - '..sr.max)
	end
	print('  sample_rate_current : '..self.sample_rate_current)
	print('  latency range       : '..self.software_latency_min..' - '..self.software_latency_max)
	print('  current_latency     : '..self.software_latency_current)
	print('  is_raw              : '..tostring(self.is_raw))
	print('  ref_count           : '..self.ref_count)
	print('  probe_error         : '..tostring(self.probe_error))
end

--streams --------------------------------------------------------------------

function dev:stream()
	local dev = self
	local self = checkptr(self.aim == 'o' and
		C.soundio_outstream_create(self) or
		C.soundio_instream_create(self))
	ffi.gc(self, self.free)
	if not dev.probe_error then
		assert(dev:supports_sample_rate(48000))
		assert(dev:supports_format(C.SoundIoFormatFloat32NE))
	end
	self.sample_rate = 48000
	self.format = C.SoundIoFormatFloat32NE
	return self
end

--streams/output -------------------------------------------------------------

local strout = {}
local stroutprop = {}
strout.__index = vprops(strout, stroutprop)

stroutprop.name = devprop.name

function stroutprop:layout_error()
	return self.layout_error_code ~= 0 and self.layout_error_code or nil
end

function strout:free()
	ffi.gc(self, nil)
	C.soundio_outstream_destroy(self)
end

function strout:open() check(C.soundio_outstream_open(self)) end
function strout:start() check(C.soundio_outstream_start(self)) end

local areas = ffi.new'struct SoundIoChannelArea*[1]'
local fcnt  = ffi.new'int[1]'

function strout:begin_write(n)
	fcnt[0] = n
	check(C.soundio_outstream_begin_write(self, areas, fcnt))
	return areas[0], fcnt[0]
end

function strout:end_write()
	local ret = C.soundio_outstream_end_write(self)
	if ret == C.SoundIoErrorUnderflow then
		return true
	end
	check(ret)
end

function strout:clear_buffer()
	check(C.soundio_outstream_clear_buffer(self))
end

function strout:pause(pause)
	check(C.soundio_outstream_pause(self, pause))
end

local dbuf = ffi.new'double[1]'

function strout:latency()
	check(C.soundio_outstream_get_latency(self, dbuf))
	return dbuf[0]
end

function stroutprop:bytes_per_second()
	return self.bytes_per_frame * self.sample_rate
end

--streams/input --------------------------------------------------------------

local strin = {}
local strinprop = {}
strin.__index = vprops(strin, strinprop)

strinprop.name         = stroutprop.name
strinprop.layout_error = stroutprop.layout_error

function strin:free()
	ffi.gc(self, nil)
	C.soundio_instream_destroy(self)
end

function strin:open() check(C.soundio_instream_open(self)) end
function strin:start() check(C.soundio_instream_start(self)) end

function strin:begin_read(n)
	fcnt[0] = n
	check(C.soundio_instream_begin_read(self, areas, fcnt))
	return areas[0], fcnt[0]
end

function strin:end_read()
	check(C.soundio_instream_end_read(self))
end

function strin:pause(pause)
	check(C.soundio_instream_pause(self, pause))
end

function strin:latency()
	check(C.soundio_instream_get_latency(self, dbuf))
	return dbuf[0]
end

strinprop.bytes_per_second = stroutprop.bytes_per_second

--ringbuffers ----------------------------------------------------------------

local rb = {}
rb.__index = rb

function sio:ringbuffer(capacity)
	local self = checkptr(C.soundio_ring_buffer_create(self, capacity))
	return ffi.gc(self, self.free)
end

function rb:free()
	ffi.gc(self, nil)
	C.soundio_ring_buffer_destroy(self)
end

rb.capacity = C.soundio_ring_buffer_capacity
rb.write_ptr = C.soundio_ring_buffer_write_ptr
rb.advance_write_ptr = C.soundio_ring_buffer_advance_write_ptr
rb.read_ptr = C.soundio_ring_buffer_read_ptr
rb.advance_read_ptr = C.soundio_ring_buffer_advance_read_ptr
rb.fill_count = C.soundio_ring_buffer_fill_count
rb.free_count = C.soundio_ring_buffer_free_count
rb.clear = C.soundio_ring_buffer_clear
function rb:write_buf() return self:write_ptr(), self:free_count() end
function rb:read_buf() return self:read_ptr(), self:fill_count() end

--buffered streaming API -----------------------------------------------------

local buf = {}
buf.__index = buf

local sample_types = {
	[C.SoundIoFormatS8]        = 'int8_t',
	[C.SoundIoFormatU8]        = 'uint8_t',
	[C.SoundIoFormatS16NE]     = 'int16_t',
	[C.SoundIoFormatU16NE]     = 'uint16_t',
	[C.SoundIoFormatS32LE]     = 'int32_t',
	[C.SoundIoFormatU32LE]     = 'uint32_t',
	[C.SoundIoFormatFloat32LE] = 'float',
	[C.SoundIoFormatFloat64LE] = 'double',
}

function strout:buffer(buffer_size_seconds)

	local function setup_state(ringbuffer_ptr, bpf)

		local ffi = require'ffi'
		local sio = require'libsoundio'

		local ringbuffer = ffi.cast('struct SoundIoRingBuffer*', ringbuffer_ptr)

		local function write_callback(outstream, frame_count_min, frame_count_max)
			local avail_bytes = ringbuffer:fill_count()
			local max_bytes = math.min(avail_bytes, frame_count_max * bpf)
			local max_frames = math.floor(max_bytes / bpf)
			local buf = ringbuffer:read_ptr()
			local frames_left = max_frames
			while frames_left > 0 do
				local areas, write_frames = outstream:begin_write(frames_left)
				if write_frames <= 0 then break end
				local read_bytes = write_frames * bpf
				ffi.copy(areas[0].ptr, buf, read_bytes)
				ringbuffer:advance_read_ptr(read_bytes)
				if outstream:end_write() then break end --underflow
				frames_left = frames_left - write_frames
			end
		end

		local write_cb = ffi.cast('SoundIoWriteCallback', write_callback)
		return tonumber(ffi.cast('intptr_t', write_cb))
	end

	local sample_type = sample_types[tonumber(self.format)]
	local ptr_type = sample_type
		and ffi.typeof('$(*)[$]', ffi.typeof(sample_type), self.layout.channel_count)
		or ffi.typeof'void*'
	local bps = self.bytes_per_second
	local buffer_size = math.ceil((buffer_size_seconds or 1) * bps)

	local ringbuffer = self.device.soundio:ringbuffer(buffer_size)

	local state = require('luastate').open()
	state:openlibs()
	state:push(setup_state)

	local ringbuffer_addr = tonumber(ffi.cast('intptr_t', ringbuffer))
	local write_cb_addr = state:call(ringbuffer_addr, self.bytes_per_frame)
	self.write_callback = ffi.cast('SoundIoWriteCallback', write_cb_addr)

	local buffer = setmetatable({
		ringbuffer = ringbuffer,
		state = state,
		ptr_type = ptr_type,
		stream = self,
	}, buf)
	return buffer
end

function buf:free()
	self.stream.write_callback = nil
	self.ringbuffer:free()
	self.state:close()
end

function buf:write_ptr()
	return ffi.cast(self.ptr_type, self.ringbuffer:write_ptr())
end

function buf:advance_write_ptr(n)
	self.ringbuffer:advance_write_ptr(n * self.stream.bytes_per_frame)
end

function buf:read_ptr()
	return ffi.cast(self.ptr_type, self.ringbuffer:read_ptr())
end

function buf:advance_read_ptr(n)
	self.ringbuffer:advance_read_ptr(n * self.stream.bytes_per_frame)
end

function buf:fill_count()
	return math.floor(self.ringbuffer:fill_count() / self.stream.bytes_per_frame)
end

function buf:free_count()
	return math.floor(self.ringbuffer:free_count() / self.stream.bytes_per_frame)
end

function buf:capacity()
	return math.floor(self.ringbuffer:capacity() / self.stream.bytes_per_frame)
end

buf.write_buf = rb.write_buf
buf.read_buf = rb.read_buf

--metatype assignments -------------------------------------------------------

ffi.metatype('struct SoundIo', sio)
ffi.metatype('struct SoundIoDevice', dev)
ffi.metatype('struct SoundIoRingBuffer', rb)
ffi.metatype('struct SoundIoOutStream', strout)
ffi.metatype('struct SoundIoInStream', strin)
ffi.metatype('struct SoundIoChannelLayout', layout)

return M
