Spoken Weather Forecasts and Internet Radio on the Orange Pi Zero with Armbian
February 2, 2018
Voice Recognition on the Orange Pi Zero (DietPi Armbian) Music on Console with Armbian on an Orange Pi Zero

These last few days I have been improving my home automation assistant based on experience gained from using the Google Home Mini. While not at all anticipated, the latter's ability to play radio stations has proved useful. It has also been helpful to get weather information with Google Home. Accordingly I wanted to add similar capabilities to my DIY project.

The project is currently running on an Orange Pi Zero on which I have installed the full Armbian distribution of Debian 9 (Stretch). Internet radio will be played through the Music Player Daemon. The local weather forecast will be obtained from our national weather service and read out using the pico engine that performs text to speech synthesis.

Table of Contents

  1. Patience
  2. Audio Input and Output
  3. Music Player Daemon
  4. Text to Speech
  5. Installing Python 3
  6. Getting Weather Forecasts
  7. Playing Internet Radio

  1. Patience
  2. For some reason the Orange Pi Zero would no longer boot. I had been playing around with CUPS the previous day but nothing terribly wrong had occurred although in the end I did remove the package. Had I been too enthusiastic in cleaning up the system? This glitch provided a good excuse to move on to Debian 9 (Stretch) which is the currently offered distribution at dietpi.com.

    No matter what I did, I could not enable the WiFi with dietpi-config. Thinking it could be a problem with the micro SD card, which could have been the reason for the boot failure, I reinstalled an older DietPi image of Debian 8 (Jessie) on the same SD card. Because WiFi worked, it seemed the culprit was a bug in the distribution.

    So I decided to go back to the Armbian which is a considerably bulkier distribution. Unfortunately, U-Boot complained:

    *** Warning - bad CRC, using default environment

    but the system did boot, albeit, complain that my SD card was very slow and things seemed a bit wonky. There are lots of queries on the Armbian forum about this and invariably the reply to these is to check the power supply and SD card. Perhaps the little Belkin wall wart I had been using lately was not good enough. So I went back to the Asus power supply which was working without problems before. The bad CRC warning did not go away.

    It was time to look at the micro SD card. There is a good blog on Testing SD Cards with Linux by Chris Collins. Instead I used F3 - an alternative to h2testw for Linux by Michel Machado (AltaMayor) at Digrati. I compiled the command line utilities, f3read, f3write as instructed and just copied them to my local binary directory rather than install them. As a consequence, I had to run the utilities as root to access the SD card

    michel@hp:~$ sudo .local/bin/f3write /media/michel/a2c25aee-d15d-4aa7-8f41-ab6bb511776f F3 write 7.0 Copyright (C) 2010 Digirati Internet LTDA. This is free software; see the source for copying conditions. ... Creating file 1.h2w ... OK! ... Creating file 14.h2w ... OK! Free space: 16.00 MB Average writing speed: 5.67 MB/s michel@hp:~$ sudo .local/bin/f3read /media/michel/a2c25aee-d15d-4aa7-8f41-ab6bb511776f F3 read 7.0 Copyright (C) 2010 Digirati Internet LTDA. This is free software; see the source for copying conditions. ... Data OK: 13.31 GB (27913520 sectors) Data LOST: 0.00 Byte (0 sectors) Corrupted: 0.00 Byte (0 sectors) Slightly changed: 0.00 Byte (0 sectors) Overwritten: 0.00 Byte (0 sectors) Average reading speed: 25.60 MB/s

    The good news, of course, is that the SD card is OK. The bad news was the very low write speed for a supposedly class 10 card. As far as I know, class 10 means minimum read and write speeds of 10 MB/s. The only way the card could be construed to be class 10 is to average the read and write speeds. With an average read speed of 25 MB/s, one would think that Armbian would not have complained. However, the 25 MB/s rate is for sequential reads and perhaps the random read speed is much lower.

    For those that may be interested, the SD card is a Verbatim PREMIUM 16GB micoSHC Card (part number #44082). On the front of the packaging it says "up to 45BM/s 300X Read", on the back it says "Read Speed Up To 45MB/s (300X), Write speed lower. X=0.15MB/s" (novel punctuation). I am not sure what that means because 300*0.15MB/s = 45MB/s, which is not slower. Or is it 45*0.15 = 6.75MB/s which is nearer to the observed read speed?

    Doing these test meant that I had to burn the distribution image onto the micro SD card and go through the boot sequence once again. I connected to the OPiZ serial port using Kermit (see Serial Connection with the Orange Pi Zero) before powering up the OPiZ.

    michel@hp:~$ kermit Connecting to /dev/ttyUSB0, speed 115200 Escape character: Ctrl-\ (ASCII 28, FS): enabled Type the escape character followed by C to get back, or followed by ? to see other options. ---------------------------------------------------- U-Boot SPL 2017.11-armbian (Jan 25 2018 - 08:04:30) DRAM: 512 MiB Trying to boot from MMC1 U-Boot 2017.11-armbian (Jan 25 2018 - 08:04:30 +0100) Allwinner Technology CPU: Allwinner H3 (SUN8I 1680) Model: Xunlong Orange Pi Zero DRAM: 512 MiB MMC: SUNXI SD/MMC: 0 *** Warning - bad CRC, using default environment In: serial Out: serial Err: serial Net: phy interface0 eth0: ethernet@1c30000 starting USB... USB0: USB EHCI 1.00 USB1: USB OHCI 1.0 scanning bus 0 for devices... 1 USB Device(s) found scanning usb for storage devices... 0 Storage Device(s) found Autoboot in 1 seconds, press to stop switch to partitions #0, OK ... then a long list of messages as the Linux kernel is booted ... orangepizero login: root Password: 1234 not echoed to screen You are required to change your password immediately (root enforced) Changing password for root. (current) UNIX password: 1234 not echoed to screen Enter new UNIX password: new-root-password not echoed to screen Retype new UNIX password: new-root-password not echoed to screen ___ ____ _ _____ / _ \ _ __ __ _ _ __ __ _ ___ | _ \(_) |__ /___ _ __ ___ | | | | '__/ _` | '_ \ / _` |/ _ \ | |_) | | / // _ \ '__/ _ \ | |_| | | | (_| | | | | (_| | __/ | __/| | / /| __/ | | (_) | \___/|_| \__,_|_| |_|\__, |\___| |_| |_| /____\___|_| \___/ |___/ Welcome to ARMBIAN 5.38 stable Debian GNU/Linux 9 (stretch) 4.14.14-sunxi System load: 0.38 0.44 0.20 Up time: 3 min Memory usage: 8 % of 493MB IP: 192.168.0.135 CPU temp: 37°C Usage of /: 7% of 15G New to Armbian? Check the documentation first: https://docs.armbian.com Thank you for choosing Armbian! Support: www.armbian.com Creating a new user account. Press <Ctrl-C> to abort Please provide a username (eg. your forename): michel change to what you want of course Trying to add user michel Adding user `michel' ... Adding new group `michel' (1000) ... Adding new user `michel' (1000) with group `michel' ... Creating home directory `/home/michel' ... Copying files from `/etc/skel' ... Enter new UNIX password: Retype new UNIX password: passwd: password updated successfully Changing the user information for michel Enter the new value, or press ENTER for the default Full Name []: Room Number []: I just left all Work Phone []: these fields empty Home Phone []: Other []: Is the information correct? [Y/n] y Dear michel, your account michel has been created and is sudo enabled. Please use this account for your daily work from now on. root@orangepizero:~#

    As can be seen, the message *** Warning - bad CRC, using default environment is still visible, but the system did boot correctly. After creating my account, I rebooted and logged in under my account.

    After the usual setup with armbian-config (change of the time zone to America/Moncton and hostname to opiz) I was disappointed to discover that I could not get WiFi going. Furthermore systemctl status reported a degraded state which meant that something had not been installed correctly.

    I played with armbian-config and nmtui without much progress. This is where the serial link shines because I could disable the Ethernet interface when trying various combinations.

    During one of the numerous reboots, there was a suggestion to upgrade to version 4.14.15 of sunxi which I did immediately. I also removed the interfaces file in /etc/network because of a recommendation I read on the Armbianforum. Lo and behold, systemctl no longer reported its state as degraded and I could get the WiFi working by toggling it down and up.

    michel@opiz:~$ sudo ifconfig wlan0 down michel@opiz:~$ sudo ifconfig wlan0 up

    Unfortunately, the interface did not come up when I rebooted, but toggling down and up again worked. This was definite progress. I decided to call it quits for the night. Yesterday morning, I was amazed to note that both the Ethernet and WiFi interfaces were available on booting the Orange Pi Zero. Later the WiFi interface came up without problem on a reboot without an Ethernet connection. This morning, I could not reach the OPiZ from the local area network even though ifconfig showed that wlan0 did have its usual IP address. I toggle the interface down and up using the serial connection and that did fix the problem. But strangely, Internet radio would not function because no stream could be decoded. Rebooting fixed everything. Why?

    Patience is touted as a virtue. It is a necessity with the Orange Pi Zero.

    In all probability, the DietPi image will contain the newest corrections to Armbian and it will be possible to get WiFil to work on it sometime in the near future.

    This is a good opportunity express my thanks to each and everyone of those very knowledgeable persons that provide these distributions to mere mortals like me. Without their precious and unselfish work, many single board computers would be nothing more than paper weights.

  3. Audio Input and Output
  4. Since this small system is being tested as a voice-activated home automation assistant, the sound hardware must be running. It turns out that alsa-utils is installed by default in the Armbian image.

    michel@opiz:~$ apt-cache policy alsa-utils alsa-utils: Installed: 1.1.3-1 Candidate: 1.1.3-1 Version table: *** 1.1.3-1 500 500 http://httpredir.debian.org/debian stretch/main armhf Packages 100 /var/lib/dpkg/status

    When I tried to record and play back a sound file with the asound and aplay utilities, nothing worked. To be fair I hadn't expected it to work and, indeed, listing playback devices with aplay -l and aplay -L showed nothing was available. Similarly, no capture devices were listed with arecord -l or arecord -L. Clearly the analogue audio input and output were not enabled.

    For those picking up this saga mid-stream, I have an expansion board with a built in microphone and a 3.5mm audio jack to which are connected powered speakers.

    As one would expect, the audio hardware can be enabled with armbian-config.

    michel@opiz:~$ sudo armbian-config

    Select System,

    and then click on OK.

    In the next screen, select Harware,

    and then again click on OK.

    Then enable analog-codec. If I remember correctly, use the up and down arrow keys to select it and then press the space bar to toggle the enabled check mark on or off. At the bottom of the list, two usb hosts were already enabled.

    Click on Save and then back out of the utility clicking on Cancel or OK as needed. In the end the system has to be rebooted.

    I think all this could have been done by modifying the overlays line of the u-boot environment file as root

    michel@opiz:~$ cat /boot/armbianEnv.txt verbosity=1 logo=disabled console=both disp_mode=1920x1080p60 overlay_prefix=sun8i-h3 overlays=analog-codec usbhost2 usbhost3 rootdev=UUID=a2c25aee-d15d-4aa7-8f41-ab6bb511776f rootfstype=ext4 usbstoragequirks=0x2537:0x1066:u,0x2537:0x1068:u
    and then rebooting.

    That's enough to enable the hardware, now it must be configured. That involves creating a sound configuration file and then setting default recording and playback volumes. For the configuration file, I used the same one I had when using DietPi.

    michel@opiz:~$ nano .asoundrc
    pcm.!default { type asym capture.pcm "mic" playback.pcm "speaker" } pcm.mic { type plug slave { pcm "hw:0,0" format S16_LE } } pcm.speaker { type plug slave { pcm "hw:0,0" } }

    Many seem to prefer to set up a global configuration file. So the same content could be saved to /etc/asound.conf, but this must be done as a super user as the owner of the file must be root.

    The two screen captures below show how I set up the volume with alsamixer following the instructions in step 1 of Configuring Orange PI PC for analogue Line-Out jack audio output (and Simultaneous HDMI output with Software Mixing) by AnonymousPi.

    michel@opiz:~$ sudo alsamixer

    First ensure that the Card shown is H3 Audio Codec. If that (or something similiar) is not what is shown in the top left corner, press F6 to select the sound card. Then press F3 to see only the playback devices. Use the right and left cursor keys to select Line Out and press the M key if a MM which stands for mute is displayed. What should be showing is OO which I guess stands for Open. The use the up and down arrow keys to adjust the Line out gain. I chose the maximum value before seeing red. Then go to the DAC input and do the same.

    Moving on to the microphone, press F4.

    As before, use the right and left cursor keys to select Mic1, but here press the space bar to toggle it on. The L  R CAPTURE caption will be shown and [Off, Off] will no longer be displayed beside Item: Mic1 in the upper left corner. Then adjust Mic1 Boost and ADC Gain levels using the up and down cursor keys. I chose to show one red bar in each case.

    Once alsamixer is exited by pressing the Esc key, the settings can be saved in the file /var/lib/alsa/asound.state with the following command.

    michel@opiz:~$ sudo alsactl store 0

    It is probably best to reboot and test that everything works. That will be very simple to do.

    michel@opiz:~$ arecord test.wav Press CtrlC to stop recording michel@opiz:~$ arecord test.wav

    Arriving at these settings is pretty much a trial and error procedure. There is a way to make things easier which I mentioned in the previous post. Here are my suggestions:

  5. Music Player Daemon
  6. Using a Google Home Mini has opened up my eyes to potential uses of a single board computer such as the Orange Pi Zero or Raspberry Pi. One possibility is as a server for Internet radio. I have been testing this possibility using the Music Player Daemon. There is a lot of information on the Web for this type of thing. Turns out that it is deceptively easy to get the daemon up on Orange Pi Zero running the latest version of Armbian.

    michel@opiz:~$ sudo apt install mpd mpc Reading package lists... Done Building dependency tree Reading state information... Done The following additional packages will be installed: fontconfig-config fonts-dejavu-core libadplug-2.2.1-0v5 libao-common libao4 libasyncns0 libaudiofile1 libavahi-client3 libavahi-common-data libavahi-common3 libavcodec57 libavformat57 libavutil55 libbinio1v5 libbluray1 libcairo2 libcdio-cdda1 libcdio-paranoia1 libcdio13 libchromaprint1 libcups2 libdrm2 libfaad2 libflac8 libfluidsynth1 libfontconfig1 libfreetype6 libgme0 libgsm1 libice6 libid3tag0 libiso9660-8 libjack-jackd2-0 libldb1 libmad0 libmikmod3 libmms0 libmodplug1 libmp3lame0 libmpcdec6 libmpdclient2 libmpg123-0 libnfs8 libogg0 libopenal-data libopenal1 libopenjp2-7 libopenmpt0 libopus0 libpixman-1-0 libpng16-16 libpulse0 libpython2.7 libroar2 libsdl1.2debian libshine3 libshout3 libsidplayfp4 libsm6 libsmbclient libsnappy1v5 libsndfile1 libsndio6.1 libsoxr0 libspeex1 libspeexdsp1 libssh-gcrypt-4 libswresample2 libtalloc2 libtdb1 libtevent0 libtheora0 libtwolame0 libupnp6 libva-drm1 libva-x11-1 libva1 libvdpau1 libvorbis0a libvorbisenc2 libvorbisfile3 libvpx4 libwavpack1 libwbclient0 libwebp6 libwebpmux2 libwildmidi-config libwildmidi2 libx11-6 libx11-data libx11-xcb1 libx264-148 libx265-95 libxau6 libxcb-render0 libxcb-shm0 libxcb1 libxdmcp6 libxext6 libxfixes3 libxi6 libxrender1 libxtst6 libxvidcore4 libyajl2 libzvbi-common libzvbi0 libzzip-0-13 python-talloc samba-libs x11-common Suggested packages: adplug-utils libaudio2 libesd0 | libesd-alsa0 libbluray-bdj cups-common jackd2 libportaudio2 opus-tools pulseaudio libroar-plugins-universal roaraudio-server libmuroar0 slpd socat sidplayfp sndiod speex avahi-daemon icecast2 Recommended packages: libaacs0 va-driver-all | va-driver vdpau-driver-all | vdpau-driver freepats The following NEW packages will be installed: fontconfig-config fonts-dejavu-core libadplug-2.2.1-0v5 libao-common libao4 libasyncns0 libaudiofile1 libavahi-client3 libavahi-common-data libavahi-common3 libavcodec57 libavformat57 libavutil55 libbinio1v5 libbluray1 libcairo2 libcdio-cdda1 libcdio-paranoia1 libcdio13 libchromaprint1 libcups2 libdrm2 libfaad2 libflac8 libfluidsynth1 libfontconfig1 libfreetype6 libgme0 libgsm1 libice6 libid3tag0 libiso9660-8 libjack-jackd2-0 libldb1 libmad0 libmikmod3 libmms0 libmodplug1 libmp3lame0 libmpcdec6 libmpdclient2 libmpg123-0 libnfs8 libogg0 libopenal-data libopenal1 libopenjp2-7 libopenmpt0 libopus0 libpixman-1-0 libpng16-16 libpulse0 libpython2.7 libroar2 libsdl1.2debian libshine3 libshout3 libsidplayfp4 libsm6 libsmbclient libsnappy1v5 libsndfile1 libsndio6.1 libsoxr0 libspeex1 libspeexdsp1 libssh-gcrypt-4 libswresample2 libtalloc2 libtdb1 libtevent0 libtheora0 libtwolame0 libupnp6 libva-drm1 libva-x11-1 libva1 libvdpau1 libvorbis0a libvorbisenc2 libvorbisfile3 libvpx4 libwavpack1 libwbclient0 libwebp6 libwebpmux2 libwildmidi-config libwildmidi2 libx11-6 libx11-data libx11-xcb1 libx264-148 libx265-95 libxau6 libxcb-render0 libxcb-shm0 libxcb1 libxdmcp6 libxext6 libxfixes3 libxi6 libxrender1 libxtst6 libxvidcore4 libyajl2 libzvbi-common libzvbi0 libzzip-0-13 mpc mpd python-talloc samba-libs x11-common 0 upgraded, 113 newly installed, 0 to remove and 0 not upgraded. Need to get 26.1 MB of archives. After this operation, 62.8 MB of additional disk space will be used. Do you want to continue? [Y/n] o

    Ouch!, that is big. Strictly speaking the command line interface mpc is not needed as I will be using my own Python based client. Furthermore, the daemon can be controlled directly over telnet but the latter is not installed by default. It is just easier to include mpc to test mpd. Amazingly, it works out of the box for the most part.

    michel@opiz:~$ mpc add http://relay3.slayradio.org:8000/ michel@opiz:~$ mpc play http://relay3.slayradio.org:8000/ [playing] #1/1 0:00/0:00 (0%) volume: n/a repeat: off random: off single: off consume: off michel@opiz:~$ mpc status SLAY Radio: fegolhuzz - Logical level 3 (Laserdance remix) [playing] #1/1 1:13/0:00 (0%) volume: n/a repeat: off random: off single: off consume: off

    However adjusting the volume does not work. The mpd configuration file has to be tweaked.

    michel@opiz:~$ sudo nano /etc/mpd.conf
    ... audio_output { type "alsa" name "My ALSA Device" # device "hw:0,0" # optional # mixer_type "hardware" # optional # mixer_device "default" # optional # mixer_control "PCM" # optional # mixer_index "0" # optional format "44100:16:2" mixer_type "software" dop "no" ... }

    Cards on the table, I did not come up with these settings on my own. I merely "borrowed" them from a configuration file created by the DietPi installation of mpd.

    I noticed that among the many libraries installed there was the WavPack library libwavpack1. Testing showed that Media Player Daemon can play wav encoded files.

    michel@opiz:~$ mpc clear volume: 70% repeat: off random: off single: off consume: off michel@opiz:~$ mpc add "file:///home/michel/sound/test.wav" michel@opiz:~$ mpc play /home/michel/sound/test.wav [playing] #1/1 0:00/0:28 (0%) volume: 70% repeat: off random: off single: off consume: off michel@opiz:~$ mpc status /home/michel/sound/test.wav [playing] #1/1 0:24/0:28 (85%) volume: 70% repeat: off random: off single: off consume: off

    I think this will prove quite useful, but I am getting ahead of myself.

  7. Text to Speech
  8. After some searching on the Web and testing out a few text to speech engines, I settled on Pico TTS which is available as a Debian package. It has reasonably good voice synthesis in English, French, Italian, etc. Installation could not be simpler.

    michel@opiz:~$ sudo apt-get install libttspico0 libttspico-utils libttspico-data [sudo] password for michel: Reading package lists... Done Building dependency tree Reading state information... Done The following NEW packages will be installed: libttspico-data libttspico-utils libttspico0 0 upgraded, 3 newly installed, 0 to remove and 0 not upgraded. Need to get 4,282 kB of archives. After this operation, 6,733 kB of additional disk space will be used. Get:1 http://cdn-fastly.deb.debian.org/debian stretch/non-free armhf libttspico-data all 1.0+git20130326-5 [4,151 kB] Get:2 http://cdn-fastly.deb.debian.org/debian stretch/non-free armhf libttspico0 armhf 1.0+git20130326-5 [122 kB] Get:3 http://cdn-fastly.deb.debian.org/debian stretch/non-free armhf libttspico-utils armhf 1.0+git20130326-5 [8,812 B] Fetched 4,282 kB in 7s (590 kB/s) Selecting previously unselected package libttspico-data. (Reading database ... 32833 files and directories currently installed.) Preparing to unpack .../libttspico-data_1.0+git20130326-5_all.deb ... Unpacking libttspico-data (1.0+git20130326-5) ... Selecting previously unselected package libttspico0:armhf. Preparing to unpack .../libttspico0_1.0+git20130326-5_armhf.deb ... Unpacking libttspico0:armhf (1.0+git20130326-5) ... Selecting previously unselected package libttspico-utils. Preparing to unpack .../libttspico-utils_1.0+git20130326-5_armhf.deb ... Unpacking libttspico-utils (1.0+git20130326-5) ... Setting up libttspico-data (1.0+git20130326-5) ... Processing triggers for libc-bin (2.24-11+deb9u1) ... Setting up libttspico0:armhf (1.0+git20130326-5) ... Setting up libttspico-utils (1.0+git20130326-5) ... Processing triggers for man-db (2.7.6.1-2) ... Processing triggers for libc-bin (2.24-11+deb9u1) ...

    Check that the binary file has been installed.

    michel@opiz:~$ pico2wave --usage Usage: pico2wave [-?] [-w|--wave=filename.wav] [-l|--lang=lang] [-?|--help] [--usage]

    The short usage message shows how words will be translated to sound. The program creates a wav sound file from the words appended to the command line. Then aplay can be used to play the sound.

    michel@opiz:~$ pico2wave -w test.wav "Hi, how are you" michel@opiz:~$ aplay test.wav Playing WAVE 'test.wav' : Signed 16 bit Little Endian, Rate 16000 Hz, Mono michel@opiz:~$ pico2wave -w test.wav -l fr-FR "Bonjour. Il fait 2°C dehors et il neige." michel@opiz:~$ aplay test.wav Playing WAVE 'test.wav' : Signed 16 bit Little Endian, Rate 16000 Hz, Mono

  9. Python
  10. The original Debian image from Armbian is much heavier than the pared-down version from DietPi. For example, both versions 2.7 and 3.5 of Python are installed. However pip, python3-dev and python3-env are not.

    michel@opiz:~$ python -V Python 2.7.13 michel@opiz:~$ python3 -V Python 3.5.3 michel@opiz:~$ pip -V Could not find the database of available applications, run update-command-not-found as root to fix this pip: command not found michel@opiz:~$ sudo update-command-not-found ... michel@opiz:~$ pip3 -V The program 'pip3' is currently not installed. To run 'pip3' please ask your administrator to install the package 'python3-pip' pip3: command not found

    Note the suggested sudo update-command-not-found command that will henceforth mean that when an application is not found, a suggestion of how to install it may be displayed. The next step is to install the two missing Python 3 packages. Since I am avoiding using Python 2, the equivalent packages are not installed for that older version.

    michel@opiz:~ $ sudo apt-get install --upgrade python3-dev python3-venv Reading package lists... Done After this operation, 51.1 MB of additional disk space will be used. Do you want to continue? [Y/n] y ... Setting up python3-venv (3.5.3-1) ... ... Setting up python3-dev (3.5.3-1) ... michel@opiz:~ $

    As explained in my post Python 3 virtual environments, pip will not be installed in the system but it will be automatically added in virtual environments. Since this newer version of Armbian comes with Python 3.5.3, I followed the instructions for a Ubuntu installation of the virtual environment tools in the post. (I will soon be updating that post to remove the reference to the different distributions and replacing it with the more meaningful Python 3 version numbers).

    Now, I will show how to use the mpd and picotts with Python wrappers. The test for picotts will be reading of local weather forecasts obtained with HTML requests. The test for mpd will be to play a radio stream. This will be done in a Python 3 virtual environment.

    First step is to create a project directory and then to create a Python virtual environment in the directory.

    michel@opiz:~$ mkdir ptests michel@opiz:~$ cd ptests michel@opiz:~/ptests$ mkvenv pvenv michel@opiz:~/ptests$ ve pvenv

    If you prefer not using my mkvenv and ve scripts, then you can replace them with python3 -m venv pvenv, source pvenv/bin/activate and pip install -U pip setuptools wheel.

    The next step is to the requests and python-mpd2 packages.

    (pvenv) michel@opiz:~/ptests$ pip install requests Collecting requests Downloading requests-2.18.4-py2.py3-none-any.whl (88kB) 100% |████████████████████████████████| 92kB 737kB/s Collecting urllib3<1.23,>=1.21.1 (from requests) Downloading urllib3-1.22-py2.py3-none-any.whl (132kB) 100% |████████████████████████████████| 133kB 699kB/s Collecting idna<2.7,>=2.5 (from requests) Downloading idna-2.6-py2.py3-none-any.whl (56kB) 100% |████████████████████████████████| 61kB 1.2MB/s Collecting chardet<3.1.0,>=3.0.2 (from requests) Downloading chardet-3.0.4-py2.py3-none-any.whl (133kB) 100% |████████████████████████████████| 143kB 762kB/s Collecting certifi>=2017.4.17 (from requests) Downloading certifi-2018.1.18-py2.py3-none-any.whl (151kB) 100% |████████████████████████████████| 153kB 758kB/s Installing collected packages: urllib3, idna, chardet, certifi, requests Successfully installed certifi-2018.1.18 chardet-3.0.4 idna-2.6 requests-2.18.4 urllib3-1.22 (pvenv) michel@opiz:~/ptests$ pip install python-mpd2 Collecting python-mpd2 Installing collected packages: python-mpd2 Successfully installed python-mpd2-0.5.5

  11. Getting Weather Forecasts
  12. Environment and Climate Change Canada a department of the Canadian Federal Government publishes weather reports, advisories and forecasts in many forms such as the box on the right and what it calls Public Text Bulletins. These are HTML pages, one for each province and territory. The political region is divided into forecast areas where the weather will presumably be homogeneous. Each area forecast is preceded by the list of geographic regions contained in the area. Don't reread that sentence, just have a look at part of tonight's short-term forecast for New-Brusnwick and things will be clear.

     
    FPCN14 CWHX 022000
    Forecasts for New Brunswick issued by Environment Canada at
    4:00 p.m. AST Friday 2 February 2018 for tonight Saturday and
    Saturday night.
    The next scheduled forecast will be issued at 5:00 a.m. AST Saturday.
        
    Fredericton and Southern York County
    Oromocto and Sunbury County
    Stanley - Doaktown - Blackville Area
    Woodstock and Carleton County.
    Tonight..Snow ending early this evening then clearing. Blowing snow
     over exposed areas this evening. Wind northwest 20 km/h gusting to
     40. Low minus 20. Cold wind chill minus 30 overnight.
    Saturday..Mainly sunny. Wind west 20 km/h. High minus 12. Cold wind
     chill minus 30.
    Saturday night..A few clouds. Increasing cloudiness near midnight
     then light snow. Wind west 20 km/h becoming light in the evening.
     Low minus 16 except minus 21 in low lying areas.
    
    Moncton and Southeast New Brunswick
    Fundy National Park.
    Flash freeze warning in effect.
    Tonight..Snow ending this evening then clearing. Amount 5 cm. Blowing
     snow over exposed areas this evening. Wind northwest 30 km/h gusting
     to 50 becoming west 20 overnight. Low minus 19. Cold wind chill
     minus 28.
    Saturday..Mainly sunny. Wind west 20 km/h gusting to 40. High
     minus 13. Cold wind chill minus 29 in the morning.
    Saturday night..A few clouds. Increasing cloudiness after midnight
     then 60 percent chance of flurries overnight. Wind west 20 km/h
     becoming southeast 20 near midnight. Low minus 16 with temperature
     rising to minus 10 by morning.
    
    Kent County
    Kouchibouguac National Park.
    Tonight..Snow ending this evening then clearing. Amount 5 cm. Blowing
     snow over exposed areas early this evening. Wind northwest 30 km/h
     gusting to 50 becoming west 20 overnight. Low minus 20. Cold wind
     chill minus 28.
    Saturday..Mainly sunny. Wind west 20 km/h gusting to 40. High
     minus 13. Cold wind chill minus 30 in the morning.
    Saturday night..Partly cloudy. 30 percent chance of flurries
     overnight. Wind west 20 km/h becoming light in the evening. Low
     minus 18. Cold wind chill minus 25.

    I live in Southeast New Brunswick about 20 minutes away from Moncton. Tonight our forecast is the same as that of the Fundy National Park Area. Sometimes we are lumped in with Kent County and Kouchibouguac National Park.

    The complete forecast is contained in the only <pre></pre> HTML tags in the page. There is a preamble with dates and times and then each forecast area begins with a blank line, a list of concerned geographic areas that terminates with a period "." and then the weather forecast itself. I wrote the following Python function to extract the weather to request the HTML page containing the forecast and then to extract the preamble and the forecast for a specified geographical region.

    Before discussing the function, it is worthwhile to repeat that I am learning Python and in no way am I suggesting that this is the optimal or even correct way of doing things. Indeed, I welcome all suggested improvements.

    The obvious first step is to make the HTML request. This is simple given the excellent requests library that was installed in the previous section. The HTML source code will be available as req.text.

    The second step is to get the forecast. I used the string find function to get the index of the <pre> and </pre> tags. Everything between these indices is extracted, split into lines and assigned to the text variable.

    The idea is then to go through all these lines, one by one, copying only those that are needed into a variable called forecast. Three flags are set up, to keep track of where we are in the file. There is inheader which will be true from the second line in the file until an empty line is reached. From then on, when a blank line is encountered, inregions is set to true and will remain so until a period "." is found. While inregions is true, a test for the desired region is done. When it is found inpredict will be set true. Whenever an empty line is found, then picotts which is a Python wrapper around pico2wave is invoked to say the content of forecast if inheader or inpredict is true.

    There is some substitution done before converting the text to speech. In the header, "AST" and "ADT" which stand for Atlantic Standard and Daylight Time are removed. Pico TTS does not know what to make of those acronyms. In the forecast itself, there are sequences of two periods ".." after each day or part of day. Pico TTS has problems with that, but replacing them with a comma to represent a rest seems to be effective.

    import picotts import requests import time def say_weather(url, place): req = requests.get(url, timeout=4, headers={}) req.encoding = "utf-8" preindex = req.text.find('<pre>') epreindex = req.text.find('</pre>',preindex) text = req.text[preindex:epreindex].split('\n') inheader = False inregions = False inpredict = False forecast = '' for i, line in enumerate(text): if i == 2: inheader = True if (line == ""): if inheader: picotts.say(forecast.replace(" AST", "").replace(" ADT", "")) time.sleep(0.5) forecast = '' inheader = False inregions = True elif inpredict: picotts.say(forecast.replace('..', ', ').replace(' ', ' ')) inpredict = False break else: inregions = True if inheader or (inpredict and not inregions): forecast += ' ' + line elif inregions: if place in line: inpredict = True if '.' in line: inregions = False

    As for picotts.py, I just adapted some code found on the Web. There are three interesting parts to it. First the wav file is stored in a directory on a temporary file system in RAM which cuts down on the wear and tear of the SD card and improves speed.

    Second, it turns out that in addition to the command line option to set the output language, the volume, pitch and speed of the output voice can be changed using tags similar to HTML markup tags in the text stream.

    Third, I find that the first word to be said by picotts is lost. This is the first word overall. First words in subsequent calls to picotts.say are uttered. It's almost as if it takes time to "connect" to the engine. In the _main routine of forecast.py, I added an initial call to picotts.say with just a space and then delayed before continuing. It is a stop-gap measure which does not work very well.

    Downloads:

        forecast.py: Python script to obtain and play weather forecast
        picotts.py: Python wrapper for pico2wave text to speech engine
        configuration.py: Python script to read configuration file
        configuration.json: Configuration file for picotts.py

    To test, set up the Python virtual environment and then

  13. Playing Internet Radio
  14. The most complicated thing about playing Internet radio is managing stations and URLs. I will show here part of the solution I came up with. It remains work in progress. I have yet to complete an updating utility, but it is not necessary at this point.

    Here is a look at part of the file, radio.json, in which information about Internet radio stations is stored.

    { "radio": [ { "name": "ICI Musique Moncton", "src": [ "http://7emct0.akacast.akamaistream.net/7/282/177408/v1/rc.akacast.akamaistream.net/7EMCT0" ], "region": "Canada", "cat": [ "classique", "publique" ] }, { "name": "ICI Radio-Canada Première Nouveau-Brunswick", "src": [ "http://2emct0.akacast.akamaistream.net/7/750/177393/v1/rc.akacast.akamaistream.net/2EMCT0" ], "region": "Canada", "cat": [ "généraliste", "publique" ] }, { "name": "BBC Radio 1", "src": [ "http://bbcmedia.ic.llnwd.net/stream/bbcmedia_radio1_mf_p?s" ], "region": "Royaume-Uni", "cat": [ "généraliste", "publique" ] }, { "name": "BBC Radio 2", "src": [ "http://bbcmedia.ic.llnwd.net/stream/bbcmedia_radio2_mf_p?s" ], "region": "Royaume-Uni", "cat": [ "généraliste", "publique" ] }, { "name": "Venice Classic Radio Italia", "src": [ "http://5.135.173.165:80/stream1", "http://144.217.49.251:80/stream1", "http://109.123.116.202:8020/stream1", "http://109.123.116.202:8022/stream", "http://174.36.206.197:8000/stream", "http://109.123.116.202:8010/stream" ], "region": "Italy", "cat": [ "classique" ] } ] }

    As can be seen, any number of URLS and categories can be saved for each station along with the station name and a geographical location. I am thinking of adding a language field.

    Multiple URLs are of interest because some stations have numerous streams corresponding to different sampling rates. Also, some stations appear to be rebroadcast by other providers and so on. And it is common to find URLs that are out of date on the Web.

    Some stations have long names that are difficult to remember. It is unreasonable to expect that users will specify the completely correct name when giving oral commands. Of course, the speech to text conversion in not foolproof either. It quickly became apparent that Google Home, for one, uses some sort of "nearest match" algorithm to identify the station it will play. So I had to devise my own and it remains to see if it will prove acceptable when my list of radio stations is fully populated.

    def findSimilar(self, name): sourceWords = {} sourceLen = 0 def intersection(stationName): stationWords = set(stationName.lower().split()) stationLen = len(stationWords) if stationLen == 0: return 10000, 10000 common = len(sourceWords & stationWords) if common == 0: return 10000, 10000 return sourceLen - common, stationLen - common sourceWords = set(name.lower().split()) sourceLen = len(sourceWords) if sourceLen < 1: return None, 10000, 10000 leftSource = None leftInCount = 0 leftOutCount = 0 rightSource = None rightInCount = 0 rightOutCount = 0 for station in self.radios: inCount, outCount = intersection(station["name"]) if (inCount == 0) and (outCount == 0): return station, 0, 0 if (not leftSource) or (inCount < leftInCount): leftSource = station leftInCount = inCount leftOutCount = outCount if (not rightSource) or (outCount < rightOutCount): rightSource = station rightInCount = inCount rightOutCount = outCount if leftInCount == 0: return leftSource, leftInCount, rightOutCount elif rightInCount == 0: return rightSource, rightInCount, rightOutCount elif (leftInCount >= 10000) and (rightInCount >= 10000): return None, 10000, 10000 elif (leftInCount + leftOutCount < rightInCount + rightOutCount): return leftSource, leftInCount, rightOutCount else: return rightSource, rightInCount, rightOutCount

    The idea behind this mess is simple. Given a requested radio station name, the function goes through the list of all known stations calculating two metrics: inCount which is the number of words in requested name not contained in a known station name and outCount which is the number of words in the station name not found in the requested name.

        requested name: "Ici Radio Canada Moncton"
        station name:   "ICI Radio-Canada Première Nouveau-Brunswick"
        inCount:        1 ("Moncton")
        outCount:       3 ("Première", "Nouveau", "Brunswick")
    
    When both counts are 0, a perfect match has been found. Otherwise, the nearest match will have the lowest inCount and outCount. I tried to use the sum of these counts but got disappointing results. So currently, I am keeping track of the station with the lowest inCount in the variable leftSource and of the station with the lowest outCount in the variable rightSource. At the end, if no perfect match has been found, then the better match of the two stations is returned along with the corresponding inCount and outCount. Any suggestion on how to improve that last bit of decision-making in choosing between the leftSource and rightSource or indeed about going about the whole matching names would be welcome. In the meantime, I might look into using the soundex type algorithms which I have used with success in another context.

    The routine that plays an Internet radio station is not too complicated. It can be invoked with the (approximate) name of a radio station or with an already specified station. The function goes through the list of URLs until it finds one that works. If that URL is not the first one in the list, it is moved to the top and the configuration file is marked as changed so that it will be saved.

    def play(self, station): if isinstance(station, str): station, inc, outc = self.findSimilar(station) if not station: return False, "unknown source" self.mpdConnect() playing = False srcs = station["src"] ## possible errors here if empty or station is not ## the correct object ## insert call back 'Searching for {} on the Internet'.station["name"] for i, url in enumerate(srcs): self.mpdClient.clear() self.mpdClient.add(url) self.mpdClient.play(0) status = self.mpdClient.status() if "error" in status: #print("{} is not a valid url".format(url)) continue else: playing = True #print("{} is a valid url".format(url)) #print("i = ", i) if i > 0: #print("inserting url ", i, " at 0") srcs.insert(0, srcs.pop(i)) self.jsonChanged = True self.currentStation = station self.currentStationName = station['name'] self.currentStationUrl = url break if playing: print('Playing {}'.format(station['name'])) print('\tURL = {}'.format(self.currentStationUrl)) else: self.mpdClient.stop() self.mpdClient.clear() self.currentStation = None self.currentStationName = "" self.currentStationUrl = "" print('No valid URL to play {}'.format(station['name'])) self.mpdDisconnect() self.currentSuspended = False return playing, station["name"]

    The iradio.py Python script contains other methods, including stop, pause and resume whose meaning is pretty clear.

    I noticed that France Musique Concerts Radio France was choppy while there was no problem playing the same station on the desktop. Perhaps the AllWinner H2+ cannot quite keep up with the 192 kbps rate or the problem has to do with the network interface. The OPiZ could play Radio BBC 3 at 128 kbps without stuttering. Or maybe it has to do with the encoding method used.

    This is all preliminary as I have said before. I am thinking of changing the radio configuration file. The play method could stand to be improved in order to optionally select a particular URL, especially in light of the preceding paragraph. There are lots of debugging print statements that need to be removed and better integration with the voice synthesis system is needed.

    Another possibility that needs investigation is the use of another media player. VLC is often mentioned and I have had success using it for video feeds on my Ubuntu desktop and Android devices.

    Downloads:

        radio.json: JSON file with information for a few Internet radio stations
        iradio.py: Python script to play Internet radio stations with mpd

    To test, set up the Python virtual environment and then

Voice Recognition on the Orange Pi Zero (DietPi Armbian) Music on Console with Armbian on an Orange Pi Zero