From ca051944bee349d062b0e6485dd9251c17b4fbae Mon Sep 17 00:00:00 2001 From: Peter Prince Date: Fri, 25 Sep 2020 09:31:32 +0100 Subject: [PATCH] Initial commit --- .gitignore | 44 ++ .../audiomothchime/AudioMothChime.kt | 625 ++++++++++++++++++ .../audiomothchime/AudioMothChimeConnector.kt | 166 +++++ Javascript/audiomothchime.js | 475 +++++++++++++ Javascript/audiomothchime_connector.js | 94 +++ iOS/AudioMothChime.swift | 610 +++++++++++++++++ iOS/AudioMothChimeConnector.swift | 130 ++++ 7 files changed, 2144 insertions(+) create mode 100644 .gitignore create mode 100644 Android/info/openacousticdevices/audiomothchime/AudioMothChime.kt create mode 100644 Android/info/openacousticdevices/audiomothchime/AudioMothChimeConnector.kt create mode 100644 Javascript/audiomothchime.js create mode 100644 Javascript/audiomothchime_connector.js create mode 100644 iOS/AudioMothChime.swift create mode 100644 iOS/AudioMothChimeConnector.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bccaa9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Node artifact files +node_modules/ +dist/ + +# Compiled Java class files +*.class + +# Compiled Python bytecode +*.py[cod] + +# Log files +*.log + +# Package files +*.jar + +# Maven +target/ +dist/ + +# JetBrains IDE +.idea/ + +# Unit test reports +TEST*.xml + +# Generated by MacOS +.DS_Store + +# Generated by Windows +Thumbs.db + +# Applications +*.app +*.exe +*.war + +# Large media files +*.mp4 +*.tiff +*.avi +*.flv +*.mov +*.wmv diff --git a/Android/info/openacousticdevices/audiomothchime/AudioMothChime.kt b/Android/info/openacousticdevices/audiomothchime/AudioMothChime.kt new file mode 100644 index 0000000..b762b5c --- /dev/null +++ b/Android/info/openacousticdevices/audiomothchime/AudioMothChime.kt @@ -0,0 +1,625 @@ +/**************************************************************************** + * AudioMothChime.kt + * openacousticdevices.info + * June 2020 + *****************************************************************************/ + +package info.openacousticdevices.audiomothchime + +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioTrack +import android.os.Build + +import kotlin.math.* + +class AudioMothChime { + + /* General constants */ + + private val SPEED_FACTOR: Float = 1.0f + + private val USE_HAMMING_CODE: Boolean = true + + private val CARRIER_FREQUENCY: Int = 18000 + + private val NUMBER_OF_STOP_BITS: Int = 8 + + private val NUMBER_OF_START_BITS: Int = 16 + + /* Tone timing constants */ + + private val BIT_RISE: Float = 0.0005f / SPEED_FACTOR + private val BIT_FALL: Float = 0.0005f / SPEED_FACTOR + + private val LOW_BIT_SUSTAIN: Float = 0.004f / SPEED_FACTOR + private val HIGH_BIT_SUSTAIN: Float = 0.009f / SPEED_FACTOR + private val START_STOP_BIT_SUSTAIN: Float = 0.0065f / SPEED_FACTOR + + private val NOTE_RISE_DURATION: Float = 0.030f / SPEED_FACTOR + private val NOTE_FALL_DURATION: Float = 0.030f / SPEED_FACTOR + private val NOTE_LONG_FALL_DURATION: Float = 0.090f / SPEED_FACTOR + + /* Note parsing constants */ + + private val REGEX = Regex( + "^(C|C#|Db|D|D#|Eb|E|F|F#|Gb|G|G#|Ab|A|A#|Bb|B)([0-9]):([1-9])$" + ) + + private val FREQUENCY_LOOKUP = mapOf( + "C0" to 16, + "C#0" to 17, + "Db0" to 17, + "D0" to 18, + "D#0" to 19, + "Eb0" to 19, + "E0" to 21, + "F0" to 22, + "F#0" to 23, + "Gb0" to 23, + "G0" to 24, + "G#0" to 26, + "Ab0" to 26, + "A0" to 28, + "A#0" to 29, + "Bb0" to 29, + "B0" to 31, + "C1" to 33, + "C#1" to 35, + "Db1" to 35, + "D1" to 37, + "D#1" to 39, + "Eb1" to 39, + "E1" to 41, + "F1" to 44, + "F#1" to 46, + "Gb1" to 46, + "G1" to 49, + "G#1" to 52, + "Ab1" to 52, + "A1" to 55, + "A#1" to 58, + "Bb1" to 58, + "B1" to 62, + "C2" to 65, + "C#2" to 69, + "Db2" to 69, + "D2" to 73, + "D#2" to 78, + "Eb2" to 78, + "E2" to 82, + "F2" to 87, + "F#2" to 92, + "Gb2" to 92, + "G2" to 98, + "G#2" to 104, + "Ab2" to 104, + "A2" to 110, + "A#2" to 117, + "Bb2" to 117, + "B2" to 123, + "C3" to 131, + "C#3" to 139, + "Db3" to 139, + "D3" to 147, + "D#3" to 156, + "Eb3" to 156, + "E3" to 165, + "F3" to 175, + "F#3" to 185, + "Gb3" to 185, + "G3" to 196, + "G#3" to 208, + "Ab3" to 208, + "A3" to 220, + "A#3" to 233, + "Bb3" to 233, + "B3" to 247, + "C4" to 262, + "C#4" to 277, + "Db4" to 277, + "D4" to 294, + "D#4" to 311, + "Eb4" to 311, + "E4" to 330, + "F4" to 349, + "F#4" to 370, + "Gb4" to 370, + "G4" to 392, + "G#4" to 415, + "Ab4" to 415, + "A4" to 440, + "A#4" to 466, + "Bb4" to 466, + "B4" to 494, + "C5" to 523, + "C#5" to 554, + "Db5" to 554, + "D5" to 587, + "D#5" to 622, + "Eb5" to 622, + "E5" to 659, + "F5" to 698, + "F#5" to 740, + "Gb5" to 740, + "G5" to 784, + "G#5" to 831, + "Ab5" to 831, + "A5" to 880, + "A#5" to 932, + "Bb5" to 932, + "B5" to 988, + "C6" to 1047, + "C#6" to 1109, + "Db6" to 1109, + "D6" to 1175, + "D#6" to 1245, + "Eb6" to 1245, + "E6" to 1319, + "F6" to 1397, + "F#6" to 1480, + "Gb6" to 1480, + "G6" to 1568, + "G#6" to 1661, + "Ab6" to 1661, + "A6" to 1760, + "A#6" to 1865, + "Bb6" to 1865, + "B6" to 1976, + "C7" to 2093, + "C#7" to 2217, + "Db7" to 2217, + "D7" to 2349, + "D#7" to 2489, + "Eb7" to 2489, + "E7" to 2637, + "F7" to 2794, + "F#7" to 2960, + "Gb7" to 2960, + "G7" to 3136, + "G#7" to 3322, + "Ab7" to 3322, + "A7" to 3520, + "A#7" to 3729, + "Bb7" to 3729, + "B7" to 3951, + "C8" to 4186, + "C#8" to 4435, + "Db8" to 4435, + "D8" to 4699, + "D#8" to 4978, + "Eb8" to 4978, + "E8" to 5274, + "F8" to 5588, + "F#8" to 5920, + "Gb8" to 5920, + "G8" to 6272, + "G#8" to 6645, + "Ab8" to 6645, + "A8" to 7040, + "A#8" to 7459, + "Bb8" to 7459, + "B8" to 7902, + "C9" to 8372, + "C#9" to 8870, + "Db9" to 8870, + "D9" to 9397, + "D#9" to 9956, + "Eb9" to 9956, + "E9" to 10548, + "F9" to 11175, + "F#9" to 11840, + "Gb9" to 11840, + "G9" to 12544, + "G#9" to 13290, + "Ab9" to 13290, + "A9" to 14080, + "A#9" to 14917, + "Bb9" to 14917, + "B9" to 15804 + ) + + /* Encoding constant */ + + private val HAMMING_CODE = arrayOf( + arrayOf(0, 0, 0, 0, 0, 0, 0), + arrayOf(1, 1, 1, 0, 0, 0, 0), + arrayOf(1, 0, 0, 1, 1, 0, 0), + arrayOf(0, 1, 1, 1, 1, 0, 0), + arrayOf(0, 1, 0, 1, 0, 1, 0), + arrayOf(1, 0, 1, 1, 0, 1, 0), + arrayOf(1, 1, 0, 0, 1, 1, 0), + arrayOf(0, 0, 1, 0, 1, 1, 0), + arrayOf(1, 1, 0, 1, 0, 0, 1), + arrayOf(0, 0, 1, 1, 0, 0, 1), + arrayOf(0, 1, 0, 0, 1, 0, 1), + arrayOf(1, 0, 1, 0, 1, 0, 1), + arrayOf(1, 0, 0, 0, 0, 1, 1), + arrayOf(0, 1, 1, 0, 0, 1, 1), + arrayOf(0, 0, 0, 1, 1, 1, 1), + arrayOf(1, 1, 1, 1, 1, 1, 1) + ) + + /* Data classes */ + + private data class State( + var amplitudePhase: Float = 0.0f, + var x: Float = 1.0f, + var y: Float = 0.0f + ) + + private data class Note(var frequency: Int = 256, var duration: Int = 1) + + private data class CRC16(var low: Int = 0, var high: Int = 0) + + /* Functions to calculate CRC code */ + + private fun updateCRC16(crc: Int, incr: Int): Int { + + val CRC_POLY: Int = 0x1021 + + val xor: Int = (crc shr 15) and 0xFFFF + var out: Int = (crc shl 1) and 0xFFFF + + if (incr > 0) out += 1 + + if (xor > 0) out = out xor CRC_POLY + + return out + + } + + private fun createCRC16(bytes: Array): CRC16 { + + var crc: Int = 0 + + bytes.forEach { + for (x in 7 downTo 0) { + crc = updateCRC16(crc, (it and (1 shl x))) + } + } + + for (i in 0 until 16) { + crc = updateCRC16(crc, 0) + } + + return CRC16( + crc and 0xFF, + (crc shr 8) and 0xFF + ) + + } + + /* Function to encode bytes */ + + private fun encode(bytes: ArrayList): ArrayList { + + val bitSequence = arrayListOf() + + bytes.forEach { + + if (USE_HAMMING_CODE) { + + val low: Int = (it and 0x0F) + val high: Int = (it and 0xF0) shr 4 + + for (x in 0 until 7) { + bitSequence.add(HAMMING_CODE[low][x]) + bitSequence.add(HAMMING_CODE[high][x]) + } + + } else { + + for (x in 0 until 8) { + + val mask = (0x01 shl x) + + bitSequence.add(if ((it and mask) == mask) 1 else 0) + + } + + } + + } + + return bitSequence + + } + + /* Functions to parses notes */ + + private fun parseNotes(noteArray: Array): ArrayList { + + var notes = ArrayList() + + noteArray.forEach { + + if (REGEX.matches(it)) { + + val frequency: Int? = FREQUENCY_LOOKUP.get(it.split(":")[0]) + + val duration = it.split(":")[1].toInt() + + frequency?.let { + notes.add( + Note( + frequency, + duration + ) + ) + } + + } + + } + + if (notes.size == 0) notes.add(Note()) + + return notes + + } + + /* Functions to generate waveforms */ + + private fun createWaveformComponent( + waveform: ArrayList, + state: State, + sampleRate: Int, + frequency: Int, + phase: Float, + rampUp: Float, + sustain: Float, + rampDown: Float + ) { + + val samplesInRampUp: Int = round(rampUp * sampleRate).toInt() + + val samplesInSustain: Int = round(sustain * sampleRate).toInt() + + val samplesInRampDown: Int = round(rampDown * sampleRate).toInt() + + val theta: Float = 2f * Math.PI.toFloat() * frequency.toFloat() / sampleRate.toFloat() + + for (k in 0 until samplesInRampUp + samplesInSustain + samplesInRampDown) { + + if (k < samplesInRampUp) { + state.amplitudePhase = + min( + Math.PI.toFloat() / 2.0f, + state.amplitudePhase + Math.PI.toFloat() / 2.0f / samplesInRampUp.toFloat() + ) + } + + if (k >= samplesInRampUp + samplesInSustain) { + state.amplitudePhase = + max( + 0.0f, + state.amplitudePhase - Math.PI.toFloat() / 2.0f / samplesInRampDown.toFloat() + ) + } + + val volume: Float = sin(state.amplitudePhase).pow(2.0f) + + waveform.add(volume * phase * state.x) + + val x: Float = state.x * cos(theta) - state.y * sin(theta) + + val y: Float = state.x * sin(theta) + state.y * cos(theta) + + state.x = x + + state.y = y + + } + + } + + private fun createWaveform( + sampleRate: Int, + byteArray: Array, + noteArray: Array + ): ArrayList { + + val waveform = ArrayList() + + val waveform1 = ArrayList() + + val waveform2 = ArrayList() + + /* Generate bit sequence */ + + val crc: CRC16 = createCRC16(byteArray) + + val bytes: ArrayList = ArrayList() + + byteArray.forEach { bytes.add(it) } + + bytes.add(crc.low) + bytes.add(crc.high) + + val bitSequence: ArrayList = encode(bytes) + + /* Display output */ + + println("AUDIOMOTHCHIME: " + bytes.size + " bytes") + + println("AUDIOMOTHCHIME: " + bitSequence.size + " bits") + + /* Generate note sequence */ + + val notes: ArrayList = parseNotes(noteArray) + + /* Counters used during sound waveform creation */ + + var state: State = + State() + + var phase: Float = 1.0f + + /* Initial start bits */ + + for (i in 0 until NUMBER_OF_START_BITS) { + + createWaveformComponent( + waveform1, + state, + sampleRate, + CARRIER_FREQUENCY, + phase, + BIT_RISE, + START_STOP_BIT_SUSTAIN, + BIT_FALL + ) + + phase *= -1.0f + + } + + /* Data bits */ + + bitSequence.forEach { + + val duration = if (it == 1) HIGH_BIT_SUSTAIN else LOW_BIT_SUSTAIN + + createWaveformComponent( + waveform1, + state, + sampleRate, + CARRIER_FREQUENCY, + phase, + BIT_RISE, + duration, + BIT_FALL + ) + + phase *= -1.0f + + } + + /* Stop bits */ + + for (i in 0 until NUMBER_OF_STOP_BITS) { + + createWaveformComponent( + waveform1, + state, + sampleRate, + CARRIER_FREQUENCY, + phase, + BIT_RISE, + START_STOP_BIT_SUSTAIN, + BIT_FALL + ) + + phase *= -1.0f + + } + + /* Reset counter */ + + state = State() + + /* Calculate note duration */ + + var sumOfDurations: Int = 0 + + notes.forEach { sumOfDurations += it.duration } + + val noteDuration: Float = + (waveform1.size.toFloat() / sampleRate.toFloat() - notes.size.toFloat() * (NOTE_RISE_DURATION + NOTE_FALL_DURATION) + NOTE_FALL_DURATION - NOTE_LONG_FALL_DURATION) / sumOfDurations.toFloat() + + /* Create note waveform */ + + notes.forEachIndexed { index, note -> + + val noteFallDuration = + if (index == notes.size - 1) NOTE_LONG_FALL_DURATION else NOTE_FALL_DURATION + + createWaveformComponent( + waveform2, + state, + sampleRate, + note.frequency, + 1.0f, + NOTE_RISE_DURATION, + noteDuration * note.duration, + noteFallDuration + ) + + } + + /* Sum the waveforms */ + + val length = min(waveform1.size, waveform2.size) + + for (i in 0 until length) waveform.add(waveform1[i] / 4.0f + waveform2[i] / 2.0f) + + return waveform + + } + + /* Public chime function */ + + fun chime(byteArray: Array, noteArray: Array) { + + /* Configure AudioTrack */ + + val sampleRate: Int = AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) + + val minBufferSize = AudioTrack.getMinBufferSize( + sampleRate, + AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) + + val player = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + AudioTrack.Builder() + .setAudioAttributes( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_ALARM) + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .build() + ) + .setAudioFormat( + AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setSampleRate(sampleRate) + .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) + .build() + ) + .setBufferSizeInBytes(minBufferSize) + .build() + } else { + AudioTrack( + AudioManager.STREAM_MUSIC, sampleRate, + AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT, + minBufferSize, + AudioTrack.MODE_STATIC + ) + } + + /* Generate waveform */ + + val waveform: ArrayList = createWaveform(sampleRate, byteArray, noteArray) + + /* Play waveform */ + + val buffer = ShortArray(waveform.size) + + waveform.forEachIndexed { index, fl -> + buffer[index] = (fl * Short.MAX_VALUE).toShort() + } + + println("AUDIOMOTHCHIME: Start") + + player.play() + + player.write(buffer, 0, waveform.size) + + println("AUDIOMOTHCHIME: Done") + + } + +} diff --git a/Android/info/openacousticdevices/audiomothchime/AudioMothChimeConnector.kt b/Android/info/openacousticdevices/audiomothchime/AudioMothChimeConnector.kt new file mode 100644 index 0000000..9cf4ceb --- /dev/null +++ b/Android/info/openacousticdevices/audiomothchime/AudioMothChimeConnector.kt @@ -0,0 +1,166 @@ +/**************************************************************************** + * AudioMothChimeConnector.kt + * openacousticdevices.info + * June 2020 + *****************************************************************************/ + +package info.openacousticdevices.audiomothchime + +import java.util.Calendar + +class AudioMothChimeConnector { + + /* Useful constants */ + + private val BITS_PER_BYTE: Int = 8 + private val BITS_IN_INT16: Int = 16 + private val BITS_IN_INT32: Int = 32 + + private val LENGTH_OF_CHIME_PACKET: Int = 6 + private val LENGTH_OF_DEPLOYMENT_ID: Int = 8 + + private val MILLISECONDS_IN_SECOND = 1000 + private val SECONDS_IN_MINUTE = 60 + + /* AudioMothChime object */ + + private val audioMothChime = + AudioMothChime() + + /* Data class to keep track of packet contents */ + + private data class State(var bytes: Array, var index: Int) + + /* Private functions to set data */ + + private fun setBit(state: State, value: Boolean) { + + val byte = state.index / BITS_PER_BYTE + val bit = state.index % BITS_PER_BYTE + + if (value) { + + state.bytes[byte] = state.bytes[byte] or (1 shl bit) + + } + + state.index += 1 + + } + + private fun setBits(state: State, value: Int, length: Int) { + + for (i in 0 until length) { + + val mask = (1 shl i) + + setBit(state, (value and mask) == mask) + + } + + } + + private fun setTimeData(calendar: Calendar, data: Array, state: State) { + + /* Calculate timestamp and offset */ + + val timestamp: Int = (calendar.timeInMillis / MILLISECONDS_IN_SECOND).toInt() + + val timezoneMinutes: Int = + (calendar.timeZone.rawOffset + calendar.timeZone.dstSavings) / SECONDS_IN_MINUTE / MILLISECONDS_IN_SECOND + + /* Time and timezone */ + + setBits(state, timestamp, BITS_IN_INT32) + + setBits(state, timezoneMinutes, BITS_IN_INT16) + + } + + /* Public interface function */ + + fun playTime(calendar: Calendar) { + + /* Set up array */ + + var data = Array(LENGTH_OF_CHIME_PACKET) { 0 } + + var state = + State( + data, + 0 + ) + + /* Set the time date */ + + setTimeData(calendar, data, state) + + /* Play the data */ + + audioMothChime.chime( + data, + arrayOf( + "C5:1", + "D5:1", + "E5:1", + "C5:3" + ) + ) + + } + + fun playTimeAndDeploymentID(calendar: Calendar, deploymentID: Array) { + + /* Check deployment ID length */ + + if (deploymentID.size != LENGTH_OF_DEPLOYMENT_ID) { + + println("AUDIOMOTHCHIME_CONNECTOR: Deployment ID is incorrect length") + + return + + } + + /* Set up array */ + + val size = LENGTH_OF_CHIME_PACKET + LENGTH_OF_DEPLOYMENT_ID + + var data = Array(size) { 0 } + + var state = + State( + data, + 0 + ) + + /* Set the time date */ + + setTimeData(calendar, data, state) + + /* Set the deployment ID */ + + for (i in 0 until LENGTH_OF_DEPLOYMENT_ID) { + + data[size - 1 - i] = deploymentID[i] and 0xFF + + } + + /* Play the data */ + + audioMothChime.chime( + data, + arrayOf( + "Eb5:1", + "G5:1", + "D5:1", + "F#5:1", + "Db5:1", + "F5:1", + "C5:1", + "E5:5" + ) + ) + + } + +} diff --git a/Javascript/audiomothchime.js b/Javascript/audiomothchime.js new file mode 100644 index 0000000..8c590d8 --- /dev/null +++ b/Javascript/audiomothchime.js @@ -0,0 +1,475 @@ +/**************************************************************************** + * audiomothchime.js + * openacousticdevices.info + * August 2020 + *****************************************************************************/ + +'use strict'; + +/* global window */ +/* jslint bitwise: true */ + +/* Main code entry point */ + +var AudioMothChime = function () { + + var obj, audioContext, frequencyLookup, HAMMING_CODES, NOTE_REGEX, BIT_RISE, BIT_FALL, LOW_BIT_SUSTAIN, HIGH_BIT_SUSTAIN, START_STOP_BIT_SUSTAIN, CARRIER_FREQUENCY, SPEED_FACTOR, USE_HAMMING_CODE, NUMBER_OF_STOP_BITS, NUMBER_OF_START_BITS, NOTE_RISE_DURATION, NOTE_FALL_DURATION, NOTE_LONG_FALL_DURATION; + + /* General constants */ + + SPEED_FACTOR = 1; + + USE_HAMMING_CODE = true; + + CARRIER_FREQUENCY = 18000; + + NUMBER_OF_STOP_BITS = 8; + + NUMBER_OF_START_BITS = 16; + + /* Tone timing constants */ + + BIT_RISE = 0.0005 / SPEED_FACTOR; + BIT_FALL = 0.0005 / SPEED_FACTOR; + + LOW_BIT_SUSTAIN = 0.004 / SPEED_FACTOR; + HIGH_BIT_SUSTAIN = 0.009 / SPEED_FACTOR; + START_STOP_BIT_SUSTAIN = 0.0065 / SPEED_FACTOR; + + NOTE_RISE_DURATION = 0.030 / SPEED_FACTOR; + NOTE_FALL_DURATION = 0.030 / SPEED_FACTOR; + NOTE_LONG_FALL_DURATION = 0.090 / SPEED_FACTOR; + + /* Note parsing constant */ + + NOTE_REGEX = /^(C|C#|Db|D|D#|Eb|E|F|F#|Gb|G|G#|Ab|A|A#|Bb|B)([0-9]):([1-9])$/; + + /* Encoding constant */ + + HAMMING_CODES = [ + [0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0], + [1, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 0, 0], + [0, 1, 0, 1, 0, 1, 0], + [1, 0, 1, 1, 0, 1, 0], + [1, 1, 0, 0, 1, 1, 0], + [0, 0, 1, 0, 1, 1, 0], + [1, 1, 0, 1, 0, 0, 1], + [0, 0, 1, 1, 0, 0, 1], + [0, 1, 0, 0, 1, 0, 1], + [1, 0, 1, 0, 1, 0, 1], + [1, 0, 0, 0, 0, 1, 1], + [0, 1, 1, 0, 0, 1, 1], + [0, 0, 0, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1] + ]; + + /* Functions to calculate CRC code */ + + function crcUpdate (crcIn, incr) { + + var xor, out, CRC_POLY; + + CRC_POLY = 0x1021; + + xor = (crcIn >> 15) & 65535; + out = (crcIn << 1) & 65535; + + if (incr > 0) { + + out += 1; + + } + + if (xor > 0) { + + out ^= CRC_POLY; + + } + + return out; + + } + + function crc16 (bytes) { + + var i, j, low, high, crc; + + crc = 0; + + for (i = 0; i < bytes.length; i += 1) { + + for (j = 7; j >= 0; j -= 1) { + + crc = crcUpdate(crc, bytes[i] & (1 << j)); + + } + + } + + for (i = 0; i < 16; i += 1) { + + crc = crcUpdate(crc, 0); + + } + + low = crc & 255; + high = (crc >> 8) & 255; + + return [low, high]; + + } + + /* Function to encode bytes */ + + function encode (bytes) { + + var i, j, low, high, mask, bitSequence; + + bitSequence = []; + + for (i = 0; i < bytes.length; i += 1) { + + if (USE_HAMMING_CODE) { + + low = bytes[i] & 0x0F; + + high = (bytes[i] & 0xFF) >> 4; + + for (j = 0; j < 7; j += 1) { + + bitSequence.push(HAMMING_CODES[low][j]); + + bitSequence.push(HAMMING_CODES[high][j]); + + } + + } else { + + for (j = 0; j < 8; j += 1) { + + mask = 0x01 << j; + + bitSequence.push((mask & bytes[i]) === mask ? 1 : 0); + + } + + } + + } + + return bitSequence; + + } + + /* Functions to parse and generate notes */ + + function generateFrequencyLookup () { + + var i, j, note, NOTE_PREFIXES, NOTE_DISTANCES, NUMBER_OF_OCTAVES, lookUpTable; + + lookUpTable = {}; + + NUMBER_OF_OCTAVES = 10; + + NOTE_PREFIXES = ['C', 'C#', 'Db', 'D', 'D#', 'Eb', 'E', 'F', 'F#', 'Gb', 'G', 'G#', 'Ab', 'A', 'A#', 'Bb', 'B']; + + NOTE_DISTANCES = [-9, -8, -8, -7, -6, -6, -5, -4, -3, -3, -2, -1, -1, 0, 1, 1, 2]; + + for (i = 0; i < NUMBER_OF_OCTAVES; i += 1) { + + for (j = 0; j < NOTE_PREFIXES.length; j += 1) { + + note = NOTE_PREFIXES[j] + i; + lookUpTable[note] = Math.round(440 * Math.pow(2, i - 4 + NOTE_DISTANCES[j] / 12)); + + } + + } + + return lookUpTable; + + } + + function parseFrequencies (notes) { + + var i, result, frequencies; + + frequencies = []; + + for (i = 0; i < notes.length; i += 1) { + + result = notes[i].match(NOTE_REGEX); + if (result) { + + frequencies.push(frequencyLookup[result[1] + result[2]]); + + } + + } + + if (frequencies.length === 0) { + + return [256]; + + } + + return frequencies; + + } + + function parseDurations (notes) { + + var i, result, durations; + + durations = []; + + for (i = 0; i < notes.length; i += 1) { + + result = notes[i].match(NOTE_REGEX); + if (result) { + + durations.push(parseInt(result[3], 10)); + + } + + } + + if (durations.length > 0) { + + return durations; + + } + + return [1]; + + } + + /* Functions to generate waveforms */ + + function createWaveformComponent (waveform, state, frequency, phase, rampUp, sustain, rampDown) { + + var k, x, y, theta, volume, samplesInRampUp, samplesInSustain, samplesInRampDown; + + samplesInRampUp = rampUp * audioContext.sampleRate; + + samplesInSustain = sustain * audioContext.sampleRate; + + samplesInRampDown = rampDown * audioContext.sampleRate; + + theta = 2 * Math.PI * frequency / audioContext.sampleRate; + + for (k = 0; k < samplesInRampUp + samplesInSustain + samplesInRampDown; k += 1) { + + if (k < samplesInRampUp) { + + state.amplitudePhase = Math.min(Math.PI / 2, state.amplitudePhase + Math.PI / 2 / samplesInRampUp); + + } + + if (k >= samplesInRampUp + samplesInSustain) { + + state.amplitudePhase = Math.max(0, state.amplitudePhase - Math.PI / 2 / samplesInRampDown); + + } + + volume = Math.pow(Math.sin(state.amplitudePhase), 2); + + waveform.push(volume * phase * state.x); + + x = state.x * Math.cos(theta) - state.y * Math.sin(theta); + y = state.x * Math.sin(theta) + state.y * Math.cos(theta); + + state.x = x; + state.y = y; + + } + + } + + function createWaveform (bytes, notes) { + + var i, phase, state, bitSequence, duration, durations, sumOfDurations, noteFallDuration, frequencies, waveform, waveform1, waveform2; + + waveform = []; + + waveform1 = []; + + waveform2 = []; + + /* Generate bit sequence */ + + bytes = bytes.concat(crc16(bytes)); + + bitSequence = encode(bytes); + + /* Display output */ + + console.log('AUDIOMOTHCHIME: ' + bytes.length + ' bytes'); + + console.log('AUDIOMOTHCHIME: ' + bitSequence.length + ' bits'); + + /* Counters used during sound wave creation */ + + state = { + amplitudePhase: 0, + x: 1, + y: 0 + }; + + phase = 1; + + /* Initial start bits */ + + for (i = 0; i < NUMBER_OF_START_BITS; i += 1) { + + createWaveformComponent(waveform1, state, CARRIER_FREQUENCY, phase, BIT_RISE, START_STOP_BIT_SUSTAIN, BIT_FALL); + + phase *= -1; + + } + + /* Data bits */ + + for (i = 0; i < bitSequence.length; i += 1) { + + duration = bitSequence[i] === 1 ? HIGH_BIT_SUSTAIN : LOW_BIT_SUSTAIN; + + createWaveformComponent(waveform1, state, CARRIER_FREQUENCY, phase, BIT_RISE, duration, BIT_FALL); + + phase *= -1; + + } + + /* Stop bits */ + + for (i = 0; i < NUMBER_OF_STOP_BITS; i += 1) { + + createWaveformComponent(waveform1, state, CARRIER_FREQUENCY, phase, BIT_RISE, START_STOP_BIT_SUSTAIN, BIT_FALL); + + phase *= -1; + + } + + /* Counters used during sound wave creation */ + + state = { + amplitudePhase: 0, + x: 1, + y: 0 + }; + + /* Parse notes */ + + durations = parseDurations(notes); + + frequencies = parseFrequencies(notes); + + sumOfDurations = 0; + + for (i = 0; i < durations.length; i += 1) { + + sumOfDurations += durations[i]; + + } + + duration = waveform1.length / audioContext.sampleRate - durations.length * (NOTE_RISE_DURATION + NOTE_FALL_DURATION) + NOTE_FALL_DURATION - NOTE_LONG_FALL_DURATION; + + duration /= sumOfDurations; + + for (i = 0; i < durations.length; i += 1) { + + noteFallDuration = i === durations.length - 1 ? NOTE_LONG_FALL_DURATION : NOTE_FALL_DURATION; + + createWaveformComponent(waveform2, state, frequencies[i], 1, NOTE_RISE_DURATION, duration * durations[i], noteFallDuration); + + } + + /* Sum the waveforms */ + + for (i = 0; i < Math.min(waveform1.length, waveform2.length); i += 1) { + + waveform.push(waveform1[i] / 4 + waveform2[i] / 2); + + } + + return waveform; + + } + + /* Code entry point */ + + frequencyLookup = generateFrequencyLookup(); + + obj = { }; + + obj.chime = function (bytes, notes, callback) { + + var i, source, buffer, channel, waveform; + + function onended () { + + console.log('AUDIOMOTHCHIME: Done'); + + callback(); + + } + + function perform () { + + console.log('AUDIOMOTHCHIME: Start'); + + if (!audioContext) { + + if (window.AudioContext) { + + audioContext = new window.AudioContext(); + + } else { + + audioContext = new window.webkitAudioContext(); + + } + + } + + if (audioContext.state === 'suspended') { + + audioContext.resume(); + + } + + waveform = createWaveform(bytes, notes); + + buffer = audioContext.createBuffer(1, waveform.length, audioContext.sampleRate); + + channel = buffer.getChannelData(0); + + for (i = 0; i < waveform.length; i += 1) { + + channel[i] = waveform[i]; + + } + + source = audioContext.createBufferSource(); + + source.buffer = buffer; + + source.onended = onended; + + source.connect(audioContext.destination); + + source.start(); + + } + + /* Play the sound */ + + setTimeout(perform, 200); + + }; + + return obj; + +}; diff --git a/Javascript/audiomothchime_connector.js b/Javascript/audiomothchime_connector.js new file mode 100644 index 0000000..23dd7cc --- /dev/null +++ b/Javascript/audiomothchime_connector.js @@ -0,0 +1,94 @@ +/**************************************************************************** + * audiomothchime_connector.js + * openacousticdevices.info + * August 2020 + *****************************************************************************/ + +'use strict'; + +/* global AudioMothChime */ +/* jslint bitwise: true */ + +var AudioMothChimeConnector = function () { + + var obj, audioMothChime, LENGTH_OF_DEPLOYMENT_ID; + + LENGTH_OF_DEPLOYMENT_ID = 8; + + /* Function to encode little-endian value */ + + function littleEndianBytes (byteCount, value) { + + var i, buffer; + + buffer = []; + + for (i = 0; i < byteCount; i += 1) { + + buffer.push((value >> (i * 8)) & 255); + + } + + return buffer; + + } + + /* Function to generate time data */ + + function setTimeData (date) { + + var bytes, unixTime, timezoneMinutes; + + unixTime = Math.round(date.valueOf() / 1000); + + timezoneMinutes = -date.getTimezoneOffset(); + + bytes = littleEndianBytes(4, unixTime); + + bytes = bytes.concat(littleEndianBytes(2, timezoneMinutes)); + + return bytes; + + } + + /* Main code entry point */ + + audioMothChime = new AudioMothChime(); + + obj = { }; + + obj.playTime = function (date, callback) { + + var bytes = setTimeData(date); + + audioMothChime.chime(bytes, ['C5:1', 'D5:1', 'E5:1', 'C5:3'], callback); + + }; + + obj.playTimeAndDeploymentID = function (date, deploymentID, callback) { + + var i, bytes; + + bytes = setTimeData(date); + + if (!deploymentID || deploymentID.length !== LENGTH_OF_DEPLOYMENT_ID) { + + console.log('AUDIOMOTHCHIME_CONNECTOR: Deployment ID is incorrect length'); + + return; + + } + + for (i = 0; i < LENGTH_OF_DEPLOYMENT_ID; i += 1) { + + bytes.push(deploymentID[deploymentID.length - 1 - i] & 0xFF); + + } + + audioMothChime.chime(bytes, ['Eb5:1', 'G5:1', 'D5:1', 'F#5:1', 'Db5:1', 'F5:1', 'C5:1', 'E5:5'], callback); + + }; + + return obj; + +}; diff --git a/iOS/AudioMothChime.swift b/iOS/AudioMothChime.swift new file mode 100644 index 0000000..b355ba7 --- /dev/null +++ b/iOS/AudioMothChime.swift @@ -0,0 +1,610 @@ +/**************************************************************************** +* AudioMothChime.swift +* openacousticdevices.info +* June 2020 +*****************************************************************************/ + +import Foundation +import AVFoundation + +var data: Data! +var audioPlayer: AVAudioPlayer! + +class AudioMothChime { + + /* General constants */ + + private let SPEED_FACTOR: Float = 1.0 + + private let USE_HAMMING_CODE: Bool = true + + private let CARRIER_FREQUENCY: Int = 18000 + + private let NUMBER_OF_STOP_BITS: Int = 8 + + private let NUMBER_OF_START_BITS: Int = 16 + + /* Tone timing constants */ + + private var BIT_RISE: Float = 0.0005 + private var BIT_FALL: Float = 0.0005 + + private var LOW_BIT_SUSTAIN: Float = 0.004 + private var HIGH_BIT_SUSTAIN: Float = 0.009 + private var START_STOP_BIT_SUSTAIN: Float = 0.0065 + + private var NOTE_RISE_DURATION: Float = 0.030 + private var NOTE_FALL_DURATION: Float = 0.030 + private var NOTE_LONG_FALL_DURATION: Float = 0.090 + + /* Note parsing constants */ + + private let REGEX: String = "^(C|C#|Db|D|D#|Eb|E|F|F#|Gb|G|G#|Ab|A|A#|Bb|B)([0-9]):([1-9])$" + + private let FREQUENCY_LOOKUP: [String: Int] = [ + "C0": 16, + "C#0": 17, + "Db0": 17, + "D0": 18, + "D#0": 19, + "Eb0": 19, + "E0": 21, + "F0": 22, + "F#0": 23, + "Gb0": 23, + "G0": 24, + "G#0": 26, + "Ab0": 26, + "A0": 28, + "A#0": 29, + "Bb0": 29, + "B0": 31, + "C1": 33, + "C#1": 35, + "Db1": 35, + "D1": 37, + "D#1": 39, + "Eb1": 39, + "E1": 41, + "F1": 44, + "F#1": 46, + "Gb1": 46, + "G1": 49, + "G#1": 52, + "Ab1": 52, + "A1": 55, + "A#1": 58, + "Bb1": 58, + "B1": 62, + "C2": 65, + "C#2": 69, + "Db2": 69, + "D2": 73, + "D#2": 78, + "Eb2": 78, + "E2": 82, + "F2": 87, + "F#2": 92, + "Gb2": 92, + "G2": 98, + "G#2": 104, + "Ab2": 104, + "A2": 110, + "A#2": 117, + "Bb2": 117, + "B2": 123, + "C3": 131, + "C#3": 139, + "Db3": 139, + "D3": 147, + "D#3": 156, + "Eb3": 156, + "E3": 165, + "F3": 175, + "F#3": 185, + "Gb3": 185, + "G3": 196, + "G#3": 208, + "Ab3": 208, + "A3": 220, + "A#3": 233, + "Bb3": 233, + "B3": 247, + "C4": 262, + "C#4": 277, + "Db4": 277, + "D4": 294, + "D#4": 311, + "Eb4": 311, + "E4": 330, + "F4": 349, + "F#4": 370, + "Gb4": 370, + "G4": 392, + "G#4": 415, + "Ab4": 415, + "A4": 440, + "A#4": 466, + "Bb4": 466, + "B4": 494, + "C5": 523, + "C#5": 554, + "Db5": 554, + "D5": 587, + "D#5": 622, + "Eb5": 622, + "E5": 659, + "F5": 698, + "F#5": 740, + "Gb5": 740, + "G5": 784, + "G#5": 831, + "Ab5": 831, + "A5": 880, + "A#5": 932, + "Bb5": 932, + "B5": 988, + "C6": 1047, + "C#6": 1109, + "Db6": 1109, + "D6": 1175, + "D#6": 1245, + "Eb6": 1245, + "E6": 1319, + "F6": 1397, + "F#6": 1480, + "Gb6": 1480, + "G6": 1568, + "G#6": 1661, + "Ab6": 1661, + "A6": 1760, + "A#6": 1865, + "Bb6": 1865, + "B6": 1976, + "C7": 2093, + "C#7": 2217, + "Db7": 2217, + "D7": 2349, + "D#7": 2489, + "Eb7": 2489, + "E7": 2637, + "F7": 2794, + "F#7": 2960, + "Gb7": 2960, + "G7": 3136, + "G#7": 3322, + "Ab7": 3322, + "A7": 3520, + "A#7": 3729, + "Bb7": 3729, + "B7": 3951, + "C8": 4186, + "C#8": 4435, + "Db8": 4435, + "D8": 4699, + "D#8": 4978, + "Eb8": 4978, + "E8": 5274, + "F8": 5588, + "F#8": 5920, + "Gb8": 5920, + "G8": 6272, + "G#8": 6645, + "Ab8": 6645, + "A8": 7040, + "A#8": 7459, + "Bb8": 7459, + "B8": 7902, + "C9": 8372, + "C#9": 8870, + "Db9": 8870, + "D9": 9397, + "D#9": 9956, + "Eb9": 9956, + "E9": 10548, + "F9": 11175, + "F#9": 11840, + "Gb9": 11840, + "G9": 12544, + "G#9": 13290, + "Ab9": 13290, + "A9": 14080, + "A#9": 14917, + "Bb9": 14917, + "B9": 15804 + ] + + /* Constructor */ + + init() { + + BIT_RISE /= SPEED_FACTOR + BIT_FALL /= SPEED_FACTOR + + LOW_BIT_SUSTAIN /= SPEED_FACTOR + HIGH_BIT_SUSTAIN /= SPEED_FACTOR + START_STOP_BIT_SUSTAIN /= SPEED_FACTOR + + NOTE_RISE_DURATION /= SPEED_FACTOR + NOTE_FALL_DURATION /= SPEED_FACTOR + NOTE_LONG_FALL_DURATION /= SPEED_FACTOR + + } + + /* Encoding constant */ + + private let HAMMING_CODE = [ + [0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0], + [1, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 0, 0], + [0, 1, 0, 1, 0, 1, 0], + [1, 0, 1, 1, 0, 1, 0], + [1, 1, 0, 0, 1, 1, 0], + [0, 0, 1, 0, 1, 1, 0], + [1, 1, 0, 1, 0, 0, 1], + [0, 0, 1, 1, 0, 0, 1], + [0, 1, 0, 0, 1, 0, 1], + [1, 0, 1, 0, 1, 0, 1], + [1, 0, 0, 0, 0, 1, 1], + [0, 1, 1, 0, 0, 1, 1], + [0, 0, 0, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1] + ] + + /* Data classes */ + + private struct State { + var amplitudePhase: Float = 0.0 + var x: Float = 1.0 + var y: Float = 0.0 + } + + private struct Note { + var frequency: Int = 256 + var duration: Int = 1 + } + + private struct CRC16 { + var low: Int = 0 + var high: Int = 0 + } + + /* Functions: calculate CRC code */ + + private func updateCRC16(crc: Int, incr: Int) -> Int { + + let CRC_POLY: Int = 0x1021 + + let xor: Int = (crc >> 15) & 0xFFFF + var out: Int = (crc << 1) & 0xFFFF + + if incr > 0 { out += 1 } + + if xor > 0 { out = out ^ CRC_POLY } + + return out + + } + + private func createCRC16(bytes: Array) -> CRC16 { + + var crc: Int = 0 + + bytes.forEach { byte in + for x in stride(from: 7, through: 0, by: -1) { + crc = updateCRC16(crc: crc, incr: byte & (1 << x)) + } + } + + for _ in 0...15 { + crc = updateCRC16(crc: crc, incr: 0) + } + + return CRC16( + low: crc & 0xFF, + high: (crc >> 8) & 0xFF + ) + + } + + /* Function: encode bytes */ + + private func encode(bytes: Array) -> Array { + + var bitSequence = Array() + + bytes.forEach { byte in + + if USE_HAMMING_CODE { + + let low: Int = byte & 0x0F + let high: Int = (byte & 0xF0) >> 4 + + for x in 0..<7 { + bitSequence.append(HAMMING_CODE[low][x]) + bitSequence.append(HAMMING_CODE[high][x]) + } + + } else { + + for x in 0..<8 { + + let mask = 0x01 << x + + bitSequence.append(byte & mask == mask ? 1 : 0) + + } + + } + + } + + return bitSequence + + } + + /* Functions: parses notes */ + + private func parseNotes(noteArray: Array) -> Array { + + var notes = Array() + + noteArray.forEach { note in + + if note.range(of: REGEX, options: .regularExpression) != nil { + + if let frequency = FREQUENCY_LOOKUP[String(note.split(separator: ":")[0])] { + + let duration = Int(note.split(separator: ":")[1]) ?? 1 + + notes.append(Note(frequency: frequency, duration: duration)) + + } + + } + + } + + if notes.count == 0 { notes.append(Note()) } + + return notes + + } + + /* Functions: generate waveforms */ + + private func createWaveformComponent( waveform: inout Array, state: inout State, sampleRate: Int, frequency: Int, phase: Float, rampUp: Float, sustain: Float, rampDown: Float) { + + let samplesInRampUp: Int = Int(round(rampUp * Float(sampleRate))) + + let samplesInSustain: Int = Int(round(sustain * Float(sampleRate))) + + let samplesInRampDown: Int = Int(round(rampDown * Float(sampleRate))) + + let theta: Float = 2.0 * Float.pi * Float(frequency) / Float(sampleRate) + + for k in 0...samplesInRampUp + samplesInSustain + samplesInRampDown { + + if k < samplesInRampUp { + state.amplitudePhase = min(Float.pi / 2.0, state.amplitudePhase + Float.pi / 2.0 / Float(samplesInRampUp)) + } + + if k >= samplesInRampUp + samplesInSustain { + state.amplitudePhase = max(0.0,state.amplitudePhase - Float.pi / 2.0 / Float(samplesInRampDown)) + } + + let volume: Float = pow(sin(state.amplitudePhase), 2.0) + + waveform.append(volume * phase * state.x) + + let x: Float = state.x * cos(theta) - state.y * sin(theta) + + let y: Float = state.x * sin(theta) + state.y * cos(theta) + + state.x = x + + state.y = y + + } + + } + + private func createWaveform(sampleRate: Int, byteArray: Array, noteArray: Array) -> Array { + + var waveform = Array() + + var waveform1 = Array() + + var waveform2 = Array() + + /* Generate bit sequence */ + + let crc: CRC16 = createCRC16(bytes: byteArray) + + var bytes: Array = Array() + + byteArray.forEach { byte in bytes.append(byte) } + + bytes.append(crc.low) + bytes.append(crc.high) + + let bitSequence: Array = encode(bytes: bytes) + + /* Display output */ + + print("AUDIOMOTHCHIME: " + String(bytes.count) + " bytes") + + print("AUDIOMOTHCHIME: " + String(bitSequence.count) + " bits") + + /* Generate note sequence */ + + let notes: Array = parseNotes(noteArray: noteArray) + + /* Counters used during sound waveform creation */ + + var state: State = State() + + var phase: Float = 1.0 + + /* Initial start bits */ + + for _ in 0.., noteArray: Array) { + + /* Generate waveform */ + + let SAMPLE_RATE: Int = 44100 + + let waveform: Array = createWaveform(sampleRate: SAMPLE_RATE, byteArray: byteArray, noteArray: noteArray) + + /* Make the WAV header */ + + let HEADER_SIZE: Int = 44 + + let BYTES_PER_SAMPLE: Int = 2 + + let BYTES_IN_UINT32_VALUE: Int = 4 + + data = Data(count: HEADER_SIZE + BYTES_PER_SAMPLE * waveform.count) + + func writeUInt32ToData(index: Int, value: UInt32) { + + data[index] = UInt8(value & 0xFF) + data[index + 1] = UInt8((value >> 8) & 0xFF) + data[index + 2] = UInt8((value >> 16) & 0xFF) + data[index + 4] = UInt8((value >> 24) & 0xFF) + + } + + for i in 0..<4 { data[i] = String("RIFF").utf8.map{ UInt8($0) }[i] } + + let riffChunkSize: UInt32 = UInt32(data.count - 4 - BYTES_IN_UINT32_VALUE) + + writeUInt32ToData(index: 4, value: riffChunkSize) + + for i in 0..<8 { data[8 + i] = String("WAVEfmt ").utf8.map{ UInt8($0) }[i] } + + data[16] = 16 // Format chunk size + + data[20] = 1 // PCM + + data[22] = 1 // Number of channels + + let sampleRate: UInt32 = UInt32(SAMPLE_RATE) + + writeUInt32ToData(index: 24, value: sampleRate) + + let bytesPerSecond: UInt32 = UInt32(data.count - 4 - BYTES_IN_UINT32_VALUE) + + writeUInt32ToData(index: 28, value: bytesPerSecond) + + data[32] = 2 // Bytes per capture + + data[34] = 16 // Bits per sample + + for i in 0..<4 { data[36 + i] = String("data").utf8.map{ UInt8($0) }[i] } + + let dataChunkSize: UInt32 = UInt32(BYTES_PER_SAMPLE * waveform.count) + + writeUInt32ToData(index: 40, value: dataChunkSize) + + /* Convert the waveform data */ + + var index: Int = HEADER_SIZE + + waveform.forEach { sample in + + let value: Int16 = Int16(sample * Float(Int16.max)) + + data[index] = UInt8(value & 0xFF) + data[index+1] = UInt8((value >> 8) & 0xFF) + + index += 2 + + } + + do { + + print("AUDIOMOTHCHIME: Start") + + audioPlayer = try AVAudioPlayer(data: data, fileTypeHint: "wav") + + audioPlayer.play() + + while (audioPlayer.isPlaying ) { } + + print("AUDIOMOTHCHIME: Done") + + } catch { + + print(error) + + } + + } + +} diff --git a/iOS/AudioMothChimeConnector.swift b/iOS/AudioMothChimeConnector.swift new file mode 100644 index 0000000..5f91b2b --- /dev/null +++ b/iOS/AudioMothChimeConnector.swift @@ -0,0 +1,130 @@ +/**************************************************************************** +* AudioMothChimeConnector.swift +* openacousticdevices.info +* June 2020 +*****************************************************************************/ + +import Foundation + +class AudioMothChimeConnector { + + /* Useful constants */ + + private let BITS_PER_BYTE: Int = 8 + private let BITS_IN_INT16: Int = 16 + private let BITS_IN_INT32: Int = 32 + + private let LENGTH_OF_CHIME_PACKET: Int = 6 + private let LENGTH_OF_DEPLOYMENT_ID: Int = 8 + + private let MILLISECONDS_IN_SECOND = 1000 + private let SECONDS_IN_MINUTE = 60 + + /* AudioMothChime object */ + + private let audioMothChime = AudioMothChime() + + /* Private functions to set data */ + + private func setBit(data: inout Array, index: inout Int, value: Bool) { + + let byte = index / BITS_PER_BYTE + let bit = index % BITS_PER_BYTE + + if (value) { + + data[byte] = data[byte] | (1 << bit) + + } + + index += 1 + + } + + private func setBits(data: inout Array, index: inout Int, value: Int, length: Int) { + + for i in 0.., index: inout Int, date: Date, timezone: TimeZone) { + + /* Calculate timestamp and offset */ + + let timestamp: Int = Int(date.timeIntervalSince1970) + + let timezoneMinutes: Int = timezone.secondsFromGMT() / SECONDS_IN_MINUTE + + /* Time and timezone */ + + setBits(data: &data, index: &index, value: timestamp, length: BITS_IN_INT32) + + setBits(data: &data, index: &index, value: timezoneMinutes, length: BITS_IN_INT16) + + } + + /* Public interface function */ + + func playTime(date: Date, timezone: TimeZone) { + + /* Set up array */ + + var index: Int = 0 + + var data = Array(repeating: 0, count: LENGTH_OF_CHIME_PACKET) + + /* Set the time date */ + + setTimeData(data: &data, index: &index, date: date, timezone: timezone) + + /* Play the data */ + + audioMothChime.chime(byteArray: data, noteArray: ["C5:1", "D5:1", "E5:1", "C5:3"]) + + } + + func playTimeAndDeploymentID(date: Date, timezone: TimeZone, deploymentID: Array) { + + /* Check deployment ID length */ + + if deploymentID.count != LENGTH_OF_DEPLOYMENT_ID { + + print("AUDIOMOTHCHIME_CONNECTOR: Deployment ID is incorrect length") + + return + + } + + /* Set up array */ + + var index: Int = 0 + + var data: Array = Array(repeating: 0, count: LENGTH_OF_CHIME_PACKET + LENGTH_OF_DEPLOYMENT_ID) + + /* Set the time date */ + + setTimeData(data: &data, index: &index, date: date, timezone: timezone) + + /* Set the deployment ID */ + + let length = LENGTH_OF_CHIME_PACKET + LENGTH_OF_DEPLOYMENT_ID + + for i in 0..