12 pt 的字,到底是多大

(重刊 2017/12/18 在 MMDays 的文章)

這幾天筆者加購了一台配備高解析螢幕的筆電,由於平時使用的並不是 Apple 系統,且筆電算算也有六年沒換,雖說蹭別人的 Mac Book Pro 玩過幾次,以高解析螢幕為主的使用經驗,在此之前還真沒有過。筆電來了,自然要把附帶的 Windows 10 縮一縮,裝上慣用的 Ubuntu 才是正辦。過程中,又碰上老問題:字太小。

這問題快十年前就碰過,大約知道是 DPI 設定不正確。雖說本行不是幹這個,但為了顧眼睛還是得處理一下,還可寫篇文章談談不專業的豆知識交差。先從字型大小說起。

字型尺寸當然是從電腦出現之前就存在,pt 是 point 縮寫,雖在歷史上幾經變遷,最終在數位出版時代訂立 desktop publishing point,長度為 1/72 英寸,大約 0.353 公釐。說到這似乎已說完了:那麼,12 pt 字,當然就是 12/72,也就是 1/6 英寸(又稱 1 pica),會有什麼問題呢?

然而對電腦使用者而言,卻往往不是如此。我們知道 12 pt 的字,用印表機印出時,大小是固定的。但在一般解析度螢幕上,字比較大,而在高解析度螢幕,字就變小了。這似乎可以自圓其說:高解析度下,一個螢幕上的「點」比較小,所以如果螢幕上的字是由固定點數所組成的,那麼每一點越小,字當然就比較小了。早期的字型的確是這樣的,叫做點陣字,也的確會有在不同解析度大小不一致的問題。但後來向量字型出現,可以平順的縮小放大,也開始用 pt 為標示單位,為何還是有大小不一致的情況呢?

要說明這個,就必須先說明 DPI 是什麼1。DPI 是 dots per inch 的縮寫,表示每英寸點數。數字越大,解析度越高。例如印刷常用的 300 dpi,看起來已經非常清晰,一般 24” 的 1080p 螢幕是 92 dpi,Apple 的 Retina 筆電多半落在 200 多,而 iPhone 由於觀看距離更近,可高達 300 多,iPhone X 更高達 458 dpi (註 1)。有了 DPI 概念之後,再回去想想 pt 的問題,就比較清楚了:假定某印表機以 300 dpi 列印,那麼 12 pt,也就是 1/6 寸的字型,其大小就是 50 點 (dots),而若是在解析度比較低的螢幕,例如 90 dpi,就是 15 點。如此,同樣的字型尺寸,顯示在不同解析度的螢幕上,就是一樣大,只是精細程度不同。

這麼說來,只要知道螢幕 DPI,字型大小的問題照理說就可以得到解決,但為何沒有呢?這又是另外一個故事。1980 年代,Apple Macintosh(麥金塔)電腦設定的解析度是 72 dpi,讀者可發現這跟 point 單位一致,也就是 1 point 剛好對應到 1 dot,12 pt 在螢幕上就用 12 dots 表示。當時搭配的螢幕是 512 x 342,換算下來寬度大約是 7.1 in,而北美慣用的 letter 大小紙張寬度 8.5 in,兩邊扣掉 0.7 in 的留白,恰好是 7.1 in,所以若螢幕不設定兩邊留白,字型大小就會完全一致。這乍看是非常合理的設定,但也產生一些問題。例如 12 pt 的字,在 300 dpi 下,每個字可以用 50 點表示,相當足夠,但在螢幕上,就只剩 12 點,加上英文字母上下有突出,主要的部份例如字母 x,只剩下 6 點可用,看起來就比較粗糙。再加上螢幕跟眼睛之間還有鍵盤,一般觀看距離比紙張來得遠,字看起來就更小了。

這時,微軟想出了一個聰明 (?) 的方法來改善這問題:相對於 Apple 的 72 dpi,他們增加 1/3,在所有軟體中將解析度設定為 96 dpi. 短期內這有幾個效果:若需要 10 pt 的字,解析度是 72 dpi,就只能用 10 點來設計字型,看起來比較粗糙;而若解析度是 96 dpi,就有 13 點,設計上會更容易些。而在顯示時,由於當時螢幕一般都還在 72 dpi 的水準,96 dpi 會導致螢幕使用比所需更多的點數來顯示,讓字母高度變成原先的 4/3 倍,看起來比較清楚。顯而易見的缺點是,這背離了原本 DPI 與 pt 的關係,而隨著 Windows 3.1 大紅,到 Windows XP 長達十多年的統治,這也造成相當深遠的影響。在 Windows 7 之前,預設的 96 dpi 並不會隨著不同螢幕而改變,這也造成前面所提到的,高解析螢幕字比較小的印象。

為了解決這問題,後來微軟發展出 device independent pixel (DIP) 這單位,其定義就是每 DIP 為一寸的 1/96。沒錯,就是 96 dpi 這個魔術數字。舊的 Windows 應用程式,其基本假設就是畫 96 個點是一寸,那麼在高解析螢幕上,只要將其使用的「點」定義為 DIP,就可以保持這程式的界面仍然可用。這也是為何一些比較古早的應用程式,看起來有點糊糊的,因為 Windows 自動以 DIP 放大了原先界面的緣故。

讓這筆糊塗帳更加糊塗的,還有另一個當初看起來很有道理的設計:網頁的 CSS 規範。CSS 全名為 Cascade Style Sheet,就是一般網頁使用的排版格式,其內有個常見的長度單位,px, pixel unit. 以 1998 年公佈的 CSS level 2 來說,CSS 的長度單位分成兩種,絕對與相對。絕對單位包括了 in, cm, mm, pt, pc (pica) 等等,可注意到也有熟悉的 pt,而相對單位則有 em, ex, px,其中 em, ex 是相對於設定的字型大小,例如 1 em 就等於設定的字型大小,所以若設定字型大小為 12 px,那 1 em 就是 12 px. 而 px 則是相對於「顯示裝置的解析度」,也就是螢幕上的一點。由於各個螢幕一點的大小不見得一致,px 就成了相對單位。在 CSS 2 裡面,針對解析度不同以及觀看距離不同的情況,還建議了參考的改變方式,這容後再論。

在 199x 的年代,一般螢幕的解析度不是那麼高,設計上差了幾點,看起來就差很多。例如一條線寬度是一點或兩點,看起來會有相當明顯的差異。所以,設計上會講究 pixel perfect,也就是仔細控制網頁上各個元件長度是幾「點」,而不使用絕對單位。在指定字型時,往往是類似 font-size: 12px 這樣的形式,而其他諸如欄位寬度等等,也一律使用 px 來設計,以確保顯示出的結果符合預期。但這造成一個問題:網頁上的長度單位,包括字型,又再度隨著螢幕解析度而改變了。在 72 dpi 的螢幕上, 12 px 就是 12 pt,沒什麼問題,但螢幕的 dpi 一直在提昇,12 px 的字也就越變越小,只能創造出各式各樣觀念去修正。

在這裡就要提到,前面 CSS 2 針對不同解析度及不同觀看距離,所做的建議。其內容是,1 px 應等於在一般觀看距離,即平均手臂長度,定義為 28 寸,觀看 90 dpi 的螢幕,螢幕上 1 點的大小。也就是說,1 px 從「長度」變成了「角度」,約略等於 0.0227 度,投射出的距離,是會隨著觀看距離而改變的。底下這張圖,直接來自 CSS2:

Reference pixel

這裡顯示出,在 28 寸的距離,1 px 約等於 0.28 mm,而在 140 寸(例如投影情況),1 px 約等於 1.4 mm,是原先的 5 倍。複雜歸複雜,至少這裡對於 1 px 提出一個還不錯的標準,希望在不同的觀看條件下,都能得到一致的經驗,只是事與願違,後來大部分實作都還是採用 1 px 對應螢幕上一點的設計,且 96 dpi 隨著 Windows 四處普及,大家都把 96 px 當成實作上需要 1 寸的約略單位,不但應用程式與網頁如此,連螢幕也是,以避免使用者由於 dpi 差距過大而難以使用。在實作日漸偏離標準的情況下,CSS 2.1 這修正版裡,針對這部份有幾個修改,都相當有趣:

  1. 參考用的 px,從原先定義的 28 寸,90 dpi,改為 96 dpi。也就是說,W3C 肯認大部分螢幕都設計成接近 96 dpi,所以在預設的閱讀距離,也就是 28 寸時,參考用的 px 約略就等於螢幕上的一點。

  2. 將 px 從相對單位,改為絕對單位,長度是 0.75 pt. 如此一來,在 96 dpi 的裝置上,96 px = 72 pt = 1 in,就一致了。

  3. 讓 px 有雙重定義:在接近 96 dpi 的螢幕上,px 保持在 0.75 pt,也就是約略仍等於螢幕上的一點。而在其他情況,則採用角度的定義,而讓其他單位如 pt 等「相對於 px」定義出來。亦即,假設現有一 200 dpi 螢幕,在 28 寸位置,若採用角度定義,px 就不再表示螢幕上的一點,而是約略等於兩點,而 pt 由於相對於 px,就仍然可以約略等於 1/72 寸。而由於是角度,所以只有在 28 寸的觀看距離,pt 才會約略等於物理上的 pt 單位。若在不同距離,例如預設的觀看距離比較近的 iPad mini 與 iPhone 等,網頁上的 pt 與物理上的,就不會相等了。

繞了這麼一大圈,回到一開始的題目:新筆電的 DPI 設定。以下是設定前與設定後比較,這也可以看出,Apple 當時提倡 Retina 螢幕,打破 96 dpi 魔咒,還要維持各類程式、網站等仍然可用,實在是相當了不起的突破,也推動其他廠商跟進,最終造福使用者。

Before

After

[1]: DPI / PPI 的問題,本文略過不表。

govendor vs dep

我的 ~/.gitconfig 裡面有這樣的設定

1
2
3
4
[url "git@github.com:"]
insteadOf = https://github.com/
[url "git@bitbucket.com:"]
insteadOf = https://bitbucket.org/

作用是存取 private repository 的時候,會自動換成 git+ssh. 本來不需要設定這個, 因為 git clone 的時候都會特別注意網址,只是為了方便使用 go get,因為它對這兩 個站預設只會用 https://,碰到 private repo 的時候就沒法自動了。

但剛才抓別人的 project 下來以後跑 govendor sync 卻發現它針對 bitbucket.org/private_team/private_repo 這樣的情況沒法處理:

1
2
3
$ govendor sync
Error: Remotes failed for:
Failed for "bitbucket.org/private_team/private_repo" (failed to ping remote repo): https://api.bitbucket.org/1.0/repositories/pilot_seals/quividisdk: 403 Forbidden

改為試著執行 golang 官方做的 dep,竟然會自己讀 govendor 用的 vendor/vendor.json 檔案來轉換,而且順利的下載了 private repo 執行完畢。

找了一下原因,發現 govendor 裡面用了官方建議的 golang.org/x/tools/go/vcs 來 處理 bitbucket 的 repo 類型判斷,然後 vcs 裡面會呼叫上面的 API 網址;但如果是 private 的,就連那網址都會 403,導致噴錯。奇怪的是,dep 就沒有這個問題。看了看 程式碼,發現它用的不是官方那份 vcs,而是來自 github.com/Masterminds/vcs 的另 外一份,說明寫說是基於 vcs 擴充而成的。看一下 bitbucket 相關程式碼,發現多了這 段:

1
2
3
4
5
6
7
8
9
10
// Fast path for ssh urls where we may not even be able to
// anonymously get details from the API.
if ul.User != nil {
un := ul.User.Username()
if un == "git" {
return Git, nil
} else if un == "hg" {
return Hg, nil
}
}

這也太黑心了,dep 自己沒用官方那份,這樣 govendor 豈不是死得不明不白…

2018/07/24 補充

golang.org/x/tools/go/vcs 是從 cmd/go/internal/get copy 出來的,而 upstream 已解掉此問題:#5375,程式碼在 vcs.gogovendor 本身的 issue 在 #151.

Intel SSD 535 480 GB Shows as "sandforce 200026BB"

症狀為使用中突然硬碟消失,重開機硬碟有一定機率無法開機,且被辨識為 sandforce 200026BB. 若能正常開機,使用一段時間後又發生相同情形。一開始不能確定是主版或 SSD問題,又是重要的工作用機器,就先訂了新的筆電。沒想到最後修好…

先照著 Intel support 上面的作法試著更新韌體,但發現原先版本已經是 RG20 無法 更新。接下來試著從首頁進去直接找最新韌體,用 dd if=issdfut_2.2.3.iso of=/dev/sdb bs=1M 做隨身碟之後開機,成功更新到 RG21,竟然就沒問題了。雖然好像應 該查一下 change log 看看究竟是什麼問題,不過懶病發作,算了。

皇帝的舊衣

Fermat’s Library 觸發讀了 C. A. R. Hoare 這篇論文[1],其中 最常被引用的一段話應該是:

I conclude that there are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies and the other way is to make it so complicated that there are no obvious deficiencies.

The first method is far more difficult.

「我斷定軟體設計有兩種方式:第一種是非常簡單,所以明顯的沒有缺點。另一種是非常複 雜,所以沒有明顯的缺點。前者困難多了。」

最後的小故事也很有意思,簡中有翻譯但實在不好,試譯如下。

皇帝的舊衣

很久以前,有位皇帝非常喜歡衣服,把所有錢都花在買衣服上。他沒去管士兵、赴宴、或審 判等麻煩事。若是提到別位皇帝或國王,人們可能會說「他正出席議會」,但提到這位皇帝, 人們總是說「他在更衣室裡」。而他真的都在裡面。在一次不幸的場合,他因被騙而裸體示 人,臣民覺得好笑,而皇帝非常懊悔。於是他決定再也不離開王座,且為了避免再次裸體, 他規定往後每件新衣服只能覆蓋在原有的衣服上。

在首都這大城中,時間快樂地過去了。部長與大臣、織工與裁縫、訪客與臣民、縫工與繡工, 為了各類事務在王座室進進出出,而他們全都大聲讚嘆:「皇帝的盛裝真是華麗!」

有一天,皇帝最老也最忠誠的部長聽說有位出類拔萃的裁縫,受教於一間古老的高等針線術 學府,他發展出一種新的抽象刺繡藝術,其針腳如此精細,以致沒人能分辨到底存不存在。 於是部長想:這繡法一定非常傑出,若能聯絡上這位裁縫擔任顧問,我們就能將皇帝裝飾的 炫耀程度提升到一個新高,世界都會認可他是自古以來最偉大的皇帝。

所以這位誠實的老部長花了一大筆錢,把裁縫請來。裁縫被帶到王座室,在那裡他對如今已 完全蓋住王座的華服敬禮。大臣們渴望地等著他的建議。所以當他提出不要再增加更精細複 雜的刺繡潤飾,而是移除層層華麗衣飾,追求簡單優雅來代替複雜細節,你可以想像到大臣 們的震驚。他們交頭接耳:這裁縫並不是他自稱的專家,他的智慧已經被象牙塔中的長時間 研究搞混,無法了解現代皇帝的縫紉需求了。裁縫大聲爭辯了許久,說他的建議是合理的, 但大臣們聽不進去。最後,他接受了酬勞而回到象牙塔中。

直到此日之前,都無人揭露這故事的全部真相:在一個晴朗早晨,皇帝覺得很熱又無聊,於 是小心翼翼地從堆積如山的衣服下鑽出,現正在另一個故事中當個快樂的養豬人。而裁縫被 封為所有顧問的守護神,因為儘管他收了一大筆顧問費,卻從未成功說服客戶相信他的明悟: 他們的衣服下,沒有皇帝。

[1] Charles Antony Richard Hoare. 1981. The emperor’s old clothes. Commun. ACM 24, 2 (February 1981), 75-83. DOI=http://dx.doi.org/10.1145/358549.358561

Scroll Lock as Caps Lock

我的 Ubuntu /etc/default/keyboard 裡面有這設定:

1
XKBOPTIONS="terminate:ctrl_alt_bksp,ctrl:nocaps"

作用是「外加」一些選擇性的鍵盤設定。其中 terminate:ctrl_alt_bksp 這是 dpkg-reconfigure keyboard-configuration 就有選項可以設定,用途是這三個鍵一起按 就可以跳出 Xorg。而 ctrl:nocaps 則是手動加上,印象中圖形界面也可設定,但很久沒 用已不記得。這選項用途是把 Caps Lock 當成 Left Ctrl 來用。這樣設定的原因是,慣用 的編輯器 Emacs 使用時實在太常按 Ctrl,但筆電上用小指按久了會不舒服,所以用 Caps Lock 來代替。而原先的 Left Ctrl 功能仍然保留,因為外接實體鍵盤時用掌緣壓 Ctrl 很容易, 且筆電上有時仍然會用掌緣按而不用小指。

這樣設定是蠻順手,但仍有個缺點,就是沒有 Caps Lock 可用。比如要定義常數常常是 ALL_CAP_CONST 這種命名,一直按著 Shift 就有點不方便。

想到一個解決方案是拿真的沒在用的 Scroll Lock 當作 Caps Lock,偶爾要用時還有個鍵 可以按,於是想把這設定也做成一個 XKBOPTIONS,步驟如下。

首先新增 /usr/share/X11/xkb/symbols/local

1
2
3
xkb_symbols "sccaps" {
replace key <SCLK> { [ Caps_Lock ] };
};

接下來修改 /usr/share/X11/xkb/rules/evdev,在 ! option = symbols 這段最後面 補上

1
local:sccaps = +local(sccaps)

以及 /usr/share/X11/xkb/rules/evdev.lst, /usr/share/X11/xkb/rules/evdev.xml 都按照原先的格式加入 local:sccaps 這選項。最後把 /etc/default/keyboard 那行改為

1
XKBOPTIONS="terminate:ctrl_alt_bksp,ctrl:nocaps,local:sccaps"

重跑 dpkg-reconfigure keyboard-configuration 就生效。

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

完整 PDF 文件

後續調整

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 也是很障礙。