20220116のC#に関する記事は6件です。

【Unity】Unity側の値をCSVへ書き出す(Android実機も)

Unity側の値をCSVへ出力する方法についてです。 今回はUnity上での書き込みと、Android実機への書き込みも行います。 環境 Windows10 Unity Hub 2.4.5 Unity 2020.3.25f1 Android Studio 2020.3.1 Unity上での書き込み まずはじめにUnity上でCSVファイルの書き込みを行います。 GameObjectの準備 まずはGameObjectの準備を行います。 以下の3つ作成いたします。 Cube > F Cube > J Empty > SaveCsv 適当に配置してOKです。 Scriptの作成 以下のように作成します。 SampleSaveCsv using System.IO; using System.Text; using UnityEngine; // csvに保存するためのコード // SaveCsvへアタッチ public class SampleSaveCsv : MonoBehaviour { // System.IO private StreamWriter sw; // Start is called before the first frame update void Start() { // 新しくcsvファイルを作成して、{}の中の要素分csvに追記をする sw = new StreamWriter(@"SaveData.csv", false, Encoding.GetEncoding("Shift_JIS")); // CSV1行目のカラムで、StreamWriter オブジェクトへ書き込む string[] s1 = { "F", "J", "time" }; /** * s1の文字列配列のすべての要素を「,」で連結する * @see https://docs.microsoft.com/ja-jp/dotnet/api/system.string.join?view=net-6.0#System_String_Join_System_String_System_String___ */ string s2 = string.Join(",", s1); /** * s2文字列をcsvファイルへ書き込む * @see https://docs.microsoft.com/ja-jp/dotnet/api/system.io.streamwriter.writeline?view=net-6.0#System_IO_StreamWriter_WriteLine_System_String_ */ sw.WriteLine(s2); } // Update is called once per frame void Update() { // Enterキーが押されたらcsvへの書き込みを終了する if (Input.GetKeyDown(KeyCode.Return)) { /** * @see https://docs.microsoft.com/ja-jp/dotnet/api/system.io.streamwriter.close?view=net-6.0#System_IO_StreamWriter_Close */ sw.Close(); } } public void SaveData(string txt1, string txt2, string txt3) { string[] s1 = { txt1, txt2, txt3 }; string s2 = string.Join(",", s1); sw.WriteLine(s2); } } ちなみに「StreamWriterクラス」は、C#のクラスです。 公式ドキュメント new StreamWriter の第二引数にboolean値を渡していますが、 これは「true」だと同階層に同じファイル名があると既存のファイルへ新しい値だけを上書きします。 「false」だと値は全て新しくファイル自体を上書きします。 SampleF using System.IO; using UnityEngine; // Fキーを検出するコード // Fへアタッチ public class SampleF : MonoBehaviour { private float time; GameObject saveCsv; SampleSaveCsv sampleSaveCsv; // Start is called before the first frame update void Start() { /** * SaveCsvのスクリプトを参照する * @see https://docs.unity3d.com/ja/current/ScriptReference/GameObject.Find.html */ saveCsv = GameObject.Find("SaveCsv"); /** * コンポーネントを返す * @see https://docs.unity3d.com/ja/current/ScriptReference/GameObject.GetComponent.html */ sampleSaveCsv = saveCsv.GetComponent<SampleSaveCsv>(); } // Update is called once per frame void Update() { // Fキーが押されたら、csvに「F」と「いつ押されたか」という情報が書き加えられる time += Time.deltaTime; if (Input.GetKeyDown(KeyCode.F)) { sampleSaveCsv.SaveData("F", " ", time.ToString()); } } } SampleJ using System.Collections; using System.Collections.Generic; using System.IO; using UnityEngine; // Jキーを検出するコード // Jへアタッチ public class SampleJ : MonoBehaviour { private float time; GameObject saveCsv; SampleSaveCsv sampleSaveCsv; // Start is called before the first frame update void Start() { /** * SaveCsvのスクリプトを参照する * @see https://docs.unity3d.com/ja/current/ScriptReference/GameObject.Find.html */ saveCsv = GameObject.Find("SaveCsv"); /** * コンポーネントを返す * @see https://docs.unity3d.com/ja/current/ScriptReference/GameObject.GetComponent.html */ sampleSaveCsv = saveCsv.GetComponent<SampleSaveCsv>(); } // Update is called once per frame void Update() { // Jキーが押されたら、csvに「J」と「いつ押されたか」という情報が書き加えられる time += Time.deltaTime; if (Input.GetKeyDown(KeyCode.J)) { sampleSaveCsv.SaveData(" ", "J", time.ToString()); } } } それぞれへアタッチ 先程、作成した3つのScriptを以下のようにアタッチします。 SampleF.cs > F SampleJ.cs > J SampleSaveCsv.cs > SaveCsv PC上で動作確認 では、「Play」ボタンを押してキーボードの「FかJ」をタイピングしてみて下さい。 最後に「Enter」キーを押します。 プロジェクトフォルダ直下に「SaveData.csv」というファイルが作成され、以下のように1行目がタイトル、2行目からはどのボタンをいつ押したかが入力されています。 Android実機への書き出し 続いてはAndroidへの書き出しを行います。 今のままのコードでは書き出しはされません。 Android用にコードを記述する必要があります。 端末ごとに処理を分ける方法 詳細は以前書いたこちらの記事をご参考下さい。 SampleSaveCsvの修正 Start内を以下のように修正します。 void Start() { #if UNITY_ANDROID // 新しくcsvファイルを作成して、{}の中の要素分csvに追記をする(Androidの処理) string FilePath = @"/SaveData.csv"; sw = new StreamWriter(Application.persistentDataPath + FilePath, false, Encoding.GetEncoding("utf-8")); #endif #if UNITY_EDITOR // 新しくcsvファイルを作成して、{}の中の要素分csvに追記をする(Unity上での処理) sw = new StreamWriter(@"SaveData.csv", false, Encoding.GetEncoding("Shift_JIS")); #endif // CSV1行目のカラムで、StreamWriter オブジェクトへ書き込む string[] s1 = { "F", "J", "time" }; /** * s1の文字列配列のすべての要素を「,」で連結する * @see https://docs.microsoft.com/ja-jp/dotnet/api/system.string.join?view=net-6.0#System_String_Join_System_String_System_String___ */ string s2 = string.Join(",", s1); /** * s2文字列をcsvファイルへ書き込む * @see https://docs.microsoft.com/ja-jp/dotnet/api/system.io.streamwriter.writeline?view=net-6.0#System_IO_StreamWriter_WriteLine_System_String_ */ sw.WriteLine(s2); } 解説をすると、Application.persistentDataPath は機種依存のファイルパスで実機へ正しく書き込むために必要になります。 GameObjectの追加 以下のように適当にButtonを配置します。 AddButtonのScriptを作成 今回はButtonをクリックしたタイミングでデータを追加したいと思います。 AddButton using UnityEngine; public class AddButton : MonoBehaviour { private float time; GameObject saveCsv; SampleSaveCsv sampleSaveCsv; // Start is called before the first frame update void Start() { saveCsv = GameObject.Find("SaveCsv"); sampleSaveCsv = saveCsv.GetComponent<SampleSaveCsv>(); } public void onClick() { time += Time.deltaTime; // テキストデータも適当です sampleSaveCsv.SaveData("B", " ", time.ToString()); } } AddButtonへアタッチ では、AddButtonへアタッチします。 AddButtonオブジェクトへAddButton.csをアタッチ Inspectore > Button の「On Click()」へAddButton.csのイベントを設定(以下図参照) SDカードへ書き込みをする場合は設定が必要 もしSDカードへ書き込みを行う場合、Unity側で少し設定が必要です。 Edit > Project setting > Player > Other Settings の下の方に 「Write Permisson」という項目があります。 こちらの項目を「External(SDCard)」へ変更します。 Android実機で動作確認 では、実機とPCを繋いで「Build And Run」をしてみましょう。 buildされたら、「Add Button」を何度かタップしてみます。 データの確認 データの確認ですが、保存先が端末によって異なります。 以下は一例になります。 「内部ストレージ/メモリーカード」 > 「Android」 > 「data」 このフォルダまで来るとインストールされているパッケージが沢山あると思います。 自分で作ったパッケージネームを探します。 パッケージネームはUnity側の、 Edit > Project Settings > Player > Other Settings の 「Package Name」から確認できます。 次のフォルダで「cache」と「files」があると思いますので、 「files」を選択します。 すると、「SaveData.csv」が保存されています。 これでAndroid実機へのcsv書き込みもできました。 まとめ ファイルの書き込みには StreamWriter を使う #if で端末の判定を行い実機によるpathを使い分ける Androidの保存先pathは Application.persistentDataPath を使う 以上ご拝読有難うございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity プロジェクト6 「フィッシングゲーム」 INDEX

削除予定
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

削除予定

削除予定
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

byte vs int (C#)

経緯 for文のindex変数、どうせ0~255しか使わないし、byte型にした方が効率的ではと思い調べてみました。 注意 C#上での話です。他の言語では多少異なる点があるかもしれません。 結論 int型で良いです。byteはかえって非効率になりそうです。 まとめ int型の方が良い理由は下記の通りです。 ・配列のindexはint型で管理しているので毎回Castする事になります。 ・C#は整数の計算系をint型に揃えているので、そこでもCastが入る可能性があります。 ・そもそもまとめて宣言しない限り0埋めが発生するので、for分のindex程度では意味がないです。 ・byteかintかとか、メモリ管理はコンパイラがいい感じにしてくれるので、逆にコンパイラの邪魔になるとかいう話もあるそうです。 ・そもそも最近のCPUの処理速度はとても速いので、その程度の最適化では大した差は生まれないです。もし動作が重い事を理由に最適化するのなら、intかbyteかよりも重大な問題が他にあるはずで、そこを修正した方が何倍も効率的です。 補足 「じゃあC#のByte型いらないじゃん!」とならないように、一応補足しておくと、byte型は通信の時に文字数を節約する為に使えるので現在でも不要ではないです。 つまり処理速度を犠牲に通信量を減らすという事ですね。 杞憂でしたが、私にとっては興味深い調査でしたので満足しています。 以上です。精進します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

音楽に関連する計算式まとめ

はじめに 音楽では、音階、音の長さ、音高など、様々なものが計算可能である. この記事は、それら計算可能なものについての計算式を考え、まとめるものである. なお、記事中に例示するコードは C# による. また、記事中ではシャープとして #、フラットとして b、ダブルシャープとして x などを用いる. 全体的に、私の勘違いやタイプミスなどによる誤りがあり得る点をご留意の上お読みください. 記事中に例示するコードにおいて登場する自作関数などの説明 FloorMod(int x, int y) x を y で割った剰余を (-abs(y) .. abs(y)) の範囲で返す関数. ただし、剰余は 0、または y と同じ符号であるものとする. (e.g. FloorMod(-20, 7) = 1) cf. C# において、x % 7 は、0、または x と同じ符号で剰余を返す. (e.g. -20 % 7 = -6) 定義通りの関数 public static int FloorMod(int x, int y) { int q = (int) System.Math.Floor((double) x / y); return x - q * y; } FloorDiv(int x, int y) System.Math.Floor((double) x / y) と同等の計算を行う関数. RoundMod(int x, int y) x を y で割った剰余を (-abs(y/2) .. abs(y/2)]、または [-abs(y/2) .. abs(y/2)) の範囲で、つまり絶対最小剰余を返す関数. (e.g. RoundMod(-20, 7) = 1) cf. C# において、x % 7 は、0、または x と同じ符号で剰余を返す. (e.g. -20 % 7 = -6) 定義通りの関数 public static int RoundMod(int x, int y) { // System.Math.Round(double, MidpointRounding.AwayFromZero) はいわゆる四捨五入 int q = (int) System.Math.Round((double) x / y, MidpointRounding.AwayFromZero); // x > 0 のとき、返り値の範囲は [-abs(y/2) .. abs(y/2)) // さもなくば、(-abs(y/2) .. abs(y/2)] return x - q * y; } Rational 分数を表す構造体. 約分された形で保持する. 四則演算子、比較演算子などは一通り実装されているものとする. コンストラクタは new Rational(int num, int den = 1) であり、 num は分子、den は分母を意味する. (numerator, denominator) 音階(音名) 音高や調号の記述にはよく音名が必要とされるため、音階(音名)と整数とのマッピングを考える. 幹音名 # や b のついていない音を幹音と呼ぶ. この記事では、C を 0、D を 1、... B を 6 として定義する. 幹音名(英語) 幹音名(ドレミ) 値 C ド 0 D レ 1 E ミ 2 F ファ 3 G ソ 4 A ラ 5 B シ 6 コードによる定義の例 enum NaturalTone { C = 0 , D , E , F , G , A , B } 変化記号 # や b など、音高を変化させる記号を指して変化記号と呼ぶ. ここでは「triple flat」や「quadruple sharp」など、基本的に使いどころはないが定義可能な記号などもあるため、 変化記号が「半音いくつ分変化させるか」によって数値にマッピングする. 記号 英名 値 bbb triple flat -3 bb double flat -2 b flat -1 ♮ natural 0 # sharp 1 x double sharp 2 #x triple sharp 3 コードによる定義の例 int alter; 五度圏 すべての音名は、幹音名と変化記号の組によって表現可能であるが、 ひとつの数字によって任意の音名を表現するために、五度圏を用いることとする. C = 0 とし、完全五度上は +1、完全五度下は -1 であるものとする. 例: 音名 値 * 音名 値 * 音名 値 Bbb -9 Bb -2 B 5 Fb -8 F -1 F# 6 Cb -7 C 0 C# 7 Gb -6 G 1 G# 8 Db -5 D 2 D# 9 Ab -4 A 3 Eb -3 E 4 (幹音名、変化記号) <=> 五度圏 の変換 コードによる定義の例 public class Tone { public NaturalTone NaturalTone { get; } public int Alter { get; } public int Fifths { get; } public Tone(NaturalTone naturalTone, int alter) { this.NaturalTone = naturalTone; this.Alter = alter; this.Fifths = ToFifths(naturalTone, alter); } public Tone(int fifths) { var pair = ToPair(fifths); this.NaturalTone = pair.naturalTone; this.Alter = pair.alter; this.Fifths = fifths; } // (幹音名、変化記号) => 五度圏 private static int ToFifths(NaturalTone naturalTone, int alter) { // naturalTone を [-1 .. 6) の範囲にマッピングする. (幹音は五度圏で F=-1, ..., B=5 の範囲であるため) int x = FloorMod(((int) naturalTone) * 2 + 1, 7) - 1; int y = alter * 7; return x + y; } // 五度圏 => (幹音名、変化記号) private static (NaturalTone naturalTone, int alter) ToPair(int fifths) { int naturalTone = FloorMod(fifths * 4, 7); int alter = FloorDiv(fifths + 1, 7); return ((NaturalTone) naturalTone, alter); } } 調号 一般的な西洋音楽では12平均律で利用可能な12音の内、特定の7音を選び、それらをメインとして作曲する. このような特定の音の集合を調(Scale)と呼び、その調ではどの音にどの変化記号が付くかを示すものが調号(Key signature)である. 調は、どの音が主役であるか(主音、Tonic)と、それ以外の音が主音とどのような関係で選ばれるか(調性、Tonality)の組によって表される. 一般に、調性は長調(7音すべてが白鍵のとき、ドを主音とする全音階)、(自然-)短調(7音すべてが白鍵のとき、ラを主音とする全音階)の2種類のみが使われる. しかしここでは、あえてドとラだけを特別扱いする理由もないためモード(日本語では旋法?)によって調性を表現する. モード(旋法) ここで扱うモードは以下の7種類: 名前 7音すべてが白鍵のときの主音 補足 Ionian ド いわゆる長調 Dorian レ Phrygian ミ Lydian ファ Mixolydian ソ Aeolian ラ いわゆる短調 Locrian シ コード例 コードによる定義の例 // 後の計算のため、 // (int) Mode.X == (int) NaturalTone.<7音すべてが白鍵のときの主音> // であるように定義する. public enum Mode { Ionian = 0 , Dorian , Phrygian , Lydian , Mixolydian , Aeolian , Locrian // 必要があれば、次を定義する: // , Major = 0// 長調 // , Minor = 5// 短調 } public class KeySignature { public Tone Tonic { get; } public Mode Mode { get; } // 特定の音についている変化記号を辞書によって管理する. private readonly Dictionary<NaturalTone, int> _alters = new(); public int GetAlter(NaturalTone naturalTone) => _alters[naturalTone]; public KeySignature(Tone tonic, Mode mode) { // ダブルシャープ/ダブルフラットなどが現れる場合は例外を送出するものとする. // 嬰ホ長調(ミ# ファx ソx ラ# シ# ドx レx) や三重変ト短調のような、 // 変な調号も許す場合(あるのか?)このチェック処理は不要 // 次は // int modeFifths = Tone.ToFifths((NaturalTone) mode, 0); // に等しい. 上で ToFiths を private にしているためこうなっている. // 五度圏は使い勝手がよいため、どこかにユーティリティとして定義すべきか. int modeFifths = FloorMod(((int) mode) * 2 + 1, 7) - 1; if(tonic.Fifths < modeFifths - 7 || modeFifths + 7 < tonic.Fifths) { throw new ArgumentException(); } this.Tonic = tonic; this.Mode = mode; int s = tonic.Fifths - modeFifths - 1; for(int i = 0; i < 7; ++i) { int fifths = i + s; Tone newTone = new Tone(fifths); _alters[newTone.NaturalTone] = newTone.Alter; } } // 同主調(英語では parallel key と言う. 後述の平行調は英語で relative key のため、混同しないように) // 名前の通り、主音が同じで、調性が異なる調のこと. public KeySignature ParallelKey(Mode newMode) => new KeySignature(this.Tonic, newMode); // 平行調(前述の parallel key と混同しないように) // 構成音が同じで、主音/調性が異なる調のこと. ks._alters と ks.RelativeKey(Mode.*)._alters は等しくなる. public KeySignature RelativeKey(Mode newMode) { // 煩雑になるので、Tone.ToFifths が public であるものとして書いている. int oldModeFifths = Tone.ToFifths((NaturalTone) this.Mode, 0); int newModeFifths = Tone.ToFifths((NaturalTone) newMode, 0); int newFifths = this.Tonic.Fifths - oldModeFifths + newModeFifths; return new KeySignature(new Tone(newFifths), newMode); } // 属調. 属音を主音とする調. 属音=完全五度上の音. // 調号としてはシャープがひとつ増える(=フラットがひとつ減る). public KeySignature DominantKey => new KeySignature(new Tone(this.Tonic.Fifths + 1), this.Mode); // 下属調. 下属音を主音とする調. 下属音=完全五度下の音. // 調号としてはフラットがひとつ増える(=シャープがひとつ減る). public KeySignature SubdominantKey => new KeySignature(new Tone(this.Tonic.Fifths - 1), this.Mode); } 音高 一般に西洋音楽では、音の高さは音名とオクターブの組で表現される. MIDIでは異名同音の別を無視したノートナンバーが用いられる. また、単純に「音の高さ」の指標としては周波数(Hz) が使われる. この節ではこれらの(相互)変換を目指す. なお、オクターブの表現には国際式、ヤマハ式があるが、この記事中では特に注釈がない限り、国際式に従うものとする. (<国際式オクターブ> = <ヤマハ式オクターブ> + 1) (音名, オクターブ) 章題の通り、ただの組である. 国際式の表現に従う場合、ピアノの「真ん中のド」は (C, 4) となる. ノートナンバー 主に MIDI で使用される表記法であり、ピアノの「真ん中のド」は 60 として定義されている. 周波数(12平均律) 一秒あたりの振動数を表す単位を Hz(ヘルツ) と言う. 12平均律は、現代日本でおそらく最も広く使われている音律である. これは丁度オクターブ上の周波数を <元の周波数> * 2 として定義し、1オクターブを対数表記で12等分するものである. 各音高表現間の変換 コードによる定義の例 public class Pitch { public Tone Tone { get; } public int Octave { get; } public int NoteNumber { get; } public double Frequency { get; } public Pitch(Tone tone, int octave) { this.Tone = tone; this.Octave = octave; // diffFromC は、幹音が同じオクターブ内の Cナチュラル から半音いくつ分離れているかを示す. int diffFromC = tone.NaturalTone <= NaturalTone.E ? ((int) tone.NaturalTone) * 2 : ((int) tone.NaturalTone) * 2 + 1; this.NoteNumber = diffFromC + (octave + 1) * 12; // 条件分けが嫌いな場合、次でも計算できる. (変化記号を無視した幹音の五度圏を7倍して12で割った余り = 半音数) // int diffFromC = FloorMod((FloorMod(((int) tone.NaturalTone) * 2 + 1, 7) - 1) * 7, 12); // あるいは、計算を行わずに switch を使用してもよい // int diffFromC = tone.NaturalTone switch { // NaturalTone.C => 0, // NaturalTone.D => 2, // NaturalTone.E => 4, // NaturalTone.F => 5, // NaturalTone.G => 7, // NaturalTone.A => 9, // NaturalTone.B => 11, // _ => throw new ArgumentException("tone.NaturalTone") // } this.Frequency = ToFrequency(NoteNumber); } public Pitch(int noteNumber) { // noteNumber が負の場合も計算できるようにするため FloorMod/FloorDiv を使っているが、 // 範囲が 0 以上であることを前提とする場合は標準の演算子 (/, %) を使ってよい. // diffFromC は、同じオクターブ内の Cナチュラル から半音いくつ分離れているかを示す. int diffFromC = FloorMod(noteNumber, 12); // 五度圏としての音名を [-6 .. 6) の範囲で得る. int fifths = RoundMod(diffFromC * 7, 12); this.Tone = new Tone(fifths); this.Octave = FloorDiv(noteNumber, 12) - 1; this.NoteNumber = noteNumber; this.Frequency = ToFrequency(noteNumber); // Tone については、単純に switch 文で書くこともできる. バグを嫌うならばこちらが確実. // noteNumber では異名同音が区別されないため、Db に代えて C# が良い、とか Gb より F# が使いたい、 // などの場合も変更が簡明. // this.Tone = diffFromC switch { // 0 => new Tone(NaturalTone.C, 0),// C // 1 => new Tone(NaturalTone.D, -1),// Db // 2 => new Tone(NaturalTone.D, 0),// D // 3 => new Tone(NaturalTone.E, -1),// Eb // 4 => new Tone(NaturalTone.E, 0),// E // 5 => new Tone(NaturalTone.F, 0),// F // 6 => new Tone(NaturalTone.G, -1),// Gb // 7 => new Tone(NaturalTone.G, 0),// G // 8 => new Tone(NaturalTone.A, -1),// Ab // 9 => new Tone(NaturalTone.A, 0),// A // 10 => new Tone(NaturalTone.B, -1),// Bb // 11 => new Tone(NaturalTone.B, 0),// B // } } private static double ToFrequency(int noteNumber, int standardPitch = 69 /* =A4 */, double standardFrequency = 440.0) { // ノートナンバーが standardPitch の音 を standardFrequency[Hz] とした場合の周波数 // この例ではこの関数をコンストラクタでしか使わないためデフォルト引数である必要がないが、 // より一般的な関数として定義する場合のためこうしている. A4=442Hz とするピッチで考えたいときとか. // 底を 2 としているのは、丁度オクターブ上の音の周波数が「2」倍であると定めているから. // 指数を <ノートナンバーの差> / 12 としているのは、「12」平均律で考えているから. return standardFrequency * System.Math.Pow(2.0, (double) (noteNumber - standardPitch) / 12.0); } public static (int noteNumber, double cents) FromFrequency(double frequency, int standardPitch = 69 /* =A4 */, double standardFrequency = 440.0) { // 与えられた周波数がぴったりノートナンバーにマッピングできることはまずない. // そのため、ノートナンバーとセントの組に変換する. // 1 セント := 半音の100分の1 // semitones := 目的の音が、standardPitch から半音いくつ分離れているか double semitones = 12 * System.Math.Log2(frequency / standardFrequency); // semitones を近いほうの整数に丸める. 常に正のセントが欲しい場合(あるのか?)はここで Floor を使えばよい. // ここで使用している Round(double) は銀行家丸めであるため、 // もし四捨五入した値が欲しい場合は Round(double, MidpointRounding.AwayFromZero) を使うこと. int intSemitones = System.Math.Round(semitones); double cents = (semitones - intSemitones) * 100; return (standardPitch + intSemitones, cents); } } 音程 音と音との高さの差を音程という. 音程は基本的に距離であるから、負の音程というのはおかしいように感じるが、 減1度は定義不能で減8度は定義可能、のような場合分けは煩雑である(特に単音程と複音程を行き来するような場合)から、 <音高> + <音程> = <より低い音高> となるような音程の存在を認める形で考える. 音程の定義 音度表現 音楽において、同じオクターブ上のドとドは1度、ドとレは2度、...、のように度数を数えていく. 同じ音高同士が 0 ではなく 1 なので、植木算的な計算が求められる. また、度数より細かい差は補助的な接頭辞によって表現される. 同じオクターブ上のドとファbbは重減4度(doubly diminished fourth) 同じオクターブ上のドとファbは減4度(diminished fourth) 同じオクターブ上のドとファは完全4度(perfect fourth) 同じオクターブ上のドとファ#は増4度(augmented fourth) 同じオクターブ上のドとファxは重増4度(doubly augmented fourth) 同じオクターブ上のドとファ#x(ファのトリプルシャープ)は3重増4度(triply augmented fourth、重々増4度とも) などと表現される. 重~でさえまれなので、余程変な計算をしない限り3重~などを見かけることはないだろうけれど、一応存在する体で書く. ややこしいことに、完全n度と呼ばれる音程を持つがある n と、完全とは決してつながらずに長n度/短n度を持つ n とがある. コード例 コードによる定義の例 // 「補助的な接頭辞」. 日本語でこれを何と呼ぶのが標準的なのかは不明. public enum Quality { Diminished // 減 , Minor // 長 , Perfect // 完全 , Major // 短 , Augmented// 増 } public class Interval { // 増/減 の数. augmented fourth なら 1、triply diminished fourth なら 3、minor sixth なら 0 public int Multiplicity { get; } // 「補助的な接頭辞」 public Quality Quality { get; } // 度数. 1以上の整数であるから、uint にしてもよいかもしれない. public int Degrees { get; } // 半音数. この記事においては、重減1度なら -1 など、負の数も許容する. public int Semitones { get; } public Interval(Multiplicity multiplicity, Quality quality, int degrees) { // 渡された音程を単音程(オクターブ、オクターブ未満の度数)に変換する int octave = FloorDiv(degrees - 1, 7); int simpleDegrees = FloorMod(degrees - 1, 7) + 1; // 完全n度を持つ n であるか? bool canBePerfect = simpleDegrees == 1 || simpleDegrees == 4 || simpleDegrees == 5; // 接頭辞によって、完全/長n度から半音いくつ分動くか int alter = quality switch { Quality.Diminished => -multiplicity - (canBePerfect? 0 : 1), Quality.Minor => -1, Quality.Perfect => 0, Quality.Major => 0, Quality.Augmented => multiplicity, _ => throw new ArgumentException(nameof(quality)), }; // 完全/長n度が半音いくつ分か int semitones = simpleDegrees switch { 1 => 0, 2 => 2, 3 => 4, 4 => 5, 5 => 7, 6 => 9, 7 => 11, _ => throw new NotImplementedException(), }; this.Multiplicity = multiplicity; this.Quality = quality; this.Degrees = degrees; this.Semitones = semitones + alter + octave * 12; // semitones は次でも求められる: // int semitones = FloorMod((FloorMod((simpleDegrees - 1) * 2 + 1, 7) - 1) * 7, 12); } public Interval(int degrees, int semitones) { // 渡された音程を単音程(オクターブ、オクターブ未満の度数)に変換する int octave = FloorDiv(degrees - 1, 7); int simpleDegrees = FloorMod(degrees - 1, 7) + 1; // 完全n度を持つ n であるか? bool canBePerfect = simpleDegrees == 1 || simpleDegrees == 4 || simpleDegrees == 5; // 完全/長n度が半音いくつ分か int semitonesPerfectOrMajor = simpleDegrees switch { 1 => 0, 2 => 2, 3 => 4, 4 => 5, 5 => 7, 6 => 9, 7 => 11, _ => throw new NotImplementedException(), }; int alter = semitones - (semitonesPerfectOrMajor + octave * 12); this.Degrees = degrees; this.Semitones = semitones; if(alter == 0) { this.Multiplicity = 0; this.Quality = canBePerfect? Quality.Perfect : Quality.Major; } else if(alter > 0) { this.Multiplicity = alter; this.Quality = Quality.Augmented; } else if(canBePerfect) { this.Multiplicity = -alter; this.Quality = Quality.Diminished; } else if(alter == -1) { this.Multiplicity = 0; this.Quality = Quality.Minor; } else { this.Multiplicity = -alter-1; this.Quality = Quality.Diminished; } // semitonesPerfectOrMajor は次でも求められる: // int semitonesPerfectOrMajor = FloorMod((FloorMod((simpleDegrees - 1) * 2 + 1, 7) - 1) * 7, 12); } // 音程の和 public static Interval operator+(Interval lhs, Interval rhs) { int degrees = lhs.Degrees + rhs.Degrees - 1; int semitones = lhs.Semitones + rhs.Semitones; return new Interval(degrees, semitones); } // 音高と音程の和 public static Pitch operator+(Pitch lhs, Interval rhs) { int newNoteNumber = lhs.NoteNumber + rhs.Semitones; int naturalTone = ((int) lhs.Tone.NaturalTone) + rhs.Degrees - 1; NaturalTone newNaturalTone = (NaturalTone) (naturalTone % 7); int newOctave = lhs.Octave + (naturalTone / 7); int diffFromC = newNaturalTone <= NaturalTone.E ? ((int) newNaturalTone) * 2 : ((int) newNaturalTone) * 2 + 1; int noteNumberAsNatural = diffFromC + newOctave * 12; int alter = newNoteNumber - noteNumberAsNatural; return new Pitch(new Tone(newNaturalTone, alter), newOctave); // diffFromC は次でも求められる: // int naturalToneFifths = floorMod(((int) newNaturalTone) * 2 + 1, 7) - 1; // int diffFromC = FloorMod(naturalToneFifths * 7, 12); } } 音の長さ(音価) 音楽において、音価は音符(休符)の種類、付点の数、連符の組によって表現され、 BPMによって秒などの時間単位と変換される. 楽譜的な音価表現 音符(休符)の種類 全音符の長さを 1 とし、二分音符は 1/2、四分音符は 1/4、... として分数で表現できる. この記事では、音符の種類の表現に整数 n を使い、分数としての長さは 2^(-n) であるものとして定義する. コードによる定義の例 public enum NoteValueType { Maxima = -3,// 日本語名称も用例も不詳だが、英語版 Wikipedia の Note value のページに記述があるためここから始める Longa, Breve, Semibreve, // 全音符 Minim, // 二分音符 Crotchet, // 四分音符 Quaver, // 八分音符 Semiquaver, Demisemiquaver, Hemidemisemiquaver, } 付点 音符(休符)につけられる点は、直前の音価の 1/2 を持つものとされる. そのため、 <四分音符> <付点> とある場合、全体としての音価は <四分音符=1/4> <付点=直前の音価の 1/2> => <四分音符=1/4> <付点=直前の音価の 1/2=1/8> => 1/4 + 1/8 => 3/8 となる. 注意が必要なのは点が複数になった場合で、 <四分音符> <付点> <付点> とある場合、全体としての音価は <四分音符=1/4> <ひとつ目の付点=直前の音価の 1/2> <ふたつ目の付点=直前の音価の 1/2> だが、ふたつ目の付点にとって「直前の音価」とは「ひとつ目の付点」のみを指す. したがって、この場合全体としての音価は 3/8 + (3/8 * 1/2) = 9/16 とはならず、 <四分音符=1/4> <ひとつ目の付点=直前の音価の 1/2=1/8> <ふたつ目の付点=直前の音価の 1/2=1/16> => 1/4 + 1/8 + 1/16 => (4 + 2 + 1) / 16 => 7/16 となる. 連符 通常、単に3連符や5連符などひとつの数字のみで呼ばれる. しかし、まれに八分音符の2連符で付点八分音符ふたつ分の音価とするような記法があるらしく、数字ひとつで連符表現とするのは考察が面倒なため、 3連符を 3:2 連符、5連符を 5:4 連符などとする記法にならい、比率で表現することとする. つまり、<全体を何分割するか>:<基準となる音いくつ分の音価か> の形で保持する. コードによる定義の例 public class Tuplet { // 全体を何分割するか public int Actual { get; } // 基準となる音いくつ分の音価か public int Normal { get; } public static Tuplet CreateStandardTuplet(int n) { // 一般的な n連符を比の形にする // Actual が n のとき、Normal としては Pow(2, Floor(Log2(n))) が使われる. int actual = n; int normal = 1 << System.Numerics.BitOperations.Log2(unchecked((uint) n)); return new Tuplet(actual, normal); // 例: // 3連符 -> 3: 2 // 5連符 -> 5: 4 // 7連符 -> 7: 4 // 9連符 -> 9: 8 // 11連符 -> 11: 8 // 13連符 -> 13: 8 // 15連符 -> 15: 8 // 17連符 -> 17:16 // ここに書いた定義では 2連符 -> 2:2 となるが、 // 2:3 になるようにすべきかは不明. // もしそうしたい場合は以下のような形か? // if(actual == normal) return new Tuplet(actual, normal + (normal >> 1) /* または (normal / 2 * 3) */); // else return new Tuplet(actual, normal); } // コンストラクタ略 // 連符を使わない場合は new Tuplet(1, 1) を使うものとする. } コード例 コードによる定義の例 public class NoteValue { public NoteValueType Type { get; } public int Dots { get; } public Tuplet Tuplet { get; } // 分数としての音の長さ public Rational Length { get; } public NoteValue(NoteValueType type, int dots, Tuplet tuplet) { int typeAsInt = (int) type; Rational baseLength = typeAsInt > 0? new Rational(1 << typeAsInt) : new Rational(1, 1 << -typeAsInt); // 付点の数から、元の音価の何倍の長さにすればよいかを求める. int den = 1 << dots; int num = (den << 1) - 1; Rational ratioByDots = new Rational(num, den); // 連符の比から、元の音価の何倍の長さにすればよいかを求める. Rational ratioByTuplet = new Rational(tuplet.Actual, tuplet.Normal); this.Type = type; this.Dots = dots; this.Tuplet = tuplet; this.Length = baseLength * ratioByDots * ratioByTuplet; // ratioByDots の求め方について: // 求めるべきは Σ[k in [0 .. dots]] 1/2^k であるから、等比数列の和として // (2^(k+1)-1) / 2^k で求められる(上記のコード例) // 愚直に計算する場合、定義そのまま以下のようにできる // Rational ratioByDots = new Rational(0); // for(int k = 0; k <= dots; ++k) { // ratioByDots += new Rational(1, 1 << k); // } } } BPM(M.M.) 一分間当たりの拍数を BPM と呼ぶ. 通常この「拍」は四分音符ひとつ分の長さを指すが、 楽譜上 <基準となる音価>=<一分間当たりの拍数> と記述されることもあるため、拍が四分音符でないことも考慮して扱う. コードによる定義の例 public class MM /* Maelzel's Metronome */ { public NoteValue Beat { get; } // 基準が四分音符でないときも BPM と呼ぶかどうかは不明だが、とりあえず BPM を採用する public double BPM { get; } // Microseconds Per Quater note, 四分音符ひとつあたりの時間[us] public double MPQ { get; } public MM(NoteValue beat, double bpm) { this.Beat = beat; this.Bpm = bpm; // 指定された拍、bpm から、拍=四分音符としたときの bpm を求める. double quaterNotesPerMinute = bpm * (beat.Length * 4); // 四分音符ひとつあたりの時間[us] を求める. // 1分 = 60秒 = 60,000ミリ秒 = 60,000,000マイクロ秒 this.MPQ = 60_000_000 / quaterNotesPerMinute; } public MM(double mpq) { // 「1拍」=四分音符で固定して計算する this.Beat = new NoteValue(NoteValueType.Crotchet, 0, new Tuplet(1, 1)); // 1分 = 60秒 = 60,000ミリ秒 = 60,000,000マイクロ秒 this.Bpm = 60_000_000 / mpq; this.MPQ = mpq; } } (音価、BPM) => 秒数(マイクロ秒) コードによる定義の例 public static double ToMicroSeconds(NoteValue noteValue, MM mm) { // 拍当たりの音価(mm.Beat) で noteValue.Length を割り、MPQ をかける return ((double) (noteValue.Length / mm.Beat)) * mm.MPQ; }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

webview.hostObjects が async で失敗するのをなんとかする ver.2

message を汚染せず匿名関数でやりくりするタイプ JavaScript addEventListener('ExeLoaded',e=>{ var url = `https://script.google.com/macros/s/SampleDeployId/exec`; var text = `SampleText: Fixed newly exposed boat not showing up in the boat yard menu Changed contraptions to drop resources and inventory objects (crates) to drop resources even when killed with fire. Previously anything killed by fire would drop nothing.` chrome.webview.hostObjects.exe.PostError(url,text).then(r=>{ console.log(r)// null、async失敗 }) _exe.post(url,text).then(r=>{ console.log(r)// async成功 }) }) var _exe = new class { constructor(){ addEventListener('HostObjectAdded',e=>{ this.Promise.data = {} this.Promise.id = 0 e.value.methods.forEach(s=>this.addMethod(e.value.name,...s.split(','))) dispatchEvent(new Event('ExeLoaded')) }) } addMethod(h,m,attr){ var s = m[0].toLowerCase() + m.slice(1); if(this[s]) return if(attr=='async'){ this[s] = (...a)=>new this.Promise.Script(h,m,...a) }else{ this[s] = (...a)=>new this.Promise.Json(h,m,...a) } } } _exe.Promise = class{ constructor(){this.hook = ()=>{}} then(resolve){ this.hook = resolve return this } } _exe.Promise.Script = class extends _exe.Promise{ constructor(h,s,...a){super() var id = ++_exe.Promise.id _exe.Promise.data[id] = this chrome.webview.hostObjects[h][s](` (function(v,i){ v[i].hook(/*value*/) delete v[i] })(_exe.Promise.data,${id}) `,...a) } } _exe.Promise.Json = class extends _exe.Promise{ constructor(h,s,...a){super() chrome.webview.hostObjects[h][s](...a).then(r=>{ try{this.hook(JSON.parse(r))} catch(e){this.hook(r)} }) } } HostObject.cs public class HostObject { protected CoreWebView2 core = null!; public HostObject(WebView2 view) { view.NavigationCompleted+=(s,e) => { core = view.CoreWebView2; core.AddHostObjectToScript("exe",this); IIFE(@$" var e = new Event('HostObjectAdded') e.value = {{ name:'exe', methods:{JsonSerializer.Serialize(GetMethods())}, }} dispatchEvent(e) "); }; } //nullが返る public async Task<string> PostError(string url,string data) { var content = new StringContent(data,Encoding.UTF8); using(var client = new HttpClient()) { var response = await client.PostAsync(url,content); var s = await response.Content.ReadAsStringAsync(); return s; } } //手動でResolve public async void Post(string script,string url,string data) { var content = new StringContent(data, Encoding.UTF8); using (var client = new HttpClient()){ var response = await client.PostAsync(url, content); var s = await response.Content.ReadAsStringAsync(); Resolve(script,s); } } protected async void Resolve(string script,object v) { var s = script.Replace("/*value*/",JsonSerializer.Serialize(v)); var r = await core.ExecuteScriptAsync(s); } protected async void IIFE(string script) {//即時実行関数 var r = await core.ExecuteScriptAsync($"(function(){{{script}}})()"); } public string[] GetMethods(){ var objectMethods = typeof(Object).GetMethods(); var methods = new List<string>(); Type async = typeof(AsyncStateMachineAttribute); foreach(MethodInfo m in GetType().GetMethods()) { if(!Array.Exists(objectMethods,v => { return v.Name==m.Name; })){ if(m.GetCustomAttribute(async)==null) { methods.Add(m.Name); } else { methods.Add($"{m.Name},async"); } } } return methods.ToArray(); } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む