{"version":"https:\/\/jsonfeed.org\/version\/1","title":"maxhaesslein.de Notes JSON Feed","description":"Some notes on different things, so I don't forget them.","home_page_url":"https:\/\/www.maxhaesslein.de\/","feed_url":"https:\/\/www.maxhaesslein.de\/notes.json","items":[{"id":"https:\/\/www.maxhaesslein.de\/notes\/my-favorite-mft-lenses","url":"https:\/\/www.maxhaesslein.de\/notes\/my-favorite-mft-lenses","title":"My favorite (MFT) lenses","content_html":"<p>I use the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Olympus_PEN_E-P7\" target=\"_blank\" title=\"Olympus PEN E-P7\">Olympus PEN E-P7<\/a> as my everyday camera. Here are some of my favorite lenses for taking pictures:<\/p><h2 id=\"heading-7d549bb4-6e60-4224-bdeb-58dfed68f0ef\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-7d549bb4-6e60-4224-bdeb-58dfed68f0ef\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Laowa Argus 18mm f\/0.95<\/span>\n\n<\/h2>\n<p>By far my favorite lens is the <a href=\"https:\/\/www.venuslens.net\/product\/laowa-argus-18mm-f-0-95-mft-apo\/\" target=\"_blank\" title=\"Laowa Argus 18mm f\/0.95 MFT APO\">Laowa Argus 18mm f\/0.95 MFT APO<\/a>. With the MFT crop factor, it's comparable to a 36mm f\/1.9 full frame lens. The optical quality is stunning, especially for the price, but it is also one of the harder lenses to use because the focus is fully manual. Especially at f\/0.95 it's hard to nail the sharp area, which means I discard a lot of photos. It's a lens for when you have time, not if you need to nail a shot quickly. I rely heavily on the zoom function and focus peaking on the camera when using this lens, especially at f\/1.4 or f\/0.95. The bokeh is especially pleasing. Sometimes it feels a little bit like an anamorphic lens due to the barrel distortion. The lens has a switch mechanism to set the aperture ring to \"click\" or \"smooth\", so it's also suitable for video. The focusing ring is buttery smooth, and the aperture ring has a satisfying click (or is buttery smooth as well when you switch it to de-clicked mode). There is also a <a href=\"https:\/\/www.venuslens.net\/product\/laowa-argus-25mm-f-0-95-mft-apo\/\" target=\"_blank\" title=\"Laowa Argus 25mm f\/0.95 MFT APO\">25mm f\/0.95<\/a> version (full frame equivalent of 50mm f\/1.9) of this lens.<\/p><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/my-favorite-mft-lenses\/89fc57128f-1768414877\/7050615-800x-q80.jpg\" width=\"800\" height=\"533\" alt=\"\"><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/my-favorite-mft-lenses\/0f74f23c78-1768414965\/8190631-800x-q80.jpg\" width=\"800\" height=\"533\" alt=\"\"><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/my-favorite-mft-lenses\/97e4f04b90-1768414894\/7190132-800x-q80.jpg\" width=\"800\" height=\"533\" alt=\"\"><h2 id=\"heading-3b73e03e-5df3-453c-b2a8-46a2c58a8b50\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-3b73e03e-5df3-453c-b2a8-46a2c58a8b50\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Lumix 14mm f\/2.5 &amp; 20mm f\/1.7<\/span>\n\n<\/h2>\n<p>My everyday lens is the <a href=\"https:\/\/www.panasonic.com\/uk\/consumer\/cameras-camcorders\/lumix-camera-lenses\/lumix-g-lenses\/h-h014ae.html\" target=\"_blank\" title=\"LUMIX G 14mm f\/2.5 II Asph.\">LUMIX G 14mm f\/2.5 II Asph.<\/a> In combination with the E-P7, it makes for a very lightweight and compact camera with a 28mm f\/5 full-frame equivalent field of view and a decent autofocus.<\/p><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/my-favorite-mft-lenses\/98c2782432-1768414986\/8190818-800x-q80.jpg\" width=\"800\" height=\"533\" alt=\"\"><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/my-favorite-mft-lenses\/9b0556e674-1768415095\/8301454-800x-q80.jpg\" width=\"800\" height=\"533\" alt=\"\"><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/my-favorite-mft-lenses\/f820df8bc7-1768415130\/9130034-800x-q80.jpg\" width=\"800\" height=\"533\" alt=\"\"><p>I also use the <a href=\"https:\/\/www.panasonic.com\/uk\/consumer\/cameras-camcorders\/lumix-camera-lenses\/lumix-g-lenses\/h-h020e.html\" target=\"_blank\" title=\"LUMIX G 20mm f\/1.7 II Asph.\">LUMIX G 20mm f\/1.7 II Asph.<\/a>, which offers a 40mm f\/3.4 full-frame equivalent field of view with shallower depth of field. I carry it with me all the time, but the autofocus is much slower and it is a bit too \"zoomed in\" for my taste, so I don't use it that often.<\/p><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/my-favorite-mft-lenses\/68d49ff373-1768808915\/5200053-800x-q80.jpg\" width=\"800\" height=\"533\" alt=\"\"><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/my-favorite-mft-lenses\/515fceed7c-1768415178\/a270391-800x-q80.jpg\" width=\"800\" height=\"533\" alt=\"\"><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/my-favorite-mft-lenses\/645a2cec7c-1768809009\/6210076-800x-q80.jpg\" width=\"800\" height=\"533\" alt=\"\"><h2 id=\"heading-88977750-a263-4518-95bb-f8823167315c\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-88977750-a263-4518-95bb-f8823167315c\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Honorable Mentions<\/span>\n\n<\/h2>\n<p>The <a href=\"https:\/\/www.venuslens.net\/product\/laowa-7-5mm-f2\/\" target=\"_blank\">Laowa 7.5mm f\/2<\/a> brought back my joy for photography in 2020. All photos in my photo diary (except the iPhone shots) from <a href=\"https:\/\/www.maxhaesslein.de\/visual\/photography\/moments\/2020\">2020<\/a> and <a href=\"https:\/\/www.maxhaesslein.de\/visual\/photography\/moments\/2021\">2021<\/a> were taken with this lens. It has manual focus, but it is so wide that everything is sharp anyway if you set the focus point to 1m or more. There is now also a <a href=\"https:\/\/www.venuslens.net\/product\/laowa-6mm-f-2-zero-d-mft\/\" target=\"_blank\">6mm variant<\/a> for an even wider field of view. Nowadays I use this lens less, but it is so small I can take it with me without adding much extra weight.<\/p><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/my-favorite-mft-lenses\/6d3e14af3e-1773235541\/1060793-800x-q80.jpg\" width=\"800\" height=\"534\" alt=\"\"><p>Another great wide-angle lens is the <a href=\"https:\/\/www.panasonic.com\/uk\/consumer\/cameras-camcorders\/lumix-camera-lenses\/lumix-g-lenses\/h-x012e.html\" target=\"_blank\">Leica 12mm f\/1.4<\/a>. It's a super sharp lens with fast autofocus, but a bit on the heavier side, so I don't carry it that often. I probably should use it more often, though.<\/p><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/my-favorite-mft-lenses\/c0ec08094f-1773236361\/8180516-800x-q80.jpg\" width=\"800\" height=\"533\" alt=\"\">","date_published":"2026-03-11T15:06:00+00:00"},{"id":"https:\/\/www.maxhaesslein.de\/notes\/timemachine-backup-to-a-linux-server","url":"https:\/\/www.maxhaesslein.de\/notes\/timemachine-backup-to-a-linux-server","title":"TimeMachine Backup to a Linux Server","content_html":"<p>I tested these instructions on Ubuntu Server 22.04, but they should work anywhere you can install Samba.<\/p><p>Install Samba:<\/p><pre class=\"hljs\"><code data-language=\"bash\"><span class=\"hljs-code-line\">sudo apt install samba<\/span><\/code><\/pre>\n<p>Add a specific user for the TimeMachine backup. This user doesn't need a home directory, a Unix password or a login. After creating the user, add a password for Samba.<\/p><pre class=\"hljs\"><code data-language=\"bash\"><span class=\"hljs-code-line\">sudo adduser --no-create-home --disabled-password --disabled-login timemachine<\/span>\n<span class=\"hljs-code-line\">sudo smbpasswd -a timemachine<\/span><\/code><\/pre>\n<p>Edit the Samba config <code>\/etc\/samba\/smb.conf<\/code> and add the following:<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\"># add these lines for TimeMachine support:<\/span>\n<span class=\"hljs-code-line\">fruit:aapl = yes<\/span>\n<span class=\"hljs-code-line\">fruit:nfs_aces = no<\/span>\n<span class=\"hljs-code-line\">fruit:model = MacSamba<\/span>\n<span class=\"hljs-code-line\">server min protocol = SMB2_02<\/span>\n<span class=\"hljs-code-line\">server max protocol = SMB3<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"># we set multicast to 'no' and install Avahi instead<\/span>\n<span class=\"hljs-code-line\">multicast dns register = no<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"># add a share for TimeMachine:<\/span>\n<span class=\"hljs-code-line\">[TimeMachine]<\/span>\n<span class=\"hljs-code-line\">path = \/srv\/timemachine<\/span>\n<span class=\"hljs-code-line\">comment = TimeMachine Backup<\/span>\n<span class=\"hljs-code-line\">vfs objects = catia fruit streams_xattr<\/span>\n<span class=\"hljs-code-line\">fruit:time machine = yes<\/span>\n<span class=\"hljs-code-line\">fruit:time machine max size = 2000G<\/span>\n<span class=\"hljs-code-line\">valid users = timemachine<\/span>\n<span class=\"hljs-code-line\">writeable = yes<\/span>\n<span class=\"hljs-code-line\">browseable = yes<\/span>\n<span class=\"hljs-code-line\">guest ok = no<\/span><\/code><\/pre>\n<p>The <code>path<\/code> must point to an empty directory. The <code>fruit:time machine max size<\/code> should be at least double the size of your Mac disk.<\/p><p>Start Samba:<\/p><pre class=\"hljs\"><code data-language=\"bash\"><span class=\"hljs-code-line\">sudo systemctl start smbd.service<\/span>\n<span class=\"hljs-code-line\">sudo systemctl <span class=\"hljs-built_in\">enable<\/span> smbd.service<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># or, if you made changes, restart:<\/span><\/span>\n<span class=\"hljs-code-line\">sudo systemctl restart smbd.service<\/span><\/code><\/pre>\n<p>You may need to open the following ports in the firewall on your server for Samba to work correctly:<\/p><ul><li>137\/udp<\/li><li>138\/udp<\/li><li>139\/tcp<\/li><li>445\/tcp<\/li><\/ul><p>You can also install Avahi, so that your Mac automatically finds the backup volume:<\/p><pre class=\"hljs\"><code data-language=\"bash\"><span class=\"hljs-code-line\">sudo apt install avahi-daemon<\/span><\/code><\/pre>\n<p>Add the service file <code>\/etc\/avahi\/services\/samba.service<\/code><\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">&lt;?xml version=\"1.0\" standalone='no'?&gt;<\/span>\n<span class=\"hljs-code-line\">&lt;!DOCTYPE service-group SYSTEM \"avahi-service.dtd\"&gt;<\/span>\n<span class=\"hljs-code-line\">&lt;service-group&gt;<\/span>\n<span class=\"hljs-code-line\">  &lt;name replace-wildcards=\"yes\"&gt;%h&lt;\/name&gt;<\/span>\n<span class=\"hljs-code-line\">  &lt;service&gt;<\/span>\n<span class=\"hljs-code-line\">    &lt;type&gt;_smb._tcp&lt;\/type&gt;<\/span>\n<span class=\"hljs-code-line\">    &lt;port&gt;445&lt;\/port&gt;<\/span>\n<span class=\"hljs-code-line\">  &lt;\/service&gt;<\/span>\n<span class=\"hljs-code-line\">  &lt;service&gt;<\/span>\n<span class=\"hljs-code-line\">    &lt;type&gt;_device-info._tcp&lt;\/type&gt;<\/span>\n<span class=\"hljs-code-line\">    &lt;port&gt;0&lt;\/port&gt;<\/span>\n<span class=\"hljs-code-line\">    &lt;txt-record&gt;model=MacSamba&lt;\/txt-record&gt;<\/span>\n<span class=\"hljs-code-line\">  &lt;\/service&gt;<\/span>\n<span class=\"hljs-code-line\">  &lt;service&gt;<\/span>\n<span class=\"hljs-code-line\">    &lt;type&gt;_adisk._tcp&lt;\/type&gt;<\/span>\n<span class=\"hljs-code-line\">    &lt;port&gt;9&lt;\/port&gt;<\/span>\n<span class=\"hljs-code-line\">    &lt;txt-record&gt;dk0=adVN=TimeMachine,adVF=0x82&lt;\/txt-record&gt;<\/span>\n<span class=\"hljs-code-line\">    &lt;txt-record&gt;sys=waMa=0,adVF=0x100&lt;\/txt-record&gt;<\/span>\n<span class=\"hljs-code-line\">  &lt;\/service&gt;<\/span>\n<span class=\"hljs-code-line\">&lt;\/service-group&gt;<\/span><\/code><\/pre>\n<p>Restart Avahi:<\/p><pre class=\"hljs\"><code data-language=\"bash\"><span class=\"hljs-code-line\">sudo systemctl restart avahi-daemon<\/span><\/code><\/pre>\n<p>You may need to additionally open a port for Avahi:<\/p><ul><li>5353\/udp<\/li><\/ul><p>Now you can add this network share as a new backup volume in your TimeMachine settings. It should appear automatically in the available disks if you are in the same network as your server.<\/p>","date_published":"2026-02-27T13:37:00+00:00"},{"id":"https:\/\/www.maxhaesslein.de\/notes\/shadowdark-player-driven-death-mechanics","url":"https:\/\/www.maxhaesslein.de\/notes\/shadowdark-player-driven-death-mechanics","title":"Shadowdark - Player-Driven Death Mechanics","content_html":"<p>I love that the <a href=\"https:\/\/www.thearcanelibrary.com\/pages\/shadowdark\" target=\"_blank\" title=\"Shadowdark RPG\">Shadowdark RPG<\/a> is deadly and character creation is fast; players can rejoin almost instantly. But sometimes you want to follow a character's story longer or give them one last heroic moment before death takes them. So here are some alternative death rules:<\/p><h2 id=\"heading-79e6b227-30a9-4bd6-a0a0-566fcc146786\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-79e6b227-30a9-4bd6-a0a0-566fcc146786\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Player-Driven Death Mechanics<\/span>\n\n<\/h2>\n<p>Optional death rules that give some agency to the players, adapted from the <a href=\"https:\/\/daggerheartsrd.com\/rules\/death\/\" target=\"_blank\" title=\"Daggerheart Death Rules\">Daggerheart RPG<\/a>.<\/p><p>Replaces death rules (p. 89 core rulebook). No death timer or stabilization.<\/p><p>At 0 HP, choose one of the following options. All rolls are flat (no ADV\/DISADV, modifiers or luck). If your max HP or any of your stats is reduced to 0, you die instantly.<\/p><p><strong>Unyielding End.<\/strong> Take one final action, then die. This action is always a critical success.<\/p><p><strong>Fate's Coin.<\/strong> Flip a coin. Negative: die. Positive: instantly regain HP (lvl 1-4: 1d4, lvl 5-8: 1d6, lvl 9+: 1d8).<\/p><p><strong>Endure.<\/strong> Fall unconscious until healed. Roll on the<em> Close Call<\/em> table. The<em> Pit Fighter<\/em> class (CS2) gets<em> +1<\/em> to this roll.<\/p><hr \/>\n<p><strong>Close Call<\/strong> (1d12)<\/p><p>1:&nbsp;<strong>Lop.<\/strong>&nbsp;You lose a limb. 1d4:&nbsp;<strong>1.<\/strong>&nbsp;Leg,&nbsp;<strong>2.<\/strong>&nbsp;Arm,&nbsp;<strong>3.<\/strong>&nbsp;Foot,&nbsp;<strong>4.<\/strong>&nbsp;Hand<br>2:&nbsp;<strong>Blinded.<\/strong>&nbsp;You lose an eye (DISADV on sight-based checks)<br>3:&nbsp;<strong>Torn Muscle.<\/strong>&nbsp;You permanently lose 1 point of STR<br>4:&nbsp;<strong>Nerve Damage.<\/strong>&nbsp;You permanently lose 1 point of DEX<br>5:&nbsp;<strong>Organ Damage.<\/strong>&nbsp;You permanently lose 1 point of CON<br>6:&nbsp;<strong>Brain Damage.<\/strong>&nbsp;You permanently lose 1 point of INT<br>7:&nbsp;<strong>Trauma.<\/strong>&nbsp;You permanently lose 1 point of WIS<br>8:&nbsp;<strong>Scars.<\/strong>&nbsp;You permanently lose 1 point of CHA<br>9:&nbsp;<strong>Festering Wound.<\/strong>&nbsp;You permanently lose 1 HP<br>10:&nbsp;<strong>Shattered.<\/strong>&nbsp;One random piece of gear you wield is destroyed<br>11:&nbsp;<strong>Stupor.<\/strong>&nbsp;You stay unconscious<br>12:&nbsp;<strong>Unbroken.<\/strong>&nbsp;You instantly regain 1 HP<\/p><p>(inspired by the <em>Enduring Wounds<\/em> table in CS2)<\/p><h2 id=\"heading-901d191d-07fb-4652-a3de-cd23e7a810b8\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-901d191d-07fb-4652-a3de-cd23e7a810b8\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Download<\/span>\n\n<\/h2>\n<p><a download href=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/shadowdark-player-driven-death-mechanics\/41f65ef6fd-1762968571\/shadowdark_player-driven-death-mechanics_v.1.0.pdf\">shadowdark_player-driven-death-mechanics_v.1.0.pdf<\/a><\/p>","date_published":"2025-11-12T19:00:00+00:00"},{"id":"https:\/\/www.maxhaesslein.de\/notes\/using-syncthing-to-back-up-photos-from-an-android-phone","url":"https:\/\/www.maxhaesslein.de\/notes\/using-syncthing-to-back-up-photos-from-an-android-phone","title":"Using Syncthing to back up photos from an Android phone","content_html":"<p>There is a <a href=\"https:\/\/syncthing.net\/\" target=\"_blank\">Syncthing<\/a> client for Android phones, which can automatically sync the <code>DCIM<\/code> folder (or any folder) from an Android phone to a server. The problem is, that Syncthing works both ways: when you rename, move or delete an image on the server, it will also be renamed, moved or deleted on the phone. Or, if you delete old photos on your phone, because you need more space, those files will also be deleted on the server.<\/p><p>This describes a setup, where we use an intermediate folder to sync the photos from the mobile phone to the server, and then use a small script to copy new files to another folder. Thus, the images in the new folder are completely independent from the intermediate folder (and thus from the mobile phone).<\/p><p>You could also add some lines to the script, to automatically delete images that are older than a few month from your mobile phone, by deleting them from the intermediate sync folder.<\/p><p>You need syncthing running on your mobile phone. Unfortunatelly, there is no good solution for iPhones, because Apple.<\/p><p>On the server, there are two folders, <code>\/srv\/phone-sync\/<\/code> and <code>\/srv\/phone-images\/<\/code>. We then install syncthing on the server, and sync the <code>DCIM<\/code> folder on the phone with the <code>\/srv\/phone-sync\/<\/code> folder on the server. See the <a href=\"https:\/\/docs.syncthing.net\/\" target=\"_blank\">syncthing documentation<\/a> for this.<\/p><p>To sync the new files from one folder into the other, a small bash script helps; you could save it at <code>~\/bin\/copy-photos.sh<\/code>:<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">#!\/bin\/bash<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">SOURCEFOLDER=\"\/srv\/phone-sync\/\"   # with trailing slash<\/span>\n<span class=\"hljs-code-line\">TARGETFOLDER=\"\/srv\/phone-images\/\" # with trailing slash<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"># make sure we are not running already:<\/span>\n<span class=\"hljs-code-line\">if [[ `pgrep -f $0` != \"$$\" ]]; then<\/span>\n<span class=\"hljs-code-line\">\techo \"Another instance of this script is already running! Exiting\"<\/span>\n<span class=\"hljs-code-line\">\texit 0<\/span>\n<span class=\"hljs-code-line\">fi<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">echo \"copying ..\"<\/span>\n<span class=\"hljs-code-line\">rsync --times --exclude '.*' \"$SOURCEFOLDER\"* \"$TARGETFOLDER\"<\/span>\n<span class=\"hljs-code-line\">exit 0<\/span><\/code><\/pre>\n<p>we then need to make sure we can start this script with <code>chmod +x ~\/bin\/copy-photos.sh<\/code><\/p><p>This script calls <code>rsync<\/code>. It excludes all files starting with a dot (while syncing, Syncthing first creates a hidden <code>.pending-*<\/code> file before renaming it to the correct filename; we don't want to sync these <code>.pending<\/code> files, or other files that are hidden, like <code>.trashed*<\/code> files, that may be trashed photos on the mobile phone). It also and keeps the timestamp (<code>--times<\/code>) while syncing. You could also add some more flags to rsync, for example <code>--user=<\/code> and <code>--group=<\/code> if you want to set a specific user\/group combination, or <code>--chmod=<\/code> if you want to set specific file permissions. Pay attention when using the <code>-a<\/code> flag, because this will delete files in the target directory that are not present in the source directory, so use <code>-rlptgo<\/code> instead.<\/p><h2 id=\"heading-524cdafb-dbd5-4b8e-99fc-6fb9a6e6d8f4\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-524cdafb-dbd5-4b8e-99fc-6fb9a6e6d8f4\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Automatically Copy New Photos<\/span>\n\n<\/h2>\n<p>I wanted to automate the copying as soon as new photos are synced via syncthing. I tried out two solutions that kind of worked: inotifywait and Syncthing Folder Event Daemon (both solutions described below). However, I could not get them to run reliably; after a few hours or days it stopped working until I restarted the script, or used <code>systemctl<\/code> to look at the output (at which point systemd notices that the script is no longer running, and restarts it automatically). So, I ended up with a simple cronjob that runs every night and calls the <code>copy-photos.sh<\/code> script, which is good enough for my use case.<\/p><p>I've documented my previous attempts below, but I no longer recommend them:<\/p><h3 id=\"heading-83278ee5-b7c7-4599-a3bb-22e1f1a3109d\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-83278ee5-b7c7-4599-a3bb-22e1f1a3109d\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Old Solution: Syncthing Folder Event Daemon<\/span>\n\n<\/h3>\n<p>In the past, I used the <code>Syncthing Folder Event Daemon<\/code> by desbma to watch the synced folder for changes. But I could not get it to run reliably, so <strong>I no longer recommend the following setup<\/strong>, but still leave it here for documentation purposes:<\/p><p>desbma wrote a small daemon called <a href=\"https:\/\/github.com\/desbma\/stfed\" target=\"_blank\">Syncthing Folder Event Daemon<\/a>, or <code>stfed<\/code>, to run alongside Syncthing and running custom commands when certain folder events happen. This is perfect for our usecase.<\/p><p>See their <a href=\"https:\/\/github.com\/desbma\/stfed\" target=\"_blank\">GitHub page<\/a> for installation instructions. I write them down here, but they may have changed in the meantime:<\/p><pre class=\"hljs\"><code data-language=\"bash\"><span class=\"hljs-code-line\"><span class=\"hljs-comment\"># you need a Rust build environment:<\/span><\/span>\n<span class=\"hljs-code-line\">sudo apt install build-essential <span class=\"hljs-comment\"># required for some rust crates<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># this uses https:\/\/rustup.rs for the installation:<\/span><\/span>\n<span class=\"hljs-code-line\">curl --proto <span class=\"hljs-string\">'=https'<\/span> --tlsv1.2 -sSf https:\/\/sh.rustup.rs | sh<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># logout and login again, so that the PATH variable gets updated<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># download and unzip the code:<\/span><\/span>\n<span class=\"hljs-code-line\">wget https:\/\/github.com\/desbma\/stfed\/archive\/refs\/heads\/master.zip<\/span>\n<span class=\"hljs-code-line\">unzip master.zip<\/span>\n<span class=\"hljs-code-line\">rm master.zip<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-built_in\">cd<\/span> stfed-master\/<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># build and install stfed:<\/span><\/span>\n<span class=\"hljs-code-line\">cargo build --release<\/span>\n<span class=\"hljs-code-line\">sudo install -Dm 755 -t \/usr\/<span class=\"hljs-built_in\">local<\/span>\/bin target\/release\/stfed<\/span>\n<span class=\"hljs-code-line\">sudo install -D -t ~\/.config\/systemd\/user .\/systemd\/stfed.service<\/span><\/code><\/pre>\n<p>The config file should be put into <code>~\/.config\/stfed\/hooks.toml<\/code> and looks something like this:<\/p><pre class=\"hljs\"><code data-language=\"yaml\"><span class=\"hljs-code-line\"><span class=\"hljs-string\">[[hooks]]<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># Syncthing folder path<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-string\">folder<\/span> <span class=\"hljs-string\">=<\/span> <span class=\"hljs-string\">\"\/srv\/phone-sync\"<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># Event type, one of:<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># file_down_sync_done: triggers when a file has been fully synchronized locally (see filter to match for a specific file)<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># folder_down_sync_done: triggers when a folder has been fully synchronized locally<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># file_conflict: triggers when Syncthing creates a .stconflict file due to a synchronization conflict<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-string\">event<\/span> <span class=\"hljs-string\">=<\/span> <span class=\"hljs-string\">\"file_down_sync_done\"<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># glob rule for specific file matching for file_down_sync_done events: only include files that do not start with a dot<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-string\">filter<\/span> <span class=\"hljs-string\">=<\/span> <span class=\"hljs-string\">\"[^.]*\"<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># command to run when event triggers<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-string\">command<\/span> <span class=\"hljs-string\">=<\/span> <span class=\"hljs-string\">\"~\/bin\/copy-photos.sh\"<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># Whether to allow several commands for the same hook to run simultaneously<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># if false, and a burst of events comes, the commands will be skipped while the previous one is still running<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># optional, defaults to false<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-string\">allow_concurrent<\/span> <span class=\"hljs-string\">=<\/span> <span class=\"hljs-literal\">false<\/span><\/span><\/code><\/pre>\n<p>You can then start stfed and see, if everything works:<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">\/usr\/local\/bin\/stfed<\/span><\/code><\/pre>\n<p>It should find the syncthing config by itself. If it doesn't, you may need another config file, <code>~\/.config\/stfed\/config.toml<\/code> with the syncthing configuration:<\/p><pre class=\"hljs\"><code data-language=\"yaml\"><span class=\"hljs-code-line\"><span class=\"hljs-string\">url<\/span> <span class=\"hljs-string\">=<\/span> <span class=\"hljs-string\">\"http:\/\/127.0.0.1:8384\/\"<\/span>  <span class=\"hljs-comment\"># Syncthing URL<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-string\">api_key<\/span> <span class=\"hljs-string\">=<\/span> <span class=\"hljs-string\">\"xyz\"<\/span>  <span class=\"hljs-comment\"># Syncthing API key<\/span><\/span><\/code><\/pre>\n<p>You can find the API Key in the web ui. See the <a href=\"https:\/\/docs.syncthing.net\/dev\/rest.html#api-key\" target=\"_blank\">Syncthing docs<\/a> for details.<\/p><p>When everything runs correctly, you can add a service to automatically start stfed (and restart it on failure):<\/p><pre class=\"hljs\"><code data-language=\"bash\"><span class=\"hljs-code-line\">systemctl --user daemon-reload<\/span>\n<span class=\"hljs-code-line\">systemctl --user <span class=\"hljs-built_in\">enable<\/span> --now stfed.service<\/span>\n<span class=\"hljs-code-line\">systemctl --user status stfed.service<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># if you ever change the config files, restart the service:<\/span><\/span>\n<span class=\"hljs-code-line\">systemctl --user restart stfed.service<\/span><\/code><\/pre>\n<h3 id=\"heading-dad97ec5-904d-4766-ba09-ef535c16a63f\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-dad97ec5-904d-4766-ba09-ef535c16a63f\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Even older Solution: inotifywait<\/span>\n\n<\/h3>\n<p>In the past, I used <code>inotifywait<\/code> to watch the synced folder for changes. But I could not get it to run reliably, so <strong>I no longer recommend the following setup<\/strong>, but still leave it here for documentation purposes:<\/p><p>To watch the <code>phone-sync<\/code> folder, we use <code>inotifywait<\/code>. If you are on Ubuntu, you can install it via <code>sudo apt install inotify-tools<\/code>, if it isn't installed already.<\/p><p>To make our lifes a bit easier, we create a bash script, for example in <code>~\/bin\/watch-images.sh<\/code> with this content:<\/p><pre class=\"hljs\"><code data-language=\"bash\"><span class=\"hljs-code-line\"><span class=\"hljs-meta\">#!\/bin\/bash<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># config<\/span><\/span>\n<span class=\"hljs-code-line\">WATCHFOLDER=<span class=\"hljs-string\">\"\/srv\/phone-sync\/\"<\/span>    <span class=\"hljs-comment\"># with trailing slash<\/span><\/span>\n<span class=\"hljs-code-line\">TARGETFOLDER=<span class=\"hljs-string\">\"\/srv\/phone-images\/\"<\/span> <span class=\"hljs-comment\"># with trailing slash<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># make sure we are not running already:<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">if<\/span> [[ `pgrep -f <span class=\"hljs-variable\">$0<\/span>` != <span class=\"hljs-string\">\"$$\"<\/span> ]]; <span class=\"hljs-keyword\">then<\/span><\/span>\n<span class=\"hljs-code-line\">\t<span class=\"hljs-built_in\">echo<\/span> <span class=\"hljs-string\">\"Another instance of this script is already running! Exiting\"<\/span><\/span>\n<span class=\"hljs-code-line\">\t<span class=\"hljs-built_in\">exit<\/span> 0<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">fi<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-built_in\">echo<\/span> <span class=\"hljs-string\">\"starting ...\"<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">inotifywait --monitor --event moved_to --exclude <span class=\"hljs-string\">'\\.\/.*'<\/span> <span class=\"hljs-string\">\"<span class=\"hljs-variable\">$WATCHFOLDER<\/span>\"<\/span> | <span class=\"hljs-keyword\">while<\/span> <span class=\"hljs-built_in\">read<\/span> line; <span class=\"hljs-keyword\">do<\/span><\/span>\n<span class=\"hljs-code-line\">\t<span class=\"hljs-built_in\">echo<\/span> <span class=\"hljs-string\">\"new file was created: <span class=\"hljs-variable\">$line<\/span>\"<\/span><\/span>\n<span class=\"hljs-code-line\">\trsync --<span class=\"hljs-built_in\">times<\/span> --exclude <span class=\"hljs-string\">'.*'<\/span> <span class=\"hljs-string\">\"<span class=\"hljs-variable\">$WATCHFOLDER<\/span>\"<\/span>* <span class=\"hljs-string\">\"<span class=\"hljs-variable\">$TARGETFOLDER<\/span>\"<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">done<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-built_in\">echo<\/span> <span class=\"hljs-string\">\"exiting ...\"<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-built_in\">exit<\/span> 1<\/span><\/code><\/pre>\n<p>we then need to make sure we can start this script with <code>chmod +x ~\/bin\/watch-images.sh<\/code><\/p><p>In the script, we first define our two folders, then we make sure that the script isn't already running (if it is, we exit). We then call <code>inotifywait<\/code> and listen to <code>MOVED_TO<\/code> events. Syncthing works by syncing the file to a hidden file first (<code>.pending-*<\/code>), and after the sync is complete it renames the hidden file to the correct filename. This renaming triggers the <code>MOVED_TO<\/code> event. We also exclude all files starting with <code>.<\/code> from being watched (<code>--exclude '\\.\/.*'<\/code>). We use the <code>--monitor<\/code> flag to run forever. If a <code>MOVED_TO<\/code> event is detected, we call <code>rsync<\/code> (for every event that is detected). <code>rsync<\/code> also excludes all files starting with a dot, and keeps the time (<code>--times<\/code>) while syncing. You could also add some more flags to rsync, for example <code>--user=<\/code> and <code>--group=<\/code> if you want to set a specific user\/group combination, or <code>--chmod=<\/code> if you want to set specific file permissions. Pay attention when using the <code>-a<\/code> flag, because this will delete files in the target directory that are not present in the source directory, so use <code>-rlptgo<\/code> instead.<\/p><p>You can now start this script manually and test it. If we want to autostart the script after a reboot (or on failure), we can use systemd (if you are on Ubuntu). Create a new file <code>~\/.config\/systemd\/user\/watch-images.service<\/code> (if the directory does not exist, create it), with the following content:<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">[Unit]<\/span>\n<span class=\"hljs-code-line\">Description=sync mobile phone images<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">[Service] <\/span>\n<span class=\"hljs-code-line\">ExecStart=%h\/bin\/watch-images.sh<\/span>\n<span class=\"hljs-code-line\">Type=Simple<\/span>\n<span class=\"hljs-code-line\">Restart=always<\/span>\n<span class=\"hljs-code-line\">RestartSec=5s<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">[Install]<\/span>\n<span class=\"hljs-code-line\">WantedBy=default.target<\/span><\/code><\/pre>\n<p>you can then enable and start this service with:<\/p><pre class=\"hljs\"><code data-language=\"bash\"><span class=\"hljs-code-line\">systemctl --user daemon-reload<\/span>\n<span class=\"hljs-code-line\">systemctl --user <span class=\"hljs-built_in\">enable<\/span> watch-images.service<\/span>\n<span class=\"hljs-code-line\">systemctl --user start watch-images.service<\/span>\n<span class=\"hljs-code-line\">systemctl --user status watch-images.service<\/span><\/code><\/pre>\n","date_published":"2024-01-26T15:20:00+00:00","date_modified":"2024-03-06T15:00:00+00:00"},{"id":"https:\/\/www.maxhaesslein.de\/notes\/real-time-beat-prediction-with-aubio","url":"https:\/\/www.maxhaesslein.de\/notes\/real-time-beat-prediction-with-aubio","title":"Real Time Beat Prediction with Aubio","content_html":"<p><a href=\"https:\/\/aubio.org\/\" target=\"_blank\" rel=\"noreferrer\">Aubio<\/a> is a great tool for extracting audio information from music in real time. It is written in C, so it runs really fast, but has also a Python interface that is easy to use with Python scripts. We can use it, together with <a href=\"https:\/\/people.csail.mit.edu\/hubert\/pyaudio\/\" target=\"_blank\" rel=\"noreferrer\">PyAudio<\/a>, to extract live audio information from a microphone, and then predict the moment of the next beat.<\/p><p>The following code is tested on a Raspberry Pi, with a fresh installation of PiOS Bullseye.<\/p><p>First, we need to install Aubio and PyAudio:<\/p><pre class=\"hljs\"><code data-language=\"bash\"><span class=\"hljs-code-line\">sudo apt install python3-pyaudio python3-aubio<\/span><\/code><\/pre>\n<p>Then we can create a Python script:<\/p><pre class=\"hljs\"><code data-language=\"python\"><span class=\"hljs-code-line\"><span class=\"hljs-comment\">#!\/usr\/bin\/python<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">import<\/span> pyaudio<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">import<\/span> aubio<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">import<\/span> numpy <span class=\"hljs-keyword\">as<\/span> np<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">from<\/span> time <span class=\"hljs-keyword\">import<\/span> sleep<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">seconds = <span class=\"hljs-number\">10<\/span> <span class=\"hljs-comment\"># how long this script should run<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">bufferSize = <span class=\"hljs-number\">512<\/span><\/span>\n<span class=\"hljs-code-line\">windowSizeMultiple = <span class=\"hljs-number\">2<\/span> <span class=\"hljs-comment\"># or 4 for higher accuracy, but more computational cost<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">audioInputDeviceIndex = <span class=\"hljs-number\">1<\/span> <span class=\"hljs-comment\"># use 'arecord -l' to check available audio devices<\/span><\/span>\n<span class=\"hljs-code-line\">audioInputChannels = <span class=\"hljs-number\">1<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># create the aubio tempo detection:<\/span><\/span>\n<span class=\"hljs-code-line\">hopSize = bufferSize<\/span>\n<span class=\"hljs-code-line\">winSize = hopSize * windowSizeMultiple<\/span>\n<span class=\"hljs-code-line\">tempoDetection = aubio.tempo(method=<span class=\"hljs-string\">'default'<\/span>, buf_size=winSize, hop_size=hopSize, samplerate=audioInputSampleRate)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># this function gets called by the input stream, as soon as enough samples are collected from the audio input:<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">readAudioFrames<\/span><span class=\"hljs-params\">(in_data, frame_count, time_info, status)<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    signal = np.frombuffer(in_data, dtype=np.float32)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    beat = tempoDetection(signal)<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> beat:<\/span>\n<span class=\"hljs-code-line\">        bpm = tempoDetection.get_bpm()<\/span>\n<span class=\"hljs-code-line\">        print(<span class=\"hljs-string\">\"beat! (running with \"<\/span>+str(bpm)+<span class=\"hljs-string\">\" bpm)\"<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">return<\/span> (in_data, pyaudio.paContinue)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># create and start the input stream<\/span><\/span>\n<span class=\"hljs-code-line\">pa = pyaudio.PyAudio()<\/span>\n<span class=\"hljs-code-line\">audioInputDevice = pa.get_device_info_by_index(audioInputDeviceIndex)<\/span>\n<span class=\"hljs-code-line\">audioInputSampleRate = int(audioInputDevice[<span class=\"hljs-string\">'defaultSampleRate'<\/span>])<\/span>\n<span class=\"hljs-code-line\">inputStream = pa.open(format=pyaudio.paFloat32,<\/span>\n<span class=\"hljs-code-line\">                input=<span class=\"hljs-literal\">True<\/span>,<\/span>\n<span class=\"hljs-code-line\">                channels=audioInputChannels,<\/span>\n<span class=\"hljs-code-line\">                input_device_index=audioInputDeviceIndex,<\/span>\n<span class=\"hljs-code-line\">                frames_per_buffer=bufferSize,<\/span>\n<span class=\"hljs-code-line\">                rate=audioInputSampleRate,<\/span>\n<span class=\"hljs-code-line\">                stream_callback=readAudioFrames)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># because the input stream runs asynchronously, we just wait for a few seconds here before stopping the script:<\/span><\/span>\n<span class=\"hljs-code-line\">sleep(seconds)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">inputStream.stop_stream()<\/span>\n<span class=\"hljs-code-line\">inputStream.close()<\/span>\n<span class=\"hljs-code-line\">pa.terminate()<\/span><\/code><\/pre>\n<p>First, we create an <code>inputStream<\/code> with a <code>stream_callback<\/code>. This callback function will automatically be called by PyAudio, as soon as it has a <code>bufferSize<\/code> worth samples.<\/p><p>We then create a <code>tempoDetection<\/code> object with aubio. Every time, the <code>stream_callback<\/code> from PyAudio is called, we pass the <code>in_data<\/code> to the <code>tempoDetection<\/code> and get back if there is (or should be) a <code>beat<\/code> in this batch of samples.<\/p><p>If there is a <code>beat<\/code>, we also check what the current estimated <code>bpm<\/code> value is. Because of the nature of the used algorithm, the <code>bpm<\/code> value may be half or double of the real bpm (the algorithm prefers measurements around 107bpm, with a minimum of 40 and a maximum of 250 bpm, so for example 80 bpm is favored over 160 bpm).<\/p><p><code>hopSize<\/code> is the number of samples per iteration that we feed into the beat detection algorithm. <code>winSize<\/code> is double or quadruple the size of the size of <code>hopSize<\/code>. With a source of 44100 Hz, a <code>hopSize<\/code> of <code>512<\/code> audio samples works great.<\/p><p>A great video about how beat detection algorithms works under the hood is \"<a href=\"https:\/\/youtu.be\/FmwpkdcAXl0\" target=\"_blank\" rel=\"noreferrer\">Tempo and Beat Tracking<\/a>\" by AudioLabsErlangen, and also the PhD dissertation of Paul M. Brossier, <a href=\"https:\/\/aubio.org\/phd\/\" target=\"_blank\" rel=\"noreferrer\">Automatic Annotation of Musical Audio for Interactive Applications<\/a> (which is the basis for Aubio).<\/p><p>We can also expand that script, so that we can run at a fixed framerate:<\/p><pre class=\"hljs\"><code data-language=\"python\"><span class=\"hljs-code-line\"><span class=\"hljs-comment\">#!\/usr\/bin\/python<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">import<\/span> pyaudio<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">import<\/span> aubio<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">import<\/span> numpy <span class=\"hljs-keyword\">as<\/span> np<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">import<\/span> time<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">from<\/span> queue <span class=\"hljs-keyword\">import<\/span> Queue<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">frameRate = <span class=\"hljs-number\">30<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">bufferSize = <span class=\"hljs-number\">512<\/span><\/span>\n<span class=\"hljs-code-line\">windowSizeMultiple = <span class=\"hljs-number\">2<\/span> <span class=\"hljs-comment\"># or 4 for higher accuracy, but more computational cost<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">audioInputDeviceIndex = <span class=\"hljs-number\">1<\/span> <span class=\"hljs-comment\"># use 'arecord -l' to check available audio devices<\/span><\/span>\n<span class=\"hljs-code-line\">audioInputChannels = <span class=\"hljs-number\">1<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># create the audio object and get some device information<\/span><\/span>\n<span class=\"hljs-code-line\">pa = pyaudio.PyAudio()<\/span>\n<span class=\"hljs-code-line\">audioInputDevice = pa.get_device_info_by_index(audioInputDeviceIndex)<\/span>\n<span class=\"hljs-code-line\">audioInputSampleRate = int(audioInputDevice[<span class=\"hljs-string\">'defaultSampleRate'<\/span>])<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># create the aubio tempo detection:<\/span><\/span>\n<span class=\"hljs-code-line\">hopSize = bufferSize<\/span>\n<span class=\"hljs-code-line\">winSize = hopSize * windowSizeMultiple<\/span>\n<span class=\"hljs-code-line\">tempoDetection = aubio.tempo(method=<span class=\"hljs-string\">'default'<\/span>, buf_size=winSize, hop_size=hopSize, samplerate=audioInputSampleRate)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># create a Queue, to handle multithreaded message exchange:<\/span><\/span>\n<span class=\"hljs-code-line\">beatQueue = Queue()<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># this function gets called by the input stream, as soon as enough samples are collected from the audio input:<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">readAudioFrames<\/span><span class=\"hljs-params\">(in_data, frame_count, time_info, status)<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    signal = np.frombuffer(in_data, dtype=np.float32)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    beat = tempoDetection(signal)<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> beat:<\/span>\n<span class=\"hljs-code-line\">        bpm = tempoDetection.get_bpm()<\/span>\n<span class=\"hljs-code-line\">        beatQueue.put(bpm)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">return<\/span> (in_data, pyaudio.paContinue)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># start the input stream<\/span><\/span>\n<span class=\"hljs-code-line\">inputStream = pa.open(format=pyaudio.paFloat32,<\/span>\n<span class=\"hljs-code-line\">                input=<span class=\"hljs-literal\">True<\/span>,<\/span>\n<span class=\"hljs-code-line\">                channels=audioInputChannels,<\/span>\n<span class=\"hljs-code-line\">                input_device_index=audioInputDeviceIndex,<\/span>\n<span class=\"hljs-code-line\">                frames_per_buffer=bufferSize,<\/span>\n<span class=\"hljs-code-line\">                rate=audioInputSampleRate,<\/span>\n<span class=\"hljs-code-line\">                stream_callback=readAudioFrames)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">targetTime = <span class=\"hljs-number\">1.<\/span>\/frameRate<\/span>\n<span class=\"hljs-code-line\">running = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\">bpm = <span class=\"hljs-number\">-1<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">while<\/span> running:<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">try<\/span>:<\/span>\n<span class=\"hljs-code-line\">        <\/span>\n<span class=\"hljs-code-line\">        startTime = time.time()<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        beat = <span class=\"hljs-literal\">False<\/span><\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> beatQueue.qsize() &gt; <span class=\"hljs-number\">0<\/span>:<\/span>\n<span class=\"hljs-code-line\">            bpm = beatQueue.get()<\/span>\n<span class=\"hljs-code-line\">            beat = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-comment\"># do something with the beat or bpm here ..<\/span><\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> beat:<\/span>\n<span class=\"hljs-code-line\">            print(<span class=\"hljs-string\">'beat detected; running with '<\/span>+str(bpm)+<span class=\"hljs-string\">' bpm'<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        usedTime = time.time() - startTime<\/span>\n<span class=\"hljs-code-line\">        sleepTime = targetTime-usedTime<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> sleepTime &lt; <span class=\"hljs-number\">1<\/span>:<\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-comment\"># print('fps not reached')<\/span><\/span>\n<span class=\"hljs-code-line\">            sleepTime = <span class=\"hljs-number\">1<\/span><\/span>\n<span class=\"hljs-code-line\">        time.sleep(sleepTime)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">except<\/span> KeyboardInterrupt:<\/span>\n<span class=\"hljs-code-line\">        running = <span class=\"hljs-literal\">False<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">inputStream.stop_stream()<\/span>\n<span class=\"hljs-code-line\">inputStream.close()<\/span>\n<span class=\"hljs-code-line\">pa.terminate()<\/span><\/code><\/pre>\n<p>This lets us run the beat detection in a seperate thread in the background, and use a <code>Queue<\/code> to store detected beats (and the estimated bpm at that time). The main thread (inside the <code>while<\/code> loop) can then run with a fixed framerate, and retreive the detected beats (and bpm values) as close to a frame as possible. With a <code>hopSize<\/code> of <code>512<\/code> and a <code>windowSize<\/code> of <code>1024<\/code> (double the <code>hopSize<\/code>), the audio thread should run with about 178 fps in the background.<\/p>","date_published":"2023-08-18T14:06:00+00:00","date_modified":"2023-10-05T16:17:00+00:00"},{"id":"https:\/\/www.maxhaesslein.de\/notes\/performant-images-on-the-web","url":"https:\/\/www.maxhaesslein.de\/notes\/performant-images-on-the-web","title":"Performant Images on the Web","content_html":"<p>Back in 2001 when I started building websites, the Internet was very, very slow \u2013 so, when you wanted to show images, you had to make them really small, and compress them a lot, or the user would have to wait for a long time. I learned a lot about efficient image delivery back then, and a those things are still true. But we also have some newer tools to help us making websites fast. Here are some of the things I learned in the last 20+ years; some of them basics, some of them very niche:<\/p><h2 id=\"heading-c5d33d03-19d2-4a66-983e-d9449fe5fd6e\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-c5d33d03-19d2-4a66-983e-d9449fe5fd6e\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">When in Doubt, Use jpeg<\/span>\n\n<\/h2>\n<p>The best format when you don't want to think about is, is definetly .jpg. So, just use .jpg for your files.<\/p><p>If you have illustrations, especially with a low color count, you could also use .png - this results in less artefacts, higher quality, and (depending on the content) a smaller file size. Another alternative, for vector files, is .svg, but make sure to include width and height attributes, because sometimes the dimensions of a svg are not defined and the browser has no idea how large the image is.<\/p><p>When using jpg, use a compression rate of 50% - 80% (where 100% is lossless, and 0% is very very small and very very ugly). The compression rate depends on the image, the dimensions and the use case, you may need to experiment a bit. A good starting point is 65%. Generally said, images with larger dimensions can be compressed more than images with smaller dimensions.<\/p><p>When using .png, you can reduce the number of colors or set the colorpalette to greyscale to save some space. When you need transparency though, use PNG-24, because the color-reduced PNG-8 only supports full transparency or no transparency, no values between (like GIF).<\/p><h2 id=\"heading-2d5ea882-9cf2-47e4-8a42-2328642e9dc2\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-2d5ea882-9cf2-47e4-8a42-2328642e9dc2\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Always Add width and height Attributes<\/span>\n\n<\/h2>\n<p>You should always add <code>width=\"\"<\/code> and <code>height=\"<\/code> attributes to the <code>&lt;img&gt;<\/code> tag, even if the image gets scaled to another size (via css or inline styles), because then the browser knows the original size of the image before it is loaded, and can lay out the space the image will occupy, once it is loaded. This helps imensly with preventing layout shifts.<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">&lt;img src=\"my-beautiful-image.jpg\" width=\"600\" height=\"400\" alt=\"This is a beautiful image\"&gt;<\/span><\/code><\/pre>\n<p>You can specify the dimensions you want the image to have via CSS. I mostly use something like this:<\/p><pre class=\"hljs\"><code data-language=\"css\"><span class=\"hljs-code-line\"><span class=\"hljs-selector-tag\">img<\/span> {<\/span>\n<span class=\"hljs-code-line\">\t<span class=\"hljs-attribute\">max-width<\/span>: <span class=\"hljs-number\">100%<\/span>;<\/span>\n<span class=\"hljs-code-line\">\t<span class=\"hljs-attribute\">height<\/span>: auto;<\/span>\n<span class=\"hljs-code-line\">}<\/span><\/code><\/pre>\n<p>This makes sure, that images don't exceed the viewport (or container) they are in (<code>max-width: 100%<\/code>), and also makes sure to not stretch images (<code>height: auto<\/code>). Without setting the <code>height<\/code> to <code>auto<\/code>, images may get stretched or squashed, because then the height ist set to a fixed pixel size.<\/p><p>Make sure to save your images in the resolution they are (likely) to be displayed. Don't include a 1000px wide image, if it gets only displayed in a 300px wide div; just resize it to 300px wide for this case. Your images should be 2500px wide and 2000px high at a maximum, even if they get displayed larger, because if you save them at a higher resolution, the filesize gets way too big. You should target 500 KB at max per image (depending on the number of images on your site you can get away with a larger filesize, or need to go lower if there are many images. A good rule is to not exceed 10 MB per page. The lower, the better.)<\/p><h2 id=\"heading-be22774a-2ca5-4fed-9f09-160afd1a81de\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-be22774a-2ca5-4fed-9f09-160afd1a81de\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">srcset for Higher Details<\/span>\n\n<\/h2>\n<p>Some screens may have a higher pixel density (high-dpi\/\"retina\" displays). You can add additional images size for those screens, to load a higher resolution if needed, and fall back to the default resolution when the user is on a \"normal\" screen.<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">&lt;img srcset=\"image_800x600.jpg 1x, image_1600x1200.jpg 2x, image_2400x1800.jpg 3x\" src=\"image_800x600.jpg\" width=\"800\" height=\"600\"&gt;<\/span><\/code><\/pre>\n<p>This will load the <code>image_800x600.jpg<\/code> by default. But if you are on a high-dpi display, it may load the <code>image_1600x1200.jpg<\/code> or <code>image_2400x1800.jpg<\/code> instead, depending on the pixel density of the target display. See <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Learn\/HTML\/Multimedia_and_embedding\/Responsive_images#resolution_switching_same_size_different_resolutions\" target=\"_blank\" rel=\"noreferrer\">mdn web docs<\/a> for details.<\/p><h2 id=\"heading-d1af2947-4567-4062-b8ed-6ecdfc4a081b\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-d1af2947-4567-4062-b8ed-6ecdfc4a081b\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">srcset for Responsive Images<\/span>\n\n<\/h2>\n<p>You can use <code>&lt;source&gt;<\/code> elements inside the <code>&lt;picture&gt;<\/code> element to provide different images for different resolutions:<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">&lt;picture&gt;<\/span>\n<span class=\"hljs-code-line\">\t&lt;source media=\"(max-width: 600px)\" srcset=\"image_400x300.jpg 1x, image_800x600.jpg 2x\" type=\"image\/jpeg\"&gt;<\/span>\n<span class=\"hljs-code-line\">\t&lt;source media=\"(min-width: 600px)\" srcset=\"image_800x600.jpg 1x, image_1600x1200.jpg 2x\" type=\"image\/jpeg\"&gt;<\/span>\n<span class=\"hljs-code-line\">\t&lt;img src=\"image_800x600.jpg\" width=\"800\" height=\"600\"&gt;<\/span>\n<span class=\"hljs-code-line\">&lt;\/picture&gt;<\/span><\/code><\/pre>\n<p>This will load images with smaller dimensions for screens that have a smaller width than 600px, and bigger images for larger screens.<\/p><h2 id=\"heading-c1a830a2-eb35-4595-bf42-f04d4cd7af78\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-c1a830a2-eb35-4595-bf42-f04d4cd7af78\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">srcset and sizes<\/span>\n\n<\/h2>\n<p>If you want to tell the browser, which size the image will be, so that the browser can pick the best size from the srcset <em>even before the layout step finishes<\/em>, you can add the <code>sizes<\/code> attribute:<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">&lt;img sizes=\"(max-width: 820px) 100vw, 800px\" srcset=\"image_480x360.jpg 480w, image_800x600.jpg 800w, image_1600x1200.jpg 1600w\" src=\"image_800x600.jpg\" width=\"800\" height=\"600\"&gt;<\/span><\/code><\/pre>\n<p>This tells the browser, that the image on the screen will have a width of 800px, unless the viewport has a maximum width of 820px, then the image will have a width of 100% of the viewportwidth. The browser can then use the <code>srcset<\/code> to determine, which image is best for the current viewport and pixel density. This can happen, before the CSS is loaded, and the browser can start (pre-)loading the correct image immediatley, as soon as the HTML is loaded. You can read more about this on <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Learn\/HTML\/Multimedia_and_embedding\/Responsive_images#resolution_switching_different_sizes\" target=\"_blank\" title='\"Resolution switching: Different sizes\" on mdn web docs'>mdn<\/a>.<\/p><p><a href=\"https:\/\/html.spec.whatwg.org\/#allows-auto-sizes\" target=\"_blank\">There will be a <\/a><code>sizes=\"auto\"<\/code><a href=\"https:\/\/html.spec.whatwg.org\/#allows-auto-sizes\" target=\"_blank\"> option in the future<\/a>, which is usefull if you want to lazy-load the image anyway, so the loading can happen after the layout step, and you don't want to add all possible image sizes in the sizes attribute. But the browser support isn't there yet.<\/p><h2 id=\"heading-b7dc914e-dcfd-4a99-ac4b-9c8b94e63099\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-b7dc914e-dcfd-4a99-ac4b-9c8b94e63099\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Lazy-Load Images<\/span>\n\n<\/h2>\n<p>You should add the <code>loading=\"lazy\"<\/code> attribute to the <code>&lt;img&gt;<\/code> tag. With this tag, the image will not be loaded when the page loads, if it is not currently in the viewport. The browser will automatically load the images below the viewport, once the users starts scrolling and gets near the image. If the user never scrolls, the image never has to load.<\/p><h2 id=\"heading-c17093d3-cf9a-4e42-bb2f-91726d23e606\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-c17093d3-cf9a-4e42-bb2f-91726d23e606\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Don't Lazy-Load Images<\/span>\n\n<\/h2>\n<p>Images that are in the initial viewport, should not be lazy loaded, so you should omit the <code>loading<\/code> attribute or set it to <code>loading=\"eager\"<\/code>. In that case, the browser starts preloading these images, before it even starts laying out the page, which helps with the initial page load and layout shifts. Because you don't know the size of the viewport of your visitor in advance (they may be on a mobile phone or a gigantic desktop display), use your best judgement. For example, on this website, the first two images are not lazy loaded, the rest is.<\/p><p>If your website targets very lossy or slow connections, omiting the <code>loading<\/code> attribute on every image could also help, because then all images load one after the other and are already there, when the user scrolls down. When you are on a lossy connection, you may open a website and then wait until everything is loaded, before starting to scroll down. If images are set to lazy load, this may be very annoying. This is a edge case though, so when in doubt set images that are below the initial viewport to lazy load.<\/p><h2 id=\"heading-35497c68-41bc-4770-a1c4-d8f37530a532\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-35497c68-41bc-4770-a1c4-d8f37530a532\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Additional File Formats: avif, webp<\/span>\n\n<\/h2>\n<p>You can use modern file formats, to make the filesize smaller or the quality higher. <em>AVIF<\/em> and <em>WEBP<\/em> are a good choice for this. Some browsers may only support one of the two, or none, so you should always provide a <em>jpg<\/em> (or <em>png<\/em>) fallback for now.<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">&lt;picture&gt;<\/span>\n<span class=\"hljs-code-line\">\t&lt;source srcset=\"image_800x600.avif 1x, image_1600x1200.avif 2x, image_2400x1800.avif 3x\" type=\"image\/avif\"&gt;<\/span>\n<span class=\"hljs-code-line\">\t&lt;source srcset=\"image_800x600.webp 1x, image_1600x1200.webp 2x, image_2400x1800.webp 3x\" type=\"image\/webp\"&gt;<\/span>\n<span class=\"hljs-code-line\">\t&lt;source srcset=\"image_800x600.jpg 1x, image_1600x1200.jpg 2x, image_2400x1800.jpg 3x\" type=\"image\/jpeg\"&gt;<\/span>\n<span class=\"hljs-code-line\">\t&lt;img src=\"image_800x600.jpg\" width=\"800\" height=\"600\"&gt;<\/span>\n<span class=\"hljs-code-line\">&lt;\/picture&gt;<\/span><\/code><\/pre>\n<p>This will load the image in avif or webp if the browser supports it, and if not falls back to the jpg. It also chooses the correct resolution depending on the pixel density of the display.<\/p><p><a href=\"https:\/\/caniuse.com\/?search=avif\" target=\"_blank\"><em>AVIF<\/em>-support<\/a> seams to be good enough now that you may ommit webp, and just use a <em>jpg<\/em> fallback, this saves on webspace and conversion time.<\/p><h2 id=\"heading-3a6f24f5-63ad-4b00-a564-84c37160e181\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-3a6f24f5-63ad-4b00-a564-84c37160e181\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Compress Your Images, then Compress Some More<\/span>\n\n<\/h2>\n<p>After you saved your images, you can use pngcrush, tinyjpg or other tools to compress them some more. This will save 10% - 20% of filesize, without loosing any quality. Sometimes, using those tools more than once on an image may give you additional compression rate. You could also automate this process with a small bash script or something like this.<\/p><h2 id=\"heading-282f494f-027c-4e74-852b-33f6cc53a3db\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-282f494f-027c-4e74-852b-33f6cc53a3db\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Tell the Browser Which Images to Fetch First<\/span>\n\n<\/h2>\n<p>You can prioritize the images you want to load faster with a higher fetchpriority.<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">&lt;img src=\"not-important.jpg\" fetchpriority=\"low\"&gt;<\/span>\n<span class=\"hljs-code-line\">&lt;img src=\"very-important.jpg\" fetchpriority=\"high\"&gt;<\/span><\/code><\/pre>\n<p>This will load the <code>very-important.jpg<\/code> before loading the <code>not-important.jpg<\/code>, even though the order in the HTML is the other way around. This is not supported in all browsers yet, and is marked as experimental, so the syntax may change in the future. See the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/HTMLImageElement\/fetchPriority\" target=\"_blank\" rel=\"noreferrer\">mdn web docs<\/a> for details. You only need this for very special use cases, so by default just omit the <code>fetchpriority<\/code> tag.<\/p><h2 id=\"heading-b05d81dc-b69d-47f4-a4aa-b19284a0614b\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-b05d81dc-b69d-47f4-a4aa-b19284a0614b\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Set the Decode Priority<\/span>\n\n<\/h2>\n<p>You can tell the browser, that it's ok to decode an image slower, if it is not important, or decode it with a high priority, if it is the most important thing on your website and needs to be there as fast as possible:<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">&lt;img src=\"very-important.jpg\" decoding=\"sync\"&gt;<\/span>\n<span class=\"hljs-code-line\">&lt;img src=\"not-important.jpg\" decoding=\"async\"&gt;<\/span><\/code><\/pre>\n<p>Using <code>decoding=\"async\"<\/code> will display the image with a lower priority, and more performance is available for other tasks, like layout or JavaScript execution. You only need this for very special use cases, so by default just omit the <code>decoding<\/code> tag. Or you could add <code>decoding=\"async\"<\/code> to every image that also has <code>loading=\"lazy\"<\/code>, and add <code>decoding=\"sync\"<\/code> to every image that has <code>loading=\"eager\"<\/code>.<\/p><h2 id=\"heading-d5225f98-fddf-45cc-b2d4-1fce0baa4e00\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-d5225f98-fddf-45cc-b2d4-1fce0baa4e00\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Color Preview<\/span>\n\n<\/h2>\n<p>You could use the average color of an image as the background color for the image, so that while the image gets (lazy-)loaded, this color is visible and is then replaced by the image. There are some online tools that can get the average color, or you could get the average color with php like this:<\/p><pre class=\"hljs\"><code data-language=\"php\"><span class=\"hljs-code-line\"><span class=\"hljs-meta\">&lt;?php<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">get_average_color<\/span><span class=\"hljs-params\">( $imagepath )<\/span> <\/span>{<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">\t$default_color = [<span class=\"hljs-string\">'r'<\/span> =&gt; <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-string\">'g'<\/span> =&gt; <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-string\">'b'<\/span> =&gt; <span class=\"hljs-number\">0<\/span>];<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">\t$imagesize = getimagesize( $imagepath );<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">\t$image = <span class=\"hljs-keyword\">false<\/span>;<\/span>\n<span class=\"hljs-code-line\">\t<span class=\"hljs-keyword\">if<\/span>( $imagesize[<span class=\"hljs-number\">2<\/span>] == IMG_JPG ) {<\/span>\n<span class=\"hljs-code-line\">\t\t$image = imagecreatefromjpeg( $imagepath );<\/span>\n<span class=\"hljs-code-line\">\t} <span class=\"hljs-keyword\">elseif<\/span>( $imagesize[<span class=\"hljs-number\">2<\/span>] == IMG_PNG ) {<\/span>\n<span class=\"hljs-code-line\">\t\t$image = imagecreatefrompng( $imagepath );<\/span>\n<span class=\"hljs-code-line\">\t}<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">\t<span class=\"hljs-keyword\">if<\/span>( ! $image ) <span class=\"hljs-keyword\">return<\/span> $default_color;<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">\t$imagewidth = $imagesize[<span class=\"hljs-number\">0<\/span>];<\/span>\n<span class=\"hljs-code-line\">\t$imageheight = $imagesize[<span class=\"hljs-number\">1<\/span>];<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">\t$small = imagecreatetruecolor(<span class=\"hljs-number\">1<\/span>,<span class=\"hljs-number\">1<\/span>);<\/span>\n<span class=\"hljs-code-line\">\timagecopyresampled( $small, $image, <span class=\"hljs-number\">0<\/span>,<span class=\"hljs-number\">0<\/span>,<span class=\"hljs-number\">0<\/span>,<span class=\"hljs-number\">0<\/span>,<span class=\"hljs-number\">1<\/span>,<span class=\"hljs-number\">1<\/span>, $width, $height );<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">\t$rgb = imagecolorat($small,<span class=\"hljs-number\">0<\/span>,<span class=\"hljs-number\">0<\/span>);<\/span>\n<span class=\"hljs-code-line\">\t$main_color[<span class=\"hljs-string\">'r'<\/span>] = ($rgb &gt;&gt; <span class=\"hljs-number\">16<\/span>) &amp; <span class=\"hljs-number\">0xFF<\/span>;<\/span>\n<span class=\"hljs-code-line\">\t$main_color[<span class=\"hljs-string\">'g'<\/span>] = ($rgb &gt;&gt; <span class=\"hljs-number\">8<\/span>) &amp; <span class=\"hljs-number\">0xFF<\/span>;<\/span>\n<span class=\"hljs-code-line\">\t$main_color[<span class=\"hljs-string\">'b'<\/span>] = $rgb &amp; <span class=\"hljs-number\">0xFF<\/span>;<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">\t<span class=\"hljs-comment\">\/\/ cache $main_color in a database or textfile or something, because it may be costly to do this for every image on every page load!<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">\t<span class=\"hljs-keyword\">return<\/span> $main_color;<\/span>\n<span class=\"hljs-code-line\">}<\/span><\/code><\/pre>\n<p>You can then use these r\/g\/b values as a background color:<\/p><pre class=\"hljs\"><code data-language=\"php\"><span class=\"hljs-code-line\"><span class=\"hljs-meta\">&lt;?php<\/span><\/span>\n<span class=\"hljs-code-line\">$imagepath = <span class=\"hljs-string\">'my_beautiful_image.jpg'<\/span>;<\/span>\n<span class=\"hljs-code-line\">$main_color = get_average_color( $imagepath );<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-meta\">?&gt;<\/span><\/span>\n<span class=\"hljs-code-line\">&lt;picture style=<span class=\"hljs-string\">\"background-color: rgb(&lt;?= $main_color['r'] ?&gt;, &lt;?= $main_color['g'] ?&gt;, &lt;?= $main_color['b'] ?&gt;);\"<\/span>&gt;<\/span>\n<span class=\"hljs-code-line\">\t&lt;img src=<span class=\"hljs-string\">\"&lt;?= $imagepath ?&gt;\"<\/span> loading=<span class=\"hljs-string\">\"lazy\"<\/span>&gt;<\/span>\n<span class=\"hljs-code-line\">&lt;\/picture&gt;<\/span><\/code><\/pre>\n<h2 id=\"heading-57d9760f-e20c-4740-a2c1-2b2bd6e60b17\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-57d9760f-e20c-4740-a2c1-2b2bd6e60b17\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">base64-encoded Preview Image<\/span>\n\n<\/h2>\n<p>You can also use a very small and blurry image, encoded as base64, directly in your HTML.<\/p><p>First, resize your image to very small dimensions, like 100px wide, and add some blur on top. Save it as .jpg, then base64 encode it (via PHP, or search for <em>jpg to base64<\/em>). This base64 encoded string can be added as a background image:<\/p><pre class=\"hljs\"><code data-language=\"php\"><span class=\"hljs-code-line\"><span class=\"hljs-meta\">&lt;?php<\/span><\/span>\n<span class=\"hljs-code-line\">$imagepath = <span class=\"hljs-string\">'the_best_image_in_the_world.jpg'<\/span>;<\/span>\n<span class=\"hljs-code-line\">$base64_encoded_preview = <span class=\"hljs-string\">'data:image\/jpg;base64,'<\/span>.base64_encode(file_get_contents($imagepath));<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-meta\">?&gt;<\/span><\/span>\n<span class=\"hljs-code-line\">&lt;picture style=<span class=\"hljs-string\">\"background-image: url(&lt;?= $base64_encoded_preview ?&gt;);\"<\/span>&gt;<\/span>\n<span class=\"hljs-code-line\">\t&lt;img src=<span class=\"hljs-string\">\"&lt;?= $imagepath ?&gt;\"<\/span> loading=<span class=\"hljs-string\">\"lazy\"<\/span>&gt;<\/span>\n<span class=\"hljs-code-line\">&lt;\/picture&gt;<\/span><\/code><\/pre>\n<p>Because the preview-image is base64-encoded and embedded directly in the HTML, it is loaded with the HTML and already there as soon as the browsers renders the <code>&lt;picture&gt;<\/code> element. The <code>&lt;img&gt;<\/code> will then get lazy-loaded, and replaces\/overlays the blurry preview image.<\/p><h2 id=\"heading-21dbc454-c7ea-4092-815d-397161a026fe\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-21dbc454-c7ea-4092-815d-397161a026fe\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">gif is the Worst File Format<\/span>\n\n<\/h2>\n<p>The gif file format is very old and inefficient. If the image does not move, use .png or .svg (for vector files) instead, which provide a much smaller file size and higher quality.<\/p><p>If you want to use animated gifs, convert them to mp4 instead. You can use <a href=\"https:\/\/handbrake.fr\/\" target=\"_blank\" rel=\"noreferrer\">Handbrake<\/a> or <a href=\"https:\/\/ffmpeg.org\/\" target=\"_blank\" rel=\"noreferrer\">ffmpeg<\/a> for this. The video should be converted to the <code>h.264<\/code> codec in a <code>mp4<\/code> (or <code>m4v<\/code>) container. <code>av1<\/code> may also be possible, but is not supported by all browsers, so you should also add a <code>h.264<\/code> fallback. The video dimensions should match the dimensions on the page, so that the video does not need to be scaled, but should not exceed <code>1920\u00d71080px<\/code>. The resulting filesize should not exceed 3 MB.<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">&lt;video src=\"my_beautiful_gif.m4v\" width=\"400\" height=\"400\" poster=\"my_beautiful_gif.jpg\" preload=\"metadata\" loading=\"lazy\" autoplay muted loop playsinline disablepictureinpicture disableremoteplayback&gt;<\/span><\/code><\/pre>\n<p>This will automatically play the video in a loop, without sound and without controls. See the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTML\/Element\/video\" target=\"_blank\" rel=\"noreferrer\">mdn web docs<\/a> for details about all those video attributes.<\/p><h2 id=\"heading-893cd519-dbd8-4bcd-8aa1-b8ef93647fad\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-893cd519-dbd8-4bcd-8aa1-b8ef93647fad\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Don't Trust Me. Test Your Website<\/span>\n\n<\/h2>\n<p>You should always test your website to see what helps. You can use the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Glossary\/Developer_Tools\" target=\"_blank\" rel=\"noreferrer\">Developer Tools<\/a> of your browser to view a waterfall diagram of what gets loaded when. You can also disable the browser cache, to force a reload of all images, and use throttling to simulate slower connections.<\/p><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/performant-images-on-the-web\/5403f40cc3-1733941282\/browsertools-800x-q80.jpg\" width=\"800\" height=\"609\" alt=\"\">","date_published":"2023-06-30T17:40:00+00:00","date_modified":"2023-12-11T17:40:00+00:00"},{"id":"https:\/\/www.maxhaesslein.de\/notes\/automatically-backup-your-complete-linux-system-when-connecting-to-a-specific-wifi-network","url":"https:\/\/www.maxhaesslein.de\/notes\/automatically-backup-your-complete-linux-system-when-connecting-to-a-specific-wifi-network","title":"Automatically backup your complete Linux system when connecting to a specific wifi network","content_html":"<p>I have a local server in my office, and use my laptop via wifi. When I arrive at the office and my laptop connects to the wifi, it will stay on for multiple hours, so it is the perfect time to make an automatic backup.<\/p><p>This consists of two scripts: one shell script that runs rsync, and a script for the NetworkManager that starts the first script as soon as I connect to the office wifi network. The first script can also be called directly via the command line, if I want to manually start a backup.<\/p><h2 id=\"heading-8f73a6bd-36a6-4cff-bedf-95a58382a015\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-8f73a6bd-36a6-4cff-bedf-95a58382a015\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">Backup Script<\/span>\n\n<\/h2>\n<p>The <code>\/home\/mh\/bin\/backup-system.sh<\/code> file looks like this:<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">#!\/bin\/bash<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">SOURCE_FOLDER=\"\/.snapshots\/\"<\/span>\n<span class=\"hljs-code-line\">TARGET_FOLDER=\"server-hostname:\/srv\/path\/to\/backups\/\"<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">RSYNC_FLAGS=\"aAXH\"<\/span>\n<span class=\"hljs-code-line\">ADDITIONAL_RSYNC_FLAGS=\"--bwlimit=10000 --delete-after --exclude=\/.snapshots --exclude=\/dev\/* --exclude=\/proc\/* --exclude=\/sys\/* --exclude=\/tmp\/* --exclude=\/run\/* --exclude=\/mnt\/* --exclude=\/lost+found --exclude=\/swap --exclude=\/home\/*\/.thumbnails\/* --exclude=\/home\/*\/.cache\/*  --exclude=\/home\/*\/.local\/share\/Trash\/*  --exclude=\/home\/*\/.gvfs --exclude=\/var\/lib\/dhcpcd\/* --exclude=\/var\/spool\/postfix\/dev\/*\"<\/span>\n<span class=\"hljs-code-line\"># --bwlimit=10000 limits the network (value ist KBps) -- 10000 is 0,08<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"># make sure we are not running already:<\/span>\n<span class=\"hljs-code-line\">if [[ \"`pidof -x $(basename $0) -o %PPID`\" ]]; then<\/span>\n<span class=\"hljs-code-line\">\techo \"[\"$(basename $0)\"]\"<\/span>\n<span class=\"hljs-code-line\">\techo \"This script is already running with PID `pidof -x $(basename $0) -o %PPID`\"<\/span>\n<span class=\"hljs-code-line\">\texit<\/span>\n<span class=\"hljs-code-line\">fi<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"># make sure we run as root<\/span>\n<span class=\"hljs-code-line\">if [[ $(id -u) -ne 0 ]] ; then<\/span>\n<span class=\"hljs-code-line\">\techo \"Please run as root\"<\/span>\n<span class=\"hljs-code-line\">\texit 1<\/span>\n<span class=\"hljs-code-line\">fi<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">VERBOSE=false<\/span>\n<span class=\"hljs-code-line\">while getopts \":v\" opt; do<\/span>\n<span class=\"hljs-code-line\">\tcase $opt in<\/span>\n<span class=\"hljs-code-line\">\t\tv)<\/span>\n<span class=\"hljs-code-line\">\t\t\techo \"verbose mode is on\"<\/span>\n<span class=\"hljs-code-line\">\t\t\tVERBOSE=true<\/span>\n<span class=\"hljs-code-line\">\t\t\t;;<\/span>\n<span class=\"hljs-code-line\">\t\t\\?)<\/span>\n<span class=\"hljs-code-line\">\t\t\techo \"invalid option: -$OPTARG\" &gt;&amp;2<\/span>\n<span class=\"hljs-code-line\">\t\t\t;;<\/span>\n<span class=\"hljs-code-line\">\tesac<\/span>\n<span class=\"hljs-code-line\">done<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">if [ \"$VERBOSE\" = true ]; then<\/span>\n<span class=\"hljs-code-line\">\techo \"starting rsync backup ..\"<\/span>\n<span class=\"hljs-code-line\">fi<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">NEWEST_SNAPSHOT=$(ls -td $SOURCE_FOLDER*\/ | head -1)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">SOURCE_FOLDER=$NEWEST_SNAPSHOT\"snapshot\/\"<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">if [ \"$VERBOSE\" = true ]; then<\/span>\n<span class=\"hljs-code-line\">\techo \"source folder is \"$SOURCE_FOLDER<\/span>\n<span class=\"hljs-code-line\">fi<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">if [ \"$VERBOSE\" = true ]; then<\/span>\n<span class=\"hljs-code-line\">\tRSYNC_FLAGS+=\"v\"<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">\techo \"using rsync flags -\"$RSYNC_FLAGS\" \"$ADDITIONAL_RSYNC_FLAGS<\/span>\n<span class=\"hljs-code-line\">fi<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">rsync -$RSYNC_FLAGS $ADDITIONAL_RSYNC_FLAGS \"$SOURCE_FOLDER\" \"$TARGET_FOLDER\"<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">if [ \"$VERBOSE\" = true ]; then<\/span>\n<span class=\"hljs-code-line\">\techo \".. rsync backup finished\"<\/span>\n<span class=\"hljs-code-line\">fi<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">exit 0<\/span><\/code><\/pre>\n<p>The script can manually be started via <code>sudo backup-system.sh -v<\/code> - the <code>-v<\/code> flag runs the script in verbose mode and displays more information, omitting it will only show rsync errors. It needs to be run as <code>sudo<\/code>, because this will backup the complete root directory and needs corresponding permissions.<\/p><p>Because I use btrfs, and it automatically takes a snapshot every hour, I backup the latest snapshot, and not the live root directory (this helps with vanishing files), but you can update the script to not look for the <code>NEWEST_SNAPSHOT<\/code> and just use the <code>SOURCE_FOLDER<\/code> for the rsync source.<\/p><h2 id=\"heading-496bd7ba-df63-41d1-b882-36412222caae\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-496bd7ba-df63-41d1-b882-36412222caae\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">NetworkManager autostart script<\/span>\n\n<\/h2>\n<p>The NetworkManager has a dispatcher script, that gets called when a wifi network gets connected, and checks the wifis SSID. If it is the correct SSID, it automatically starts the <code>backup-system.sh<\/code> script.<\/p><p>The script is saved at <code>\/etc\/NetworkManager\/dispatcher.d\/50-system-backup<\/code> and looks like this:<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">#!\/bin\/bash<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">INTERFACE=$1<\/span>\n<span class=\"hljs-code-line\">ACTION=$2<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">ESSID_OFFICE=\"The SSID of my office wifi network\"<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"># find SSID of connected wifi:<\/span>\n<span class=\"hljs-code-line\">ESSID=`iwconfig $INTERFACE | grep ESSID | cut -d\":\" -f2 | sed 's\/^[^\"]*\"\\|\"[^\"]*$\/\/g'`<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">case \"$ACTION\" in<\/span>\n<span class=\"hljs-code-line\">\tup)<\/span>\n<span class=\"hljs-code-line\">\t\tif [ \"$ESSID\" = \"$ESSID_OFFICE\" ]; then<\/span>\n<span class=\"hljs-code-line\">\t\t\t\/home\/mh\/bin\/backup-system.sh<\/span>\n<span class=\"hljs-code-line\">\t\tfi<\/span>\n<span class=\"hljs-code-line\">\t\t;;<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">\tdown)<\/span>\n<span class=\"hljs-code-line\">\t\t;;<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">\tpre-up)<\/span>\n<span class=\"hljs-code-line\">\t\t;;<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">\tpost-down)<\/span>\n<span class=\"hljs-code-line\">\t\t;;<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">\t*)<\/span>\n<span class=\"hljs-code-line\">\t\techo $\"Usage: $0 {up|down|pre-up|post-down}\"<\/span>\n<span class=\"hljs-code-line\">\t\texit 1<\/span>\n<span class=\"hljs-code-line\">esac<\/span><\/code><\/pre>\n","date_published":"2023-03-09T15:41:00+00:00"},{"id":"https:\/\/www.maxhaesslein.de\/notes\/pi-hole-fritz-box-ipv6","url":"https:\/\/www.maxhaesslein.de\/notes\/pi-hole-fritz-box-ipv6","title":"Pi-hole, FRITZ!Box & IPv6","content_html":"<p>I'm using a FRITZ!Box 3490, but it should work with all (modern) FRITZ!Box variants. All the FRITZ!Box settings are noted in german, I may update this later to the correct english setting names.<\/p><p>This is how it is configured: the Pi-hole is the primary DNS server, FRITZ!Box is the Upstream DNS-Server and the DHCP server. So new DNS requests go to the Pi-hole, and if not blocked are forwarded to the FRITZ!Box, which itself forwards it (if its not an internal client) to quad9. The Pi-hole also has some local DNS and CNAME records, which get priority over the FRITZ!Box.<\/p><p>IPv6 is enabled in the FRITZ!Box. The Pi-hole has a static IP-address.<\/p><p>Go to <em>Heimnetz - Netzwerk - Netzwerkeinstellungen<\/em>. At <em>IP-Adressen<\/em> click the <em>IPv4-Einstellungen<\/em> button and add the Pi-hole IPv4 address as the <em>Lokaler DNS-Server<\/em> address. <em>DHCP-Server aktivieren<\/em> must be set to true.<\/p><p>Go to <em>Heimnetz - Netzwerk - Netzwerkeinstellungen<\/em>. At <em>IP-Adressen<\/em> click the <em>IPv6-Einstellungen<\/em> button. Set <em>Router Advertisment im LAN aktiv<\/em> to true, and set it to <em>Unique Local Addresses (ULA) immer zuweisen<\/em>. I set the prefix to <code>fd00::\/64<\/code>. You may want to restart your FRITZ!Box and the Pi-hole now, to make sure they get a new IPv6 address with the correct <em>ULA-Pr\u00e4fix<\/em>.<\/p><p>On the same page, set <em>DNSv6-Server auch \u00fcber Router Advertisement bekanntgeben (RFC 5006)<\/em> to true, and add the Pi-hole IPv6 address as your <em>Lokaler DNSv6-Server<\/em>. Use the IPv6 address that starts with the <em>ULA-Pr\u00e4fix<\/em>, in my case the address that starts with <code>fd00<\/code>.<\/p><p>On the same page, at <em>Unique Local Address Ihrer FRITZ!Box<\/em> you can find the IPv6 of your FRITZ!Box (or use <code>ping fritz.box -6<\/code> from your Pi-hole).<\/p><p>The IPv6 address of your FRITZ!Box must be added in your Pi-hole under <em>Settings - DNS - Upstream DNS Servers<\/em> as <em>Custom 3 (IPv6)<\/em>. Also add the IPv4 of your FRITZ!Box as <em>Custom 1 (IPv4)<\/em> - this should be <code>192.168.178.1<\/code> but you can also use <code>ping fritz.box -4<\/code> to find it. All other <em>Upstream DNS Servers<\/em> must be disabled. At <em>Advanced DNS Settings<\/em>, both <em>Never forward non-FQDN A and AAAA queries<\/em> and <em>Never forward reverse lookups for private IP ranges<\/em> must be disabled, and <em>Use Conditional Forwarding<\/em> must be enabled, with the <em>Local network in CIDR notation<\/em> set to <code>192.168.178.0\/24<\/code> (or whatever your range is) and <em>IP address of your DHCP server (router)<\/em> must be set to <code>192.168.178.1<\/code> - all those settings are needed for Pi-hole to get the client names (otherwise, the logs would only show the IP address)<\/p>","date_published":"2022-10-19T15:27:00+00:00"},{"id":"https:\/\/www.maxhaesslein.de\/notes\/raspberry-pi-os-headless-install","url":"https:\/\/www.maxhaesslein.de\/notes\/raspberry-pi-os-headless-install","title":"Raspberry Pi OS Headless Install","content_html":"<p><strong>Now, you can use the <\/strong><a href=\"https:\/\/www.raspberrypi.com\/software\/\" target=\"_blank\" rel=\"noreferrer\"><strong>Raspberry Pi Imager<\/strong><\/a><strong> and then set the ssh, password and WiFi information in the settings, before flashing the OS.<\/strong> This text describes how you can achieve the same thing without the official imager program.<\/p><p>-----<\/p><p>Download the <a href=\"https:\/\/www.raspberrypi.com\/software\/operating-systems\/\" target=\"_blank\" rel=\"noreferrer\"><em>Raspberry Pi OS Lite<\/em> image<\/a>. Use <a href=\"https:\/\/www.balena.io\/etcher\/\" target=\"_blank\" rel=\"noreferrer\">etcher<\/a> or something similar to burn the image to the SD card.<\/p><p>In the <em>boot<\/em> partition (on the SD card), create a empty file called <em>ssh<\/em> to automatically enable the SSH server.<\/p><p>In newer versions of PiOS, we need to create a default user and password. Write the password into a temp file into <em>\/tmp\/pi.txt<\/em> as a string. The password is set on the <em>boot<\/em> partition into a file called <em>userconf<\/em> in the format <code>username:password<\/code>; the password is saved as a SHA256\/SHA512 hash. To use the username <em>pi<\/em> and the password from the file <em>\/tmp\/pi.txt<\/em> us the following command (replace <em>\/path\/to\/boot\/<\/em> with the path to the boot directory on the SD card):<\/p><pre class=\"hljs\"><code data-language=\"bash\"><span class=\"hljs-code-line\"><span class=\"hljs-built_in\">echo<\/span> -n pi: &gt; \/path\/to\/boot\/userconf<\/span>\n<span class=\"hljs-code-line\">cat \/tmp\/pi.txt | openssl passwd -6 -stdin &gt;&gt; \/path\/to\/boot\/userconf<\/span><\/code><\/pre>\n<p>if needed, add your wifi configuration into <em>boot\/wpa_supplicant.conf<\/em> - replace <code>YOUR-COUNTRY-CODE<\/code> with your <a href=\"https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1_alpha-2#Officially_assigned_code_elements\" target=\"_blank\" rel=\"noopener noreferrer\">2-letter country-code<\/a> (uppercase), and <code>your-wifi-ssid<\/code> and <code>your-wifi-password<\/code> with the correct values:<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">country=YOUR-COUNTRY-CODE<\/span>\n<span class=\"hljs-code-line\">ctrl_interface=DIR=\/var\/run\/wpa_supplicant GROUP=netdev<\/span>\n<span class=\"hljs-code-line\">update_config=1<\/span>\n<span class=\"hljs-code-line\">network={<\/span>\n<span class=\"hljs-code-line\">    ssid=\"your-wifi-ssid\"<\/span>\n<span class=\"hljs-code-line\">    psk=\"your-wifi-password\"<\/span>\n<span class=\"hljs-code-line\">}<\/span><\/code><\/pre>\n<p>If you don't want to add your wifi password as plain text, you can use <code>wpa_passphrase<\/code> to encrpyt it:<\/p><pre class=\"hljs\"><code data-language=\"bash\"><span class=\"hljs-code-line\">wpa_passphrase your-wifi-ssid your-wifi-password<\/span><\/code><\/pre>\n<p>then use this output as your <code>your-wifi-password<\/code> value in the <em>wpa_supplicant<\/em>.conf file.<\/p><p>You can add muliple <em>network<\/em> blocks if you want to add different wifi configurations; the first one that works will be used.<\/p>","date_published":"2022-10-16T10:00:00+00:00"},{"id":"https:\/\/www.maxhaesslein.de\/notes\/build-your-own-headless-raspberry-pi-audio-player","url":"https:\/\/www.maxhaesslein.de\/notes\/build-your-own-headless-raspberry-pi-audio-player","title":"Build your own headless Raspberry Pi audio player","content_html":"<p>For a hassle-free solution, have a look at <a href=\"https:\/\/www.hifiberry.com\/hifiberryos\/\" target=\"_blank\">HiFiBerryOS<\/a>, <a href=\"https:\/\/volumio.com\/get-started\/\" target=\"_blank\">Volumio<\/a>, <a href=\"https:\/\/www.picoreplayer.org\/\" target=\"_blank\">piCorePlayer<\/a>, <a href=\"https:\/\/ropieee.org\/\" target=\"_blank\">RoPieee<\/a> or similiar solutions. These notes describe how you can build your own headless player step by step by installing all the software yourself and configure it via the command line.<\/p><p><strong>NOTE: this is from 2020. It may no longer be up to date.<\/strong> I may revisit and update this in the future, in the meantime you are on your own.<\/p><p>You can read more about this project <a href=\"https:\/\/www.maxhaesslein.de\/audio\/tools\/suspiria\/\" target=\"_blank\">here<\/a>.<\/p><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/build-your-own-headless-raspberry-pi-audio-player\/79a2b69311-1733941282\/img_7146-800x-q80.jpg\" width=\"800\" height=\"600\" alt=\"\"><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/build-your-own-headless-raspberry-pi-audio-player\/c57ba99e74-1733941282\/img_7124-800x-q80.jpg\" width=\"800\" height=\"600\" alt=\"\"><h2 id=\"heading-4ef2d0e6-dca2-4fae-b678-5d0de8dbd141\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-4ef2d0e6-dca2-4fae-b678-5d0de8dbd141\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">1. Hardware<\/span>\n\n<\/h2>\n<p>If you want the most low budget solution, use a Raspberry Pi 3 with the onboard audio. Or you can use a Raspberry Pi Zero, with a cheap USB audio adapter, or use audio over HDMI if you connect directly to a soundbar or your TV or something like that. I won't cover those use cases here, but you can find out how to play audio on your own and then jump to <em>4. headless Spotify Connect player<\/em><\/p><p>I'm using a Raspberry Pi Zero W because it is the smallest of the Raspberry Pis, it's cheap and powerful enough.<\/p><p>There are a lot of audio hats you can use:<\/p><ul><li><a href=\"https:\/\/www.hifiberry.com\/\" target=\"_blank\" rel=\"noopener noreferrer\">HiFiBerry<\/a><\/li><li><a href=\"https:\/\/allo.com\/sparky-dac.html\" target=\"_blank\" rel=\"noopener noreferrer\">Allo<\/a><\/li><li><a href=\"https:\/\/raspiaudio.com\/produit\/audio\" target=\"_blank\" rel=\"noopener noreferrer\">Raspiaudio Audio+<\/a><\/li><li><a href=\"https:\/\/shop.pimoroni.com\/products\/phat-dac\" target=\"_blank\" rel=\"noopener noreferrer\">Pimoroni pHAT DAC<\/a><\/li><li><a href=\"https:\/\/www.adafruit.com\/product\/4037\" target=\"_blank\" rel=\"noopener noreferrer\">Adafruit I2S Audio Bonnet<\/a><\/li><li><a href=\"https:\/\/www.hifiberry.com\/shop\/boards\/hifiberry-dac-zero\/\" target=\"_blank\" rel=\"noopener noreferrer\">HiFiBerry DAC+ Zero<\/a><\/li><li><a href=\"https:\/\/www.hifiberry.com\/shop\/boards\/miniamp\/\" target=\"_blank\" rel=\"noopener noreferrer\">HiFiBerry MiniAmp<\/a> (when you want to directly drive small speakers)<\/li><\/ul><p>I used the Allo MiniBoss, the Pimoroni pHAT DAC, the Adafruit I2S Audio Bonnet and the Raspiaudio Audio+. All work without problems.<\/p><p>On a non-zero pi you can use the 3.5mm audio jack, or use the HDMI output directly with a Soundbar or a HDMI to Cinch converter.<\/p><p>The 3.5mm audio jack and the onboard dac are not that great, so a special dac shield or audio over HDMI is a better solution if you want high quality audio.<\/p><p>Use a decent power supply; the more expensive your audio shield is, the more expensive the power supply should be.<\/p><h2 id=\"heading-abad585c-18da-44d4-8d57-3214c492b697\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-abad585c-18da-44d4-8d57-3214c492b697\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">2. Get the Raspberry Pi Running<\/span>\n\n<\/h2>\n<p>I'm using a <a href=\"https:\/\/www.raspberrypi.org\/products\/raspberry-pi-zero-w\/\" target=\"_blank\" rel=\"noopener noreferrer\">Raspberry Pi Zero W<\/a>, but any Rasberry Pi should work.<\/p><p>(Note: for updated notes on how to install Pi OS on a headless server, <a href=\"https:\/\/www.maxhaesslein.de\/notes\/raspberry-pi-os-headless-install\/\" target=\"_blank\" rel=\"noopener noreferrer\">read the notes on \"Raspberry Pi OS Headless Install\"<\/a> and then continue with updating the Raspberry Pi; my old notes for the installation are still here, but may no longer be accurate)<\/p><p>Download the latest <a href=\"https:\/\/www.raspberrypi.org\/downloads\/raspberry-pi-os\/\" target=\"_blank\" rel=\"noopener noreferrer\">Raspberry Pi OS Lite<\/a> and extract the zip file. We use the lite version, because we want to run our Pi in headless mode without a gui and no monitor or keyboard connected to the pi.<\/p><p>Flash the extracted iso file to an microSD-card (a small 4GB card is enough; you could even get away with a 2GB card, but then updating the kernel may be a bit tricky) using <a href=\"https:\/\/etcher.balena.io\/\" target=\"_blank\" rel=\"noopener noreferrer\">Etcher<\/a>, <a href=\"https:\/\/www.raspberrypi.org\/documentation\/installation\/installing-images\/README.md\" target=\"_blank\" rel=\"noopener noreferrer\">or another tool<\/a>.<\/p><p>Mount the sd-card and add an empty file called <em>ssh<\/em> to the <em>\/boot<\/em> partition so that the ssh server will be enabled at boot.<\/p><p>If you want to use wifi, add a text file called <em>wpa_supplicant.conf<\/em> in the <em>\/boot<\/em> partition with the following content:<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">ctrl_interface=\/var\/run\/wpa_supplicant<\/span>\n<span class=\"hljs-code-line\">update_config=1<\/span>\n<span class=\"hljs-code-line\">country={COUNTRY}<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">network={<\/span>\n<span class=\"hljs-code-line\">    ssid=\"{SSID}\"<\/span>\n<span class=\"hljs-code-line\">    psk={PSK}<\/span>\n<span class=\"hljs-code-line\">}<\/span><\/code><\/pre>\n<p>Exchange <code>{COUNTRY}<\/code> with the <a href=\"https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1_alpha-2\" target=\"_blank\" rel=\"noopener noreferrer\">two-letter-code of your country<\/a> of your country. This is needed so that the pi knows on which bands it can send its wifi signal.<\/p><p>Exchange <code>{SSID}<\/code> with the name of your wifi.<\/p><p>Exchange <code>{PSK}<\/code> with a psk string. This is a encrypted version of your wifi password. You can generate this string <a href=\"https:\/\/www.wireshark.org\/tools\/wpa-psk.html\" target=\"_blank\" rel=\"noopener noreferrer\">on this website<\/a> or using the command line using <a href=\"https:\/\/linux.die.net\/man\/8\/wpa_passphrase\" target=\"_blank\" rel=\"noopener noreferrer\">wpa_passphrase<\/a> (when using the command line, watch out for special characters and spaces in your SSID or password).<\/p><p>You can add multiple <code>network<\/code> options if you want to save more than one set of wifi credentials.<\/p><p>Then unmount the sd card, put it to your pi, connect the pi to power. Wait some time (until the LED stops blinking), the pi will boot, expand the partition to the full size of the sd-card and then reboot.<\/p><p>If the info in your wpa_supplicant.conf was correct, the pi should automatically connect to your wifi. Otherwise, you can use an ethernet connection.<\/p><p>Open a terminal, ssh into the pi (<a href=\"https:\/\/www.raspberrypi.org\/documentation\/remote-access\/ssh\/\" target=\"_blank\" rel=\"noopener noreferrer\">look here<\/a> at <em>4. Set up your client<\/em> for more information; we took care of steps 1.\u20133. with the <em>wpa_supplicant<\/em> and the <em>ssh<\/em> file in the boot partition); the default user is <em>pi<\/em> and the default password is <em>raspberry.<\/em><\/p><p>Update your pi with <code>sudo apt update &amp;&amp; sudo apt upgrade -y<\/code>.<\/p><p>Start <em><a href=\"https:\/\/www.raspberrypi.org\/documentation\/configuration\/raspi-config.md\" target=\"_blank\" rel=\"noopener noreferrer\">raspi-config<\/a><\/em> with <code>sudo raspi-config<\/code> and under <em>System Options<\/em>, change the <em>Password<\/em> and the <em>Hostname. <\/em>Under <em>Localisation Options<\/em>, change the <em>Timezone.<\/em><\/p><p>Reboot your pi, reconnect via ssh (when rebooting, you get disconnected automatically) - if you changed your hostname, you now need to use <code>ssh pi@{NEW_HOSTNAME}<\/code>.<\/p><p>You maybe want to disable low power mode for wifi by running <code>sudo iwconfig wlan0 power off<\/code> so that the device will always be available. This is not needed when using Ethernet. To make this change consist over reboots, add this to <em>\/etc\/rc.local<\/em> before the final <em>exit 0<\/em> line: <code>\/sbin\/iwconfig wlan0 power off<\/code>.<\/p><h2 id=\"heading-ee2a7739-25ae-4fbd-b8b0-1072da3be1e6\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-ee2a7739-25ae-4fbd-b8b0-1072da3be1e6\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">3. Get the Audio Output Running<\/span>\n\n<\/h2>\n<p>If you use a shield for audio, see the corresponding manual how to set up audio output.<\/p><p>If you want to use the 3.5mm audio jack (on a big pi) or audio over HDIM, use <em>raspi-config<\/em> to select your audio output (<em>System Options - Audio<\/em>).<\/p><p>You can use <code>sudo aplay \/usr\/share\/sounds\/alsa\/Front_Center.wav<\/code> to test the audio output and <code>sudo alsamixer<\/code> to set the volume level<\/p><h2 id=\"heading-be7e0591-7224-4071-9fa1-ff03d2d42266\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-be7e0591-7224-4071-9fa1-ff03d2d42266\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">4. Headless Spotify Connect Player<\/span>\n\n<\/h2>\n<p>We use <a href=\"https:\/\/dtcooper.github.io\/raspotify\/\" target=\"_blank\" rel=\"noopener noreferrer\">raspotify<\/a> as a spotify connect client. To install it, we use the script provided on the webpage: <code>curl -sL https:\/\/dtcooper.github.io\/raspotify\/install.sh | sh<\/code> (you can see what the script does by typing <code>wget -qO- https:\/\/dtcooper.github.io\/raspotify\/install.sh<\/code> or opening the URL in a browser; this is generally a good idea before executing some script downloaded from the internet).<\/p><p>After installing, open the config file <em>\/etc\/default\/raspotify<\/em> in an editor of your choice, for example in <em>nano<\/em> by running <code>sudo nano \/etc\/default\/raspotify<\/code> (in nano, you can press [CTRL] + [X] and then [Y] to close and save the changes you made).<\/p><p>Change the line <code>#DEVICE_NAME=\"raspotify\"<\/code> to <code>DEVICE_NAME=\"{name}\"<\/code> (removed the <code>#<\/code> at the beginning and change the name; this is what the device will be called in Spotify)<br>You can also remove the <code>#<\/code> in front of <code>OPTIONS<\/code> and add your username and password. This is not required, your device will be visible to all devices in your wifi running Spotify by default, but if you add your username and password you can also access your device over the internet.<\/p><p>After changing and saving the options file, restart raspotify by typing <code>sudo systemctl restart raspotify<\/code><\/p><p>If you use a firewall on your pi, you may want to open some ports for Spotify Connect to work correctly; <a href=\"https:\/\/wiki.archlinux.org\/index.php\/Spotify#Spotify_does_not_detect_other_devices_on_local_network\" target=\"_blank\" rel=\"noopener noreferrer\">see this post for details<\/a>.<\/p><p>Now you should be able to open Spotify on another device and connect to this new Spotify Connect client. If you have problems, have a look at the <a href=\"https:\/\/github.com\/dtcooper\/raspotify\/issues\" target=\"_blank\" rel=\"noopener noreferrer\">raspotify GitHub page<\/a>.<\/p><h2 id=\"heading-a98193bc-b2d4-4f32-b106-8b6b1dbd482c\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-a98193bc-b2d4-4f32-b106-8b6b1dbd482c\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">5. Headless AirPlay Player<\/span>\n\n<\/h2>\n<p>We use <a href=\"https:\/\/github.com\/mikebrady\/shairport-sync\" target=\"_blank\" rel=\"noopener noreferrer\">shairport-sync<\/a> as a AirPlay client. To install it, run <code>sudo apt install shairport-sync -y<\/code>; this will install an older version, which is good enough, but if you want to run the newest version you can follow <a href=\"https:\/\/github.com\/mikebrady\/shairport-sync\/blob\/master\/BUILD.md\" target=\"_blank\" rel=\"noopener noreferrer\">these instructions<\/a>.<\/p><p>After installing, open the config file <em>\/etc\/shairport-sync.conf<\/em> in an editor of your choice, for example in <em>nano<\/em> by running <code>sudo nano \/etc\/shairport-sync.conf<\/code> (in nano, you can press [CTRL] + [X] and then [Y] to close and save the changes you made).<\/p><p>In the <em>general<\/em> area, remove the <code>\/\/<\/code> in front of <code>name = \"%H\";<\/code> and change <code>%H<\/code> to a name of your choice; this is the name that will appear in the AirPlay client select screen of your iOS device.<\/p><p>In the <em>sessioncontrol<\/em> area, remove the <code>\/\/<\/code> in front of <code>allowsessioninterruption = \"no\";<\/code> and change the <code>no<\/code> to <code>yes<\/code>, if you want to be able to allow another device to interrupt playing.<\/p><p>Run <code>sudo systemctl enable shairport-sync<\/code> to start shairport-sync automatically when booting, and then <code>sudo systemctl start shairport-sync<\/code> to start the AirPlay client. You can use <code>sudo systemctl status shairport-sync.service<\/code> to see if the service is running<\/p><p>Now you should be able to see the client in the AirPlay device selection dialog on an iOS device. If you have problems, have a look at the <a href=\"https:\/\/github.com\/mikebrady\/shairport-sync\/issues\" target=\"_blank\" rel=\"noopener noreferrer\">shairport-sync GitHub page<\/a>.<\/p><h2 id=\"heading-5c85a723-0209-421d-a5f4-23fceff24824\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-5c85a723-0209-421d-a5f4-23fceff24824\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">6. Headless Plexamp Player<\/span>\n\n<\/h2>\n<p><em>NOTE: I don't seem to get this to run reliably; as soon as you listen to something via Plexamp, the Spotify and AirPlay client stop working and you need to reboot the system. I stopped using the headless plexamp player and just use plexamp on my phone or tablet, with AirPlay. I might revisit this problem in the future, in the meantime the old solution is still here, but you have been warned. Here be dragons:<\/em><\/p><p>When running a <a href=\"https:\/\/www.plex.tv\/your-media\/music\/\" target=\"_blank\" rel=\"noopener noreferrer\">Plex Media Server for your music<\/a> and have a Plex Pass you can use <a href=\"https:\/\/plexamp.com\/\" target=\"_blank\" rel=\"noopener noreferrer\">Plexamp<\/a> to listen to your music collection in high quality.<\/p><p>We use <a href=\"https:\/\/diyfuturism.com\/index.php\/2020\/06\/14\/plexamp-music-streaming-with-raspberry-pi-zero\/\" target=\"_blank\" rel=\"noopener noreferrer\">this tutorial<\/a> which is based on <a href=\"https:\/\/forums.plex.tv\/t\/plexamp-for-raspberry-pi-release-notes\/368282\" target=\"_blank\" rel=\"noopener noreferrer\">this forum post<\/a>:<\/p><p>Get the correct nodejs version and install it by running <code>wget https:\/\/nodejs.org\/download\/release\/v9.11.2\/node-v9.11.2-linux-armv6l.tar.gz<\/code> and then <code>sudo tar -C \/usr\/local --strip-components=1 -xzf node-v9.11.2-linux-armv6l.tar.gz<\/code> (after that, you can removed the downloaded archive with <code>rm node-v9.11.2-linux-armv6l.tar.gz<\/code><\/p><p>Download the PlexAmp Raspberry Pi Beta by running <code>wget https:\/\/files.plexapp.com\/elan\/Plexamp-v2.0.0-rPi-beta.2.tar.bz2<\/code>, then unpack it with <code>bunzip2 Plexamp-v2.0.0-rPi-beta.2.tar.bz2<\/code> followed by <code>tar -xvf Plexamp-v2.0.0-rPi-beta.2.tar<\/code> (remove the original archive with <code>rm Plexamp-v2.0.0-rPi-beta.2.tar<\/code>)<\/p><p>Open the <em>\/home\/pi\/plexamp\/plexamp.service<\/em> file in an editor and change the <code>ExecStart=\/usr\/bin\/node \/home\/pi\/plexamp\/server\/server.prod.js<\/code> to <code>ExecStart=\/usr\/local\/bin\/node \/home\/pi\/plexamp\/server\/server.prod.js<\/code> to make sure that the correct node version is used<\/p><p>Now it get's a bit tricky - we need a <em>server.json<\/em> file that contains the authorization information. This file was generated by plexamp v.1 and v.2, the current plexamp v.3 doesn't generate this file anymore. I found an <a href=\"https:\/\/plexamp.plex.tv\/plexamp.plex.tv\/Plexamp%20Setup%201.1.0.exe\" target=\"_blank\" rel=\"noopener noreferrer\">old Windows installer for v.1.1.0<\/a>, which generates a <em>server.json<\/em> and places it in <em>C:\\Users\\{USERNAME}\\AppData\\Local\\Plexamp\\Plexamp<\/em>. Copy this file to your pi at <em>\/home\/pi\/.config\/Plexamp\/server.json<\/em>. The server.json looks something like this:<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">{<\/span>\n<span class=\"hljs-code-line\">  \"player\": {<\/span>\n<span class=\"hljs-code-line\">    \"name\": \"{NAME}\",<\/span>\n<span class=\"hljs-code-line\">    \"identifier\": \"{????}\"<\/span>\n<span class=\"hljs-code-line\">  },<\/span>\n<span class=\"hljs-code-line\">  \"user\": {<\/span>\n<span class=\"hljs-code-line\">    \"id\": {????},<\/span>\n<span class=\"hljs-code-line\">    \"token\": \"{????}\"<\/span>\n<span class=\"hljs-code-line\">  },<\/span>\n<span class=\"hljs-code-line\">  \"server\": {<\/span>\n<span class=\"hljs-code-line\">    \"identifier\": \"{????}\",<\/span>\n<span class=\"hljs-code-line\">    \"library\": \"\/library\/sections\/{????}\"<\/span>\n<span class=\"hljs-code-line\">  }<\/span>\n<span class=\"hljs-code-line\">}<\/span><\/code><\/pre>\n<p>copy the service file with <code>sudo cp \/home\/pi\/plexamp\/plexamp.service \/lib\/systemd\/system\/plexamp.service<\/code>, then reload the service files with <code>sudo systemctl daemon-reload<\/code>, enable the plexamp service with <code>sudo systemctl enable plexamp<\/code> and start it with <code>sudo systemctl start plexamp<\/code>. You can use <code>sudo systemctl status plexamp.service<\/code> to see if it is running.<\/p><p>Now you should be able to select the pi plexamp client in the plexamp player.<\/p><p>(You may need to also open port 32500)<\/p><h2 id=\"heading-00f000c4-cf7d-4ea6-aa37-55a86829bac9\" class=\"content-heading\">\n\n\t<span class=\"direct-link\"><a href=\"https:\/\/www.maxhaesslein.de\/sitemap\/#heading-00f000c4-cf7d-4ea6-aa37-55a86829bac9\" title=\"direct link to this heading\">\u2625<\/a><\/span>\n\t\n\t<span class=\"content-heading-title\">7. Overlay Filesystem<\/span>\n\n<\/h2>\n<p>We can protect the SD-card by using an overlay filesystem. As long as the overlay filesystem is active, the SD-card is read-only and nothing gets written to it, and after a reboot every change you made is gone.<br>You can use raspi-config to enable\/disable the overlay filesystem: start raspi-config with <code>sudo raspi-config<\/code> and then go to <em>Performance Options - Overlay File System<\/em> (you do not need to make the boot partition write-protected). After enabling it, the pi needs to reboot.<br>Don't forget to disable the overlay filesystem before making any changes to the config files or installing new software.<\/p>","date_published":"2020-11-15T11:27:00+00:00","date_modified":"2023-01-04T16:18:00+00:00"},{"id":"https:\/\/www.maxhaesslein.de\/notes\/build-a-raspberry-pi-vhs-player","url":"https:\/\/www.maxhaesslein.de\/notes\/build-a-raspberry-pi-vhs-player","title":"Build a Raspberry Pi VHS player","content_html":"<p>Build your own Raspberry Pi video player, that randomly plays snippets of old VHS footage from the <a href=\"https:\/\/archive.org\/details\/vhsvault\" target=\"_blank\" title=\"The VHS Vault\" rel=\"noopener noreferrer\">archive.org VHS vault<\/a>.<\/p><p><strong>NOTE: this is from 2020. It may no longer be up to date.<\/strong> I may revisit and update this in the future, in the meantime you are on your own.<\/p><p>Read more about this project <a href=\"https:\/\/www.maxhaesslein.de\/visual\/objects\/videodrome\/\" title=\"Videodrome\" rel=\"noopener noreferrer\">here<\/a>.<\/p><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/build-a-raspberry-pi-vhs-player\/d30cfe9f46-1733941282\/vimeo_414562775_887690910_2880x2160-800x-q80.jpg\" width=\"800\" height=\"600\" alt=\"\"><p>To download the movies I used wget, <a href=\"https:\/\/blog.archive.org\/2012\/04\/26\/downloading-in-bulk-using-wget\/\" rel=\"noreferrer\" target=\"_blank\">based on this article<\/a>. The command itself is just a oneliner, executed in multiple shells to download in parallel:<\/p>\n<pre class=\"hljs\"><code data-language=\"bash\"><span class=\"hljs-code-line\">wget -r -H -nc -np -nH --cut-dirs=1 -e robots=off -A .mp4,.m4v,.mov,.webm,.avi -l1 -i .\/source.txt -B <span class=\"hljs-string\">'http:\/\/archive.org\/download\/'<\/span><\/span><\/code><\/pre>\n<p>the <em>source.txt<\/em>, which was generated from crawling the html, has about 20000 lines and looks something like this:<\/p>\n<pre class=\"hljs\"><code data-language=\"\"><span class=\"hljs-code-line\"><span class=\"hljs-code-line\">wget -r -H -nc -np -nH --cut-dirs=1 -e robots=off -A .mp4,.m4v,.mov,.webm,.avi -l1 -i .\/source.txt -B <span class=\"hljs-string\">'http:\/\/archive.org\/download\/'<\/span><\/span><\/span><\/code><\/pre>\n<p>I let the download run for about 30 hours and that gave me 320 videos. Thats 120 GB or 24 hours playtime in total. The complete VHS Vault is much larger.<\/p>\n<p>The Raspberry Pi is a model 3B and uses a small 2GB microSD card with <em>Raspbian Buster Lite<\/em>. I installed some dependecies with<\/p>\n<pre class=\"hljs\"><code data-language=\"bash\"><span class=\"hljs-code-line\">sudo apt install omxplayer mediainfo timelimit<\/span><\/code><\/pre>\n<p>I use <em>omxplayer<\/em> as the mediaplayer, because it is hardware accelerated on the Pi. <em>mediainfo<\/em> is for getting the length of a videofile and <em>timelimit<\/em> ends the <em>omxplayer<\/em> process after a specific time if the video is still playing.<\/p>\n<pre class=\"hljs\"><code data-language=\"bash\"><span class=\"hljs-code-line\"><span class=\"hljs-meta\">#!\/bin\/bash<\/span>\n<span class=\"hljs-code-line\"><\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># dependencies: omxplayer mediainfo timelimit<\/span><\/span>\n<span class=\"hljs-code-line\"><\/span>\n<span class=\"hljs-code-line\">MAXSECONDS=30 <span class=\"hljs-comment\"># maximum duration of clip<\/span><\/span>\n<span class=\"hljs-code-line\"><\/span>\n<span class=\"hljs-code-line\">sudo mount -o ro \/dev\/sda1 \/media\/usb<\/span>\n<span class=\"hljs-code-line\">FOLDER=<span class=\"hljs-string\">\"\/media\/usb\/videodrome\"<\/span><\/span>\n<span class=\"hljs-code-line\"><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">while<\/span> <span class=\"hljs-literal\">true<\/span>; <span class=\"hljs-keyword\">do<\/span> <span class=\"hljs-comment\"># infinite loop<\/span><\/span>\n<span class=\"hljs-code-line\"><\/span>\n<span class=\"hljs-code-line\">    pkill omxplayer <span class=\"hljs-comment\"># kill omxplayer if it is still active (somehow 'timelimit' does not do this)<\/span><\/span>\n<span class=\"hljs-code-line\"><\/span>\n<span class=\"hljs-code-line\">    FILE=$( find <span class=\"hljs-string\">\"<span class=\"hljs-variable\">$FOLDER<\/span>\/\"<\/span> -<span class=\"hljs-built_in\">type<\/span> f -print0 | shuf -z -n 1 ) <span class=\"hljs-comment\"># pick random file<\/span><\/span>\n<span class=\"hljs-code-line\">    clear <span class=\"hljs-comment\"># clear screen<\/span><\/span>\n<span class=\"hljs-code-line\"><\/span>\n<span class=\"hljs-code-line\">    LENGTH=<span class=\"hljs-string\">\"<span class=\"hljs-variable\">$(mediainfo --Inform=\"Video;%Duration%\" $FILE)<\/span>\"<\/span> <span class=\"hljs-comment\"># length is in ms<\/span><\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> [ -z <span class=\"hljs-string\">\"<span class=\"hljs-variable\">$LENGTH<\/span>\"<\/span> ]; <span class=\"hljs-keyword\">then<\/span> <span class=\"hljs-comment\"># cannot get length; just assume 60 seconds<\/span><\/span>\n<span class=\"hljs-code-line\">        LENGTH=60000<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">fi<\/span><\/span>\n<span class=\"hljs-code-line\">    ((LENGTH=<span class=\"hljs-variable\">${LENGTH}<\/span>\/1000)) <span class=\"hljs-comment\"># convert to seconds<\/span><\/span>\n<span class=\"hljs-code-line\">    ((MAXPOS=<span class=\"hljs-variable\">$LENGTH<\/span> - 5))<\/span>\n<span class=\"hljs-code-line\">    ((RANDOMPOS=<span class=\"hljs-string\">\"<span class=\"hljs-variable\">$(shuf -i 0-$MAXPOS -n 1)<\/span>\"<\/span>)) <span class=\"hljs-comment\"># get a random position between 0 and $MAXPOS<\/span><\/span>\n<span class=\"hljs-code-line\">    ((h=<span class=\"hljs-variable\">$RANDOMPOS<\/span>\/3600))<\/span>\n<span class=\"hljs-code-line\">    ((m=(<span class=\"hljs-variable\">$RANDOMPOS<\/span>%3600)\/60))<\/span>\n<span class=\"hljs-code-line\">    ((s=<span class=\"hljs-variable\">$RANDOMPOS<\/span>%60))<\/span>\n<span class=\"hljs-code-line\">    RANDOMPOSOMX=<span class=\"hljs-string\">\"<span class=\"hljs-variable\">$(printf \"%02d:%02d:%02d\" $h $m $s)<\/span>\"<\/span> <span class=\"hljs-comment\"># convert to hh:mm:ss<\/span><\/span>\n<span class=\"hljs-code-line\">    ((RANDOMLENGTH=<span class=\"hljs-string\">\"<span class=\"hljs-variable\">$(shuf -i 0-$MAXSECONDS -n 1)<\/span>\"<\/span>))<\/span>\n<span class=\"hljs-code-line\">    timelimit -t <span class=\"hljs-variable\">$RANDOMLENGTH<\/span> -T <span class=\"hljs-variable\">$RANDOMLENGTH<\/span> -q omxplayer --pos <span class=\"hljs-variable\">$RANDOMPOSOMX<\/span> --no-osd <span class=\"hljs-variable\">$FILE<\/span> > \/dev\/null<\/span>\n<span class=\"hljs-code-line\"><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">done<\/span><\/span><\/code><\/pre>\n<p>The script to play the videos is just a small bash script that mounts an usb thumb drive, selects a random video from the drive, gets its length, selects a random position between 0 and the video length and generates a random clip length. It then uses <em>omxplayer<\/em> to play the videofile. The Pi 3B is fast enough to play the videos from archive.org without the need to convert them to a more suitable format. The Pi is set to <em>Autologin (Console)<\/em> and starts the bash script on log in. I also set the <em>Overlay FS<\/em> option in raspi-config and mount the usb-drive as read only to reduce data loss when unplugging the Pi. Because I use an old tube television with a scart connector I use the Pis composite output.<\/p>\n<p>Long live the new flesh!<\/p>","date_published":"2020-06-06T15:48:00+00:00"},{"id":"https:\/\/www.maxhaesslein.de\/notes\/build-your-own-basic-raspberry-pi-audio-sampler","url":"https:\/\/www.maxhaesslein.de\/notes\/build-your-own-basic-raspberry-pi-audio-sampler","title":"Build your own basic Raspberry Pi audio sampler","content_html":"<p>Build your own basic audio sampler that can play mp3 sounds from a usb thumb drive, written in Python.<\/p><p>You can read more about this project <a href=\"https:\/\/www.maxhaesslein.de\/audio\/tools\/victoria\/\" title=\"Victoria\" rel=\"noopener noreferrer\">here<\/a>.<\/p><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/build-your-own-basic-raspberry-pi-audio-sampler\/5f0ac75a97-1733941282\/1070614-800x-q80.jpg\" width=\"800\" height=\"534\" alt=\"\"><p>It uses Pimoronis <a href=\"https:\/\/shop.pimoroni.com\/products\/piano-hat\" target=\"blank\" rel=\"noopener noreferrer\">Piano HAT<\/a>, <a href=\"https:\/\/shop.pimoroni.com\/products\/drum-hat\" target=\"blank\" rel=\"noopener noreferrer\">Drum HAT<\/a>, <a href=\"https:\/\/shop.pimoroni.com\/products\/phat-stack\" target=\"blank\" rel=\"noopener noreferrer\">pHAT Stack<\/a> and <a href=\"https:\/\/www.adafruit.com\/product\/4037\" target=\"blank\" rel=\"noopener noreferrer\">Adafruits I2S Audio Bonnet<\/a> (Pimoronis <a href=\"https:\/\/shop.pimoroni.com\/products\/phat-dac\" target=\"blank\" rel=\"noopener noreferrer\">pHAT DAC<\/a> also works; for direct output to speakers the <a href=\"https:\/\/www.hifiberry.com\/shop\/boards\/miniamp\/\" target=\"blank\" rel=\"noopener noreferrer\">HiFiBerry MiniAmp<\/a> is a good option). A <a href=\"https:\/\/www.raspberrypi.org\/products\/raspberry-pi-zero-w\/\" target=\"blank\" rel=\"noopener noreferrer\">Raspberry Pi Zero W<\/a> was soldered directly onto the pHAT Stack with enough spacing to put the Audio Bonnet on top. Only one micro USB cable is needed to power the sampler, so a power bank can be used to play on the go. One of those mobile phone OTG thumb drives is used (for example the <a href=\"https:\/\/transcend-info.com\/Products\/No-691\" target=\"blank\" rel=\"noopener noreferrer\">Transcend JetFlash 880<\/a> is the perfect size to fit next to the power connector) because it plugs directly into the Pi Zeros micro USB interface, while also providing a USB Type-A connector.On the USB thumb drive are two directories: <em>drums<\/em> and <em>piano<\/em>. The <em>drums<\/em> folder can hold up to 8 samples which can be played via the Drum HAT, the <em>piano<\/em> folder can hold hundreds of samples which can be played via the Piano HAT; with the <em>Octave Up<\/em> \/ <em>Octave Down<\/em> buttons one can cycle through the samples in batches of 13.<\/p><p>When holding the <em>Instrument<\/em> button and pressing either <em>Octave Up<\/em> or <em>Octave Down<\/em> one can change the output volume. Holding <em>Instrument<\/em> and pressing the Drum HAT <em>pad #8<\/em> two times will shut down the Pi.<\/p><p>The code is written in Python and is based on the Piano HATs <a href=\"https:\/\/github.com\/pimoroni\/Piano-HAT\/blob\/master\/examples\/simple-piano.py\" target=\"_blank\" rel=\"noopener noreferrer\">simple-piano.py example<\/a> and the Drum HATs <a href=\"https:\/\/github.com\/pimoroni\/drum-hat\/blob\/master\/examples\/drums.py\" target=\"_blank\" rel=\"noopener noreferrer\">drums.py example<\/a>.<\/p><p>The code is written in Python:<\/p><pre class=\"hljs\"><code data-language=\"python\"><span class=\"hljs-code-line\"><span class=\"hljs-comment\">#!\/usr\/bin\/python<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\">#############################<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># Victoria<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># by maxhaesslein, 2020-2022<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># https:\/\/maxhaesslein.de<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\">#############################<\/span><\/span>\n<span class=\"hljs-code-line\">version = <span class=\"hljs-string\">\"2.0.0\"<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># this script gets started automatically on login via crontab.<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># user 'crontab' -e and add:<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># @reboot ~\/victoria\/victoria<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">import<\/span> glob<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">import<\/span> stat<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">import<\/span> os<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">import<\/span> re<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">import<\/span> signal<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">import<\/span> time<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">from<\/span> sys <span class=\"hljs-keyword\">import<\/span> exit<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">import<\/span> pygame<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">import<\/span> pianohat<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">import<\/span> drumhat<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">import<\/span> buttonshim<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">BANK_PIANO = os.path.join(os.path.dirname(__file__), <span class=\"hljs-string\">\"sounds\/piano\"<\/span>)<\/span>\n<span class=\"hljs-code-line\">BANK_DRUMS = os.path.join(os.path.dirname(__file__), <span class=\"hljs-string\">\"sounds\/drums2\"<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">MOUNT_PATH = <span class=\"hljs-string\">'\/mnt\/victoria_usb'<\/span><\/span>\n<span class=\"hljs-code-line\">MOUNT_VOLUME = <span class=\"hljs-string\">'\/dev\/sda1'<\/span><\/span>\n<span class=\"hljs-code-line\">FILETYPES = [<span class=\"hljs-string\">'*.wav'<\/span>, <span class=\"hljs-string\">'*.WAV'<\/span>, <span class=\"hljs-string\">'*.ogg'<\/span>, <span class=\"hljs-string\">'*.OGG'<\/span>]<\/span>\n<span class=\"hljs-code-line\">use_drive = <span class=\"hljs-literal\">False<\/span> <span class=\"hljs-comment\"># gets set to true if the volume is mounted<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">volume_step = <span class=\"hljs-number\">1.0<\/span>\/<span class=\"hljs-number\">13.0<\/span><\/span>\n<span class=\"hljs-code-line\">global_volume = <span class=\"hljs-number\">7.0<\/span>\/<span class=\"hljs-number\">13.0<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">modus = <span class=\"hljs-string\">\"default\"<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">buffer_size = <span class=\"hljs-number\">2048<\/span> <span class=\"hljs-comment\"># mixer buffer size. default: 512. smaller = less latency, but may have buffer underrun and scratchy sound. must be power of 2<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">running = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\">update_channel = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">print(<span class=\"hljs-string\">\"Press CTRL+C to exit.\"<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">octave_piano = <span class=\"hljs-number\">0<\/span><\/span>\n<span class=\"hljs-code-line\">octaves_piano = <span class=\"hljs-number\">0<\/span><\/span>\n<span class=\"hljs-code-line\">instrument_button_down = <span class=\"hljs-literal\">False<\/span><\/span>\n<span class=\"hljs-code-line\">shutdown_button_counter = <span class=\"hljs-number\">0<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">samples_piano = []<\/span>\n<span class=\"hljs-code-line\">samples_drums = []<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">channels = []<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">options = {<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-string\">'samplerate'<\/span>: <span class=\"hljs-number\">44100<\/span>,<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-string\">'folder_piano'<\/span>: <span class=\"hljs-string\">'\/piano'<\/span>,<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-string\">'folder_drums'<\/span>: <span class=\"hljs-string\">'\/drums'<\/span>,<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-string\">'channels'<\/span>: <span class=\"hljs-number\">32<\/span>,<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-string\">'use_display'<\/span>: <span class=\"hljs-literal\">True<\/span>,<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-string\">'fps'<\/span>: <span class=\"hljs-number\">4<\/span><\/span>\n<span class=\"hljs-code-line\">    }<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">display_content = [<span class=\"hljs-literal\">False<\/span>, <span class=\"hljs-literal\">False<\/span>]<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">pianohat.auto_leds(<span class=\"hljs-literal\">False<\/span>)<\/span>\n<span class=\"hljs-code-line\">drumhat.auto_leds = <span class=\"hljs-literal\">False<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">buttonshim.set_pixel(<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0xff<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">set_display<\/span><span class=\"hljs-params\">( line1 = False, line2 = False )<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">global<\/span> display_content<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    display_content[<span class=\"hljs-number\">0<\/span>] = line1<\/span>\n<span class=\"hljs-code-line\">    display_content[<span class=\"hljs-number\">1<\/span>] = line2<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">set_volume<\/span><span class=\"hljs-params\">( direction )<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">global<\/span> global_volume<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> direction &gt; <span class=\"hljs-number\">0<\/span>:<\/span>\n<span class=\"hljs-code-line\">        global_volume += volume_step<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">elif<\/span> direction &lt; <span class=\"hljs-number\">0<\/span>:<\/span>\n<span class=\"hljs-code-line\">        global_volume -= volume_step<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> global_volume &gt;= <span class=\"hljs-number\">1<\/span>:<\/span>\n<span class=\"hljs-code-line\">        global_volume = <span class=\"hljs-number\">1.0<\/span><\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">elif<\/span> global_volume &lt;= <span class=\"hljs-number\">0<\/span>:<\/span>\n<span class=\"hljs-code-line\">        global_volume = <span class=\"hljs-number\">0.0<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    max_led = int(round(global_volume * <span class=\"hljs-number\">13<\/span>))<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> range(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">13<\/span>):<\/span>\n<span class=\"hljs-code-line\">        pianohat.set_led(i, <span class=\"hljs-literal\">False<\/span>)<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> range(<span class=\"hljs-number\">0<\/span>, max_led):<\/span>\n<span class=\"hljs-code-line\">        pianohat.set_led(i, <span class=\"hljs-literal\">True<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    print(<span class=\"hljs-string\">\"set volume to \"<\/span>+str(global_volume))<\/span>\n<span class=\"hljs-code-line\">    set_display(<span class=\"hljs-string\">\"\"<\/span>,<span class=\"hljs-string\">\"volume: \"<\/span>+str(round(global_volume*<span class=\"hljs-number\">100<\/span>))+<span class=\"hljs-string\">\"%\"<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">for<\/span> channel <span class=\"hljs-keyword\">in<\/span> channels:<\/span>\n<span class=\"hljs-code-line\">        channel[<span class=\"hljs-string\">'channel'<\/span>].set_volume( global_volume )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">get_filename<\/span><span class=\"hljs-params\">(path)<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    filename = os.path.basename(path)<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">return<\/span> filename<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">reset_playing_channel_leds<\/span><span class=\"hljs-params\">()<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> range(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">13<\/span>):<\/span>\n<span class=\"hljs-code-line\">        pianohat.set_led(i, <span class=\"hljs-literal\">False<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> modus == <span class=\"hljs-string\">\"default\"<\/span> <span class=\"hljs-keyword\">or<\/span> modus == <span class=\"hljs-string\">\"toggle\"<\/span>:<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">for<\/span> channel <span class=\"hljs-keyword\">in<\/span> channels:<\/span>\n<span class=\"hljs-code-line\">            sound = channel[<span class=\"hljs-string\">'channel'<\/span>].get_sound()<\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">if<\/span> channel[<span class=\"hljs-string\">'hat'<\/span>] != <span class=\"hljs-string\">'pianohat'<\/span>:<\/span>\n<span class=\"hljs-code-line\">                <span class=\"hljs-keyword\">continue<\/span><\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">if<\/span> sound <span class=\"hljs-keyword\">is<\/span> <span class=\"hljs-keyword\">not<\/span> <span class=\"hljs-literal\">None<\/span>:<\/span>\n<span class=\"hljs-code-line\">                print(<span class=\"hljs-string\">\"playing led of this channel: \"<\/span>+str(channel[<span class=\"hljs-string\">'led'<\/span>]))<\/span>\n<span class=\"hljs-code-line\">                led = channel[<span class=\"hljs-string\">'led'<\/span>] - (octave_piano*<span class=\"hljs-number\">13<\/span>)<\/span>\n<span class=\"hljs-code-line\">                <span class=\"hljs-keyword\">if<\/span> led &gt;= <span class=\"hljs-number\">0<\/span> <span class=\"hljs-keyword\">and<\/span> led &lt; <span class=\"hljs-number\">13<\/span>:<\/span>\n<span class=\"hljs-code-line\">                    pianohat.set_led( led, <span class=\"hljs-literal\">True<\/span> )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">handle_note<\/span><span class=\"hljs-params\">(channel_org, pressed)<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    channel = channel_org + (<span class=\"hljs-number\">13<\/span> * octave_piano)<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> pressed:<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> channel &lt; len(samples_piano):<\/span>\n<span class=\"hljs-code-line\">            pianohat.set_led( channel_org, <span class=\"hljs-literal\">True<\/span> )<\/span>\n<span class=\"hljs-code-line\">            filename = samples_piano[channel][<span class=\"hljs-string\">'filename'<\/span>]<\/span>\n<span class=\"hljs-code-line\">            print( <span class=\"hljs-string\">\"Piano #{}\"<\/span>.format(channel) )<\/span>\n<span class=\"hljs-code-line\">            set_display( <span class=\"hljs-string\">\"Piano #{}\"<\/span>.format(channel), get_filename(filename) )<\/span>\n<span class=\"hljs-code-line\">            play_sound( filename, <span class=\"hljs-string\">'pianohat'<\/span>, channel_org )<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">else<\/span>:<\/span>\n<span class=\"hljs-code-line\">            print( <span class=\"hljs-string\">'Piano #'<\/span>+str(channel)+<span class=\"hljs-string\">' has no sound'<\/span> )<\/span>\n<span class=\"hljs-code-line\">            set_display( <span class=\"hljs-string\">'Piano #'<\/span>+str(channel)+<span class=\"hljs-string\">' has no sound'<\/span> )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">handle_instrument<\/span><span class=\"hljs-params\">(channel, pressed)<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">global<\/span> instrument_button_down<\/span>\n<span class=\"hljs-code-line\">    instrument_button_down = pressed<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    pianohat.set_led(<span class=\"hljs-number\">13<\/span>, <span class=\"hljs-literal\">False<\/span>)<\/span>\n<span class=\"hljs-code-line\">    pianohat.set_led(<span class=\"hljs-number\">14<\/span>, <span class=\"hljs-literal\">False<\/span>)<\/span>\n<span class=\"hljs-code-line\">    pianohat.set_led(<span class=\"hljs-number\">15<\/span>, <span class=\"hljs-literal\">False<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> pressed:<\/span>\n<span class=\"hljs-code-line\">        pianohat.set_led(channel, <span class=\"hljs-literal\">True<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        max_led = int(round(global_volume * <span class=\"hljs-number\">13<\/span>))<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> range(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">13<\/span>):<\/span>\n<span class=\"hljs-code-line\">            pianohat.set_led(i, <span class=\"hljs-literal\">False<\/span>)<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> range(<span class=\"hljs-number\">0<\/span>, max_led):<\/span>\n<span class=\"hljs-code-line\">            pianohat.set_led(i, <span class=\"hljs-literal\">True<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">else<\/span>:<\/span>\n<span class=\"hljs-code-line\">        pianohat.set_led(channel, <span class=\"hljs-literal\">False<\/span>)<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">global<\/span> shutdown_button_counter<\/span>\n<span class=\"hljs-code-line\">        shutdown_button_counter = <span class=\"hljs-number\">0<\/span><\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> range(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">16<\/span>):<\/span>\n<span class=\"hljs-code-line\">            pianohat.set_led(i, <span class=\"hljs-literal\">False<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        reset_playing_channel_leds()<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">set_octave<\/span><span class=\"hljs-params\">( direction )<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">global<\/span> octave_piano<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> direction &gt; <span class=\"hljs-number\">0<\/span>:<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> octave_piano &lt;= (octaves_piano<span class=\"hljs-number\">-1<\/span>):<\/span>\n<span class=\"hljs-code-line\">            octave_piano += <span class=\"hljs-number\">1<\/span><\/span>\n<span class=\"hljs-code-line\">            print(<span class=\"hljs-string\">'piano set: {}'<\/span>.format(octave_piano))<\/span>\n<span class=\"hljs-code-line\">            set_display( <span class=\"hljs-string\">\"piano set: {}\"<\/span>.format(octave_piano+<span class=\"hljs-number\">1<\/span>) )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">elif<\/span> direction &lt; <span class=\"hljs-number\">0<\/span>:<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> octave_piano &gt; <span class=\"hljs-number\">0<\/span>:<\/span>\n<span class=\"hljs-code-line\">            octave_piano -= <span class=\"hljs-number\">1<\/span><\/span>\n<span class=\"hljs-code-line\">            print(<span class=\"hljs-string\">'piano set: {}'<\/span>.format(octave_piano))<\/span>\n<span class=\"hljs-code-line\">            set_display( <span class=\"hljs-string\">\"piano set: {}\"<\/span>.format(octave_piano+<span class=\"hljs-number\">1<\/span>) )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    reset_playing_channel_leds()<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">handle_octave_up<\/span><span class=\"hljs-params\">(channel, pressed)<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    pianohat.set_led(channel, pressed)<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> instrument_button_down:<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> pressed:<\/span>\n<span class=\"hljs-code-line\">            set_volume( +<span class=\"hljs-number\">1<\/span> )<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">else<\/span>:<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> pressed:<\/span>\n<span class=\"hljs-code-line\">            set_octave( +<span class=\"hljs-number\">1<\/span> )<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">else<\/span>:<\/span>\n<span class=\"hljs-code-line\">            reset_playing_channel_leds()<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">handle_octave_down<\/span><span class=\"hljs-params\">(channel, pressed)<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    pianohat.set_led(channel, pressed)<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> instrument_button_down:<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> pressed:<\/span>\n<span class=\"hljs-code-line\">            set_volume( <span class=\"hljs-number\">-1<\/span> )<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">else<\/span>:<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> pressed:<\/span>\n<span class=\"hljs-code-line\">            set_octave( <span class=\"hljs-number\">-1<\/span> )<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">else<\/span>:<\/span>\n<span class=\"hljs-code-line\">            reset_playing_channel_leds()<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">handle_drums_hit<\/span><span class=\"hljs-params\">(event)<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-comment\"># event.channel is a zero based channel index for each pad<\/span><\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-comment\"># event.pad is the pad number from 1 to 8<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-comment\"># maybe handle shutdown:<\/span><\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> event.pad == <span class=\"hljs-number\">8<\/span> <span class=\"hljs-keyword\">and<\/span> instrument_button_down:<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">global<\/span> shutdown_button_counter<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        shutdown_button_counter += <span class=\"hljs-number\">1<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> shutdown_button_counter &gt; <span class=\"hljs-number\">0<\/span>:<\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> range(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">16<\/span>):<\/span>\n<span class=\"hljs-code-line\">                pianohat.set_led(i, <span class=\"hljs-literal\">True<\/span>)<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> shutdown_button_counter &gt; <span class=\"hljs-number\">1<\/span>:<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">            buttonshim.set_pixel(<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0xff<\/span>)<\/span>\n<span class=\"hljs-code-line\">            reset_led = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">            drumhat.all_on()<\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> range(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">16<\/span>):<\/span>\n<span class=\"hljs-code-line\">                pianohat.set_led(i, <span class=\"hljs-literal\">False<\/span>)<\/span>\n<span class=\"hljs-code-line\">                time.sleep(<span class=\"hljs-number\">0.02<\/span>)<\/span>\n<span class=\"hljs-code-line\">            drumhat.all_off()<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">            shutdown_action( <span class=\"hljs-literal\">False<\/span> )<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">return<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">try<\/span>:<\/span>\n<span class=\"hljs-code-line\">        filename = samples_drums[event.channel][<span class=\"hljs-string\">'filename'<\/span>]<\/span>\n<span class=\"hljs-code-line\">        drumhat.led_on(event.pad)<\/span>\n<span class=\"hljs-code-line\">        print( <span class=\"hljs-string\">\"Pad {}\"<\/span>.format(event.pad) )<\/span>\n<span class=\"hljs-code-line\">        set_display( <span class=\"hljs-string\">\"Pad {}\"<\/span>.format(event.pad), get_filename(filename) )<\/span>\n<span class=\"hljs-code-line\">        play_sound( filename, <span class=\"hljs-string\">'drumhat'<\/span>, event.pad )<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">except<\/span> IndexError:<\/span>\n<span class=\"hljs-code-line\">        print(<span class=\"hljs-string\">\"Pad {} has no sound\"<\/span>.format(event.pad))<\/span>\n<span class=\"hljs-code-line\">        set_display( <span class=\"hljs-string\">\"Pad {} has no sound\"<\/span>.format(event.pad) )<\/span>\n<span class=\"hljs-code-line\">        drumhat.led_off(event.pad)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">handle_drums_release<\/span><span class=\"hljs-params\">()<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">pass<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">play_sound<\/span><span class=\"hljs-params\">( filename, hat, led )<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">global<\/span> update_channel<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> modus == <span class=\"hljs-string\">\"default\"<\/span> <span class=\"hljs-keyword\">or<\/span> modus == <span class=\"hljs-string\">\"toggle\"<\/span>:<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">for<\/span> channel <span class=\"hljs-keyword\">in<\/span> channels:<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">if<\/span> channel[<span class=\"hljs-string\">'hat'<\/span>] != hat:<\/span>\n<span class=\"hljs-code-line\">                <span class=\"hljs-keyword\">continue<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">if<\/span> channel[<span class=\"hljs-string\">'hat'<\/span>] == <span class=\"hljs-string\">'pianohat'<\/span>:<\/span>\n<span class=\"hljs-code-line\">                relative_led = channel[<span class=\"hljs-string\">'led'<\/span>] - (octave_piano*<span class=\"hljs-number\">13<\/span>)<\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">else<\/span>:<\/span>\n<span class=\"hljs-code-line\">                relative_led = channel[<span class=\"hljs-string\">'led'<\/span>]<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">if<\/span> relative_led == led:<\/span>\n<span class=\"hljs-code-line\">                <span class=\"hljs-keyword\">if<\/span> modus == <span class=\"hljs-string\">\"default\"<\/span>:<\/span>\n<span class=\"hljs-code-line\">                    print( <span class=\"hljs-string\">'  this  hat\/channel already plays a sound'<\/span>)<\/span>\n<span class=\"hljs-code-line\">                    set_display( <span class=\"hljs-string\">''<\/span>,<span class=\"hljs-string\">'already playing sound'<\/span> )<\/span>\n<span class=\"hljs-code-line\">                    <span class=\"hljs-keyword\">return<\/span><\/span>\n<span class=\"hljs-code-line\">                <span class=\"hljs-keyword\">elif<\/span> modus == <span class=\"hljs-string\">\"toggle\"<\/span>:<\/span>\n<span class=\"hljs-code-line\">                    channel[<span class=\"hljs-string\">'channel'<\/span>].fadeout(<span class=\"hljs-number\">400<\/span>)<\/span>\n<span class=\"hljs-code-line\">                    print(<span class=\"hljs-string\">\"  stopping this sound\"<\/span>)<\/span>\n<span class=\"hljs-code-line\">                    set_display( <span class=\"hljs-string\">''<\/span>,<span class=\"hljs-string\">'stopping sound'<\/span> )<\/span>\n<span class=\"hljs-code-line\">                    <span class=\"hljs-keyword\">return<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> len(channels) &gt;= options[<span class=\"hljs-string\">'channels'<\/span>]:<\/span>\n<span class=\"hljs-code-line\">        print(<span class=\"hljs-string\">\"  no free channel, abort playing\"<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> hat == <span class=\"hljs-string\">\"pianohat\"<\/span>:<\/span>\n<span class=\"hljs-code-line\">            pianohat.set_led( led, <span class=\"hljs-literal\">False<\/span> )<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">elif<\/span> hat == <span class=\"hljs-string\">\"drumhat\"<\/span>:<\/span>\n<span class=\"hljs-code-line\">            drumhat.led_off( led )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">return<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    print( <span class=\"hljs-string\">'  Playing Sound: {}'<\/span>.format(filename))<\/span>\n<span class=\"hljs-code-line\">    sound = pygame.mixer.Sound( file=filename )<\/span>\n<span class=\"hljs-code-line\">    channelObj = pygame.mixer.find_channel()<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    loops_number = <span class=\"hljs-number\">0<\/span><\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> modus == <span class=\"hljs-string\">\"toggle\"<\/span>:<\/span>\n<span class=\"hljs-code-line\">        loops_number = <span class=\"hljs-number\">-1<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    channelObj.play( sound, loops=loops_number )<\/span>\n<span class=\"hljs-code-line\">    channelObj.set_volume( global_volume )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> hat == <span class=\"hljs-string\">'pianohat'<\/span>:<\/span>\n<span class=\"hljs-code-line\">        led_octave = led + (octave_piano*<span class=\"hljs-number\">13<\/span>)<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">else<\/span>:<\/span>\n<span class=\"hljs-code-line\">        led_octave = led<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    channel = {<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-string\">'filename'<\/span>: filename,<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-string\">'hat'<\/span>: hat,<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-string\">'led'<\/span>: led_octave,<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-string\">'channel'<\/span>: channelObj<\/span>\n<span class=\"hljs-code-line\">    }<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    channels.append( channel )<\/span>\n<span class=\"hljs-code-line\">    update_channel = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> modus == <span class=\"hljs-string\">\"chaos\"<\/span>:<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> hat == <span class=\"hljs-string\">\"pianohat\"<\/span>:<\/span>\n<span class=\"hljs-code-line\">            pianohat.set_led( led, <span class=\"hljs-literal\">False<\/span> )<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">elif<\/span> hat == <span class=\"hljs-string\">\"drumhat\"<\/span>:<\/span>\n<span class=\"hljs-code-line\">            drumhat.led_off( led )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">shutdown_action<\/span><span class=\"hljs-params\">( skip_shutdown )<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">global<\/span> running<\/span>\n<span class=\"hljs-code-line\">    print( <span class=\"hljs-string\">'starting shut down ...'<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    buttonshim.set_pixel(<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0xff<\/span>)<\/span>\n<span class=\"hljs-code-line\">    reset_led = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    running = <span class=\"hljs-literal\">False<\/span><\/span>\n<span class=\"hljs-code-line\">    time.sleep(<span class=\"hljs-number\">0.4<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> options[<span class=\"hljs-string\">'use_display'<\/span>]:<\/span>\n<span class=\"hljs-code-line\">        draw.rectangle((<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>, width, height), outline=<span class=\"hljs-number\">0<\/span>, fill=<span class=\"hljs-number\">0<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        text = <span class=\"hljs-string\">\"bye!\"<\/span><\/span>\n<span class=\"hljs-code-line\">        (font_width, font_height) = font.getsize(text)<\/span>\n<span class=\"hljs-code-line\">        draw.text(<\/span>\n<span class=\"hljs-code-line\">            (disp.width \/\/ <span class=\"hljs-number\">2<\/span> - font_width \/\/ <span class=\"hljs-number\">2<\/span>, disp.height \/\/ <span class=\"hljs-number\">2<\/span> - font_height \/\/ <span class=\"hljs-number\">2<\/span>),<\/span>\n<span class=\"hljs-code-line\">            text,<\/span>\n<span class=\"hljs-code-line\">            font=font,<\/span>\n<span class=\"hljs-code-line\">            fill=<span class=\"hljs-number\">255<\/span>,<\/span>\n<span class=\"hljs-code-line\">        )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        disp.image(image)<\/span>\n<span class=\"hljs-code-line\">        disp.show()<\/span>\n<span class=\"hljs-code-line\">        time.sleep(<span class=\"hljs-number\">0.1<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    print(<span class=\"hljs-string\">\"mixer fadeout (1s)\"<\/span>)<\/span>\n<span class=\"hljs-code-line\">    pygame.mixer.fadeout(<span class=\"hljs-number\">1000<\/span>)<\/span>\n<span class=\"hljs-code-line\">    time.sleep(<span class=\"hljs-number\">1<\/span>)<\/span>\n<span class=\"hljs-code-line\">    pygame.mixer.quit()<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> range(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">16<\/span>):<\/span>\n<span class=\"hljs-code-line\">        pianohat.set_led(i, <span class=\"hljs-literal\">False<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    drumhat.all_off()<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> options[<span class=\"hljs-string\">'use_display'<\/span>]:<\/span>\n<span class=\"hljs-code-line\">        print(<span class=\"hljs-string\">\"clear display\"<\/span>)<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-comment\"># clear display:<\/span><\/span>\n<span class=\"hljs-code-line\">        draw.rectangle((<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>, width, height), outline=<span class=\"hljs-number\">0<\/span>, fill=<span class=\"hljs-number\">0<\/span>)<\/span>\n<span class=\"hljs-code-line\">        disp.image(image)<\/span>\n<span class=\"hljs-code-line\">        disp.show()<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> use_drive:<\/span>\n<span class=\"hljs-code-line\">        print( <span class=\"hljs-string\">'unmounting '<\/span>+str(MOUNT_PATH) )<\/span>\n<span class=\"hljs-code-line\">        os.system( <span class=\"hljs-string\">'sudo umount '<\/span>+str(MOUNT_PATH) )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    buttonshim.set_pixel(<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0xff<\/span>)<\/span>\n<span class=\"hljs-code-line\">    reset_led = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> <span class=\"hljs-keyword\">not<\/span> skip_shutdown:<\/span>\n<span class=\"hljs-code-line\">        print(<span class=\"hljs-string\">\"shut down system ...\"<\/span>)<\/span>\n<span class=\"hljs-code-line\">        os.system(<span class=\"hljs-string\">'sudo shutdown now'<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    print(<span class=\"hljs-string\">\"end program. bye.\"<\/span>)<\/span>\n<span class=\"hljs-code-line\">    exit(<span class=\"hljs-number\">0<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">sigint_handler<\/span><span class=\"hljs-params\">(signal_received, frame)<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    print(<span class=\"hljs-string\">'SIGINT or CTRL-C detected. Exiting gracefully'<\/span>)<\/span>\n<span class=\"hljs-code-line\">    shutdown_action( <span class=\"hljs-literal\">True<\/span> )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">disk_exists<\/span><span class=\"hljs-params\">(path)<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">try<\/span>:<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">return<\/span> stat.S_ISBLK(os.stat(path).st_mode)<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">except<\/span>:<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-literal\">False<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">set_modus<\/span><span class=\"hljs-params\">( new_modus )<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">global<\/span> modus<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    modus = new_modus<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    print(<span class=\"hljs-string\">\"set modus to \"<\/span>+str(new_modus))<\/span>\n<span class=\"hljs-code-line\">    set_display( <span class=\"hljs-string\">\"\"<\/span>, <span class=\"hljs-string\">\"MODUS: \"<\/span>+str(new_modus) )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># check if usb thumb drive exists<\/span><\/span>\n<span class=\"hljs-code-line\">print( <span class=\"hljs-string\">'check if '<\/span>+str(MOUNT_VOLUME)+<span class=\"hljs-string\">' exists'<\/span>)<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">if<\/span>( disk_exists(MOUNT_VOLUME) ):<\/span>\n<span class=\"hljs-code-line\">    print(<span class=\"hljs-string\">'  yes'<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-comment\"># try to mount<\/span><\/span>\n<span class=\"hljs-code-line\">    os.system( <span class=\"hljs-string\">'sudo mount -o ro '<\/span>+str(MOUNT_VOLUME)+<span class=\"hljs-string\">' '<\/span>+str(MOUNT_PATH) )<\/span>\n<span class=\"hljs-code-line\">    use_drive = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\">    BANK_PIANO = str(MOUNT_PATH)+options[<span class=\"hljs-string\">'folder_piano'<\/span>]<\/span>\n<span class=\"hljs-code-line\">    BANK_DRUMS = str(MOUNT_PATH)+options[<span class=\"hljs-string\">'folder_drums'<\/span>]<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">else<\/span>:<\/span>\n<span class=\"hljs-code-line\">    print(<span class=\"hljs-string\">'  no'<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">if<\/span> use_drive <span class=\"hljs-keyword\">and<\/span> os.path.isfile(str(MOUNT_PATH)+<span class=\"hljs-string\">'\/config.txt'<\/span>):<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">import<\/span> configparser<\/span>\n<span class=\"hljs-code-line\">    configParser = configparser.RawConfigParser()<\/span>\n<span class=\"hljs-code-line\">    configParser.read( str(MOUNT_PATH)+<span class=\"hljs-string\">'\/config.txt'<\/span> )<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">for<\/span> option <span class=\"hljs-keyword\">in<\/span> options:<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> configParser.has_option(<span class=\"hljs-string\">'Victoria'<\/span>, option):<\/span>\n<span class=\"hljs-code-line\">            options[option] = configParser.get(<span class=\"hljs-string\">'Victoria'<\/span>, option)<\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">if<\/span> options[option] == <span class=\"hljs-string\">'False'<\/span>:<\/span>\n<span class=\"hljs-code-line\">                options[option] = <span class=\"hljs-literal\">False<\/span><\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">if<\/span> options[option] == <span class=\"hljs-string\">'True'<\/span>:<\/span>\n<span class=\"hljs-code-line\">                options[option] = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">if<\/span> option == <span class=\"hljs-string\">'samplerate'<\/span> <span class=\"hljs-keyword\">or<\/span> option == <span class=\"hljs-string\">'channels'<\/span>:<\/span>\n<span class=\"hljs-code-line\">                options[option] = int(options[option])<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">print( <span class=\"hljs-string\">'options:'<\/span>, options )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">if<\/span> options[<span class=\"hljs-string\">'use_display'<\/span>]:<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-comment\"># init the display<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    print( <span class=\"hljs-string\">'using display'<\/span> )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">import<\/span> subprocess<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">from<\/span> board <span class=\"hljs-keyword\">import<\/span> SCL, SDA<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">import<\/span> busio<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">from<\/span> PIL <span class=\"hljs-keyword\">import<\/span> Image, ImageDraw, ImageFont<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">import<\/span> adafruit_ssd1306<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    i2c = busio.I2C(SCL, SDA)<\/span>\n<span class=\"hljs-code-line\">    disp = adafruit_ssd1306.SSD1306_I2C(<span class=\"hljs-number\">128<\/span>, <span class=\"hljs-number\">32<\/span>, i2c)<\/span>\n<span class=\"hljs-code-line\">    disp.fill(<span class=\"hljs-number\">0<\/span>)<\/span>\n<span class=\"hljs-code-line\">    disp.show()<\/span>\n<span class=\"hljs-code-line\">    width = disp.width<\/span>\n<span class=\"hljs-code-line\">    height = disp.height<\/span>\n<span class=\"hljs-code-line\">    image = Image.new(<span class=\"hljs-string\">\"1\"<\/span>, (width, height))<\/span>\n<span class=\"hljs-code-line\">    draw = ImageDraw.Draw(image)<\/span>\n<span class=\"hljs-code-line\">    draw.rectangle((<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>, width, height), outline=<span class=\"hljs-number\">0<\/span>, fill=<span class=\"hljs-number\">0<\/span>)<\/span>\n<span class=\"hljs-code-line\">    padding = <span class=\"hljs-number\">-2<\/span><\/span>\n<span class=\"hljs-code-line\">    font = ImageFont.load_default()<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    draw.rectangle((<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>, width, height), outline=<span class=\"hljs-number\">0<\/span>, fill=<span class=\"hljs-number\">0<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    text = <span class=\"hljs-string\">\"V I C T O R I A\"<\/span><\/span>\n<span class=\"hljs-code-line\">    (font_width, font_height) = font.getsize(text)<\/span>\n<span class=\"hljs-code-line\">    draw.text(<\/span>\n<span class=\"hljs-code-line\">        (disp.width \/\/ <span class=\"hljs-number\">2<\/span> - font_width \/\/ <span class=\"hljs-number\">2<\/span>, <span class=\"hljs-number\">5<\/span>),<\/span>\n<span class=\"hljs-code-line\">        text,<\/span>\n<span class=\"hljs-code-line\">        font=font,<\/span>\n<span class=\"hljs-code-line\">        fill=<span class=\"hljs-number\">255<\/span>,<\/span>\n<span class=\"hljs-code-line\">    )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    text = <span class=\"hljs-string\">\"v.\"<\/span>+str(version)<\/span>\n<span class=\"hljs-code-line\">    (font_width, font_height) = font.getsize(text)<\/span>\n<span class=\"hljs-code-line\">    draw.text(<\/span>\n<span class=\"hljs-code-line\">        (disp.width \/\/ <span class=\"hljs-number\">2<\/span> - font_width \/\/ <span class=\"hljs-number\">2<\/span>, disp.height - font_height),<\/span>\n<span class=\"hljs-code-line\">        text,<\/span>\n<span class=\"hljs-code-line\">        font=font,<\/span>\n<span class=\"hljs-code-line\">        fill=<span class=\"hljs-number\">255<\/span>,<\/span>\n<span class=\"hljs-code-line\">    )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    disp.image(image)<\/span>\n<span class=\"hljs-code-line\">    disp.show()<\/span>\n<span class=\"hljs-code-line\">    time.sleep(<span class=\"hljs-number\">0.1<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">pygame.mixer.pre_init(options[<span class=\"hljs-string\">'samplerate'<\/span>], <span class=\"hljs-number\">-16<\/span>, <span class=\"hljs-number\">2<\/span>, buffer_size)<\/span>\n<span class=\"hljs-code-line\">pygame.mixer.init()<\/span>\n<span class=\"hljs-code-line\">pygame.mixer.set_num_channels(options[<span class=\"hljs-string\">'channels'<\/span>])<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">files_piano = []<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">for<\/span> filetype <span class=\"hljs-keyword\">in<\/span> FILETYPES:<\/span>\n<span class=\"hljs-code-line\">    files_piano.extend(glob.glob(os.path.join(BANK_PIANO, filetype)))<\/span>\n<span class=\"hljs-code-line\">files_piano.sort()<\/span>\n<span class=\"hljs-code-line\">octaves_piano = len(files_piano) \/ <span class=\"hljs-number\">13<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">files_drums = []<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">for<\/span> filetype <span class=\"hljs-keyword\">in<\/span> FILETYPES:<\/span>\n<span class=\"hljs-code-line\">    files_drums.extend(glob.glob(os.path.join(BANK_DRUMS, filetype)))<\/span>\n<span class=\"hljs-code-line\">files_drums.sort()<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">for<\/span> filename <span class=\"hljs-keyword\">in<\/span> files_drums:<\/span>\n<span class=\"hljs-code-line\">    sample = {<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-string\">'channel'<\/span>: <span class=\"hljs-literal\">False<\/span>,<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-string\">'filename'<\/span>: filename<\/span>\n<span class=\"hljs-code-line\">    }<\/span>\n<span class=\"hljs-code-line\">    samples_drums.append(sample)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">for<\/span> filename <span class=\"hljs-keyword\">in<\/span> files_piano:<\/span>\n<span class=\"hljs-code-line\">    sample = {<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-string\">'channel'<\/span>: <span class=\"hljs-literal\">False<\/span>,<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-string\">'filename'<\/span>: filename<\/span>\n<span class=\"hljs-code-line\">    }<\/span>\n<span class=\"hljs-code-line\">    samples_piano.append(sample)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">print( <span class=\"hljs-string\">'Piano: {} samples'<\/span>.format(len(samples_piano)) )<\/span>\n<span class=\"hljs-code-line\">print( <span class=\"hljs-string\">'Drums: {} samples'<\/span>.format(len(samples_drums)) )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">set_display( <span class=\"hljs-string\">\"Piano: \"<\/span>+str(len(samples_piano))+<span class=\"hljs-string\">\" samples\"<\/span>, <span class=\"hljs-string\">\"DrumHat: \"<\/span>+str(len(samples_drums))+<span class=\"hljs-string\">\" samples\"<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> range(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">16<\/span>):<\/span>\n<span class=\"hljs-code-line\">    pianohat.set_led(i, <span class=\"hljs-literal\">False<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-comment\"># ready-animation<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> range(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">13<\/span>):<\/span>\n<span class=\"hljs-code-line\">    pianohat.set_led(i, <span class=\"hljs-literal\">True<\/span>)<\/span>\n<span class=\"hljs-code-line\">    time.sleep(<span class=\"hljs-number\">0.05<\/span>)<\/span>\n<span class=\"hljs-code-line\">time.sleep(<span class=\"hljs-number\">0.2<\/span>)<\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> range(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">16<\/span>):<\/span>\n<span class=\"hljs-code-line\">    pianohat.set_led(i, <span class=\"hljs-literal\">False<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">drumhat.on_hit(drumhat.PADS, handle_drums_hit)<\/span>\n<span class=\"hljs-code-line\">drumhat.on_release(drumhat.PADS, handle_drums_release)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">pianohat.on_note(handle_note)<\/span>\n<span class=\"hljs-code-line\">pianohat.on_octave_up(handle_octave_up)<\/span>\n<span class=\"hljs-code-line\">pianohat.on_octave_down(handle_octave_down)<\/span>\n<span class=\"hljs-code-line\">pianohat.on_instrument(handle_instrument)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-meta\">@buttonshim.on_press(buttonshim.BUTTON_A)<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">button_a<\/span><span class=\"hljs-params\">(button, pressed)<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    buttonshim.set_pixel(<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0xff<\/span>)<\/span>\n<span class=\"hljs-code-line\">    reset_led = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\">    set_volume( +<span class=\"hljs-number\">1<\/span> )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-meta\">@buttonshim.on_release(buttonshim.BUTTON_A)<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">button_a_release<\/span><span class=\"hljs-params\">(button, pressed)<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    handle_instrument(<span class=\"hljs-number\">16<\/span>, <span class=\"hljs-literal\">False<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-meta\">@buttonshim.on_press(buttonshim.BUTTON_B)<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">button_b<\/span><span class=\"hljs-params\">(button, pressed)<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    buttonshim.set_pixel(<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0xff<\/span>)<\/span>\n<span class=\"hljs-code-line\">    reset_led = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\">    set_volume( <span class=\"hljs-number\">-1<\/span> )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-meta\">@buttonshim.on_release(buttonshim.BUTTON_B)<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">button_b_release<\/span><span class=\"hljs-params\">(button, pressed)<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    handle_instrument(<span class=\"hljs-number\">16<\/span>, <span class=\"hljs-literal\">False<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-meta\">@buttonshim.on_press(buttonshim.BUTTON_C)<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">button_c<\/span><span class=\"hljs-params\">(button, pressed)<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    buttonshim.set_pixel(<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0xff<\/span>)<\/span>\n<span class=\"hljs-code-line\">    reset_led = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\">    set_modus( <span class=\"hljs-string\">\"default\"<\/span> )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-meta\">@buttonshim.on_press(buttonshim.BUTTON_D)<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">button_d<\/span><span class=\"hljs-params\">(button, pressed)<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    buttonshim.set_pixel(<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0xff<\/span>)<\/span>\n<span class=\"hljs-code-line\">    reset_led = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\">    set_modus( <span class=\"hljs-string\">\"toggle\"<\/span> )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-meta\">@buttonshim.on_press(buttonshim.BUTTON_E)<\/span><\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">button_e<\/span><span class=\"hljs-params\">(button, pressed)<\/span>:<\/span><\/span>\n<span class=\"hljs-code-line\">    buttonshim.set_pixel(<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0x00<\/span>,<span class=\"hljs-number\">0xff<\/span>)<\/span>\n<span class=\"hljs-code-line\">    reset_led = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\">    set_modus( <span class=\"hljs-string\">\"chaos\"<\/span> )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">signal.signal(signal.SIGINT, sigint_handler) <span class=\"hljs-comment\"># capture ctrl+c<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">target_time = <span class=\"hljs-number\">1<\/span>\/options[<span class=\"hljs-string\">'fps'<\/span>]<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">buttonshim.set_pixel(<span class=\"hljs-number\">0xff<\/span>,<span class=\"hljs-number\">0xff<\/span>,<span class=\"hljs-number\">0xff<\/span>)<\/span>\n<span class=\"hljs-code-line\">change_led = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">CPU = <span class=\"hljs-string\">''<\/span><\/span>\n<span class=\"hljs-code-line\">MemUsage = <span class=\"hljs-string\">''<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">last_line_1 = <span class=\"hljs-string\">'force refresh'<\/span><\/span>\n<span class=\"hljs-code-line\">last_line_1_count = <span class=\"hljs-number\">12<\/span><\/span>\n<span class=\"hljs-code-line\">last_line_2 = <span class=\"hljs-string\">'force refresh'<\/span><\/span>\n<span class=\"hljs-code-line\">last_line_2_count = <span class=\"hljs-number\">12<\/span><\/span>\n<span class=\"hljs-code-line\">cpumem_frame_counter = <span class=\"hljs-number\">10<\/span><\/span>\n<span class=\"hljs-code-line\">screen_refresh = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"><span class=\"hljs-keyword\">while<\/span> running:<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    start_time = time.time()<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> change_led:<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> modus == <span class=\"hljs-string\">\"chaos\"<\/span>:<\/span>\n<span class=\"hljs-code-line\">            buttonshim.set_pixel(<span class=\"hljs-number\">0xff<\/span>, <span class=\"hljs-number\">0x00<\/span>, <span class=\"hljs-number\">0x00<\/span>) <span class=\"hljs-comment\"># red<\/span><\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">elif<\/span> modus == <span class=\"hljs-string\">\"toggle\"<\/span>:<\/span>\n<span class=\"hljs-code-line\">            buttonshim.set_pixel(<span class=\"hljs-number\">0x00<\/span>, <span class=\"hljs-number\">0xff<\/span>, <span class=\"hljs-number\">0x00<\/span>) <span class=\"hljs-comment\"># green<\/span><\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">else<\/span>: <span class=\"hljs-comment\"># default<\/span><\/span>\n<span class=\"hljs-code-line\">            buttonshim.set_pixel(<span class=\"hljs-number\">0xff<\/span>, <span class=\"hljs-number\">0xff<\/span>, <span class=\"hljs-number\">0xff<\/span>) <span class=\"hljs-comment\"># white<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    channel_removed = <span class=\"hljs-literal\">False<\/span><\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">for<\/span> channel <span class=\"hljs-keyword\">in<\/span> list(channels): <span class=\"hljs-comment\"># <span class=\"hljs-doctag\">NOTE:<\/span> we use list(channels) to iterate over a copy of the list, so we can remove elements from the original list<\/span><\/span>\n<span class=\"hljs-code-line\">        sound = channel[<span class=\"hljs-string\">'channel'<\/span>].get_sound()<\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-comment\"># if sound is \"None\" then no sound is playing<\/span><\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> sound <span class=\"hljs-keyword\">is<\/span> <span class=\"hljs-literal\">None<\/span>:<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">            channels.remove(channel)<\/span>\n<span class=\"hljs-code-line\">            channel_removed = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">if<\/span> modus == <span class=\"hljs-string\">\"default\"<\/span> <span class=\"hljs-keyword\">or<\/span> modus == <span class=\"hljs-string\">\"toggle\"<\/span>:<\/span>\n<span class=\"hljs-code-line\">                <span class=\"hljs-keyword\">if<\/span> channel[<span class=\"hljs-string\">'hat'<\/span>] == <span class=\"hljs-string\">'pianohat'<\/span>:<\/span>\n<span class=\"hljs-code-line\">                    led = channel[<span class=\"hljs-string\">'led'<\/span>] - (octave_piano*<span class=\"hljs-number\">13<\/span>)<\/span>\n<span class=\"hljs-code-line\">                    <span class=\"hljs-keyword\">if<\/span> led &gt;= <span class=\"hljs-number\">0<\/span> <span class=\"hljs-keyword\">and<\/span> led &lt; <span class=\"hljs-number\">13<\/span>:<\/span>\n<span class=\"hljs-code-line\">                        pianohat.set_led( led, <span class=\"hljs-literal\">False<\/span> )<\/span>\n<span class=\"hljs-code-line\">                <span class=\"hljs-keyword\">elif<\/span> channel[<span class=\"hljs-string\">'hat'<\/span>] == <span class=\"hljs-string\">'drumhat'<\/span>:<\/span>\n<span class=\"hljs-code-line\">                    drumhat.led_off( channel[<span class=\"hljs-string\">'led'<\/span>] )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> channel_removed:<\/span>\n<span class=\"hljs-code-line\">        update_channel = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> options[<span class=\"hljs-string\">'use_display'<\/span>]:<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> display_content[<span class=\"hljs-number\">0<\/span>]:<\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">if<\/span> last_line_1_count &gt; <span class=\"hljs-number\">0<\/span>:<\/span>\n<span class=\"hljs-code-line\">                last_line_1_count = last_line_1_count - <span class=\"hljs-number\">1<\/span><\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">else<\/span>:<\/span>\n<span class=\"hljs-code-line\">                display_content[<span class=\"hljs-number\">0<\/span>] = <span class=\"hljs-string\">''<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> display_content[<span class=\"hljs-number\">1<\/span>]:<\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">if<\/span> last_line_2_count &gt; <span class=\"hljs-number\">0<\/span>:<\/span>\n<span class=\"hljs-code-line\">                last_line_2_count = last_line_2_count - <span class=\"hljs-number\">1<\/span><\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">else<\/span>:<\/span>\n<span class=\"hljs-code-line\">                display_content[<span class=\"hljs-number\">1<\/span>] = <span class=\"hljs-string\">''<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> update_channel:<\/span>\n<span class=\"hljs-code-line\">            draw.rectangle((<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">8<\/span>, width, <span class=\"hljs-number\">16<\/span>), outline=<span class=\"hljs-number\">0<\/span>, fill=<span class=\"hljs-number\">0<\/span>)<\/span>\n<span class=\"hljs-code-line\">            draw.text((<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">8<\/span><span class=\"hljs-number\">-1<\/span>), <span class=\"hljs-string\">'Channels: '<\/span>+str(len(channels))+<span class=\"hljs-string\">'\/'<\/span>+str(options[<span class=\"hljs-string\">'channels'<\/span>]), font=font, fill=<span class=\"hljs-number\">255<\/span>)<\/span>\n<span class=\"hljs-code-line\">            update_channel = <span class=\"hljs-literal\">False<\/span><\/span>\n<span class=\"hljs-code-line\">            screen_refresh = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> display_content[<span class=\"hljs-number\">0<\/span>] != last_line_1:<\/span>\n<span class=\"hljs-code-line\">            draw.rectangle((<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">16<\/span>, width, <span class=\"hljs-number\">24<\/span>), outline=<span class=\"hljs-number\">0<\/span>, fill=<span class=\"hljs-number\">0<\/span>)<\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">if<\/span> display_content[<span class=\"hljs-number\">0<\/span>]:<\/span>\n<span class=\"hljs-code-line\">                draw.text((<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">16<\/span><span class=\"hljs-number\">-1<\/span>), display_content[<span class=\"hljs-number\">0<\/span>], font=font, fill=<span class=\"hljs-number\">255<\/span>)<\/span>\n<span class=\"hljs-code-line\">            last_line_1 = display_content[<span class=\"hljs-number\">0<\/span>]<\/span>\n<span class=\"hljs-code-line\">            last_line_1_count = <span class=\"hljs-number\">12<\/span><\/span>\n<span class=\"hljs-code-line\">            screen_refresh = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> display_content[<span class=\"hljs-number\">1<\/span>] != last_line_2:<\/span>\n<span class=\"hljs-code-line\">            draw.rectangle((<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">24<\/span>, width, <span class=\"hljs-number\">32<\/span>), outline=<span class=\"hljs-number\">0<\/span>, fill=<span class=\"hljs-number\">0<\/span>)<\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">if<\/span> display_content[<span class=\"hljs-number\">1<\/span>]:<\/span>\n<span class=\"hljs-code-line\">                draw.text((<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">24<\/span><span class=\"hljs-number\">-1<\/span>), display_content[<span class=\"hljs-number\">1<\/span>], font=font, fill=<span class=\"hljs-number\">255<\/span>)<\/span>\n<span class=\"hljs-code-line\">            last_line_2 = display_content[<span class=\"hljs-number\">1<\/span>]<\/span>\n<span class=\"hljs-code-line\">            last_line_2_count = <span class=\"hljs-number\">12<\/span><\/span>\n<span class=\"hljs-code-line\">            screen_refresh = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">if<\/span> screen_refresh:<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">            disp.image(image)<\/span>\n<span class=\"hljs-code-line\">            disp.show()<\/span>\n<span class=\"hljs-code-line\">            screen_refresh = <span class=\"hljs-literal\">False<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">        <span class=\"hljs-keyword\">else<\/span>:<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-comment\"># cpu and mem update takes some time, so we only refresh it, if we don't update the screen this frame<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">if<\/span> cpumem_frame_counter%<span class=\"hljs-number\">12<\/span> == <span class=\"hljs-number\">0<\/span>:<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">                cmd = <span class=\"hljs-string\">'cut -f 1 -d \" \" \/proc\/loadavg'<\/span><\/span>\n<span class=\"hljs-code-line\">                CPU = str(subprocess.check_output(cmd, shell=<span class=\"hljs-literal\">True<\/span>).decode(<span class=\"hljs-string\">\"utf-8\"<\/span>).strip())<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">                draw.rectangle((<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>, width, <span class=\"hljs-number\">8<\/span>), outline=<span class=\"hljs-number\">0<\/span>, fill=<span class=\"hljs-number\">0<\/span>)<\/span>\n<span class=\"hljs-code-line\">                draw.text((<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span><span class=\"hljs-number\">-1<\/span>), <span class=\"hljs-string\">\"CPU: \"<\/span> + CPU + <span class=\"hljs-string\">' \/ MEM: '<\/span> + MemUsage, font=font, fill=<span class=\"hljs-number\">255<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">                <span class=\"hljs-comment\"># screen_refresh = True # next frame has a mem refresh, so we wait for that to happen before refreshing the creen<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">elif<\/span> cpumem_frame_counter%<span class=\"hljs-number\">12<\/span> == <span class=\"hljs-number\">1<\/span>:<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">                cmd = <span class=\"hljs-string\">\"free -m | awk 'NR==2{printf \\\"%.0f%%\\\", $3*100\/$2 }'\"<\/span><\/span>\n<span class=\"hljs-code-line\">                MemUsage = str(subprocess.check_output(cmd, shell=<span class=\"hljs-literal\">True<\/span>).decode(<span class=\"hljs-string\">\"utf-8\"<\/span>).strip())<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">                draw.rectangle((<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>, width, <span class=\"hljs-number\">8<\/span>), outline=<span class=\"hljs-number\">0<\/span>, fill=<span class=\"hljs-number\">0<\/span>)<\/span>\n<span class=\"hljs-code-line\">                draw.text((<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">-1<\/span>), <span class=\"hljs-string\">\"CPU: \"<\/span> + CPU + <span class=\"hljs-string\">' \/ MEM: '<\/span> + MemUsage, font=font, fill=<span class=\"hljs-number\">255<\/span>)<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">                screen_refresh = <span class=\"hljs-literal\">True<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">            cpumem_frame_counter = cpumem_frame_counter + <span class=\"hljs-number\">1<\/span><\/span>\n<span class=\"hljs-code-line\">            <span class=\"hljs-keyword\">if<\/span> cpumem_frame_counter &gt; <span class=\"hljs-number\">24<\/span>: <span class=\"hljs-comment\"># <span class=\"hljs-doctag\">TODO:<\/span> check what a good rollover point is<\/span><\/span>\n<span class=\"hljs-code-line\">                cpumem_frame_counter = <span class=\"hljs-number\">0<\/span><\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">    dif_time = time.time() - start_time<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">if<\/span> dif_time &lt; target_time:<\/span>\n<span class=\"hljs-code-line\">        sleep_time = target_time - dif_time<\/span>\n<span class=\"hljs-code-line\">        time.sleep(sleep_time)<\/span>\n<span class=\"hljs-code-line\">    <span class=\"hljs-keyword\">else<\/span>:<\/span>\n<span class=\"hljs-code-line\">        print( <span class=\"hljs-string\">'fps too low'<\/span> )<\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\"> <\/span>\n<span class=\"hljs-code-line\">signal.pause()<\/span><\/code><\/pre>\n<p>If you want to change some of the options on the fly, you can create a <em>config.txt<\/em> file in the root directory of the thumb drive to overwrite some options; it must look like this:<\/p><pre class=\"hljs\"><code data-language=\"plaintext\"><span class=\"hljs-code-line\">[Victoria]<\/span>\n<span class=\"hljs-code-line\">samplerate = 48000<\/span>\n<span class=\"hljs-code-line\">folder_piano = \/my-piano-folder<\/span>\n<span class=\"hljs-code-line\">folder_drums = \/my-drums-folder<\/span><\/code><\/pre>\n<p>If you want to use this script make sure that:<\/p><ul><li>you have the <a href=\"https:\/\/learn.adafruit.com\/adafruit-i2s-audio-bonnet-for-raspberry-pi\/raspberry-pi-usage\" target=\"_blank\" rel=\"noopener noreferrer\">Adafruit I2S Audio Bonnet script<\/a> or the <a href=\"https:\/\/learn.pimoroni.com\/tutorial\/phat\/raspberry-pi-phat-dac-install\" target=\"_blank\" rel=\"noopener noreferrer\">Pimoroni pHAT DAC script<\/a><em> <\/em>installed (whichever you want to use) or have audiooutput via HDMI or other means<\/li><li>you have the <a href=\"https:\/\/github.com\/pimoroni\/piano-hat\" target=\"_blank\" rel=\"noopener noreferrer\">pianohat<\/a> and <a href=\"https:\/\/github.com\/pimoroni\/drum-hat\" target=\"_blank\" rel=\"noopener noreferrer\">drumhat<\/a> scripts installed<\/li><li>you have <a href=\"https:\/\/www.pygame.org\/wiki\/GettingStarted\" target=\"_blank\" rel=\"noopener noreferrer\">pygame<\/a> installed (should come with the drumhat\/pianohat script)<\/li><li>you have the <a href=\"https:\/\/learn.adafruit.com\/adafruit-pioled-128x32-mini-oled-for-raspberry-pi\/usage\" target=\"_blank\" rel=\"noopener noreferrer\">Adafruit PiOLED display<\/a> installed<\/li><li>you have the <a href=\"https:\/\/github.com\/pimoroni\/button-shim\" target=\"_blank\" rel=\"noopener noreferrer\">Pimoroni Button Shim<\/a> installed<\/li><li>the directory <em>\/mnt\/victoria_usb<\/em> exists so the USB thumb drive can be mounted (you can change this with the <code>MOUNTPATH<\/code> variable) - the script assumes the path to thumb drive is <em>\/dev\/sda1<\/em> (if not, change the <code>MOUNT_VOLUME<\/code> variable)<\/li><li>the samples need to be <em>.wav<\/em> or <em>.ogg<\/em> files with stereo channel and the correct samplerate (see options)<\/li><li>on the thumb drive there must be a folder called <em>drums<\/em> with up to 8 samples and a folder called <em>piano<\/em> with up to or more than 13 samples; the folder names can be changed in the <code>options<\/code> variable or via the <em>config.txt<\/em> file<\/li><li>the default sample rate is <code>44100<\/code> but can be changed in the options variable or via the <em>config.txt<\/em> file; all samples need to be in this sample rate. Samples that are in another samplerate will not be played correctly<\/li><li>there a two folders (<a href=\"https:\/\/github.com\/pimoroni\/Piano-HAT\/tree\/master\/examples\/sounds\/piano\" target=\"_blank\" rel=\"noopener noreferrer\">piano<\/a> and <a href=\"https:\/\/github.com\/pimoroni\/drum-hat\/tree\/master\/examples\/drums2\" target=\"_blank\" rel=\"noopener noreferrer\">drums<\/a>) with default samples if the USB thumb drive is not present, in the same folder as the script in a subfolder called <em>sounds<\/em>; change the variables <code>BANK_PIANO<\/code> and <code>BANK_DRUMS<\/code> if you want to move them<\/li><li>auto start the script on log in (for example via <code>crontab -e<\/code>: add a line <code>@reboot ~\/victoria<\/code>)<\/li><li>you can activate the <em>Overlay FS<\/em> option in raspi-config to protect the SD-card from data loss<\/li><li>the thumb drive gets mounted as read only (<code>-o ro<\/code> argument). This may be a problem with some drives as they will not mount correctly, so if you can't play the files from your drive, try removing the <code>-o ro<\/code> argument<\/li><li>the <em>I2S Audio Bonnet<\/em> or <em>pHAT DAC<\/em> have a line out, not a headphone jack plug; if you use headphones the output volume will be too loud<\/li><li>to find out which HATs work together you can use the <a href=\"https:\/\/pinout.xyz\/phatstack\" target=\"_blank\" rel=\"noopener noreferrer\">pinout.xyz pHAT Stack Configurator<\/a><\/li><\/ul>\t<ul class=\"gallery\">\n\t\t<img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/build-your-own-basic-raspberry-pi-audio-sampler\/f926f9473a-1733941282\/img_6365-400x-q80.jpg\" width=\"400\" height=\"533\" alt=\"\"><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/build-your-own-basic-raspberry-pi-audio-sampler\/838a7d12aa-1733941282\/img_6364-400x-q80.jpg\" width=\"400\" height=\"533\" alt=\"\"><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/build-your-own-basic-raspberry-pi-audio-sampler\/8c6b8b4a18-1733941282\/img_6363-800x-q80.jpg\" width=\"800\" height=\"600\" alt=\"\"><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/build-your-own-basic-raspberry-pi-audio-sampler\/e8d39ea90d-1733941282\/img_6266-400x-q80.jpg\" width=\"400\" height=\"533\" alt=\"\"><img src=\"https:\/\/www.maxhaesslein.de\/media\/pages\/notes\/build-your-own-basic-raspberry-pi-audio-sampler\/9f449cbc69-1733941282\/img_6367-400x-q80.jpg\" width=\"400\" height=\"533\" alt=\"\">\t<\/ul>\n\t","date_published":"2020-05-27T22:02:00+00:00","date_modified":"2023-01-15T16:18:00+00:00"}]}