summaryrefslogtreecommitdiff
path: root/src/factories/naudio/v0/SampleFactory.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/factories/naudio/v0/SampleFactory.cpp')
-rw-r--r--src/factories/naudio/v0/SampleFactory.cpp218
1 files changed, 217 insertions, 1 deletions
diff --git a/src/factories/naudio/v0/SampleFactory.cpp b/src/factories/naudio/v0/SampleFactory.cpp
index c800df4..7f1898d 100644
--- a/src/factories/naudio/v0/SampleFactory.cpp
+++ b/src/factories/naudio/v0/SampleFactory.cpp
@@ -62,4 +62,220 @@ std::optional<std::shared_ptr<IParsedData>> SampleFactory::parse(std::vector<uin
}
AudioBankSample entry = AudioManager::Instance->get_aifc(id);
return std::make_shared<SampleData>(entry);
-} \ No newline at end of file
+}
+#ifdef BUILD_UI
+#include <cmath>
+#include <cstring>
+
+#include "ui/BaseBackend.h"
+#include "ui/ExportUtils.h"
+#include "ui/Widgets.h"
+
+namespace {
+
+struct DecodedSample {
+ std::string name;
+ std::vector<int16_t> pcm;
+ int rate = 0;
+ int channels = 1;
+};
+DecodedSample sDecoded; // last decoded sample (they can be large)
+std::string sPlayingName;
+std::unordered_map<std::string, float> sSampleSpeeds;
+
+uint32_t ReadU32BE(const uint8_t* p) {
+ return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
+}
+
+// 80-bit IEEE 754 extended float, as used by the AIFF COMM sample rate.
+double ReadExtended80(const uint8_t* p) {
+ const int exponent = (((p[0] & 0x7F) << 8) | p[1]) - 16383;
+ uint64_t mantissa = 0;
+ for (int i = 0; i < 8; ++i) {
+ mantissa = (mantissa << 8) | p[2 + i];
+ }
+ double value = (double)mantissa * std::ldexp(1.0, exponent - 63);
+ return (p[0] & 0x80) != 0 ? -value : value;
+}
+
+bool ParseAiff(const std::vector<char>& bytes, DecodedSample& out) {
+ const auto* data = (const uint8_t*)bytes.data();
+ const size_t size = bytes.size();
+ if (size < 12 || std::memcmp(data, "FORM", 4) != 0 || std::memcmp(data + 8, "AIFF", 4) != 0) {
+ return false;
+ }
+ uint32_t frames = 0;
+ int sampleBits = 16;
+ size_t pos = 12;
+ bool gotCommon = false, gotData = false;
+ while (pos + 8 <= size) {
+ const uint32_t ckSize = ReadU32BE(data + pos + 4);
+ const uint8_t* ck = data + pos + 8;
+ // The AIFC decoder over-declares the SSND size (the header overwrites
+ // the head of the sample data), so clamp instead of rejecting.
+ const size_t avail = std::min((size_t)ckSize, size - pos - 8);
+ if (std::memcmp(data + pos, "COMM", 4) == 0 && avail >= 18) {
+ out.channels = (int16_t)((ck[0] << 8) | ck[1]);
+ frames = ReadU32BE(ck + 2);
+ sampleBits = (int16_t)((ck[6] << 8) | ck[7]);
+ out.rate = (int)std::lround(ReadExtended80(ck + 8));
+ gotCommon = true;
+ } else if (std::memcmp(data + pos, "SSND", 4) == 0 && avail >= 8) {
+ const uint32_t offset = ReadU32BE(ck);
+ if (avail > 8 + offset) {
+ const size_t count = (avail - 8 - offset) / 2;
+ out.pcm.resize(count);
+ const uint8_t* smp = ck + 8 + offset;
+ for (size_t i = 0; i < count; ++i) {
+ out.pcm[i] = (int16_t)((smp[i * 2] << 8) | smp[i * 2 + 1]);
+ }
+ gotData = true;
+ }
+ }
+ pos += 8 + ckSize + (ckSize & 1);
+ }
+ (void)frames;
+ return gotCommon && gotData && sampleBits == 16 && out.rate > 0 && out.channels > 0 && !out.pcm.empty();
+}
+
+bool DecodeSample(const ParseResultData& item) {
+ if (sDecoded.name == item.name) {
+ return !sDecoded.pcm.empty();
+ }
+ sDecoded = {};
+ sDecoded.name = item.name;
+
+ auto sample = std::static_pointer_cast<SampleData>(item.data.value());
+ const bool ok = DecodeSampleToPcm(&sample->mSample, sDecoded.pcm, sDecoded.rate);
+ sDecoded.channels = 1;
+ return ok;
+}
+
+} // namespace
+
+bool DecodeAiffBytes(const std::vector<char>& bytes, std::vector<int16_t>& pcm, int& rate) {
+ DecodedSample decoded;
+ if (!ParseAiff(bytes, decoded)) {
+ return false;
+ }
+ pcm = std::move(decoded.pcm);
+ rate = decoded.rate;
+ return true;
+}
+
+bool DecodeSampleToPcm(AudioBankSample* sample, std::vector<int16_t>& pcm, int& rate) {
+ LUS::BinaryWriter aifc;
+ AudioConverter::SampleV0ToAIFC(sample, aifc);
+ LUS::BinaryWriter aiff;
+ write_aiff(aifc.ToVector(), aiff);
+ aifc.Close();
+
+ DecodedSample decoded;
+ const bool ok = ParseAiff(aiff.ToVector(), decoded);
+ aiff.Close();
+ if (!ok) {
+ return false;
+ }
+ // The converter's header overwrites the head of the decoded stream; pad the
+ // front back to the expected frame count so loop points stay aligned.
+ const size_t expected = sample->data.size() * 16 / 9;
+ if (decoded.pcm.size() < expected) {
+ decoded.pcm.insert(decoded.pcm.begin(), expected - decoded.pcm.size(), 0);
+ }
+ // Tunings are stored as floats, so derived rates land near the standard
+ // recording rates; snap when within 6%.
+ static const int kRates[] = { 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000 };
+ for (const int snap : kRates) {
+ if (std::fabs((float)decoded.rate - (float)snap) <= snap * 0.06f) {
+ decoded.rate = snap;
+ break;
+ }
+ }
+ pcm = std::move(decoded.pcm);
+ rate = decoded.rate;
+ return true;
+}
+
+float SampleFactoryUI::GetItemHeight(const ParseResultData&) {
+ return ImGui::GetTextLineHeightWithSpacing() * 3.0f + ImGui::GetFrameHeightWithSpacing() * 2.0f +
+ ImGui::GetStyle().ItemSpacing.y * 4.0f;
+}
+
+void SampleFactoryUI::DrawUI(const ParseResultData& item) {
+ UI::AssetHeader(item.name, item.type);
+ if (!item.data.has_value()) {
+ ImGui::TextDisabled("no data");
+ return;
+ }
+ const auto& sample = std::static_pointer_cast<SampleData>(item.data.value())->mSample;
+ ImGui::TextDisabled("sample \xe2\x80\x94 %zu bytes vadpcm, loop %u-%u (x%d), book order %d", sample.data.size(),
+ sample.loop.start, sample.loop.end, sample.loop.count, sample.book.order);
+
+ auto speedIt = sSampleSpeeds.emplace(item.name, 1.0f).first;
+ const bool playingThis = sPlayingName == item.name && UI::GetBackend()->AudioProgress() >= 0.0f;
+ if (ImGui::Button(playingThis ? "Stop##sample" : "Play##sample")) {
+ if (playingThis) {
+ UI::GetBackend()->StopAudio();
+ sPlayingName.clear();
+ } else if (DecodeSample(item)) {
+ if (UI::GetBackend()->PlaySamples(sDecoded.pcm.data(), sDecoded.pcm.size() / sDecoded.channels,
+ sDecoded.rate, sDecoded.channels)) {
+ sPlayingName = item.name;
+ UI::GetBackend()->SetAudioSpeed(speedIt->second);
+ }
+ }
+ }
+ ImGui::SameLine();
+ if (ImGui::Button("WAV##sampleexp")) {
+ if (DecodeSample(item)) {
+ const auto path = UI::ExportFilePath(item.name, "wav");
+ UI::NoteExport(item.name,
+ UI::WriteWavFile(path, sDecoded.pcm.data(), sDecoded.pcm.size() / sDecoded.channels,
+ sDecoded.channels, sDecoded.rate)
+ ? path.string()
+ : "export failed");
+ } else {
+ UI::NoteExport(item.name, "decode failed");
+ }
+ }
+ if (ImGui::IsItemHovered()) {
+ ImGui::SetTooltip("Export decoded sample to torch-exports/");
+ }
+ UI::DrawExportMarker(item.name);
+ ImGui::SameLine();
+ float volume = UI::GetBackend()->GetAudioVolume();
+ ImGui::SetNextItemWidth(140.0f);
+ if (ImGui::SliderFloat("##vol", &volume, 0.0f, 1.0f, "vol %.2f")) {
+ UI::GetBackend()->SetAudioVolume(volume);
+ }
+ ImGui::SameLine();
+ float& speed = speedIt->second;
+ ImGui::SetNextItemWidth(140.0f);
+ if (ImGui::SliderFloat("##speed", &speed, 0.25f, 4.0f, "%.2fx", ImGuiSliderFlags_Logarithmic)) {
+ // Snap near the common half/normal/double rates.
+ static const float kSnaps[] = { 0.5f, 1.0f, 2.0f };
+ for (const float snap : kSnaps) {
+ if (std::fabs(speed - snap) < 0.05f) {
+ speed = snap;
+ break;
+ }
+ }
+ if (playingThis) {
+ UI::GetBackend()->SetAudioSpeed(speed);
+ }
+ }
+ ImGui::SameLine();
+ if (sDecoded.name == item.name && !sDecoded.pcm.empty()) {
+ const float seconds = (float)sDecoded.pcm.size() / (float)(sDecoded.rate * sDecoded.channels);
+ ImGui::TextDisabled("%d Hz, %s, %.2fs", sDecoded.rate, sDecoded.channels == 1 ? "mono" : "stereo", seconds);
+ } else {
+ ImGui::TextDisabled("press play to decode");
+ }
+
+ float progress = playingThis ? std::max(UI::GetBackend()->AudioProgress(), 0.0f) : 0.0f;
+ ImGui::SetNextItemWidth(std::min(ImGui::GetContentRegionAvail().x, 420.0f));
+ if (ImGui::SliderFloat("##seek", &progress, 0.0f, 1.0f, "") && playingThis) {
+ UI::GetBackend()->SeekAudio(progress);
+ }
+}
+#endif // BUILD_UI