SuperPlayer - The DSP_Engine (CamillaDSP) samplerate switching & ESP32 remote control

And you changed that, Jesper, didn't you..? But I thought you changed it the other way around..?

Well...

I plugged in a ! in my hack back when we coded my son and i.
I remember that we coulden't make it work if we did not...

Actually i do not know if it does the opposite :confused:

This is the original squeezelite around line 690
// FIXME - some alsa hardware requires opening twice for a new sample rate to work
// this is a workaround which should be removed
if (alsa.reopen) {

And the hacked one
// FIXME - some alsa hardware requires opening twice for a new sample rate to work
// this is a workaround which should be removed
if (!alsa.reopen) {

Son was telling me, that ! does the same a boolan "NOT" ... but i'am not sure of this sry...

Can you send when you got the time the .py files you use?
I cannot make it work att all right now!

Jesper.
 
You will most likely need two "triggers" from squeezelite!
When either side of a loopback device is open, it doesn't allow changing the rate. This means that for a successful rate change:
- First both squeezelite and camilladsp must close the loopback device.
- Then they can both reopen the loopback at the new rate (doesn't matter if squeezelite or camilladsp does it first)

Is it possible that the original squeezelite-custom worked like this (with the double commands)?
- Squeezelite requests CamillaDSP to change rate, but still keeps loopback open.
- CamillaDSP tries to open loopback at the new rate, but fails and exits
- Squeezelite sets the new rate (allowed now since CamillaDSP has exited)
- Squeezelite requests CamillDSP to start with the new config, and this time it works.
 
Ok, this seems like you're all doing way too much work. Here is how I use it with the alsa hook. You need to compile and install the hook from my github to make this work.

The python scripts are the height of ugly so I've not generally felt like sharing, but they work as a basic if ugly scaffold until I get around to cleaning them up. This is using the previous beta of camilladsp to the one just released.

I guess the code blocks on the forum can break python, so you might have to fix indentation.

For the record I'm still using the old named lbparams hook, which I changed to hwparams. I edited it here for this change but maybe I made an error manually editing it. Test the hook with the echo commands as described on github.

/etc/asound.conf (or ~/.asoundrc for the user running squeezelite)
Code:
pcm_hook_type.hwparams {
  install "_snd_pcm_hook_hwparams_install"
  lib "libasound_module_pcm_hook_hwparams.so"
}

pcm.camilladsp {
  type hooks
  slave.pcm {
    type hw
    card 0
  }
  hooks.0 {
    type "hwparams"
    hook_args {
      opencmd "/usr/local/bin/startcamilla"
      closecmd "/usr/local/bin/stopcamilla"
    }
  }
}

/usr/local/bin/startcamilla <- Make sure this has executable permissions for the user that runs squeezelite
Note in the script {rate} and {fmt} are python string substitutions. They get replaced by the pending rate
and format. They are not something to hardcode.

The output device and format need to be set for your hardware.
Code:
#!/usr/bin/env python3
from websocket import create_connection
import json
import sys
import time

verbose = False
FORMATS = {
    "S16_LE": "S16LE",
    "S24_LE": "S24LE",
    "S24_3LE": "S24LE3",
    "S32_LE": "S32LE",
    "FLOAT_LE": "FLOAT32LE",
    "FLOAT64_LE": "FLOAT64LE",
}

def update_camilla(fmt, rate, channels):
  cfg = """devices:
  samplerate: {rate}
  chunksize: 4096
  enable_rate_adjust: true
  target_level: 2048
  adjust_period: 10
  capture:
    type: Alsa
    channels: 2
    device: "hw:Loopback,1"
    format: {fmt}
  playback:
    type: Alsa
    channels: 2
    device: "hw: ###### YOUR OUTPUT DEVICE GOES HERE"
    format: S32LE ##### THIS HAS TO MATCH YOUR OUTPUT DEVICE'S CAPABILITIES
filters:
  left_fir:
    type: Conv
    parameters:
      type: File 
      filename: /pathtofilter/filter_{rate}.pcm
      format: FLOAT32LE
  right_fir:
    type: Conv
    parameters:
      type: File 
      filename: /pathtofilter/filter_{rate}.pcm
      format: FLOAT32LE
  atten:
    type: Gain
    parameters:
      gain: -6.0
      inverted: false
pipeline:
  - type: Filter
    channel: 0
    names:
      - atten
      - left_fir
  - type: Filter
    channel: 1
    names:
      - atten
      - right_fir
"""
  ws = create_connection("ws://127.0.0.1:1234")

  d = {'rate':rate, 'fmt':FORMATS[fmt]}
  if verbose: print(cfg.format(**d))
  ws.send(json.dumps({"SetConfig":cfg.format(**d)}))
  time.sleep(0.1)
  out = ws.recv()
  if verbose: print(out)
  ws.close()

if __name__ == '__main__':
  fmt = sys.argv[1]
  rate = sys.argv[2]
  channels = sys.argv[3]
  update_camilla(fmt, rate, channels)

/usr/local/bin/stopcamilla <- also executable by user that runs squeezelite
Code:
#!/usr/bin/env python3
from websocket import create_connection
import json
import sys, time
verbose = False
if verbose: print("Stopping Camilla")
ws = create_connection("ws://127.0.0.1:1234")
# Check if it's already stopped and do nothing
ws.send(json.dumps("GetState"))
res = json.loads(ws.recv())
if res['GetState']['value'] == 'Inactive':
  sys.exit(0)

ws.send(json.dumps("Stop"))
out = ws.recv()
if verbose: print(out)
ws.send(json.dumps("GetState"))
res = json.loads(ws.recv())
while res['GetState']['value'] != 'Inactive':
  time.sleep(0.01)
  ws.send(json.dumps("GetState"))
  res = json.loads(ws.recv())
if verbose: print("Stopped")
ws.close()
sys.exit(0)

Parameters for squeezelite. Most set by moode. I added "-o camilladsp -r 44100 384000". Obviously the rate should match what your hardware can do. I have no idea what version of squeezelite moode uses. I'd not even run squeezelite before a few days ago.
Code:
/squeezelite -a 80 4  1 -b 40000 100000 -p 45 -c flac,pcm,mp3,ogg,aac,alac,dsd -W -D 500 -R E -o camilladsp -r 44100 384000

camilladsp parameters
Code:
camilladsp -a 0.0.0.0 -p 1234 -w


With this setup rate and/or format switches work fine for me with both squeezelite and mpd and switching back and forth. If you somehow get out of sync it's typically easily fixed by clicking play again. If you play a file with parameters your hardware can't support it will probably break things. Sadly any way you don't attach directly to the hardware will not let your player program get the parameters the hardware supports. You can make the camilla python scripts smarter though and even enable resampling for unsupported rates.

I suggest trying with files with parameters you know work before you go for the exotic ones.

You can see what the player end of the loopback device is currently set to with this command.

Code:
cat /proc/asound/Loopback/pcm0p/sub0/hw_params
It should say "closed" when nothing is running. Note squeezelite I believe by default never closes the port while it is "on" so you won't see "closed" while it is running. squeezelite also seems to always use S32_LE regardless of the file format. mpd releases the hardware when you press stop and uses the file native format.

Some of this assumes Loopback is dev 0. You can fix that or type in Loopback where needed.
 
** EDIT EDIT ** Posted at same time as SEASHELL

Morning... early start at work :eek:

Is it possible that the original squeezelite-custom worked like this (with the double commands)?


This is a snip from the original squeezelite:(~line 690) squeezelite/output_alsa.c at master * ralph-irving/squeezelite * GitHub
// FIXME - some alsa hardware requires opening twice for a new sample rate to work
// this is a workaround which should be removed
if (alsa.reopen) {
#if DSD
alsa_open(output.device, output.current_sample_rate, output.buffer, output.period, output.outfmt);
#else
alsa_open(output.device, output.current_sample_rate, output.buffer, output.period);
#endif
}
There seem's to be problems with some hardware not accepting a one shot opening of new sample rate.

For clearification i repeat myself that i remember that we couldent make it work until we attached a ! in front of the alsa like this
if (!alsa.reopen)
I didn't try to remove it afterwards through!

Anyway, i have tried a lot of times running squeezelite before i started camilladsp and this is not possible as far as i know. -Allway's do it the other way around.

When either side of a loopback device is open, it doesn't allow changing the rate.

This is most proberly why we struggle, so how do we accomplish this :)

Jesper.
 
Last edited:
Ok, this seems like you're all doing way too much work. Here is how I use it with the alsa hook. You need to compile and install the hook from my github to make this work.

The python scripts are the height of ugly so I've not generally felt like sharing, but they work as a basic if ugly scaffold until I get around to cleaning them up. This is using the previous beta of camilladsp to the one just released.

I guess the code blocks on the forum can break python, so you might have to fix indentation.

For the record I'm still using the old named lbparams hook, which I changed to hwparams. I edited it here for this change but maybe I made an error manually editing it. Test the hook with the echo commands as described on github.

/etc/asound.conf (or ~/.asoundrc for the user running squeezelite)
Code:
pcm_hook_type.hwparams {
  install "_snd_pcm_hook_hwparams_install"
  lib "libasound_module_pcm_hook_hwparams.so"
}

pcm.camilladsp {
  type hooks
  slave.pcm {
    type hw
    card 0
  }
  hooks.0 {
    type "hwparams"
    hook_args {
      opencmd "/usr/local/bin/startcamilla"
      closecmd "/usr/local/bin/stopcamilla"
    }
  }
}

/usr/local/bin/startcamilla <- Make sure this has executable permissions for the user that runs squeezelite
Note in the script {rate} and {fmt} are python string substitutions. They get replaced by the pending rate
and format. They are not something to hardcode.

The output device and format need to be set for your hardware.
Code:
#!/usr/bin/env python3
from websocket import create_connection
import json
import sys
import time

verbose = False
FORMATS = {
    "S16_LE": "S16LE",
    "S24_LE": "S24LE",
    "S24_3LE": "S24LE3",
    "S32_LE": "S32LE",
    "FLOAT_LE": "FLOAT32LE",
    "FLOAT64_LE": "FLOAT64LE",
}

def update_camilla(fmt, rate, channels):
  cfg = """devices:
  samplerate: {rate}
  chunksize: 4096
  enable_rate_adjust: true
  target_level: 2048
  adjust_period: 10
  capture:
    type: Alsa
    channels: 2
    device: "hw:Loopback,1"
    format: {fmt}
  playback:
    type: Alsa
    channels: 2
    device: "hw: ###### YOUR OUTPUT DEVICE GOES HERE"
    format: S32LE ##### THIS HAS TO MATCH YOUR OUTPUT DEVICE'S CAPABILITIES
filters:
  left_fir:
    type: Conv
    parameters:
      type: File 
      filename: /pathtofilter/filter_{rate}.pcm
      format: FLOAT32LE
  right_fir:
    type: Conv
    parameters:
      type: File 
      filename: /pathtofilter/filter_{rate}.pcm
      format: FLOAT32LE
  atten:
    type: Gain
    parameters:
      gain: -6.0
      inverted: false
pipeline:
  - type: Filter
    channel: 0
    names:
      - atten
      - left_fir
  - type: Filter
    channel: 1
    names:
      - atten
      - right_fir
"""
  ws = create_connection("ws://127.0.0.1:1234")

  d = {'rate':rate, 'fmt':FORMATS[fmt]}
  if verbose: print(cfg.format(**d))
  ws.send(json.dumps({"SetConfig":cfg.format(**d)}))
  time.sleep(0.1)
  out = ws.recv()
  if verbose: print(out)
  ws.close()

if __name__ == '__main__':
  fmt = sys.argv[1]
  rate = sys.argv[2]
  channels = sys.argv[3]
  update_camilla(fmt, rate, channels)

/usr/local/bin/stopcamilla <- also executable by user that runs squeezelite
Code:
#!/usr/bin/env python3
from websocket import create_connection
import json
import sys, time
verbose = False
if verbose: print("Stopping Camilla")
ws = create_connection("ws://127.0.0.1:1234")
# Check if it's already stopped and do nothing
ws.send(json.dumps("GetState"))
res = json.loads(ws.recv())
if res['GetState']['value'] == 'Inactive':
  sys.exit(0)

ws.send(json.dumps("Stop"))
out = ws.recv()
if verbose: print(out)
ws.send(json.dumps("GetState"))
res = json.loads(ws.recv())
while res['GetState']['value'] != 'Inactive':
  time.sleep(0.01)
  ws.send(json.dumps("GetState"))
  res = json.loads(ws.recv())
if verbose: print("Stopped")
ws.close()
sys.exit(0)

Parameters for squeezelite. Most set by moode. I added "-o camilladsp -r 44100 384000". Obviously the rate should match what your hardware can do. I have no idea what version of squeezelite moode uses. I'd not even run squeezelite before a few days ago.
Code:
/squeezelite -a 80 4  1 -b 40000 100000 -p 45 -c flac,pcm,mp3,ogg,aac,alac,dsd -W -D 500 -R E -o camilladsp -r 44100 384000

camilladsp parameters
Code:
camilladsp -a 0.0.0.0 -p 1234 -w


With this setup rate and/or format switches work fine for me with both squeezelite and mpd and switching back and forth. If you somehow get out of sync it's typically easily fixed by clicking play again. If you play a file with parameters your hardware can't support it will probably break things. Sadly any way you don't attach directly to the hardware will not let your player program get the parameters the hardware supports. You can make the camilla python scripts smarter though and even enable resampling for unsupported rates.

I suggest trying with files with parameters you know work before you go for the exotic ones.

You can see what the player end of the loopback device is currently set to with this command.

Code:
cat /proc/asound/Loopback/pcm0p/sub0/hw_params
It should say "closed" when nothing is running. Note squeezelite I believe by default never closes the port while it is "on" so you won't see "closed" while it is running. squeezelite also seems to always use S32_LE regardless of the file format. mpd releases the hardware when you press stop and uses the file native format.

Some of this assumes Loopback is dev 0. You can fix that or type in Loopback where needed.

Wooow... :)

Seem's we had some to work with now.

I had problems using python copy/paste as you mention, so if you please attach the "working" one you have as a .zip or whatever i would be pleased.

This is proberly the way to go, but a lot of work have to be done for my head to understand it fully :D

Thank's.

Jesper
 
Not sure why you guy make it that complicated. :p

On LMS you simply add a DRC rule to custom-convert.conf, write a 3-line bash wrapper
that switches filters and you'll be set. ;)
For me because I don't just use LMS/squeezelite and I want all the audio on my computer to go through the DSP engine.

But why don't you tell me? A quick search says you've posted 27 times in the camilladsp thread even talking about using brute-fir. And are you "K S" on the DRC-FIR mailing list stating camilladsp is your DSP engine?

Since you have a three line solution and this thread seems to be about fixing just LMS/squeezelite why don't you post your solution for the people here. I mean it's only three lines right?
 
Uhh... Now new approaches drop in faster than I can read them... :boggled:

I'm also looking at a more general solution. Not just LMS/squeezelite/piCorePlayer, even if that is my primary target for the moment.

I guess that soundcheck's solution is supposed to run on the LMS server, which is nice in some aspects, but also have some limitations. Assuming LMS is strictly 2 channel (?), we can't make it do XO-filtering. Which is my goal.

But i'd also love to hear some more details about it. It might interest others.
 

TNT

Member
Joined 2003
Paid Member
Is the goal with this to avoid re-sampling? Or, can re-sampling solve what this intended to fix?

I have also been thinking that switching is a cleaner solution but if we start to do filtering and what not, is yet one calculation step the "whole world"?

Post #1 did not define a problem...

//
 
You're poking a week spot here...
I guess it's all about lack of knowledge about resampling by LMS/squeezelite/alsa(?), leading to lack of trust. That's the case for me... I do trust resampling by camilladsp though.
That - combined with a vain hunt for perfection... :rolleyes:
And maybe just for fun...
 
For me because I don't just use LMS/squeezelite and I want all the audio on my computer to go through the DSP engine.

Then you're obviously at the wrong spot. "superplayer" is just a hacked squeezelite and that one works in a LMS/squeezelite environment only.

But why don't you tell me? A quick search says you've posted 27 times in the camilladsp thread even talking about using brute-fir. And are you "K S" on the DRC-FIR mailing list stating camilladsp is your DSP engine?

Gee. Interesting. You're tracking me down all over the place. And learned nothing.

Yep. I used to use brutefir. I left the whole DRC and active crossover thing alone for years though, because it was messing with the sound. You gain on one side and you loose on the other. I had the impression I lost more then I gained. The effort was simply not worth it.

And now being back on the subject I slowly but surely realize that it might still be the case. Looking at what has to be done to get going.


Over @ Henriks thread I brought up brutefir as reference. Henrik was obviously using it as well earlier. Fun fact. brutefir still performs better in the narrow corridor of fir processing.
And brutefir didn't have any issues performing stdin/stdout. And brutefir offered
a test filter setup. If that took me just 27 posts to get it all settled. Fair enough. :D

Result: I am using Camilla now. Because it's alive.

Since you have a three line solution and this thread seems to be about fixing just LMS/squeezelite why don't you post your solution for the people here. I mean it's only three lines right?

Look. You're not a LMS/squeezelite guy. You basically don't know what you're talking about. LMS is build to allow advanced routing, conversions and DSP on the server. There are numerous plugins. It was done this way to have it all on a powerful server.
And not on some narrow-chested clients. Instead of customizing each and every client you do the job on the server.

And what you do. You basically can sneak into a stream and do whatever you want with it using sox or whatever app you prefer. Yes also brutefir and Camilla will do.

I am still in the phase of setting up the new chain. Right now I got stuck on the measuring and filter creation part. A hundred times more important than switching samplerates if 99.0% of your collection is 44.1/16 anyhow.

Anyhow. Just to give you an idea about a wrapper that could be called by LMS from custom-convert.conf could like this:

Code:
#!/bin/bash
newrate=$1
client=$2
camconfig=/etc/camilladsp/configs/$client.yml

camrate="$(grep "samplerate" $camconfig | awk '{print $2}')"
sed -i "s/$camrate/$newrate/g" $camconfig
camilladsp $camconfig

Basically a 3-liner. ;)

Your filters must have the samplerates in the filenames of course

I havn't tested it yet. As soon as I have it all working I'll write a short article on my blog.
 
Last edited:
Then you're obviously at the wrong spot. "superplayer" is just a hacked squeezelite and that one works in a LMS/squeezelite environment only.
:no: No - You're mistaken.

The "Superplayer" is a CamillaDSP enabled piCorePlayer.
The solution must work on a pCP.
It's not a condition that it shall not work anywhere else.
General solutions are highly welcome.

Aside from that, thank you for sharing one possible solution, even if not general.

Meanwhile, I have tested piping squeezelite stdout to camilladsp, with a modified python script listening to squeezelite's log output.
That works. But there is a 20 seconds delay to start/stop playing, and manually change tracks. Playing a list with different sample rates is smooth. It's just start and stop...
 
Meanwhile, I have tested piping squeezelite stdout to camilladsp, with a modified python script listening to squeezelite's log output.
That works. But there is a 20 seconds delay to start/stop playing, and manually change tracks. Playing a list with different sample rates is smooth. It's just start and stop...
Did you set queuelimit=1 in the camilla config? If not, do it :)
 
Now - How superb support wasn't that..? :wave2:

No - I did not...
Now I have - and no delay... Thanks..!

I just have to find out a proper way of launching and running it all...
Is it even reasonable to rely on having a simple python script running for months, listening to a piped log output from squeezelite?
 
The "Superplayer" is a CamillaDSP enabled piCorePlayer.

Sorry for assuming piCorePlayer is a LMS/squeezelite based streaming solution which is
based on TinyCore/piCore OS.

And I think to recall that the thread was named "an ugly hacked squeezelite auto switching
machine"

I have to admit. That I don't go into details if something greats me with "ugly hacked".

Sorry for all the mistakes I've done. :rolleyes:


And just as a side note. piCorePlayer is IMO the worst OS for hacking.
Running a full-fledged 64bit system such as piOS64 as base is really what people should be looking for.

Enjoy.
 
Now - How superb support wasn't that..? :wave2:

No - I did not...
Now I have - and no delay... Thanks..!

I just have to find out a proper way of launching and running it all...
Is it even reasonable to rely on having a simple python script running for months, listening to a piped log output from squeezelite?
Great that it works! There is no reason why a python script couldn't be trusted to perform it's duties for many months. We rely heavily on that at work!


Sorry for assuming piCorePlayer is a LMS/squeezelite based streaming solution which is
based on TinyCore/piCore OS.

And I think to recall that the thread was named "an ugly hacked squeezelite auto switching
machine"

I have to admit. That I don't go into details if something greats me with "ugly hacked".

Sorry for all the mistakes I've done. :rolleyes:


And just as a side note. piCorePlayer is IMO the worst OS for hacking.
Running a full-fledged 64bit system such as piOS64 as base is really what people should be looking for.

Enjoy.
Yeah any embedded distribution will be a pain when playing around with stuff like this, but that's part of the concept. You do all the hacking before, then you "print" a ready system that isn't meant to be changed a lot.