diff --git a/Makefile b/Makefile index 0b2d935..6ead142 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,7 @@ CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++11 ASFLAGS := -g $(ARCH) LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) -LIBS := -lsfil -ljpeg -lsftd -lfreetype -lpng -lz -lsf2d -lctru -lm +LIBS := -lsfil -ljpeg -lsftd -lfreetype -lpng -lz -lsf2d -lctru -lvorbisfile -lvorbis -logg -lm #--------------------------------------------------------------------------------- # list of directories containing libraries, this must be the top level containing @@ -142,7 +142,7 @@ $(BUILD): @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile build-portlibs: - @make -C libs/3ds_portlibs zlib freetype libjpeg-turbo libpng + @make -C libs/3ds_portlibs zlib freetype libjpeg-turbo libpng libogg libvorbis build-sf2dlib: @make -C libs/sf2dlib/libsf2d build diff --git a/libs/3ds_portlibs/.gitignore b/libs/3ds_portlibs/.gitignore index 115f9d0..8e0d03f 100644 --- a/libs/3ds_portlibs/.gitignore +++ b/libs/3ds_portlibs/.gitignore @@ -4,4 +4,6 @@ libjpeg-* libpng-* sqlite-* zlib-* +libogg-* +libvorbis-* build/ \ No newline at end of file diff --git a/libs/3ds_portlibs/Makefile b/libs/3ds_portlibs/Makefile index 355b8ad..c47b8c1 100644 --- a/libs/3ds_portlibs/Makefile +++ b/libs/3ds_portlibs/Makefile @@ -28,6 +28,16 @@ ZLIB_VERSION := $(ZLIB)-1.2.8 ZLIB_SRC := $(ZLIB_VERSION).tar.gz ZLIB_DOWNLOAD := "http://prdownloads.sourceforge.net/libpng/zlib-1.2.8.tar.gz" +LIBOGG := libogg +LIBOGG_VERSION := $(LIBOGG)-1.3.2 +LIBOGG_SRC := $(LIBOGG_VERSION).tar.gz +LIBOGG_DOWNLOAD := "http://downloads.xiph.org/releases/ogg/libogg-1.3.2.tar.gz" + +LIBVORBIS := libvorbis +LIBVORBIS_VERSION := $(LIBVORBIS)-1.3.5 +LIBVORBIS_SRC := $(LIBVORBIS_VERSION).tar.gz +LIBVORBIS_DOWNLOAD := "http://downloads.xiph.org/releases/vorbis/libvorbis-1.3.5.tar.gz" + export PORTLIBS := $(CURDIR)/build export PATH := $(DEVKITARM)/bin:$(PATH) export PKG_CONFIG_PATH := $(PORTLIBS)/lib/pkgconfig @@ -66,8 +76,8 @@ $(FREETYPE): $(FREETYPE_SRC) ./configure --prefix=$(PORTLIBS) --host=arm-none-eabi --disable-shared --enable-static --without-harfbuzz @$(MAKE) -C $(FREETYPE_VERSION) @make create_build_dir - @cp -srf $(CURDIR)/freetype-2.6/include/. $(CURDIR)/build/include - @cp -sf $(CURDIR)/freetype-2.6/objs/.libs/libfreetype.a $(CURDIR)/build/lib/libfreetype.a + @cp -srf $(CURDIR)/$(FREETYPE_VERSION)/include/. $(CURDIR)/build/include + @cp -sf $(CURDIR)/$(FREETYPE_VERSION)/objs/.libs/libfreetype.a $(CURDIR)/build/lib/libfreetype.a $(LIBEXIF): $(LIBEXIF_SRC) @[ -d $(LIBEXIF_VERSION) ] || tar -xf $< @@ -81,8 +91,8 @@ $(LIBJPEGTURBO): $(LIBJPEGTURBO_SRC) ./configure --prefix=$(PORTLIBS) --host=arm-none-eabi --disable-shared --enable-static @$(MAKE) CFLAGS+="\"-Drandom()=rand()\"" -C $(LIBJPEGTURBO_VERSION) @make create_build_dir - @cp -sf $(CURDIR)/libjpeg-turbo-*/*.h $(CURDIR)/build/include - @cp -sf $(CURDIR)/libjpeg-turbo-*/.libs/libjpeg.a $(CURDIR)/build/lib/libjpeg.a + @cp -sf $(CURDIR)/$(LIBJPEGTURBO_VERSION)/*.h $(CURDIR)/build/include + @cp -sf $(CURDIR)/$(LIBJPEGTURBO_VERSION)/.libs/libjpeg.a $(CURDIR)/build/lib/libjpeg.a $(LIBPNG): $(LIBPNG_SRC) @[ -d $(LIBPNG_VERSION) ] || tar -xf $< @@ -90,8 +100,8 @@ $(LIBPNG): $(LIBPNG_SRC) ./configure --prefix=$(PORTLIBS) --host=arm-none-eabi --disable-shared --enable-static @$(MAKE) -C $(LIBPNG_VERSION) @make create_build_dir - @cp -sf $(CURDIR)/libpng-*/*.h $(CURDIR)/build/include - @cp -sf $(CURDIR)/libpng-*/.libs/*.a $(CURDIR)/build/lib/libpng.a + @cp -sf $(CURDIR)/$(LIBPNG_VERSION)/*.h $(CURDIR)/build/include + @cp -sf $(CURDIR)/$(LIBPNG_VERSION)/.libs/*.a $(CURDIR)/build/lib/libpng.a # sqlite won't work with -ffast-math $(SQLITE): $(SQLITE_SRC) @@ -107,8 +117,26 @@ $(ZLIB): $(ZLIB_SRC) CHOST=arm-none-eabi ./configure --static --prefix=$(PORTLIBS) @$(MAKE) -C $(ZLIB_VERSION) @make create_build_dir - @cp -sf $(CURDIR)/zlib-*/*.h $(CURDIR)/build/include - @cp -sf $(CURDIR)/zlib-*/libz.a $(CURDIR)/build/lib/libz.a + @cp -sf $(CURDIR)/$(ZLIB_VERSION)/*.h $(CURDIR)/build/include + @cp -sf $(CURDIR)/$(ZLIB_VERSION)/libz.a $(CURDIR)/build/lib/libz.a + +$(LIBOGG): $(LIBOGG_SRC) + @[ -d $(LIBOGG_VERSION) ] || tar -xf $< + @cd $(LIBOGG_VERSION) && \ + ./configure --prefix=$(PORTLIBS) --host=arm-none-eabi --disable-shared --enable-static + @$(MAKE) -C $(LIBOGG_VERSION) + @make create_build_dir + @cp -srf $(CURDIR)/$(LIBOGG_VERSION)/include/. $(CURDIR)/build/include + @cp -sf $(CURDIR)/$(LIBOGG_VERSION)/src/.libs/*.a $(CURDIR)/build/lib + +$(LIBVORBIS): $(LIBVORBIS_SRC) + @[ -d $(LIBVORBIS_VERSION) ] || tar -xf $< + @cd $(LIBVORBIS_VERSION) && \ + ./configure --prefix=$(PORTLIBS) --host=arm-none-eabi --disable-shared --enable-static + @$(MAKE) -C $(LIBVORBIS_VERSION) + @make create_build_dir + @cp -srf $(CURDIR)/$(LIBVORBIS_VERSION)/include/. $(CURDIR)/build/include + @cp -sf $(CURDIR)/$(LIBVORBIS_VERSION)/lib/.libs/*.a $(CURDIR)/build/lib # Downloads $(ZLIB_SRC): @@ -129,6 +157,12 @@ $(LIBPNG_SRC): $(SQLITE_SRC): wget -O $@ $(SQLITE_DOWNLOAD) +$(LIBOGG_SRC): + wget -O $@ $(LIBOGG_DOWNLOAD) + +$(LIBVORBIS_SRC): + wget -O $@ $(LIBVORBIS_DOWNLOAD) + install-zlib: @$(MAKE) -C $(ZLIB_VERSION) install @@ -138,6 +172,8 @@ install: @[ ! -d $(LIBJPEGTURBO_VERSION) ] || $(MAKE) -C $(LIBJPEGTURBO_VERSION) install @[ ! -d $(LIBPNG_VERSION) ] || $(MAKE) -C $(LIBPNG_VERSION) install @[ ! -d $(SQLITE_VERSION) ] || $(MAKE) -C $(SQLITE_VERSION) install-libLTLIBRARIES install-data + @[ ! -d $(LIBOGG_VERSION) ] || $(MAKE) -C $(LIBOGG_VERSION) install + @[ ! -d $(LIBVORBIS_VERSION) ] || $(MAKE) -C $(LIBVORBIS_VERSION) install clean: @$(RM) -r $(FREETYPE_VERSION) @@ -146,5 +182,7 @@ clean: @$(RM) -r $(LIBPNG_VERSION) @$(RM) -r $(SQLITE_VERSION) @$(RM) -r $(ZLIB_VERSION) + @$(RM) -r $(LIBOGG_VERSION) + @$(RM) -r $(LIBVORBIS_VERSION) @rm -rf $(CURDIR)/build @rm -f $(CURDIR)/*.tar.* diff --git a/sdcard/3ds/ctruLua/main.lua b/sdcard/3ds/ctruLua/main.lua index a0870ab..64d0770 100644 --- a/sdcard/3ds/ctruLua/main.lua +++ b/sdcard/3ds/ctruLua/main.lua @@ -8,6 +8,22 @@ repeat local file = require("openfile")("Choose a Lua file to execute", nil, ".lua", "exist") if file then fs.setDirectory(file:match("^(.-)[^/]*$")) - dofile(file) + local success, err = pcall(dofile, file) + if not success then + local gfx = require("ctr.gfx") + local hid = require("ctr.hid") + gfx.set3D(false) + gfx.color.setDefault(0xFFFFFFFF) + gfx.color.setBackground(0xFF000000) + gfx.font.setDefault() + while true do + hid.read() + if hid.keys().down.start then break end + gfx.startFrame(gfx.GFX_TOP) + gfx.wrappedText(0, 0, err, gfx.TOP_WIDTH) + gfx.endFrame() + gfx.render() + end + end end until not file \ No newline at end of file diff --git a/sdcard/3ds/ctruLua/tests/audio/main.lua b/sdcard/3ds/ctruLua/tests/audio/main.lua new file mode 100644 index 0000000..169c09c --- /dev/null +++ b/sdcard/3ds/ctruLua/tests/audio/main.lua @@ -0,0 +1,52 @@ +local ctr = require("ctr") +local hid = require("ctr.hid") +local gfx = require("ctr.gfx") +local audio = require("ctr.audio") + +local test = assert(audio.load("test.wav")) + +local channel = -1 +local speed = 1 +local leftBalance = 0.5 + +while true do + hid.read() + local keys = hid.keys() + if keys.down.start then break end + + if keys.down.a then + channel = test:play() + end + + if keys.down.up then + speed = speed + 0.01 + test:speed(speed) + audio.speed(nil, speed) + end + if keys.down.down then + speed = speed - 0.01 + test:speed(speed) + audio.speed(nil, speed) + end + + if keys.down.left then + leftBalance = leftBalance + 0.1 + test:mix(1-leftBalance, leftBalance) + audio.mix(nil, leftBalance, 1-leftBalance) + end + if keys.down.right then + leftBalance = leftBalance - 0.1 + test:mix(1-leftBalance, leftBalance) + audio.mix(nil, leftBalance, 1-leftBalance) + end + + gfx.startFrame(gfx.GFX_TOP) + gfx.text(5, 5, "Audio test! "..tostring(test:time()).."/"..tostring(test:duration()).."s") + gfx.text(5, 25, "Last audio played on channel "..tostring(channel)) + gfx.text(5, 65, "Speed: "..(speed*100).."% - Left balance: "..(leftBalance*100).."%") + gfx.endFrame() + + gfx.render() +end + +test:unload() \ No newline at end of file diff --git a/sdcard/3ds/ctruLua/tests/audio/test.wav b/sdcard/3ds/ctruLua/tests/audio/test.wav new file mode 100644 index 0000000..3a71ee6 Binary files /dev/null and b/sdcard/3ds/ctruLua/tests/audio/test.wav differ diff --git a/source/audio.c b/source/audio.c new file mode 100644 index 0000000..2bc9024 --- /dev/null +++ b/source/audio.c @@ -0,0 +1,851 @@ +/*** +The `audio` module. +An audio channel can play only one audio object at a time. +There are 24 audio channels available, numbered from 0 to 23. +@module ctr.audio +@usage local audio = require("ctr.audio") +*/ + +#include <3ds.h> + +#include +#include +#include + +#include +#include + +#include +#include + +// Audio object type +typedef enum { + TYPE_UNKNOWN = -1, + TYPE_OGG = 0, + TYPE_WAV = 1 +} filetype; + +// Audio object userdata +typedef struct { + filetype type; // file type + + // OGG Vorbis specific + OggVorbis_File vf; // ogg vorbis file + + // Needed for playback + float rate; // sample rate (per channel) (Hz) + u32 channels; // channel count + u32 encoding; // data encoding (NDSP_ENCODING_*) + u32 nsamples; // numbers of samples in the audio (per channel, not the total) + u32 size; // number of bytes in the audio (total, ie data size) + char* data; // raw audio data + + // Playing parameters (type-independant) + float mix[12]; // mix parameters + ndspInterpType interp; // interpolation type + double speed; // playing speed +} audio_userdata; + +// Indicate if NDSP was initialized or not. +// NDSP doesn't work on citra yet. +// Please only throw an error related to this when using a ndsp function, so other parts of the +// audio module are still available on citra, like audio.load() and audio:info(). +bool isAudioInitialized = false; + +// Array of the last audio_userdata sent to each channel; channels range from 0 to 23 +audio_userdata* channels[24]; + +/*** +Load an audio file. +OGG Vorbis and PCM WAV file format are currently supported. +(Most WAV files use the PCM encoding). +@function load +@tparam string path path to the file +@tparam[opt=detect] string type file type, `"ogg"` or `"wav"`. + If set to `"detect"`, will try to deduce the type from the filename. +@treturn[1] audio the loaded audio object +@treturn[2] nil if a error happened +@treturn[2] string error message +*/ +static int audio_load(lua_State *L) { + const char *path = luaL_checkstring(L, 1); + const char* argType = luaL_optstring(L, 2, "detect"); + + // Create userdata + audio_userdata *audio = lua_newuserdata(L, sizeof(*audio)); + luaL_getmetatable(L, "LAudio"); + lua_setmetatable(L, -2); + for (int i=0; i<12; i++) audio->mix[i] = 1; + audio->interp = NDSP_INTERP_LINEAR; + audio->speed = 1; + + // Get file type + filetype type = TYPE_UNKNOWN; + if (strcmp(argType, "detect") == 0) { + const char *dot = strrchr(path, '.'); + if (!dot || dot == path) dot = ""; + const char *ext = dot + 1; + if (strncmp(ext, "ogg", 3) == 0) type = TYPE_OGG; + else if (strncmp(ext, "wav", 3) == 0) type = TYPE_WAV; + } else if (strcmp(argType, "ogg") == 0) { + type = TYPE_OGG; + } else if (strcmp(argType, "wav") == 0) { + type = TYPE_WAV; + } + + // Open and read file + if (type == TYPE_OGG) { + audio->type = TYPE_OGG; + + // Load audio file + if (ov_fopen(path, &audio->vf) < 0) { + lua_pushnil(L); + lua_pushstring(L, "input does not appear to be a valid ogg vorbis file or doesn't exist"); + return 2; + } + + // Some Ogg Vorbis decoding parameters + bool bigendianp = false; // bigendianness + u8 word = 2; // word size; 1 or 2 (8bits/sample or 16bits/sample) + bool sgned = true; // signed data + + // Decoding Ogg Vorbis bitstream + vorbis_info* vi = ov_info(&audio->vf, -1); + if (vi == NULL) luaL_error(L, "could not retrieve ogg audio stream informations"); + + audio->rate = vi->rate; + audio->channels = vi->channels; + audio->encoding = NDSP_ENCODING_PCM16; + audio->nsamples = ov_pcm_total(&audio->vf, -1); + audio->size = audio->nsamples * word; + + if (linearSpaceFree() < audio->size) luaL_error(L, "not enough linear memory available"); + audio->data = linearAlloc(audio->size); + + // Decoding loop + int offset = 0; + int eof = 0; + int current_section; + while (!eof) { + long ret = ov_read(&audio->vf, &audio->data[offset], 4096, bigendianp, word, sgned, ¤t_section); + + if (ret == 0) { + eof = 1; + } else if (ret < 0) { + ov_clear(&audio->vf); + linearFree(audio->data); + luaL_error(L, "error in the ogg vorbis stream"); + return 0; + } else { + // TODO handle multiple links (http://xiph.org/vorbis/doc/vorbisfile/decoding.html) + offset += ret; + } + } + + return 1; + + } else if (type == TYPE_WAV) { + audio->type = TYPE_WAV; + + // Used this as a reference for the WAV format: http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html + + // Load file + FILE *file = fopen(path, "rb"); + if (file) { + bool valid = true; // if something goes wrong, this will be false + + char buff[8]; + + // Master chunk + fread(buff, 4, 1, file); // ckId + if (strncmp(buff, "RIFF", 4) != 0) valid = false; + + fseek(file, 4, SEEK_CUR); // skip ckSize + + fread(buff, 4, 1, file); // WAVEID + if (strncmp(buff, "WAVE", 4) != 0) valid = false; + + // fmt Chunk + fread(buff, 4, 1, file); // ckId + if (strncmp(buff, "fmt ", 4) != 0) valid = false; + + fread(buff, 4, 1, file); // ckSize + if (*buff != 16) valid = false; // should be 16 for PCM format + + fread(buff, 2, 1, file); // wFormatTag + if (*buff != 0x0001) valid = false; // PCM format + + u16 channels; + fread(&channels, 2, 1, file); // nChannels + audio->channels = channels; + + u32 rate; + fread(&rate, 4, 1, file); // nSamplesPerSec + audio->rate = rate; + + fseek(file, 4, SEEK_CUR); // skip nAvgBytesPerSec + + u16 byte_per_block; // 1 block = 1*channelCount samples + fread(&byte_per_block, 2, 1, file); // nBlockAlign + + u16 byte_per_sample; + fread(&byte_per_sample, 2, 1, file); // wBitsPerSample + byte_per_sample /= 8; // bits -> bytes + + // There may be some additionals chunks between fmt and data + // TODO handle some usefull chunks that may be here + fread(&buff, 4, 1, file); // ckId + while (strncmp(buff, "data", 4) != 0) { + u32 size; + fread(&size, 4, 1, file); // ckSize + + fseek(file, size, SEEK_CUR); // skip chunk + + int i = fread(&buff, 4, 1, file); // next chunk ckId + + if (i < 4) { // reached EOF before finding a data chunk + valid = false; + break; + } + } + + // data Chunk (ckId already read) + u32 size; + fread(&size, 4, 1, file); // ckSize + audio->size = size; + + audio->nsamples = audio->size / byte_per_block; + + if (byte_per_sample == 1) audio->encoding = NDSP_ENCODING_PCM8; + else if (byte_per_sample == 2) audio->encoding = NDSP_ENCODING_PCM16; + else luaL_error(L, "unknown encoding, needs to be PCM8 or PCM16"); + + if (!valid) { + fclose(file); + luaL_error(L, "invalid PCM wav file"); + return 0; + } + + // Read data + if (linearSpaceFree() < audio->size) luaL_error(L, "not enough linear memory available"); + audio->data = linearAlloc(audio->size); + + fread(audio->data, audio->size, 1, file); + + fclose(file); + return 1; + + } else { + lua_pushnil(L); + lua_pushfstring(L, "error while opening wav file: %s", strerror(errno));; + return 2; + } + } + + luaL_error(L, "unknown audio type"); + return 0; +} + +/*** +Check if audio is currently playing on a channel. +@function playing +@tparam[opt] integer channel number; if `nil` will search the first channel playing an audio +@treturn boolean `true` if the channel is currently playing the audio, `false` otherwise. + If channel is not set, `false` means no audio is playing at all. +*/ +static int audio_playing(lua_State *L) { + if (!isAudioInitialized) { + lua_pushboolean(L, false); + return 1; + } + + int channel = luaL_optinteger(L, 1, -1); + if (channel < -1 || channel > 23) luaL_error(L, "channel number must be between 0 and 23"); + + // Search a channel playing audio + if (channel == -1) { + for (int i = 0; i <= 23; i++) { + if (ndspChnIsPlaying(i)) { + lua_pushboolean(L, true); + return 1; + } + } + + lua_pushboolean(L, false); + return 1; + + } else { + lua_pushboolean(L, ndspChnIsPlaying(channel)); + return 1; + } + + return 0; +} + +/*** +Set the mix parameters (volumes) of a channel. +Volumes go from 0 (0%) to 1 (100%). +Note that when a new audio object will play on this channel, theses parameters will be +reset with the new audio object defaults (set in `audio:mix()`). +@function mix +@tparam[opt] integer channel the channel number, if `nil` will change the mix parmaters of all channels +@tparam[opt=1] number frontLeft front left volume +@tparam[opt=frontLeft] number frontRight front right volume +@tparam[opt=frontLeft] number backLeft back left volume +@tparam[opt=frontRight] number backRight back right volume +*/ +static int audio_mix(lua_State *L) { + if (!isAudioInitialized) luaL_error(L, "audio wasn't initialized correctly"); + + int channel = luaL_optinteger(L, 1, -1); + if (channel < -1 || channel > 23) luaL_error(L, "channel number must be between 0 and 23"); + + float mix[12]; + mix[0] = luaL_optnumber(L, 2, 1); + mix[1] = luaL_optnumber(L, 3, mix[0]); + mix[2] = luaL_optnumber(L, 4, mix[0]); + mix[3] = luaL_optnumber(L, 5, mix[2]); + + if (channel == -1) { + for (int i=0; i<=23; i++) ndspChnSetMix(i, mix); + } else { + ndspChnSetMix(channel, mix); + } + + return 0; +} + +/*** +Set the interpolation type of a channel. +Note that when a new audio object will play on this channel, this parameter will be +reset with the new audio object default (set in `audio:interpolation()`). +@function interpolation +@tparam[opt] integer channel stop playing audio on this channel; if `nil` will change interpolation type on all channels +@tparam[opt=linear] string "none", "linear" or "polyphase" +*/ +static int audio_interpolation(lua_State *L) { + if (!isAudioInitialized) luaL_error(L, "audio wasn't initialized correctly"); + + int channel = luaL_optinteger(L, 1, -1); + if (channel < -1 || channel > 23) luaL_error(L, "channel number must be between 0 and 23"); + + const char* interpArg = luaL_optstring(L, 2, "linear"); + + ndspInterpType interp; + if (strcmp(interpArg, "none") == 0) + interp = NDSP_INTERP_NONE; + else if (strcmp(interpArg, "linear") == 0) + interp = NDSP_INTERP_LINEAR; + else if (strcmp(interpArg, "polyphase") == 0) + interp = NDSP_INTERP_POLYPHASE; + else { + luaL_error(L, "unknown interpolation type"); + return 0; + } + + if (channel == -1) { + for (int i=0; i<=23; i++) ndspChnSetInterp(i, interp); + } else { + ndspChnSetInterp(channel, interp); + } + + return 0; +} + +/*** +Set the speed of the audio playing in a channel. +Speed is expressed as a percentage of the normal playing speed. +1 is 100% speed and 2 is 200%, etc. +Note that when a new audio object will play on this channel, this parameter will be +reset with the new audio object default (set in `audio:speed()`). +@function speed +@tparam[opt] integer channel stop playing audio on this channel; if `nil` will change interpolation type on all channels +@tparam[opt=1] number speed percentage +*/ +static int audio_speed(lua_State *L) { + if (!isAudioInitialized) luaL_error(L, "audio wasn't initialized correctly"); + + int channel = luaL_optinteger(L, 1, -1); + if (channel < -1 || channel > 23) luaL_error(L, "channel number must be between 0 and 23"); + + double speed = luaL_optnumber(L, 2, 1); + + if (channel == -1) { + for (int i=0; i<=23; i++) { + if (channels[i]) ndspChnSetRate(i, channels[i]->rate * speed); + } + } else { + if (channels[channel]) ndspChnSetRate(channel, channels[channel]->rate * speed); + } + + return 0; +} + +/*** +Stop playing all audio on all channels or a specific channel. +@function stop +@tparam[opt] integer channel stop playing audio on this channel; if `nil` will stop audio on all channels +@treturn integer number of channels where audio was stopped +*/ +static int audio_stop(lua_State *L) { + if (!isAudioInitialized) { + lua_pushinteger(L, 0); + return 1; + } + + int channel = luaL_optinteger(L, 1, -1); + + int n = 0; + + if (channel == -1) { + for (int i = 0; i <= 23; i++) { + if (ndspChnIsPlaying(i)) { + ndspChnWaveBufClear(i); + n++; + } + } + } else if (channel < 0 || channel > 23) { + luaL_error(L, "channel number must be between 0 and 23"); + } else { + if (ndspChnIsPlaying(channel)) { + ndspChnWaveBufClear(channel); + n++; + } + } + + lua_pushinteger(L, n); + + return 1; +} + +/*** +audio object +@section Methods +*/ + +/*** +Returns the audio object duration. +@function :duration +@treturn number duration in seconds +*/ +static int audio_object_duration(lua_State *L) { + audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); + + lua_pushnumber(L, (double)(audio->nsamples) / audio->rate); + + return 1; +} + +/*** +Returns the current playing position. +@function :time +@tparam[opt] integer channel number; if `nil` will use the first channel found which played this audio +@treturn number time in seconds +*/ +static int audio_object_time(lua_State *L) { + audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); + + int channel = luaL_optinteger(L, 2, -1); + if (channel < -1 || channel > 23) luaL_error(L, "channel number must be between 0 and 23"); + + // Search a channel playing the audio object + if (channel == -1) { + for (int i = 0; i <= 23; i++) { + if (channels[i] == audio) { + channel = i; + break; + } + } + } + + if (channel == -1 || channels[channel] != audio || !isAudioInitialized) // audio not playing + lua_pushnumber(L, 0); + else + lua_pushnumber(L, (double)(ndspChnGetSamplePos(channel)) / audio->rate); + + return 1; +} + +/*** +Check if the audio is currently playing. +@function :playing +@tparam[opt] integer channel channel number; if `nil` will search the first channel playing this audio +@treturn boolean true if the channel is currently playing the audio, false otherwise +*/ +static int audio_object_playing(lua_State *L) { + if (!isAudioInitialized) { + lua_pushboolean(L, false); + return 1; + } + + audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); + + int channel = luaL_optinteger(L, 2, -1); + if (channel < -1 || channel > 23) luaL_error(L, "channel number must be between 0 and 23"); + + // Search a channel playing the audio object + if (channel == -1) { + for (int i = 0; i <= 23; i++) { + if (channels[i] == audio && ndspChnIsPlaying(i)) { + lua_pushboolean(L, true); + return 1; + } + } + + lua_pushboolean(L, false); + return 1; + + } else { + lua_pushboolean(L, channels[channel] == audio && ndspChnIsPlaying(channel)); + return 1; + } + + return 0; +} + +/*** +Set the mix parameters (volumes) of the audio. +Volumes go from 0 (0%) to 1 (100%). +@function :mix +@tparam[opt=1] number frontLeft front left volume +@tparam[opt=frontLeft] number frontRight front right volume +@tparam[opt=frontLeft] number backLeft back left volume +@tparam[opt=frontRight] number backRight back right volume +*/ +static int audio_object_mix(lua_State *L) { + audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); + + audio->mix[0] = luaL_optnumber(L, 2, 1); + audio->mix[1] = luaL_optnumber(L, 3, audio->mix[0]); + audio->mix[2] = luaL_optnumber(L, 4, audio->mix[0]); + audio->mix[3] = luaL_optnumber(L, 5, audio->mix[2]); + + return 0; +} + +/*** +Set the interpolation type of the audio. +@function :interpolation +@tparam[opt=linear] string "none", "linear" or "polyphase" +*/ +static int audio_object_interpolation(lua_State *L) { + audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); + + const char* interp = luaL_optstring(L, 2, "linear"); + + if (strcmp(interp, "none") == 0) + audio->interp = NDSP_INTERP_NONE; + else if (strcmp(interp, "linear") == 0) + audio->interp = NDSP_INTERP_LINEAR; + else if (strcmp(interp, "polyphase") == 0) + audio->interp = NDSP_INTERP_POLYPHASE; + else { + luaL_error(L, "unknown interpolation type"); + return 0; + } + + return 0; +} + +/*** +Set the speed of the audio. +Speed is expressed as a percentage of the normal playing speed. +1 is 100% speed and 2 is 200%, etc. +@function :speed +@tparam[opt=1] number speed percentage +*/ +static int audio_object_speed(lua_State *L) { + audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); + + audio->speed = luaL_optnumber(L, 2, 1); + + return 0; +} + +/*** +Plays the audio file. +@function :play +@tparam[opt=false] boolean loop if the audio should loop or not +@tparam[opt] integer channel the channel to play the audio on (0-23); if `nil` will use the first available channel. + If the channel was playing another audio, it will be stopped and replaced by this audio. + If not set and no channel is available, will return nil plus an error message. +@treturn[1] integer channel number the audio is playing on +@treturn[2] nil an error happened and the audio was not played +@treturn[2] error the error message +*/ +static int audio_object_play(lua_State *L) { + if (!isAudioInitialized) luaL_error(L, "audio wasn't initialized correctly"); + + audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); + bool loop = lua_toboolean(L, 2); + int channel = luaL_optinteger(L, 3, -1); + + // Find a free channel + if (channel == -1) { + for (int i = 0; i <= 23; i++) { + if (!ndspChnIsPlaying(i)) { + channel = i; + break; + } + } + } + if (channel == -1) { + lua_pushnil(L); + lua_pushstring(L, "no audio channel is currently available"); + return 2; + } + if (channel < 0 || channel > 23) luaL_error(L, "channel number must be between 0 and 23"); + + // Set channel parameters + ndspChnWaveBufClear(channel); + ndspChnReset(channel); + ndspChnInitParams(channel); + ndspChnSetMix(channel, audio->mix); + ndspChnSetInterp(channel, audio->interp); + ndspChnSetRate(channel, audio->rate * audio->speed); // maybe hackish way to set a different speed, but it works + ndspChnSetFormat(channel, NDSP_CHANNELS(audio->channels) | NDSP_ENCODING(audio->encoding)); + + // Send & play audio data + ndspWaveBuf* waveBuf = calloc(1, sizeof(ndspWaveBuf)); + + waveBuf->data_vaddr = audio->data; + waveBuf->nsamples = audio->nsamples; + waveBuf->looping = loop; + + DSP_FlushDataCache((u32*)audio->data, audio->size); + + ndspChnWaveBufAdd(channel, waveBuf); + channels[channel] = audio; + + lua_pushinteger(L, channel); + + return 1; +} + +/*** +Stop playing an audio object. +@function :stop +@tparam[opt] integer channel stop playing the audio on this channel; if `nil` will stop all channels playing this audio. + If the channel is playing another audio object, this function will do nothing. +@treturn integer number of channels where this audio was stopped +*/ +static int audio_object_stop(lua_State *L) { + if (!isAudioInitialized) { + lua_pushinteger(L, 0); + return 1; + } + + audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); + int channel = luaL_optinteger(L, 2, -1); + + int n = 0; + + if (channel == -1) { + for (int i = 0; i <= 23; i++) { + if (channels[i] == audio && ndspChnIsPlaying(i)) { + ndspChnWaveBufClear(i); + n++; + } + } + } else if (channel < 0 || channel > 23) { + luaL_error(L, "channel number must be between 0 and 23"); + } else { + if (channels[channel] == audio && ndspChnIsPlaying(channel)) { + ndspChnWaveBufClear(channel); + n++; + } + } + + lua_pushinteger(L, n); + + return 1; +} + +/*** +Returns the audio object type. +@function :type +@treturn string "ogg" or "wav" +*/ +static int audio_object_type(lua_State *L) { + audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); + + if (audio->type == TYPE_OGG) + lua_pushstring(L, "ogg"); + else if (audio->type == TYPE_WAV) + lua_pushstring(L, "wav"); + + return 1; +} + +/*** +Unload an audio object. +@function :unload +*/ +static int audio_object_unload(lua_State *L) { + audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); + + // Stop playing the audio + if (isAudioInitialized) { + for (int i = 0; i <= 23; i++) { + if (channels[i] == audio) { + ndspChnWaveBufClear(i); + } + } + } + + if (audio->type == TYPE_OGG) ov_clear(&audio->vf); + + // Free memory + linearFree(audio->data); + + return 0; +} + +/*** +audio object (ogg-only). +Ogg Vorbis files specific methods. +Using one of theses methods will throw an error if used on an non-ogg audio object. +@section Methods +*/ + +/*** +Returns basic information about the audio in a vorbis bitstream. +@function :info +@treturn infoTable information table +*/ +static int audio_object_info(lua_State *L) { + audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); + + if (audio->type != TYPE_OGG) luaL_error(L, "only avaible on OGG audio objects"); + + vorbis_info* vi = ov_info(&audio->vf, -1); + if (vi == NULL) luaL_error(L, "could not retrieve audio stream informations"); + + lua_createtable(L, 0, 6); + + lua_pushinteger(L, vi->version); + lua_setfield(L, -2, "version"); + + lua_pushinteger(L, vi->channels); + lua_setfield(L, -2, "channels"); + + lua_pushinteger(L, vi->rate); + lua_setfield(L, -2, "rate"); + + lua_pushinteger(L, vi->bitrate_upper); + lua_setfield(L, -2, "bitrateUpper"); + + lua_pushinteger(L, vi->bitrate_nominal); + lua_setfield(L, -2, "bitrateNominal"); + + lua_pushinteger(L, vi->bitrate_lower); + lua_setfield(L, -2, "bitrateLower"); + + return 1; +} + +/*** +Returns the Ogg Vorbis bitstream comment. +@function :comment +@treturn commentTable comment table +*/ +static int audio_object_comment(lua_State *L) { + audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); + + if (audio->type != TYPE_OGG) luaL_error(L, "only avaible on OGG audio objects"); + + vorbis_comment *vc = ov_comment(&audio->vf, -1); + + if (vc == NULL) luaL_error(L, "could not retrieve audio stream comment"); + + lua_createtable(L, 0, 5); + + lua_newtable(L); + for (int i=0; icomments; i++) { + lua_pushstring(L, vc->user_comments[i]); + lua_seti(L, -2, i+1); + } + lua_setfield(L, -2, "userComments"); + + lua_pushstring(L, vc->vendor); + lua_setfield(L, -2, "vendor"); + + return 1; +} + +/*** +Tables return. +The detailled table structures returned by some methods of audio objects. +@section +*/ + +/*** +Vorbis bitstream information, returned by audio:info(). +If bitrateLower == bitrateNominal == bitrateUpper, the stream is fixed bitrate. +@table infoTable +@tfield integer version Vorbis encoder version used to create this bitstream +@tfield integer channels number of channels in bitstream +@tfield integer rate sampling rate of the bitstream +@tfield integer bitrateUpper the upper limit in a VBR bitstream; may be unset if no limit exists +@tfield integer bitrateNominal the average bitrate for a VBR bitstream; may be unset +@tfield integer bitrateLower the lower limit in a VBR bitstream; may be unset if no limit exists +*/ + +/*** +Vorbis bitstream comment, returned by audio:comment(). +@table commentTable +@tfield table userComments list of all the user comment +@tfield string vendor information about the Vorbis implementation that encoded the file +*/ + +// Audio object methods +static const struct luaL_Reg audio_object_methods[] = { + // common + { "duration", audio_object_duration }, + { "time", audio_object_time }, + { "playing", audio_object_playing }, + { "mix", audio_object_mix }, + { "interpolation", audio_object_interpolation }, + { "speed", audio_object_speed }, + { "play", audio_object_play }, + { "stop", audio_object_stop }, + { "type", audio_object_type }, + { "unload", audio_object_unload }, + { "__gc", audio_object_unload }, + // ogg only + { "info", audio_object_info }, + { "comment", audio_object_comment }, + { NULL, NULL } +}; + +// Library functions +static const struct luaL_Reg audio_lib[] = { + { "load", audio_load }, + { "playing", audio_playing }, + { "mix", audio_mix }, + { "interpolation", audio_interpolation }, + { "speed", audio_speed }, + { "stop", audio_stop }, + { NULL, NULL } +}; + +int luaopen_audio_lib(lua_State *L) { + luaL_newmetatable(L, "LAudio"); + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); + luaL_setfuncs(L, audio_object_methods, 0); + + luaL_newlib(L, audio_lib); + + return 1; +} + +void load_audio_lib(lua_State *L) { + isAudioInitialized = !ndspInit(); // ndspInit returns 0 in case of success + + luaL_requiref(L, "ctr.audio", luaopen_audio_lib, false); +} + +void unload_audio_lib(lua_State *L) { + if (isAudioInitialized) ndspExit(); +} diff --git a/source/ctr.c b/source/ctr.c index da788c0..0eff2a8 100644 --- a/source/ctr.c +++ b/source/ctr.c @@ -91,6 +91,14 @@ The `ctr.cam` module. */ void load_cam_lib(lua_State *L); +/*** +The `ctr.audio` module. +@table audio +@see ctr.audio +*/ +void load_audio_lib(lua_State *L); +void unload_audio_lib(lua_State *L); + /*** Return whether or not the program should continue. @function run @@ -132,7 +140,8 @@ struct { char *name; void (*load)(lua_State *L); void (*unload)(lua_State *L); } { "qtm", load_qtm_lib, NULL }, { "cfgu", load_cfgu_lib, NULL }, { "socket", load_socket_lib, NULL }, - { "cam", load_cam_lib, NULL }, + { "cam", load_cam_lib, NULL }, + { "audio", load_audio_lib, unload_audio_lib }, { NULL, NULL } }; diff --git a/source/gfx.c b/source/gfx.c index aa64956..49c8195 100644 --- a/source/gfx.c +++ b/source/gfx.c @@ -16,7 +16,7 @@ The `gfx` module. #include "font.h" -bool isGfxInitialised = false; +bool isGfxInitialized = false; bool is3DEnabled = false; //TODO: add a function for this in the ctrulib/sf2dlib. /*** @@ -459,7 +459,7 @@ void load_gfx_lib(lua_State *L) { sf2d_init(); sftd_init(); - isGfxInitialised = true; + isGfxInitialized = true; luaL_requiref(L, "ctr.gfx", luaopen_gfx_lib, 0); } diff --git a/source/main.c b/source/main.c index c9e7473..270c655 100644 --- a/source/main.c +++ b/source/main.c @@ -7,11 +7,11 @@ void load_ctr_lib(lua_State *L); void unload_ctr_lib(lua_State *L); -bool isGfxInitialised; +bool isGfxInitialized; // Display an error void error(const char *error) { - if (!isGfxInitialised) gfxInitDefault(); + if (!isGfxInitialized) gfxInitDefault(); gfxSet3D(false); consoleInit(GFX_TOP, NULL); @@ -28,7 +28,7 @@ void error(const char *error) { gspWaitForVBlank(); } - if (!isGfxInitialised) gfxExit(); + if (!isGfxInitialized) gfxExit(); } // Main loop