Fix JOC detection inside mp4/mkv containers
This commit is contained in:
@@ -1,12 +1,14 @@
|
|||||||
name: build
|
name: build
|
||||||
|
|
||||||
# Builds both architectures and publishes the two installable component packages.
|
# Builds both architectures, publishes the two installable component packages as
|
||||||
|
# workflow artifacts, and — when a tag is pushed — turns them into a GitHub release.
|
||||||
# The SDK is fetched from foobar2000.org at build time (tools/setup_sdk.ps1) and the
|
# The SDK is fetched from foobar2000.org at build time (tools/setup_sdk.ps1) and the
|
||||||
# renderer sources are part of this repository (kernel/), so nothing else is needed.
|
# renderer sources are part of this repository (kernel/), so nothing else is needed.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main, master ]
|
branches: [ main, master ]
|
||||||
|
tags: [ 'v*' ]
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -20,6 +22,16 @@ jobs:
|
|||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: ./tools/setup_sdk.ps1 -TargetVersion 80
|
run: ./tools/setup_sdk.ps1 -TargetVersion 80
|
||||||
|
|
||||||
|
- name: Check the tag against the component version
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$declared = (Select-String -Path src/main.cpp -Pattern '#define JOC_VERSION "([^"]+)"').Matches[0].Groups[1].Value
|
||||||
|
$tag = '${{ github.ref_name }}'
|
||||||
|
if ($tag -ne "v$declared") {
|
||||||
|
throw "the tag $tag does not match JOC_VERSION $declared; the release would be mislabelled"
|
||||||
|
}
|
||||||
|
|
||||||
- name: Build both architectures and package them
|
- name: Build both architectures and package them
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: ./tools/package.ps1
|
run: ./tools/package.ps1
|
||||||
@@ -41,3 +53,28 @@ jobs:
|
|||||||
name: foo_input_joc-x64
|
name: foo_input_joc-x64
|
||||||
path: dist/*-x64.fb2k-component
|
path: dist/*-x64.fb2k-component
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
release:
|
||||||
|
# A pushed tag is what makes a release; branch builds only leave artifacts.
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
needs: windows
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Collect the packaged components
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: dist
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: List what will be published
|
||||||
|
run: ls -l dist
|
||||||
|
|
||||||
|
- name: Create the release for this tag
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
name: ${{ github.ref_name }}
|
||||||
|
files: dist/*.fb2k-component
|
||||||
|
fail_on_unmatched_files: true
|
||||||
|
generate_release_notes: true
|
||||||
|
|||||||
@@ -9,16 +9,26 @@ install beside `foo_input_joc.dll`.
|
|||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
1. Reads the E-AC-3 syncframes and decides from the bitstream whether the file really
|
1. Finds the audio: a bare `.eac3` / `.ec3` stream is read as it is, while a container
|
||||||
carries JOC (an EMDF container holding both the OAMD and the JOC payload).
|
(`.mp4`, `.m4a`, `.m4b`, `.m4p`, `.m4r`, `.mov`, `.mkv`, `.mka`, `.webm`) is looked into
|
||||||
2. A file without JOC is handed back to foobar2000 with `exception_io_unsupported_format`,
|
first — the container's own headers say whether an E-AC-3 track is present and which
|
||||||
so the built-in E-AC-3 decoder plays it — this component never decodes plain E-AC-3.
|
audio track it is (MP4 sample entry `ec-3`, Matroska `CodecID A_EAC3`), and ffmpeg then
|
||||||
3. A JOC file is decoded as: the syncframes go to the renderer as metadata, the 5.1 core
|
copies that track out of the file byte for byte. The header walk is bounded and cheap, so
|
||||||
|
an MP4 holding AAC is declined without starting anything.
|
||||||
|
2. Decides from the bitstream whether it really carries JOC (an EMDF container holding both
|
||||||
|
the OAMD and the JOC payload — container metadata only ever says "E-AC-3", and the JOC
|
||||||
|
flag inside it is frequently missing).
|
||||||
|
3. A file with no E-AC-3 track, or with one that carries no JOC, is handed back to
|
||||||
|
foobar2000 with `exception_io_unsupported_format`, so the built-in decoder plays it — this
|
||||||
|
component never decodes plain AC-3 or E-AC-3.
|
||||||
|
4. A JOC file is decoded as: the syncframes go to the renderer as metadata, the 5.1 core
|
||||||
PCM comes from ffmpeg, and the renderer pairs them (one syncframe : 1536 bed samples)
|
PCM comes from ffmpeg, and the renderer pairs them (one syncframe : 1536 bed samples)
|
||||||
and produces the output PCM, which is handed back to foobar2000.
|
and produces the output PCM, which is handed back to foobar2000.
|
||||||
|
|
||||||
```
|
```
|
||||||
.eac3 file
|
.eac3 / .ec3 file container (.mp4 .mkv .m4a ...)
|
||||||
|
│ ├─ header walk (src/container_scan.cpp) ── no E-AC-3 ──▶ next decoder
|
||||||
|
│ └─ E-AC-3 track ── ffmpeg -c:a copy ──▶ syncframes
|
||||||
├─ JOC check (src/eac3_scan.cpp) ─── no JOC ──▶ built-in E-AC-3 decoder
|
├─ JOC check (src/eac3_scan.cpp) ─── no JOC ──▶ built-in E-AC-3 decoder
|
||||||
└─ JOC
|
└─ JOC
|
||||||
├─ syncframes ────────────────────▶ renderer metadata
|
├─ syncframes ────────────────────▶ renderer metadata
|
||||||
@@ -34,12 +44,13 @@ install beside `foo_input_joc.dll`.
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `kernel/` | Copy of the `joc_core` C++ sources (`include/` + `src/`) and `joc_kernel.vcxproj`, the static library the component links |
|
| `kernel/` | Copy of the `joc_core` C++ sources (`include/` + `src/`) and `joc_kernel.vcxproj`, the static library the component links |
|
||||||
| `src/eac3_scan.*` | Syncframe walk and the JOC bitstream test |
|
| `src/eac3_scan.*` | Syncframe walk and the JOC bitstream test |
|
||||||
|
| `src/container_scan.*` | Bounded header walk of MP4/MOV and Matroska: is there an E-AC-3 track, which one, and how long is the file |
|
||||||
| `src/joc_decode.*` | Decode engine: starts ffmpeg, drives the renderer, handles the end of stream. No foobar2000 headers, so it also builds into the offline tools |
|
| `src/joc_decode.*` | Decode engine: starts ffmpeg, drives the renderer, handles the end of stream. No foobar2000 headers, so it also builds into the offline tools |
|
||||||
| `src/input_joc.cpp` | The foobar2000 input: format recognition, yielding, `get_info`, `initialize`, `run` |
|
| `src/input_joc.cpp` | The foobar2000 input: format recognition, yielding, `get_info`, `initialize`, `run` |
|
||||||
| `src/settings.*` | Configuration values and their environment overrides (development only) |
|
| `src/settings.*` | Configuration values and their environment overrides (development only) |
|
||||||
| `src/prefs.cpp`, `src/prefs.rc` | The preferences page |
|
| `src/prefs.cpp`, `src/prefs.rc` | The preferences page |
|
||||||
| `src/log.*` | Diagnostic log written next to the DLL |
|
| `src/log.*` | Diagnostic log written next to the DLL |
|
||||||
| `tests/` | Offline tools: bitstream self-test and cross-check against the renderer, render harness, preferences-page layout check |
|
| `tests/` | Offline tools: bitstream self-test and cross-check against the renderer, render harness, preferences-page layout check, container-probe check |
|
||||||
| `tools/` | SDK fetch, build, package, deploy, unattended test bed run |
|
| `tools/` | SDK fetch, build, package, deploy, unattended test bed run |
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
@@ -70,6 +81,21 @@ Always let foobar2000 exit through `/exit`; a force-killed instance leaves a
|
|||||||
`<profile>\running` marker behind and the next start then refuses to load any user
|
`<profile>\running` marker behind and the next start then refuses to load any user
|
||||||
component.
|
component.
|
||||||
|
|
||||||
|
### Containers need one look at the decoder list
|
||||||
|
|
||||||
|
foobar2000 tries the decoders in the order shown in Preferences → **Decoding** (the
|
||||||
|
"list of available decoders", where entries can be moved up and down). The built-in
|
||||||
|
container readers are in that list too, and when one of them is offered an MP4 or Matroska
|
||||||
|
file before this component, it takes the file and the JOC objects are lost — the file plays
|
||||||
|
as plain E-AC-3.
|
||||||
|
|
||||||
|
So, to play JOC from a container, move **JOC decoder (E-AC-3 JOC)** above **foobar2000 MP4
|
||||||
|
Demuxer** and **foobar2000 Matroska/WebM Reader** in that list. Nothing else is needed, and
|
||||||
|
bare `.eac3` / `.ec3` files are unaffected by the order. This is the same thing every
|
||||||
|
third-party decoder (the FFmpeg wrapper, for one) asks for, which is why the component does
|
||||||
|
not try to work around it. If a container still plays as plain E-AC-3, that list is where to
|
||||||
|
look.
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
Preferences → Tools → **JOC decoder**:
|
Preferences → Tools → **JOC decoder**:
|
||||||
@@ -101,7 +127,10 @@ Development only: they override the stored settings for one run and every use is
|
|||||||
## Known limitations
|
## Known limitations
|
||||||
|
|
||||||
* ADM BWF output is not implemented.
|
* ADM BWF output is not implemented.
|
||||||
* Containers (`.m4a`, `.mkv`) are not claimed: only bare `.eac3` / `.ec3` streams.
|
* A container is only claimed when this component is ahead of the built-in container reader
|
||||||
|
in Preferences → Decoding, as described under [Install](#install); the core does not let a
|
||||||
|
decoder ask for a file another entry has already taken.
|
||||||
|
* Transport streams (`.ts`, `.m2ts`) are not claimed.
|
||||||
* The room tail is returned in full; the reference command-line renderer additionally trims
|
* The room tail is returned in full; the reference command-line renderer additionally trims
|
||||||
trailing samples below a threshold, so its output can be shorter.
|
trailing samples below a threshold, so its output can be shorter.
|
||||||
* x86 and x64 do not produce bit-identical binaural output (last-bit differences): the
|
* x86 and x64 do not produce bit-identical binaural output (last-bit differences): the
|
||||||
|
|||||||
@@ -113,3 +113,22 @@ disabled, which is what "the option is there but cannot be clicked" otherwise lo
|
|||||||
|
|
||||||
Not verified here: how the page and the `%joc_*%` fields look on screen; that needs a human
|
Not verified here: how the page and the `%joc_*%` fields look on screen; that needs a human
|
||||||
in front of the window.
|
in front of the window.
|
||||||
|
|
||||||
|
## Container support (mp4 / m4a / mov / mkv / mka / webm)
|
||||||
|
|
||||||
|
Verified with the Win32 build, a 5.1 speaker layout, and the component installed in a
|
||||||
|
portable foobar2000 1.6.19 profile:
|
||||||
|
|
||||||
|
- `ffmpeg -i <bare .eac3> -c:a copy` muxed into MP4 and Matroska, then `-map 0:a:0 -c:a copy
|
||||||
|
-f eac3` extracted again, is byte-identical to a direct `-t 30 -c:a copy` of the source
|
||||||
|
(same SHA-256) — the renderer therefore sees the stored syncframes, not a re-encode.
|
||||||
|
- The header probe (`tests/container_scan_test.cpp`, no foobar2000 involved) reports, for the
|
||||||
|
same material: `joc.mp4 -> mp4 eac3=1 audio#0 codec=ec-3 30.016 s`,
|
||||||
|
`joc.mkv -> matroska eac3=1 audio#0 codec=A_EAC3 30.016 s`, `plain_eac3.mp4 -> ec-3`,
|
||||||
|
`ac3.mp4 -> ac-3 (declined)`, `aac.mp4 -> mp4a (declined)`.
|
||||||
|
- End to end, with the component ordered ahead of the container reader in
|
||||||
|
Preferences -> Decoding: `open()` is called, the track is found, the JOC verdict is positive,
|
||||||
|
the file is claimed, and playback reaches `end of stream`.
|
||||||
|
- Negative case end to end: an MP4 holding E-AC-3 without JOC yields with
|
||||||
|
`E-AC-3 track 0 carries no JOC`, and the built-in decoder plays it.
|
||||||
|
- Bare `.eac3` / `.ec3` handling is unchanged.
|
||||||
|
|||||||
@@ -95,6 +95,7 @@
|
|||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ClCompile Include="src\container_scan.cpp" />
|
||||||
<ClCompile Include="src\eac3_scan.cpp" />
|
<ClCompile Include="src\eac3_scan.cpp" />
|
||||||
<ClCompile Include="src\input_joc.cpp" />
|
<ClCompile Include="src\input_joc.cpp" />
|
||||||
<ClCompile Include="src\joc_decode.cpp" />
|
<ClCompile Include="src\joc_decode.cpp" />
|
||||||
@@ -105,6 +106,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ClInclude Include="src\container_scan.h" />
|
||||||
<ClInclude Include="src\eac3_scan.h" />
|
<ClInclude Include="src\eac3_scan.h" />
|
||||||
<ClInclude Include="src\joc_decode.h" />
|
<ClInclude Include="src\joc_decode.h" />
|
||||||
<ClInclude Include="src\joc_pch.h" />
|
<ClInclude Include="src\joc_pch.h" />
|
||||||
|
|||||||
@@ -0,0 +1,528 @@
|
|||||||
|
#include "container_scan.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <cstdarg>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
|
namespace joc_container {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::wstring utf8_to_wide(const std::string& text) {
|
||||||
|
if (text.empty()) return {};
|
||||||
|
const int needed = MultiByteToWideChar(CP_UTF8, 0, text.c_str(),
|
||||||
|
static_cast<int>(text.size()), nullptr, 0);
|
||||||
|
std::wstring out(static_cast<std::size_t>(needed), L'\0');
|
||||||
|
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), static_cast<int>(text.size()), out.data(),
|
||||||
|
needed);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A bounded, read-only view of the file. Every accessor returns false instead of
|
||||||
|
// throwing, so a truncated or hostile file simply fails the probe.
|
||||||
|
class Window {
|
||||||
|
public:
|
||||||
|
bool open(const std::string& path) {
|
||||||
|
const std::wstring wide = utf8_to_wide(path);
|
||||||
|
handle_ = CreateFileW(wide.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||||
|
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||||
|
if (handle_ == INVALID_HANDLE_VALUE) {
|
||||||
|
handle_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LARGE_INTEGER size{};
|
||||||
|
if (GetFileSizeEx(handle_, &size) == FALSE) return false;
|
||||||
|
size_ = static_cast<std::uint64_t>(size.QuadPart);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
~Window() {
|
||||||
|
if (handle_ != nullptr) CloseHandle(handle_);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint64_t size() const { return size_; }
|
||||||
|
|
||||||
|
// Reads at an absolute offset; false when the range is not fully available.
|
||||||
|
bool read(std::uint64_t offset, void* destination, std::size_t bytes) const {
|
||||||
|
if (offset + bytes > size_) return false;
|
||||||
|
LARGE_INTEGER position{};
|
||||||
|
position.QuadPart = static_cast<LONGLONG>(offset);
|
||||||
|
if (SetFilePointerEx(handle_, position, nullptr, FILE_BEGIN) == FALSE) return false;
|
||||||
|
std::size_t done = 0;
|
||||||
|
auto* target = static_cast<std::uint8_t*>(destination);
|
||||||
|
while (done < bytes) {
|
||||||
|
const DWORD chunk = static_cast<DWORD>(
|
||||||
|
(bytes - done) > 0x10000u ? 0x10000u : (bytes - done));
|
||||||
|
DWORD got = 0;
|
||||||
|
if (ReadFile(handle_, target + done, chunk, &got, nullptr) == FALSE || got == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
done += got;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool load(std::uint64_t offset, std::size_t bytes, std::vector<std::uint8_t>* out) const {
|
||||||
|
out->assign(bytes, 0);
|
||||||
|
return bytes == 0 || read(offset, out->data(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
HANDLE handle_ = nullptr;
|
||||||
|
std::uint64_t size_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set JOC_SCAN_TRACE=1 to have the walk printed: the practical way to see where a
|
||||||
|
// container's structure stops matching what the probe expects.
|
||||||
|
bool trace_enabled() {
|
||||||
|
static const bool enabled = [] {
|
||||||
|
return GetEnvironmentVariableA("JOC_SCAN_TRACE", nullptr, 0) != 0;
|
||||||
|
}();
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void trace(const char* format, ...) {
|
||||||
|
if (!trace_enabled()) return;
|
||||||
|
va_list arguments;
|
||||||
|
va_start(arguments, format);
|
||||||
|
std::vfprintf(stdout, format, arguments);
|
||||||
|
va_end(arguments);
|
||||||
|
std::fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t be32(const std::uint8_t* p) {
|
||||||
|
return (static_cast<std::uint32_t>(p[0]) << 24) | (static_cast<std::uint32_t>(p[1]) << 16) |
|
||||||
|
(static_cast<std::uint32_t>(p[2]) << 8) | static_cast<std::uint32_t>(p[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string fourcc(const std::uint8_t* p) { return std::string(reinterpret_cast<const char*>(p), 4); }
|
||||||
|
|
||||||
|
bool is_audio_sample_entry(const std::string& format) {
|
||||||
|
// E-AC-3 is "ec-3"; "ac-3" is plain Dolby Digital and is *not* ours.
|
||||||
|
return format == "ec-3" || format == "ec3 " || format == "EAC3";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_ac3_sample_entry(const std::string& format) { return format == "ac-3" || format == "ac3 "; }
|
||||||
|
|
||||||
|
// --- MP4 / ISO base media -------------------------------------------------
|
||||||
|
|
||||||
|
// Walks the boxes inside [begin, end) and hands each to the visitor.
|
||||||
|
template <typename Fn>
|
||||||
|
bool walk_boxes(const Window& file, std::uint64_t begin, std::uint64_t end,
|
||||||
|
std::size_t depth, Fn visitor) {
|
||||||
|
std::uint64_t offset = begin;
|
||||||
|
std::uint8_t header[8];
|
||||||
|
while (offset + 8 <= end) {
|
||||||
|
if (!file.read(offset, header, sizeof(header))) return false;
|
||||||
|
std::uint64_t size = be32(header);
|
||||||
|
const std::string type = fourcc(header + 4);
|
||||||
|
std::uint64_t payload = offset + 8;
|
||||||
|
if (size == 1) {
|
||||||
|
std::uint8_t extended[8];
|
||||||
|
if (!file.read(offset + 8, extended, sizeof(extended))) return false;
|
||||||
|
size = (static_cast<std::uint64_t>(be32(extended)) << 32) | be32(extended + 4);
|
||||||
|
payload = offset + 16;
|
||||||
|
} else if (size == 0) {
|
||||||
|
size = end - offset; // "to the end of the file"
|
||||||
|
}
|
||||||
|
trace("%*sbox %s size=%llu\n", static_cast<int>(depth) * 2, "", type.c_str(), static_cast<unsigned long long>(size));
|
||||||
|
if (size < 8 || offset + size > end) return false;
|
||||||
|
if (!visitor(type, payload, offset + size, depth)) return false;
|
||||||
|
offset += size;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One trak: is it an audio track, and which sample entry does it use?
|
||||||
|
struct TrackInfo {
|
||||||
|
bool audio = false;
|
||||||
|
std::string format;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool scan_stbl(const Window& file, std::uint64_t begin, std::uint64_t end, TrackInfo* info) {
|
||||||
|
return walk_boxes(file, begin, end, 0, [&](const std::string& type, std::uint64_t payload,
|
||||||
|
std::uint64_t box_end, std::size_t) {
|
||||||
|
(void)box_end;
|
||||||
|
if (type == "stsd") {
|
||||||
|
std::uint8_t head[16];
|
||||||
|
if (file.read(payload, head, sizeof(head))) {
|
||||||
|
// version+flags(4) entry_count(4), then the first sample entry:
|
||||||
|
// size(4) format(4).
|
||||||
|
info->format = fourcc(head + 12);
|
||||||
|
trace("%*sstsd format=%s\n", 8, "", info->format.c_str());
|
||||||
|
}
|
||||||
|
return false; // one sample entry is all the probe needs
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// minf -> stbl -> stsd
|
||||||
|
bool scan_minf(const Window& file, std::uint64_t begin, std::uint64_t end, TrackInfo* info) {
|
||||||
|
return walk_boxes(file, begin, end, 0, [&](const std::string& type, std::uint64_t payload,
|
||||||
|
std::uint64_t box_end, std::size_t) {
|
||||||
|
if (type == "stbl") scan_stbl(file, payload, box_end, info);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// mdia: the track handler says whether this is audio, minf leads to the sample entry.
|
||||||
|
bool scan_mdia(const Window& file, std::uint64_t begin, std::uint64_t end, TrackInfo* info) {
|
||||||
|
return walk_boxes(file, begin, end, 0, [&](const std::string& type, std::uint64_t payload,
|
||||||
|
std::uint64_t box_end, std::size_t) {
|
||||||
|
if (type == "hdlr") {
|
||||||
|
std::uint8_t head[12];
|
||||||
|
if (file.read(payload, head, sizeof(head))) {
|
||||||
|
info->audio = fourcc(head + 8) == "soun";
|
||||||
|
trace("%*shdlr handler=%s audio=%d\n", 6, "", fourcc(head + 8).c_str(),
|
||||||
|
info->audio ? 1 : 0);
|
||||||
|
}
|
||||||
|
} else if (type == "minf") {
|
||||||
|
scan_minf(file, payload, box_end, info);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// trak -> mdia
|
||||||
|
bool scan_trak(const Window& file, std::uint64_t begin, std::uint64_t end, TrackInfo* info) {
|
||||||
|
return walk_boxes(file, begin, end, 0, [&](const std::string& type, std::uint64_t payload,
|
||||||
|
std::uint64_t box_end, std::size_t) {
|
||||||
|
if (type == "mdia") scan_mdia(file, payload, box_end, info);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Result scan_mp4(const Window& file, std::size_t max_bytes) {
|
||||||
|
Result result;
|
||||||
|
result.kind = Kind::kMp4;
|
||||||
|
|
||||||
|
// moov is usually at the start for streamed files and at the end otherwise.
|
||||||
|
struct Range {
|
||||||
|
std::uint64_t begin;
|
||||||
|
std::uint64_t end;
|
||||||
|
};
|
||||||
|
std::vector<Range> ranges{{0, (std::min<std::uint64_t>)(file.size(), max_bytes)}};
|
||||||
|
if (file.size() > max_bytes) {
|
||||||
|
ranges.push_back({file.size() - max_bytes, file.size()});
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned audio_seen = 0;
|
||||||
|
bool found_any_audio = false;
|
||||||
|
std::string first_audio_format;
|
||||||
|
for (const Range& range : ranges) {
|
||||||
|
walk_boxes(file, range.begin, range.end, 0,
|
||||||
|
[&](const std::string& type, std::uint64_t payload, std::uint64_t box_end,
|
||||||
|
std::size_t) {
|
||||||
|
if (type != "moov") return true;
|
||||||
|
walk_boxes(file, payload, box_end, 1,
|
||||||
|
[&](const std::string& inner, std::uint64_t inner_payload,
|
||||||
|
std::uint64_t inner_end, std::size_t) {
|
||||||
|
if (inner == "mvhd") {
|
||||||
|
// version(1) flags(3) then either
|
||||||
|
// creation/modification/timescale/duration (32-bit)
|
||||||
|
// or the 64-bit variant.
|
||||||
|
std::uint8_t header[32];
|
||||||
|
if (file.read(inner_payload, header, sizeof(header))) {
|
||||||
|
const bool wide = header[0] == 1;
|
||||||
|
const std::uint32_t timescale =
|
||||||
|
wide ? be32(header + 20) : be32(header + 12);
|
||||||
|
const std::uint64_t duration =
|
||||||
|
wide ? ((static_cast<std::uint64_t>(
|
||||||
|
be32(header + 24))
|
||||||
|
<< 32) |
|
||||||
|
be32(header + 28))
|
||||||
|
: be32(header + 16);
|
||||||
|
if (timescale != 0) {
|
||||||
|
result.duration_seconds =
|
||||||
|
static_cast<double>(duration) /
|
||||||
|
static_cast<double>(timescale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (inner != "trak") return true;
|
||||||
|
TrackInfo info;
|
||||||
|
scan_trak(file, inner_payload, inner_end, &info);
|
||||||
|
if (!info.audio) return true;
|
||||||
|
if (!found_any_audio) {
|
||||||
|
found_any_audio = true;
|
||||||
|
first_audio_format = info.format;
|
||||||
|
}
|
||||||
|
if (is_audio_sample_entry(info.format)) {
|
||||||
|
result.eac3 = true;
|
||||||
|
result.audio_index = audio_seen;
|
||||||
|
result.codec = info.format;
|
||||||
|
} else if (is_ac3_sample_entry(info.format)) {
|
||||||
|
result.codec = info.format;
|
||||||
|
}
|
||||||
|
++audio_seen;
|
||||||
|
return !result.eac3; // stop once found
|
||||||
|
});
|
||||||
|
return !result.eac3;
|
||||||
|
});
|
||||||
|
if (result.eac3) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.eac3) {
|
||||||
|
result.detail = "mp4: E-AC-3 audio track " + std::to_string(result.audio_index);
|
||||||
|
} else if (found_any_audio) {
|
||||||
|
result.detail = "mp4: first audio track is " +
|
||||||
|
(first_audio_format.empty() ? std::string("unknown")
|
||||||
|
: first_audio_format);
|
||||||
|
} else {
|
||||||
|
result.detail = "mp4: no audio track found in the scanned window";
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Matroska / WebM ------------------------------------------------------
|
||||||
|
|
||||||
|
struct EbmlReader {
|
||||||
|
const std::uint8_t* data = nullptr;
|
||||||
|
std::size_t size = 0;
|
||||||
|
std::size_t position = 0;
|
||||||
|
|
||||||
|
bool at_end() const { return position >= size; }
|
||||||
|
|
||||||
|
// Element IDs keep their leading bits; sizes drop the marker bit.
|
||||||
|
bool read_id(std::uint64_t* id) {
|
||||||
|
if (at_end()) return false;
|
||||||
|
const std::uint8_t first = data[position];
|
||||||
|
int length = 1;
|
||||||
|
for (int bit = 0x80; bit != 0; bit >>= 1, ++length) {
|
||||||
|
if ((first & bit) != 0) break;
|
||||||
|
}
|
||||||
|
if (length > 4 || position + static_cast<std::size_t>(length) > size) return false;
|
||||||
|
std::uint64_t value = 0;
|
||||||
|
for (int i = 0; i < length; ++i) value = (value << 8) | data[position + i];
|
||||||
|
position += static_cast<std::size_t>(length);
|
||||||
|
*id = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool read_size(std::uint64_t* value, bool* unknown) {
|
||||||
|
*unknown = false;
|
||||||
|
if (at_end()) return false;
|
||||||
|
const std::uint8_t first = data[position];
|
||||||
|
int length = 1;
|
||||||
|
for (int bit = 0x80; bit != 0; bit >>= 1, ++length) {
|
||||||
|
if ((first & bit) != 0) break;
|
||||||
|
}
|
||||||
|
if (length > 8 || position + static_cast<std::size_t>(length) > size) return false;
|
||||||
|
std::uint64_t result = first & (0xFFu >> length);
|
||||||
|
bool all_ones = (first & (0xFFu >> length)) == (0xFFu >> length);
|
||||||
|
for (int i = 1; i < length; ++i) {
|
||||||
|
const std::uint8_t byte = data[position + i];
|
||||||
|
all_ones = all_ones && byte == 0xFFu;
|
||||||
|
result = (result << 8) | byte;
|
||||||
|
}
|
||||||
|
position += static_cast<std::size_t>(length);
|
||||||
|
*unknown = all_ones;
|
||||||
|
*value = result;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Result scan_matroska(const Window& file, std::size_t max_bytes) {
|
||||||
|
Result result;
|
||||||
|
result.kind = Kind::kMatroska;
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> data;
|
||||||
|
if (!file.load(0, static_cast<std::size_t>(
|
||||||
|
(std::min<std::uint64_t>)(file.size(), max_bytes)),
|
||||||
|
&data)) {
|
||||||
|
result.detail = "matroska: cannot read the header";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned audio_seen = 0;
|
||||||
|
std::string first_audio_codec;
|
||||||
|
bool saw_tracks = false;
|
||||||
|
bool found = false;
|
||||||
|
double timestamp_scale_ns = 1000000.0; // Matroska default: 1 ms per tick
|
||||||
|
double duration_ticks = 0.0;
|
||||||
|
|
||||||
|
// Only the two levels that matter are walked: Tracks, then TrackEntry.
|
||||||
|
auto walk = [&](std::size_t begin, std::size_t end, int depth,
|
||||||
|
auto&& self) -> void {
|
||||||
|
EbmlReader reader{data.data(), end, begin};
|
||||||
|
while (!reader.at_end() && !found) {
|
||||||
|
const std::size_t element_start = reader.position;
|
||||||
|
std::uint64_t id = 0;
|
||||||
|
std::uint64_t size = 0;
|
||||||
|
bool unknown = false;
|
||||||
|
if (!reader.read_id(&id) || !reader.read_size(&size, &unknown)) break;
|
||||||
|
std::size_t payload = reader.position;
|
||||||
|
std::size_t payload_end =
|
||||||
|
unknown ? end : (std::min<std::size_t>)(end, payload + static_cast<std::size_t>(size));
|
||||||
|
if (payload_end < payload) break;
|
||||||
|
|
||||||
|
switch (id) {
|
||||||
|
case 0x1654AE6Bu: // Tracks
|
||||||
|
saw_tracks = true;
|
||||||
|
self(payload, payload_end, depth + 1, self);
|
||||||
|
break;
|
||||||
|
case 0x1549A966u: { // Info: carries the segment duration
|
||||||
|
EbmlReader inner{data.data(), payload_end, payload};
|
||||||
|
while (!inner.at_end()) {
|
||||||
|
std::uint64_t child_id = 0;
|
||||||
|
std::uint64_t child_size = 0;
|
||||||
|
bool child_unknown = false;
|
||||||
|
if (!inner.read_id(&child_id) ||
|
||||||
|
!inner.read_size(&child_size, &child_unknown)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const std::size_t child_payload = inner.position;
|
||||||
|
const std::size_t child_end =
|
||||||
|
child_unknown ? payload_end
|
||||||
|
: (std::min<std::size_t>)(
|
||||||
|
payload_end,
|
||||||
|
child_payload + static_cast<std::size_t>(child_size));
|
||||||
|
// TimestampScale (ns per tick, default 1 ms) and Duration (ticks).
|
||||||
|
if (child_id == 0x2AD7B1u && child_size >= 1 && child_size <= 8) {
|
||||||
|
std::uint64_t scale = 0;
|
||||||
|
for (std::size_t i = 0; i < child_size; ++i) {
|
||||||
|
scale = (scale << 8) | data[child_payload + i];
|
||||||
|
}
|
||||||
|
if (scale != 0) timestamp_scale_ns = scale;
|
||||||
|
} else if (child_id == 0x4489u && (child_size == 4 || child_size == 8)) {
|
||||||
|
// Duration is a big-endian float: 4 or 8 bytes, both occur.
|
||||||
|
const std::uint8_t* field = data.data() + child_payload;
|
||||||
|
if (child_size == 4) {
|
||||||
|
const std::uint32_t bits = be32(field);
|
||||||
|
float value = 0.0f;
|
||||||
|
std::memcpy(&value, &bits, sizeof(value));
|
||||||
|
duration_ticks = static_cast<double>(value);
|
||||||
|
} else {
|
||||||
|
const std::uint64_t bits =
|
||||||
|
(static_cast<std::uint64_t>(be32(field)) << 32) | be32(field + 4);
|
||||||
|
double value = 0.0;
|
||||||
|
std::memcpy(&value, &bits, sizeof(value));
|
||||||
|
duration_ticks = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inner.position = child_end;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0xAEu: { // TrackEntry
|
||||||
|
// Read this track's type and codec.
|
||||||
|
std::uint64_t track_type = 0;
|
||||||
|
std::string codec;
|
||||||
|
EbmlReader inner{data.data(), payload_end, payload};
|
||||||
|
while (!inner.at_end()) {
|
||||||
|
std::uint64_t child_id = 0;
|
||||||
|
std::uint64_t child_size = 0;
|
||||||
|
bool child_unknown = false;
|
||||||
|
if (!inner.read_id(&child_id) ||
|
||||||
|
!inner.read_size(&child_size, &child_unknown)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const std::size_t child_payload = inner.position;
|
||||||
|
const std::size_t child_end =
|
||||||
|
child_unknown ? payload_end
|
||||||
|
: (std::min<std::size_t>)(
|
||||||
|
payload_end,
|
||||||
|
child_payload + static_cast<std::size_t>(child_size));
|
||||||
|
if (child_id == 0x83u && child_size >= 1) { // TrackType
|
||||||
|
track_type = data[child_payload];
|
||||||
|
} else if (child_id == 0x86u) { // CodecID
|
||||||
|
codec.assign(reinterpret_cast<const char*>(data.data() + child_payload),
|
||||||
|
child_end - child_payload);
|
||||||
|
}
|
||||||
|
inner.position = child_end;
|
||||||
|
}
|
||||||
|
if (track_type == 2u) { // audio
|
||||||
|
if (first_audio_codec.empty()) first_audio_codec = codec;
|
||||||
|
if (codec == "A_EAC3") {
|
||||||
|
result.eac3 = true;
|
||||||
|
result.audio_index = audio_seen;
|
||||||
|
result.codec = codec;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
++audio_seen;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x18538067u: // Segment: descend, the size may be unknown
|
||||||
|
self(payload, payload_end, depth + 1, self);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (found) break;
|
||||||
|
reader.position = payload_end;
|
||||||
|
if (payload_end == element_start) break; // no progress: stop
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(0, data.size(), 0, walk);
|
||||||
|
|
||||||
|
if (duration_ticks > 0.0 && timestamp_scale_ns > 0.0) {
|
||||||
|
result.duration_seconds = duration_ticks * timestamp_scale_ns / 1.0e9;
|
||||||
|
}
|
||||||
|
if (result.eac3) {
|
||||||
|
result.detail = "matroska: E-AC-3 audio track " + std::to_string(result.audio_index);
|
||||||
|
} else if (saw_tracks) {
|
||||||
|
result.detail = "matroska: first audio track is " +
|
||||||
|
(first_audio_codec.empty() ? std::string("unknown") : first_audio_codec);
|
||||||
|
} else {
|
||||||
|
result.detail = "matroska: no track list in the scanned window";
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const char* kind_name(Kind kind) {
|
||||||
|
switch (kind) {
|
||||||
|
case Kind::kMp4: return "mp4";
|
||||||
|
case Kind::kMatroska: return "matroska";
|
||||||
|
default: return "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_container_extension(const char* extension) {
|
||||||
|
if (extension == nullptr) return false;
|
||||||
|
static const char* const kExtensions[] = {"mp4", "m4a", "m4b", "m4p", "m4r",
|
||||||
|
"mov", "mkv", "mka", "webm"};
|
||||||
|
for (const char* candidate : kExtensions) {
|
||||||
|
if (_stricmp(extension, candidate) == 0) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result scan(const std::string& path, std::size_t max_bytes) {
|
||||||
|
Result result;
|
||||||
|
Window file;
|
||||||
|
if (!file.open(path)) {
|
||||||
|
result.detail = "cannot open the file";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (file.size() < 16) {
|
||||||
|
result.detail = "file too small to be a container";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint8_t head[16];
|
||||||
|
if (!file.read(0, head, sizeof(head))) {
|
||||||
|
result.detail = "cannot read the file header";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// Matroska starts with the EBML header element (0x1A45DFA3); ISO base media
|
||||||
|
// starts with a box whose type is at offset 4 ("ftyp" for a normal file).
|
||||||
|
if (head[0] == 0x1A && head[1] == 0x45 && head[2] == 0xDF && head[3] == 0xA3) {
|
||||||
|
result = scan_matroska(file, max_bytes);
|
||||||
|
} else {
|
||||||
|
result = scan_mp4(file, max_bytes);
|
||||||
|
}
|
||||||
|
joc_log::line("container: %s -> %s (eac3=%d audio#%u codec=%s)", path.c_str(),
|
||||||
|
result.detail.c_str(), result.eac3 ? 1 : 0, result.audio_index,
|
||||||
|
result.codec.empty() ? "-" : result.codec.c_str());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace joc_container
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Cheap structural probe of container files, to answer one question: does this
|
||||||
|
// file hold an E-AC-3 audio track, and if so which one?
|
||||||
|
//
|
||||||
|
// It exists so that a Media Library scan does not pay for an ffmpeg launch on
|
||||||
|
// every MP4 in the collection. The probe reads a bounded window of the file and
|
||||||
|
// walks only the headers it needs:
|
||||||
|
//
|
||||||
|
// MP4 / MOV top-level boxes -> moov -> trak -> hdlr(mdia) + stsd sample entry.
|
||||||
|
// moov may sit at the end (no faststart), so the tail is checked too.
|
||||||
|
// Matroska EBML header -> Segment -> Tracks -> TrackEntry -> CodecID.
|
||||||
|
//
|
||||||
|
// The E-AC-3 bitstream itself is never demuxed here: ffmpeg extracts it when the
|
||||||
|
// file is actually decoded, which keeps this file small and lets every container
|
||||||
|
// ffmpeg understands work without a demuxer of our own.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace joc_container {
|
||||||
|
|
||||||
|
enum class Kind {
|
||||||
|
kNone,
|
||||||
|
kMp4,
|
||||||
|
kMatroska,
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* kind_name(Kind kind);
|
||||||
|
|
||||||
|
struct Result {
|
||||||
|
Kind kind = Kind::kNone;
|
||||||
|
bool eac3 = false; // an E-AC-3 audio track is present
|
||||||
|
unsigned audio_index = 0; // which audio track it is, 0-based
|
||||||
|
double duration_seconds = 0.0; // from the container header, 0 when not stated
|
||||||
|
std::string codec; // codec identifier found, for the log
|
||||||
|
std::string detail;
|
||||||
|
};
|
||||||
|
|
||||||
|
// True when the extension belongs to a container this component looks inside.
|
||||||
|
bool is_container_extension(const char* extension);
|
||||||
|
|
||||||
|
// Reads at most max_bytes of header (plus the same amount at the end for MP4) and
|
||||||
|
// reports what it found. Missing files, empty files and unknown containers come
|
||||||
|
// back as Kind::kNone with a detail string.
|
||||||
|
Result scan(const std::string& path, std::size_t max_bytes = 4u * 1024u * 1024u);
|
||||||
|
|
||||||
|
} // namespace joc_container
|
||||||
+104
-24
@@ -18,6 +18,7 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "container_scan.h"
|
||||||
#include "eac3_scan.h"
|
#include "eac3_scan.h"
|
||||||
#include "joc_decode.h"
|
#include "joc_decode.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
@@ -62,6 +63,24 @@ public:
|
|||||||
input_open_file_helper(source, path, reason, abort);
|
input_open_file_helper(source, path, reason, abort);
|
||||||
m_file = source;
|
m_file = source;
|
||||||
|
|
||||||
|
// The core passes URLs ("file://C:\..."). ffmpeg and the file APIs need a
|
||||||
|
// native path, and a URL we cannot map to one is a file we cannot decode.
|
||||||
|
try {
|
||||||
|
m_native_path = filesystem::g_get_native_path(m_path.c_str());
|
||||||
|
} catch (const pfc::exception& error) {
|
||||||
|
joc_log::line("decoder: not a native file path (%s): %s", m_path.c_str(),
|
||||||
|
error.what());
|
||||||
|
throw exception_io_unsupported_format();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* extension = std::strrchr(m_native_path.get_ptr(), '.');
|
||||||
|
extension = (extension != nullptr) ? extension + 1 : "";
|
||||||
|
|
||||||
|
if (joc_container::is_container_extension(extension)) {
|
||||||
|
open_container(extension);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
pfc::array_t<t_uint8> buffer;
|
pfc::array_t<t_uint8> buffer;
|
||||||
buffer.set_size(kSniffBytes);
|
buffer.set_size(kSniffBytes);
|
||||||
const std::size_t got = m_file->read(buffer.get_ptr(), kSniffBytes, abort);
|
const std::size_t got = m_file->read(buffer.get_ptr(), kSniffBytes, abort);
|
||||||
@@ -78,18 +97,43 @@ public:
|
|||||||
joc_log::line("decoder: yielding to the built-in decoder (%s)", scan.detail);
|
joc_log::line("decoder: yielding to the built-in decoder (%s)", scan.detail);
|
||||||
throw exception_io_unsupported_format();
|
throw exception_io_unsupported_format();
|
||||||
}
|
}
|
||||||
joc_log::line("decoder: claiming this file as E-AC-3 JOC");
|
joc_log::line("decoder: claiming this file as E-AC-3 JOC (bare stream)");
|
||||||
|
m_input_kind = joc_decode::InputKind::kBare;
|
||||||
|
}
|
||||||
|
|
||||||
// The core passes URLs ("file://C:\..."). ffmpeg and the file APIs need a
|
// Two questions, in this order: is there an E-AC-3 track inside (answered from
|
||||||
// native path, and a URL we cannot map to one is a file we cannot decode.
|
// the container's own headers, so a library scan pays nothing for the MP4s that
|
||||||
try {
|
// hold AAC), and does that track carry JOC (answered from a copied prefix of
|
||||||
m_native_path = filesystem::g_get_native_path(m_path.c_str());
|
// the track). Either "no" hands the file to the next decoder in the table.
|
||||||
} catch (const pfc::exception& error) {
|
void open_container(const char* extension) {
|
||||||
joc_log::line("decoder: not a native file path (%s): %s", m_path.c_str(),
|
m_container = joc_container::scan(m_native_path.get_ptr());
|
||||||
error.what());
|
if (!m_container.eac3) {
|
||||||
|
joc_log::line("decoder: yielding to the built-in decoder (%s)",
|
||||||
|
m_container.detail.c_str());
|
||||||
throw exception_io_unsupported_format();
|
throw exception_io_unsupported_format();
|
||||||
}
|
}
|
||||||
joc_log::line("decoder: native path \"%s\"", m_native_path.get_ptr());
|
|
||||||
|
const joc_decode::Settings settings = joc_settings::current();
|
||||||
|
bool joc = false;
|
||||||
|
std::string detail;
|
||||||
|
if (!joc_decode::probe_container_joc(settings.ffmpeg_path, m_native_path.get_ptr(),
|
||||||
|
m_container.audio_index, &joc, &detail)) {
|
||||||
|
joc_log::line("decoder: cannot examine the E-AC-3 track (%s); yielding",
|
||||||
|
detail.c_str());
|
||||||
|
throw exception_io_unsupported_format();
|
||||||
|
}
|
||||||
|
if (!joc) {
|
||||||
|
joc_log::line("decoder: yielding to the built-in decoder (E-AC-3 track %u carries "
|
||||||
|
"no JOC: %s)",
|
||||||
|
m_container.audio_index, detail.c_str());
|
||||||
|
throw exception_io_unsupported_format();
|
||||||
|
}
|
||||||
|
|
||||||
|
joc_log::line("decoder: claiming this file as E-AC-3 JOC (%s, audio track %u, .%s)",
|
||||||
|
joc_container::kind_name(m_container.kind), m_container.audio_index,
|
||||||
|
extension);
|
||||||
|
m_input_kind = joc_decode::InputKind::kContainer;
|
||||||
|
m_audio_index = m_container.audio_index;
|
||||||
}
|
}
|
||||||
|
|
||||||
void get_info(file_info& info, abort_callback& abort) {
|
void get_info(file_info& info, abort_callback& abort) {
|
||||||
@@ -97,6 +141,17 @@ public:
|
|||||||
const joc_decode::FileProbe probe = joc_decode::probe_file(m_native_path.get_ptr());
|
const joc_decode::FileProbe probe = joc_decode::probe_file(m_native_path.get_ptr());
|
||||||
const joc_decode::Settings settings = joc_settings::current();
|
const joc_decode::Settings settings = joc_settings::current();
|
||||||
|
|
||||||
|
// A container knows its own duration even though the E-AC-3 syncframes are
|
||||||
|
// not directly addressable in the file.
|
||||||
|
const bool container = (m_input_kind == joc_decode::InputKind::kContainer);
|
||||||
|
const double duration = (container && m_container.duration_seconds > 0.0)
|
||||||
|
? m_container.duration_seconds
|
||||||
|
: probe.duration_seconds;
|
||||||
|
const std::uint64_t frames =
|
||||||
|
probe.frames != 0
|
||||||
|
? probe.frames
|
||||||
|
: ((duration > 0.0) ? static_cast<std::uint64_t>(duration * 48000.0 / 1536.0) : 0);
|
||||||
|
|
||||||
unsigned channels = 2;
|
unsigned channels = 2;
|
||||||
std::string render;
|
std::string render;
|
||||||
if (settings.output == joc_decode::Output::kBinaural) {
|
if (settings.output == joc_decode::Output::kBinaural) {
|
||||||
@@ -115,12 +170,12 @@ public:
|
|||||||
info.info_set_int("channels", channels);
|
info.info_set_int("channels", channels);
|
||||||
info.info_set_int("bitspersample", 32);
|
info.info_set_int("bitspersample", 32);
|
||||||
info.info_set("bitspersample_extra", "floating-point");
|
info.info_set("bitspersample_extra", "floating-point");
|
||||||
info.set_length(probe.duration_seconds);
|
info.set_length(duration);
|
||||||
if (probe.duration_seconds > 0.0) {
|
if (duration > 0.0) {
|
||||||
const t_filesize bytes = m_file.is_valid() ? m_file->get_size(abort) : filesize_invalid;
|
const t_filesize bytes = m_file.is_valid() ? m_file->get_size(abort) : filesize_invalid;
|
||||||
if (bytes != filesize_invalid && bytes > 0) {
|
if (bytes != filesize_invalid && bytes > 0) {
|
||||||
info.info_set_bitrate(static_cast<t_int64>(
|
info.info_set_bitrate(static_cast<t_int64>(
|
||||||
static_cast<double>(bytes) * 8.0 / probe.duration_seconds / 1000.0));
|
static_cast<double>(bytes) * 8.0 / duration / 1000.0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Custom fields: visible in Properties and usable as %joc_*% in title
|
// Custom fields: visible in Properties and usable as %joc_*% in title
|
||||||
@@ -129,11 +184,19 @@ public:
|
|||||||
info.info_set("joc_hrtf", settings.hrtf_file.empty()
|
info.info_set("joc_hrtf", settings.hrtf_file.empty()
|
||||||
? "(未设置)"
|
? "(未设置)"
|
||||||
: file_name_of(settings.hrtf_file).c_str());
|
: file_name_of(settings.hrtf_file).c_str());
|
||||||
info.info_set_int("joc_frames", static_cast<t_int64>(probe.frames));
|
info.info_set_int("joc_frames", static_cast<t_int64>(frames));
|
||||||
info.info_set("joc_scan", probe.detail.c_str());
|
info.info_set("joc_scan", container ? m_container.detail.c_str() : probe.detail.c_str());
|
||||||
joc_log::line("decoder: get_info duration=%.3f s frames=%llu channels=%u render=%s",
|
if (container) {
|
||||||
probe.duration_seconds, static_cast<unsigned long long>(probe.frames),
|
char text[128] = {};
|
||||||
channels, render.c_str());
|
std::snprintf(text, sizeof(text), "%s (%s, audio track %u)",
|
||||||
|
joc_container::kind_name(m_container.kind),
|
||||||
|
m_container.codec.empty() ? "E-AC-3" : m_container.codec.c_str(),
|
||||||
|
m_container.audio_index);
|
||||||
|
info.info_set("joc_container", text);
|
||||||
|
}
|
||||||
|
joc_log::line("decoder: get_info duration=%.3f s frames=%llu channels=%u render=%s%s",
|
||||||
|
duration, static_cast<unsigned long long>(frames), channels, render.c_str(),
|
||||||
|
container ? " (container)" : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
t_filestats2 get_stats2(uint32_t flags, abort_callback& abort) {
|
t_filestats2 get_stats2(uint32_t flags, abort_callback& abort) {
|
||||||
@@ -144,6 +207,8 @@ public:
|
|||||||
void decode_initialize(unsigned flags, abort_callback& abort) {
|
void decode_initialize(unsigned flags, abort_callback& abort) {
|
||||||
(void)abort;
|
(void)abort;
|
||||||
m_settings = joc_settings::current();
|
m_settings = joc_settings::current();
|
||||||
|
m_settings.input_kind = m_input_kind;
|
||||||
|
m_settings.audio_index = m_audio_index;
|
||||||
joc_log::line("decoder: initialize flags=0x%X settings: %s", flags,
|
joc_log::line("decoder: initialize flags=0x%X settings: %s", flags,
|
||||||
joc_settings::describe(m_settings).c_str());
|
joc_settings::describe(m_settings).c_str());
|
||||||
|
|
||||||
@@ -224,18 +289,28 @@ public:
|
|||||||
|
|
||||||
static bool g_is_our_path(const char* path, const char* extension) {
|
static bool g_is_our_path(const char* path, const char* extension) {
|
||||||
(void)path;
|
(void)path;
|
||||||
// Claim by extension, then decide from the bitstream in open(): a file that
|
// Claim by extension, then decide from the contents in open(): a file whose
|
||||||
// turns out not to carry JOC is handed back with
|
// audio turns out not to be E-AC-3 JOC is handed back with
|
||||||
// exception_io_unsupported_format, and the core moves on to the next
|
// exception_io_unsupported_format, and the core moves on to the next decoder
|
||||||
// decoder in its priority table.
|
// in its priority table. Containers are included because an E-AC-3 JOC
|
||||||
|
// track is commonly wrapped in MP4 or Matroska; the container walk in
|
||||||
|
// open() is what keeps the other files cheap to decline.
|
||||||
|
if (joc_container::is_container_extension(extension)) return true;
|
||||||
return (extension != nullptr) &&
|
return (extension != nullptr) &&
|
||||||
((stricmp_utf8(extension, "eac3") == 0) || (stricmp_utf8(extension, "ec3") == 0));
|
((stricmp_utf8(extension, "eac3") == 0) || (stricmp_utf8(extension, "ec3") == 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool g_is_our_content_type(const char* content_type) {
|
static bool g_is_our_content_type(const char* content_type) {
|
||||||
(void)content_type;
|
if (content_type == nullptr) return false;
|
||||||
// Container dispatch (MP4 ec-3, Matroska A_EAC3) is not claimed: only bare
|
// E-AC-3 content types, and only the E-AC-3 ones: the generic Dolby aliases
|
||||||
// E-AC-3 streams are handled, so let the container readers have them.
|
// are deliberately left alone, because a track the core can already decode
|
||||||
|
// must not end up claimed by an entry that will not decode it.
|
||||||
|
static const char* const kTypes[] = {"audio/eac3", "audio/eac3joc", "audio/ec3",
|
||||||
|
"audio/x-eac3", "E-AC-3", "eac3",
|
||||||
|
"ec3"};
|
||||||
|
for (const char* candidate : kTypes) {
|
||||||
|
if (stricmp_utf8(content_type, candidate) == 0) return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +325,11 @@ private:
|
|||||||
pfc::string8 m_native_path;
|
pfc::string8 m_native_path;
|
||||||
joc_decode::Settings m_settings;
|
joc_decode::Settings m_settings;
|
||||||
joc_decode::Engine m_engine;
|
joc_decode::Engine m_engine;
|
||||||
|
// Set by open(): a bare stream is read directly, an E-AC-3 track inside a
|
||||||
|
// container is extracted by ffmpeg before it reaches the renderer.
|
||||||
|
joc_decode::InputKind m_input_kind = joc_decode::InputKind::kBare;
|
||||||
|
unsigned m_audio_index = 0;
|
||||||
|
joc_container::Result m_container;
|
||||||
std::vector<float> m_buffer;
|
std::vector<float> m_buffer;
|
||||||
unsigned m_channels = 2;
|
unsigned m_channels = 2;
|
||||||
std::uint64_t m_frames_delivered = 0;
|
std::uint64_t m_frames_delivered = 0;
|
||||||
|
|||||||
+154
-23
@@ -88,19 +88,24 @@ const char* error_text(const CoreApi& api, joc_error code) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// ffmpeg child process producing the 5.1 core PCM on a pipe.
|
// ffmpeg child process producing the 5.1 core PCM on a pipe.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
class BedProcess {
|
class FfmpegPipe {
|
||||||
public:
|
public:
|
||||||
~BedProcess() { stop(); }
|
~FfmpegPipe() { stop(); }
|
||||||
|
|
||||||
|
// ffmpeg_path, the input file and whatever should follow "-i <input>" are
|
||||||
|
// separate: the 5.1 bed and the E-AC-3 metadata stream of a container file are
|
||||||
|
// both ffmpeg output, they only differ in those arguments.
|
||||||
bool start(const std::string& ffmpeg_path, const std::string& input_path,
|
bool start(const std::string& ffmpeg_path, const std::string& input_path,
|
||||||
const std::wstring& stderr_path, std::string* error) {
|
const std::wstring& output_arguments, const char* label,
|
||||||
|
const std::wstring& stderr_path, std::string* error,
|
||||||
|
std::size_t pipe_bytes = kBedPipeBytes) {
|
||||||
SECURITY_ATTRIBUTES attributes{};
|
SECURITY_ATTRIBUTES attributes{};
|
||||||
attributes.nLength = sizeof(attributes);
|
attributes.nLength = sizeof(attributes);
|
||||||
attributes.bInheritHandle = TRUE;
|
attributes.bInheritHandle = TRUE;
|
||||||
HANDLE read_end = nullptr;
|
HANDLE read_end = nullptr;
|
||||||
HANDLE write_end = nullptr;
|
HANDLE write_end = nullptr;
|
||||||
if (CreatePipe(&read_end, &write_end, &attributes, static_cast<DWORD>(kBedPipeBytes)) == FALSE) {
|
if (CreatePipe(&read_end, &write_end, &attributes, static_cast<DWORD>(pipe_bytes)) == FALSE) {
|
||||||
if (error != nullptr) *error = "cannot create the core PCM pipe";
|
if (error != nullptr) *error = std::string("cannot create the ") + label + " pipe";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Only the child's end is inheritable.
|
// Only the child's end is inheritable.
|
||||||
@@ -115,10 +120,8 @@ public:
|
|||||||
std::wstring command = L"\"" + utf8_to_wide(ffmpeg_path) + L"\"";
|
std::wstring command = L"\"" + utf8_to_wide(ffmpeg_path) + L"\"";
|
||||||
command += L" -hide_banner -loglevel error -nostdin -y -i \"";
|
command += L" -hide_banner -loglevel error -nostdin -y -i \"";
|
||||||
command += utf8_to_wide(input_path);
|
command += utf8_to_wide(input_path);
|
||||||
// Identical to the reference CLI's core decode: 5.1 interleaved float32
|
command += L"\" ";
|
||||||
// at 48 kHz, which is the layout the rendering core expects
|
command += output_arguments;
|
||||||
// (L R C LFE Ls Rs).
|
|
||||||
command += L"\" -map 0:a:0 -vn -ac 6 -ar 48000 -c:a pcm_f32le -f f32le -";
|
|
||||||
|
|
||||||
STARTUPINFOW startup{};
|
STARTUPINFOW startup{};
|
||||||
startup.cb = sizeof(startup);
|
startup.cb = sizeof(startup);
|
||||||
@@ -138,13 +141,13 @@ public:
|
|||||||
if (error_file != INVALID_HANDLE_VALUE) CloseHandle(error_file);
|
if (error_file != INVALID_HANDLE_VALUE) CloseHandle(error_file);
|
||||||
if (created == FALSE) {
|
if (created == FALSE) {
|
||||||
CloseHandle(read_end);
|
CloseHandle(read_end);
|
||||||
if (error != nullptr) *error = "cannot start ffmpeg for the 5.1 core PCM";
|
if (error != nullptr) *error = std::string("cannot start ffmpeg for the ") + label;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
CloseHandle(process.hThread);
|
CloseHandle(process.hThread);
|
||||||
pipe_ = read_end;
|
pipe_ = read_end;
|
||||||
process_ = process.hProcess;
|
process_ = process.hProcess;
|
||||||
joc_log::line("core bed: ffmpeg started (pid %lu)", process.dwProcessId);
|
joc_log::line("core %s: ffmpeg started (pid %lu)", label, process.dwProcessId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,15 +344,109 @@ FileProbe probe_file(const std::string& path, std::size_t max_scan_bytes) {
|
|||||||
return probe;
|
return probe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// JOC verdict for a container file, without rendering anything.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
bool probe_container_joc(const std::string& ffmpeg_path, const std::string& path,
|
||||||
|
unsigned audio_index, bool* joc, std::string* detail) {
|
||||||
|
struct Entry {
|
||||||
|
std::string path;
|
||||||
|
unsigned audio_index = 0;
|
||||||
|
std::uint64_t size = 0;
|
||||||
|
std::uint64_t modified = 0;
|
||||||
|
bool joc = false;
|
||||||
|
std::string detail;
|
||||||
|
};
|
||||||
|
static std::vector<Entry> cache;
|
||||||
|
|
||||||
|
std::uint64_t size = 0;
|
||||||
|
std::uint64_t modified = 0;
|
||||||
|
{
|
||||||
|
WIN32_FILE_ATTRIBUTE_DATA data{};
|
||||||
|
if (GetFileAttributesExW(utf8_to_wide(path).c_str(), GetFileExInfoStandard, &data) !=
|
||||||
|
FALSE) {
|
||||||
|
size = (static_cast<std::uint64_t>(data.nFileSizeHigh) << 32) | data.nFileSizeLow;
|
||||||
|
modified = (static_cast<std::uint64_t>(data.ftLastWriteTime.dwHighDateTime) << 32) |
|
||||||
|
data.ftLastWriteTime.dwLowDateTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const Entry& entry : cache) {
|
||||||
|
if (entry.path == path && entry.audio_index == audio_index && entry.size == size &&
|
||||||
|
entry.modified == modified) {
|
||||||
|
if (joc != nullptr) *joc = entry.joc;
|
||||||
|
if (detail != nullptr) *detail = entry.detail;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ffmpeg_path.empty()) {
|
||||||
|
if (detail != nullptr) *detail = "no ffmpeg configured";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FfmpegPipe pipe;
|
||||||
|
std::string error;
|
||||||
|
std::wstring arguments = L"-map 0:a:";
|
||||||
|
arguments += std::to_wstring(audio_index);
|
||||||
|
arguments += L" -vn -t 3 -c:a copy -f eac3 -";
|
||||||
|
const std::wstring stderr_path = [] {
|
||||||
|
wchar_t temp[MAX_PATH + 1] = {};
|
||||||
|
const DWORD length = GetTempPathW(MAX_PATH, temp);
|
||||||
|
return length == 0 ? std::wstring(L"NUL")
|
||||||
|
: std::wstring(temp) + L"joc_container_probe.log";
|
||||||
|
}();
|
||||||
|
if (!pipe.start(ffmpeg_path, path, arguments, "probe", stderr_path, &error, 1u << 20)) {
|
||||||
|
if (detail != nullptr) *detail = error;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> prefix(512u * 1024u);
|
||||||
|
std::size_t filled = 0;
|
||||||
|
while (filled < prefix.size()) {
|
||||||
|
const std::size_t got = pipe.read(prefix.data() + filled, prefix.size() - filled);
|
||||||
|
if (got == 0) break;
|
||||||
|
filled += got;
|
||||||
|
}
|
||||||
|
pipe.stop();
|
||||||
|
|
||||||
|
const joc_eac3::ScanResult scan = joc_eac3::scan(prefix.data(), filled, 8);
|
||||||
|
const bool carries_joc = (scan.joc == joc_eac3::JocState::kYes);
|
||||||
|
|
||||||
|
Entry entry;
|
||||||
|
entry.path = path;
|
||||||
|
entry.audio_index = audio_index;
|
||||||
|
entry.size = size;
|
||||||
|
entry.modified = modified;
|
||||||
|
entry.joc = carries_joc;
|
||||||
|
entry.detail = scan.detail;
|
||||||
|
if (cache.size() >= 8) cache.erase(cache.begin());
|
||||||
|
cache.push_back(entry);
|
||||||
|
|
||||||
|
joc_log::line("container: %s track %u -> %s (%s)", path.c_str(), audio_index,
|
||||||
|
carries_joc ? "JOC" : "not JOC", scan.detail);
|
||||||
|
if (joc != nullptr) *joc = carries_joc;
|
||||||
|
if (detail != nullptr) *detail = scan.detail;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Engine
|
// Engine
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
struct Engine::Impl {
|
struct Engine::Impl {
|
||||||
CoreApi api;
|
CoreApi api;
|
||||||
joc_stream* stream = nullptr;
|
joc_stream* stream = nullptr;
|
||||||
InputFile eac3;
|
InputFile eac3; // bare E-AC-3 input: read straight from the file
|
||||||
BedProcess bed;
|
FfmpegPipe eac3_pipe; // E-AC-3 inside a container: ffmpeg extracts the stream
|
||||||
|
bool eac3_from_pipe = false;
|
||||||
|
FfmpegPipe bed;
|
||||||
Settings settings;
|
Settings settings;
|
||||||
|
|
||||||
|
// The metadata stream comes either from the file itself or from ffmpeg.
|
||||||
|
std::size_t read_eac3(void* destination, std::size_t bytes) {
|
||||||
|
return eac3_from_pipe ? eac3_pipe.read(destination, bytes)
|
||||||
|
: eac3.read(destination, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
unsigned channels = 0;
|
unsigned channels = 0;
|
||||||
std::uint64_t frames_queued = 0; // E-AC-3 frames handed to the core
|
std::uint64_t frames_queued = 0; // E-AC-3 frames handed to the core
|
||||||
std::uint64_t bed_frames_pushed = 0;
|
std::uint64_t bed_frames_pushed = 0;
|
||||||
@@ -382,7 +479,9 @@ void Engine::stop() {
|
|||||||
impl.stream = nullptr;
|
impl.stream = nullptr;
|
||||||
}
|
}
|
||||||
impl.bed.stop();
|
impl.bed.stop();
|
||||||
|
impl.eac3_pipe.stop();
|
||||||
impl.eac3.close();
|
impl.eac3.close();
|
||||||
|
impl.eac3_from_pipe = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Engine::start(const std::string& input_path, const Settings& settings, std::string* error) {
|
bool Engine::start(const std::string& input_path, const Settings& settings, std::string* error) {
|
||||||
@@ -402,9 +501,15 @@ bool Engine::start(const std::string& input_path, const Settings& settings, std:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!impl.eac3.open(input_path)) {
|
// Metadata stream: a bare E-AC-3 file is read directly, while the E-AC-3 track
|
||||||
if (error != nullptr) *error = "cannot open the input file";
|
// of a container is extracted by ffmpeg (stream copy, so the syncframes reach
|
||||||
return false;
|
// the renderer exactly as stored).
|
||||||
|
impl.eac3_from_pipe = (settings.input_kind == InputKind::kContainer);
|
||||||
|
if (!impl.eac3_from_pipe) {
|
||||||
|
if (!impl.eac3.open(input_path)) {
|
||||||
|
if (error != nullptr) *error = "cannot open the input file";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
joc_stream_config config{};
|
joc_stream_config config{};
|
||||||
@@ -479,20 +584,46 @@ bool Engine::start(const std::string& input_path, const Settings& settings, std:
|
|||||||
settings.speaker_layout.c_str(),
|
settings.speaker_layout.c_str(),
|
||||||
hrtf_in_use.empty() ? "(none)" : hrtf_in_use.c_str());
|
hrtf_in_use.empty() ? "(none)" : hrtf_in_use.c_str());
|
||||||
|
|
||||||
const std::wstring stderr_path = [] {
|
// ffmpeg's stderr lands next to the component rather than in whatever working
|
||||||
|
// directory the host process happens to have. The two children write separate
|
||||||
|
// files so neither can truncate the other's diagnostics.
|
||||||
|
const auto stderr_path_for = [](const wchar_t* name) {
|
||||||
HMODULE self = nullptr;
|
HMODULE self = nullptr;
|
||||||
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||||
reinterpret_cast<LPCWSTR>(&speaker_channels), &self);
|
reinterpret_cast<LPCWSTR>(&speaker_channels), &self);
|
||||||
wchar_t path[4096] = {};
|
wchar_t path[4096] = {};
|
||||||
const DWORD length = GetModuleFileNameW(self, path, 4096);
|
const DWORD length = GetModuleFileNameW(self, path, 4096);
|
||||||
if (length == 0) return std::wstring(L"joc_ffmpeg.log");
|
if (length == 0) return std::wstring(name);
|
||||||
const std::wstring text(path, length);
|
const std::wstring text(path, length);
|
||||||
const std::wstring::size_type slash = text.find_last_of(L"\\/");
|
const std::wstring::size_type slash = text.find_last_of(L"\\/");
|
||||||
return slash == std::wstring::npos ? std::wstring(L"joc_ffmpeg.log")
|
return slash == std::wstring::npos ? std::wstring(name)
|
||||||
: text.substr(0, slash) + L"\\joc_ffmpeg.log";
|
: text.substr(0, slash) + L"\\" + name;
|
||||||
}();
|
};
|
||||||
if (!impl.bed.start(settings.ffmpeg_path, input_path, stderr_path, error)) return false;
|
|
||||||
|
// The 5.1 core PCM, exactly as the reference renderer's own core decode does it:
|
||||||
|
// 5.1 interleaved float32 at 48 kHz, the layout the renderer expects
|
||||||
|
// (L R C LFE Ls Rs).
|
||||||
|
std::wstring bed_arguments = L"-map 0:a:";
|
||||||
|
bed_arguments += std::to_wstring(settings.audio_index);
|
||||||
|
bed_arguments += L" -vn -ac 6 -ar 48000 -c:a pcm_f32le -f f32le -";
|
||||||
|
if (!impl.bed.start(settings.ffmpeg_path, input_path, bed_arguments, "bed",
|
||||||
|
stderr_path_for(L"joc_ffmpeg_bed.log"), error)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (impl.eac3_from_pipe) {
|
||||||
|
// Stream copy: the syncframes arrive byte for byte as they are stored, which
|
||||||
|
// is what the JOC metadata needs.
|
||||||
|
std::wstring stream_arguments = L"-map 0:a:";
|
||||||
|
stream_arguments += std::to_wstring(settings.audio_index);
|
||||||
|
stream_arguments += L" -vn -c:a copy -f eac3 -";
|
||||||
|
if (!impl.eac3_pipe.start(settings.ffmpeg_path, input_path, stream_arguments, "metadata",
|
||||||
|
stderr_path_for(L"joc_ffmpeg_stream.log"), error,
|
||||||
|
1u << 20)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,7 +665,7 @@ std::size_t Engine::read(float* destination, std::size_t frames, std::string* er
|
|||||||
// across chunk boundaries: counting each chunk on its own loses one
|
// across chunk boundaries: counting each chunk on its own loses one
|
||||||
// frame at the end, which leaves the bed a frame short and the last
|
// frame at the end, which leaves the bed a frame short and the last
|
||||||
// frame of the file unrendered.
|
// frame of the file unrendered.
|
||||||
const std::size_t got = impl.eac3.read(impl.eac3_buffer.data() + impl.eac3_carry,
|
const std::size_t got = impl.read_eac3(impl.eac3_buffer.data() + impl.eac3_carry,
|
||||||
impl.eac3_buffer.size() - impl.eac3_carry);
|
impl.eac3_buffer.size() - impl.eac3_carry);
|
||||||
const std::size_t total = impl.eac3_carry + got;
|
const std::size_t total = impl.eac3_carry + got;
|
||||||
if (total == 0) {
|
if (total == 0) {
|
||||||
|
|||||||
@@ -35,8 +35,18 @@ enum class HrtfSource {
|
|||||||
kRosella = 1,
|
kRosella = 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Where the E-AC-3 syncframes come from. A bare stream is read straight from the
|
||||||
|
// file; inside a container the track has to be extracted first (ffmpeg, stream copy).
|
||||||
|
enum class InputKind {
|
||||||
|
kBare = 0,
|
||||||
|
kContainer = 1,
|
||||||
|
};
|
||||||
|
|
||||||
struct Settings {
|
struct Settings {
|
||||||
Output output = Output::kBinaural;
|
Output output = Output::kBinaural;
|
||||||
|
InputKind input_kind = InputKind::kBare;
|
||||||
|
// Which of the container's audio tracks holds the E-AC-3 stream, 0-based.
|
||||||
|
unsigned audio_index = 0;
|
||||||
std::string speaker_layout = "7.1";
|
std::string speaker_layout = "7.1";
|
||||||
HrtfSource hrtf_source = HrtfSource::kSofa;
|
HrtfSource hrtf_source = HrtfSource::kSofa;
|
||||||
// Empty means the default file in the default folder, see resolve_hrtf_file().
|
// Empty means the default file in the default folder, see resolve_hrtf_file().
|
||||||
@@ -87,6 +97,14 @@ struct FileProbe {
|
|||||||
// does pointer arithmetic over the stream, no decoding, no HRTF work.
|
// does pointer arithmetic over the stream, no decoding, no HRTF work.
|
||||||
FileProbe probe_file(const std::string& path, std::size_t max_scan_bytes = 0);
|
FileProbe probe_file(const std::string& path, std::size_t max_scan_bytes = 0);
|
||||||
|
|
||||||
|
// Decides whether the E-AC-3 track of a container file carries JOC, without
|
||||||
|
// rendering anything: ffmpeg copies a short prefix of that track out (stream copy,
|
||||||
|
// so the syncframes are the stored ones) and the same bitstream test is applied.
|
||||||
|
// The verdict is cached per file, because the information and the decode pass would
|
||||||
|
// otherwise each launch ffmpeg for it.
|
||||||
|
bool probe_container_joc(const std::string& ffmpeg_path, const std::string& path,
|
||||||
|
unsigned audio_index, bool* joc, std::string* detail);
|
||||||
|
|
||||||
class Engine {
|
class Engine {
|
||||||
public:
|
public:
|
||||||
Engine();
|
Engine();
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
#include <SDK/componentversion.h>
|
#include <SDK/componentversion.h>
|
||||||
#include <SDK/coreversion.h>
|
#include <SDK/coreversion.h>
|
||||||
#include <SDK/initquit.h>
|
#include <SDK/initquit.h>
|
||||||
|
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
|
|
||||||
// Kept in one place: the string reported to foobar2000 and written to the log
|
// Kept in one place: the string reported to foobar2000 and written to the log
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Exercises the container probe directly, without going through foobar2000.
|
||||||
|
//
|
||||||
|
// container_scan_test <file> [more files...]
|
||||||
|
//
|
||||||
|
// It exists because container dispatch is decided by the core's decoder priority
|
||||||
|
// table: when a built-in demuxer is offered the file first, this component never
|
||||||
|
// sees it, so the probe would otherwise never run during a test. Running it here
|
||||||
|
// shows what the probe decides for each file on its own.
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "../src/container_scan.h"
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
if (argc < 2) {
|
||||||
|
std::fprintf(stderr, "usage: container_scan_test <file> [more files...]\n");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
int failures = 0;
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
const std::string path = argv[i];
|
||||||
|
const joc_container::Result result = joc_container::scan(path);
|
||||||
|
std::printf("%-28s kind=%-9s eac3=%d audio#%u codec=%-6s duration=%8.3f s %s\n",
|
||||||
|
path.c_str(), joc_container::kind_name(result.kind), result.eac3 ? 1 : 0,
|
||||||
|
result.audio_index, result.codec.empty() ? "-" : result.codec.c_str(),
|
||||||
|
result.duration_seconds, result.detail.c_str());
|
||||||
|
if (result.kind == joc_container::Kind::kNone) ++failures;
|
||||||
|
}
|
||||||
|
return failures == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -39,10 +39,11 @@ $lines = @(
|
|||||||
"call `"$vcvars`" >nul",
|
"call `"$vcvars`" >nul",
|
||||||
"cd /d `"$projectRoot`"",
|
"cd /d `"$projectRoot`"",
|
||||||
# log.cpp comes along because the engine writes its diagnostics through it.
|
# log.cpp comes along because the engine writes its diagnostics through it.
|
||||||
|
"cl $common /Fe:`"$outDir\container_scan_test.exe`" /Fo:`"$outDir\\`" tests\container_scan_test.cpp src\container_scan.cpp src\log.cpp",
|
||||||
"cl $common /Fe:`"$outDir\scan_selftest.exe`" /Fo:`"$outDir\\`" tests\scan_selftest.cpp src\eac3_scan.cpp",
|
"cl $common /Fe:`"$outDir\scan_selftest.exe`" /Fo:`"$outDir\\`" tests\scan_selftest.cpp src\eac3_scan.cpp",
|
||||||
"cl $common /Fe:`"$outDir\prefs_layout_check.exe`" /Fo:`"$outDir\\`" tests\prefs_layout_check.cpp user32.lib gdi32.lib",
|
"cl $common /Fe:`"$outDir\prefs_layout_check.exe`" /Fo:`"$outDir\\`" tests\prefs_layout_check.cpp user32.lib gdi32.lib",
|
||||||
"cl $common /Fe:`"$outDir\scan_crosscheck.exe`" /Fo:`"$outDir\\`" tests\scan_crosscheck.cpp src\eac3_scan.cpp `"$coreLib`" shell32.lib",
|
"cl $common /Fe:`"$outDir\scan_crosscheck.exe`" /Fo:`"$outDir\\`" tests\scan_crosscheck.cpp src\eac3_scan.cpp `"$coreLib`" shell32.lib",
|
||||||
"cl $common /Fe:`"$outDir\render_harness.exe`" /Fo:`"$outDir\\`" tests\render_harness.cpp src\joc_decode.cpp src\eac3_scan.cpp src\log.cpp `"$coreLib`" shell32.lib",
|
"cl $common /Fe:`"$outDir\render_harness.exe`" /Fo:`"$outDir\\`" tests\render_harness.cpp src\joc_decode.cpp src\eac3_scan.cpp src\container_scan.cpp src\log.cpp `"$coreLib`" shell32.lib",
|
||||||
'if errorlevel 1 exit /b 1',
|
'if errorlevel 1 exit /b 1',
|
||||||
'exit /b 0'
|
'exit /b 0'
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user