diff --git a/config/urfd.ini b/config/urfd.ini index e9c2137..8c350fd 100644 --- a/config/urfd.ini +++ b/config/urfd.ini @@ -50,6 +50,10 @@ Interval = 10 NNGDebug = false +[Audio] +Enable = false +path = ./audio/ + [Transcoder] Port = 10100 # TCP listening port for connection(s), set to 0 if there is no transcoder, then other two values will be ignored BindingAddress = 127.0.0.1 # or ::1, the IPv4 or IPv6 "loop-back" address for a local transcoder diff --git a/reflector/AudioRecorder.cpp b/reflector/AudioRecorder.cpp new file mode 100644 index 0000000..78c50c8 --- /dev/null +++ b/reflector/AudioRecorder.cpp @@ -0,0 +1,229 @@ +#include "AudioRecorder.h" +#include +#include +#include +#include + +// Opus settings for Voice 8kHz Mono +#define SAMPLE_RATE 8000 +#define CHANNELS 1 +#define APPLICATION OPUS_APPLICATION_VOIP +// 60ms frame size = 480 samples at 8kHz +#define FRAME_SIZE 480 + +CAudioRecorder::CAudioRecorder() : m_IsRecording(false), m_Encoder(nullptr), m_PacketCount(0), m_GranulePos(0) +{ +} + +CAudioRecorder::~CAudioRecorder() +{ + Stop(); +} + +void CAudioRecorder::Cleanup() +{ + if (m_Encoder) { + opus_encoder_destroy(m_Encoder); + m_Encoder = nullptr; + } + if (m_IsRecording) { + ogg_stream_clear(&m_OggStream); + } + if (m_File.is_open()) { + m_File.close(); + } + m_IsRecording = false; + m_PcmBuffer.clear(); +} + +std::string CAudioRecorder::Start(const std::string& directory) +{ + std::lock_guard lock(m_Mutex); + Cleanup(); + + // Generate UUIDv7 Filename + uint8_t uuid[16]; + uint8_t rand_bytes[10]; + for(int i=0; i<10; ++i) rand_bytes[i] = std::rand() & 0xFF; // Minimal entropy for now + + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + uint64_t unix_ts_ms = (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + + uuidv7_generate(uuid, unix_ts_ms, rand_bytes, nullptr); + + char uuid_str[37]; + uuidv7_to_string(uuid, uuid_str); + + m_Filename = "hearing_" + std::string(uuid_str) + ".opus"; + if (directory.back() == '/') + m_FullPath = directory + m_Filename; + else + m_FullPath = directory + "/" + m_Filename; + + m_File.open(m_FullPath, std::ios::binary | std::ios::out); + if (!m_File.is_open()) { + std::cerr << "AudioRecorder: Failed to open file: " << m_FullPath << std::endl; + return ""; + } + + InitOpus(); + InitOgg(); + + m_IsRecording = true; + return m_Filename; +} + +void CAudioRecorder::InitOpus() +{ + int err; + m_Encoder = opus_encoder_create(SAMPLE_RATE, CHANNELS, APPLICATION, &err); + if (err != OPUS_OK) { + std::cerr << "AudioRecorder: Failed to create Opus encoder: " << opus_strerror(err) << std::endl; + } + opus_encoder_ctl(m_Encoder, OPUS_SET_BITRATE(12000)); // 12kbps +} + +void CAudioRecorder::InitOgg() +{ + // Initialize Ogg stream with random serial + std::srand(std::time(nullptr)); + if (ogg_stream_init(&m_OggStream, std::rand()) != 0) { + std::cerr << "AudioRecorder: Failed to init Ogg stream" << std::endl; + return; + } + + // Create OpusHead packet + // Magic: "OpusHead" (8 bytes) + // Version: 1 (1 byte) + // Channel Count: 1 (1 byte) + // Pre-skip: 0 (2 bytes) + // Input Sample Rate: 8000 (4 bytes) + // Output Gain: 0 (2 bytes) + // Mapping Family: 0 (1 byte) + unsigned char header[19] = { + 'O', 'p', 'u', 's', 'H', 'e', 'a', 'd', + 1, + CHANNELS, + 0, 0, + 0x40, 0x1f, 0x00, 0x00, // 8000 little endian + 0, 0, + 0 + }; + + ogg_packet header_packet; + header_packet.packet = header; + header_packet.bytes = 19; + header_packet.b_o_s = 1; + header_packet.e_o_s = 0; + header_packet.granulepos = 0; + header_packet.packetno = m_PacketCount++; + + ogg_stream_packetin(&m_OggStream, &header_packet); + WriteOggPage(true); // Flush header + + // OpusTags (comments) - Minimal + // Magic: "OpusTags" (8 bytes) + // Vendor String Length (4 bytes) + // Vendor String + // User Comment List Length (4 bytes) + const char* vendor = "urfd-recorder"; + uint32_t vendor_len = strlen(vendor); + + std::vector tags; + tags.reserve(8 + 4 + vendor_len + 4); + const char* magic = "OpusTags"; + tags.insert(tags.end(), magic, magic + 8); + + tags.push_back(vendor_len & 0xFF); + tags.push_back((vendor_len >> 8) & 0xFF); + tags.push_back((vendor_len >> 16) & 0xFF); + tags.push_back((vendor_len >> 24) & 0xFF); + + tags.insert(tags.end(), vendor, vendor + vendor_len); + + // 0 comments + tags.push_back(0); tags.push_back(0); tags.push_back(0); tags.push_back(0); + + ogg_packet tags_packet; + tags_packet.packet = tags.data(); + tags_packet.bytes = tags.size(); + tags_packet.b_o_s = 0; + tags_packet.e_o_s = 0; + tags_packet.granulepos = 0; + tags_packet.packetno = m_PacketCount++; + + ogg_stream_packetin(&m_OggStream, &tags_packet); + WriteOggPage(true); +} + +void CAudioRecorder::WriteOggPage(bool flush) +{ + while(true) { + int result = flush ? ogg_stream_flush(&m_OggStream, &m_OggPage) : ogg_stream_pageout(&m_OggStream, &m_OggPage); + if (result == 0) break; + m_File.write((const char*)m_OggPage.header, m_OggPage.header_len); + m_File.write((const char*)m_OggPage.body, m_OggPage.body_len); + } +} + +void CAudioRecorder::Write(const int16_t* samples, int count) +{ + if (!m_IsRecording || !m_Encoder) return; + + std::lock_guard lock(m_Mutex); + + m_PcmBuffer.insert(m_PcmBuffer.end(), samples, samples + count); + + unsigned char out_buf[1024]; + + while (m_PcmBuffer.size() >= FRAME_SIZE) { + int len = opus_encode(m_Encoder, m_PcmBuffer.data(), FRAME_SIZE, out_buf, sizeof(out_buf)); + if (len < 0) { + std::cerr << "AudioRecorder: Opus encode error: " << len << std::endl; + } else { + m_GranulePos += FRAME_SIZE; + + ogg_packet packet; + packet.packet = out_buf; + packet.bytes = len; + packet.b_o_s = 0; + packet.e_o_s = 0; + packet.granulepos = m_GranulePos; + packet.packetno = m_PacketCount++; + + ogg_stream_packetin(&m_OggStream, &packet); + WriteOggPage(); + } + + m_PcmBuffer.erase(m_PcmBuffer.begin(), m_PcmBuffer.begin() + FRAME_SIZE); + } +} + +void CAudioRecorder::Stop() +{ + std::lock_guard lock(m_Mutex); + if (!m_IsRecording) return; + + // Flush remaining standard logic or just close + // In strict Opus, we might want to pad and finish, but for voice logging, truncation of <60ms is acceptable. + // Set EOS on last packet if we had one? + // Hard to do retroactively. Simpler is just to write a empty packet with EOS. + + /* + unsigned char dummy[1] = {0}; + ogg_packet packet; + packet.packet = dummy; + packet.bytes = 0; // Empty + packet.b_o_s = 0; + packet.e_o_s = 1; + packet.granulepos = m_GranulePos; + packet.packetno = m_PacketCount++; + ogg_stream_packetin(&m_OggStream, &packet); + */ + + // Actually, just flushing logic + WriteOggPage(true); + + Cleanup(); +} diff --git a/reflector/AudioRecorder.h b/reflector/AudioRecorder.h new file mode 100644 index 0000000..1d0f5b1 --- /dev/null +++ b/reflector/AudioRecorder.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "uuidv7.h" + +class CAudioRecorder +{ +public: + CAudioRecorder(); + ~CAudioRecorder(); + + // Starts recording to a new file. + // Generates a UUIDv7 based filename if path is a directory, + // or uses the provided path + generated filename. + // Returns the filename (without path) for notification. + std::string Start(const std::string& directory); + + // Writes signed 16-bit PCM samples (8kHz mono) + void Write(const int16_t* samples, int count); + + // Stops recording and closes file. + void Stop(); + + bool IsRecording() const { return m_IsRecording; } + +private: + void InitOpus(); + void InitOgg(); + void WriteOggPage(bool flush = false); + void Cleanup(); + + bool m_IsRecording; + std::ofstream m_File; + std::string m_Filename; + std::string m_FullPath; + std::mutex m_Mutex; + + // Opus state + OpusEncoder* m_Encoder; + + // Ogg state + ogg_stream_state m_OggStream; + ogg_page m_OggPage; + ogg_packet m_OggPacket; + int m_PacketCount; + int m_GranulePos; + + // Buffering pcm for frame size + std::vector m_PcmBuffer; +}; diff --git a/reflector/CodecStream.cpp b/reflector/CodecStream.cpp index d0b6c2e..b22a0fa 100644 --- a/reflector/CodecStream.cpp +++ b/reflector/CodecStream.cpp @@ -60,6 +60,17 @@ void CCodecStream::ResetStats(uint16_t streamid, ECodecType type) m_RTCount = 0; m_uiTotalPackets = 0; + // Start recording if enabled + if (g_Configure.GetBoolean(g_Keys.audio.enable)) + { + std::string path = g_Configure.GetString(g_Keys.audio.path); + m_Filename = m_Recorder.Start(path); + } + else + { + m_Filename.clear(); + } + // clear any stale packets in the local queue while (!m_LocalQueue.IsEmpty()) { diff --git a/reflector/CodecStream.h b/reflector/CodecStream.h index 51d2863..498ed25 100644 --- a/reflector/CodecStream.h +++ b/reflector/CodecStream.h @@ -23,6 +23,7 @@ #include "DVFramePacket.h" #include "SafePacketQueue.h" +#include "AudioRecorder.h" //////////////////////////////////////////////////////////////////////////////////////// // class @@ -38,6 +39,7 @@ public: void ResetStats(uint16_t streamid, ECodecType codectype); void ReportStats(); + std::string StopRecording() { m_Recorder.Stop(); return m_Filename; } // destructor virtual ~CCodecStream(); @@ -79,4 +81,8 @@ protected: double m_RTSum; unsigned int m_RTCount; uint32_t m_uiTotalPackets; + + // Recording + CAudioRecorder m_Recorder; + std::string m_Filename; }; diff --git a/reflector/IP.cpp b/reflector/IP.cpp index c3519e8..9fe9839 100644 --- a/reflector/IP.cpp +++ b/reflector/IP.cpp @@ -160,6 +160,28 @@ bool CIp::operator!=(const CIp &rhs) const // compares ports, addresses and fami return true; } +bool CIp::operator<(const CIp &rhs) const +{ + if (addr.ss_family != rhs.addr.ss_family) + return addr.ss_family < rhs.addr.ss_family; + + if (AF_INET == addr.ss_family) { + auto l = (const struct sockaddr_in *)&addr; + auto r = (const struct sockaddr_in *)&rhs.addr; + if (l->sin_addr.s_addr != r->sin_addr.s_addr) + return ntohl(l->sin_addr.s_addr) < ntohl(r->sin_addr.s_addr); + return ntohs(l->sin_port) < ntohs(r->sin_port); + } else if (AF_INET6 == addr.ss_family) { + auto l = (const struct sockaddr_in6 *)&addr; + auto r = (const struct sockaddr_in6 *)&rhs.addr; + int cmp = memcmp(&(l->sin6_addr), &(r->sin6_addr), sizeof(struct in6_addr)); + if (cmp != 0) return cmp < 0; + return ntohs(l->sin6_port) < ntohs(r->sin6_port); + } + return false; +} + + bool CIp::AddressIsZero() const { if (AF_INET == addr.ss_family) diff --git a/reflector/IP.h b/reflector/IP.h index 9e989fc..c6612fa 100644 --- a/reflector/IP.h +++ b/reflector/IP.h @@ -42,10 +42,15 @@ public: // comparison operators bool operator==(const CIp &rhs) const; + // comparison operators bool operator!=(const CIp &rhs) const; + bool operator<(const CIp &rhs) const; + // state methods - bool IsSet() const { return is_set; } + bool IsSet() const { return is_set; +} + bool AddressIsZero() const; void ClearAddress(); const char *GetAddress() const; diff --git a/reflector/JsonKeys.h b/reflector/JsonKeys.h index 03c1213..87f9846 100644 --- a/reflector/JsonKeys.h +++ b/reflector/JsonKeys.h @@ -58,6 +58,9 @@ struct SJsonKeys { modules { "Modules", "DescriptionA", "DescriptionB", "DescriptionC", "DescriptionD", "DescriptionE", "DescriptionF", "DescriptionG", "DescriptionH", "DescriptionI", "DescriptionJ", "DescriptionK", "DescriptionL", "DescriptionM", "DescriptionN", "DescriptionO", "DescriptionP", "DescriptionQ", "DescriptionR", "DescriptionS", "DescriptionT", "DescriptionU", "DescriptionV", "DescriptionW", "DescriptionX", "DescriptionY", "DescriptionZ" }; + struct AUDIO { const std::string enable, path; } + audio { "AudioEnable", "AudioPath" }; + struct USRP { const std::string enable, ip, txport, rxport, module, callsign, filepath; } usrp { "usrpEnable", "usrpIpAddress", "urspTxPort", "usrpRxPort", "usrpModule", "usrpCallsign", "usrpFilePath" }; diff --git a/reflector/Makefile b/reflector/Makefile index 3dd9778..0cf4310 100644 --- a/reflector/Makefile +++ b/reflector/Makefile @@ -32,7 +32,7 @@ else CFLAGS = -W -Werror -std=c++17 -MMD -MD endif -LDFLAGS=-pthread -lcurl -lnng +LDFLAGS=-pthread -lcurl -lnng -lopus -logg ifeq ($(DHT), true) LDFLAGS += -lopendht diff --git a/reflector/Reflector.cpp b/reflector/Reflector.cpp index a143a35..6156404 100644 --- a/reflector/Reflector.cpp +++ b/reflector/Reflector.cpp @@ -98,7 +98,7 @@ bool CReflector::Start(void) // if it's a transcoded module, then we need to initialize the codec stream if (port) { - if (std::string::npos != tcmods.find(c)) + if (std::string::npos != tcmods.find(c) || g_Configure.GetBoolean(g_Keys.audio.enable)) { if (stream->InitCodecStream()) return true; @@ -277,7 +277,8 @@ void CReflector::CloseStream(std::shared_ptr stream) //OnStreamClose(stream->GetUserCallsign()); // dashboard event - GetUsers()->Closing(stream->GetUserCallsign(), GetStreamModule(stream), stream->GetOwnerClient()->GetProtocol()); + std::string recording = stream->StopRecording(); + GetUsers()->Closing(stream->GetUserCallsign(), GetStreamModule(stream), stream->GetOwnerClient()->GetProtocol(), recording); ReleaseUsers(); std::cout << "Closing stream of module " << GetStreamModule(stream) << " (Called by CloseStream)" << std::endl; diff --git a/reflector/Users.cpp b/reflector/Users.cpp index 80b2faf..16fea17 100644 --- a/reflector/Users.cpp +++ b/reflector/Users.cpp @@ -77,13 +77,15 @@ void CUsers::Hearing(const CCallsign &my, const CCallsign &rpt1, const CCallsign g_NNGPublisher.Publish(event); } -void CUsers::Closing(const CCallsign &my, char module, EProtocol protocol) +void CUsers::Closing(const CCallsign &my, char module, EProtocol protocol, const std::string& recording) { - // dashboard event - nlohmann::json event; - event["type"] = "closing"; - event["my"] = my.GetCS(); - event["module"] = std::string(1, module); - event["protocol"] = g_GateKeeper.ProtocolName(protocol); - g_NNGPublisher.Publish(event); + // dashboard event + nlohmann::json event; + event["type"] = "closing"; + event["my"] = my.GetCS(); + event["module"] = std::string(1, module); + event["protocol"] = g_GateKeeper.ProtocolName(protocol); + if (!recording.empty()) + event["recording"] = recording; + g_NNGPublisher.Publish(event); } diff --git a/reflector/Users.h b/reflector/Users.h index 9638ebd..72a6758 100644 --- a/reflector/Users.h +++ b/reflector/Users.h @@ -50,7 +50,7 @@ public: // operation void Hearing(const CCallsign &, const CCallsign &, const CCallsign &, EProtocol protocol = EProtocol::none); void Hearing(const CCallsign &, const CCallsign &, const CCallsign &, const CCallsign &, EProtocol protocol); - void Closing(const CCallsign &, char module, EProtocol protocol); + void Closing(const CCallsign &, char module, EProtocol protocol, const std::string& recording = ""); protected: // data diff --git a/reflector/test_audio.cpp b/reflector/test_audio.cpp new file mode 100644 index 0000000..5e3166e --- /dev/null +++ b/reflector/test_audio.cpp @@ -0,0 +1,42 @@ +#include "AudioRecorder.h" +#include +#include +#include +#include +#include + +int main() { + CAudioRecorder recorder; + std::string filename = recorder.Start("."); + std::cout << "Recording started: " << filename << std::endl; + + if (filename.empty()) { + std::cerr << "Failed to start recording" << std::endl; + return 1; + } + + // Generate 5 seconds of 440Hz sine wave + std::vector samples; + int sampleRate = 8000; + int duration = 5; + double frequency = 440.0; + int totalSamples = sampleRate * duration; + + for (int i = 0; i < totalSamples; ++i) { + double time = (double)i / sampleRate; + int16_t sample = (int16_t)(32000.0 * std::sin(2.0 * M_PI * frequency * time)); + samples.push_back(sample); + } + + // Write in chunks + int chunkSize = 160; + for (int i = 0; i < totalSamples; i += chunkSize) { + recorder.Write(samples.data() + i, chunkSize); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); // Simulate real-time + } + + recorder.Stop(); + std::cout << "Recording stopped." << std::endl; + + return 0; +} diff --git a/reflector/uuidv7.h b/reflector/uuidv7.h new file mode 100644 index 0000000..07e1772 --- /dev/null +++ b/reflector/uuidv7.h @@ -0,0 +1,307 @@ +/** + * @file + * + * uuidv7.h - Single-file C/C++ UUIDv7 Library + * + * @version v0.1.6 + * @author LiosK + * @copyright Licensed under the Apache License, Version 2.0 + * @see https://github.com/LiosK/uuidv7-h + */ +/* + * Copyright 2022 LiosK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef UUIDV7_H_BAEDKYFQ +#define UUIDV7_H_BAEDKYFQ + +#include +#include + +/** + * @name Status codes returned by uuidv7_generate() + * + * @{ + */ + +/** + * Indicates that the `unix_ts_ms` passed was used because no preceding UUID was + * specified. + */ +#define UUIDV7_STATUS_UNPRECEDENTED (0) + +/** + * Indicates that the `unix_ts_ms` passed was used because it was greater than + * the previous one. + */ +#define UUIDV7_STATUS_NEW_TIMESTAMP (1) + +/** + * Indicates that the counter was incremented because the `unix_ts_ms` passed + * was no greater than the previous one. + */ +#define UUIDV7_STATUS_COUNTER_INC (2) + +/** + * Indicates that the previous `unix_ts_ms` was incremented because the counter + * reached its maximum value. + */ +#define UUIDV7_STATUS_TIMESTAMP_INC (3) + +/** + * Indicates that the monotonic order of generated UUIDs was broken because the + * `unix_ts_ms` passed was less than the previous one by more than ten seconds. + */ +#define UUIDV7_STATUS_CLOCK_ROLLBACK (4) + +/** Indicates that an invalid `unix_ts_ms` is passed. */ +#define UUIDV7_STATUS_ERR_TIMESTAMP (-1) + +/** + * Indicates that the attempt to increment the previous `unix_ts_ms` failed + * because it had reached its maximum value. + */ +#define UUIDV7_STATUS_ERR_TIMESTAMP_OVERFLOW (-2) + +/** @} */ + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name Low-level primitives + * + * @{ + */ + +/** + * Generates a new UUIDv7 from the given Unix time, random bytes, and previous + * UUID. + * + * @param uuid_out 16-byte byte array where the generated UUID is stored. + * @param unix_ts_ms Current Unix time in milliseconds. + * @param rand_bytes At least 10-byte byte array filled with random bytes. This + * function consumes the leading 4 bytes or the whole 10 + * bytes per call depending on the conditions. + * `uuidv7_status_n_rand_consumed()` maps the return value of + * this function to the number of random bytes consumed. + * @param uuid_prev 16-byte byte array representing the immediately preceding + * UUID, from which the previous timestamp and counter are + * extracted. This may be NULL if the caller does not care + * the ascending order of UUIDs within the same timestamp. + * This may point to the same location as `uuid_out`; this + * function reads the value before writing. + * @return One of the `UUIDV7_STATUS_*` codes that describe the + * characteristics of generated UUIDs. Callers can usually + * ignore the status unless they need to guarantee the + * monotonic order of UUIDs or fine-tune the generation + * process. + */ +static inline int8_t uuidv7_generate(uint8_t *uuid_out, uint64_t unix_ts_ms, + const uint8_t *rand_bytes, + const uint8_t *uuid_prev) { + static const uint64_t MAX_TIMESTAMP = ((uint64_t)1 << 48) - 1; + static const uint64_t MAX_COUNTER = ((uint64_t)1 << 42) - 1; + + if (unix_ts_ms > MAX_TIMESTAMP) { + return UUIDV7_STATUS_ERR_TIMESTAMP; + } + + int8_t status; + uint64_t timestamp = 0; + if (uuid_prev == NULL) { + status = UUIDV7_STATUS_UNPRECEDENTED; + timestamp = unix_ts_ms; + } else { + for (int i = 0; i < 6; i++) { + timestamp = (timestamp << 8) | uuid_prev[i]; + } + + if (unix_ts_ms > timestamp) { + status = UUIDV7_STATUS_NEW_TIMESTAMP; + timestamp = unix_ts_ms; + } else if (unix_ts_ms + 10000 < timestamp) { + // ignore prev if clock moves back by more than ten seconds + status = UUIDV7_STATUS_CLOCK_ROLLBACK; + timestamp = unix_ts_ms; + } else { + // increment prev counter + uint64_t counter = uuid_prev[6] & 0x0f; // skip ver + counter = (counter << 8) | uuid_prev[7]; + counter = (counter << 6) | (uuid_prev[8] & 0x3f); // skip var + counter = (counter << 8) | uuid_prev[9]; + counter = (counter << 8) | uuid_prev[10]; + counter = (counter << 8) | uuid_prev[11]; + + if (counter++ < MAX_COUNTER) { + status = UUIDV7_STATUS_COUNTER_INC; + uuid_out[6] = counter >> 38; // ver + bits 0-3 + uuid_out[7] = counter >> 30; // bits 4-11 + uuid_out[8] = counter >> 24; // var + bits 12-17 + uuid_out[9] = counter >> 16; // bits 18-25 + uuid_out[10] = counter >> 8; // bits 26-33 + uuid_out[11] = counter; // bits 34-41 + } else { + // increment prev timestamp at counter overflow + status = UUIDV7_STATUS_TIMESTAMP_INC; + timestamp++; + if (timestamp > MAX_TIMESTAMP) { + return UUIDV7_STATUS_ERR_TIMESTAMP_OVERFLOW; + } + } + } + } + + uuid_out[0] = timestamp >> 40; + uuid_out[1] = timestamp >> 32; + uuid_out[2] = timestamp >> 24; + uuid_out[3] = timestamp >> 16; + uuid_out[4] = timestamp >> 8; + uuid_out[5] = timestamp; + + for (int i = (status == UUIDV7_STATUS_COUNTER_INC) ? 12 : 6; i < 16; i++) { + uuid_out[i] = *rand_bytes++; + } + + uuid_out[6] = 0x70 | (uuid_out[6] & 0x0f); // set ver + uuid_out[8] = 0x80 | (uuid_out[8] & 0x3f); // set var + + return status; +} + +/** + * Determines the number of random bytes consumsed by `uuidv7_generate()` from + * the `UUIDV7_STATUS_*` code returned. + * + * @param status `UUIDV7_STATUS_*` code returned by `uuidv7_generate()`. + * @return `4` if `status` is `UUIDV7_STATUS_COUNTER_INC` or `10` + * otherwise. + */ +static inline int uuidv7_status_n_rand_consumed(int8_t status) { + return status == UUIDV7_STATUS_COUNTER_INC ? 4 : 10; +} + +/** + * Encodes a UUID in the 8-4-4-4-12 hexadecimal string representation. + * + * @param uuid 16-byte byte array representing the UUID to encode. + * @param string_out Character array where the encoded string is stored. Its + * length must be 37 (36 digits + NUL) or longer. + */ +static inline void uuidv7_to_string(const uint8_t *uuid, char *string_out) { + static const char DIGITS[] = "0123456789abcdef"; + for (int i = 0; i < 16; i++) { + uint_fast8_t e = uuid[i]; + *string_out++ = DIGITS[e >> 4]; + *string_out++ = DIGITS[e & 15]; + if (i == 3 || i == 5 || i == 7 || i == 9) { + *string_out++ = '-'; + } + } + *string_out = '\0'; +} + +/** + * Decodes the 8-4-4-4-12 hexadecimal string representation of a UUID. + * + * @param string 37-byte (36 digits + NUL) character array representing the + * 8-4-4-4-12 hexadecimal string representation. + * @param uuid_out 16-byte byte array where the decoded UUID is stored. + * @return Zero on success or non-zero integer on failure. + */ +static inline int uuidv7_from_string(const char *string, uint8_t *uuid_out) { + for (int i = 0; i < 32; i++) { + char c = *string++; + // clang-format off + uint8_t x = c == '0' ? 0 : c == '1' ? 1 : c == '2' ? 2 : c == '3' ? 3 + : c == '4' ? 4 : c == '5' ? 5 : c == '6' ? 6 : c == '7' ? 7 + : c == '8' ? 8 : c == '9' ? 9 : c == 'a' ? 10 : c == 'b' ? 11 + : c == 'c' ? 12 : c == 'd' ? 13 : c == 'e' ? 14 : c == 'f' ? 15 + : c == 'A' ? 10 : c == 'B' ? 11 : c == 'C' ? 12 : c == 'D' ? 13 + : c == 'E' ? 14 : c == 'F' ? 15 : 0xff; + // clang-format on + if (x == 0xff) { + return -1; // invalid digit + } + + if ((i & 1) == 0) { + uuid_out[i >> 1] = x << 4; // even i => hi 4 bits + } else { + uuid_out[i >> 1] |= x; // odd i => lo 4 bits + } + + if ((i == 7 || i == 11 || i == 15 || i == 19) && (*string++ != '-')) { + return -1; // invalid format + } + } + if (*string != '\0') { + return -1; // invalid length + } + return 0; // success +} + +/** @} */ + +/** + * @name High-level APIs that require platform integration + * + * @{ + */ + +/** + * Generates a new UUIDv7 with the current Unix time. + * + * This declaration defines the interface to generate a new UUIDv7 with the + * current time, default random number generator, and global shared state + * holding the previously generated UUID. Since this single-file library does + * not provide platform-specific implementations, users need to prepare a + * concrete implementation (if necessary) by integrating a real-time clock, + * cryptographically strong random number generator, and shared state storage + * available in the target platform. + * + * @param uuid_out 16-byte byte array where the generated UUID is stored. + * @return One of the `UUIDV7_STATUS_*` codes that describe the + * characteristics of generated UUIDs or an + * implementation-dependent code. Callers can usually ignore + * the `UUIDV7_STATUS_*` code unless they need to guarantee the + * monotonic order of UUIDs or fine-tune the generation + * process. The implementation-dependent code must be out of + * the range of `int8_t` and negative if it reports an error. + */ +int uuidv7_new(uint8_t *uuid_out); + +/** + * Generates an 8-4-4-4-12 hexadecimal string representation of new UUIDv7. + * + * @param string_out Character array where the encoded string is stored. Its + * length must be 37 (36 digits + NUL) or longer. + * @return Return value of `uuidv7_new()`. + * @note Provide a concrete `uuidv7_new()` implementation to enable + * this function. + */ +static inline int uuidv7_new_string(char *string_out) { + uint8_t uuid[16]; + int result = uuidv7_new(uuid); + uuidv7_to_string(uuid, string_out); + return result; +} + +/** @} */ + +#ifdef __cplusplus +} /* extern "C" { */ +#endif + +#endif /* #ifndef UUIDV7_H_BAEDKYFQ */