Line data Source code
1 : #include "audio/recorder/recorder.h"
2 : #include "audio/engine/audio_engine.h"
3 : #include <nlohmann/json.hpp>
4 : #include <iostream>
5 : #include <fstream>
6 : #include <ctime>
7 : #include <cstring>
8 :
9 : namespace Amplitron {
10 :
11 72 : void Recorder::write_wav_header() {
12 : // Write a placeholder WAV header (44 bytes)
13 : // Will be finalized when recording stops
14 24 : char header[44];
15 72 : std::memset(header, 0, 44);
16 :
17 72 : int byte_rate = sample_rate_ * channels_ * 2; // 16-bit = 2 bytes
18 72 : int block_align = channels_ * 2;
19 :
20 : // RIFF header
21 72 : std::memcpy(header + 0, "RIFF", 4);
22 : // Chunk size (placeholder — filled in finalize)
23 72 : std::memcpy(header + 8, "WAVE", 4);
24 :
25 : // fmt sub-chunk
26 72 : std::memcpy(header + 12, "fmt ", 4);
27 72 : int fmt_size = 16;
28 72 : std::memcpy(header + 16, &fmt_size, 4);
29 72 : int16_t audio_format = 1; // PCM
30 72 : std::memcpy(header + 20, &audio_format, 2);
31 72 : int16_t num_channels = static_cast<int16_t>(channels_);
32 72 : std::memcpy(header + 22, &num_channels, 2);
33 72 : std::memcpy(header + 24, &sample_rate_, 4);
34 72 : std::memcpy(header + 28, &byte_rate, 4);
35 72 : int16_t block_align_16 = static_cast<int16_t>(block_align);
36 72 : std::memcpy(header + 32, &block_align_16, 2);
37 72 : int16_t bits_per_sample = 16;
38 72 : std::memcpy(header + 34, &bits_per_sample, 2);
39 :
40 : // data sub-chunk
41 72 : std::memcpy(header + 36, "data", 4);
42 : // Data size (placeholder — filled in finalize)
43 :
44 72 : file_.write(header, 44);
45 72 : }
46 :
47 72 : void Recorder::finalize_wav_header() {
48 72 : if (!file_.is_open()) return;
49 :
50 72 : int64_t total_samples = samples_written_.load();
51 : // Clamp channels_ to a safe positive value before multiplication to prevent
52 : // signed overflow or negative data_size_64 if channels_ is zero or negative.
53 72 : int64_t safe_channels = (channels_ > 0 && channels_ <= 8)
54 96 : ? static_cast<int64_t>(channels_)
55 : : 1LL;
56 : // Compute data size in 64-bit to avoid overflow
57 72 : int64_t data_size_64 = total_samples * safe_channels * 2LL;
58 : // WAV format uses 32-bit sizes; clamp to max int32
59 : // Use uint32_t to match the WAV spec (unsigned 32-bit chunk sizes)
60 : // and avoid signed integer overflow when data_size approaches INT32_MAX.
61 : // Cap at 0xFFFFFFD8 to leave room for the 36-byte RIFF overhead.
62 72 : uint32_t data_size = static_cast<uint32_t>(
63 24 : (data_size_64 > static_cast<int64_t>(0xFFFFFFD8LL))
64 : ? 0xFFFFFFD8u
65 24 : : static_cast<uint32_t>(data_size_64));
66 72 : uint32_t riff_size = data_size + 36u;
67 :
68 : // Seek back and write correct sizes
69 72 : file_.seekp(4, std::ios::beg);
70 72 : file_.write(reinterpret_cast<const char*>(&riff_size), 4);
71 :
72 72 : file_.seekp(40, std::ios::beg);
73 72 : file_.write(reinterpret_cast<const char*>(&data_size), 4);
74 :
75 : // Seek to end
76 72 : file_.seekp(0, std::ios::end);
77 24 : }
78 :
79 9 : void Recorder::write_metadata(const std::string& wav_path, AudioEngine& engine) {
80 : // Write a JSON sidecar file with recording details
81 9 : std::string meta_path = wav_path;
82 : // Replace .wav with .meta.json
83 9 : size_t dot = meta_path.rfind('.');
84 9 : if (dot != std::string::npos) {
85 9 : meta_path = meta_path.substr(0, dot);
86 3 : }
87 9 : meta_path += ".meta.json";
88 :
89 9 : std::ofstream meta(meta_path);
90 9 : if (!meta.is_open()) {
91 0 : std::cerr << "Could not write metadata: " << meta_path << std::endl;
92 0 : return;
93 : }
94 :
95 9 : std::time_t now = std::time(nullptr);
96 3 : char timebuf[64];
97 3 : std::tm time_info;
98 : #ifdef _WIN32
99 3 : localtime_s(&time_info, &now);
100 : #else
101 6 : localtime_r(&now, &time_info);
102 : #endif
103 9 : std::strftime(timebuf, sizeof(timebuf), "%Y-%m-%dT%H:%M:%S", &time_info);
104 :
105 9 : float duration = get_duration();
106 :
107 : // Build the recording metadata object
108 9 : nlohmann::ordered_json recording = nlohmann::ordered_json::object();
109 9 : recording["filename"] = wav_path;
110 9 : recording["recorded_at"] = timebuf;
111 9 : recording["duration_seconds"] = duration;
112 12 : recording["total_samples"] = samples_written_.load();
113 9 : recording["format"] = "WAV PCM 16-bit";
114 9 : recording["sample_rate"] = sample_rate_;
115 9 : recording["channels"] = channels_;
116 9 : recording["bit_depth"] = 16;
117 :
118 : // Build the audio settings object
119 9 : nlohmann::ordered_json audio_settings = nlohmann::ordered_json::object();
120 9 : audio_settings["input_device"] = engine.get_input_device_name();
121 9 : audio_settings["output_device"] = engine.get_output_device_name();
122 9 : audio_settings["engine_sample_rate"] = engine.get_sample_rate();
123 9 : audio_settings["buffer_size"] = engine.get_buffer_size();
124 9 : audio_settings["input_gain"] = engine.get_input_gain();
125 9 : audio_settings["output_gain"] = engine.get_output_gain();
126 :
127 : // Build the signal chain array
128 9 : nlohmann::ordered_json signal_chain = nlohmann::ordered_json::array();
129 12 : for (auto& fx : engine.effects()) {
130 0 : nlohmann::ordered_json jfx = nlohmann::ordered_json::object();
131 0 : jfx["name"] = fx->name();
132 0 : jfx["enabled"] = fx->is_enabled();
133 0 : jfx["mix"] = fx->get_mix();
134 :
135 0 : nlohmann::ordered_json params_obj = nlohmann::ordered_json::object();
136 0 : for (auto& p : fx->params()) {
137 0 : params_obj[p.name] = p.value;
138 : }
139 0 : jfx["parameters"] = std::move(params_obj);
140 :
141 0 : signal_chain.push_back(std::move(jfx));
142 0 : }
143 :
144 : // Assemble root object
145 9 : nlohmann::ordered_json root = nlohmann::ordered_json::object();
146 9 : root["recording"] = std::move(recording);
147 9 : root["audio_settings"] = std::move(audio_settings);
148 9 : root["signal_chain"] = std::move(signal_chain);
149 :
150 12 : meta << root.dump(2) << "\n";
151 9 : meta.close();
152 12 : std::cout << "Metadata written: " << meta_path << std::endl;
153 9 : }
154 :
155 : } // namespace Amplitron
156 :
|