Solved Sound Device Order Changes on Reboot – Need Persistent Headset Default

I have a Lenovo laptop connected to a Lenovo docking station. A USB headset is plugged into the docking station. Occasionally, after a reboot, the headset's PCM device switches places with the docking station's sound card. As a result, sysctl hw.snd.default_unit=x no longer points to the headset, and I lose audio output.
Is there a way to ensure that the USB headset always remains the default sound device, regardless of the boot order?


Code:
$ cat /dev/sndstat
Installed devices:
pcm0: <ATI R6xx (HDMI)> (play)
pcm1: <ATI R6xx (HDMI)> (play)
pcm2: <ATI R6xx (HDMI)> (play)
pcm3: <Realtek ALC257 (Analog 2.0+HP)> (play)
pcm4: <Lenovo ThinkPad USB-C Dock Gen2 USB Audio> (play/rec)
pcm5: <Plantronics Plantronics Blackwire 3220 Series> (play/rec) default
No devices installed from userspace.

pcm4 and pcm5 sometimes switches places and I am tired to set the default sound device.
 
I have a Lenovo laptop connected to a Lenovo docking station. A USB headset is plugged into the docking station. Occasionally, after a reboot, the headset's PCM device switches places with the docking station's sound card. As a result, sysctl hw.snd.default_unit=x no longer points to the headset, and I lose audio output.
Is there a way to ensure that the USB headset always remains the default sound device, regardless of the boot order?


Code:
$ cat /dev/sndstat
Installed devices:
pcm0: <ATI R6xx (HDMI)> (play)
pcm1: <ATI R6xx (HDMI)> (play)
pcm2: <ATI R6xx (HDMI)> (play)
pcm3: <Realtek ALC257 (Analog 2.0+HP)> (play)
pcm4: <Lenovo ThinkPad USB-C Dock Gen2 USB Audio> (play/rec)
pcm5: <Plantronics Plantronics Blackwire 3220 Series> (play/rec) default
No devices installed from userspace.

pcm4 and pcm5 sometimes switches places and I am tired to set the default sound device.
Have you considered writing a script that will set it for you?
 
You may be able to leverage devd to match on the desired device and sysctl to set it to the default.
Heres a sample that I use to set permissions on a Nikon D610 when it's plugged in. It lives in /usr/local/etc/devd/d610.conf. Change the vendor and product to your USB device, action would be calling the sysctl. Not sure how the "x" would map to $cdev or other variable available to devd.

notify 100 {
match "vendor" "0x04B0";
match "product" "0x0434";
action "chgrp wheel /dev/$cdev && chmod 660 /dev/$cdev";
};
 
Have you considered writing a script that will set it for you?
Thanks to both of you for the helpful tips!
In the end, I created this script (with a little help from AI):

Code:
$ cat /root/default_sound_scripter.sh
#!/bin/sh

# Extract the pcm number for Plantronics
plantronics_pcm=$(cat /dev/sndstat | grep 'Plantronics' | awk -F: '{print $1}' | sed 's/pcm//')

# If found, set it as default
if [ -n "$plantronics_pcm" ]; then
  sysctl hw.snd.default_unit=$plantronics_pcm
else
  echo "Plantronics device not found in /dev/sndstat"
fi

And it's triggered by this devd rule:

Code:
$ cat /usr/local/etc/devd/plantronics.conf
attach 100 {
  match "vendor" "0x047f";        # Replace with actual vendor ID
  match "product" "0xc056";       # Replace with actual product ID
  action "/root/default_sound_scripter.sh";
};

I tested it, and it works perfectly!
 
Thanks to both of you for the helpful tips!
In the end, I created this script (with a little help from AI):

Code:
$ cat /root/default_sound_scripter.sh
#!/bin/sh

# Extract the pcm number for Plantronics
plantronics_pcm=$(cat /dev/sndstat | grep 'Plantronics' | awk -F: '{print $1}' | sed 's/pcm//')

# If found, set it as default
if [ -n "$plantronics_pcm" ]; then
  sysctl hw.snd.default_unit=$plantronics_pcm
else
  echo "Plantronics device not found in /dev/sndstat"
fi

And it's triggered by this devd rule:

Code:
$ cat /usr/local/etc/devd/plantronics.conf
attach 100 {
  match "vendor" "0x047f";        # Replace with actual vendor ID
  match "product" "0xc056";       # Replace with actual product ID
  action "/root/default_sound_scripter.sh";
};

I tested it, and it works perfectly!
Awesome! That's a good solution. You can create the action script and test independently, create the devd rule test independently, then when they both work, hook them together. Thanks for the update.
 
Thanks to both of you for the helpful tips!
In the end, I created this script (with a little help from AI):

Code:
$ cat /root/default_sound_scripter.sh
#!/bin/sh

# Extract the pcm number for Plantronics
plantronics_pcm=$(cat /dev/sndstat | grep 'Plantronics' | awk -F: '{print $1}' | sed 's/pcm//')

# If found, set it as default
if [ -n "$plantronics_pcm" ]; then
  sysctl hw.snd.default_unit=$plantronics_pcm
else
  echo "Plantronics device not found in /dev/sndstat"
fi

And it's triggered by this devd rule:

Code:
$ cat /usr/local/etc/devd/plantronics.conf
attach 100 {
  match "vendor" "0x047f";        # Replace with actual vendor ID
  match "product" "0xc056";       # Replace with actual product ID
  action "/root/default_sound_scripter.sh";
};

I tested it, and it works perfectly!
I'm very glad that you found solution, and that your script works, just one question: did AI recommended using awk? In my experience with AI (which is not huge, TBH) it tends to bring out heavy artillery even when there is no actual need for that.
As much as I love awk(1), instead of | awk -F: '{print $1}' | , you can get same result with simpler | cut -d: -f1 |

If you wanted awk, it could be done with awk only, without cat, grep, sed and pipes, something like:
$(awk '/Plantronics/ { split($0, a, ":"); gsub("pcm", "", a[1]); print a[1] }' /dev/sndstat)
 
# Extract the pcm number for Plantronics plantronics_pcm=$(cat /dev/sndstat | grep 'Plantronics' | awk -F: '{print $1}' | sed 's/pcm//')
If you wanted awk, it could be done with awk only, without cat, grep, sed and pipes, something like:
$(awk '/Plantronics/ { split($0, a, ":"); gsub("pcm", "", a[1]); print a[1] }' /dev/sndstat)
Variations on a theme. For the awk minded:
awk -Fpcm '/Plantronics/ { print substr($2,1,1) }'

If, however, one has a lot of pcm devices, because, well, one does :) , then a variation on covacat's solution:
sed -nE '/Plantronics/ s/pcm([^:]*):.*/\1/ p' *

In my view the most important easy win, compared to the three step solution, is to realise that you can use a regular expression as an address for the line you are interested in; that holds for both sed(1) and awk(1). This may also be the easiest to remember when you need to solve similar problems.

___
* this resembles grep 'Plantronics' | awk -F: '{print $1}' | sed 's/pcm//' in how things are matched.
Edit: slightly changed the layout of the commands.
 
Variations on a theme. For the awk minded:
awk -Fpcm '/Plantronics/{print substr($2,1,1)}'

If, however, one has a lot of pcm devices, because, well, one does :) , then a variation on covacat's solution:
sed -nE '/Plantronics/s/pcm([^:]*):.*/\1/p' *

In my view the most important easy win, compared to the three step solution, is to realise that you can use a regular expression as an address for the line you are interested in; that holds for both sed(1) and awk(1). This may also be the easiest to remember when you need to solve similar problems.

___
* this resembles grep 'Plantronics' | awk -F: '{print $1}' | sed 's/pcm//' in how things are matched.
Thanks! Those are great examples, and I really like and appreciate them as I'm of opinion that regex should be first (okay, maybe second) thing to learn for anyone interested in Unix or Linux, but there is another more important thing – KISS principle.

For example, I have bunch of scripts that I wrote some years ago that are heavily based on sed(1); I had great time writing them and I was very proud of myself for coming up with right syntax, but if I look at them now, I'm like: "WTF is this line doing, and what I was thinking when I wrote this???"

Using simpler cat | grep | cut | tr | (simple) sed or something similar, will allow for the scripts to stay readable and understandable for us, even when we age.
 
for the scripts to stay readable and understandable for us, even when we age.
Well, it's not just about our own age. As we get older, the correlation between ourselves aging and the aging of code only increases.

My first thought was that cut wasn't going to make it, however:
Rich (BB code):
$ echo 'cat | grep | cut | tr | (simple) sed' '| basic sed | basic awk' | cut -d'|' -f 2,6,7
 grep | basic sed | basic awk
In all seriousness, there is a balance to be struck.

There is a big difference between thinking in regular expressions—combined or not with exploiting sed's limited capabilities of a state—and reading, even decrypting, regular expressions after they have been designed and written. That's the price that has to be paid for the brevity and power of regular expressions covering this class of languages. Regexes are part of so many languages and useful in so many domains, that one can hardly afford not to learn their basic use.

Consider the following list:
  1. Grep
  2. Regular Expressions
  3. sed
  4. awk
  5. UNIX SHELL Quote Tutorial
  6. Sh - the POSIX Shell
or any equivalent sources. I've found these of indispensable value. It's not even required to learn it by heart: not as in Everything Everywhere All at Once ;), learn as you go and practice!

As to the last two items, sh(1) programming beyond the basics is, in my personal view, only required when in need of portability or when contributing to FreeBSD. Personally, I don't like the lack of a basic data structure like the list or array.

If you can forego the use of libraries, awk can be a very versatile tool; IMO, for use on the command line there's hardly any competition when it comes to a balance between being non-cryptic and compact. However, as Brian Kernighan says in The AWK Programming Language, Second Edition: "Realistically, if you’re going to learn only one language, Python is the one". I view this book as a very valuable option for sysadmins and programmers. It also contains balanced considerations on when to look for other programming languages as options to solve your problems.

Returning to practical matters: in my view, the use of
awk -F: '/Plantronics/ { print $1 }' /dev/sndstat | sed 's/pcm//' *

instead of:
cat /dev/sndstat | grep 'Plantronics' | awk -F: '{print $1}' | sed 's/pcm//'
should be within every FreeBSD user's grasp.

However, using a regexp explicitly shouldn't be that dificult:
awk -F: '/Plantronics/ { match($1, /[[:digit:]]*$/); print substr($1, RSTART, RLENGTH) }' /dev/sndstat

As for:
awk -Fpcm '/Plantronics/ { print substr($2,1,1) }'
sed -nE '/Plantronics/ s/pcm([^:]*):.*/\1/ p'
a general idea of what is going on here is a start. Understanding this by means of the above references requires some study and practice.
And, as you can see, cut didn't make the cut ;)

___
* In my book, this is simple sed, not basic sed.
 
Last edited:
Excellent post Erichans, thanks! I do agree with (almost) all of what you said here, and I admit – I’m so hooked on bash arrays even when there is no need, I go for them just for fun of it 😁
I only don’t understand what you have against cut? I can’t imagine writing any of my monstrosities of one-liners without it. For example, some time ago, I was looking at HECnet node name database information service at MIM (MIM is currently not responsive, that’s why archive) and I thought "Can I figure out how many people are running those nodes, sort them by surname and pretty print so that all can fit on one screen?"
BTW, here is example of sed using address for the lines 😜

Disclaimer and trigger warning: this is ugly; kids, don’t be like me!
Code:
elinks -dump "https://web.archive.org/web/20240316090019/http://mim.stupi.net/nodedb" | \
sed -e1,36d -e'1145,$d' | cut -c 20-45 | sort -u | sed -e 's/ *$//g' | sed -r 's/(.*) /\1,/g' | sort -t, -fk2 | nl -nln | \
sed -r 's/([[:digit:]]) /\1\./g' | sed -e 's/[[:space:]]\+/ /g' -e 's/,/ /g' -e 11,100's/^/ /g' | tr '\t' ' ' | tr -s ' ' | \
sed -e 1,10's/^/  /g' | column -x
Output:
Code:
  1. Mark Abene                   2. Peter Allan                  3. Anders Andersson             4. Brian Angus                  5. Paul Anokhin                 6. Robert Armstrong
  7. Madeline Autumn-Rose         8. Moishe Bar                   9. Mark Benson                 10. Jean-Yves Bernier           11. Mark Berryman               12. Johnny Billquist
 13. Mark J. Blair               14. Robin Blair                 15. Tony Blews                  16. Wilm Boerhout               17. Dennis Boone                18. Jason Brady
 19. Olof Breuer                 20. Jeroen Brons                21. David Brown                 22. Christine Caulfield         23. Jesper Christensen          24. Dana Chus
 25. Fred Coffey                 26. Peter Coghlan               27. Dave Comley                 28. Mark Curtis                 29. Mark Darvill                30. Steve Davidson
 31. Thomas DeBellis             32. Johan Dees                  33. Thierry Dusset              34. Pete Edwards                35. Joe Ferraro                 36. James Gay
 37. Lee Gleason                 38. Andy Green                  39. Ed Groenenberg              40. Chuck Guldenschuh           41. Keith Halewood              42. Douglas Hall
 43. Michael Hamer               44. Kurt Hamm                   45. Iain Hardcastle             46. Zane Healy                  47. Brian Hechinger             48. Michael Holmes
 49. Ulli Hölscher               50. Vladimir Isakov             51. Peter Jackson               52. Yvan Janssens               53. Rob Jarratt                 54. Jeffrey Johnson
 55. John Kemker                 56. Paul Koning                 57. Fedor Konstantinov          58. Mike Kostersitz             59. Sampsa Laine                60. Ruslan Laishev
 61. Peter Löthberg              62. Karl Maftoum                63. Stuart Martin               64. Mark Matlock                65. Dave McGuire                66. Ian McLaughlin
 67. Gordon Miller               68. Eric Moore                  69. David Moylan                70. Tony Nicholson              71. Thord Nilsson               72. Bruno Novak
 73. Robert Nydahl               74. Erik Olofsen                75. Ales Petan                  76. Jordi Guillaumes Pons       77. Tomas Prybil                78. John H. Reinhardt
 79. Christopher Rivett          80. Katherine Rohl              81. Lex van Roon                82. Brian Roth                  83. Oleg Safiullin              84. Supratim Sanyal
 85. Fausto Saporito             86. Saku Setala                 87. Tim Sneddon                 88. Michael Spencer             89. Timothy Stark               90. Jason Stevens
 91. Mark Thomas                 92. August Treubig              93. Unknown                     94. Oscar Vermeulen             95. Björn Victor                96. Rok Vidmar
 97. Hans Vlems                  98. Reindert Voorhorst          99. Trevor Warwick             100. Gullik Webjörn             101. Peter Whisker              102. Mark Wickens
103. Dan Williams               104. John Wilson                105. Julian Wolfe               106. Frank Wortner              107. Alice Wyan                 108. John Yaldwyn
109. Connor Youngquist
🤪
 
I've worked out two additional solutions of my own, reflecting different ways to solve your problem:
  1. use sed(1) and make 'smart' use of utilities
  2. let sed(1) do as much as possible, minimizing its invocations
Perhaps this helps to solve future problems using utilities, and sed in particular.
There are many ways leading to Rome.

I only don’t understand what you have against cut? I can’t imagine writing any of my monstrosities of one-liners without it. For example, [...]
Well, you asked ...
Thank you for providing an example, solution and example run; I found the explanation helpful too. Unfortunately, often a problem or code trying to solve a problem comes without a useful description.

As to my previous answer, I had a longer answer prepared but I trimmed it. There is of course nothing against cut(1) or tr(1). In my personal, limited, experience when I want to go from a to b, sometimes I need more than cut(1) can provide, or I can easily integrate the desired functionality in adjacent commands at the other side of the fence, that is: |. That might be considered a personal preference. All utilities have their particular use cases. However, when one chooses to use more utilities (as opposed to some extra coding), it usually pays to carefully read man pages to avoid unnecessary coding. nl(1) provides useful options in this case.

Regarding the problem at hand. I imagine the original source is better formatted (e.g. XML or HTML). In that case, a parser would be a good tool. Often, we have to work with the cards we're dealt. If further, more flexible data manipulation is needed, the obvious approach would be to structure all the data and store it appropriately—perhaps in a database. As we're dealing with plain text (not even <tab> formatted), sed & awk (with additional assistance) seem like good candidates. As a result of working on my sed solutions, I can't see an awk solution that does things differently or more easily than sed, so I only used sed.

Two things that sed cannot do is sorting and formatting columns, so external help is needed here. In addition to sed(1), only one call of sort(1) and column(1) remain as part of my second solution. You might have already noticed by careful reading: I've cut out cut(1), again! However, I agree that using cut is appropriate in this context, even though it increases the number of commands and pipes.

Where I don't comment my first solution, I do provide comments in the script files of my second solution. There, I also handled the right justification of numbers below 100. In much the same way you did; however, one doesn't have to do that when using nl(1), as is demonstrated in the first solution.

I've decided to put both solutions in a spoiler. If one wants to work out an alternative solution without my solutions in plain view, one can choose to do so; viewing them is just a couple of clicks away though. While using sed, I ran into what looks like a sed bug. That took considerable time to analyse; I've worked around it. I used the file i, created by:
elinks -dump "https://web.archive.org/web/20240316090019/http://mim.stupi.net/nodedb" > i

Main processing steps - second solution
Rich (BB code):
$ cat i | \
sed -Ef preSort.sed | \
sort -uk1 | \
sed -Ef postSortLN.sed | \
sed -Ef mergeRjustLN.sed | \
column -x

Rich (BB code):
$ cat i | \
sed -n '37,1143 p' | \
cut -c 20-45 | \
sed -E 's/ *$//;s/(.* )([^ ]+)$/\2\1/' | \
sort -uk1 | \
sed -E 's/^([^ ]+)( .*)/\2\1/' | \
nl -nrn -s'.' | \
column -x

Rich (BB code):
[1-0] % cat preSort.sed
# select table rows by deleting other lines
# cut out name column at the start of first name
# remove line ending spaces
# move surname to the front as preparation for sort
1,36 d
1144,$ d
s/.{20}(.{22}).*/\1/
s/ *$//
s/(.*) ([^ ]+)$/\2 \1/
[2-0] % cat postSortLN.sed
# restore original name order
# generate line numbers, to be processed later
s/^([^ ]+) (.*)/\2 \1/
=
[3-0] % cat mergeRjustLN.sed
# merge line numbers
# add line number formatting
N
s/\n/. /
# right justify single and double digit line numbers
s/^[^.]\./  &/
s/^[^.]{2}\./ &/

Rich (BB code):
$ cat i | sed -Ef preSort.sed | sort -uk1 | sed -Ef postSortLN.sed | sed -Ef mergeRjustLN.sed | column -x
  1. Mark Abene                   2. Peter Allan                  3. Anders Andersson             4. Brian Angus                  5. Paul Anokhin
  6. Robert Armstrong             7. Madeline Autumn-Rose         8. Moishe Bar                   9. Mark Benson                 10. Jean-Yves Bernier
 11. Mark Berryman               12. Johnny Billquist            13. Mark J. Blair               14. Robin Blair                 15. Tony Blews
   <snap>
101. Peter Whisker              102. Mark Wickens               103. Dan Williams               104. John Wilson                105. Julian Wolfe
106. Frank Wortner              107. Alice Wyan                 108. John Yaldwyn               109. Connor Youngquist

___
Edit: there's final editing for you: spaces fly where there not supposed to. Changed preSort.sed as intended.
 
Excellent! Thanks 🙏 Only two problems that I see:
1) First and Last name are merged into one string "LastFirst"; it's not "Autumn-RoseMadeline" but "Madeline Autumn-Rose", "J. BlairMark" vs "Mark J. Blair" etc. In my version, Name comes first, then Middle Name initial, then Surname, but all are sorted by surname.

and

2) It's not one-liner and do I love my monstrosities of one-liners very much. 😜

Perhaps someone who is good with Perl (unfortunately, I'm not) can do it as one line solution?

BTW, I just spotted an error in version that I posted, I had better one which was trowing out all Unknown (like 93. Unknown). I was interested in known persons list, not number of nodes.

P.S. Edit: Thanks for the edit, Erichans; now output looks much better, but still, it's not one-liner 🤭
 
Only two problems that I see:
1) First and Last name are merged into one string
Yes, I changed that.

2) It's not one-liner and do I love my monstrosities of one-liners very much. 😜
Well, perhaps, with a little imagination?
These look like one-liners to me:
Rich (BB code):
$ cat i | sed -n 37,1143p | cut -c20-45 | sed -E 's/ *$//;s/(.* )([^ ]+)$/\2\1/' | sort -uk1 | sed -E 's/^([^ ]+)( .*)/\2\1/' | nl -nrn -s'.' | column -x

Rich (BB code):
$ cat i | sed -E '1,36d;1144,$d;s/.{20}(.{22}).*/\1/;s/ *$//;s/(.* )([^ ]+)$/\2 \1/' | sort -uk1 | sed -E 's/^([^ ]+) (.*)/\2\1/;=' | sed -E 'N;s/\n/. /;s/^[^.]\./  &/;s/^[^.]{2}\./ &/' | column -x
 
Yes, I changed that.


Well, perhaps, with a little imagination?
These look like one-liners to me:
Rich (BB code):
$ cat i | sed -n 37,1143p | cut -c20-45 | sed -E 's/ *$//;s/(.* )([^ ]+)$/\2\1/' | sort -uk1 | sed -E 's/^([^ ]+)( .*)/\2\1/' | nl -nrn -s'.' | column -x

Rich (BB code):
$ cat i | sed -E '1,36d;1144,$d;s/.{20}(.{22}).*/\1/;s/ *$//;s/(.* )([^ ]+)$/\2 \1/' | sort -uk1 | gsed -E 's/^([^ ]+) (.*)/\2\1/;=' | sed -E 'N;s/\n/. /;s/^[^.]\./  &/;s/^[^.]{2}\./ &/' | column -x
That's what I'm talking about, kudos Erichans 👍 Much shorter, much smarter and far more elegant than my version.
Also, it works as true one liner with elinks -dump, no need for cat i.

Again, thanks 🙏 and congrats 🫡

P.S. Edit:
Apologies to OP zsolt for turning this thread, which was about sound devices order, into regex exercise in real /. style 😊
 
Back
Top