-
Notifications
You must be signed in to change notification settings - Fork 62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Help with using with love2d #64
Comments
I would also like to output audio in love, and have a bit of experience with raw-audio output (see lua-openmpt) but again, really not sure how to get a handle of some raw sound-data from luaradio. |
As a sidenote, I'm happy to write a nice love2d demo for luaradio, once I figure it out. I feel like this is a really ripe area for fancy graphical radio apps, with a super-custom UI. |
I realized this just uses luajit, and works fine there, without anything special. That's great! Love2d also uses luajit too, so often things are just compatible, so I tried to use it directly, and got this error:
Here is my script (based on this) that works in luajit, but not love: local radio = require('radio')
local function playFM(frequency)
local tune_offset = -250e3
local source = radio.RtlSdrSource(frequency + tune_offset, 1102500)
local tuner = radio.TunerBlock(tune_offset, 200e3, 5)
local fm_demod = radio.FrequencyDiscriminatorBlock(1.25)
local hilbert = radio.HilbertTransformBlock(129)
local delay = radio.DelayBlock(129)
local pilot_filter = radio.ComplexBandpassFilterBlock(129, {18e3, 20e3})
local pilot_pll = radio.PLLBlock(100, 19e3-50, 19e3+50, 2)
local mixer = radio.MultiplyConjugateBlock()
local lpr_filter = radio.LowpassFilterBlock(128, 15e3)
local lpr_am_demod = radio.ComplexToRealBlock()
local lmr_filter = radio.LowpassFilterBlock(128, 15e3)
local lmr_am_demod = radio.ComplexToRealBlock()
local l_summer = radio.AddBlock()
local l_af_deemphasis = radio.FMDeemphasisFilterBlock(75e-6)
local l_downsampler = radio.DownsamplerBlock(5)
local r_subtractor = radio.SubtractBlock()
local r_af_deemphasis = radio.FMDeemphasisFilterBlock(75e-6)
local r_downsampler = radio.DownsamplerBlock(5)
local top = radio.CompositeBlock()
-- this should be some other way to get it into love...
local sink = radio.PulseAudioSink(2)
top:connect(source, tuner, fm_demod, hilbert, delay)
top:connect(hilbert, pilot_filter, pilot_pll)
top:connect(delay, 'out', mixer, 'in1')
top:connect(pilot_pll, 'out', mixer, 'in2')
top:connect(delay, lpr_filter, lpr_am_demod)
top:connect(mixer, lmr_filter, lmr_am_demod)
top:connect(lpr_am_demod, 'out', l_summer, 'in1')
top:connect(lmr_am_demod, 'out', l_summer, 'in2')
top:connect(lpr_am_demod, 'out', r_subtractor, 'in1')
top:connect(lmr_am_demod, 'out', r_subtractor, 'in2')
top:connect(l_summer, l_af_deemphasis, l_downsampler)
top:connect(r_subtractor, r_af_deemphasis, r_downsampler)
top:connect(l_downsampler, 'out', sink, 'in1')
top:connect(r_downsampler, 'out', sink, 'in2')
return top
end
-- OPB, in Portland
local myradio = playFM(91.5e6)
myradio:run() |
Does |
That may have been a red-herring. I think it might be because I had some other FFI thing (like luarequest) loaded. I made a cross-lua version that works in both, here, so I guess my original question remains: what is the best way to transfer audio memory & fft-data to love? |
A love2d frontend would be super cool. There's basically two ways to approach this. I should first explain that LuaRadio spawns every block in a flow graph in its own process (they communicate with one another via anonymous UNIX sockets created with There are two basic approaches to interface with love2d. The first approach would be to create a LuaRadio block, and handle love entirely within it. This has the benefit of looking like any other LuaRadio block that sinks samples, so it's easy to use in a pure LuaRadio script, but has the downside of being limited in purpose and functionality to what any other block can do, which is just sink and source samples across some number of inputs and outputs. For a standalone waterfall sink with its own window and love instance, this might be OK, but for a more involved UI, or some frontend with more than one purpose, it's probably pretty limiting. The other downside of this approach is that you'll have to find a way to service LuaRadio's sample processing loop inside of love's event loop. The second approach is to have the parent process handle love (and love's event loop), while the child processes are taking care of the signal processing. The flow graph can deliver data back to the parent process through an IPC mechanism like a local radio = require('radio')
local ffi = require('ffi')
ffi.cdef[[
int pipe(int fildes[2]);
]]
-- LuaRadio flow graph top block
top = radio.CompositeBlock()
-- fd pair from pipe(), fds[0] is read end, fds[1] is write end
fds = ffi.new('int[2]')
-- read buffer
buf = ffi.new('char[1024]')
-- last value read
value = nil
function love.load()
-- Create a pipe
assert(ffi.C.pipe(fds) == 0)
-- Create a flow graph sinking a slow triangle wave to the write end of the pipe
local src = radio.SignalSource('triangle', 0.1, 100e3)
local throttle = radio.ThrottleBlock()
local snk = radio.RealFileSink(fds[1], 's8')
top:connect(src, throttle, snk)
-- Start the flow graph
top:start()
end
function love.update()
-- Poll for new samples from the read end of the pipe
local pollfds = ffi.new("struct pollfd[1]")
pollfds[0].fd = fds[0]
pollfds[0].events = ffi.C.POLLIN
local ret = ffi.C.poll(pollfds, 1, 0)
assert(ret >= 0)
-- If there are new samples, read them from the pipe into buf
if ret > 0 then
local bytes_read = ffi.C.read(fds[0], buf, ffi.sizeof(buf))
assert(bytes_read > 0)
-- Save the last value read
value = buf[bytes_read - 1]
end
end
function love.draw()
-- Draw the last value read
if value then
love.graphics.print(value, 200, 200)
end
end
function love.quit()
-- Close the read end of the pipe
ffi.C.close(fds[0])
-- Stop and clean up the flow graph
top:stop()
top:wait()
end This example spawns a LuaRadio flow graph with a slow triangle wave source that is sinked into a pipe, periodically consumed by the love program (in the Audio data would be no different, just substitute the There are some low-level C library calls in there ( |
Excellent info. Thanks! Seems very straightforward. I think I could probly wrap this second approach all in a simple (and familiar to love users) interface, and it'd be just ideal. I noticed there is no way to change parameters, in some other forum, so changing the frequency in this situation is off the table, right? I'd have to stop, then start it again? |
That's right, the block parameters are currently frozen at instantiation time and would require a restart. Once #40 is implemented, blocks will be able to register control interfaces that can receive msgpack or JSON serialized messages to handle parameters changes at runtime (this time probably through a UNIX socket path). This will open up all sorts of possibilities -- aside from runtime parameter changes, you could spawn multiple different receiver chains (AM, WBFM, NBFM, etc.) and have simple a "gating" block in front of each one that turns on and off sample flow to each of chains to enable/disable different receivers at runtime. This would be pretty efficient too, because when a receiver is gated, there is no sample flow, and thus no signal processing or CPU consumption. |
Nice! The I am wondering how I should interface this. My first instinct is to do as I did with openmpt and copy the mem-buffer into a love Maybe a local top
function love.load()
local src = radio.SignalSource('triangle', 440, 44100)
local throttle = radio.ThrottleBlock()
local snk = LoveAudioSink(1, 44100) -- 1 for mono, like how pulse works
top = radio.CompositeBlock()
top:connect(src, throttle, snk)
top:start()
end
function love.quit()
top:stop()
top:wait()
end Does that seem reasonable? Since I have a pointer to the memory for the audio-stream, in love (via Is there a reason there is no Also, I think maybe the demo-code is not closing things properly. I get this error, on exit (via Ctrl-C or close button):
|
I'm still not quite figuring out how to do this with audio. I tried looking at pulseaudio as an example, but I think I am missing some key C-related thing (I keep getting type errors.) In love, there is a love.sound.newSoundData that creates a memory-buffer In other FFI'd libraries, you can push interleaved audio data into that buffer and play it with love.audio.newQueueableSource, but I'm not understanding how to do that here. Here is an example from openmpt: local mod
local sd
local qs
local bitDepth = 16
local samplingRate = 44100
local channelCount = 2
local bufferSize = 1024
function love.load()
mod = OpenMPT:new("strobe_-_android_assembled.xm")
sd = love.sound.newSoundData(bufferSize, samplingRate, bitDepth, channelCount)
qs = love.audio.newQueueableSource(samplingRate, bitDepth, channelCount)
end
function love.update(dt)
if qs:getFreeBufferCount() == 0 then return end
mod:read_interleaved_stereo(samplingRate, bufferSize, sd:getFFIPointer())
qs:queue(sd)
qs:play()
end Is it possible to fit this into a luaradio block? local LoveAudioSink = block.factory("LoveAudioSink")
function LoveAudioSink:instantiate(num_channels, samplingRate, bitDepth, bufferSize)
end
function LoveAudioSink:initialize()
-- setup sd & qs here
end
function LoveAudioSink:process(...)
local samples = {...}
-- push interleaved audio into sd
end
function LoveAudioSink:cleanup()
-- destroy sd
end |
This is fine, but keep in mind that the LoveAudioSink and the top-level application will be two separate Love applications that won't be able to share state, and can't really communicate with one another easily out of the box (the control interface in #40 would help with this).
Yeah, it should be possible in a LoveAudioSink to copy the input samples directly to this audio buffer.
There's no reason this can't exist, but it would likely have limited performance compared to sinking contiguous buffers of C types. There are some other serialization sinks (e.g. JSONSink) already, but they're generally used with slower, structured data.
Yeah, this is a known issue with blocking pipes. As a hack, I'm closing the read end of the pipe to collapse the flow graph. A cleaner solution to this across the board requires catching interrupted system calls and calling the |
I think the first problem you'll encounter here is that both LuaRadio blocks and Love applications have their own implied Here is an example of playing audio with the second approach: local radio = require('radio')
local ffi = require('ffi')
ffi.cdef[[
int pipe(int fildes[2]);
]]
-- LuaRadio flow graph
top = radio.CompositeBlock()
-- fd pair from pipe(), fds[0] is read end, fds[1] is write end
fds = ffi.new('int[2]')
-- audio source
source = love.audio.newQueueableSource(44100, 16, 1, 16)
-- audio buffer
buffer = love.sound.newSoundData(1024, 44100, 16, 1)
function love.load()
-- Create a pipe
assert(ffi.C.pipe(fds) == 0)
-- Create a flow graph sinking a 440 Hz triangle to the write end of the pipe
local src = radio.SignalSource('triangle', 440, 44100)
local throttle = radio.ThrottleBlock()
local snk = radio.RealFileSink(fds[1], 's16le')
top:connect(src, throttle, snk)
-- Start the flow graph
top:start()
end
function love.update()
-- If source is full, skip reading new samples
if source:getFreeBufferCount() == 0 then
return
end
-- Poll for new samples from the read end of the pipe
local pollfds = ffi.new("struct pollfd[1]")
pollfds[0].fd = fds[0]
pollfds[0].events = ffi.C.POLLIN
local ret = ffi.C.poll(pollfds, 1, 0)
assert(ret >= 0)
-- If there are new samples, read them from the pipe into buf
if ret > 0 then
local bytes_read = ffi.C.read(fds[0], buffer:getFFIPointer(), buffer:getSize())
assert(bytes_read > 0)
-- Write to audio source
source:queue(buffer, tonumber(bytes_read))
source:play()
end
end
function love.draw()
love.graphics.print(string.format("source free buffers: %d", source:getFreeBufferCount()), 200, 200)
end
function love.quit()
-- Close read end of the pipe
ffi.C.close(fds[0])
-- Stop and clean up the flow graph
top:stop()
top:wait()
end |
It seems like I still get this prob, even if I catch it myself and close things (in luajit and love) but I am probly doing it wrong.
If all of this is running inside a single love app, why can't they talk to each other? If I am understanding how it fits together, I think it should perform about as well as port or pulseaudio (it uses openal under the hood, which is maybe even lighter.)
For a lua-space thing (they are both running in same lua runtime) I think it might be nice in my stuff to emit on a callback (so I don't have to use serialize JSON on both ends) even for slower stuff. I imagine a situation where I'm dealing with digital radio, and only hit a callback when a packet comes in, and that seems nice & simple. I still wonder if I get the main-loop & memory right, if it might be pretty perfomant, this way for audio, too.
That works! It stutters, but maybe that is intended (the 16 buffers and whatnot.)
I think I understand. A few solutions come to mind, immediately:
I will use the awesome info you have provided, and keep hacking on it. Maybe a first working thing would just be to wrap this logic in a function, to at least simplify it, and make it work for more use-cases, then I can keep trying with the custom luaradio block. Thanks again for all your help. I'll let you know if I make any progress. |
Since every block runs in its own a child process, the instance of LoveAudioSink would be a separate process -- and therefore a separate love app -- from the parent process that started the flow graph. They have to communicate over some form of IPC.
This may also be from the ThrottleBlock adjusting the buffering in real time. It would be good to test it with a real audio source (e.g. the WBFM radio example) to see if there is any substantial processing delay from the love game loop and polling.
For a love application embedded in a LuaRadio block, you could certainly do something like this, since they're all in the same process.
This is only possible if the LoveAudioSink itself is the love application, but not if the parent process is, for the reasons described above. The other two solutions - threading with careful sharing of state and running the LuaRadio sample processing from love -- should also be possible. You will probably have to override the If you are looking for something more than just a love audio sink, you might end up back in IPC territory with the flow graph (file descriptors, or shared memory regions, etc.).
No problem. I think once some of the future issues are implemented (#35, #40), this will get much easier. |
I've got a basic example that uses some of these ideas here. I think I am starting to understand your points about IPC & file-descriptors now. I just wrapped
Yeh, that is my goal. I see what you did with GnuPlot, for example, and I think I see what you mean, in terms of hooking into it externally, but my goal is to integrate it, like have the luaradio pipe running inside and pumping data (audio and eventually spectrum) efficiently to it's parent. I am thinking it would be a cool way to make a highly-customized UI for a luaradio pipe, in general, like the way gnuradio has Qt & wxgtk, but better. |
Just checking back in on this. I think it is too advanced for me to work out all the parts of getting this to work, but I'd still really like to use luaradio with love. Is there anything I can help with? I'm pretty good with love and lua, even if my C/SDR is not so strong. |
I could definitely use your help testing out the new framework. I'm about to release v0.10.0 with built-in application support (in devel) -- I just need to add an Applications document to the documentation. Async control is next up and will be the main feature for v0.11.0 (once stable, should probably become v1.0.0). |
@vsergeev Happy to help in any way I can, just let me know. |
I am a total noob to SDR, but I am working on a platform for hacking it on a pizero, in a small form-factor, using a small screen & joystick. I am using love2d for UIs, and it seems to perform fairly well for graphics/joystick/sound. On my quest to learn to write software for hacking radio, I have decided my first task should be a waterfalll view like the main window on gqrx, or at the very least a simple bargraph spectrum analyzer. luaradio seems like a really good place to work on this, as it uses lua (like love2d) and is pretty light and easier to install than gnu-radio. I am imagining I'll be making different modes eventually, where you can select a subset of radio visually, and pipe it through more complex luaradio chains to do other things.
Can you suggest a good way to pipe
SoapySDRSource
data to a love2d waterfall view, efficiently? I couldpopen
on luaradio runtime, or maybe do some hacking wrappinglibluaradio
with lua FFI, or maybe there is some better way, I'm just not sure where to start.The text was updated successfully, but these errors were encountered: