aboutsummaryrefslogtreecommitdiffhomepage
path: root/sndfile-stream.lua
blob: 68ed5ccc5f57fdfc92cbbb82f2a0a2122cde9184 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
local sndfile = require "sndfile"

SndfileStream = DeriveClass(Stream)

function SndfileStream:ctor(filename, samplerate, channels, format)
	-- FIXME: This fails if the file is not at the
	-- correct sample rate. Need to resample...
	-- NOTE: samplerate and channels are ignored unless SF_FORMAT_RAW
	-- files are read.
	local handle = sndfile:new(filename, "SFM_READ",
	                           samplerate, channels, format)
	self.filename = filename
	self.samplerate = handle.info.samplerate
	self.channel_no = handle.info.channels
	self.format = handle.info.format
	self.frames = tonumber(handle.info.frames)
	handle:close()

	if self.channel_no > 1 then
		local cached = self:cache()
		local streams = {}
		for i = 0, self.channel_no-1 do
			streams[i+1] = cached:map(function(frame)
				return tonumber(frame[i])
			end)
		end
		return MuxStream:new(unpack(streams))
	end
end

function SndfileStream:gtick()
	-- The file is reopened, so each tick has an independent
	-- read pointer which is important when reusing the stream.
	-- NOTE: We could do this with a single handle per object but
	-- by maintaining our own read position and seeking before reading.
	local handle = sndfile:new(self.filename, "SFM_READ",
	                           self.samplerate, self.channel_no, self.format)

	-- Make sure that we are still reading the same file;
	-- at least with the same meta-data.
	-- Theoretically, the file could have changed since object
	-- construction.
	assert(handle.info.channels == self.channel_no and
	       handle.info.frames == self.frames,
	       "Sndfile changed")

	if self.channel_no == 1 then
		local read = handle.read

		return function()
			return read(handle)
		end
	else
		-- For multi-channel audio files, we generate a stream
		-- of frame buffers.
		-- However, the user never sees these since they are translated
		-- to a MuxStream automatically (see ctor())
		local readf = handle.readf
		local frame = sndfile.frame_type(self.channel_no)

		return function()
			return readf(handle, frame) and frame or nil
		end
	end
end

function SndfileStream:len() return self.frames end

-- TODO: Use a buffer to improve perfomance (e.g. 1024 samples)
function Stream:save(filename, format)
	if self:len() == math.huge then
		error("Cannot save infinite stream")
	end

	local channels = self.channels
	local hnd = sndfile:new(filename, "SFM_WRITE",
	                        samplerate, channels, format)

	local frame_buffer = sndfile.frame_type(channels)

	self:foreach(function(frame)
		-- NOTE: This should be (hopefully) automatically
		-- unrolled for single-channel streams
		-- Otherwise each loop copies an entire frame.
		-- This should be faster than letting LuaJIT translate
		-- the frame directly.
		for i = 1, channels do
			local sample = tonumber(frame[i])
			assert(sample ~= nil)
			frame_buffer[i-1] = sample
		end

		-- NOTE: Apparently we cannot use hnd:write() if a frame is larger than one sample
		-- (i.e. multichannel streams)
		-- FIXME: Check return value
		hnd:writef(frame_buffer)
	end)

	hnd:close()
end