• WANTED: Happy members who like to discuss audio and other topics related to our interest. Desire to learn and share knowledge of science required. There are many reviews of audio hardware and expert members to help answer your questions. Click here to have your audio equipment measured for free!

WiiM Mini Streamer

soapsuds

Member
Joined
Mar 22, 2022
Messages
27
Likes
13
I see. Did you consider the cheap generic DACs? Most of them have decent sound quality for what you are trying to do and won’t have hum. Here’s a $12 one

I didn’t, but it would have been a good idea.
 

Jpdelaney.tml

Active Member
Joined
Dec 29, 2021
Messages
159
Likes
33
I have the Mini and I am happy with it. Looking at ASR's list of streamers and it seems to place a head or equal to the Bluesound. Its' performance is bested by the Pi based solutions (DACs are better) and ranks 3rd in SINAD. Once connected to an external DAC (Soncoz LXD1) how could I complain? When using Alexa and the Mini as an Alexa slave, do we know the optical output in Bitdepth and rate using Amason HD Music files for the Mini?
Any insight would be appreciated. Multi-room is a wonderful world and so easy now.
Correction, almost 3 to Node
 

My adventures in stereo

Senior Member
Joined
Feb 9, 2022
Messages
300
Likes
294
CCA Vs Wiim Mini edited.jpg


Got one , to replace a chromecast audio and it sounds great
Running it in 24 bit 192 KHZ, through optical, using Topping Dx7 Pro DAC
 

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,593
Likes
3,493
OK, enough dinking around with code. Here's the "final" script, driving a crappy little 3.5" screen similar to this one, with a Pi 3A+ that I happened to have in a drawer (good luck finding any Pi right now). A Topping Topper for the Wiim Mini.


I really don't like grabbing and unlocking my phone to see what's playing while listening to a radio stream or playlist. This little display will always show what's playing. When done streaming, it shows the date/time.

P1290940.jpg



wiim.py
Requires async-upnp-client, Pillow, xmltodict, and probably a few other Python libs I've forgotten.

Python:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# pylint: disable=invalid-name

from time import sleep
import requests
import textwrap
import re

import asyncio
import json
import logging
import operator
import sys
import time
import xmltodict
from datetime import datetime
from typing import Any, Optional, Sequence, Tuple, Union, cast
from collections import OrderedDict

from didl_lite import didl_lite
NAMESPACES = {
    "didl_lite": "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/",
    "dc": "http://purl.org/dc/elements/1.1/",
    "upnp": "urn:schemas-upnp-org:metadata-1-0/upnp/",
    "xsi": "http://www.w3.org/2001/XMLSchema-instance",
    "song": "www.wiimu.com/song/",
    "custom": "www.linkplay.com/custom/",
}

from async_upnp_client.advertisement import SsdpAdvertisementListener
from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpRequester
from async_upnp_client.client import UpnpDevice, UpnpService, UpnpStateVariable
from async_upnp_client.client_factory import UpnpFactory
from async_upnp_client.const import NS, AddressTupleVXType, SsdpHeaders
from async_upnp_client.exceptions import UpnpResponseError
from async_upnp_client.profiles.dlna import dlna_handle_notify_last_change
from async_upnp_client.search import async_search as async_ssdp_search
from async_upnp_client.ssdp import SSDP_IP_V4, SSDP_IP_V6, SSDP_PORT, SSDP_ST_ALL
from async_upnp_client.utils import get_local_ip

from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw

###### I/O devices may be different on your setup #####
###### can optionally use numpy to write to fb ########
#h, w, c = 320, 480, 4
#fb = np.memmap('/dev/fb0', dtype='uint8',mode='w+',shape=(h,w,c))

fbw, fbh = 480, 320         # framebuffer dimensions
fb = open("/dev/fb0", "wb") # framebuffer device

#######################################################

fonts = []
fonts.append( ImageFont.truetype('/usr/share/fonts/truetype/oswald/Oswald-Bold.ttf', 24) )
fonts.append( ImageFont.truetype('/usr/share/fonts/truetype/oswald/Oswald-Light.ttf', 20) )
fonts.append( ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf', 30) )
fonts.append( ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf', 144) )

## Red and Blue color channels are reversed from normal RGB on pi framebuffer
def swap_redblue(img):
  "Swap red and blue channels in image"
  r, g, b, a = img.split()
  return Image.merge("RGBA", (b, g, r, a))

## Paint image to screen at position
def blit(img, pos):

  size = img.size
  w = size[0]
  h = size[1]
  x = pos[0]
  y = pos[1]

### to use numpy, uncomment...
#  n = np.array(img)
#  n[:,:,[0,1,2]] = n[:,:,[2,1,0]]
#  fb[y:y+h,x:x+w] = n
### ... and comment all below

  img = swap_redblue(img)
  try:
    fb.seek(4 * ((pos[1]) * fbw + pos[0]))
  except Exception as e:
    print("seek error: ", e)

  iby = img.tobytes()
  for i in range(h):
    try:
      fb.write(iby[4*i*w:4*(i+1)*w])
      fb.seek(4 * (fbw - w), 1)
    except Exception as e:
      break


## Display date and time when idle
def displaydatetime(force):

  if not force:
    sec = datetime.now().second
    if sec not in {0,15,30,45}:
      return

  dt = datetime.today().strftime('%a, %d %B %Y')
  tm = datetime.today().strftime('%H:%M')

  img = Image.new('RGBA',(480, 320))
  draw = ImageDraw.Draw(img)
 
  draw.text((20,10), tm, (255,255,255),font=fonts[3])
  draw.text((65,200), dt, (255,255,255),font=fonts[2])

  blit(img,(0,0))


## Red song progress line
def displayprogress(seek, duration):

  if duration > 0:
    progress = seek / duration * 480
  else:
    progress = 0

  img = Image.new('RGBA', (480, 6))

  draw = ImageDraw.Draw(img)
  draw.line((0,0,progress,0),fill='red',width=6)

  blit(img,(0,44))

def clearscreen():
   img = Image.new('RGBA',size=(480,320),color=(0,0,0,255))
   blit(img,(0,0))

## Display artist, song title, album title
def displaymeta(data):

  img = Image.new('RGBA',size=(210,270),color=(0,0,0,255))

  tw1 = textwrap.TextWrapper(width=30)
  tw2 = textwrap.TextWrapper(width=30)
  s = "\n"

  try:
    artist = data['upnp:artist']
  except:
    artist = ""

  try:
    title = data['dc:title']
  except:
    title = ""

  try:
    album = data['upnp:album']
  except:
    album = ""

  if album == "":
    try:
      album = data['dc:subtitle']
    except:
      pass

  artist = s.join(tw2.wrap(artist)[:6])
  album = s.join(tw2.wrap(album)[:6])

  draw = ImageDraw.Draw(img)

  draw.text((10,0), artist, (191,245,245),font=fonts[1])
  draw.text((10,165), album, (255,255,255),font=fonts[1])

  blit(img,(270,50))

  img = Image.new('RGBA',size=(480,50),color=(0,0,0,255))
  draw = ImageDraw.Draw(img)
  draw.text((0,0),  title, (255,255,255),font=fonts[0])

  blit(img,(0,0))

## Get album cover and display
def getcoverart(url):

  try:
    img = Image.open(requests.get(url, stream=True).raw)
    img = img.resize((270,270))
    img = img.convert('RGBA')

    blit(img,(0,50))
  except Exception as e:
    print(e)
    pass

## Init the screen
displaydatetime(True)

detail = []
items = {}
art = ""

pprint_indent = 4

event_handler = None
playing = False

async def create_device(description_url: str) -> UpnpDevice:
    """Create UpnpDevice."""
    timeout = 60
    non_strict = True
    requester = AiohttpRequester(timeout)
    factory = UpnpFactory(requester, non_strict=non_strict)
    return await factory.async_create_device(description_url)


def get_timestamp() -> Union[str, float]:
    """Timestamp depending on configuration."""
    return time.time()


def service_from_device(device: UpnpDevice, service_name: str) -> Optional[UpnpService]:
    """Get UpnpService from UpnpDevice by name or part or abbreviation."""
    for service in device.all_services:
        part = service.service_id.split(":")[-1]
        abbr = "".join([c for c in part if c.isupper()])
        if service_name in (service.service_type, part, abbr):
            return service

    return None

def on_event(
    service: UpnpService, service_variables: Sequence[UpnpStateVariable]
) -> None:
    """Handle a UPnP event."""
    obj = {
        "timestamp": get_timestamp(),
        "service_id": service.service_id,
        "service_type": service.service_type,
        "state_variables": {sv.name: sv.value for sv in service_variables},
    }
    global playing
    global items
    global art
  
    # special handling for DLNA LastChange state variable
    if len(service_variables) == 1 and service_variables[0].name == "LastChange":
        last_change = service_variables[0]
        dlna_handle_notify_last_change(last_change)
    else:
        for sv in service_variables:
            ### PAUSED, PLAYING, STOPPED, etc
            #print(sv.name,sv.value)
            if sv.name == "TransportState":
                print(sv.value)
                if sv.value == "PLAYING":
                  playing = True
                  displaymeta(items)
                  if art:
                    getcoverart(art)
                else:
                  playing = False

            ### Grab and print the metadata
            if sv.name == "CurrentTrackMetaData" or sv.name == "AVTransportURIMetaData":
                ### Convert the grubby XML to beautiful JSON, because we HATE XML!
                items = xmltodict.parse(sv.value)["DIDL-Lite"]["item"]
                ### Print the entire mess
                print(json.dumps(items,indent=4))

                ### Print each item of interest
                try:
                  title = items["dc:title"]
                  print("Title:",title)
                  displaymeta(items)
                except:
                  pass

                try:
                  subtitle = items["dc:subtitle"]
                  print("Subtitle:",subtitle)
                except:
                  pass

                try:
                  artist = items["upnp:artist"]
                  print("Artist:",artist)
                except:
                  pass

                try:
                  album = items["upnp:album"]
                  print("Album:",album)
                except:
                  pass

                try:
                  arttmp = items["upnp:albumArtURI"]
                  if isinstance(arttmp, dict):
                    art = art["#text"]
                  else:
                    art = arttmp

                  print("Art:",art)
                  getcoverart(art)
                except:
                  pass


async def subscribe(description_url: str, service_names: Any) -> None:
    """Subscribe to service(s) and output updates."""
    global event_handler  # pylint: disable=global-statement

    device = await create_device(description_url)

    # start notify server/event handler
    source = (get_local_ip(device.device_url), 0)
    server = AiohttpNotifyServer(device.requester, source=source)
    await server.async_start_server()

    # gather all wanted services
    if "*" in service_names:
        service_names = device.services.keys()

    services = []

    for service_name in service_names:
        service = service_from_device(device, service_name)
        if not service:
            print(f"Unknown service: {service_name}")
            sys.exit(1)
        service.on_event = on_event
        services.append(service)

    # subscribe to services
    event_handler = server.event_handler
    for service in services:
       try:
            await event_handler.async_subscribe(service)
       except UpnpResponseError as ex:
            print("Unable to subscribe to %s: %s", service, ex)

    s = 0
    # keep the webservice running
    while True:
        if playing == False:
          displaydatetime(True)

        await asyncio.sleep(10)
        s = s + 1
        if s >= 12:
          await event_handler.async_resubscribe_all()
          s = 0

async def async_main() -> None:
    """Async main."""

    ####  NOTICE!!!! #####################################
    ####  Your WiiM Mini's IP and port go here
    device = "http://192.168.68.112:49152/description.xml"
    ####             #####################################
    service = ["AVTransport"]

    await subscribe(device, service)


def main() -> None:
    """Set up async loop and run the main program."""
    loop = asyncio.get_event_loop()

    try:
        loop.run_until_complete(async_main())
    except KeyboardInterrupt:
        if event_handler:
            loop.run_until_complete(event_handler.async_unsubscribe_all())
    finally:
        loop.close()


if __name__ == "__main__":
    main()

wiim.service

Code:
[Unit]
Description=Wiim
Wants=network-online.target
After=network-online.target
StartLimitIntervalSec=33
StartLimitBurst=5

[Service]
ExecStart=/home/pi/wiim/wiim.py
WorkingDirectory=/home/pi/wiim
StandardOutput=inherit
StandardError=inherit
Restart=always
RestartSec=5
User=pi

[Install]
WantedBy=multi-user.target
 
Last edited:

Smartplug

Addicted to Fun and Learning
Joined
Apr 25, 2022
Messages
618
Likes
138
The new wiim apps update suppose to have bit depth and sampling rate on UI but I’m not seeing it.
 

Smartplug

Addicted to Fun and Learning
Joined
Apr 25, 2022
Messages
618
Likes
138
Already been posted that it needs a new version of firmware which was meant to be today
I reset the wiim and it check the update but did not find any. Today's date is about to be over.
 

mateolo2

Member
Joined
Mar 15, 2022
Messages
23
Likes
13
@WiiM Support

On behalfe of many Wiim users around the world, please make this few things:

1. When using fixed volume we want to use +/- buttons on wiim to next song/previous song. Please! Its super easy to make i think

2. When wiim goes to sleep/standby can light can goes off too? Thats the way we can see when wiim is in standby.

3. Can wiim app can show bitdepth/sample rate of current stream/song? Last update note that, but when using Spotify Connect there is no info about that on now play screen.
 

Brantome

Major Contributor
Joined
Apr 4, 2020
Messages
1,184
Likes
1,028
@WiiM Support

On behalfe of many Wiim users around the world, please make this few things:

1. When using fixed volume we want to use +/- buttons on wiim to next song/previous song. Please! Its super easy to make i think

2. When wiim goes to sleep/standby can light can goes off too? Thats the way we can see when wiim is in standby.

3. Can wiim app can show bitdepth/sample rate of current stream/song? Last update note that, but when using Spotify Connect there is no info about that on now play screen.
Re 3, please see the response above
 

morillon

Major Contributor
Joined
Apr 19, 2022
Messages
1,373
Likes
276
see (and it may be related to the meaning of this site), if their engineers can study the subject of the jitter which seems quite high on the toslink output (more than the small chromecast despite its age!)...
if just hardware, or can be care to bring to the firmware?
;-)
 
Last edited:

Don927

Member
Joined
Apr 8, 2022
Messages
9
Likes
1
Hi,

I bought a WiiM Mini Streamer for 99EUR.
Connected by optical TOSLINK to an optical splitter. One output of the splitter goes into a Topping E30 DAC and then to a headphone amp. The other output of the splitter goes into an optical input of my AV receiver.

There has been a firmware update a few days ago with the new features:
- bitperfect TOSLINK output. So the sampling rate/depth is identical to the source. Up to 192kHz 24Bit
- switch for fixed volume, so no influence from the streamer (no DSP processing etc)

I am using it with Amazon Music and Spotify.
From my understanding the optical out should now send the original music signal to the DAC without having any influence on the music quality.

Is there any chance that a (very much) more expensive streamer can have any advantage in audio quality? Or only snakeoil?
Just speaking about digital out, not the DAC inside the streamer.

Cheers
Mario
Hey Mario, just saw this. I'm running the same Wiim Mini with a Topping E30. Quite a combo for not a lot of money. Very good stuff.
 

Brantome

Major Contributor
Joined
Apr 4, 2020
Messages
1,184
Likes
1,028
New firmware being rolled out in batches over the next few days - response for WiiM just received:

”The firmware( 4.6.415059) update that support display bit depth and sample rate is performed in batch such that the devices are taking turns to get update. All the devices will be updated during the next few days
  1. New Feature - Display sample rate and bit depth on the WiiM Home app
  2. Fixed - Random playback stuttering (from 44.1k/16 up to 192k/24) not due to network bandwidth
  3. Fixed - M4A decoding compatibility bug for TIDAL Connect
  4. Fixed – Incorrect red color LED even the network is connected
  5. Fixed - Random no sound issue for AirPlay 2 during multiroom playback
  6. Fixed - No sound issue when switching to ‘work with Alexa’ mode on certain use cases
  7. Fixed – The connection loss of TIDAL Connect after playlist is complete (need one extra device selection) “
 

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,593
Likes
3,493
Last edited:

telemike

Senior Member
Joined
Jul 8, 2020
Messages
308
Likes
259
Waveshare 7” 1024X600 IPS display driven with a Pi Zero and a modified version of my Python script. Hoping that the new sample rate / bit depth info will show up in UPnP metadata so I can display it, too.

You should create a separate thread with steps on how to do this. I have a pi and would need screen and step by spte instructions for the software
 

tsammyc

Member
Joined
Apr 18, 2022
Messages
59
Likes
30
Sorry if this is a silly question, but how do you change WiiM’s WiFi hotspot? Can’t seem to find a way to do it in the WiiM app
 

Brantome

Major Contributor
Joined
Apr 4, 2020
Messages
1,184
Likes
1,028
Sorry if this is a silly question, but how do you change WiiM’s WiFi hotspot? Can’t seem to find a way to do it in the WiiM app
Can’t see it either - I guess you may need to reset the device and set it up again which does seem like a sledgehammer to crack a nut…
 
Top Bottom