diff options
-rw-r--r-- | applause.lua | 56 | ||||
-rw-r--r-- | sndfile.lua | 197 |
2 files changed, 253 insertions, 0 deletions
diff --git a/applause.lua b/applause.lua index 423268a..6fa25a6 100644 --- a/applause.lua +++ b/applause.lua @@ -1,3 +1,4 @@ +local sndfile = require "sndfile" local ffi = require "ffi" -- @@ -290,6 +291,26 @@ function Stream:play() error("C function not registered!") end +function Stream:save(filename, format) + if self:len() == math.huge then + error("Cannot save infinite stream") + end + + local hnd = sndfile:new(filename, "SFM_WRITE", + samplerate, 1, format) + + local tick = self:tick() + + while true do + local sample = tick() + if not sample then break end + + hnd:write(sample) + end + + hnd:close() +end + function Stream:toplot(rows, cols) rows = rows or 25 cols = cols or 80 @@ -440,6 +461,41 @@ function VectorStream:len() return #self.vector end +SndfileStream = DeriveClass(Stream, function(self, filename) + self.filename = filename +end) + +function SndfileStream:tick() + -- NOTE: Opening the file here has the advantage + -- that the stream can be reused multiple times + -- FIXME: This fails if the file is not at the + -- correct sample rate. Need to resample... + local handle = sndfile:new(self.filename, "SFM_READ") + + return function() + if not handle then return end + + local sample = handle:read() + if not sample then + handle:close() + handle = nil + end + + return sample + end +end + +function SndfileStream:len() + -- Since the file is not yet opened, + -- we must assume that its size never changes between + -- calling len() and depending on the length reported + local handle = sndfile:new(self.filename, "SFM_READ") + local len = tonumber(handle.info.frames) + + handle:close() + return len +end + ConcatStream = DeriveClass(Stream, function(self, ...) self.is_concatstream = true diff --git a/sndfile.lua b/sndfile.lua new file mode 100644 index 0000000..263893d --- /dev/null +++ b/sndfile.lua @@ -0,0 +1,197 @@ +-- module table +local sndfile = {} + +local ffi = require "ffi" + +ffi.cdef[[ +typedef struct SNDFILE_tag SNDFILE; + +typedef int64_t sf_count_t; + +typedef enum SF_FORMAT { /* Major formats. */ + SF_FORMAT_WAV = 0x010000, /* Microsoft WAV format (little endian default). */ + SF_FORMAT_AIFF = 0x020000, /* Apple/SGI AIFF format (big endian). */ + SF_FORMAT_AU = 0x030000, /* Sun/NeXT AU format (big endian). */ + SF_FORMAT_RAW = 0x040000, /* RAW PCM data. */ + SF_FORMAT_PAF = 0x050000, /* Ensoniq PARIS file format. */ + SF_FORMAT_SVX = 0x060000, /* Amiga IFF / SVX8 / SV16 format. */ + SF_FORMAT_NIST = 0x070000, /* Sphere NIST format. */ + SF_FORMAT_VOC = 0x080000, /* VOC files. */ + SF_FORMAT_IRCAM = 0x0A0000, /* Berkeley/IRCAM/CARL */ + SF_FORMAT_W64 = 0x0B0000, /* Sonic Foundry's 64 bit RIFF/WAV */ + SF_FORMAT_MAT4 = 0x0C0000, /* Matlab (tm) V4.2 / GNU Octave 2.0 */ + SF_FORMAT_MAT5 = 0x0D0000, /* Matlab (tm) V5.0 / GNU Octave 2.1 */ + SF_FORMAT_PVF = 0x0E0000, /* Portable Voice Format */ + SF_FORMAT_XI = 0x0F0000, /* Fasttracker 2 Extended Instrument */ + SF_FORMAT_HTK = 0x100000, /* HMM Tool Kit format */ + SF_FORMAT_SDS = 0x110000, /* Midi Sample Dump Standard */ + SF_FORMAT_AVR = 0x120000, /* Audio Visual Research */ + SF_FORMAT_WAVEX = 0x130000, /* MS WAVE with WAVEFORMATEX */ + SF_FORMAT_SD2 = 0x160000, /* Sound Designer 2 */ + SF_FORMAT_FLAC = 0x170000, /* FLAC lossless file format */ + SF_FORMAT_CAF = 0x180000, /* Core Audio File format */ + SF_FORMAT_WVE = 0x190000, /* Psion WVE format */ + SF_FORMAT_OGG = 0x200000, /* Xiph OGG container */ + SF_FORMAT_MPC2K = 0x210000, /* Akai MPC 2000 sampler */ + SF_FORMAT_RF64 = 0x220000, /* RF64 WAV file */ + + /* Subtypes from here on. */ + + SF_FORMAT_PCM_S8 = 0x0001, /* Signed 8 bit data */ + SF_FORMAT_PCM_16 = 0x0002, /* Signed 16 bit data */ + SF_FORMAT_PCM_24 = 0x0003, /* Signed 24 bit data */ + SF_FORMAT_PCM_32 = 0x0004, /* Signed 32 bit data */ + + SF_FORMAT_PCM_U8 = 0x0005, /* Unsigned 8 bit data (WAV and RAW only) */ + + SF_FORMAT_FLOAT = 0x0006, /* 32 bit float data */ + SF_FORMAT_DOUBLE = 0x0007, /* 64 bit float data */ + + SF_FORMAT_ULAW = 0x0010, /* U-Law encoded. */ + SF_FORMAT_ALAW = 0x0011, /* A-Law encoded. */ + SF_FORMAT_IMA_ADPCM = 0x0012, /* IMA ADPCM. */ + SF_FORMAT_MS_ADPCM = 0x0013, /* Microsoft ADPCM. */ + + SF_FORMAT_GSM610 = 0x0020, /* GSM 6.10 encoding. */ + SF_FORMAT_VOX_ADPCM = 0x0021, /* OKI / Dialogix ADPCM */ + + SF_FORMAT_G721_32 = 0x0030, /* 32kbs G721 ADPCM encoding. */ + SF_FORMAT_G723_24 = 0x0031, /* 24kbs G723 ADPCM encoding. */ + SF_FORMAT_G723_40 = 0x0032, /* 40kbs G723 ADPCM encoding. */ + + SF_FORMAT_DWVW_12 = 0x0040, /* 12 bit Delta Width Variable Word encoding. */ + SF_FORMAT_DWVW_16 = 0x0041, /* 16 bit Delta Width Variable Word encoding. */ + SF_FORMAT_DWVW_24 = 0x0042, /* 24 bit Delta Width Variable Word encoding. */ + SF_FORMAT_DWVW_N = 0x0043, /* N bit Delta Width Variable Word encoding. */ + + SF_FORMAT_DPCM_8 = 0x0050, /* 8 bit differential PCM (XI only) */ + SF_FORMAT_DPCM_16 = 0x0051, /* 16 bit differential PCM (XI only) */ + + SF_FORMAT_VORBIS = 0x0060, /* Xiph Vorbis encoding. */ + + /* Endian-ness options. */ + + SF_ENDIAN_FILE = 0x00000000, /* Default file endian-ness. */ + SF_ENDIAN_LITTLE = 0x10000000, /* Force little endian-ness. */ + SF_ENDIAN_BIG = 0x20000000, /* Force big endian-ness. */ + SF_ENDIAN_CPU = 0x30000000, /* Force CPU endian-ness. */ + + SF_FORMAT_SUBMASK = 0x0000FFFF, + SF_FORMAT_TYPEMASK = 0x0FFF0000, + SF_FORMAT_ENDMASK = 0x30000000 +} SF_FORMAT; + +typedef struct SF_INFO { + sf_count_t frames; /* Used to be called samples. Changed to avoid confusion. */ + int samplerate; + int channels; + int format; + int sections; + int seekable; +} SF_INFO; + +typedef struct { + int format; + const char *name; + const char *extension; +} SF_FORMAT_INFO; + +/* The enum was not named in the sndfile.h */ +typedef enum SF_MODE { + /* Modes for opening files. */ + SFM_READ = 0x10, + SFM_WRITE = 0x20, + SFM_RDWR = 0x30 +} SF_MODE; + +SNDFILE* sf_open(const char *path, int mode, SF_INFO *sfinfo); +const char* sf_strerror(SNDFILE *sndfile); + +int sf_command(SNDFILE *sndfile, int command, void *data, int datasize); + +sf_count_t sf_read_double(SNDFILE *sndfile, double *ptr, sf_count_t items) ; +sf_count_t sf_write_double(SNDFILE *sndfile, const double *ptr, sf_count_t items); + +int sf_close(SNDFILE *sndfile); +]] + +local lib = ffi.load("sndfile") + +local double_type = ffi.typeof("double[?]") + +-- This can be reused in sndfile:read() and sndfile:write() +-- to avoid allocations. +local double_buffer = double_type(1) + +-- NOTE: Constants are also in ffi.C +sndfile.SF_FORMAT = ffi.typeof("SF_FORMAT") + +-- FIXME: Maybe fall back to sf_command(SFC_GET_SIMPLE_FORMAT) +-- for unknown extensions. The simple formats are not always +-- ideal. +function sndfile.guess_format(filename) + local ext2format = { + raw = ffi.C.SF_FORMAT_RAW + ffi.C.SF_FORMAT_DOUBLE, + wav = ffi.C.SF_FORMAT_WAV + ffi.C.SF_FORMAT_FLOAT, + xi = ffi.C.SF_FORMAT_XI + ffi.C.SF_FORMAT_DPCM_16, + flac = ffi.C.SF_FORMAT_FLAC + ffi.C.SF_FORMAT_PCM_24, + ogg = ffi.C.SF_FORMAT_OGG + ffi.C.SF_FORMAT_DOUBLE + } + + local ext = filename:match("%.(.+)$") + + -- assume raw format for missing extensions + local format = ext2format[ext and ext:lower() or "raw"] + if not format then error("Unknown extension \""..ext.."\"") end + + return format +end + +function sndfile:new(path, mode, samplerate, channels, format) + local obj = {} + + local info = ffi.new("SF_INFO[1]") + + if mode ~= "SFM_READ" then + info[0].samplerate = samplerate or 44100 + info[0].channels = channels or 1 + info[0].format = format or sndfile.guess_format(path) + end + + obj.handle = lib.sf_open(path, ffi.new("SF_MODE", mode), info) + if obj.handle == nil then + error(ffi.string(lib.sf_strerror(nil))) + end + + obj.info = info[0] + + -- NOTE: FFI also allows metatables and finalizers + -- to be set on the C-type itself. + setmetatable(obj, { + __index = self, + __gc = sndfile.close + }) + + return obj +end + +-- TODO: Maybe support reading multiple samples at once. +function sndfile:read() + return lib.sf_read_double(self.handle, double_buffer, 1) == 1 and + tonumber(double_buffer[0]) or nil +end + +-- TODO: Maybe support writing multiple samples at once +function sndfile:write(sample) + double_buffer[0] = sample + return lib.sf_write_double(self.handle, double_buffer, 1) +end + +function sndfile:close() + if self.handle then + lib.sf_close(self.handle) + self.handle = nil + end +end + +return sndfile |