Unverified Commit dfd47401 authored by RobLoach's avatar RobLoach Committed by GitHub
Browse files

Merge branch 'master' into lutro.window.showMessageBox

parents 20f9acf2 b6a32d4d
# Editor workspaces
.vs/
.vscode/
*.user
# Object files
*.o
*.ko
......@@ -5,9 +10,10 @@
*.elf
/obj
# Precompiled Headers
# Precompiled Headers vand other Build Artifacts
*.gch
*.pch
*.ilk
# Libraries
*.lib
......@@ -23,6 +29,7 @@
# Executables
*.exe
*.pdb
*.out
*.app
*.i*86
......
......@@ -305,7 +305,7 @@ CORE_DIR := .
include Makefile.common
OBJS += $(SOURCES_C:.c=.o) $(SOURCES_CXX:.cpp=.o) $(SOURCES_ASM:.S=.o)
OBJS += $(SOURCES_C:.c=.o) $(VORBIS_SOURCES_C:.c=.o) $(SOURCES_CXX:.cpp=.o) $(SOURCES_ASM:.S=.o)
CFLAGS += -Wall -pedantic $(fpic) $(INCFLAGS)
......@@ -351,6 +351,42 @@ OBJS := $(addprefix obj/,$(OBJS))
all: $(TARGET)
# TARGET: vcxproj
#
# This target bypasses most config options and generates an msbuild property sheet which is included into
# one or more vcxproj targets. Most build options are still configured via the vcxproj itself (via Visual
# Studio target selector, and other VS IDE things). This just provides a nice way of generating sources and
# include dirs from one authorative list: Makefile.common
#
# Limitations:
# - msbuild cannot handle easily options per-sourcefile. Such functionality will require multiple msbuild files,
# or authoring a standalone tool that can can inject the massive amount of red-tape xml boilerplate needed to
# specify build settings per sourcefile. (the latter is not recommended)
#
# - if any of the SOURCES_C items are populated using wildcards then this will fail. In that case, there needs
# to be dependency checks on the contents of each dir (gnu make supports this, just specify dirs as dependencies
# and any files added/removed/renamed within them will trigger a rule rebuild).
#
vcxproj: msbuild/lutro_sources.props
msbuild/lutro_sources.props: Makefile Makefile.common
@> msbuild/lutro_sources.props echo '<?xml version="1.0" encoding="utf-8"?>'
@>> msbuild/lutro_sources.props echo '<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">'
@>> msbuild/lutro_sources.props echo ' <ItemGroup>'
@>> msbuild/lutro_sources.props printf " <ClCompile Include=\"../%s\" />\n" $(SOURCES_C)
@>> msbuild/lutro_sources.props echo ' </ItemGroup>'
@[[ -z "$(SOURCES_H)" ]] || \
>> msbuild/lutro_sources.props echo ' <ItemGroup>' \
>> msbuild/lutro_sources.props printf " <ClInclude Include=\"../%s\" />\n" $(SOURCES_H) \
>> msbuild/lutro_sources.props echo ' </ItemGroup>'
@>> msbuild/lutro_sources.props echo ' <ItemDefinitionGroup>'
@>> msbuild/lutro_sources.props echo ' <ClCompile>'
@>> msbuild/lutro_sources.props printf " <AdditionalIncludeDirectories>%%(AdditionalIncludeDirectories);../%s</AdditionalIncludeDirectories>\n" $(subst -I,,$(filter -I%,$(CFLAGS)))
@>> msbuild/lutro_sources.props echo ' </ClCompile>'
@>> msbuild/lutro_sources.props echo ' </ItemDefinitionGroup>'
@>> msbuild/lutro_sources.props echo '</Project>'
@touch msbuild/lutro.vcxproj
ifneq ($(MMD),)
-include $(OBJS:.o=.d)
endif
......
......@@ -5,9 +5,7 @@ INCFLAGS := -I$(CORE_DIR) \
-I$(CORE_DIR)/deps/zlib \
-I$(CORE_DIR)/deps/vorbis \
-I$(CORE_DIR)/deps/ogg \
-I$(CORE_DIR)/deps \
-I$(LUALIB_DIR)
-I$(CORE_DIR)/deps
SOURCES_C := $(CORE_DIR)/libretro.c \
$(CORE_DIR)/lutro.c \
......@@ -30,6 +28,7 @@ SOURCES_C := $(CORE_DIR)/libretro.c \
$(CORE_DIR)/painter.c
ifeq ($(WANT_LUALIB),1)
INCFLAGS += -I$(LUALIB_DIR)
SOURCES_C += $(LUALIB_DIR)/lapi.c \
$(LUALIB_DIR)/lauxlib.c \
$(LUALIB_DIR)/lbaselib.c \
......@@ -180,7 +179,11 @@ ifeq ($(WANT_PHYSFS), 1)
endif
# Ogg Vorbis
SOURCES_C += \
# These sources are added to their own list so that they can be handled differently when generating
# windows msbuild scripts. Notably, msbuild doesn't like window.c because it conflicts with window.c
# in lutro -- such things can only be resolved in msbuld by using project encapsulation.
VORBIS_SOURCES_C += \
$(CORE_DIR)/deps/ogg/bitwise.c \
$(CORE_DIR)/deps/ogg/framing.c \
$(CORE_DIR)/deps/vorbis/analysis.c \
......
......@@ -28,6 +28,11 @@ Alternatively, you can load a compressed `.lutro` file:
Compile Lutro by [installing the RetroArch dependencies](https://github.com/libretro/retroarch#dependencies-pc), and running:
make
There are a few optional defines you can use to change how Lutro behaves.
- `make HAVE_COMPOSITION=1` Enables alpha-blending.
## Test
......
......@@ -12,8 +12,15 @@
/* TODO/FIXME - no sound on big-endian */
static unsigned num_sources = 0;
static audio_Source** sources = NULL;
// any source which is playing must maintain a ref in lua, to avoid __gc.
typedef struct {
audio_Source* source;
int lua_ref;
} audioSourceWithRef;
static int num_sources = 0;
static audioSourceWithRef* sources_playing = NULL;
static float volume = 1.0;
#define CHANNELS 2
......@@ -34,110 +41,158 @@ int audio_sources_nullify_refs(const audio_Source* source)
for(int i=0; i<num_sources; ++i)
{
if (sources[i] == source)
audioSourceWithRef* srcref = &sources_playing[i];
if (srcref->source == source)
{
if (sources[i]->state != AUDIO_STOPPED)
if (srcref->source->state != AUDIO_STOPPED)
++counted;
// do not free - the pointers in sources are lua user data
sources[i] = NULL;
srcref->source = NULL;
srcref->lua_ref = LUA_NOREF;
}
}
return counted;
}
// unrefs stopped sounds.
// this is done periodically by lutro to avoid adding lua dependencies to the mixer.
void mixer_unref_stopped_sounds(lua_State* L)
{
// Loop over audio sources
for (int i = 0; i < num_sources; i++)
{
if (sources_playing[i].lua_ref >= 0)
{
audio_Source* source = sources_playing[i].source;
if (!source || source->state == AUDIO_STOPPED)
{
lua_getglobal(L, "refs_audio_playing");
luaL_unref(L, -1, sources_playing[i].lua_ref);
sources_playing[i].lua_ref = LUA_REFNIL;
}
}
if (sources_playing[i].lua_ref < 0)
sources_playing[i].source = NULL;
}
}
void lutro_audio_stop_all(void)
{
// Loop over audio sources
// no cleanup needed, __gc will handle it later after a call to mixer_unref_stopped_sounds()
for (int i = 0; i < num_sources; i++)
{
if (sources_playing[i].source)
sources_playing[i].source->state = AUDIO_STOPPED;
}
}
void mixer_render(int16_t *buffer)
{
static mixer_presaturate_t presaturateBuffer[AUDIO_FRAMES * CHANNELS];
memset(presaturateBuffer, 0, AUDIO_FRAMES * CHANNELS * sizeof(mixer_presaturate_t));
presaturate_buffer_desc bufdesc;
bufdesc.data = presaturateBuffer;
bufdesc.channels = CHANNELS;
bufdesc.samplelen = AUDIO_FRAMES;
// Loop over audio sources
for (int i = 0; i < num_sources; i++)
{
if (!sources[i])
audio_Source* source = sources_playing[i].source;
if (!source)
continue;
if (sources[i]->state == AUDIO_STOPPED)
if (source->state == AUDIO_STOPPED)
continue;
// options here are to premultiply source volumes with master volume, or apply master volume at the end of mixing
// during the saturation step. Each approach has its strengths and weaknesses and overall neither differs much when
// using float or double for presaturation buffer (see final saturation step below)
float srcvol = sources[i]->volume;
float srcvol = source->volume;
if (sources[i]->oggData)
// Decoder Seek Position Note:
// It's unclear if a decoder can be shared by multiple sources, so always seek the decoder for each chunk.
// Our decoder APIs internally optimize away redundant seeks.
if (source->oggData)
{
bool finished = decoder_decodeOgg(sources[i]->oggData, presaturateBuffer, srcvol, sources[i]->loop);
decOgg_seek(source->oggData, source->sndpos);
bool finished = decOgg_decode(source->oggData, &bufdesc, srcvol, source->loop);
if (finished)
{
decoder_seek(sources[i]->oggData, 0);
sources[i]->state = AUDIO_STOPPED;
decOgg_seek(source->oggData, 0); // see notes above
source->state = AUDIO_STOPPED;
source->sndpos = 0;
}
source->sndpos = decOgg_sampleTell(source->oggData);
continue;
}
void* rawsamples_alloc = calloc(
AUDIO_FRAMES * sources[i]->bps, sizeof(uint8_t));
fseek(sources[i]->sndta.fp, WAV_HEADER_SIZE + sources[i]->pos, SEEK_SET);
bool end = ! fread(rawsamples_alloc,
sizeof(uint8_t),
AUDIO_FRAMES * sources[i]->bps,
sources[i]->sndta.fp);
// ogg outputs float values range 1.0 to -1.0
// 16-bit wav outputs values range 32767 to -32768
// 8-bit wav is scaled up to 16 bit and then normalized using 16-bit divisor.
float srcvol_and_scale_to_one = srcvol / 32767;
if (sources[i]->sndta.head.BitsPerSample == 8)
if (source->wavData)
{
const int8_t* rawsamples8 = (int8_t*)rawsamples_alloc;
for (int j = 0; j < AUDIO_FRAMES; j++)
decWav_seek(source->wavData, source->sndpos); // see notes above
bool finished = decWav_decode(source->wavData, &bufdesc, srcvol, source->loop);
if (finished)
{
// note this is currently *64 because the mixer is mixing 8-bit smaples as 0->255 instead of normalizing to -128 to 127.
// This would have caused the more appropriate *128 multiplier to cause saturation along the top of the waveform.
// We should be able to change this to *128 now, but it will affect volume behavior of any games that use 8 bit samples and
// were authored with the current *64 behavior. So need to verify how we want to go about handling this --jstine
mixer_presaturate_t left = (sources[i]->sndta.head.NumChannels == 2) ? rawsamples8[j*2+0] : rawsamples8[j] * 64;
mixer_presaturate_t right = (sources[i]->sndta.head.NumChannels == 2) ? rawsamples8[j*2+1] : rawsamples8[j] * 64;
if (sources[i]->sndta.head.NumChannels == 2) { right = rawsamples8[j*2+1]*64; }
presaturateBuffer[j*2+0] += (left * srcvol_and_scale_to_one);
presaturateBuffer[j*2+1] += (right * srcvol_and_scale_to_one);
sources[i]->pos += sources[i]->bps;
decWav_seek(source->wavData, 0);
source->state = AUDIO_STOPPED;
}
source->sndpos = decWav_sampleTell(source->wavData);
continue;
}
if (sources[i]->sndta.head.BitsPerSample == 16)
if (source->sndta)
{
const int16_t* rawsamples16 = (int16_t*)rawsamples_alloc;
for (int j = 0; j < AUDIO_FRAMES; j++)
{
mixer_presaturate_t left = (sources[i]->sndta.head.NumChannels == 2) ? rawsamples16[j*2+0] : rawsamples16[j];
mixer_presaturate_t right = (sources[i]->sndta.head.NumChannels == 2) ? rawsamples16[j*2+1] : rawsamples16[j];
snd_SoundData* sndta = source->sndta;
if (sources[i]->sndta.head.NumChannels == 2) { right = rawsamples16[j*2+1]; }
int total_mixed = 0;
presaturateBuffer[j*2+0] += (left * srcvol_and_scale_to_one);
presaturateBuffer[j*2+1] += (right * srcvol_and_scale_to_one);
sources[i]->pos += sources[i]->bps;
while (total_mixed < AUDIO_FRAMES)
{
int mixchunksz = AUDIO_FRAMES - total_mixed;
int remaining = sndta->numSamples - source->sndpos;
if (mixchunksz > remaining)
{
mixchunksz = remaining;
}
if (sndta->numChannels == 1)
{
for (int j = 0; j < mixchunksz; ++j, ++total_mixed, ++source->sndpos)
{
presaturateBuffer[(total_mixed*2) + 0] += sndta->data[source->sndpos] * srcvol;
presaturateBuffer[(total_mixed*2) + 1] += sndta->data[source->sndpos] * srcvol;
}
}
if (sndta->numChannels == 2)
{
for (int j = 0; j < mixchunksz; ++j, ++total_mixed, ++source->sndpos)
{
presaturateBuffer[(total_mixed*2) + 0] += sndta->data[(source->sndpos*2) + 0] * srcvol;
presaturateBuffer[(total_mixed*2) + 1] += sndta->data[(source->sndpos*2) + 1] * srcvol;
}
}
assert(source->sndpos <= sndta->numSamples);
assert(total_mixed <= AUDIO_FRAMES);
if (source->sndpos == sndta->numSamples)
{
source->sndpos = 0;
if (!source->loop)
{
source->state = AUDIO_STOPPED;
break;
}
}
}
}
if (end)
{
if (!sources[i]->loop)
sources[i]->state = AUDIO_STOPPED;
sources[i]->pos = 0;
}
free(rawsamples_alloc);
}
// final saturation step - downsample.
......@@ -168,43 +223,56 @@ int lutro_audio_preload(lua_State *L)
return 1;
}
void lutro_audio_init()
void lutro_audio_init(lua_State* L)
{
num_sources = 0;
sources = NULL;
sources_playing = NULL;
volume = 1.0;
lua_newtable(L);
lua_setglobal(L, "refs_audio_playing");
}
void lutro_audio_deinit()
{
if (sources)
if (!sources_playing) return;
// lua owns most of our objects so there's only two proper ways to deinit audio:
// 1. assume luaState has been forcibly destroyed without its own cleanup.
// 2. run lua_close() and let it clean most of this up first.
lutro_audio_stop_all();
int counted = 0;
for (int i = 0; i < num_sources; i++)
{
for (unsigned i = 0; i < num_sources; i++)
{
if (sources[i] && sources[i]->oggData)
{
ov_clear(&sources[i]->oggData->vf);
free(sources[i]->oggData);
}
}
if (sources_playing[i].source)
++counted;
}
free(sources);
sources = NULL;
num_sources = 0;
if (counted)
{
fprintf(stderr, "Found %d leaked audio source references. Was lua_close() called first?\n", counted);
fflush(stderr);
//assert(false);
return;
}
free(sources_playing);
sources_playing = NULL;
num_sources = 0;
}
static int assign_to_existing_source_slot(audio_Source* self)
static int find_empty_source_slot(audio_Source* self)
{
for(int i=0; i<num_sources; ++i)
{
if (!sources[i])
{
sources[i] = self;
return 1;
}
// it's possible a stopped source is still in the sources_playing list, since cleanup
// operations are deferred. This is OK and expected, just use the slot that's still assigned...
if (!sources_playing[i].source || sources_playing[i].source == self)
return i;
}
return 0;
return -1;
}
int audio_newSource(lua_State *L)
......@@ -216,70 +284,46 @@ int audio_newSource(lua_State *L)
audio_Source* self = (audio_Source*)lua_newuserdata(L, sizeof(audio_Source));
self->oggData = NULL;
self->sndta.fp = NULL;
//if (lua_isstring(L,1)) // (lua_type(L, 1) == LUA_TSTRING)
//{
//
//}
self->wavData = NULL;
void *p = lua_touserdata(L, 1);
if (p == NULL)
{
const char* path = luaL_checkstring(L, 1);
char fullpath[PATH_MAX_LENGTH];
strlcpy(fullpath, settings.gamedir, sizeof(fullpath));
strlcat(fullpath, path, sizeof(fullpath));
//get file extension
char ext[PATH_MAX_LENGTH];
strcpy(ext, path_get_extension(path));
for(int i = 0; ext[i]; i++)
ext[i] = tolower((uint8_t)ext[i]);
//ogg
if (strstr(ext, "ogg"))
// when matching file paths, only lua string types are OK.
// lua_tostring will convert numbers into strings implicitly, which is not what we want.
luaL_checktype(L, 1, LUA_TSTRING); // explicit non-converted string type test
const char* path = lua_tostring(L, 1);
AssetPathInfo asset;
lutro_assetPath_init(&asset, path);
if (strstr(asset.ext, "ogg"))
{
self->oggData = malloc(sizeof(OggData));
decoder_initOgg(self->oggData, fullpath);
self->oggData = malloc(sizeof(dec_OggData));
decOgg_init(self->oggData, asset.fullpath);
}
//default: WAV file
else
{
FILE *fp = fopen(fullpath, "rb");
if (!fp)
return -1;
fread(&self->sndta.head, sizeof(uint8_t), WAV_HEADER_SIZE, fp);
self->sndta.fp = fp;
if (strstr(asset.ext, "wav"))
{
self->wavData = malloc(sizeof(dec_WavData));
decWav_init(self->wavData, asset.fullpath);
}
}
else
{
snd_SoundData* sndta = (snd_SoundData*)luaL_checkudata(L, 1, "SoundData");
self->sndta = *sndta;
self->sndta = sndta;
lua_pushvalue(L, 1); // push ref to SoundData parameter
self->lua_ref_sndta = luaL_ref(L, LUA_REGISTRYINDEX);
}
self->loop = false;
self->volume = 1.0;
self->pos = 0;
self->sndpos = 0;
self->state = AUDIO_STOPPED;
//WAV file
if (self->sndta.fp)
{
self->bps = self->sndta.head.NumChannels * self->sndta.head.BitsPerSample / 8;
fseek(self->sndta.fp, 0, SEEK_END);
}
if (!assign_to_existing_source_slot(self))
{
num_sources++;
sources = (audio_Source**)realloc(sources, num_sources * sizeof(audio_Source));
sources[num_sources-1] = self;
}
if (luaL_newmetatable(L, "Source") != 0)
{
static luaL_Reg audio_funcs[] = {
......@@ -402,18 +446,8 @@ int source_tell(lua_State *L)
//currently assuming samples vs seconds
//TODO: check if 2nd param is "seconds" or "samples"
//WAV file
if (self->sndta.fp)
{
lua_pushnumber(L, self->pos / self->bps);
}
//OGG file
else if (self->oggData)
{
uint32_t pos = 0;
decoder_sampleTell(self->oggData, &pos);
lua_pushnumber(L, pos);
}
// sndpos should always be accurate for any given source or stream.
lua_pushnumber(L, self->sndpos);
return 1;
}
......@@ -430,16 +464,13 @@ int source_seek(lua_State *L)
//currently assuming samples vs seconds
//TODO: check if 3rd param is "seconds" or "samples"
//WAV file
if (self->sndta.fp)
{
self->pos = self->bps * (unsigned)luaL_checknumber(L, 2);
}
//OGG file
else if (self->oggData)
{
decoder_seek(self->oggData, (unsigned)luaL_checknumber(L, 2));
}
self->sndpos = luaL_checkinteger(L, 2);
if (self->wavData)
decWav_seek(self->wavData, self->sndpos);
if (self->oggData)
decOgg_seek(self->oggData, self->sndpos);
return 0;
}
......@@ -476,45 +507,101 @@ int source_gc(lua_State *L)
if (leaks)
{
fprintf(stderr, "source_gc: playing audio references were nullified.\n");
fflush(stderr);
//assert(false);
}
luaL_unref(L, LUA_REGISTRYINDEX, self->lua_ref_sndta);
self->lua_ref_sndta = LUA_REFNIL;
if (self->wavData)
{
if (self->wavData->fp)
fclose(self->wavData->fp);
free(self->wavData);
}
if (self->oggData)
{
ov_clear(&self->oggData->vf);
free(self->oggData);
}
(void)self;
return 0;
}
// audio.play returns nothing, but source:play returns a boolean.
// it's OK enough for us to always return a boolean.
int audio_play(lua_State *L)
{
audio_Source* self = (audio_Source*)luaL_checkudata(L, 1, "Source");
//play pos should not reset if play called again before finished
//as in Love2D, game code should explicitly call stop or seek(0, nil) before play to reset pos if already playing
if (self->state == AUDIO_PLAYING)
return 0; // nothing to do.
if (self->state == AUDIO_PAUSED)
{
self->state = AUDIO_PLAYING;
return 0;
}
assert(self->state == AUDIO_STOPPED);
self->state = AUDIO_PLAYING;
lua_pushboolean(L, true);
// add a ref to our playing audio registry. this blocks __gc until the ref(s) are removed.