LI7XI / AudioAnalyzerDocs

Documentations for AudioAnalyzer Plugin.
0 stars 0 forks source link

DeviceList parsing #3

Open d-uzlov opened 3 years ago

d-uzlov commented 3 years ago

DeviceList section variable returns a machine-readable string, that requires a specialized parsing routine.

The proposed routine in the LUA script is the following:

```lua local function unescape(str) return str:gsub('%%percent', '%'):gsub('%%semicolon', ';'):gsub('%%forwardslash', '/') end local function parseDevice(packedDescription) local array = {} for s in packedDescription:gmatch("([^;]+);") do s = unescape(s) table.insert(array, s) end -- channels can potentially be empty -- it's unlikely this will ever happen in real world -- but technically it's possible -- let's ignore such devices if array[6] == '' then return nil end local result = {} result.id = array[1] result.name = array[2] result.descString = array[3] result.formFactor = array[4] result.sampleRate = array[5] result.channels = {} for s in array[6]:gmatch("([^,]+),") do table.insert(result.channels, s) end result.type = array[7] return result end local function createDeviceArray(packedDescriptionList) local deviceArray = {} for packedDescription in packedDescriptionList:gmatch("([^/]+)/") do local desc = parseDevice(packedDescription) if desc ~= nil then table.insert(deviceArray, desc) end end return deviceArray end ```

Function createDeviceArray returns an array of objects, each of which contains info about one device. This info can be used later for showing user some device-specific info, changing Parent measure source, etc.

d-uzlov commented 3 years ago

Here is an example of usage of the routine above:

```lua local function createSetVarBang(name, value, file) local result = '' result = result .. '[!WriteKeyValue Variables ' .. name .. ' """' .. value .. '""" "' .. file .. '"]' result = result .. '[!SetVariable ' .. name .. ' """' .. value .. '"""]' return result end local function setCtxMenuFor(deviceArray, reloadBang) local action local varsFile = '#@#Variables/Common.inc' local varDevName = 'AudioDeviceName' local varDevId = 'AudioDeviceID' SKIN:Bang("!SetOption", "Rainmeter", "ContextTitle", 'Default Input') action = '' action = action .. createSetVarBang(varDevName, 'Default Input', varsFile) action = action .. createSetVarBang(varDevId, 'DefaultInput', varsFile) action = action .. reloadBang SKIN:Bang("!SetOption", "Rainmeter", "ContextAction", action) SKIN:Bang("!SetOption", "Rainmeter", "ContextTitle2", 'Default Output') action = '' action = action .. createSetVarBang(varDevName, 'Default Output', varsFile) action = action .. createSetVarBang(varDevId, 'DefaultOutput', varsFile) action = action .. reloadBang SKIN:Bang("!SetOption", "Rainmeter", "ContextAction2", action) for i, desc in pairs(deviceArray) do local ctxName = desc.descString action = '' action = action .. createSetVarBang(varDevName, ctxName, varsFile) action = action .. createSetVarBang(varDevId, 'id:' .. desc.id, varsFile) action = action .. reloadBang SKIN:Bang("!SetOption", "Rainmeter", "ContextTitle" .. (i + 2), ctxName) SKIN:Bang("!SetOption", "Rainmeter", "ContextAction" .. (i + 2), action) end end local gDeviceArray = {} function updateContextMenu(getValueVariable, reloadBang) local packedDescriptionList = SKIN:ReplaceVariables(getValueVariable) gDeviceArray = createDeviceArray(packedDescriptionList) setCtxMenuFor(gDeviceArray, reloadBang) end ```

This script will set skin context menu to all of the audio devices in the system, so user can choose from them. To use this script, you will have to add the following callback to Parent measure: callback-onDeviceListChange=[!commandMeasure DevListScript "updateContextMenu('[&*ParentMeasure:resolve(DeviceList)*]', '[!setOption ParentMeasure non-existent 1]')"]

LI7XI commented 3 years ago

Um.. i'm a bit confused, there are 3 scripts now:

Idk which one to use actually, or how to use it. Should i just copy paste the script and it will work just fine using the same skin? Or i need to modify the skin?

d-uzlov commented 3 years ago

Script in the first comment here parses the string that contains info about audio devices. It's a reference parsing script, it can be used everywhere where you want to parse the device list string.

Script in the second comment here calls the first script and uses the result to add context menu items to the skin.

Original script in the neighbour issue is literally these two scripts concatenated, without a separation to "parse" and "use the result".

The second part of the script uses the same assumptions as your original script: it sets variables for device change (with the same names of variables), it writes variables into #@#Variables/Common.inc file. Though, it doesn't refresh the skin, because it's not necessary. It's not the best variant of such script, there is a lot that can be improved, but it will work. Maybe when (if) I have some free time and a desire to make it better, I will rewrite it to have a more decent example in the docs, but for now I'm fine with what we have.

Generally users will want to just copy the first script (with parsing) and write their own second part, that uses parsed data in some way.

d-uzlov commented 3 years ago

For now I have several concerns with the device list string.

  1. Special symbol % which I used to escape other special symbols happened to be a special symbol for LUA, which is inconvenient.
  2. I see that there are other special symbols that aren't covered by the 2.0 version of the plugin. Namely, ' and ". They may be not special for the plugin but they make it much harder to pass the device list string around in the Rainmeter. Potentially there can be more special symbols that are not desirable in the string that I don't know about. There should probably be a mechanism to specify custom special symbols.

By the way, the reason special symbols are an issue at all is that all audio devices can have custom user-defined names, and Windows doesn't have anything against names like %\\/;!@#$%^&*(). That's where most special symbols come from.

LI7XI commented 3 years ago

Though, it doesn't refresh the skin, because it's not necessary.

Changing the device id in Source will automatically take an effect?

By the way, the reason special symbols are an issue at all is that all audio devices can have custom user-defined names, and Windows doesn't have anything against names like %\/;!@#$%^&*(). That's where most special symbols come from.

In changes between v1 and v2 issue you mentioned:

Symbols / ; % inside values are escaped by %forwardslash, %semicolon and %percent respectively and must be unescaped on parsing

I still don't exactly get it, but if you mean that a user can name there audio device something like "my%spe@kers" then why not make the plugin escape these with the corresponding ascii numbers?

For example, something like $64$ for @ and so on.

LI7XI commented 3 years ago

The proposed routine in the LUA script is the following:

Hmm.. it's not scary as it looked in first time. Even though i don't exactly get the regex part, but i will try to explain what you did using comments inside the code.

Here is an example of usage of the routine above:

um....

Is all this necessary? i mean it looks a bit overengineered 😹

callback-onDeviceListChange=[!commandMeasure DevListScript "updateContextMenu('[&ParentMeasure:resolve(DeviceList)]', '[!setOption ParentMeasure non-existent 1]')"]

Also why this if the context menu will update anyway once the users use the settings skin? I mean if they connected an audio device they may come and change the settings themselves.

I'm afraid of when they disconnect a selected audio device and it becomes unavailable, the plugin will just use the default one until it reconnects right?

d-uzlov commented 3 years ago

Changing the device id in Source will automatically take an effect?

To make plugin see changes, Reload must be called. There are two ways to make rainmeter call reload: turn on dynamic variables or call setoption on the measure to be reloaded. Script above calls setoption with an option that doesn't exist, just to cause reload.

why not make the plugin escape these with the corresponding ascii numbers?

What would be the difference from the current implementation except that parsing will be much-much harder as you would need to remember ascii codes? If you mean "all symbols except ascii characters will be escaped" then it won't work because there are about 2^16 allowed symbols in windows. Well, I can escape then, it's just that you wouldn't be able to parse them.

d-uzlov commented 3 years ago

Also, one another concern for parsing. Non-ascii symbols are of course allowed in device names, but LUA can't really work with them. I didn't test how it behaves now yet.

Maybe a better solution would be to provide names via some specialized section variable, so than lua can be bypassed when working with names. Like, allow getting name by device id, so lua would only get device id and channels, and then set option in rainmeter to [parent:resolve(devName, id)].

LI7XI commented 3 years ago

Script above calls setoption with an option that doesn't exist, just to cause reload.

You did something like [!SetOption ParentMeasure Source "#AudioDeviceID#"]?

Well, I can escape then, it's just that you wouldn't be able to parse them.

Actually i don't understand why would someone change the audio device name in first place, even if they did, the chance of using some of these symbols is pretty low. Unless they used to write assembly code.

The current method is good btw 👍

LI7XI commented 3 years ago

Maybe a better solution would be to provide names via some specialized section variable, so than lua can be bypassed when working with names. Like, allow getting name by device id, so lua would only get device id and channels, and then set option in rainmeter to [parent:resolve(devName, id)].

Oh, that's great. You can call that argument DeviceName.

But will you remove the name from DeviceList? ({0.0.0.00000000}.{c73b9bf1-9f30-4a46-9786-c5d3d2c18aba};Speakers;Speakers;48000;fl,fr,;output;/)

d-uzlov commented 3 years ago

You did something like [!SetOption ParentMeasure Source "#AudioDeviceID#"]?

No, I didn't bother with this. There is '[!setOption ParentMeasure non-existent 1] for that purpose. Also, if you want to set option to variable, not value of a variable, you should escape the variable. See rainmeter docs on variables for details on how to do this.

Actually i don't understand why would someone change the audio device name in first place, even if they did, the chance of using some of these symbols is pretty low. Unless they use to write assembly code.

Well, I have all of my devices renamed because default names aren't helpful when you have more than 1-2 devices. And if something is a valid name in windows, the plugin should also correctly handle it.

But will you remove the name from DeviceList?

I will need to test how rainmeter and lua react to non-ascii symbols, and then decide what to do. But most probably yes.

LI7XI commented 3 years ago

Also, if you want to set option to variable, not value of a variable, you should escape the variable. See rainmeter docs on variables for details on how to do this.

I know, but i actually want it to be the value of a variable. I will setup the skin then discuss that again.

I will need to test how rainmeter and lua react to non-ascii symbols, and then decide what to do.

I don't think lua would be involved, Since we will just use a bang to set the ctx name to what Parent:Resolve(DevName, ID) gives.

d-uzlov commented 3 years ago

I don't think lua would be involved, Since we will just use a bang to set the ctx name to what Parent:Resolve(DevName, ID) gives.

I mean, if device list string will contain the name, lua will be involved. If it causes issues, then it will be necessary to remove it and only use section variables, otherwise maybe not.

LI7XI commented 3 years ago

Hello, i made the old script use / as a separator for audio devices, and it worked just fine.

function Initialize()
  ContextIndex = 1

  for i in string.gmatch(SKIN:ReplaceVariables('[&GetAudioDevices:Resolve(DeviceList)]'), "[^/]+") do
    ContextIndex = ContextIndex + 1
    AudioDeviceInfos = {}

    for j in string.gmatch(i, "[^;]+") do
      table.insert(AudioDeviceInfos, j) 
    end

    AudioDeviceID = AudioDeviceInfos[1]
    AudioDeviceName = AudioDeviceInfos[2]

    SKIN:Bang("!SetOption", "Rainmeter", "ContextTitle" .. ContextIndex, AudioDeviceName)
    SKIN:Bang("!SetOption", "Rainmeter", "ContextAction" .. ContextIndex, '[!WriteKeyValue Variables AudioDeviceName "' .. AudioDeviceName .. '" "#@#Variables/Common.inc"][!WriteKeyValue Variables AudioDeviceID "ID: ' .. AudioDeviceID .. '" "#@#Variables/Common.inc"][!RefreshGroup [#GroupName]]')

  end
end

But i wonder, is there a problem with this implementation? i mean i'm not unescaping anything so i'm afraid there might be an error somewhere.

d-uzlov commented 3 years ago

is there a problem with this implementation?

There are few. If you fix them, you get something like the script in the first two posts in this thread.

Do you not like them?

d-uzlov commented 3 years ago

Either way, there needs to be testing of how all these scripts work with non-ASCII symbols, and have not done it yet.

LI7XI commented 3 years ago

There are few.

Can you tell me where are they?

Do you not like them?

Nope, i just wanna do it myself so i can explain it better.

Either way, there needs to be testing of how all these scripts work with non-ASCII symbols, and have not done it yet.

I'll try to test it out.

d-uzlov commented 3 years ago

About parsing:

About the part that adds custom menu items:

d-uzlov commented 3 years ago
AudioDeviceName = AudioDeviceInfos[2]

Field name mimics the naming of WASAPI. However, in reality this is not a name, it's more of a device family, of something. "High Definition Audio Device", for example. For real user-readable name you should use the field that has "Device description" name in the docs. I'm not the one who came up with this nonsense, I'm just following the naming from the Microsoft, so that users can actually google it in case of some troubles.

LI7XI commented 3 years ago

I will apply the things you suggested, but lemme tell you how i'm planning to do it:

Since all skins have group name, we can use !SetVariableGroup to change the id var without skin refresh or !SetOptionGroup if for some reason the plugin can't read the new value of the variable.

The point of settings skin is to let users select the audio device they want, when they select one, the method above should be applied.

That seems enough for the purpose of the settings skin. What i don't get is, what's the need for using Callbacks? I mean, as i understand, when an audio device disconnects, the plugin will switch to "default" audio device, and when it reconnects, the plugin will just reconnect, without skin refresh needed.

Also if users plugged or unplugged an audio device intentionally, they will go to the settings skin to change it from there, for example if they connected a new audio device, or of they removed one. This is what i think.

The reason i used initialize is because the the menu needs to open only once, when a user clicks on it. I avoid using custom functions because i really think it's not needed, and it will complicate things more for no real benefit, maybe i'm wrong, i'm just talking with my limited experience.

The whole point of settings skin and the tutorial is showing skins devs how to let users select one of the available audio devices.

Anyone that needs something custom would make it in there own way, since they already know how to use lua, and section vars usage is crystal clear. The tutorial is there just for guidance, although yes it should be written well anyway.

I'm not the one who came up with this nonsense, I'm just following the naming from the Microsoft, so that users can actually google it in case of some troubles.

Don't worry i understand, but since you are planning to extract the dev name to it's own section variable, why not let rainmeter handle the non ascii characters with the method i suggested earlier:

Since we will just use a bang to set the ctx name to what Parent:Resolve(DevName, ID) gives.

I see this as the most straight forward solution, and it doesn't involve that much lua.

LI7XI commented 3 years ago

About local variables, i always use them, this time i just forgot to do so :P

Btw, is it possible to add dev list count section variable? it would be useful in some cases, for example creating a function to that has 3 args, name, bang, ctx index, and for each device we will pass the parameters and make a new option for it.

P.S nvm, i just noticed the for i, desc, i guess i can be used for indexing.

LI7XI commented 3 years ago
if array[6] == '<unknown>' then
       return nil
end

you mean no audio device will be outputted if the channel is unknown?

d-uzlov commented 3 years ago

That seems enough for the purpose of the settings skin. What i don't get is, what's the need for using Callbacks? I mean, as i understand, when an audio device disconnects, the plugin will switch to "default" audio device, and when it reconnects, the plugin will just reconnect, without skin refresh needed.

  1. When you requested some certain audio device and this device was disconnected, plugin will stop working until you reconnect this device. Automatic switching only works when you explicitly request default device — whichever this default may be and however often it changes.
  2. Let's say you have two devices. You disconnect the second one, and it is no longer available. But you didn't update the list of devices in the rainmeter, so when user opens context menu, they see this disconnected device and can click on it, resulting in plugin stopping working because this device is no longer available. Whoops. Or if you connect some new device: without update using callback user will not be able to select it unless the skin is manually refreshed to reread device list.

The whole point of settings skin and the tutorial is showing skins devs how to let users select one of the available audio devices.

Yeah, how to do it right. The right way is to use callbacks to always provide relevant data.

Anyone that needs something custom would make it in there own way, since they already know how to use lua, and section vars usage is crystal clear. The tutorial is there just for guidance, although yes it should be written well anyway.

Oh, believe me, not everyone who has decided to create a skin has all the required skills to make it good. Also, even people who have the skills is likely to not spend time thinking about it if even the official docs say that it's alright to write it the wrong way. This is the reason we have this absurd 260 max symbol length in full file path in Windows: it's not like it's intended to be this way, it's just that old docs had this, and everyone did this, and if you change it many programs will break. If only we had better docs from the beginning...

I'm not the one who came up with this nonsense, I'm just following the naming from the Microsoft, so that users can actually google it in case of some troubles.

Don't worry i understand, but since you are planning to extract the dev name to it's own section variable, why not let rainmeter handle the non ascii characters with the method i suggested earlier:

Since we will just use a bang to set the ctx name to what Parent:Resolve(DevName, ID) gives.

I see this as the most straight forward solution, and it doesn't involve that much lua.

Well, I think that LUA-only solutions are easier to use than some convoluted interactions between native plugins and LUA. I would like to avoid this if LUA doesn't have much troubles with non-ascii symbols.

you mean no audio device will be outputted if the channel is unknown?

Well, previously the plugin parsed all of the existing audio devices and passed them in resolve. Some of the values could potentially not be parsed correctly, so some fields could be <unknown>. 2.0.3.3 actually discards devices that can't be read. However, channels are special, because on all the other fields errors happen because WASAPI call to get info failed, and channels parsing can fail for another reason: a device can potentially have only some exotic channels that plugin doesn't know about. It would require additional research on what the plugin will do when it encounters such device. I believe there is a fallback to first available channel, so it can still make sense to use this device, but I'm not sure and this is not tested. So in the script I discard such devices.

LI7XI commented 3 years ago

Hello, i was a bit busy installing wsl, before that i started working on the script, and after idk how many hours of fighting with rainmeter (skin object doesn't work if called outside of initialize() or update(), and the script suddenly stopped working so i had to restart rm multiple times, local functions cannot be used in inline lua, also talking regex wasn't easy), i got the script to work.

I'll add comments later, but i want your approval first, also i had to use init because ondevicelistchange for some reason can't execute inline lua or !commandMeasure. I guess it's a problem on my side but idk how to fix it, anyway here is the script:

```lua local function unescape(str) return str:gsub('%%percent', '%'):gsub('%%semicolon', ';'):gsub('%%forwardslash', '/') end local function ParseDeviceProperties(DeviceProperties) local TempArray = {} for property in DeviceProperties:gmatch("([^;]+);") do property = unescape(property) table.insert(TempArray, property) end if TempArray[6] == '' then return nil end local DeviceInfos = {} DeviceInfos.id = TempArray[1] DeviceInfos.name = TempArray[2] DeviceInfos.description = TempArray[3] DeviceInfos.formFactor = TempArray[4] DeviceInfos.sampleRate = TempArray[5] DeviceInfos.channels = {} for channel in TempArray[6]:gmatch("([^,]+),") do table.insert(DeviceInfos.channels, channel) end DeviceInfos.type = TempArray[7] return DeviceInfos end local function CreateBang(GroupName, VarName, VarValue, VarFile) local Bang = '' Bang = Bang .. '[!SetVariableGroup "' .. VarName .. '" "' .. VarValue .. '" ' .. GroupName .. ']' Bang = Bang .. '[!WriteKeyValue Variables "' .. VarName .. '" "' .. VarValue .. '" "' .. VarFile .. '"]' return Bang end local function CreateDevicesMenu(DevicesList) local GroupName = SKIN:GetVariable('GroupName') local VarDevName = 'AudioDeviceName' local VarDevId = 'AudioDeviceID' local VarFile = '#@#Variables/Common.inc' local Bang -- "Default Input" Option Bang = '' Bang = Bang CreateBang(GroupName, VarDevName, 'Default Input', VarFile) Bang = Bang .. CreateBang(GroupName, VarDevId , 'DefaultInput' , VarFile) SKIN:Bang("!SetOption", "Rainmeter", "ContextTitle", 'Default Input') SKIN:Bang("!SetOption", "Rainmeter", "ContextAction", Bang) -- "Default Output" Option Bang = '' Bang = Bang CreateBang(GroupName, VarDevName, 'Default Output', VarFile) Bang = Bang .. CreateBang(GroupName, VarDevId , 'DefaultOutput' , VarFile) SKIN:Bang("!SetOption", "Rainmeter", "ContextTitle2", 'Default Output') SKIN:Bang("!SetOption", "Rainmeter", "ContextAction2", Bang) -- Audio Devices for i, DeviceProperties in pairs(CreateDevicesList(DevicesList)) do local DeviceName = DeviceProperties.description local DeviceId = DeviceProperties.id Bang = '' Bang = Bang .. CreateBang(GroupName, VarDevName, DeviceName, VarFile, VarDevName) Bang = Bang .. CreateBang(GroupName, VarDevId, 'ID: ' .. DeviceId, VarFile, VarDevId) SKIN:Bang("!SetOption", "Rainmeter", "ContextTitle" .. (i + 2), DeviceName) SKIN:Bang("!SetOption", "Rainmeter", "ContextAction" .. (i + 2), Bang) end end function CreateDevicesList(AudioDevices) local DevicesList = {} for AudioDevice in AudioDevices:gmatch("([^/]+)/") do local DeviceProperties = ParseDeviceProperties(AudioDevice) if DeviceProperties ~= nil then table.insert(DevicesList, DeviceProperties) end end return DevicesList end function Initialize() CreateDevicesMenu(SKIN:ReplaceVariables('[&GetAudioDevices:Resolve(DeviceList)]')) end ```

It's a copy paste from yours but in a way that makes it easy for me to explain.


When you requested some certain audio device and this device was disconnected, plugin will stop working until you reconnect this device. Automatic switching only works when you explicitly request default device — whichever this default may be and however often it changes.

Oh, i got it now.

Or if you connect some new device: without update using callback user will not be able to select it unless the skin is manually refreshed to reread device list. Yeah, how to do it right. The right way is to use callbacks to always provide relevant data.

But the custom menu is a skin itself, so whenever it's initialized the plugin will be initialized and it will give the current list of audio device, i mean it does reread the audio devices list on init right?


Oh, believe me, not everyone who has decided to create a skin has all the required skills to make it good. Also, even people who have the skills is likely to not spend time thinking about it if even the official docs say that it's alright to write it the wrong way. If only we had better docs from the beginning...

I got your point 👍

I would like to avoid this if LUA doesn't have much troubles with non-ascii symbols.

Except the \ (and other symbols when using regex methods), i don't think lua will have a problem, afaik non-ascii are mostly unicode, and unicode itself is represented in ascii (&u21FF). I mean lua should see it just as a string.

It would require additional research on what the plugin will do when it encounters such device. I believe there is a fallback to first available channel, so it can still make sense to use this device, but I'm not sure and this is not tested. So in the script I discard such devices.

Hmm, so if the device channels can't be parsed correctly, the device should be ignored (not displayed) in the select menu? I guess this may lead to a confusion, users may think they didn't connect it correctly or something.

Also if we made it selectable and added a log warning about unsupported device channels, i don't think many users (not skin devs) will look at the log windows. What about adding a message beside the device name? for example "MyAudioDevice (Device channels are not supported)", and additionally we can leave it without a bang [] or remove ContextAction entirely.

The name may look a bit long but it's a decent solution.

LI7XI commented 3 years ago

Field name mimics the naming of WASAPI. However, in reality this is not a name, it's more of a device family, of something. "High Definition Audio Device", for example. For real user-readable name you should use the field that has "Device description" name in the docs.

I have a suggestion, why don't you fix this from the plugin itself? i mean swapping Name with Description, and probably renaming description to something else, DeviceFamily for example.

d-uzlov commented 3 years ago

I'm very sorry I didn't answer earlier. I saw notifications about your messages, but I didn't really have energy to dive into the plugin. I was hoping I would have free time on weekend few days ago but turned out my family already had plans for my time.

Fell free to make a post starting with something like "URGENT!" or "IMPORTANT!" so I see it in the notification and know I can't procrastinate on answering, if there is anything urgent or important.

I'll add comments later, but i want your approval first, also i had to use init because ondevicelistchange for some reason can't execute inline lua or !commandMeasure. I guess it's a problem on my side but idk how to fix it, anyway here is the script:

Look good.

But the custom menu is a skin itself, so whenever it's initialized the plugin will be initialized and it will give the current list of audio device, i mean it does reread the audio devices list on init right?

What if user opened this skin and then connected, or disconnected, or renamed some device?

afaik non-ascii are mostly unicode, and unicode itself is represented in ascii (&u21FF). I mean lua should see it just as a string

Is it really? I made a quick check with string h-phones ×áá привет (where first á is a separate letter from before unicode and second are two codepoints: a and ´) and I got this: 2021-05-04 01-01-22 Скриншот экрана where onDeviceChange line is from callback-onDeviceChange=[!log """onDeviceChange [&Measure001_Parent:resolve(current device, description)]"""] and descString line is from print('descString', result.descString), inserted into parsing method in lua. Same results as in descString can be seen in the Rainmeter custom menu in skin. Symbols like ≈⌘§⌀∞*ßθ also don't work. Considering my system is set to use cyrillic language for non-unicode apps, and of all non-ASCII symbols only cyrillic are shown correctly, I suspect that Rainmeter transforms full unicode string into ASCII+whatever your non-unicode lang is before sending the string to LUA, so LUA doesn't event get a chance to work with unicode. There is probably some note for this in rainmeter LUA docs, but last time I've read them was too long ago, so I don't remember.

Hmm, so if the device channels can't be parsed correctly, the device should be ignored (not displayed) in the select menu? I guess this may lead to a confusion, users may think they didn't connect it correctly or something.

Well, you see, here the thing: the plugin supports front, side, back, center and LFE channels. That's a lot, but even in existing Windows audio API there are other (mostly very strange) channels, like "SideOrBackLeft", or something, and it's possible that in some update new channels will be added. If some device doesn't have any of the channels the plugin supports, then user can't really get much audio information from this device, However, it's still possible to use this device: auto channel will use whatever channel is first in the channel list, even if this channel is not supported.

How probable is it that there will be a device that is generally valid but doesn't have any of the supported channels? I would say that this should never happen. That's why I simply skipped such devices in my script: to really make sure that channels array in the script is always valid. However, in case such devices do exist, the plugin allows user to interact with them. So, yeah, it's perfectly valid to write a script that just marks such devices as something like Device channels are not supported, though you can't get anything besides auto channel from them. It depends on how you want to treat unrealistic situations.

I have a suggestion, why don't you fix this from the plugin itself? i mean swapping Name with Description, and probably renaming description to something else, DeviceFamily for example.

Yeah, that's probably a better solution. I'll change it in next release.

LI7XI commented 3 years ago

I'm very sorry I didn't answer earlier. I saw notifications about your messages, but I didn't really have energy to dive into the plugin. I was hoping I would have free time on weekend few days ago but turned out my family already had plans for my time.

Please for god sake, don't apologize, it's alright. Since last month (and for the next 2 weeks), i've been kinda busy with so many things, so i wanted to take some time then come back to this with a fresh mind.

Also, even though you told me we are not on a rush, i still feel sorry for taking this long, i didn't know that writing good docs is time consuming 😅 (or maybe it's procrastination lmao)


What if user opened this skin and then connected, or disconnected, or renamed some device?

It's a rare case but we can do Callback-OnDeviceListChange=[!Hide], they will reopen it themselves if they need to.

Considering my system is set to use ... a chance to work with unicode.

Hmm, maybe i misunderstood how Unicode is interpreted.

So, yeah, it's perfectly valid to write a script that just marks such devices as something like Device channels are not supported, though you can't get anything besides auto channel from them. It depends on how you want to treat unrealistic situations.

Well, it's extremely rare that this might happen, but if the user still selects that device, will the plugin automatically use auto channel? idk how this will work if some specific channels are specified in channels parameter in unit-name option or channels option in child measures.


Btw, i have a totally unrelated question, but i didn't know where to ask so pardon me ><

I use stb_image to write images on disk so rainmeter can read and display them, the problem is, it takes 40ms on avg per write, and i want to do this 60fps. Both bmp and jpg don't work (the image meter appears and disappears for few ms randomly due to slow writing i guess).

I wanna know how you managed to get over this? also have you built your bmp_writer from scratch or used external library? (note, the images i'm writing are 1280x720, they are video frames actually so the pixels contain many colors)

d-uzlov commented 3 years ago

What if user opened this skin and then connected, or disconnected, or renamed some device?

It's a rare case but we can do Callback-OnDeviceListChange=[!Hide], they will reopen it themselves if they need to.

As a user I wouldn't like if a configuration skin would hide automatically when something changes.

Maybe I'm out of context of your skin, but is running function to update values using Callback-OnDeviceListChange difficult? Like, a skin already uses device list, then it already has that update function, so it should be just a matter of writing [!CommandMeasure MyScript UpdateLabels()] or something.

Also, I don't agree that this is a rare case. I can easily imagine a situation where I wanted to switch the plugin from speakers to headphones, opened the config menu and suddenly remembered that my headphones are disconnected, so I plug them in and expect that they will simply appear in the list, like in any other audio application. Well, that's considering I specify an audio device specifically for Rainmeter, which is most likely a rare case. But among those who would do it, changing the list after the config skin was opened shouldn't be that rare.

Well, it's extremely rare that this might happen, but if the user still selects that device, will the plugin automatically use auto channel? idk how this will work if some specific channels are specified in channels parameter in unit-name option or channels option in child measures.

"Extremely rare" is a light way to put it. As I said, I believe that this should never-ever happen in real life. This special case is only present in the plugin docs because it's technically possible.

If a channel is not supported by the audio device, then nothing will happen. Like, if you specified Left, but your device is mono and doesn't have left channel, then you will only ever get 0.0 as a value. And if you specified Left, but your device is something exotic, and doesn't have any supported channels, then nothing changes compared to your mono device.

You know, after thinking about it, I don't think we need any special case for this. It's like, if you want auto channel, you are not interested in any certain channels, and you can use anything. If you want some certain channels, then you either don't check anything and hope for the best, or you check it, and device without channels simply won't pass this test. So I think I will just write an empty channel string if there are no supported channels, and it will be OK. If the string will be empty, then channel array will be empty, and everyone will be happy without any special cases! Yay.

I have a suggestion, why don't you fix this from the plugin itself? i mean swapping Name with Description, and probably renaming description to something else, DeviceFamily for example.

Yeah, that's probably a better solution. I'll change it in next release.

By the way, I believe that most of the name shenanigans are in the docs, so there is not much to be changed in the plugin. The only place where there are naming issues in the plugin itself is current device section variable.

d-uzlov commented 3 years ago

I use stb_image to write images on disk so rainmeter can read and display them, the problem is, it takes 40ms on avg per write, and i want to do this 60fps

It depends on where you have that latency. Is it on image generations (=video decoding)? Is it on image encoding (=creating file from pixel array)? Is it on actual drive write? Or it can be decoding. I remember I messed up BMP pixel order one time, and Rainmeter became super slow, likely because it couldn't use the the standard optimized BMP reading functions and had to use something slow to read my images. Maybe decoding of the format that you use is also slow.

In my plugin I actually did have quite a lot of troubles with images. Well, I already said few months ago that one part of why Rainmeter devs don't want to add image support for plugins is probably from the fact that making images is tricky. I discovered that generating the pixel array was quite slow in the version, that is still the last announced on Rainmeter forum. Though it didn't matter that much when using background thread, it still was quite nice to have CPU load drop when I reworked the image generation. Making image file out of pixel array is essentially free in my case, because I write BMP files, which are essentially a pixel array written on disk. I don't use any library because of this: the only thing needed to write BMP is a small simple header. I pay the price of having to write more bytes on disk for this. Not a big price for a RAM drive, because when writing files on disk the most latency comes from the write operation, not from how much I write.

the image meter appears and disappears for few ms randomly due to slow writing i guess

I think this is not due to slow writing, but due to you writing files in background thread. Rainmeter doesn't read files that are open for write by someone else, so sometimes it can't open the file, hence image meter disappears. In my plugin I generate files in background thread but then transfer it into my main plugin object and write it during Update call from Rainmeter. This has a downside of making Rainmeter slower.

Though, I think it would be more optimal to write into 2 files alternately: you would provide Rainmeter path to one file, and write into another, and then swap them. That way Rainmeter will never encounter an issue that it can't read the file, and you won't slow down main Rainmeter thread. Maybe I'll try to use this approach in the plugin, um, soon-ish.

  That said, I personally use image like 500×500 pixels on my desktop, for audio visualization, and I consider this very big, too slow for HDD drives (as, like, any writes 60 times per second to HDD, kinda regardless of size), and death-bringing to SSD drives. If you try to write images of size 1280×720 60 times per second, please reconsider. It's kinda insane. Maybe it would be better to at least try to make a pull request to Rainmeter with changes to support something to transfer images, so you at least don't have to compress it (and then Rainmeter don't have to decompress it) and flush to a drive. Maybe they will accept it, though I don't have high hopes.

LI7XI commented 3 years ago

but isn't running function to update values using Callback-OnDeviceListChange difficult?

Yes but they won't actually appear in the list until it's refreshed.

So I think I will just write an empty channel string if there are no supported channels, and it will be OK. If the string will be empty, then channel array will be empty, and everyone will be happy without any special cases! Yay.

so good! but last thing, should we let that device be selectable from the menu (with a warning message)?

By the way, I believe that most of the name shenanigans are in the docs, so there is not much to be changed in the plugin. The only place where there are naming issues in the plugin itself is current device section variable.

Okay but if i swapped them in the docs, will it still "technically" make sense? or that will result in unexpected behavior at some point?

LI7XI commented 3 years ago
>It depends on where you have that latency. Is it on image generations (=video decoding)? Is it on image encoding (=creating file from pixel array)? Is it on actual drive write? Video decoding is not hardware accelerated (yet), so it depends on the video, for 720p 60fps video it takes 8-17ms to decode a frame, but the main latency is the actual writing, that alone takes 40ms on avg per frame. Lemme give you the full context, i made a simple video player using sfml to test if i'm decoding videos correctly, and it worked, so i wanted to test if i can display these frames in rainmeter, so the only way was to write them to disk etc.., i didn't make any plugin yet because the video decoder wasn't working properly (i really hate seg faults lol) and missing a lot of features, so i wanted to finish that first. I have 2 problems, first i can't develop using visual studio, it takes forever to load and it eats all ram once it's lunched, so i use vscode with wsl. I can use the command line in msvc but it's not fun, and i get a lot of errors. P.S few days ago i tried building your plugins, i wanted localPluginLoader for easier development, but i got errors about some headers not found while they clearly exist (fatal error C1083: Cannot open include file:), i didn't know if it's a problem on my part or something is missing. Can we discuss this here or i should make a new issue? Second, how to actually make a plugin lol? i mean i know about static and dynamic linking stuff, but i didn't actually try to make a lib/dll, so i lack a bit of knowledge in this area. Also, i'm developing on linux (technically), but the target platform is windows, i googled and found out about cross compiling and stuff but there is something i don't understand, rainmeter is compiled using msvc, but i want to make the dll using mingw, will the plugin work? i mean you know, for example some compilers treat int like 16bit while others treat it like 32bit, i'm worried that it will be a problem for me. >Not a big price for a RAM drive, because when writing files on disk the most latency comes from the write operation, not from how much I write. Writing to disk using stb_bmp have same effect right? i mean do you override the file or you edit it in place? >not from how much I write. umm.. but the more you write the longer the latency... >Though, I think it would be more optimal to write into 2 files alternately: you would provide Rainmeter path to one file, and write into another, and then swap them. Hmm, i thought about that but can renaming be done quickly? if so then it's a great idea, i'll try it. That way i can do the renaming after the writing is done, yes the frame will be displayed longer than it should be but at least it won't flicker. All this is just for testing tho, using that in an actual plugin is a hardware suicide lol. > If you try to write images of size 1280×720 60 times per second, please reconsider. It's kinda insane. Yeah i realized this, thanks for pointing out😹 --- >Maybe it would be better to at least try to make a pull request to Rainmeter with changes to support something to transfer images, so you at least don't have to compress it (and then Rainmeter don't have to decompress it) and flush to a drive. Maybe they will accept it, though I don't have high hopes. There is a lot to say about this so bare with me, before going anywhere near rainmeter devs, i want to make a custom build, long time ago i said it's not functional, but now the point is getting attention, as i understand they said there isn't enough users that will benefit from this feature, but if my custom build worked well then they may accept the idea, i'm hoping for the best. Now i have few other problems, first is knowledge, howww to make rainmeter support in-memory image-transfer? i don't understand what is evolved in this process, winapi? direct2d? or the way rainmeter api is written? Also, after modifying rainmeter, what about the plugins api (that .lib thing you have to compile with your plugins)? does it require modification as well? Lastly, after i take more time to learn how to get this to work, i need a design, i mean how users are going to interact with this api? and for example, a measure that returns image data, what should the message look like in log window? 1 if image is provided, 0 otherwise? "Image"? Btw, in rainmeter forums i remember you said: >>Allow image path to be URI. >>memory://[config.name.]// >>And then the list of plugin functions can be updated to have something like this: >>`DWORD* ResolveImage(void* data, int pathPartsCount, const WCHAR* path[], int *width, int *height);` >>It should set width and height to proper values and return a pointer to array of (*width) × (*height) values in ARGB format (or any other you like). Basically, a bitmap. >>Or nullptr in case it doesn't have image that matche specified path. Can you actually make a memory url? and as i understand dword is another form of strings, if so then how can rainmeter read this? i mean how does it work? --- If somehow the custom build worked out and the plugins worked as expected in custom + offical rainmeter, i won't make a pull request yet, because currently i'm trying to make other plugins that depend on this feature, so if they asked for the use case of such a request i can show them what rainmeter is capable of with it. but my motivation is -0 due to the problems i mentioned above, so i'm procrastinating until i find a solution to them
d-uzlov commented 3 years ago

but isn't running function to update values using Callback-OnDeviceListChange difficult?

Yes but they won't actually appear in the list until it's refreshed.

Yeah, that's why you need to use Callback-OnDeviceListChange, to make values update when needed without a refresh. You are setting all the values in the script in the first place, so it shouldn't be difficult to just call this script from this callback, so the values will be updated both in the script and on the screen without a refresh.

so good! but last thing, should we let that device be selectable from the menu (with a warning message)?

If you only use auto channel in the example skin that uses device selection, then I don't think any warning is needed.

Okay but if i swapped them in the docs, will it still "technically" make sense? or that will result in unexpected behavior at some point?

I think you can leave references to original names, with a note that original names doesn't make much sense.

Or, I dunno, maybe it would be better to forget the idea of giving users original names of the properties, because it's unlikely that anyone will try to google it. I feel like googling something with original names of these values actually require some C++ knowledge, so everyone who can google it can also just read code to find out what original names are.

d-uzlov commented 3 years ago

Off-topic about writing files.

> umm.. but the more you write the longer the latency... As far as I understand, when writing images on disk to use it in rainmeter, latency mainly comes from syscalls. Copying bytes in memory is essentially free, and sequential write onto disk is very cheap, even on hard drives. There may be some latency due to HDD seek operation, but I hope that modern HDDs cache write operations at least a little bit. So, writing 1 byte the same as writing at least 4 kB, and the difference between 4 kB and 1 MB should be much less, than the absolute latency value. Though, I didn't test this. > Writing to disk using stb_bmp have same effect right? i mean do you override the file or you edit it in place? I dunno if it's the same because I have never used it. Honestly, I just open the file for writing using `std::ofstream` with the default options. I don't know what exactly happens. > P.S few days ago i tried building your plugins, i wanted localPluginLoader for easier development, but i got errors about some headers not found while they clearly exist (fatal error C1083: Cannot open include file:), i didn't know if it's a problem on my part or something is missing. Can we discuss this here or i should make a new issue? > > Second, how to actually make a plugin lol? i mean i know about static and dynamic linking stuff, but i didn't actually try to make a lib/dll, so i lack a bit of knowledge in this area. Yeah, I think an issue in the main repository would be appropriate. Something like "help with building LocalPluginLoader". > Hmm, i thought about that but can renaming be done quickly? You don't rename files, you just change the file name in the Rainmeter. This is essentially free, compared to actually writing something into filesystem. > Now i have few other problems, first is knowledge, howww to make rainmeter support in-memory image-transfer? i don't understand what is evolved in this process, winapi? direct2d? or the way rainmeter api is written? I was having in mind just simple pixel array transfer. Plugins would only need to provide an array of size M×N, no graphics apis involved. Minimal changes to API would only require adding 1 function to plugin API, something like `void FillImage()`. > Also, after modifying rainmeter, what about the plugins api (that .lib thing you have to compile with your plugins)? does it require modification as well? With absolutely minimal changes: no. .lib file is only needed for plugin to link with Rainmeter. Rainmeter loads dlls manually and then calling the functions from them manually. > Lastly, after i take more time to learn how to get this to work, i need a design, i mean how users are going to interact with this api? and for example, a measure that returns image data, what should the message look like in log window? 1 if image is provided, 0 otherwise? "Image"? _Users_ aren't going to interact with this API. Users could just put the plugin name into `measureName` field in the `image` meter. Plugin devs would only provide 1 additional function. Image would be an additional, third value of the measure, with first 2 being "number" and "string" values. I don't think it should be displayed in the "About" window, at least not before everything works. First 2 values can be left as they are. > Can you actually make a memory url? You can make whatever URL, it's just a string with in the form `:///`. Of course, you browser won't understand any of your custom protocols, but nobody can stop you from writing anything inside of your application. >and as i understand dword is another form of strings, if so then how can rainmeter read this? i mean how does it work? Honestly, the thing you cited is not very good idea. It's over-complicated. **_A lot_**. A better idea would be this: ```cpp struct RainmeterImage { DWORD* pixels; DWORD width; DWORD height; }; void GetImage(void* data, RainmeterImage* image); ``` No URLs needed, no paths needed. `RainmeterImage` struct can be extended if need be. DWORD is just a 4-byte integer. In the Visual Studio world it is commonly referred to as `unsigned int`.
LI7XI commented 3 years ago

Yeah, that's why you need to use Callback-OnDeviceListChange, to make values update when needed without a refresh.

You didn't get me... i meant that the list itself needs a refresh before displaying new devices, that's why closing and reopening is needed, afaik it's the only way to refresh a context menu that is made by the os.

If you only use auto channel in the example skin that uses device selection, then I don't think any warning is needed.

I used left and right channels in some examples, so i guess it's better to add a warning.

Or, I dunno, maybe it would be better to forget the idea of giving users original names of the properties, because it's unlikely that anyone will try to google it. I feel like googling something with original names of these values actually require some C++ knowledge, so everyone who can google it can also just read code to find out what original names are.

um.. sorry but i'm a bit lost lol, are we talking in same topic? what i meant is that it's weird to have value of description holding the actual device name (in sound panel) and name holds the device description or the device family string.

It might be confusing for users so i thought about swapping them from the docs or inside the plugin itself.

d-uzlov commented 3 years ago

afaik it's the only way to refresh a context menu that is made by the os.

Oh, I didn't realize you were talking about context menu. Yeah, it's probably impossible to change the native windows menu without closing and reopening it.

um.. sorry but i'm a bit lost lol, are we talking in same topic? what i meant is that it's weird to have value of description holding the actual device name (in sound panel) and name holds the device description or the device family string.

It might be confusing for users so i thought about swapping them from the docs or inside the plugin itself.

From the user perspective they are only distinguished in 2 places: in documentation and in one section variable. So, to swap them you need changes in 2 places: in documentation and in one section variable.

Regarding deviceListString, there are no name and description, just an array of strings. They get their names only inside the parsing script, which gets the names from documentation.

The link between name, description and C++ code is ephemeral, it's only just that there are things like PKEY_Device_DeviceDesc (got name description because of it) in the WAS API. I tried to use this ephemeral link when I was writing the original documentation (thus we got these non-intuitive names) but now I think that it was a bad idea and we should just forget about it and change the docs to use more appropriate names.

LI7XI commented 3 years ago

Off-topic about writing files.

Really sorry, idk where to discuss this.

>I dunno if it's the same because I have never used it. Did you made your bmp-writer from scratch? >You don't rename files, you just change the file name in the Rainmeter. This is essentially free, compared to actually writing something into filesystem. wut? how? with a bang? i mean i didn't make a plugin yet as i told you above. I just want to do testing. >With absolutely minimal changes: no. .lib file is only needed for plugin to link with Rainmeter. Rainmeter loads dlls manually and then calling the functions from them manually. i-... i mean no changes are required to the static lib that you compile with your dll? if so then what plugin api do you mean? there is a folder called plugin api or something like that in rainmeter source code, but also there is an entire repo for that as well. >Users aren't going to interact with this API. Users could just put the plugin name into measureName field in the image meter. Typo, i meant devs* >Image would be an additional, third value of the measure, with first 2 being "number" and "string" values. I don't think it should be displayed in the "About" window, at least not before everything works. First 2 values can be left as they are. Here is an example, if you set a measure that returns image data as a measureName for string meter, what will that meter display? Same for about window, what value we gonna see there? (i mean a little indicator, something tells as what value this measure returns). >DWORD is just a 4-byte integer. In the Visual Studio world it is commonly referred to as unsigned int. Got it, but lastly, what's your opinion about the essay i wrote above? (about compiling with mingw)
LI7XI commented 3 years ago

So, to swap them you need changes in 2 places: in documentation and in one section variable.

Nope, only in one, if we changed them in the docs then the issue is solved. Because users expect to get the data in the order of the docs (devID, name, desc, etc..).

P.S "only in one" i mean no need to change them from the plugin itself if that what you meant by section variable.

Regarding deviceListString, there are no name and description, just an array of strings. They get their names only inside the parsing script, which gets the names from documentation.

Yep i know, that's why i suggest changing it there.

but now I think that it was a bad idea and we should just forget about it and change the docs to use more appropriate names.

hmm... i can't think of other synonyms, these names are actually good 😹

d-uzlov commented 3 years ago

Did you made your bmp-writer from scratch?

Yes. It's not like there is a lot to write, BMP is a very simple format.

wut? how? with a bang? i mean i didn't make a plugin yet as i told you above. I just want to do testing.

Well, yeah, with a plugin. You could probably something very close to it with a bang, if you manage to perform a bang from your app, outside of Rainmeter. IIRC, there was some way. Though, it won't be 100% flicker free because you won't have a perfect synchronization, but it will be very close.

i-... i mean no changes are required to the static lib that you compile with your dll? if so then what plugin api do you mean? there is a folder called plugin api or something like that in rainmeter source code, but also there is an entire repo for that as well.

If you only add 1 function that plugins should implement, you don't need any changes there, if I correctly understand what you mean.

Here is an example, if you set a measure that returns image data as a measureName for string meter, what will that meter display? Same for about window, what value we gonna see there? (i mean a little indicator, something tells as what value this measure returns).

We already have string and number values. Where number is expected, Rainmeter uses number value, where string is expected, Rainmeter uses string value. All measures always have number value (though there are measures which number value in always 0). If a measure doesn't have a string value and its string value is used, then its number value get transformed into a string. Now you will also have image value that will only be used in places where image is expected. If specified measure don't have image value, its string value will be used instead.

As for indicator of whether the measure has image value or not: the easiest way would be to have DWORD returned from that proposed GetImage function, with value 0 meaning that measure doesn't have image value and 1 indicating that it has.

Got it, but lastly, what's your opinion about the essay i wrote above? (about compiling with mingw)

Create an issue in the plugins' repository, let's discuss it there. In short: I didn't try it, but I believe it should be possible, especially with the LocalPluginLoader plugin, which is a lot simpler to build than my other plugins.

Really sorry, idk where to discuss this.

You can create an issue for file-transfer discussion in plugins' repository or if you have any other repository you can create an issue there and ping me. Or in some other place if github is not comfortable for you.

d-uzlov commented 3 years ago

P.S "only in one" i mean no need to change them from the plugin itself if that what you meant by section variable.

I meant current device section variable.

LI7XI commented 3 years ago

Yo, i finally finished the tutorial about device list parsing, you can find it here. Altho i still feel like something is missing, somewhere there or in section variables page.

Also about this comment:

  local DeviceName = DeviceProperties.description -- for some reason, the windows api gives device name instead of device description and vise versa
  local DeviceId   = DeviceProperties.id

You said:

From the user perspective they are only distinguished in 2 places: in documentation and in one section variable. So, to swap them you need changes in 2 places: in documentation and in one section variable.

I think it's better to change the order from the plugin itself, and keep everything else as it is. Because i like the current order (id, name, desk, ...).

Changing it from the docs will create some mess.

P.S

I meant current device section variable.

And DeviceList as well.

LI7XI commented 3 years ago

For the usage examples, i almost finished them, they will be ready to review after some polishing.

LI7XI commented 3 years ago

You can create an issue for file-transfer discussion in plugins' repository or if you have any other repository you can create an issue there and ping me. Or in some other place if github is not comfortable for you.

Sure, soon i'll create an issue somewhere to discuss it with you.