Electron用の検索ダイアログ作った
つくった
Electronアプリのページ内を検索するダイアログ。
https://www.npmjs.com/package/electron-search-dialog
使うと、こんな感じに検索ウィンドウが表示されて、
Electronのページのテキストを検索できる。
Excelの検索ダイアログに似せてみた。
npmにpublishしたので、npm installで利用できる
npm install --save electron-search-dialog
localeの設定が違えば、検索ダイアログの言語も変わる。
アメリカ語と、日本語しかサポートしてないけどね。
使い方
- ライブラリをインストールする
npm install --save electron-search-dialog
- 検索するウィンドウを指定して、インスタンス生成。
- openDialog()でウィンドウを表示。
var SearchDialog = require('electron-search-dialog').default; // create instance. var win = ... // searching page (BrowserWindow) var sd = new SearchDialog(win); // open search dialog sd.openDialog();
実際には
実際には検索ダイアログは、画面の操作をトリガーにして開かれるでしょうから、
ipcRendererで起動処理を走らせるような使い方になるでしょう。
サンプルコードがこちらにあります。
- main.js
var {app, BrowserWindow, ipcMain} = require('electron') var SearchDialog = require('electron-search-dialog').default; let mainWindow function createWindow () { mainWindow = new BrowserWindow({width: 800, height: 600}) mainWindow.loadFile('index.html') mainWindow.on('closed', function () { mainWindow = null }) // ----------------------------- // create SearchDialog instance, // and add open search dialog event // ----------------------------- var _searchDialog = new SearchDialog(mainWindow); ipcMain.on('openSearchDialog', (event, message) => { _searchDialog.openDialog(); }); } app.on('ready', createWindow) app.on('window-all-closed', function () { if (process.platform !== 'darwin') { app.quit() } }) app.on('activate', function () { if (mainWindow === null) { createWindow() } })
- index.html
<!DOCTYPE html> <html> <body> <h1>Hello World!</h1> We are using Node.js <script>document.write(process.versions.node)</script>, Chromium <script>document.write(process.versions.chrome)</script>, and Electron <script>document.write(process.versions.electron)</script>. <br> <button id="btn">open</button> <script> require('./renderer.js') </script> </body> </html>
- renderer.js
// ----------------------------- // at search button clicked, // send open search dialog event. // ----------------------------- var ipcRenderer = require('electron').ipcRenderer; var btn = document.getElementById('btn'); btn.addEventListener('click', () => { ipcRenderer.send('openSearchDialog', 'open'); });
いろいろ
BrowserWindowベースの検索ダイアログは重い
- それはそうだ。
- 一回開けば、そこからは表示・非表示の切り替えなので、遅くない。
- 検索ダイアログのBrowserWindowインスタンスを、余裕のある時にあらかじめ初期化するような作りにしておけば、全然軽く出来そう。
クリアボタン
入力のクリアボタンなんか用意しないでも、
HTML5のsearchボタンを使えば、入力クリアボタンがついてくる。
<input type="search" value="">
なのですが、検索ダイアログの縦幅が小さすぎると、
なんか格好悪くなってしまったので、入力クリアボタン入れてしまった。
Electronでのlocaleの切り替え方法
Electronでlocaleを切り替えるには、こうする。
特にAPIとかないみたい。
var {app} = require('electron') app.commandLine.appendSwitch('lang', 'en');
おわり
このライブラリを作った理由はだいたいElectronのfindInPageが使いづらいせい。
https://electronjs.org/docs/api/web-contents#contentsfindinpagetext-options
- 検索ワードを入力したテキストフィールドが検索マッチしたり、
- 次の検索候補に遷移しなかったり
同じように、Electronで画面の検索を行うライブラリには、
他に、chromeのwebviewを使って検索を実現する electron-in-page-search があります。
https://www.npmjs.com/package/electron-in-page-search
おわりだよ〜
Web Audio APIのOfflineAudioContextを使うたびに増えていく、スレッド数とメモリ使用量の調査
経緯
音声を操作するWeb Audio APIという、WebブラウザのAPIがあります。
そのAPIの中の、OfflineAudioContextというオブジェクトを、MacのElectron製アプリで利用していたのですが、
アプリを動作させながらアクティビティモニタを確認していたら、何かがおかしい。おかしい。
何故か、
音声を再生するごとに、
アプリが使用しているスレッドの数が増えていったのです。
ということがあり、これは困ったぞ、と、いろいろ調べてみたのでありました。
Electron 1.8.8 で検証
Electron バージョン 1.8.8。最初に問題を発見した環境。少し古い。
Electronに組み込まれているChromiumのバージョンは
>process.versions.chrome "59.0.3071.115"
最小の検証コード(一部)
ボタンを押したら音声を再生する。
https://github.com/taku-o/offlineaudiocontext-memoryleak/blob/master/electron188/renderer.js
var fs = require('fs'); var audioCtx = new window.AudioContext(); function toArrayBuffer(buf) { var _aBuffer = new ArrayBuffer(buf.length); var view = new Uint8Array(_aBuffer); for (let i = 0; i < buf.length; ++i) { view[i] = buf[i]; } return _aBuffer; } var btn = document.getElementById('play'); btn.addEventListener('click', function(elm, ev) { console.log('click play'); // read fs.readFile('./voice.wav', function(err, bufWav) { if (err) { console.log('failed to read wav. path %s', './voice.wav'); return false; } console.log('read wav'); // decode audio var aBuffer = toArrayBuffer(bufWav); audioCtx.decodeAudioData(aBuffer).then(function(decodedData) { console.log('decode wav'); // process audio var offlineCtx = new OfflineAudioContext(decodedData.numberOfChannels, decodedData.length, decodedData.sampleRate); var inSourceNode = offlineCtx.createBufferSource(); inSourceNode.buffer = decodedData; var gainNode = offlineCtx.createGain(); gainNode.gain.value = 0.4; inSourceNode.connect(gainNode); gainNode.connect(offlineCtx.destination); inSourceNode.start(0); // offlineCtx rendering offlineCtx.startRendering().then(function(renderedBuffer) { console.log('process wav'); // play var audioNode = audioCtx.createBufferSource(); audioNode.buffer = renderedBuffer; audioNode.connect(audioCtx.destination); audioNode.start(0); console.log('play processed wav'); }); }); }); }, false);
発生した現象
前述した通り、OfflineAudioContextで音声を再生する毎に、動作しているスレッド数が増えていく。
メモリの消費量も徐々に増えていく。
アクティビティモニタで確認してみると、
"offline audio renderer"という、いかにもOfflineAudioContextに関係ありそうなスレッドが
OfflineAudioContextが不要になった後でも残り続けているようです。
いろいろ試してみましたが、Javascript側からこの問題を解消することはできませんでした。
2550 Thread_13873430: offline audio renderer 2550 thread_start (in libsystem_pthread.dylib) + 13 [0x7fffb3b3e08d] 2550 _pthread_start (in libsystem_pthread.dylib) + 286 [0x7fffb3b3e887] 2550 _pthread_body (in libsystem_pthread.dylib) + 180 [0x7fffb3b3e93b] 2550 ??? (in Electron Framework) load address 0x10eaea000 + 0x2640e7 [0x10ed4e0e7] 2550 ??? (in Electron Framework) load address 0x10eaea000 + 0x290709 [0x10ed7a709] 2550 ??? (in Electron Framework) load address 0x10eaea000 + 0x26f183 [0x10ed59183] 2550 ??? (in Electron Framework) load address 0x10eaea000 + 0x25a15c [0x10ed4415c] 2550 ??? (in Electron Framework) load address 0x10eaea000 + 0x25a6bf [0x10ed446bf] 2550 CFRunLoopRunSpecific (in CoreFoundation) + 420 [0x7fff9e185114] 2550 __CFRunLoopRun (in CoreFoundation) + 1361 [0x7fff9e1858c1] 2550 __CFRunLoopServiceMachPort (in CoreFoundation) + 212 [0x7fff9e186434] 2550 mach_msg (in libsystem_kernel.dylib) + 55 [0x7fffb3a4b797] 2550 mach_msg_trap (in libsystem_kernel.dylib) + 10 [0x7fffb3a4c34a]
Electron 2.1.0 で検証
Electronのバージョンを上げて試してみる。
Electronを2.1.0に上げてみました。
>process.versions.chrome "61.0.3163.100"
Electron 1.8.8で試した時のように、
音声を再生する毎にスレッド数が増えて残り続けることはなくなりました。
やった!!
しかし、ごっつりメモリを消費するようにもなってしまいました。
ギエエエエエエエ
検証コード
https://github.com/taku-o/offlineaudiocontext-memoryleak/blob/master/electron210/renderer.js
Electron 2.1.0 に終了処理を追加
これには解決方法があります。
使用済みのOfflineAudioContext、AudioContextの終了処理をすれば良いのです。
disconnectとcloseを使えば良い。
(参考) Web Audio APIの闇
https://qiita.com/zprodev/items/7fcd8335d7e8e613a01f
var fs = require('fs'); function toArrayBuffer(buf) { var _aBuffer = new ArrayBuffer(buf.length); var view = new Uint8Array(_aBuffer); for (let i = 0; i < buf.length; ++i) { view[i] = buf[i]; } return _aBuffer; } var btn = document.getElementById('play'); btn.addEventListener('click', function(elm, ev) { console.log('click play'); // read fs.readFile('./voice.wav', function(err, bufWav) { if (err) { console.log('failed to read wav. path %s', './voice.wav'); return false; } console.log('read wav'); // decode audio var audioCtx = new window.AudioContext(); var aBuffer = toArrayBuffer(bufWav); audioCtx.decodeAudioData(aBuffer).then(function(decodedData) { console.log('decode wav'); // process audio var offlineCtx = new OfflineAudioContext(decodedData.numberOfChannels, decodedData.length, decodedData.sampleRate); var inSourceNode = offlineCtx.createBufferSource(); inSourceNode.buffer = decodedData; var gainNode = offlineCtx.createGain(); gainNode.gain.value = 0.4; inSourceNode.connect(gainNode); gainNode.connect(offlineCtx.destination); inSourceNode.start(0); // offlineCtx rendering offlineCtx.startRendering().then(function(renderedBuffer) { console.log('process wav'); // play var audioNode = audioCtx.createBufferSource(); audioNode.buffer = renderedBuffer; audioNode.connect(audioCtx.destination); audioNode.onended = function(evEnd) { console.log('play processed wav ended'); inSourceNode.disconnect(); gainNode.disconnect(); audioNode.disconnect(); audioCtx.close(); global.gc(); console.log('close audio node'); }; audioNode.start(0); console.log('play processed wav'); }); }); }); }, false);
メモリの消費量はだいぶ減りました。
音声再生しても、スレッド数も増えません。
しかし、まだまだ音声再生するごとに、メモリを消費してしまいます。
ChromiumのDevelop ToolのHeap Snapshotで、
音声再生前、音声再生後のスナップショットを取り、
どのようなゴミが残ってしまうのかと確認すると、
"Pending activities / 1 entries", ↓ "Pending activities / 2 entries", ↓ "Pending activities / 3 entries",
OfflineAudioContextで音声を再生する毎に、
"Pending activities"というゴミが増え続けていくことがわかりました。
こいつがメモリを掴んでいるのでしょう。多分。きっと。
検証コード
https://github.com/taku-o/offlineaudiocontext-memoryleak/blob/master/electron210close/renderer.js
Electron 3.1.1 で検証
さらにElectronのバージョンを上げる。3.1.1。
ちなみに最新のElectronのバージョンは4であり、まだバージョンを上げる余地がある。
4は新しすぎるので、個人的にはまだちょっと不安ありますけれど。
>process.versions.chrome "66.0.3359.181"
良くなった。
Heap Snapshotで確認してみたが、
"Pending activities"の数が、音声再生後に増えるようなこともない。
勝ったな!!(なにが?)
まだ少しずつはメモリの消費が増えている。
他にも問題があるのかな?
issueリストの中に該当するものがあるかもしれない。
https://bugs.chromium.org/p/chromium/issues/list?can=2&q=component:Blink%3EWebAudio&sort=-modified&colspec=ID%20Pri%20M%20Stars%20ReleaseBlock%20Component%20Status%20Owner%20Summary%20OS%20Modified
まぁでも、これぐらいなら良いでしょう。
(そ、そうかな?)
検証コード
https://github.com/taku-o/offlineaudiocontext-memoryleak/blob/master/electron311/renderer.js
MacのElectronアプリに、MacのHelp Bookを組み込む
Macでヘルプで検索した時に起動するアレ。Help Book。
MacでElectronアプリを作るのであれば、
Help Bookにヘルプ機能を作り込めば、よりMacライクなアプリになるのではないか。
そう考えてトライしたのであった。
作業手順
1. Help Bookを作る 2. ElectronアプリにHelp Bookを組み込む 3. ElectronアプリからHelp Bookを立ち上げる
Help Bookを作ろう
まず、ヘルプを作る。手順は下のようになる。
面倒くさい。
ヘルプ作成用のアプリを使った方が良いかもしれない。Help Crafter(http://www.putercraft.com) とか
1. ディレクトリ構造を組み立てる 2. ヘルプをHTMLで書く 3. 索引を作る 4. ExactMatch.plistを用意する
Help Bookの資料は、このあたりにあります。
https://developer.apple.com/library/archive/documentation/Carbon/Conceptual/ProvidingUserAssitAppleHelp/authoring_help/authoring_help_book.html
http://macwiki.osdn.jp/wiki/index.php/ヘルプ作成のノウハウ
ただ、これだけだと資料が足りない。
次の本にもHelp Bookの説明が書いてあるらしい。
https://www.amazon.co.jp/Cocoa-Recipes-Mac-OS-X-ebook/dp/B003HOXLGG
Help Bookのディレクトリ構造を作る
AppleのHelp Bookの資料は↓のようになっている。
なのだけど、ディレクトリ名とか、ヘルプファイルの置き場所は、推奨から変えても動作する。
SampleApp.help/ Contents/ Info.plist -- 設定 Resources/ shrd/ -- アイコンなど共通の素材入れる index.html -- トップページ search.helpindex -- 索引 ja.lproj/ ExactMatch.plist -- このキーワードで検索したら、このanchorに遷移させる、という設定 InfoPlist.strings pgs/ about.html -- ヘルプの各ページ voicecode.html gfx/ -- 画像 scrpt/ -- JavaScriptなど sty/ -- スタイルシート
ヘルプを書く
ヘルプはHTMLで書く。
SafariのHelp Bookをみると、割と新しいめのHTMLの書き方でも良いようです。
jQueryとか使ってた。
ハイパーリンクも、Help Book特有の書き方しなくて良い。
/Applications/Safari.app/Contents/Resources/Safari.help
HTMLは閉じタグが必要。
XMLとしてパースできないと、あとの索引作成の処理でエラーになる。
<html lang="ja"> <head> <meta charset="UTF-8" /> <meta name="ROBOTS" content="ANCHORS" /> <meta name="KEYWORDS" content="about" /> <meta name="AppleIcon" content="shrd/icon.png" /> <meta name="description" content="SampleAppについて" /> <meta name="AppleTitle" content="SampleApp Help" /> <title>SampleApp Help</title> <link rel="stylesheet" type="text/css" href="../assets/css/snap.css" /> <link rel="stylesheet" type="text/css" href="../assets/css/nav.css" /> </head> <body> <div class="snap-drawers" id="sidebar"> <div class="snap-drawer snap-drawer-left"> <div> <h4>アプリに関わること</h4> <ul> <li><a href="apps-about.html">SampleAppについて</a></li> <li><a href="apps-voicecode.html">音声記号列</a></li> <li><a href="apps-trouble.html">トラブルシューティング</a></li> <li><a href="apps-update.html">アプリを更新するには</a></li> <li><a href="apps-uninstall.html">アンインストールするには</a></li> <li><a href="apps-backup.html">設定をバックアップするには</a></li> <li><a href="apps-license.html">AquesTalkのライセンスについて</a></li> <li><a href="apps-contact.html">連絡先</a></li> </ul> </div> </div> <div class="snap-drawer snap-drawer-right"></div> </div> <div id="content" class="snap-content"> <div id="toolbar"> <a href="#" id="open-left"></a> <h1>SampleApp Help</h1> <div class="docs"> <a name="about"></a> SampleAppについて </div> </div> </div> <script src="../assets/js/snap.min.js"></script> <script type="text/javascript" src="../assets/js/nav.js"></script> </body> </html>
索引を作る
hiutilコマンドを実行して索引ファイルを作る。拡張子は.helpindex
タイトルページと同じ場所に置く。
cd SampleApp.help/Contents/Resources/ja.lproj; hiutil -C -agv -f search.helpindex .
ExactMatch.plist
ExactMatch.plistで指定すると、あるキーで検索した時に指定したanchorが検索でヒットするようになる。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>about</key> <string>about</string> <key>ABOUT_SAMPLEAPP</key> <string>about</string> </dict> </plist>
別に空っぽも良い。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> </dict> </plist>
ElectronアプリにHelp Bookを組み込もう
作ったHelp BookをクリックしてHelpViewerを起動しても、このままではヘルプは表示されない(うげー)。
システムにHelp Bookを登録する。
アプリのInfo.plistで指定しておくと、アプリ起動時に自動的に登録してくれる。
1. Info.plistでHelp Bookを指定する 2. アプリをビルドする 3. 作ったHelp Bookを、ElectronアプリのResourcesにコピーする 4. アプリを起動して、Help Bookを登録する
Info.plistでHelp Bookを指定する
このInfo.plistは、Help BookのInfo.plistではなくて、ElectronアプリのInfo.plist。
こんな感じのxmlファイルを用意して、
- extend.plist
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleHelpBookFolder</key> <string>SampleApp.help</string> <key>CFBundleHelpBookName</key> <string>SampleApp Help</string> </dict> </plist>
アプリをビルドする
作ったxmlファイルをextend-infoで指定して、Electronアプリをビルドする。
ElectronアプリのInfo.plistに書き加えられる。
electron-packager . SampleApp --platform=darwin --arch=x64 --electronVersion=2.0.8 --overwrite --asar.unpackDir=execmd --extend-info=extend.plist
作ったHelp Bookを、ElectronアプリのResourcesにコピーする
Help BookをElectronアプリのResources直下にコピーする。
Help Bookはアプリと一緒に配ることになる。
SampleApp.app/Contents/Resources/SampleApp.help
アプリからHelp Bookを起動する
さて、Help Bookを作ったのは良いが、
Electronアプリから起動するにはどうしたら良いだろう(えー)
AppleScriptを使うのが良いかな?
↓をマルパクリした。
http://piyocast.com/as/archives/2661
これでヘルプが起動した。
use AppleScript version "2.4" use scripting additions use framework "Foundation" use framework "AppKit" set anAppName to "SampleApp" set aTargAnchor to "about" set hRes to openHelpBook(anAppName, aTargAnchor) of me on openHelpBook(anAppName, aTargAnchor) set locBookName to getHelpBook(anAppName) of me if locBookName = false then return false current application's NSHelpManager's sharedHelpManager()'s openHelpAnchor:aTargAnchor inBook:locBookName end openHelpBook on getHelpBook(anAppName) set aWorkspace to current application's NSWorkspace's sharedWorkspace() set appPath to aWorkspace's fullPathForApplication:anAppName if appPath is equal to missing value then return false set locBookName to (current application's NSBundle's bundleWithPath:appPath)'s objectForInfoDictionaryKey:"CFBundleHelpBookName" if locBookName is equal to missing value then return false return locBookName end getHelpBook
var exec = require('child_process').exec var epath = require('electron-path'); var unpackedPath = epath.getUnpackedPath(); var cmd = unpackedPath+ '/execmd/launch.scpt'; var el = document.getElementById('launch'); el.addEventListener('click', function(){ exec('/usr/bin/osascript '+ cmd, {}, function(err, stdout, stderr) { //alert(err); }); });
完装した感想
さて、ElectronアプリにHelp Bookを組み込んだ感想ですが。
- BrowserWindowでヘルプを表示するのに比べて、HelpViewerの起動が遅い。
- 取り立てて、HelpViewer上のヘルプが読みやすいわけではない。
- Help Bookにしたら、ヘルプを書きやすくなるようなこともない。
- Macでしか動かない。
- 一応、他のプラットフォームに流用が効くようにヘルプを書けなくもない。
- 検索機能はついてる。
微妙かな!!
おわり。
Mac用ゆっくり系ボイス再生アプリMYukkuriVoiceでユーザー辞書機能に対応した
https://github.com/taku-o/myukkurivoice
MYukkuriVoiceは、Mac用のゆっくりボイスを再生したり、録音したりする機能のあるアプリです。
ユーザー辞書機能を作ろうとして、
同系統のアプリ、SofTalk、棒読みちゃんでは、自力で文章を字句解析して置き換えていたようなので、
自分もまた自力で作ろうとしていたが、
AquesTalkのライブラリで、Mac環境用の辞書作成機能が提供されたので、想定より楽に作れてしまった。
動画作成用のアプリなので、宣伝は動画で行うべきである(主張)。
https://www.nicovideo.jp/watch/sm34217643
https://www.nicovideo.jp/watch/sm34217643
続・WAVファイル。JavaScriptでwavファイルを分解する
前回、wavファイルのフォーマットを調べたので、
wavファイルをパースできるようになった。JavaScriptで。
http://d.hatena.ne.jp/taku-o/20181120/1542726865
import * as fs from 'fs'; const cTable = require('console.table'); fs.readFile('sample.wav', 'binary', (err, content) => { if (err) { console.error(err); return; } let offset = 0; let d = null; let data:any = null; const tables = []; let buffer = Buffer.from(content, 'binary'); // 1-4 Chunk ID "RIFF" d = buffer.readUIntBE(offset, 4); data = Buffer.from(d.toString(16), 'hex').toString(); offset += 4; tables.push({ position: '1-4', header: 'Chunk ID "RIFF"', data: data, }); // 5-8 Chunk Size d = buffer.readUIntLE(offset, 4); data = d; offset += 4; tables.push({ position: '5-8', header: 'Chunk Size', data: data, }); // 9-12 Format "WAVE" d = buffer.readUIntBE(offset, 4); data = Buffer.from(d.toString(16), 'hex').toString(); offset += 4; tables.push({ position: '9-12', header: 'Format "WAVE"', data: data, }); // 13-16 Subchunk1 ID "fmt " d = buffer.readUIntBE(offset, 4); data = Buffer.from(d.toString(16), 'hex').toString(); offset += 4; tables.push({ position: '13-16', header: 'Subchunk1 ID "fmt "', data: data, }); // 17-20 Subchunk1 Size "16" d = buffer.readUIntLE(offset, 4); data = d; offset += 4; tables.push({ position: '17-20', header: 'Subchunk1 Size', data: data, }); // 21-22 Audio Format "1" d = buffer.readUIntLE(offset, 2); data = d; offset += 2; tables.push({ position: '21-22', header: 'Audio Format "1" PCM', data: data, }); // 23-24 Num Channels d = buffer.readUIntLE(offset, 2); data = d; offset += 2; tables.push({ position: '23-24', header: 'Num Channels', data: data, }); // 25-28 Sample Rate d = buffer.readUIntLE(offset, 4); data = d; offset += 4; tables.push({ position: '25-28', header: 'Sample Rate', data: data, }); // 29-32 Byte Rate d = buffer.readUIntLE(offset, 4); data = d; offset += 4; tables.push({ position: '29-32', header: 'Byte Rate', data: data, }); // 33-34 Block Align d = buffer.readUIntLE(offset, 2); data = d; offset += 2; tables.push({ position: '33-34', header: 'Block Align', data: data, }); // 35-36 Bits Per Sample d = buffer.readUIntLE(offset, 2); data = d; offset += 2; tables.push({ position: '35-36', header: 'Bits Per Sample', data: data, }); // 37-40 Subchunk2 ID "data" d = buffer.readUIntBE(offset, 4); data = Buffer.from(d.toString(16), 'hex').toString(); offset += 4; tables.push({ position: '37-40', header: 'Subchunk2 ID "data"', data: data, }); // 41-44 Subchunk2 Size d = buffer.readUIntLE(offset, 4); data = d; offset += 4; tables.push({ position: '41-44', header: 'Subchunk2 Size', data: data, }); // 45- Subchunk2 data console.table(tables); });
- 結果
position header data -------- -------------------- ------ 1-4 Chunk ID "RIFF" RIFF 5-8 Chunk Size 131108 9-12 Format "WAVE" WAVE 13-16 Subchunk1 ID "fmt " fmt 17-20 Subchunk1 Size 16 21-22 Audio Format "1" PCM 1 23-24 Num Channels 1 25-28 Sample Rate 44100 29-32 Byte Rate 88200 33-34 Block Align 2 35-36 Bits Per Sample 16 37-40 Subchunk2 ID "data" data 41-44 Subchunk2 Size 131072
パースした感想
良い勉強の題材でありました。
WAVファイルのフォーマットの確認
アプリで出力したWAVファイルが正しくないのではないか、という問題に遭遇して、
その際、バイナリファイルといろいろな資料を読みながら、
WAVファイルがどのような仕様になっているか調べたので、その時の調査メモ。
用意したもの
バイナリエディタと電卓。
大きな数値の項目はリトルエンディアンで、文字の項目はASCII。
16進数を10進数で読む。
なので、用意する電卓は↑の変換が出来るものでないと読めないですのぅ。
バイナリファイルを読む
実際に作ったWAVファイルをバイナリエディタで読んでみつつ、
どの位置にどのような値が入っているか確認。
WAVEファイル その1
bytes | 定義 | 入っていた値 | 解読 | 説明、または検算 |
---|---|---|---|---|
1-4 | Chunk ID | 52 49 46 46 | RIFF | "RIFF"固定 |
5-8 | Chunk Size | FC 72 00 00 | 29436 | ファイルサイズ - 8bytes |
ファイルサイズからChunk ID Chunk Size除いた分か | ||||
29444 - 8 = 29436 | ||||
9-12 | Format | 57 41 56 45 | WAVE | "WAVE"固定 |
13-16 | Subchunk1 ID | 66 6D 74 | fmt | "fmt"固定 |
17-20 | Subchunk1 Size | 20 10 00 00 | 16 | "16"固定。Subchunk1の長さ(21-36)なので |
21-22 | Audio Format | 01 00 | 1 | "1"固定。(PCMなら) |
23-24 | Num Channels | 01 00 | 1 | チャネル数 |
25-28 | Sample Rate | 40 1F 00 00 | 8000 | サンプリングレート |
29-32 | Byte Rate | 80 3E 00 00 | 16000 | ブロックサイズ |
SampleRate * NumChannels * BitsPerSample/8 | ||||
(8000 * 1 * 16) / 8 = 16000 | ||||
33-34 | Block Align | 02 00 | 2 | 1サンプルあたりのビット数 |
NumChannels * BitsPerSample / 8 | ||||
(1 * 16) / 8 = 2 | ||||
35-36 | Bits Per Sample | 10 00 | 16 | 8 bits = 8, 16 bits = 16 |
37-40 | Subchunk2 ID | 64 61 74 61 | data | "data"固定 |
41-44 | Subchunk2 Size | D8 72 00 00 | 29400 | このチャンクのデータのサイズ |
ここからファイルの最後までが波形データだから | ||||
ファイルサイズ - ここまでのサイズ(44)になるはず | ||||
29444 - 44 = 29400 | ||||
45- | data | ここからファイルの最後まで波形データが入っている |
WAVEファイル その2
bytes | 定義 | 入っていた値 | 解読 | 説明、または検算 |
---|---|---|---|---|
1-4 | Chunk ID | 52 49 46 46 | RIFF | "RIFF"固定 |
5-8 | Chunk Size | 24 00 02 00 | 131108 | ファイルサイズ - 8bytes |
131116 - 8 = 131108 | ||||
9-12 | Format | 57 41 56 45 | WAVE | "WAVE"固定 |
13-16 | Subchunk1 ID | 66 6D 74 | fmt | "fmt"固定 |
17-20 | Subchunk1 Size | 20 10 00 00 | 16 | "16"固定。 |
21-22 | Audio Format | 01 00 | 1 | "1"固定。(PCMなら) |
23-24 | Num Channels | 01 00 | 1 | チャネル数 |
25-28 | Sample Rate | 44 AC 00 00 | 44100 | サンプリングレート |
29-32 | Byte Rate | 88 58 01 00 | 88200 | ブロックサイズ |
SampleRate * NumChannels * BitsPerSample/8 | ||||
(44100 * 1 * 16) / 8 = 88200 | ||||
33-34 | Block Align | 02 00 | 2 | 1サンプルあたりのビット数 |
NumChannels * BitsPerSample / 8 | ||||
(1 * 16) / 8 = 2 | ||||
35-36 | Bits Per Sample | 10 00 | 16 | 8 bits = 8, 16 bits = 16 |
37-40 | Subchunk2 ID | 64 61 74 61 | data | "data"固定 |
41-44 | Subchunk2 Size | 00 00 02 00 | 131072 | このチャンクのデータのサイズ |
131116 - 44 = 131072 | ||||
45- | data | ここから波形データ |
WAVEのバイナリ読んだ感想
読みやすいフォーマット。
アプリからPCMのWAVEファイルを出力すると仮定した時、
ファイルサイズの箇所以外は割と固定値になる。
RIFFの仕様
WAVEで使われているRIFF (Resource Interchange File Format)の仕様も見ておく。
なるほどなー。
bytes | 定義 | 説明、または検算 | |
---|---|---|---|
1-4 | Chunk ID | ChunkのID | |
5-8 | Chunk Size | Chunkのサイズ - IDとSizeフィールドを除いた値 | |
9- | data | チャンクのデータ。データのサイズが偶数で無いなら埋める |
もし、WAVEファイルにiXMLチャンクを差し込むなら
WAVEファイルにiXMLチャンクという仕様があって、
ずっと、どうやってデータを入れれば良いのかサンプルが見つからなくて困っていたんだけど、
RIFFの仕様をみたら、分かってきた。
同じように入れれば良いのだろう。(試してない)
bytes | 定義 | 入れる値 | 解読 | 説明、または検算 |
---|---|---|---|---|
1-4 | Chunk ID | 69 58 4D 4C | iXML | "iXML"固定 |
5-8 | Chunk Size | iXMLのデータサイズ - 8 | ||
9- | data | iXMLのデータ |
chunkのdataに入れるデータは次のようなものになるだろう。
role nameの所以外は固定になりそう?
<?xml version="1.0" encoding="UTF-8"?> <BWFXML> <IXML_VERSION>2.02</IXML_VERSION> <TRACK_LIST> <TRACK_COUNT>1</TRACK_COUNT> <TRACK> <CHANNEL_INDEX>1</CHANNEL_INDEX> <INTERLEAVE_INDEX>1</INTERLEAVE_INDEX> <NAME>role name</NAME> </TRACK> </TRACK_LIST> </BWFXML>
↓実際に入れるデータ
<?xml version="1.0" encoding="UTF-8"?><BWFXML><IXML_VERSION>2.02</IXML_VERSION><TRACK_LIST><TRACK_COUNT>1</TRACK_COUNT><TRACK><CHANNEL_INDEX>1</CHANNEL_INDEX><INTERLEAVE_INDEX>1</INTERLEAVE_INDEX><NAME>role name</NAME></TRACK></TRACK_LIST></BWFXML>
結論。ダメだった。BWFの仕様に合わせる必要があって、
他にも入れないとイケナイデータがある。
それと、Chunkの順番が決まっているようだ。
マルチなウィンドウのElectronアプリで、メニュー制御する話
Electronのアプリで、ウィンドウ毎に、メニューを切り替えたいとか、
有効無効を切り替えたいことってあると思うんです。
今回はフォーカスが当たっている時だけ、メニューを有効にする方法の話。
メニューアイテムのenabledを切り替える
メニューの有効無効を切り替えるには、
メニューのアイテムのenabled属性を切り替えれば良し。
メニューを作るときに、id属性を設定しておいて。
import {Menu} from 'electron'; const menuList = [ { ... (略) ... }, { label: '辞書', submenu: [ { id: 'dict-close', // ←←← idを設定 enabled: false, label: '辞書を閉じる (⌘W)', click() { if (myApp.dictWindow) { myApp.dictWindow.close(); } }, }, { id: 'dict-tutorial', // ←←← idを設定 enabled: false, label: '辞書チュートリアル', click() { if (myApp.dictWindow) { myApp.dictWindow.webContents.send('menu', 'tutorial'); } }, }, {type: 'separator'}, { id: 'dict-add', // ←←← idを設定 enabled: false, label: '定義データ追加 (⌘N)', click() { if (myApp.dictWindow) { myApp.dictWindow.webContents.send('menu', 'add'); } }, }, ] }, { ... (略) ... }, ]; const menuTemplate = Menu.buildFromTemplate(menuList); Menu.setApplicationMenu(menuTemplate);
そのidでメニューを取り出して、enabled属性を切り替える。
import {Menu} from 'electron'; const dictMenuItems = [ 'dict-close', 'dict-tutorial', 'dict-add', 'dict-delete', 'dict-save', 'dict-cancel', 'dict-export', 'dict-reset', ]; const menu = Menu.getApplicationMenu(); for (let m of dictMenuItems) { const item = menu.getMenuItemById(m); item.enabled = true; // ←←← enabledを切り替え }
フォーカスされたらメニューを有効に、フォーカスが外れたらメニューを無効に
BrowserWindowがフォーカスされている時だけメニューを有効にするには、
focus、blur、それとcloseのイベントを拾う。
closeも拾って置かないと、フォーカスされた状態でウィンドウ閉じられると、
ウィンドウ閉じてるのにメニューが有効なままになる。
this.dictWindow.on('close', () => { disableDictMenu(); // ←←← イベントを拾って、メニューのenabledを切り替える }); this.dictWindow.on('focus', () => { enableDictMenu(); // ←←← イベントを拾って、メニューのenabledを切り替える }); this.dictWindow.on('blur', () => { disableDictMenu(); // ←←← イベントを拾って、メニューのenabledを切り替える });
ウィンドウまわりのコードも含めると。
... (略) ... function showDictWindow(): void { const acceptFirstMouse = this.appCfg.acceptFirstMouse; this.dictWindow = new BrowserWindow({ width: width, height: height, show: false, // show at did-finish-load event }); this.dictWindow.loadURL(`file://${__dirname}/contents-dict.html`); // window event this.dictWindow.webContents.on('did-finish-load', () => { myApp.dictWindow.show(); myApp.dictWindow.focus(); }); this.dictWindow.on('close', () => { disableDictMenu(); }); this.dictWindow.on('focus', () => { enableDictMenu(); }); this.dictWindow.on('blur', () => { disableDictMenu(); }); }
メニューのacceleratorが邪魔な話
メニューアイテムにacceleratorを設定していると、
そのショートカットキーは、全てのウィンドウで効いてしまう。
import {Menu} from 'electron'; const menuList = [ { ... (略) ... }, { label: '辞書', submenu: [ { role: 'quit', accelerator: 'Command+Q', // ←←← acceleratorってこれ }, ] }, { ... (略) ... }, ]; const menuTemplate = Menu.buildFromTemplate(menuList); Menu.setApplicationMenu(menuTemplate);
これが邪魔なら、なんらかの対処をいれましょう。
acceleratorを無効にするか、
有効にしたい時以外はacceleratorを切る。
終わり
オワリダヨ