Digital Room Correction (DRC) on Ubuntu 16.04

由於去年裝潢時各種因素權衡下折衷,目前音響喇叭是放在客廳大型衣/帽/鞋櫃的前面,當 時已知擺位不佳,沒想到開聲比預期更差,整體相當不平衡。尤其 68Hz 左右有個無法忽視 的響應尖峰,只要音樂本身有些低頻,播放時就轟轟作響,摸櫃子都可感到振動。近來終於 有時間處理,採用 Digital Room Correction (DRC) 技術,效果很明顯。 中文相關資訊雖然有,但多半只貼結果,過程怎麼做講得不多,就在這裡簡述一下。相關的 設定檔、script 等等都已公開在 GitHub

硬體

訊源採用 Lavry DA-11,麥克風 miniDSP UMIK-1,不過這樣設定沒辦法 做 reference channel,也就是把左右聲道分開,左聲道用麥克風錄,右聲道直接用線接 loopback. 沒有 reference 會導致時間同步不完美,以目前設備這問題只能改善,沒辦法 解。

UMIK-1 每支都提供個別的校正檔,可按照序號下載。

Filter

採用 drc,參數很多、功能強,但文件實在又長又專業,門檻有點高。看懂怎麼跑以後, 文件 4.4.3 Sample automated script file 裡面提到的 measure 這個 script 已經相當可用,但他的假設是有 reference channel,所以必須做些改寫才能使用。 為了減輕時間不同步的程度,必須盡量讓播音與錄音同時開始,這裡用了 parallel,可 以經由安裝 moreutils 得到。

把麥克風放在聆聽位置,音響調到平時習慣音量(但太大聲會傷喇叭),然後分別跑 aplay -l 以及 arecord -l 來確定設備名稱。這兩個命令需要 alsa-utils,一般都 已經有預設安裝。例如這樣的輸出:

1
2
3
4
5
$ arecord -l
**** List of CAPTURE Hardware Devices ****
card 0: PCH [HDA Intel PCH], device 0: Generic Analog [Generic Analog]
Subdevices: 1/1
Subdevice #0: subdevice #0

注意 card 0device 0,要使用這設備錄音的話就要下 arecord -D hw:0,0,第 一個 0 表示 card,第二個 0 表示 device。接下來確定 aplay 可放出聲音,而 arecord 可錄到聲音,就可以設定 work/run.sh 裡面的參數了。要改 INDEV, OUTDEV 兩個,分別對應到 arecordaplay-D 設定。

要跑完整個 run.sh 需要 moreutils, sox 以及當然的 drc 這些 packages. run.sh 會先呼叫 measure-noref 產生 impulse response 檔案,然後呼叫 sox 轉成適合 drc 使用的格式,接下來呼叫 drc 產生最後的 filter erb-44.1.pcm

measure-noref 是將 measure 改寫成不用 reference channel 的版本,其餘行為都是 相同的。執行時盡量保持整個環境沒有背景聲音,喇叭會播放從 10Hz 到 21KHz 的音頻讓 麥克風錄下,產生 impulse-48.pcm 這個 impulse response 測量結果。

這邊值得注意的是,由於 UMIK-1 是以 48KHz 錄音,所以到這裡都是用相同取樣頻率來處 理。但我一般訊源格式都是 44.1KHz 的 CD 格式,所以先用 sox 轉換檔案格式及取樣頻 率,接下來 drc 的部份就都是用 44.1KHz 處理。

另外,對應到 44.1KHz,UMIK-1 的校正檔也需要稍微修改後,drc 才會接受。原本的校 正檔內容大概是這樣:

1
2
3
4
5
6
"Sens Factor =-.7423dB, SERNO: 7023040"
10.054 -5.0400
10.179 -4.8727
10.306 -4.7086
<snipped>
20016.816 -0.3030

drc 需要從 0 到 f/2 的範圍,以 44.1Khz 為例,就是 0 ~ 22050,而且 drc 也 不認得 "Sens Factor.. 這樣的檔頭,必須刪掉。修改結果會像這樣:

1
2
3
4
5
6
7
0 0
10.054 -5.0400
10.179 -4.8727
10.306 -4.7086
<snipped>
20016.816 -0.3030
22050 0

新加入的 0Hz 以及 22050Hz 的調整值為 0,因為超過了這支麥克風可以錄到的範圍。改好 後,存在 work 目錄下,並修改 run.sh 裡面的 MIC_CALIBRATION 參數。

整理一下流程:

  1. 安裝 alsa-utils, drc, sox, moreutils
  2. 設定 INDEV, OUTDEV, MIC_CALIBRATION
  3. 執行 run.sh
  4. 若成功的話,會看到 erb-44.1.pcm 這個檔案。

播放

平時慣用的 Audacious 本身就有 JACK output,JACK 可以把數位音訊輸入、輸出 任意搭接,其中 Jconvolver 可用 drc 的輸出將數位訊號先做處理,所以 只要把 Audacious 的輸出接到 Jconvolver 的輸入,然後輸出到 DAC 就行了。為了簡單的 管理 JACK,也裝了 qjackctl,利用裡面 patchbay 的功能自動處理這些介接。

先叫起 qjackctl,設定好輸出裝置,按 Start 然後到 terminal 執行 jconvolver drc-44.1.conf。接下來叫起 Audacious,輸出設定為 JACK output,按旁邊的 Settings 把 Automatically connect to output ports 關掉,Bit depth 設為 Floating point,先 播放然後按暫停。這時回到 qjackctl,按 Connect 應該可以看到 Audacious 跟 jconvolver 都出現了。手動把輸入輸出接成這樣

connect

恢復 Audacious 播放後,應該就可以聽到聲音。也可利用 Patchbay 自動化,設定好以後 會像這樣:

patchbay

設定檔已附上,檔名為 drc-patchbay.xml,其內還有 pulseaudio 的設定,可安裝 pulseaudio-module-jack,如此 pulseaudio 的音訊也可處理到。

jconvolver 需要不少運算,若要確保播放時不出現中斷,可用

1
$ sudo chrt -r -p 50 `pidof jconvolver`

把優先權提高。

圖形輸出

drc 下載 drc-3.2.1.tar.gz,解開後到 source/doc/octave/,照著文件 A Sample results 內的敘述,讀取 work/rmc.pcm 以及 work/rtc.pcm 就 可用 createdrcplots 產出圖形。這裡需要 octave 以及 octave-signal,比較麻煩 的是另一個需要的 plot 已經無人維護,所以 Ubuntu 也沒有打包好的 package 可以用, 必須在 octave 內執行 pkg install -forge plot 裝起來。裝好之後,要跑的指令大 致如下:

1
2
3
4
5
graphics_toolkit('gnuplot');
pkg load signal plot
ru = loadpcm("/somewhere/drc-mashup/work/rmc.pcm");
rc = loadpcm("/somewhere/drc-mashup/work/rtc.pcm");
createdrcplots(ru,-1,"R Uncorrected",rc,-1,"R Corrected","/somewhere","R",".png","-dpng");

在我的空間,效果是像這樣:

R-MRFDWSmoothed.png

後續調整

drc 提供從 minimal, soft, normal, strong, extreme, insane 幾種範例設定,insane 可以清楚聽到一些改過頭造成的失真,用來訓練耳朵辨識,用以調整參數。另外還提供 erb 這設定,類似 minimal,改得很少,適合多人(多位置)聆聽。run.sh 之中預設是用 erb 作為起始點,聽感已有差異且不易有反效果。接下來就是嘗試不同範例,了解參數、比 較差異再去微調。相關設定可搭配聆聽訊源的 sample rate 一起修改。例如,要產生 96KHz 的 normal 設定,就把 TARGET_RATE 改為 96000CONFIG 改為 normal-96.0 即可。缺的設定檔從 /usr/share/drc/config 底下複製過來,這裡只提 供 erb-44.1.drc. 另外也要複製對應的「修正目標」檔,例如 44.1Khz 就是 pa-44.1.txt,96Khz 就是 pa-96.0.txt,位置在 /usr/share/drc/target.

MongoDB: unique sparse index of array field

MongoDB 中可以對 document (以 sql 說法就是 record)中的某個「陣列」欄位做 index, 其方式是這樣:

Multikey Indexes

To index a field that holds an array value, MongoDB creates an index key for each element in the array.

就是把陣列值通通展開一起做 index 的意思。這方法乍看合理,但是,如果把 unique, sparse 一起考慮進去,搭配 $push, $pop 等 array operator 的行為,就會發生種 種奇妙的情況。從 1.x 至今,這件事是一整本糊塗帳。

先從 unique 說起。unique 的定義是:index 中每個值只能對應到一個 document。在 multikey 的情況,就會是以下這種「可能不符直覺」的行為:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> db.c.ensureIndex({arrayfield: 1}, {unique: true, name: 'unique'});
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
> db.c.insert({arrayfield: [1, 2]});
WriteResult({ "nInserted" : 1 })
> db.c.insert({arrayfield: [1, 3]});
WriteResult({
"nInserted" : 0,
"writeError" : {
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: test.c index: arrayfield_1 dup key: { : 1.0 }"
}
})

原因是 insert {arrayfield: [1, 2]} 所產生的 index 為 1, 2 各一筆,而 {arrayfield: [1, 3]} 會需要產生 1, 3 各一筆,此時 1 就重複了。所以這邊的 unique 意思其實是「陣列中的值在不同筆紀錄中不能重複」的意思。值得注意的是,以 下的例子又是可以的:

1
2
> db.c.insert({arrayfield: [3, 3]});
WriteResult({ "nInserted" : 1 })

這裡產生的 index 3 只有對應到一個 document,所以沒有違反 unique。要避免這種 情況,就必須先確保產生陣列時沒有重複的值,而後續的操作不用 $push 而用 $addToSet

unique 有個常見的問題,就是「沒有」也算值,所以若有兩筆紀錄都沒有要 index 的欄 位,那「沒有欄位」這件事就重複了,這是不被允許的:

1
2
3
4
5
6
7
8
9
10
> db.c.insert({name: 'a'});
WriteResult({ "nInserted" : 1 })
> db.c.insert({name: 'b'});
WriteResult({
"nInserted" : 0,
"writeError" : {
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: test.c index: unique dup key: { : null }"
}
})

這時候一般就會加入 sparse,其定義是「不 index collection 中所有的 document,而 只管『有這個欄位』的那些」。直接看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
> // drop previous unique index
> db.c.dropIndex('unique');
{ "nIndexesWas" : 2, "ok" : 1 }
> // create an unique & sparse index
> db.c.ensureIndex({arrayfield: 1}, {unique: true, sparse: true});
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
> db.c.insert({name: 'b'}); // can insert this now
WriteResult({ "nInserted" : 1 })

看似問題解決了,其實不然。對陣列的操作,經常需要加入或移除元素,假定現在先把 [3, 3] 清成空陣列,後續又把 [1, 2] 清空,此時照 sparse 的原則來想,兩筆紀 錄都是「沒有值」,應該要被允許才對。在 mongo 1.x 版本,這是成立的。但這造成一個 問題,就是如果在「有 index」的情況下查詢 {arrayindex: []},會查不出東西,因為 「空陣列」沒有被 index,就被當成不存在了。然而如果不用 index,一筆一筆比對,就又 會出現,此時就有行為不一致的問題,也就是 SERVER-2258 裡面的例子:

1
2
3
4
5
6
> db.c.save({a:[]});
> db.c.ensureIndex({a:1});
> db.c.find({a:[]}); // no result
> db.c.find({a:[]}).hint( {$natural:1} );
{ "_id" : ObjectId("4d0fba6fc6237b412f53adeb"), "a" : [ ] }
> db.c.find({a:[]}).hint({a:1}); // again, no result

如前所述,multikey 是針對陣列中的每個「值」做 index 的。為了解決這問題,2.0 版之 後的空陣列就被當成包含一個值 undefined。這解了 SERVER-2258,但又產生了新的問題, 因為這樣一來,重複的空陣列就會違反 unique

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
> db.c.update({arrayfield: 1}, {$pull: {arrayfield: 1}});
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.c.update({arrayfield: 2}, {$pull: {arrayfield: 2}});
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.c.find().pretty();
{ "_id" : ObjectId("58c256eb84c90ec348fb8950"), "arrayfield" : [ ] }
{
"_id" : ObjectId("58c2582384c90ec348fb8952"),
"arrayfield" : [
3,
3
]
}
{ "_id" : ObjectId("58c25a2c84c90ec348fb8955"), "name" : "a" }
{ "_id" : ObjectId("58c25be084c90ec348fb8957"), "name" : "b" }
> db.c.update({arrayfield: 3}, {$pull: {arrayfield: 3}});
WriteResult({
"nMatched" : 0,
"nUpserted" : 0,
"nModified" : 0,
"writeError" : {
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: test.c index: arrayfield_1 dup key: { : undefined }"
}
})

可看到錯誤訊息 arrayfield_1 dup key: { : undefined },表示 undefined 這個值重 複了。而且,這個 breaking change 並沒有寫在 release note 中。此問題被反應在 SERVER-3934,至今未解。一個 workaround 是採用 {v: 0} 也就是舊版的 index, 但看來在 2.6+ 之後也不再支援。

小結一下:對於「array field 中的值在整個 collection 中不能重複,但此 array field 有可能不存在」的 use case,以目前 MongoDB (3.4) 機制是無法直接做到的。

電子支付、第三方支付、實名制

終於稍微多搞懂一點台灣的第三方支付。

首先要把名詞釐清,一般人認知的第三方支付就是金錢以「非」銀行帳戶到銀行帳戶的方式 在網路上流來流去,例如在中國,從使用者銀行帳號以支付寶付給某淘寶電商,而某電商又 以支付寶支付給上游。在整個流程中,錢沒有被領出來,也沒有實際到達鏈上每個人的銀行 帳號,只在支付寶平台上以數位紀錄型態流動。

但在台灣,類似的行為其實被細分為幾個不同營業項目以及監管機關。首先看一下金管會的 這篇 FAQ,內文提到:

於付款方及收款方間經營「代理收付實質交易款項」、「收受儲值款項」、「電子支付帳 戶間款項移轉」等業務之公司,係屬「電子支付機構」,應向本會申請許可,其營業項目 代碼為「HZ06011電子支付業」,屬本會金融監理業務之範疇。…

惟如「僅經營代理收付實質交易款項」業務,且所保管代理收付款項總餘額未逾新臺幣10 億元者,其營業項目代碼為「I301040第三方支付服務業」之公司,其主管機關為「經濟 部」,並由經濟部商業司對第三方支付服務業者進行一般商業管理。

比較兩段業務項目異同,可發現所謂「電子支付業」多了「收受儲值款項」、「電子支付帳 戶間款項移轉」這些業務,保管款項上限也比較高。前段提及「以支付寶收進來的錢,直接 再轉給其他支付寶帳號」這種業務,是屬於「電子支付帳戶間款項移轉」,會被歸類在「電 子支付」,監管單位是金管會。在台灣,這類業者審核條件較高,有智付寶、歐付寶等等, 名單可以在金管會銀行局的「電子支付機構」項目下查到。

至於「第三方支付服務業」,也就是可以經營「代收代付」的,在台灣其實很多,目前四千 三百多間,包括「台灣連線有限公司」,也就是 LINE Pay,都在其中。名單可以在 政府資料開放平臺取得。

而像悠遊卡公司,就屬於電子票證,不能代理收付也不能在電子支付帳戶間轉帳,相關法律 規範是「電子票證發行管理條例」,與前段所提「電子支付業」、「第三方支付服務業」又 不一樣了。

網路上可找到「呼叫米球」的文章說得很清楚,可參考。

釐清這些差異後,再回來看金管會之前引起軒然大波的「電子支付機構使用者身分確認機制 及交易限額管理辦法」第22條修正草案,才比較看得懂:

一、調整期間最終時點定為106年9月30日:

鑑於我國將於107年第4季接受亞太防制洗錢組織(APG)之相互評鑑,評鑑結果如不理想, 除影響我國國際聲譽外,將衝擊我國金融機構業務發展及國人金融交易活動。為兼顧防制 洗錢國際標準及社會大眾對法規鬆綁之需求,考量相關評鑑作業之準備須於106年底前完 成,針對該辦法修正草案第22條第1項規定,調整期間最終時點為106年9月30日。

二、調整期間須「確認使用者提供之電子郵件信箱或社群媒體帳號」之身分確程序:

為適度提高簡化身分確認程序之嚴謹度,於調整期間內除採取「確認使用者提供之行動電 話號碼」之方式外,並保留「確認使用者提供之電子郵件信箱或社群媒體帳號」之身分確 認程序,維持雙重確認機制。

三、調整期間預告期間條文草案係為3階段,改採2階段配套措施:

(一) 第1階段(106年6月30日前):每月總交易金額上限為等值新臺幣1萬元。

(二) 第2階段(106年7月1日至9月30日止):每月總交易金額上限為等值新臺幣5千元。

(三) 另考量調整期間最終時點定為106年9月30日,且設有金額限制機制可適度控管風險, 刪除原定交易次數限制。

原文網址

乍看之下這很像「從此以後第三方支付如果沒用實名制,交易上限就變成一萬,然後變五千, 今年10月後還非得要實名制不可」,但其實這針對的只是「電子支付業」,而「第三方支付 服務業」是不受影響的。這篇新聞稿出來以後一片嘩然,金管會第二天還趕緊再發一篇 試圖滅火。

所以,若有需要用到這些服務,可視本身需求來決定採用那一類業者的服務。若只有代收代 付需求,選擇第三方支付業者即可,限制比較少些。

初學 functional programming

初學 Scala,限定只能使用 functional 以及 immutable 時,被卡在以下問題:

1
2
3
4
5
/**
* This function should return the first position of character `c` in the
* 2 dimensional vector `v`.
*/
def findChar(c: Char, v: Vector[Vector[Char]]): Option[(Int, Int)] = ???

看起來真夠簡單,但想不出怎麼回傳第二層的 indexOf 值。第一版是這樣寫的:

1
2
3
4
5
6
7
8
9
10
11
def findChar(c: Char, v: Vector[Vector[Char]]): Option[(Int, Int)] = {
var y = -1
val x = v.indexWhere(vv => {
y = vv.indexOf(c)
y > -1
})
if (x > -1)
Some((x, y))
else
None
}

這樣是不差啦,但就是多了個 var y。自己硬寫的結果是這樣:

1
2
3
4
5
6
7
def findChar(c: Char, v: Vector[Vector[Char]]): Option[(Int, Int)] = {
val x = v.indexWhere(_.indexOf(c) > -1)
if (x > -1)
Some((x, v(x).indexOf(c)))
else
None
}

用了兩次 indexOf 實在有夠難看。最後忍不住 Google:

1
2
3
4
5
6
7
def findChar(c: Char, v: Vector[Vector[Char]]): Option[(Int, Int)] = {
val r = v.map(_ indexOf c).zipWithIndex.find(_._1 > -1)
r match {
case Some((x, y)) => Some((y, x))
case None => None
}
}

但找到以後馬上結束就好,map 會轉換整個 Vector,其實是多的,還是感覺有點硬寫。

感想是:

  1. 要記得有 zip 這類東西。
  2. 思考方式還是偏 imperative.

另外一個大問題就是如果自我要求每個 recursive 都要是 @tailrec tail recursive 也是很障礙。

Go standard library net/rpc/jsonrpc 不支援 HTTP

之前寫 JSON-RPC over HTTP 的時候是直接用 github.com/gorilla/rpc,蠻簡 單就寫出來。這次打算用 standard library 去做,不額外引用 gorilla,結果 踢到鐵板。

目前 net/rpc 支援兩種 codec,一種是 Go 內建的 encoding/gob,另一種 是 net/rpc/jsonrpc。gob 可以透過 HTTP 包起來使用,也可以獨立作為 TCP service。而 jsonrpc 則是「只能獨立使用」。這在 API 裡面並沒有說明得很清 楚。相關討論在: https://github.com/golang/go/issues/2738 ,裡面提到原因是

jsonrpc can be used with HTTP by writing a suitable codec. Given that there is no standard definition of jsonrpc over http, I think that’s the best we can do.

原來 jsonrpc over http 沒有標準定義啊…

reflect.DeepEqual 與 time.Time

Go 很像加強版的 C,例如只要把 type 想像成 function call 的第一個參數, 那 method 就只是自動放參數而已。比如 POSIX C 裡頭的 time:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NAME
time - get time in seconds
SYNOPSIS
#include <time.h>
time_t time(time_t *t);
DESCRIPTION
time() returns the time as the number of seconds since the
Epoch, 1970-01-01 00:00:00 +0000 (UTC).
If t is non-NULL, the return value is also stored in the memory
pointed to by t.

對照 Go 的 time.Unix():

1
2
3
4
func (t Time) Unix() int64
Unix returns t as a Unix time, the number of seconds elapsed since
January 1, 1970 UTC.

這樣就很明顯了。

會講到這個是因為這陣子碰到一個奇怪的 bug,相同的情況在 native 環境下 reflect.DeepEqual 是相等,而 Docker 環境下 DeepEqual 不相等。印 log 出來發現不相等原因是 struct 裡面 time.Time 這個 field 比出來不相等, 但用 %v 印出來卻是一樣的。參考 DeepEqual 的說明,推測應該是針對 Time 裡面的每個 field 用 == 去比對,所以就改用 %#v 印,抓到是 loc *Location 這欄位不一致,原因是 Docker 預設 timezone UTC,而我本機環境 是 CST。參考原始碼,time.Time 定義如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Time struct {
// sec gives the number of seconds elapsed since
// January 1, year 1 00:00:00 UTC.
sec int64
// nsec specifies a non-negative nanosecond
// offset within the second named by Seconds.
// It must be in the range [0, 999999999].
nsec int32
// loc specifies the Location that should be used to
// determine the minute, hour, month, day, and year
// that correspond to this Time.
// Only the zero Time has a nil Location.
// In that case it is interpreted to mean UTC.
loc *Location
}

sec, nsec 這兩個欄位放的是 Epoch 絕對時間,而用 loc 放時區。所以 同樣的「時間點」,不同的「時區」時,會造成 struct 內容不一致, DeepEqual 就認為不相等了。追到這裡覺得似乎很合理,就把兩邊的時間都轉換 成 func (Time) Local 再來比對,結果卻仍然不相等,而且 %#v 印出來 「每個欄位」的值,包括 *Location 的 pointer 指向位址都一樣,這樣還是不 相等,就很傻眼了。

要再知道原因當然有辦法,讀 DeepEqual 的原始碼就可以,但已經花了不少時間, 而且這方向感覺解法不漂亮。因為 reflect 相關函數的速度都會慢,已經看這個 DeepEqual 不爽很久了,就直接另外用 type switches 寫了針對不同型別的比對 方式,碰到 time.Time 就直接呼叫 func (Time) Equal,把 DeepEqual 當 成最後手段來用。

然而這個疑惑還是沒完全解開,最近時間不夠用,只好先丟著以後再追。

Netflix says Geography, Age, and Gender are “Garbage” for Predicting Taste

FORTUNE 原文連結

一如往常的這背後有很多可以談,但所知不足,也恐怕沒有足夠空閒好好整理出 來,只能簡短打一些。首先這篇點出個人資訊的差異性遠遠不如「好惡」的差異, Netflix 以使用者喜好去 grouping 時,地理位置、年齡、性別等等幾乎完全不 列入考慮。在全世界都是同一個演算法。比如文中例子,90% 動漫流量來自日本 之外,國籍影響遠不如宅度來得大。

以這個類比到廣告產業,有趣的因素就多了。首先影片與文學、繪畫等等有類似 性質,往往可以跨越語言、地理限制等,但廣告常常是與地理高度相關的。比如 新開一家餐廳,若不是高度針對某特殊族群,例如高價位米其林美食,或是主打 韓劇歐巴主角等,往往較為重視餐廳周遭的客人,而非對全市或全縣打廣告,更 不用說全台灣。但若是大品牌廣告,例如某車廠新車上市,就會是全台。再例如 新的化妝品,針對女性就遠大於針對男性。之前忘記從哪裡看到的資料,也說若 發傳單這行為能達到某種針對性,轉化率可以有很大的差異。

把這兩個觀點結合起來會是什麼呢?個人認為這在暗示目前網路廣告準確度還有 非常大空間,而有趣的是,儘管這是個事實,但從廣告主開始,卻似乎還無法做 出很好的應對。這是指台灣情況,再遠我就無知了。廣告主目前的嘗試,多半是 採取風險轉嫁,將 CPV (cost per view), CPC (cost per click) 一步步推到 CPI (cost per installation),或是更廣義的 CPA (cost per action),簡言之 就是「我賺到錢才付你錢」。然後中間又穿插了 agent 下單到各種平台,但以實 際接觸經驗,也多半是各種經驗上、實務上理由來決定怎麼配,中間並沒牽涉到 什麼神奇的 DM/ML 技術。如此無效率的情況下必然中間有市場,往這方向談下去, 立刻就會碰到 Appier 這類公司。

數據要多大才有用?這中間牽涉到媒體、廣告平台、agent、廣告主、以及一大堆 沒寫出的各種角色競合,有興趣可以看看 LUMA Partners 廣受引用的 這些圖。似 乎掌握相當多有效資料的是 Facebook 跟 Google,但在我看到的、弱弱的場景中, 這兩間往往只被當成下單的標的之一,是 agent 在決定這邊下多少、那邊下 多少、怎麼下。在我沒有實際看到的地方,聽說有很多大咖在互相接近,消滅中 間人,直接以各自掌握的資源對接,並藉以壓迫沒跟上時代的人。

這一片亂局中,目前所處的公司正站在哪裡,該站在哪裡,要怎樣才能走過去, 這些我都還不是看得很清楚。

Go 1.5 Vendor Experiment with Git Submodule

一開始當然要先看這篇 Go 1.5 Vendor Experiment了解一下究竟 怎麼做的,簡言之就是把 /vendor 這目錄當成類似 /node_modules 這樣的 概念去用,從目錄最尾端開始往上,有就優先用,都沒有才會用到 GOPATH 裡 面的。

由於最終打算用 docker 包起來,所以想用 git submodule 來管 vendor 底下的東西,這樣 docker build 的過程中可以不需要裝 godep 這樣的 package manager,也不用把關聯的程式碼放入 vcs 裡。但在把專案換過去的時 候,覺得最麻煩的是沒有適合的自動化工具,godep save 是在 GOPATH 的想 法下運作的,若把 GO15VENDOREXPERIMENT=1 打開,會把目前用到的複製到 vendor 底下,然後不包括 vcs 目錄例如 .git,這個在轉換到 submodule 的時候會有問題。研究出來比較簡單的方式是:

  1. glide,或只要類似功能的都 可以。

  2. glide init,如果之前有用 godep 之類其他的,它會自動嘗試匯入,不 然會自己找關聯。跑完會產生 glide.yaml。注意,這邊只會找第一層,也 就是你的專案裡面直接 import 的關聯。glide.yaml是可以自己編的,要 固定版本等等的都是在這邊指定。

  3. glide install,會遍查各層關聯,把所有用到的全部抓下來放到 vendor,並把版本也一併列出放入 glide.lock。這檔案是自動產生,自 己編了下一次跑也會被蓋掉。

  4. 接下來用這支 python script 自動產生命令,餵給 shell 執行。python glide-gitsubmodule.py | sh

Method as Argument in Javascript

前些時候談過 在 Go 中使用 method 作為 argument ,它的觀念、適用場合有點像 partial application ,但當然形式完全不同。若把使用語言換為 JavaScript,要傳 function 通常都 是拿來當 callback,常常是 anonymous。如果用到超過一次,通常就會拉出來取 個名字。如果會重複用但參數略有不同,那可以寫個角色像是 factory 的 function 用來產生「要被當成 callback 的 function」。不過並非所有人都像 我一樣那麼不愛用 JavaScript 的(半殘)物件導向,自然免不了會有「想拿 method 當成 callback」的情況。以下程式碼雖然直覺,但就會掉入 this 陷 阱裡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'use strict';
function Bot(name) {
this.name = name;
}
Bot.prototype.selfintro = function () {
return "I'm " + this.name;
};
function roll(intro) {
console.log(intro());
}
var no5 = new Bot('Number 5');
roll(no5.selfintro);

原因是執行到 roll 裡面時,this 並未被定義,所以呼叫傳入的參數 no5.selfintro 時,裡面 this 也一樣沒有定義。修正方法是用 bind 指 定 this,把最後一行換成:

1
roll(no5.selfintro.bind(no5));

語法很簡單,但在 JavaScript 程式中盡量把一切都化為資料與 function 個人 覺得是蠻好的設計方式。

高科技人才難尋?

這話題也出來一陣子了,但自身看法似乎比較怪異,沒看到類似的,遂姑且寫下 留個紀錄。由於經驗限制,本文所指人才,範圍侷限在「軟體工程師」。

企業說高科技人才難尋,而觀察周遭同行,抱怨多半集中在老闆沒用、薪水太低、 長官太蠢、工時過長等等,似乎少有人抱怨失業,所以大概可判斷此事為真。從 供給與需求來看,人才難找、就業充分,表示需求高而供給低。

先從需求高談起。需求高,企業搶人,自然有動力盡量提高薪水及福利。常看到 工程師抱怨老闆小氣,但若是提高薪資福利能提高獲利,老闆就會去做。薪水停 滯,背後原因並不一定是簡單的「小氣」,而可能是企業獲利能力沒那麼好,提 高薪資無法反映在獲利上,賺不到錢,自然也付不出錢。

而從供給面談,個人覺得最受忽視的一點是:人才數量,本來就是有限的。 台灣新生的人力有限,條件好的,除資訊外,也有其他學門可選擇。如果條件沒 變好,人才選擇學資訊的比例就不會增加。增設相關科系,只會產出更多不適任 者,不會憑空變出人才來。而若讓排名好的大學增加招生人數,教學品質必定降 低,還會壓擠到原本分配已經極度不均的教學資源。

只看資訊人才的話,與為台灣企業工作相比,外商(包括中國)以及新創公司也 吃掉了不少供給。這都代表價值選擇上,後兩者能夠提供文化、環境、薪資、福 利等綜合起來相對有競爭力的選項。

談完現象,來談解法;一樣從供給與需求入手。

搶不到人,但又無法增加獲利,一個合理的解決方案就是「減少需求」,也就是 減少所需的工程師數量。台灣工程師普遍程度不錯,但賣肝的多,抱怨公司無效 率的也多,與其找更多人,不如設法改善效率。這可由幾個不同面向著手:

  1. 中高階主管的世代交替很重要,因台灣是硬體起家,研發主管懂軟體開發的相 對少,需盡快汰換。

  2. 制度上需加強中階主管的實作責任,不能只是發號施令,要把實作設定為工作 一部分。只有真正參與,才能正確辨認出提昇效率的方法,以及避免錯誤決策。

  3. 與前項搭配,制度上增進開會效率、盡可能資訊透明、組織扁平,以上種種都 是為了減少主管的「非研發庶務」。

  4. 與 2, 3 項搭配,優先升遷具有研發實力以及溝通能力的工程師。簡言之,設 法讓「喜歡研發並擅長溝通」的人出線,而非「不寫程式也沒關係,只要升遷 就好」的人。

工程師的美德是懶惰,若讓能力最佳的工程師最大程度發揮懶惰的美德,並且將 此美德在組織中擴散,整體效率就會提高。不看程式的主管容易讓「寫爛程式快 速達成上級要求」的工程師出頭,而這樣的人上位後也只會搞政治、尸位素餐, 惡性循環。科技業幾間巨頭內部不乏人才,但往往排列沒有最佳化,搖動一下, 會有很大改善空間。

而在業務擴張方面,台灣企業主常一開口就大氣地說要雇幾百個工程師,這很可 能正是問題所在。究竟這幾百人進來要做什麼事?這事能賺多少錢?而這些錢, 分給這幾百人,還剩多少?如果換一下觀念,同樣預算,人找少一點,錢給多一 點,花更多功夫在組織效率上,很可能做得到一樣的事情。差別是錢多了,找來 的是人才,錢少了,只能找人家挑剩的。

另外一個可能解法是「提高獲利能力」。但如何提高?現有的高科技業巨頭,商 業模式已經固定,不太可能突然獲利大增。新創是個機會,但由於世界最佳新創 環境仍在矽谷,以及中國的資金與市場優勢,要成氣候,無可避免要與矽谷或中 國接軌。如此結果,雖然賺到錢的不見得是台灣人,但會是台灣人才的創業經驗, 然後以這些人為種子,才容易產生成功的本土新創事業。如此案例,已經有好幾 個正在發生。

另一機會是傳產。台灣傳產一直很強,而現在的高科技,已經不能只視為單一產 業,而應該是「所有行業的高科技化」。高科技業待遇相對不差,所以傳產要高 科技化的第一個功課,是要狠下心出得起錢。當然,一開始來的不見得是實力派, 很有可能是「口才很好派」,但只要待遇好、講實績、砍人不手軟,遲早會吸引 到高科技業內幹得不爽、敢衝敢拼的實力者。

與此同時,要理解的是,並不是架了網站、發行了 app 就是高科技化,創新才是。 比如 data visualization 鼎鼎大名的 D3.js 創始者之一 Mike Bostock 當時是 任職於紐約時報,傳產要做高科技,應以此為師。從這角度思考,前面所提新創 事業人才與傳產,就有了強烈結合理由。

整理一下以上提出的幾點看法:

  1. 已建立的大型科技企業,應以改善組織效率、精簡人力、提高待遇為優先。對 新增業務,應以精兵為目標。

  2. 以國外經驗與資金為引,培植創業人才,進而培育自有、高獲利之新創企業。

  3. 傳產應跳入競爭,追求創新或新創公司,吸引人才回流。

而其後邏輯是:

人才數量有其自然上限,提高較為困難,不如設法集中資源、提高獲利能力以爭 搶人才。若以「生產更多軟體從業人員」為解方,恐怕適得其反。