vsergeev / luaradio

A lightweight, embeddable software-defined radio framework built on LuaJIT
https://luaradio.io
Other
600 stars 63 forks source link

Help with using with love2d #64

Open konsumer opened 3 years ago

konsumer commented 3 years ago

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 could popen on luaradio runtime, or maybe do some hacking wrapping libluaradio with lua FFI, or maybe there is some better way, I'm just not sure where to start.

konsumer commented 3 years ago

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.

konsumer commented 3 years ago

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.

konsumer commented 3 years ago

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:

Error: [string "radio.utilities.network_utils"]:0: attempt to redefine 'sockaddr' at line 4
stack traceback:
    [string "boot.lua"]:777: in function <[string "boot.lua"]:773>
    [C]: in function 'cdef'
    [string "radio.utilities.network_utils"]: in function <[string "radio.utilities.network_utils"]:0>
    [C]: in function 'require'
    [string "radio.blocks.sources.networkclient"]: in function <[string "radio.blocks.sources.networkclient"]:0>
    [C]: in function 'require'
    [string "radio.blocks"]: in function <[string "radio.blocks"]:0>
    [C]: in function 'require'
    [string "radio"]: in function <[string "radio"]:0>
    ...
    [C]: in function 'require'
    plugins/radio/plugin.lua:1: in main chunk
    [C]: in function 'require'
    main.lua:38: in main chunk
    [C]: in function 'require'
    [string "boot.lua"]:570: in function <[string "boot.lua"]:380>
    [C]: in function 'xpcall'
    [string "boot.lua"]:787: in function <[string "boot.lua"]:780>
    [C]: in function 'xpcall'

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()
konsumer commented 3 years ago

Does radio.utilities.network_utils maybe need a check for if sockaddr is already FFI-wrapped?

konsumer commented 3 years ago

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?

vsergeev commented 3 years ago

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 socketpair()). This means that when you call top:run(), all of the actual sample processing in the flow graph is actually happening in the background by child processes, and the parent process is just waiting for them to finish to return from top:run(). The parent process that called top:run() isn't directly involved in the data path of flow graph (but is with the second approach described below).

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 pipe() or a socketpair(). Instead calling top:run(), which is blocking (it internally does start() followed by blocking wait()), you can just call start() and do other things in the parent process, like run the love program. You'll need to periodically receive data from the IPC mechanism and handle it in love. It would probably be most efficient to find a way to hook the IPC into love's event loop and have a callback within love when new data has arrived from the flow graph, if this is possible. The example below uses polling in the update() callback of love:

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 update() callback) and drawn to the window (in the draw() callback). You should see an 8-bit number decrementing and incrementing in the window.

Audio data would be no different, just substitute the RawFileSink in your gist for a RealFileSink with the file descriptor for the write end, and add some similar poll() / read() handling as in the example above. You'll have to read chunks of audio samples as they arrive and write them to the audio device.

There are some low-level C library calls in there (pipe(), poll(), read()) in the example above, but they can be pretty easily abstracted away once you have a design in mind.

konsumer commented 3 years ago

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?

vsergeev commented 3 years ago

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.

konsumer commented 3 years ago

Nice! The source:ctrl_set_frequency(135e6) style seems particular useful and cool for my purposes.

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 QueueableSource + SoundData with radio.RealFileSink, but I'm realizing that might not be the best for all the cool (even non-audio) things that loveradio can do. Maybe I could make a custom block for love audio use, then the graph would look more luaradio-ish, without a bunch of ffi calls, but also work easily with love, as sort of a hybrid of your 2 approaches.

Maybe a LoveAudioSink? I imagine something that looks like this. that uses process and cleanup to work with luaradio's pipe better:

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 soundata:getFFIPointer()) would it be possioble to skip the file and just copy into the audio-buffer?

Is there a reason there is no TableSink to just grab the data, in lua? Is it a performance issue? If I can figure it out, some outputs like Gnuplot* but in love, would also be useful.

Also, I think maybe the demo-code is not closing things properly. I get this error, on exit (via Ctrl-C or close button):

[RealFileSink] Block runtime error: [string "radio.blocks.sinks.realfile"]:0: fwrite(): Broken pipe
stack traceback:
        [C]: in function 'error'
        [string "radio.blocks.sinks.realfile"]: in function 'process'
        [string "radio.core.block"]: in function 'run'
        [string "radio.core.composite"]: in function <[string "radio.core.composite"]:0>
        [C]: in function 'xpcall'
        [string "radio.core.composite"]: in function 'start'
        main.lua:28: in function 'load'
        [string "boot.lua"]:586: in function <[string "boot.lua"]:585>
        [C]: in function 'xpcall'
        [string "boot.lua"]:793: in function <[string "boot.lua"]:780>
        [C]: in function 'xpcall'
        [string "boot.lua"]:802: in function <[string "boot.lua"]:769>
AL lib: (EE) alc_cleanup: 1 device not closed
[ThrottleBlock] Downstream block RealFileSink terminated unexpectedly.
[ThrottleBlock] Block runtime error: [string "radio.core.pipe"]:0: write(): Broken pipe
stack traceback:
        [C]: in function 'error'
        [string "radio.core.pipe"]: in function 'write'
        [string "radio.blocks.signal.throttle"]: in function 'run'
        [string "radio.core.composite"]: in function <[string "radio.core.composite"]:0>
        [C]: in function 'xpcall'
        [string "radio.core.composite"]: in function 'start'
        main.lua:28: in function 'load'
        [string "boot.lua"]:586: in function <[string "boot.lua"]:585>
        [C]: in function 'xpcall'
        [string "boot.lua"]:793: in function <[string "boot.lua"]:780>
        [C]: in function 'xpcall'
        [string "boot.lua"]:802: in function <[string "boot.lua"]:769>
AL lib: (EE) alc_cleanup: 1 device not closed
[SignalSource] Downstream block ThrottleBlock terminated unexpectedly.
[SignalSource] Block runtime error: [string "radio.core.pipe"]:0: write(): Broken pipe
stack traceback:
        [C]: in function 'error'
        [string "radio.core.pipe"]: in function 'write'
        [string "radio.core.block"]: in function 'run'
        [string "radio.core.composite"]: in function <[string "radio.core.composite"]:0>
        [C]: in function 'xpcall'
        [string "radio.core.composite"]: in function 'start'
        main.lua:28: in function 'load'
        [string "boot.lua"]:586: in function <[string "boot.lua"]:585>
        [C]: in function 'xpcall'
        [string "boot.lua"]:793: in function <[string "boot.lua"]:780>
        [C]: in function 'xpcall'
        [string "boot.lua"]:802: in function <[string "boot.lua"]:769>
AL lib: (EE) alc_cleanup: 1 device not closed
konsumer commented 3 years ago

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
vsergeev commented 3 years ago

Maybe a LoveAudioSink? I imagine something that looks like this. that uses process and cleanup to work with luaradio's pipe better:

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).

Since I have a pointer to the memory for the audio-stream, in love (via soundata:getFFIPointer()) would it be possioble to skip the file and just copy into the audio-buffer?

Yeah, it should be possible in a LoveAudioSink to copy the input samples directly to this audio buffer.

Is there a reason there is no TableSink to just grab the data, in lua? Is it a performance issue? If I can figure it out, some outputs like Gnuplot* but in love, would also be useful.

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.

Also, I think maybe the demo-code is not closing things properly. I get this error, on exit (via Ctrl-C or close button):

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 cleanup() hook, and also addressing to #35, which I'm planning on implementing for the next v0.8.0 release.

vsergeev commented 3 years ago

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.

Is it possible to fit this into a luaradio block?

I think the first problem you'll encounter here is that both LuaRadio blocks and Love applications have their own implied run() loops. For LuaRadio, it's Block:run(), which is called after the block is forked from the parent and runs forever. For Love, it's love.run(), which is called by the Love framework repeatedly. Basically, you have two frameworks that want ownership of the same main loop. Somehow you need to run both of these -- or the relevant parts of them. It's possible to incorporate the LuaRadio sample processing into Love's game loop (Love's run()), but it would take some investigation and I'm not sure what the easiest way is.

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
konsumer commented 3 years ago

As a hack, I'm closing the read end of the pipe to collapse the flow graph.

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.

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

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.)

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.

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.

Here is an example of playing audio with the second approach:

That works! It stutters, but maybe that is intended (the 16 buffers and whatnot.)

Basically, you have two frameworks that want ownership of the same main loop.

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.

vsergeev commented 3 years ago

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.)

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.

That works! It stutters, but maybe that is intended (the 16 buffers and whatnot.)

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 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.

For a love application embedded in a LuaRadio block, you could certainly do something like this, since they're all in the same process.

I see the example is still using file-descriptors. It seems like it would work better if I pumped the data from inside a block that's in the radio-chain, inside process, into the love SoundData ffi memory-buffer directly (more like other sound blocks work), but as I said I am having issues getting it right. If the only way to get audio data out of luaradio is a file-descriptor, then I think I could still use one inside a LoveAudio block to pump data that way, but at least be self-contained and work roughly like other audio blocks, but it seems like Block.process should be able to pump the data directly, unless i am misunderstanding.

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 Block:run() to prevent LuaRadio from running the default main loop.

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.).

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.

No problem. I think once some of the future issues are implemented (#35, #40), this will get much easier.

konsumer commented 3 years ago

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 RealFileSink. It runs smoothly (you were right about ThrottleBlock) but hangs on exit. Is there something else I need to do to better encapsulate it? It's just a quick hack to ghet somethign that kind of looks like a luaraio block, but works in love app.

For a love application embedded in a LuaRadio block, you could certainly do something like this, since they're all in the same process.

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.

konsumer commented 3 years ago

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.

vsergeev commented 3 years ago

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).

konsumer commented 3 years ago

@vsergeev Happy to help in any way I can, just let me know.