10
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Swift その2Advent Calendar 2017

Day 23

文字列をモールス信号に変換してフラッシュライトで表現してみた話

Posted at

はじめにのはじめに

大変遅くなって申し訳ないです。この記事は Swift その2 Advent Calendar 2017 23 日目の記事です。が、内容自体は実は下書きの状態のまま 1 年以上寝かしたものです。そのため、記事内容にある「今季」とかは 2016 年年末の話です。また、ぶっちゃけ 1 年前に書いた下書きなので正直今現在の筆者の記憶では当時の考えを再現できる範囲は限られています。ご了承ください。

でも時間あったら Swift 4 の Encoder として作ってみたい感 is ある。

はじめに

こんな記事を見かけたんですよ。LED ライトをチカチカさせる話。

そこで思ったのです。チカチカってまさに 0 と 1 の表現じゃないですか。まあ別に新しい発想でもなんでもないですけどね。

というわけで、なんか中二心くすぐるから、変換フレームワーク作ってみたいと思った。対象はとりあえず中二病がみんな大好きなモールス信号と、今季去年の今季で絶賛放送中のアニメ Occultic;Nine に登場したボーコードというやつにしてみました。

ちなみに変換フレームワークは GitHub で Alizes という名前で公開してます。本当は正しい綴りは Alizés ですが GitHub が 1 バイト文字以外の文字のリポジトリ名サポートしてないので仕方なく普通の e にしました。ちなみに由来はごちうさ天々座理世の名前の由来である Thé des Alizés です。なぜこの子にしたかというとこの子が一番モールス信号の話題に食いついてきそうだから。なお Alizés とはフランス語で「貿易風」のことです。

モールス信号

みんな大好きモールス信号。ご存知の通り単音「.」と長音「-」でアルファベットや数字を表現する信号で、また地域によっては独自の拡張、例えば日本なら仮名に対応する変換もあります。

でも 0 と 1 で表現するには単音が 0、長音が 1 なんて安易な発想はダメですね。例えばの話「SOS」をモールス信号に変換した場合「... --- ...」になるわけだが、上記のような変換法で「000111000」に変換してしまったら「EEETTTEEE」にもなりうるわけです。ですので実は信号と信号の間にあるギャップの方が「0」で、信号自体は「1」で表現しています。さらに正確に言うと「長音」の長さは「単音」の 3 倍だと規定されていますので、単音は「1」、長音は「111」になります。まあこのあたりは英語版の Wikipedia を参照すればわかるとおもいます。ちなみに先ほどの「SOS」はこのように変換すると「1010100011101110111000101010」になります。

ボーコード

有名なモールス信号と違い、ボーコードは今となってはかなりマイナーなコードとなります。というか筆者もそもそも Occultic;Nine 見るまでこのコードの存在すら知らなかった。

モールス信号と違い、長音単音なんて概念はなく、すべては 5 桁の 0 と 1 で表現する、まあすなわちぶっちゃけ言っちゃうと 5 ビット文字コードです。

詳しい説明はぜひ Wikipedia をご参照ください。また、Alizes はボーコードも対応していますし、この記事でも取り上げる予定でしたが、モールス信号の説明で燃え尽きたし相当なボリュームになって来たので詳しい実装を省きます。アプローチは基本モールス信号の対応と一緒なのでぜひ Alizes のソースコードをごらんください。

バイナリーコードコンテナーを作る

最終的には 0 と 1 を表現するものを作るわけですから、BinaryCodeContainer という struct と、BinaryCodeRepresentable という上記の容器に変換できるという protocol を作ります。

まあもちろん要するにバイナリーコードですからそのまま Data 型を使うのも一つの手ですが、Data 型ってそのまま各ビットを扱うのって微妙に手間かかるからね、というわけで io の二種類の case がある enum を作って、そしてこの enum の配列を持つ BinaryCodeContainer を作ります

BinaryCodeContainer
public struct BinaryCodeContainer {
	
	public enum Code {
		case o // 0
		case i // 1
	}
	
	public static let empty = BinaryCodeContainer(codes: [])
	
	// バイナリデータを `Code` の配列として保存
	public let codes: [Code]
	
	public init(codes: [Code]) {
		self.codes = codes
	}
	
	public init(count: Int, repeatedCode: Code) {
		
		self.codes = (0 ..< count).map { (_) -> Code in
			return repeatedCode
		}
		
	}
	
	public init(value: UInt, digitCount: Int) {
		
		self.codes = (0 ..< digitCount).reversed().map({ (i) -> Code in
			return (value >> UInt(i)) & 0b1 == 0 ? .o : .i
		})
		
	}
	
	public var isEmpty: Bool {
		return self.codes.count == 0
	}
	
}

そしてデバッグするときに print() で中身確認しやすいように CustomStringConvertibleprotocol を対応させておきましょう

BinaryCodeContainer
extension BinaryCodeContainer.Code: CustomStringConvertible {
	
	public var description: String {
		switch self {
		case .o:
			return "0"
			
		case .i:
			return "1"
		}
	}
	
}

extension BinaryCodeContainer: CustomStringConvertible {
	
	public var description: String {
		return self.codes.reduce("", { (description, code) -> String in
			return description + code.description
		})
	}
	
}

さらに複数の BinaryCodeContainer が連結できるように + 演算も一緒に

BinaryCodeContainer
public func + (lhs: BinaryCodeContainer, rhs: BinaryCodeContainer) -> BinaryCodeContainer {
	return BinaryCodeContainer(codes: lhs.codes + rhs.codes)
}

そして、BinaryCodeRepresentableprotocol を作ります

BinaryCodeRepresentable
public protocol BinaryCodeRepresentable {
	var binaryCodeContainer: BinaryCodeContainer { get }
}

これでとりあえずフラッシュライトの制御に必要なものができました。フラッシュライトを制御するとき、グローバルスレッドで桁ずつ走査し、.i ならフラッシュライトを .on に、.o なら .off にして一定時間スレッドをスリープさせ次の桁を参照するようにプログラム作れば猿でもフラッシュライト発信器が作れます。

ついでだから、文字列から生成できる StringInitializable という protocol も作っちゃいましょう

StringInitializable
public protocol StringInitializable {
	var initializedString: String { get }
	init(_ string: String)
}

モールス信号容器を作る

本体

モールス信号は長音と単音の組み合わせですが、実際は区切りとしての「ギャップ」もあります。ギャップはさらに信号自体の区切りを表す Inter Element ギャップと、アルファベットの区切りを表すというわけで信号を表す Short ギャップ、そして単語の区切りを表す Medium ギャップがあります。というわけで長音単音を表す enum とギャップを表す enum が必要です。また、文字列を容器に保存する際はギャップの保存は必要ありませんので、アルファベットの配列で作られた単語の配列を保存すれば大丈夫です。つまり信号の配列でアルファベットを作り、アルファベットの配列で単語を作り、そしてこの単語の配列がモールス信号の容器に入れます。

MorseCode
public struct MorseCode {
	
	// モールス信号を表現するための型を作ります
	public enum Unit {
		
		public enum Code {
			
			case dot // .
			case dash // -
			
		}
		
		public enum Gap {
			
			case interElement // _
			case short // ___
			case medium // _______
			
		}
		
		case code(Code)
		case gap(Gap)
		
	}
	
	// 一つ一つの単語を一つの塊にしてまとめます
	public struct Word {
		// 単語内に出てくる文字
		public struct Letter {
			
			public let codes: [Unit.Code]
			
		}
		
		fileprivate let content: String
		public let letters: [Letter]
		
	}
	// 実際の文字列
	fileprivate let content: String
	// 文字列内の各単語
	let words: [Word]
	
}

バイナリコードコンテナー変換メソッド

この MorseCodeBinaryCodeRepresentable に対応させればフラッシュライトのチカチカができるようになります。アプローチとしては単語 words の間には Medium のギャップを入れ、単語ないのアルファベット letters の間に Short のギャップを入れ、アルファベットを構成する信号の間に Inter Element をギャップを入れます。

MorseCode
extension MorseCode.Unit.Code: BinaryCodeRepresentable {
	// `Unit.Code` は `1` として扱います
	public var binaryCodeContainer: BinaryCodeContainer {
		switch self {
		case .dot: // .
			return BinaryCodeContainer(count: 1, repeatedCode: .i) // 1
			
		case .dash: // -
			return BinaryCodeContainer(count: 3, repeatedCode: .i) // 111
		}
	}
	
}

extension MorseCode.Unit.Gap: BinaryCodeRepresentable {
	// `Unit.Gap` は `0` として扱います
	public var binaryCodeContainer: BinaryCodeContainer {
		switch self {
		case .interElement: // _
			return BinaryCodeContainer(count: 1, repeatedCode: .o) // 0
			
		case .short: // ___
			return BinaryCodeContainer(count: 3, repeatedCode: .o) // 000
			
		case .medium: // _______
			return BinaryCodeContainer(count: 7, repeatedCode: .o) // 0000000
		}
	}
	
}

extension MorseCode.Word.Letter: BinaryCodeRepresentable {
	
	public var binaryCodeContainer: BinaryCodeContainer {
		// コードとコードの間には `Unit.Gap.InterElement` を入れます
		return self.codes.reduce(.empty) { (container, morseCode) -> BinaryCodeContainer in
			if container.isEmpty {
				return morseCode.binaryCodeContainer
			} else {
				return container + MorseCode.Unit.Gap.interElement.binaryCodeContainer + morseCode.binaryCodeContainer
			}
		}
	}
	
}

extension MorseCode.Word: BinaryCodeRepresentable {
	
	public var binaryCodeContainer: BinaryCodeContainer {
		// 文字と文字の間には `Unit.Gap.Short` を入れます
		return self.letters.reduce(.empty) { (container, letter) -> BinaryCodeContainer in
			if container.isEmpty {
				return letter.binaryCodeContainer
			} else {
				return container + MorseCode.Unit.Gap.short.binaryCodeContainer + letter.binaryCodeContainer
			}
		}
	}
	
}

extension MorseCode: BinaryCodeRepresentable {
	
	public var binaryCodeContainer: BinaryCodeContainer {
		// 単語と単語の間には `Unit.Gap.Medium` を入れます
		return self.words.reduce(.empty, { (container, word) -> BinaryCodeContainer in
			if container.isEmpty {
				return word.binaryCodeContainer
			} else {
				return container + Unit.Gap.medium.binaryCodeContainer + word.binaryCodeContainer
			}
		})
	}
	
}

文字列からモールス信号を作るイニシャライザー

ここまで来たらできた MorseCode インスタンスがあればバイナリーデータを読み取れ、フラッシュライトの制御ができるようになります。が、肝心の文字列からモールス信号に変換するイニシャライザーがまだ作られてない。ので、変換表を参照しながらイニシャライザーを作りましょう。アプローチは MorseCode の構造にちなんで、まずは文を単語に分けて、そして単語をアルファベットに分けて、アルファベットを変換表どおりにモールス信号に変換すれば OK です

MorseCode
extension MorseCode.Unit.Code {
	
	init?(_ code: Character) {
		switch code {
		case ".":
			self = .dot
			
		case "-":
			self = .dash
			
		case _:
			return nil
		}
	}
	
}

extension MorseCode.Word {
	
	public init(_ string: String) {
		// モールス信号変換表
		let dictionary = MorseCodeDictionary()
		let tuples = string.characters.flatMap { (character) -> (character: Character, code: MorseCode.Word.Letter)? in
			if let code = dictionary.getCode(for: character) {
				return (character, code)
			} else {
				return nil
			}
		}
		// モールス信号に変換済みの単語を作ります
		self.content = tuples.reduce("", { (content, tuple) -> String in
			return content + String(tuple.character)
		})
		// 単語内の文字として変換済みのモールス信号の配列を作ります
		self.letters = tuples.map({ (tuple) -> MorseCode.Word.Letter in
			return tuple.code
		})
		
	}
	
}

extension MorseCode: StringInitializable {
	
	public var initializedString: String {
		return self.content
	}
	
	public init (_ string: String) {
		
		// 文字列を単語単位に分割して `MorseCode.Word` を作ります
		let words = string.components(separatedBy: .whitespaces).map { (word) -> MorseCode.Word in
			return MorseCode.Word(word)
		}
		// モールス信号に変換済みの文字列を作ります
		self.content = words.reduce("", { (content, word) -> String in
			if content.isEmpty {
				return word.content
			} else {
				return content + " " + word.content
			}
		})
		// 単語として変換済みのモールス信号の配列を作ります
		self.words = words
		
	}
	
}
MorseCodeDictionary
// モールス信号変換表
public struct MorseCodeDictionary {
	
	private static let dictionary: [String: String] = [
		// アルファベット
		"A": ".-",
		"B": "-...",
		"C": "-.-.",
		"D": "-..",
		"E": ".",
		"F": "..-.",
		"G": "--.",
		
		"H": "....",
		"I": "..",
		"J": ".---",
		"K": "-.-",
		"L": ".-..",
		"M": "--",
		"N": "-.",
		
		"O": "---",
		"P": ".--.",
		"Q": "--.-",
		"R": ".-.",
		"S": "...",
		"T": "-",
		
		"U": "..-",
		"V": "...-",
		"W": ".--",
		"X": "-..-",
		"Y": "-.--",
		"Z": "--..",
		
		// 数字
		"0": "-----",
		"1": ".----",
		"2": "..---",
		"3": "...--",
		"4": "....-",
		"5": ".....",
		"6": "-....",
		"7": "--...",
		"8": "---..",
		"9": "----.",
		
		// 記号
		".": ".-.-.-",
		",": "--..--",
		"?": "..--..",
		"'": ".----.",
		"!": "-.-.--",
		"/": "-..-.",
		"(": "-.--.",
		")": "-.--.-",
		"&": ".-...",
		":": "---...",
		";": "-.-.-.",
		"=": "-...-",
		"+": ".-.-.",
		"-": "-....-",
		"_": "..--.-",
		"\"": ".-..-.",
		"$": "...-..-",
		"@": ".--.-.",
	]
	
	// 与えられた文字のモールス信号を返す
	public func getCode(for character: Character) -> MorseCode.Word.Letter? {
		
		let text = String(character)
		if let codeString = type(of: self).dictionary[text.uppercased()] {
			return MorseCode.Word.Letter(codeString: codeString)
			
		} else {
			return nil
		}
		
	}
	
}

extension MorseCode.Word.Letter {
	
	// 変換されたの "." と "-" で `MorseCode.Word.Letter` を作ります
	fileprivate init(codeString: String) {
		
		let codes = codeString.characters.map { (codeCharacter) -> MorseCode.Unit.Code in
			guard let code = MorseCode.Unit.Code(codeCharacter) else {
				fatalError("Invalid code string")
			}
			return code
		}
		
		self.codes = codes
		
	}
	
}

ここでわざわざ Letter だけ別ファイルにイニシャライザー作ったのは、変換表が後でカスタマイズ(例えば日本語への対応とか)がしやすいようにしたいからです。

確認用出力

ここまで実装したら、例えば let morseCode = MorseCode("hello, world!") を呼び出せば、morseCode という "hello, world" のモールス信号が生成されます。でも確認がまだしにくいです。これで 01 が出力できても .- が出力できません。というわけで CustomStringConvertible を実装します:

MorseCode
extension MorseCode.Unit.Code: CustomStringConvertible {
	
	// `"."` と `"-"` を出力
	public var description: String {
		switch self {
		case .dot:
			return "."
			
		case .dash:
			return "-"
		}
	}
	
}

extension MorseCode.Unit.Gap: CustomStringConvertible {
	
	// ギャップを出力。バイナリと違ってコードとコードの間に区切りは必要ないので、文字ごとと単語ごとのギャップだけが必要です。
	// スペースでは見づらいので、習慣的に `/` を使うことが多いっぽい
	public var description: String {
		switch self {
		case .interElement:
			return ""
			
		case .short:
			return "/"
			
		case .medium:
			return "///"
		
	}
	
}

extension MorseCode.Word.Letter: CustomStringConvertible {
	
	// 文字を出力。コードとコードの間に `.interElement` のギャップを出力するが実質何もないので `.` と `-` の連続です
	public var description: String {
		return self.codes.reduce("", { (description, code) -> String in
			if description.isEmpty {
				return code.description
			} else {
				return container + MorseCode.Unit.Gap.interElement.binaryCodeContainer + morseCode.binaryCodeContainer
			}
		})
	}
	
}

extension MorseCode.Word: CustomStringConvertible {
	
	// 単語を出力。文字と文字の間には `.short` のギャップ `/` を出力して区切りを表します。
	public var description: String {
		return self.letters.reduce("", { (description, letter) -> String in
			if description.isEmpty {
				return letter.description
			} else {
				return description + MorseCode.Unit.Gap.short.description + letter.description
			}
		})
	}
	
}

extension MorseCode: CustomStringConvertible {
	
	// 文を出力。単語と単語の間には `.medium` のギャップ `///` を出力して区切りを表します。
	public var description: String {
		return self.words.reduce("", { (description, word) -> String in
			if description.isEmpty {
				return word.description
			} else {
				return content + MorseCode.Unit.Gap.medium.description + word.content
			}
		})
	}
	
}

これでさっき作った let morseCode = MorseCode("hello, world!")print(morseCode) すると、...././.-../.-../---/--..--///.--/---/.-./.-../-../-.-.-- が出力されます。なんかそれっぽくなって来たんじゃないでしょうか!

ボーコード

確認用出力

ボーコードの実装はアプローチとして基本的にモールス信号と大して変わらないので、詳しい説明を省きます。ここでボーコードの確認用出力だけちょっと説明します。

ボーコードの Wikipedia 記事を読むとわかる通り、可変長ではなくただの 5bit コードなので、実装自体はとても単純です。でももちろん 01 だけで表示させるのはとても読みづらいです。5bit ごとに区切る必要があります。

そして、さらに英語版記事を読むとわかりますが、ボーコードは昔テープみたいな形で、そのテープに穴を開けることでデータを保存していました。というわけでこの形をぜひとも実現して見たいですね。そしてせっかくだから横に流れる形にしてみたいです。これはどういうことかというと、通常のように 5bit ごとに改行するとデータは縦に流れていきますので、このデータを転置行列のように 5 行にして、横が長い形にしたいです。

ボーコードの保存は、文字ごとに [Code] にしてます、つまり例えば A はボーコードで 11000 ですので、Alizes では A[.i, .i, .o, .o, .o] のように保存されています。なので、これを転置行列のように出力したければ、下記のように (0 ..< 5).map して、中身を self.codes.map すればいいです:

extension BaudotCode: CustomStringConvertible {
	
	public var description: String {
		return self.descriptionCodes(within: 0 ..< 5).joined(separator: "\n")
	}
	
}

private extension BinaryCodeContainer.Code {
	
	var baudotDescription: String {
		switch self {
		case .i:
			return "."
			
		case .o:
			return " "
		}
	}
	
}

private extension BaudotCode {
	
	func descriptionCodes(within lineRange: CountableRange<Int>) -> [String] {
		return lineRange.map({ (i) -> String in
			return self.codes.map({ (code) -> String in
				return code.binaryCodeContainer.codes[i].baudotDescription
			}).joined()
		})
	}
	
}

これで let baudotCode = BaudotCode("hello, world!"); print(baudotCode) すれば、下記のような出力がされます:

. .   . . .   ...
.  .. . . . .. . 
..     ...      .
.    ....  .. ...
.. .... . .. . . 

うーん、分かりづらいですね。

問題点としては、

  1. 基本文字の表示は行間が文字感覚より広いので、縦の 1 行が一つのまとまりとしてみづらい
  2. . は文字スペースの下に表示されているので、どっちかというと真ん中に表示される点が欲しい
  3. 文字コードの順番は上から読むのか下から読むのかが微妙にわかりにくい

というわけで、ちょっと改良します。

まず、行間隔はプログラムではどうにもならないので、縦を一つのまとまりとしてみてもらいやすいように、コードとコードの間に | を挿入します

次に、. の代わりに、 を使います。

そして最後の問題は、実はボーコードの英語版記事のテープの写真とかをみてみるとわかりますが、順番をわかりやすくするために、2bit 目と 3bit 目の間には小さい穴があります。というわけで、ここもその穴を表すために · を使います。

というわけで、これで確認用出力のコードはこんな風になります:

extension BaudotCode: CustomStringConvertible {
	
	public var description: String {
		let alignmentLine = self.codes.map({ _ in "·" }).joined(separator: "|")
		let transformedCodes1 = self.descriptionCodes(within: 0 ..< 2).joined(separator: "\n")
		let transformedCodes2 = self.descriptionCodes(within: 2 ..< 5).joined(separator: "\n")
		return [transformedCodes1, alignmentLine, transformedCodes2].joined(separator: "\n")
	}
	
}

private extension BinaryCodeContainer.Code {
	
	var baudotDescription: String {
		switch self {
		case .i:
			return "•"
			
		case .o:
			return " "
		}
	}
	
}

private extension BaudotCode {
	
	func descriptionCodes(within lineRange: CountableRange<Int>) -> [String] {
		return lineRange.map({ (i) -> String in
			return self.codes.map({ (code) -> String in
				return code.binaryCodeContainer.codes[i].baudotDescription
			}).joined(separator: "|")
		})
	}
	
}

これで先ほどと同じ let baudotCode = BaudotCode("hello, world!"); print(baudotCode) をやると、下記にように出力されます:

•| |•| | | |•| |•| |•| | | |•|•|•
•| | |•|•| |•| |•| |•| |•|•| |•| 
·|·|·|·|·|·|·|·|·|·|·|·|·|·|·|·|·
•|•| | | | | |•|•|•| | | | | | |•
•| | | | |•|•|•|•| | |•|•| |•|•|•
•|•| |•|•|•|•| |•| |•|•| |•| |•| 

これでだいぶ読みやすくなってでしょう。

フラッシュ!

さて、だいぶ長くなりましたが、まだ本題のエルチカの話に入ってません。というわけでいよいよチカります。

iPhone の LED をチカるには AVCaptureDevice.TorchMode.on.off の間に切り替えればいいです。これは簡単ですね、1 なら .on0 なら .off にすればいいです。というわけで作ります:

class Model {
	
	private let device = AVCaptureDevice.default(for: AVMediaType.video)
	
	private let torchQueue = DispatchQueue(label: "torch")
	
	@discardableResult
	func sendMessage(_ message: String) -> (text: String, code: String) {
		
		let unitCodeTimeInterval: TimeInterval = 0.1
		let code = MorseCode(message)
		
		self.torchQueue.async {
			
			code.binaryCodeContainer.codes.forEach({ (code) in
				self.turnTorchMode(to: code.torchMode)
				Thread.sleep(forTimeInterval: interval)
			})
			
			self.turnTorchMode(to: .off)
			
		}
		
		return (code.initializedString, code.description)
		
	}
	
	private func turnTorchMode(to mode: AVCaptureDevice.TorchMode) {
		
		guard let device = self.device else {
			return
		}
		
		do {
			try device.lockForConfiguration()
			device.torchMode = mode
			device.unlockForConfiguration()
			
		} catch let error {
			Console.shared.warning(error)
		}
		
	}
	
}

extension BinaryCodeContainer.Code {
	
	var torchMode: AVCaptureDevice.TorchMode {
		switch self {
		case .i:
			return .on
			
		case .o:
			return .off
		}
	}
	
}

これで Model().sendMessage("Hello, World") すれば、iPhone の LED がチカチカして "Hello, World!" のモールス信号を送れるようになります。

ちなみに、Alizes を使って LED をチカチカさせるアプリも GitHub で L-pika として公開してます、よろしければぜひみてみてください。

最後に前回の記事と同じ宣伝をします:L-pika はそもそも去年のこの時期に作ったものでしたのでレイアウトはまだアレですが、自作の Auto Layout を使わずに簡単にコードでレイアウト組めるフレームワーク NotAutoLayout の解説本を 1000 円で頒布します。100 部刷ったので間違いなく余るはずだから、興味あればぜひとも嫁を確保した上で 1 日目の東キ-11b までお越しいただければ嬉しいです

10
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?