2015年1月29日木曜日

LOOX-U/C40 への Voyage MPD 0.9.5 の導入(6) playlistへの自動ランダム登録 (2)

(こんな連載記録になるとは)

ランダム再生はいいのだが,このスクリプトを見た Muraoka 師が問題点に気づいた.
bash スクリプトの中では乱数として組み込み変数の RANDOM が使える.今回使った Jonas Klang 氏のスクリプトでも RANDOM を使っている.あまりよい疑似乱数でもないらしいが,それ以前に,この変数は 32K までの整数を返す.

では,自分のライブラリにはどのくらいの曲数があるのかというと,これは

mpc stats

で調べることができて,現時点で 41,000 を超えている.ということは,1万曲近くがそもそも再生の対象にならない.ということで,スクリプトを修正してみた.


もとのスクリプトでは mpc listall コマンドで出てくる曲のリストから選び出しているので,フルパスをアルファベット順に並べて上位 32K から選ばれる.つまり,Zabadak などは絶対に出てこないw

となると,32K を超える範囲で乱数を得る必要がある.Unix では乱数といえば,/dev/random ないし /dev/urandom からもらってくるものらしい.
具体的には以下のようにしてみた.

終わりの方にある addSong() の中の

song=`mpc listall | sed -n $[RANDOM % $(mpc stats | grep Songs | awk '{print $2}')+1]p`

という行でランダムに曲を決めている.
mpc listall で出てくる曲リストから,ランダムに 1行 sed で拾うのだが,乱数を全曲数 (mpc stats で取得する) で割った余り (+1) を番号としているわけだ.

最初は,RANDOM の部分を

$(( $(od -vAn -N4 -tu4 < /dev/random) % 100000 ))

に置き換えるのを試してみた.これはまず 4バイト整数の乱数を得て,それを 100,000 で割ることで 100,000 までの乱数にしている.
これはこれでちゃんと動いたのだが,別に 100,000 に制限する意味もないので,もっと短くこれでいいのではないか.

song=`mpc listall |  sed -n $[ $(od -vAn -N4 -tu4 < /dev/random) % $(mpc stats | grep Songs | awk '{print $2}')+1]p`

というか,こうでないとまずいのか.

たとえば,1~5 の整数を出したいとする.1~10までの乱数を出して,5で割った余りを見る.この場合は0から4までの余りの出る確率は等確率.しかし,1~11までの乱数だったらどうか.余り1の出る場合が,1,6,11の3通りあり,2通りしか出現パターンのない他の余りより高い確率,たとえば余り2の出現する確率の 1.5 倍で出現することになる.

一方,1~101 までの乱数を5で割ってとやれば,余り1は11通り,他の余りは10通りなので,違いは10% .つまり,元の乱数範囲を,最終的に出したい乱数範囲よりも十分に大きく取ってやれば,こういう問題は発生しなくなる.元の乱数範囲をA,出したい範囲をBとすると,A ÷ B の余りまでの数は,それ以上の数より1回多く出現パターンがある.出現パターンの数は,余りの数より大きい数については A ÷ B の商である.だから,A ÷ B が1より十分に大きければ,1回のパターン増加は無視できることになる.

さて,4バイト整数 = 32ビット整数 だから,要するに 4G までの整数ということになる (2^10 = 1K を覚えておくと便利).一方,今の曲数は 40K くらい.これが増え続けて,仮に 100K まで行ったとしよう.そうすると,4G / 100K = 4M / 100 = 40K.このくらいなら1増えても無視してよいだろう.

一方,あらかじめ10万までに制限してしまうと,曲数が4万だと,2万までは3通りの出現パターンがあるのに,2万以上の数は2通りしか出現パターンがないので,後半の曲が出てくる確率が前半の2/3になってしまい,違いが無視できない.

というような実装をしたら,ようやく Watanabe, Sadao 辺りも出てくるようになった.

で,いいのかな.どっか間違ってるかもしれないw


/dev/random 使っているとはいえ,それはあくまでも長い目で見た一様性だから,短期的には偏ってくることはあるって感じはするなあ.昨日かかった曲がまたリストに挙がってきてる.

以下,改造版のスクリプト


#!/bin/bash
# stdin check
if (( "$#" < 2 )); then
  echo "Usage: `basename $0` HOSTNAME MIN_PLAYLIST_LENGTH [NEXT_ON_ERROR (y/N)]"
  exit 65
fi

# variables
host=$1
length=$2
state_file="$host.state"

export MPD_HOST=$host

if [ "$3" ]; then
  cont=${3^^}
  cont=${cont:0:1}
else
  cont="N"
fi

# go go go
echo "Mpd_Add v.0.6a"
echo "Orignally by Jonas Klang, modified by yjo"
echo "Host: $host / Playlist length: $length"
echo "Next on error: $cont"

# loop
loop () {
  while true; do
    if `mpc version 2>/dev/null | grep -q 'mpd version:'`; then
      setState
      while (( `mpc playlist | grep -c ^` < $length )); do
        addSong
        sleep 1
      done
      mpc idle &> /dev/null
    else
      sleep 30
    fi
  done
}

# output current status to file
# does conversion from utf8 to iso-8859-15 for compatability
#+with samurize on windows
setState() {
  if `mpc | grep -q '\[playing\]'`; then
    mpc --format "%artist% (%date%) %album% / %track% / %title%" current | iconv -s -c -f UTF-8 -t iso-8859-15 -o $state_file
  elif `mpc | grep -q '\[paused\]'`; then
    echo "pause" > $state_file
    if `mpc | grep -q 'single: on'`; then
      mpc -q single off && mpc -q stop && echo "not playing" > $state_file
    fi
  else
    if [ $cont = "Y" ]; then
      mpc -q play
    else
      echo "not playing" > $state_file
    fi
  fi
}

# add one random song from mpd library
addSong () {
  song=`mpc listall |  sed -n $[ $(od -vAn -N4 -tu4 < /dev/random) % $(mpc stats | grep Songs | awk '{print $2}')+1]p`
  if [ "$song" ]; then
    mpc -q add "$song"
  fi
}

# backgrounds the loop and exits main thread
loop &
exit 0

0 件のコメント:

コメントを投稿