IRremote
ir_DistanceWidthProtocol.hpp
Go to the documentation of this file.
1 /*
2  * ir_DistanceWidthProtocol.hpp
3  *
4  * Contains only the decoder functions for universal pulse width or pulse distance protocols!
5  * The send functions are used by almost all protocols and therefore in IRSend.hh.
6  *
7  * This decoder tries to decode a pulse distance or pulse distance width with constant period (or pulse width - not enabled yet) protocol.
8  * 1. Analyze all space and mark length
9  * 2. Decide which protocol we have
10  * 3. Try to decode with the mark and space data found in step 1
11  * 4. Assume one start bit / header and one stop bit, since pulse distance data must have a stop bit!
12  * No data and address decoding, only raw data as result.
13  *
14  * Pulse distance data can be sent with the generic function as in SendDemo example line 155:
15  * https://github.com/Arduino-IRremote/Arduino-IRremote/blob/d51b540cb2ddf1424888d2d9e6b62fe1ef46859d/examples/SendDemo/SendDemo.ino#L155
16  * void sendPulseDistanceWidthData(unsigned int aOneMarkMicros, unsigned int aOneSpaceMicros, unsigned int aZeroMarkMicros,
17  * unsigned int aZeroSpaceMicros, uint32_t aData, uint8_t aNumberOfBits, bool aMSBfirst, bool aSendStopBit = false)
18  * The header must be sent manually with:
19  * IrSender.mark(MarkMicros)
20  * IrSender.space(SpaceMicros);
21  *
22  * Or send it by filling a DecodedRawDataArray and with the sendPulseDistanceWidthFromArray() function as in SendDemo example line 175:
23  * https://github.com/Arduino-IRremote/Arduino-IRremote/blob/d51b540cb2ddf1424888d2d9e6b62fe1ef46859d/examples/SendDemo/SendDemo.ino#L175
24  * sendPulseDistanceWidthFromArray(uint_fast8_t aFrequencyKHz, unsigned int aHeaderMarkMicros,
25  * unsigned int aHeaderSpaceMicros, unsigned int aOneMarkMicros, unsigned int aOneSpaceMicros, unsigned int aZeroMarkMicros,
26  * unsigned int aZeroSpaceMicros, uint32_t *aDecodedRawDataArray, unsigned int aNumberOfBits, bool aMSBFirst,
27  * bool aSendStopBit, unsigned int aRepeatPeriodMillis, int_fast8_t aNumberOfRepeats)
28  *
29  * This file is part of Arduino-IRremote https://github.com/Arduino-IRremote/Arduino-IRremote.
30  *
31  ************************************************************************************
32  * MIT License
33  *
34  * Copyright (c) 2022-2023 Armin Joachimsmeyer
35  *
36  * Permission is hereby granted, free of charge, to any person obtaining a copy
37  * of this software and associated documentation files (the "Software"), to deal
38  * in the Software without restriction, including without limitation the rights
39  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
40  * copies of the Software, and to permit persons to whom the Software is furnished
41  * to do so, subject to the following conditions:
42  *
43  * The above copyright notice and this permission notice shall be included in all
44  * copies or substantial portions of the Software.
45  *
46  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
47  * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
48  * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
49  * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
50  * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
51  * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
52  *
53  ************************************************************************************
54  */
55 #ifndef _IR_DISTANCE_WIDTH_HPP
56 #define _IR_DISTANCE_WIDTH_HPP
57 
58 #if !defined(DISTANCE_WIDTH_MAXIMUM_REPEAT_DISTANCE_MICROS)
59 #define DISTANCE_WIDTH_MAXIMUM_REPEAT_DISTANCE_MICROS 100000 // 100 ms, bit it is just a guess
60 #endif
61 
62 #if defined(DEBUG) && !defined(LOCAL_DEBUG)
63 #define LOCAL_DEBUG
64 #else
65 //#define LOCAL_DEBUG // This enables debug output only for this file
66 #endif
67 
68 // accept durations up to 50 * 50 (MICROS_PER_TICK) 2500 microseconds
69 #define DURATION_ARRAY_SIZE 50
70 
71 // Switch the decoding according to your needs
72 //#define USE_MSB_DECODING_FOR_DISTANCE_DECODER // If active, it resembles LG, otherwise LSB first as most other protocols e.g. NEC and Kaseikyo/Panasonic
73 
77 //=====================================================================================
78 // DDD III SSS TTTTTT AA N N CCC EEEE W W III DDD TTTTTT H H
79 // D D I S TT A A NN N C E W W I D D TT H H
80 // D D I SSS TT AAAA N N N C EEE W W W I D D TT HHHH
81 // D D I S TT A A N NN C E W W W I D D TT H H
82 // DDD III SSSS TT A A N N CCC EEEE W W III DDD TT H H
83 //=====================================================================================
84 // see: https://www.mikrocontroller.net/articles/IRMP_-_english#Codings
85 #if defined(LOCAL_DEBUG)
86 void printDurations(uint8_t aArray[], uint8_t aMaxIndex) {
87  for (uint_fast8_t i = 0; i <= aMaxIndex; i++) {
88  //Print index at the beginning of a new line
89  if (i % 10 == 0) {
90  if (i == 0) {
91  Serial.print(' '); // indentation for the first index 0
92  } else {
93  Serial.println(); // new line for next indexes 10, 20 etc.
94  }
95  Serial.print(i);
96  Serial.print(F(": "));
97  }
98  // Print number of values in array and duration if != 0
99  Serial.print(aArray[i]);
100  if (aArray[i] != 0) {
101  Serial.print('x');
102  Serial.print(i * (uint16_t) MICROS_PER_TICK);
103  }
104  Serial.print(F(" | "));
105  }
106  Serial.println();
107 }
108 #endif
109 
110 /*
111  * @return false if more than 2 distinct duration values found
112  */
113 bool aggregateArrayCounts(uint8_t aArray[], uint8_t aMaxIndex, uint8_t *aShortIndex, uint8_t *aLongIndex) {
114  uint8_t tSum = 0;
115  uint16_t tWeightedSum = 0;
116  for (uint_fast8_t i = 0; i <= aMaxIndex; i++) {
117  uint8_t tCurrentDurations = aArray[i];
118  if (tCurrentDurations != 0) {
119  // Add it to sum and remove array content
120  tSum += tCurrentDurations;
121  tWeightedSum += (tCurrentDurations * i);
122  aArray[i] = 0;
123  }
124  if ((tCurrentDurations == 0 || i == aMaxIndex) && tSum != 0) {
125  // here we have a sum and a gap after the values
126  uint8_t tAggregateIndex = (tWeightedSum + (tSum / 2)) / tSum; // with rounding
127  aArray[tAggregateIndex] = tSum; // disabling this line increases code size by 2 - unbelievable!
128  // store aggregate for later decoding
129  if (*aShortIndex == 0) {
130  *aShortIndex = tAggregateIndex;
131  } else if (*aLongIndex == 0) {
132  *aLongIndex = tAggregateIndex;
133  } else {
134  // we have 3 bins => this is likely no pulse width or distance protocol. e.g. it can be RC5.
135  return false;
136  }
137  // initialize for next aggregation
138  tSum = 0;
139  tWeightedSum = 0;
140  }
141  }
142  return true;
143 }
144 
145 /*
146  * Try to decode a pulse distance or pulse width protocol.
147  * 1. Analyze all space and mark length
148  * 2. Decide if we have an pulse width or distance protocol
149  * 3. Try to decode with the mark and space data found in step 1
150  * No data and address decoding, only raw data as result.
151  */
153  uint8_t tDurationArray[DURATION_ARRAY_SIZE]; // For up to 49 ticks / 2450 us
154 
155  /*
156  * Accept only protocols with at least 8 bits
157  */
158  if (decodedIRData.rawlen < (2 * 8) + 4) {
159  IR_DEBUG_PRINT(F("PULSE_DISTANCE_WIDTH: "));
160  IR_DEBUG_PRINT(F("Data length="));
162  IR_DEBUG_PRINTLN(F(" is less than 20"));
163  return false;
164  }
165 
166  uint_fast8_t i;
167 
168  // Reset duration array
169  memset(tDurationArray, 0, DURATION_ARRAY_SIZE);
170 
171  uint8_t tIndexOfMaxDuration = 0;
172  /*
173  * Count number of mark durations up to 49 ticks. Skip leading start and trailing stop bit.
174  */
175  for (i = 3; i < (uint_fast8_t) decodedIRData.rawlen - 2; i += 2) {
176  auto tDurationTicks = decodedIRData.rawDataPtr->rawbuf[i];
177  if (tDurationTicks < DURATION_ARRAY_SIZE) {
178  tDurationArray[tDurationTicks]++; // count duration if less than DURATION_ARRAY_SIZE (50)
179  if (tIndexOfMaxDuration < tDurationTicks) {
180  tIndexOfMaxDuration = tDurationTicks;
181  }
182  } else {
183 #if defined(LOCAL_DEBUG)
184  Serial.print(F("PULSE_DISTANCE_WIDTH: "));
185  Serial.print(F("Mark "));
186  Serial.print(tDurationTicks * MICROS_PER_TICK);
187  Serial.print(F(" is longer than maximum "));
188  Serial.print(DURATION_ARRAY_SIZE * MICROS_PER_TICK);
189  Serial.print(F(" us. Index="));
190  Serial.println(i);
191 #endif
192  return false;
193  }
194  }
195 
196  /*
197  * Aggregate mark counts to one duration bin
198  */
199  uint8_t tMarkTicksShort = 0;
200  uint8_t tMarkTicksLong = 0;
201  bool tSuccess = aggregateArrayCounts(tDurationArray, tIndexOfMaxDuration, &tMarkTicksShort, &tMarkTicksLong);
202 #if defined(LOCAL_DEBUG)
203  Serial.println(F("Mark:"));
204  printDurations(tDurationArray, tIndexOfMaxDuration);
205 #endif
206 
207  if (!tSuccess) {
208 #if defined(LOCAL_DEBUG)
209  Serial.print(F("PULSE_DISTANCE_WIDTH: "));
210  Serial.println(F("Mark aggregation failed, more than 2 distinct mark duration values found"));
211 #endif
212  return false;
213  }
214 
215  // Reset duration array
216  memset(tDurationArray, 0, DURATION_ARRAY_SIZE);
217 
218  /*
219  * Count number of space durations. Skip leading start and trailing stop bit.
220  */
221  tIndexOfMaxDuration = 0;
222  for (i = 4; i < (uint_fast8_t) decodedIRData.rawlen - 2; i += 2) {
223  auto tDurationTicks = decodedIRData.rawDataPtr->rawbuf[i];
224  if (tDurationTicks < DURATION_ARRAY_SIZE) {
225  tDurationArray[tDurationTicks]++;
226  if (tIndexOfMaxDuration < tDurationTicks) {
227  tIndexOfMaxDuration = tDurationTicks;
228  }
229  } else {
230 #if defined(LOCAL_DEBUG)
231  Serial.print(F("PULSE_DISTANCE_WIDTH: "));
232  Serial.print(F("Space "));
233  Serial.print(tDurationTicks * MICROS_PER_TICK);
234  Serial.print(F(" is longer than maximum "));
235  Serial.print(DURATION_ARRAY_SIZE * MICROS_PER_TICK);
236  Serial.print(F(" us. Index="));
237  Serial.println(i);
238 #endif
239  return false;
240  }
241  }
242 
243  /*
244  * Aggregate space counts to one duration bin
245  */
246  uint8_t tSpaceTicksShort = 0;
247  uint8_t tSpaceTicksLong = 0;
248  tSuccess = aggregateArrayCounts(tDurationArray, tIndexOfMaxDuration, &tSpaceTicksShort, &tSpaceTicksLong);
249 #if defined(LOCAL_DEBUG)
250  Serial.println(F("Space:"));
251  printDurations(tDurationArray, tIndexOfMaxDuration);
252 #endif
253 
254  if (!tSuccess) {
255 #if defined(LOCAL_DEBUG)
256  Serial.print(F("PULSE_DISTANCE_WIDTH: "));
257  Serial.println(F("Space aggregation failed, more than 2 distinct space duration values found"));
258 #endif
259  return false;
260  }
261 
262  /*
263  * Print characteristics of this protocol. Durations are in ticks.
264  * Number of bits, start bit, start pause, long mark, long space, short mark, short space
265  *
266  * NEC: 32, 180, 90, 0, 34, 11, 11
267  * Samsung32: 32, 90, 90, 0, 34, 11, 11
268  * LG: 28, 180, 84, 0, 32, 10, 11
269  * JVC: 16, 168, 84, 0, 32, 10, 10
270  * Kaseikyo: 48. 69, 35, 0, 26, 9, 9
271  * Sony: 12|15|20, 48, 12, 24, 0, 12, 12 // the only known pulse width protocol
272  */
273 #if defined(LOCAL_DEBUG)
274  Serial.print(F("DistanceWidthTimingInfoStruct: "));
275  Serial.print(decodedIRData.rawDataPtr->rawbuf[1] * MICROS_PER_TICK);
276  Serial.print(F(", "));
277  Serial.print(decodedIRData.rawDataPtr->rawbuf[2] * MICROS_PER_TICK);
278  Serial.print(F(", "));
279  Serial.print(tMarkTicksLong * MICROS_PER_TICK);
280  Serial.print(F(", "));
281  Serial.print(tSpaceTicksLong * MICROS_PER_TICK);
282  Serial.print(F(", "));
283  Serial.print(tMarkTicksShort * MICROS_PER_TICK);
284  Serial.print(F(", "));
285  Serial.println(tSpaceTicksShort * MICROS_PER_TICK);
286 #endif
287  uint8_t tStartIndex = 3;
288  // skip leading start bit for decoding.
289  uint16_t tNumberOfBits = (decodedIRData.rawlen / 2) - 1;
290  if (tSpaceTicksLong > 0 && tMarkTicksLong == 0) {
291  // For PULSE_DISTANCE a stop bit is mandatory, for PULSE_WIDTH it is not required!
292  tNumberOfBits--; // Correct for stop bit
293  }
294  decodedIRData.numberOfBits = tNumberOfBits;
295  uint8_t tNumberOfAdditionalArrayValues = (tNumberOfBits - 1) / BITS_IN_RAW_DATA_TYPE;
296 
297  /*
298  * We can have the following protocol timings
299  * Pulse distance: Pulses/marks are constant, pause/spaces have different length, like NEC.
300  * Pulse width: Pulses/marks have different length, pause/spaces are constant, like Sony.
301  * Pulse distance width: Pulses/marks and pause/spaces have different length, often the bit length is constant, like MagiQuest.
302  * Pulse distance width can be decoded by pulse width decoder, if this decoder does not check the length of pause/spaces.
303  */
304 
305  if (tMarkTicksLong == 0 && tSpaceTicksLong == 0) {
306 #if defined(LOCAL_DEBUG)
307  Serial.print(F("PULSE_DISTANCE: "));
308  Serial.println(F("Only 1 distinct duration value for each space and mark found"));
309 #endif
310  return false;
311  }
312  unsigned int tSpaceMicrosShort;
313 #if defined DECODE_STRICT_CHECKS
314  if(tMarkTicksLong > 0 && tSpaceTicksLong > 0) {
315  // We have different mark and space length here, so signal decodePulseDistanceWidthData() not to check against constant length decodePulseDistanceWidthData
316  tSpaceMicrosShort = 0;
317  }
318 #endif
319  tSpaceMicrosShort = tSpaceTicksShort * MICROS_PER_TICK;
320  unsigned int tMarkMicrosShort = tMarkTicksShort * MICROS_PER_TICK;
321  unsigned int tMarkMicrosLong = tMarkTicksLong * MICROS_PER_TICK;
322  unsigned int tSpaceMicrosLong = tSpaceTicksLong * MICROS_PER_TICK;
323 
324  for (uint_fast8_t i = 0; i <= tNumberOfAdditionalArrayValues; ++i) {
325  uint8_t tNumberOfBitsForOneDecode = tNumberOfBits;
326  /*
327  * Decode in 32/64 bit chunks. Only the last chunk can contain less than 32/64 bits
328  */
329  if (tNumberOfBitsForOneDecode > BITS_IN_RAW_DATA_TYPE) {
330  tNumberOfBitsForOneDecode = BITS_IN_RAW_DATA_TYPE;
331  }
332  bool tResult;
333  if (tMarkTicksLong > 0) {
334  /*
335  * Here short and long mark durations found.
336  */
338  tResult = decodePulseDistanceWidthData(tNumberOfBitsForOneDecode, tStartIndex, tMarkMicrosLong, tMarkMicrosShort,
339  tSpaceMicrosShort, 0,
340 #if defined(USE_MSB_DECODING_FOR_DISTANCE_DECODER)
341  true
342 #else
343  false
344 #endif
345  );
346  } else {
347  /*
348  * Here short and long space durations found.
349  */
351  tResult = decodePulseDistanceWidthData(tNumberOfBitsForOneDecode, tStartIndex, tMarkMicrosShort, tMarkMicrosShort,
352  tSpaceMicrosLong, tSpaceMicrosShort,
353 #if defined(USE_MSB_DECODING_FOR_DISTANCE_DECODER)
354  true
355 #else
356  false
357 #endif
358  );
359  }
360  if (!tResult) {
361 #if defined(LOCAL_DEBUG)
362  Serial.print(F("PULSE_WIDTH: "));
363  Serial.println(F("Decode failed"));
364 #endif
365  return false;
366  }
367 #if defined(LOCAL_DEBUG)
368  Serial.print(F("PULSE_WIDTH: "));
369  Serial.print(F("decodedRawData=0x"));
370  Serial.println(decodedIRData.decodedRawData, HEX);
371 #endif
372  // fill array with decoded data
373  decodedIRData.decodedRawDataArray[i] = decodedIRData.decodedRawData;
374  tStartIndex += (2 * BITS_IN_RAW_DATA_TYPE);
375  tNumberOfBits -= BITS_IN_RAW_DATA_TYPE;
376  }
377 
378 #if defined(USE_MSB_DECODING_FOR_DISTANCE_DECODER)
380 #endif
381 
382  // Check for repeat
384 
385  /*
386  * Store timing data to reproduce frame for sending
387  */
388  decodedIRData.DistanceWidthTimingInfo.HeaderMarkMicros = (decodedIRData.rawDataPtr->rawbuf[1] * MICROS_PER_TICK);
389  decodedIRData.DistanceWidthTimingInfo.HeaderSpaceMicros = (decodedIRData.rawDataPtr->rawbuf[2] * MICROS_PER_TICK);
390  decodedIRData.DistanceWidthTimingInfo.ZeroMarkMicros = tMarkMicrosShort;
391  decodedIRData.DistanceWidthTimingInfo.ZeroSpaceMicros = tSpaceMicrosShort;
392  if (tMarkMicrosLong != 0) {
393  decodedIRData.DistanceWidthTimingInfo.OneMarkMicros = tMarkMicrosLong;
394 
395  decodedIRData.DistanceWidthTimingInfo.OneSpaceMicros = tSpaceMicrosShort;
396  if (tSpaceMicrosLong != 0) {
397  // Assume long space for zero when we have PulseDistanceWidth -> enables constant bit length
398  decodedIRData.DistanceWidthTimingInfo.ZeroSpaceMicros = tSpaceMicrosLong;
399  }
400  } else {
401  decodedIRData.DistanceWidthTimingInfo.OneMarkMicros = tMarkMicrosShort;
402 
403  // Here tMarkMicrosLong is 0 => tSpaceMicrosLong != 0
404  decodedIRData.DistanceWidthTimingInfo.OneSpaceMicros = tSpaceMicrosLong;
405  }
406 
407 #if defined(LOCAL_DEBUG)
408  Serial.print(F("DistanceWidthTimingInfo="));
409  IrReceiver.printDistanceWidthTimingInfo(&Serial, &decodedIRData.DistanceWidthTimingInfo);
410  Serial.println();
411 #endif
412  return true;
413 }
414 
416 #if defined(LOCAL_DEBUG)
417 #undef LOCAL_DEBUG
418 #endif
419 #endif // _IR_DISTANCE_WIDTH_HPP
MICROS_PER_TICK
#define MICROS_PER_TICK
microseconds per clock interrupt tick
Definition: IRremote.hpp:249
BITS_IN_RAW_DATA_TYPE
#define BITS_IN_RAW_DATA_TYPE
Definition: IRremoteInt.h:118
IRData::numberOfBits
uint16_t numberOfBits
Number of bits received for data (address + command + parity) - to determine protocol length if diffe...
Definition: IRProtocol.h:118
aggregateArrayCounts
bool aggregateArrayCounts(uint8_t aArray[], uint8_t aMaxIndex, uint8_t *aShortIndex, uint8_t *aLongIndex)
Definition: ir_DistanceWidthProtocol.hpp:113
IRrecv::decodeDistanceWidth
bool decodeDistanceWidth()
Definition: ir_DistanceWidthProtocol.hpp:152
IRData::rawlen
uint_fast8_t rawlen
counter of entries in rawbuf
Definition: IRProtocol.h:123
PULSE_WIDTH
@ PULSE_WIDTH
Definition: IRProtocol.h:42
irparams_struct::rawbuf
uint16_t rawbuf[RAW_BUFFER_LENGTH]
raw data / tick counts per mark/space, first entry is the length of the gap between previous and curr...
Definition: IRremoteInt.h:113
IRData::rawDataPtr
irparams_struct * rawDataPtr
Pointer of the raw timing data to be decoded. Mainly the OverflowFlag and the data buffer filled by r...
Definition: IRProtocol.h:129
IRData::decodedRawData
IRRawDataType decodedRawData
Up to 32/64 bit decoded raw data, to be used for send functions.
Definition: IRProtocol.h:112
PULSE_DISTANCE
@ PULSE_DISTANCE
Definition: IRProtocol.h:43
IRrecv::checkForRepeatSpaceTicksAndSetFlag
void checkForRepeatSpaceTicksAndSetFlag(uint16_t aMaximumRepeatSpaceTicks)
Definition: IRReceive.hpp:1042
IR_DEBUG_PRINT
#define IR_DEBUG_PRINT(...)
If DEBUG, print the arguments, otherwise do nothing.
Definition: IRremoteInt.h:137
IRrecv::decodePulseDistanceWidthData
bool decodePulseDistanceWidthData(PulseDistanceWidthProtocolConstants *aProtocolConstants, uint_fast8_t aNumberOfBits, uint_fast8_t aStartOffset=3)
Decode pulse distance protocols for PulseDistanceWidthProtocolConstants.
Definition: IRReceive.hpp:845
IRrecv::decodedIRData
IRData decodedIRData
Definition: IRremoteInt.h:321
IRData::flags
uint8_t flags
IRDATA_FLAGS_IS_REPEAT, IRDATA_FLAGS_WAS_OVERFLOW etc. See IRDATA_FLAGS_* definitions above.
Definition: IRProtocol.h:119
IRrecv::printDistanceWidthTimingInfo
void printDistanceWidthTimingInfo(Print *aSerial, DistanceWidthTimingInfoStruct *aDistanceWidthTimingInfo)
Definition: IRReceive.hpp:1268
IRDATA_FLAGS_IS_MSB_FIRST
#define IRDATA_FLAGS_IS_MSB_FIRST
Value is mainly determined by the (known) protocol.
Definition: IRProtocol.h:99
DURATION_ARRAY_SIZE
#define DURATION_ARRAY_SIZE
Definition: ir_DistanceWidthProtocol.hpp:69
IR_DEBUG_PRINTLN
#define IR_DEBUG_PRINTLN(...)
If DEBUG, print the arguments as a line, otherwise do nothing.
Definition: IRremoteInt.h:141
DISTANCE_WIDTH_MAXIMUM_REPEAT_DISTANCE_MICROS
#define DISTANCE_WIDTH_MAXIMUM_REPEAT_DISTANCE_MICROS
Definition: ir_DistanceWidthProtocol.hpp:59
IRData::protocol
decode_type_t protocol
UNKNOWN, NEC, SONY, RC5, PULSE_DISTANCE, ...
Definition: IRProtocol.h:108
IrReceiver
IRrecv IrReceiver
The receiver instance.
Definition: IRReceive.hpp:64