/*
* melisma - extensible and scriptable audio engine
* Copyright (C) 2026 Ludvig Gunne Lindström
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include
#include
#include
#include
#include "melisma.h"
#include "mutil.h"
#include "sample.h"
#define PLAYBACK_TYPENAME "playback"
struct playback
{
ml_node *node;
struct sample *smp;
double speed;
double phase;
bool active;
bool loop;
int fn;
int arg;
};
static void proc(const ml_proc_args *args)
{
struct playback *pb = args->userp;
if (pb->smp == NULL)
{
memset(args->out[0],0,sizeof(*args->out[0])*args->nframes);
return;
}
unsigned long sr = ml_sample_rate();
double phase = pb->phase;
float *dst = args->out[0];
size_t len = pb->smp->len;
float *data = pb->smp->data;
for (size_t i = 0; i < args->nframes; i++)
{
if (!pb->active)
{
memset(&dst[i],0,(args->nframes - i)*sizeof(*dst));
return;
}
float t = phase*len;
size_t i0 = (size_t) t;
size_t i1 = i0 + 1;
float ip = t - i0;
i0 %= len;
i1 %= len;
float s0 = data[i0];
float s1 = data[i1];
dst[i] = s0 + ip*(s1 - s0);
phase += pb->speed/len;
while (phase > 1)
{
phase -= 1;
if (pb->fn != LUA_NOREF)
{
ml_schedule_event(0,pb->fn,pb->arg);
}
if (!pb->loop)
{
pb->active = false;
pb->phase = 0;
}
}
while (phase < 0)
{
phase += 1;
if (pb->fn != LUA_NOREF)
{
ml_schedule_event(0,pb->fn,pb->arg);
}
if (!pb->loop)
{
pb->active = false;
pb->phase = 0;
}
}
}
pb->phase = phase;
}
static int l_init(lua_State *s)
{
struct sample *smp = NULL;
if (!lua_isnoneornil(s,1))
{
smp = *(void**) luaL_checkudata(s,1,SAMPLE_TYPENAME);
}
struct playback *pb = calloc(1,sizeof(*pb));
pb->smp = smp;
pb->speed = 1;
pb->fn = LUA_NOREF;
pb->arg = LUA_NOREF;
pb->node = ml_create_node(proc,pb);
ml_label_node(pb->node,"playback (%s)",smp ? smp->path : "NULL");
ml_add_output(pb->node);
*(void**) lua_newuserdata(s,sizeof(void*)) = pb;
luaL_setmetatable(s,PLAYBACK_TYPENAME);
return 1;
}
static int l_set_sample(lua_State *s)
{
struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
struct sample *smp = *(void**) luaL_checkudata(s,2,SAMPLE_TYPENAME);
pb->smp = smp;
return 0;
}
static int l_play(lua_State *s)
{
struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
pb->active = true;
return 0;
}
static int l_pause(lua_State *s)
{
struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
pb->active = false;
return 0;
}
static int l_reset(lua_State *s)
{
struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
pb->phase = 0;
return 0;
}
static int l_stop(lua_State *s)
{
struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
pb->phase = 0;
pb->active = false;
return 0;
}
static int l_speed(lua_State *s)
{
struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
if (!lua_isnil(s,2))
{
pb->speed = luaL_checknumber(s,2);
}
lua_pushnumber(s,pb->speed);
return 1;
}
static int l_loop(lua_State *s)
{
struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
if (!lua_isnil(s,2))
{
luaL_checktype(s,2,LUA_TBOOLEAN);
pb->loop = lua_toboolean(s,2);
}
lua_pushboolean(s,pb->loop);
return 1;
}
static int l_phase(lua_State *s)
{
struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
if (!lua_isnil(s,2))
{
double phase = luaL_checknumber(s,2);
if (phase > 1)
{
phase = 1;
}
if (phase < 0)
{
phase = 0;
}
pb->phase = phase;
}
lua_pushnumber(s,pb->phase);
return 1;
}
static int l_out(lua_State *s)
{
struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
*(void**) lua_newuserdata(s,sizeof(void*)) = ml_get_output(pb->node,0);
luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
return 1;
}
int luaopen_melisma_playback(lua_State *s)
{
/*** Audio sample playback.
local msample = require 'melisma.sample'
local mplayback = require 'melisma.playback'
local sample = msample.load('sample.wav')
local playb = mplayback.init(sample)
playb:loop(true)
playb:spped(2)
playb:play()
@classmod playback
*/
lua_newtable(s);
int tbl = lua_gettop(s);
/*** Intialize a playback unit.
@static
@function init
@tparam sample|nil smp
@treturn playback
*/
lua_pushcfunction(s,l_init);
lua_setfield(s,tbl,"init");
/*** Set playback sample.
@function set_sample
@tparam sample smp
*/
lua_pushcfunction(s,l_set_sample);
lua_setfield(s,tbl,"set_sample");
/*** Start playback.
@function play
*/
lua_pushcfunction(s,l_play);
lua_setfield(s,tbl,"play");
/*** Pause playback.
@function pause
*/
lua_pushcfunction(s,l_pause);
lua_setfield(s,tbl,"pause");
/*** Stop playback.
Equivalent to @{playback.pause}; @{playback.reset}.
@function stop
*/
lua_pushcfunction(s,l_stop);
lua_setfield(s,tbl,"stop");
/*** Reset playback.
Equivalent to @{playback.phase}(0).
@function reset
*/
lua_pushcfunction(s,l_reset);
lua_setfield(s,tbl,"reset");
/*** Set playback speed.
A negative speed will play the sample in reverse.
@function reset
*/
lua_pushcfunction(s,l_speed);
lua_setfield(s,tbl,"speed");
/*** Enable/disable automatic looping.
@function loop
@tparam bool b
*/
lua_pushcfunction(s,l_loop);
lua_setfield(s,tbl,"loop");
/*** Set playback phase.
@function phase
@tparam number x Phase ranging from 0 to 1.
*/
lua_pushcfunction(s,l_phase);
lua_setfield(s,tbl,"phase");
lua_pushcfunction(s,l_out);
lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION);
luaL_newmetatable(s,PLAYBACK_TYPENAME);
int mt = lua_gettop(s);
lua_pushvalue(s,tbl);
lua_setfield(s,mt,"__index");
lua_settop(s,tbl);
return 1;
}