Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 61 additions & 5 deletions .clang-format
Original file line number Diff line number Diff line change
@@ -1,9 +1,65 @@
# Tap House Rules — the Tap family house style. Copy verbatim into every *Tap repo.
# 4-space indent (incl. namespaces), aligned declaration/assignment columns,
# attached braces (else/catch break), comma-first ctor initializers,
# left-bound pointers, 120-column limit. Layout only — naming and mandatory
# braces are enforced separately by .clang-tidy (clang-format cannot check
# identifier names, and its brace insertion is not semantically aware).
Language: Cpp
BasedOnStyle: LLVM
Standard: c++20

ColumnLimit: 120
IndentWidth: 4
ColumnLimit: 100
AccessModifierOffset: -4
AllowShortFunctionsOnASingleLine: Inline
AlwaysBreakTemplateDeclarations: Yes
AccessModifierOffset: -2
NamespaceIndentation: All

PointerAlignment: Left
DerivePointerAlignment: false
BreakBeforeBinaryOperators: NonAssignment
SpaceBeforeCpp11BracedList: false
AlwaysBreakTemplateDeclarations: Yes

# Braces attach everywhere (including functions); only else/catch break.
BreakBeforeBraces: Custom
BraceWrapping:
AfterFunction: false
AfterClass: false
AfterStruct: false
AfterNamespace: false
AfterControlStatement: Never
BeforeElse: true
BeforeCatch: true
BreakConstructorInitializers: BeforeComma
PackConstructorInitializers: Never

AlignConsecutiveAssignments: true
AlignConsecutiveDeclarations: true
AlignTrailingComments: true

# Short accessor functions and lambdas may stay inline, but control-flow
# statements never do: every if/for/while is braced AND expanded (see
# .clang-tidy readability-braces-around-statements).
AllowShortFunctionsOnASingleLine: Inline
AllowShortLambdasOnASingleLine: All
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AllowShortBlocksOnASingleLine: Never

BreakStringLiterals: false
KeepEmptyLinesAtTheStartOfBlocks: false
InsertNewlineAtEOF: true

# Include ordering: main header (auto, priority 0) -> C++ standard ->
# third-party -> this project. Regroup enforces it; blank lines between groups.
SortIncludes: CaseSensitive
IncludeBlocks: Preserve
IncludeBlocks: Regroup
IncludeCategories:
# C++ standard library: <angle> with no '/' and no '.' (e.g. <vector>)
- Regex: '^<[[:alnum:]_]+>$'
Priority: 2
# Other angle-bracket headers (third-party, e.g. <gtest/gtest.h>)
- Regex: '^<.*>$'
Priority: 3
# This project: quoted includes
- Regex: '^".*"$'
Priority: 4
132 changes: 132 additions & 0 deletions .clang-tidy
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Tap House Rules — naming + mandatory-braces enforcement. Copy verbatim into
# every *Tap repo. This is what actually checks m_ members, k_ constants, snake_case
# types/functions, PascalCase template parameters, and braces around every
# control-flow body — clang-format cannot (and its InsertBraces is not
# semantically aware). Scope is intentionally limited to these for now;
# correctness/modernize checks can be layered on later.
#
# NOTE: WarningsAsErrors is intentionally NOT set here so local runs only warn.
# CI passes --warnings-as-errors=readability-* to make the gate blocking.
Checks: >
-*,
readability-identifier-naming,
readability-braces-around-statements
# Analyze this project's own headers only (under include/); vendored third_party
# and fetched deps live outside include/ and are excluded. Generated tables
# (room_data.h, hrtf_data.h, tdesigns.h) live under include/ but carry
# // NOLINTBEGIN(readability-identifier-naming) markers from their generators.
# NOTE: clang-tidy uses llvm::Regex, which has NO negative lookahead — a
# '^(?!...)' pattern silently matches nothing and disables the check.
HeaderFilterRegex: '.*/(include|tests)/.*'

# --- Linear-algebra notation carve-out --------------------------------------
# The DSP math deliberately uses capitalized matrix/vector symbols (Y = SH
# matrix, D = decoder, R = rotation, ...). Permit a leading-capital symbol with
# an optional short subscript and _snake suffixes (Y, Yd, R9, Y_virtual). This
# Also matrix products (DtD, YtD). Still rejects camelCase (frameCount).
# Applied below per category via <Category>IgnoredRegexp.
CheckOptions:
# --- Types: snake_case ---
- key: readability-identifier-naming.ClassCase
value: lower_case
- key: readability-identifier-naming.StructCase
value: lower_case
- key: readability-identifier-naming.UnionCase
value: lower_case
- key: readability-identifier-naming.EnumCase
value: lower_case
- key: readability-identifier-naming.EnumConstantCase
value: lower_case
- key: readability-identifier-naming.ScopedEnumConstantCase
value: lower_case
- key: readability-identifier-naming.TypeAliasCase
value: lower_case
- key: readability-identifier-naming.TypedefCase
value: lower_case
- key: readability-identifier-naming.NamespaceCase
value: lower_case

# --- Concepts: snake_case (like the types they constrain, per P1754) ---
- key: readability-identifier-naming.ConceptCase
value: lower_case

# --- Functions / methods: snake_case ---
- key: readability-identifier-naming.FunctionCase
value: lower_case
- key: readability-identifier-naming.MethodCase
value: lower_case

# --- Variables / parameters / locals: snake_case, no prefix ---
- key: readability-identifier-naming.VariableCase
value: lower_case
- key: readability-identifier-naming.ParameterCase
value: lower_case
- key: readability-identifier-naming.LocalVariableCase
value: lower_case
- key: readability-identifier-naming.LocalConstantCase
value: lower_case
# Math-notation carve-out (see header): capitalized matrix/vector symbols.
- key: readability-identifier-naming.ParameterIgnoredRegexp
value: '^[A-Z][A-Za-z0-9]*(_[A-Za-z0-9]+)*$'
- key: readability-identifier-naming.LocalVariableIgnoredRegexp
value: '^[A-Z][A-Za-z0-9]*(_[A-Za-z0-9]+)*$'
- key: readability-identifier-naming.LocalConstantIgnoredRegexp
value: '^[A-Z][A-Za-z0-9]*(_[A-Za-z0-9]+)*$'
- key: readability-identifier-naming.VariableIgnoredRegexp
value: '^[A-Z][A-Za-z0-9]*(_[A-Za-z0-9]+)*$'

# --- Data members: private/protected get m_; public struct fields bare ---
- key: readability-identifier-naming.PrivateMemberCase
value: lower_case
- key: readability-identifier-naming.PrivateMemberPrefix
value: 'm_'
- key: readability-identifier-naming.ProtectedMemberCase
value: lower_case
- key: readability-identifier-naming.ProtectedMemberPrefix
value: 'm_'
- key: readability-identifier-naming.PublicMemberCase
value: lower_case
# const (non-static) data members are still members -> keep the m_ marker
- key: readability-identifier-naming.ConstantMemberCase
value: lower_case
- key: readability-identifier-naming.ConstantMemberPrefix
value: 'm_'
# Math-notation carve-out for capitalized matrix/vector member symbols.
- key: readability-identifier-naming.PublicMemberIgnoredRegexp
value: '^[A-Z][A-Za-z0-9]*(_[A-Za-z0-9]+)*$'
- key: readability-identifier-naming.PrivateMemberIgnoredRegexp
value: '^[A-Z][A-Za-z0-9]*(_[A-Za-z0-9]+)*$'
- key: readability-identifier-naming.ProtectedMemberIgnoredRegexp
value: '^[A-Z][A-Za-z0-9]*(_[A-Za-z0-9]+)*$'

# --- Constants at namespace/class/static scope: k_ + snake_case ---
# (constexpr/const LOCALS stay bare via LocalConstantCase above)
- key: readability-identifier-naming.GlobalConstantCase
value: lower_case
- key: readability-identifier-naming.GlobalConstantPrefix
value: 'k_'
- key: readability-identifier-naming.ClassConstantCase
value: lower_case
- key: readability-identifier-naming.ClassConstantPrefix
value: 'k_'
- key: readability-identifier-naming.StaticConstantCase
value: lower_case
- key: readability-identifier-naming.StaticConstantPrefix
value: 'k_'

# --- Template parameters: PascalCase (the ONLY leading-capital names) ---
# Applies to type AND non-type params: template <int Order>, not <int order>.
- key: readability-identifier-naming.TemplateParameterCase
value: CamelCase
- key: readability-identifier-naming.TypeTemplateParameterCase
value: CamelCase
- key: readability-identifier-naming.ValueTemplateParameterCase
value: CamelCase

# --- Macros: ALL_CAPS ---
- key: readability-identifier-naming.MacroDefinitionCase
value: UPPER_CASE

# --- Mandatory braces: brace every control-flow body, even one-liners ---
- key: readability-braces-around-statements.ShortStatementLines
value: '0'
2 changes: 2 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Bulk clang-format reformat under the shared Tap house style.
34bb89eb806a91667fcd895b56dea8c0e701739a
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -498,10 +498,10 @@ jobs:
run: |
sudo apt-get update -q && sudo apt-get install -y -q clang-format-18
clang-format-18 --dry-run --Werror \
include/srt/*.hpp include/srt/detail/*.hpp \
include/srt/*.h include/srt/detail/*.h \
bench/*.cpp bench/icount/*.cpp bench/compare/*.cpp \
tools/capi/*.cpp tools/qemu_insn_plugin/*.c \
tests/*.cpp tests/support/*.hpp examples/*.cpp platform/*.c
tests/*.cpp tests/support/*.h examples/*.cpp platform/*.c

# The book (book/) quotes library code via mdBook anchor includes; this
# gate makes a refactor that orphans an excerpt fail CI, the same
Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/style.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Tap House Style

# Enforces the shared Tap House Rules. clang-format is already checked in
# ci.yml; this adds (1) a drift check against the canonical TapHouse configs
# and (2) clang-tidy naming + mandatory-braces enforcement.
on: [push, pull_request]

jobs:
drift:
uses: tap/taphouse/.github/workflows/drift-check.yml@v1
with:
ref: v1

clang-tidy:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Install tools
run: sudo apt-get update && sudo apt-get install -y clang-tidy-18 libgtest-dev cmake python3
- name: Configure (compile database)
run: cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
- name: clang-tidy (project TUs; third_party excluded)
run: |
files=$(python3 -c "import json; print('\n'.join(e['file'] for e in json.load(open('build/compile_commands.json')) if 'third_party' not in e['file']))")
fail=0
for f in $files; do
out=$(clang-tidy-18 -p build "$f" 2>/dev/null || true)
if echo "$out" | grep -qE "warning:|error:"; then echo "$out"; fail=1; fi
done
[ "$fail" -eq 0 ] && echo "clang-tidy clean." || { echo "::error::clang-tidy found violations"; exit 1; }
52 changes: 26 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,21 @@ target_link_libraries(app PRIVATE SampleRateTap::SampleRateTap)
```

```cpp
#include <srt/srt.hpp>
#include <srt/srt.h>

srt::Config cfg;
cfg.sampleRateHz = 48000.0;
srt::config cfg;
cfg.sample_rate_hz = 48000.0;
cfg.channels = 2;
srt::AsyncSampleRateConverter asrc(cfg); // allocates + designs filter; may throw
srt::async_sample_rate_converter asrc(cfg); // allocates + designs filter; may throw

// Input-device thread (input clock):
asrc.push(inputInterleaved, frames); // noexcept, lock-free
asrc.push(input_interleaved, frames); // noexcept, lock-free

// Output-device thread (output clock):
asrc.pull(outputInterleaved, frames); // noexcept, lock-free; silence
asrc.pull(output_interleaved, frames); // noexcept, lock-free; silence
// until filled/locked

srt::Status st = asrc.status(); // any thread: state, ppm, fill,
srt::converter_status st = asrc.status(); // any thread: state, ppm, fill,
// underruns/overruns/resyncs
```

Expand Down Expand Up @@ -78,7 +78,7 @@ one-clock-domain-per-core RP2350 deployment, self-validating).

**Consuming the library**: `add_subdirectory` or `FetchContent` only —
there are no install/package rules yet. Version 0.1.0 (`SRT_VERSION_*` in
`srt/srt.hpp`, `srt_version()` over the C ABI); pre-1.0, the API may
`srt/srt.h`, `srt_version()` over the C ABI); pre-1.0, the API may
still change between versions.

## The book
Expand Down Expand Up @@ -150,7 +150,7 @@ stages with the integrator (the ppm estimate) handed across transitions:

**Why three stages.** The FIFO count is quantized to whole frames (or whole
push blocks), so the occupancy observable carries a deterministic sawtooth at
the *beat frequency* `ppm × pushRate`. Whatever the loop passes into ε̂
the *beat frequency* `ppm × push_rate`. Whatever the loop passes into ε̂
frequency-modulates the audio. The Quiet stage rejects a one-frame sawtooth
to roughly −120 dBc equivalent at 20 kHz while still tracking a 1 ppm/s
oscillator drift ramp with under half a frame of standing error. With coarse
Expand Down Expand Up @@ -187,23 +187,23 @@ into a full FIFO drops the newest frames and counts an overrun.
## Latency

```
latency = targetLatencyFrames + (L·T − 1)/(2L) [input frames]
latency = target_latency_frames + (L·T − 1)/(2L) [input frames]
= 48 + ~24 ≈ 72 frames ≈ 1.5 ms at 48 kHz (defaults)
```

`designedLatencySeconds()` reports the figure; the FIFO term breathes by a
`designed_latency_seconds()` reports the figure; the FIFO term breathes by a
fraction of the block size as the servo tracks drift. The filter is linear
phase. For lower latency use `FilterSpec::fast()` (~16-frame group delay)
and a smaller `targetLatencyFrames`.
phase. For lower latency use `filter_spec::fast()` (~16-frame group delay)
and a smaller `target_latency_frames`.

**The setpoint must exceed the pull block size** — a pull synthesizes from
frames already buffered, so a setpoint at or below the callback size is
infeasible and would drain into a permanent dropout cycle. The converter
enforces this automatically: when it observes pull blocks larger than the
configured setpoint it raises the effective setpoint (block + ~half-block
margin, bounded by FIFO capacity) and reports the value in
`Status::effectiveTargetLatencyFrames`; latency follows the raised
setpoint. Callbacks above ~340 frames also need `fifoFrames` sized
`converter_status::effective_target_latency_frames`; latency follows the raised
setpoint. Callbacks above ~340 frames also need `fifo_frames` sized
explicitly. The setpoint must additionally stay above the peak occupancy
excursion of your push/pull jitter, as before.

Expand All @@ -229,12 +229,12 @@ signal frequency). Servo lock from a cold start takes ~1 s; a 0 → 300 ppm
drift ramp at 10 ppm/s is tracked without unlocking.

The same structure holds at other deployment rates, e.g. 16 kHz for
reference-microphone processing — but `FilterSpec` band edges and
`ServoConfig` bandwidths are absolute Hz designed for ~48 kHz, and running
reference-microphone processing — but `filter_spec` band edges and
`servo_config` bandwidths are absolute Hz designed for ~48 kHz, and running
another rate with unscaled defaults silently costs quality (measured:
~32 dB at 16 kHz). Start any non-48 kHz deployment from
`srt::Config::forSampleRate(rateHz)`, which rescales both (plus the servo
hold times); `FilterSpec::scaledTo` / `ServoConfig::scaledTo` exist for
`srt::config::for_sample_rate(rate_hz)`, which rescales both (plus the servo
hold times); `filter_spec::scaled_to` / `servo_config::scaled_to` exist for
custom presets. Measured through that factory
(`tests/test_asrc_quality_16k.cpp`), 16 kHz matches the 48 kHz
normalized-frequency structure: 136.6 dB at 333 Hz and 106.5 dB at 6.5 kHz,
Expand Down Expand Up @@ -353,14 +353,14 @@ Indicative numbers from a shared machine (Intel(R) Xeon(R) Processor @ 2.80GHz,

## Sample types

The datapath is templated on the sample type via `srt::SampleTraits`
(`include/srt/sample_traits.hpp`). Three formats are provided:
The datapath is templated on the sample type via `srt::sample_traits`
(`include/srt/sample_traits.h`). Three formats are provided:

| Type | Alias | Format | Measured SNR (997 Hz / 19.5 kHz, half scale, +200 ppm) |
|---|---|---|---|
| `float` | `AsyncSampleRateConverter` | float I/O, double accumulation | 135 dB / 105 dB |
| `std::int32_t` | `AsyncSampleRateConverterQ31` | Q31 I/O, Q1.30 coeffs, int64 accumulation, saturating | 133 dB / 105 dB |
| `std::int16_t` | `AsyncSampleRateConverterQ15` | Q15 I/O, Q1.14 coeffs, int64 accumulation, saturating | 77 dB (format-limited) |
| `float` | `async_sample_rate_converter` | float I/O, double accumulation | 135 dB / 105 dB |
| `std::int32_t` | `async_sample_rate_converter_q31` | Q31 I/O, Q1.30 coeffs, int64 accumulation, saturating | 133 dB / 105 dB |
| `std::int16_t` | `async_sample_rate_converter_q15` | Q15 I/O, Q1.14 coeffs, int64 accumulation, saturating | 77 dB (format-limited) |

The fixed-point datapaths have integer-only inner loops (the μ blend factor
is converted once per output sample), making them the appropriate choice for
Expand All @@ -371,11 +371,11 @@ of operations per block).

## Limitations

- Near-unity ratios only (±`maxDeviationPpm`, default 1000 ppm). No
- Near-unity ratios only (±`max_deviation_ppm`, default 1000 ppm). No
44.1 ↔ 48 kHz conversion.
- The rate estimate is derived from FIFO counts only. With block-quantized
transfer its instantaneous value wobbles at the block-beat frequency
(see `Status.ppm` vs. a few seconds of averaging), and ultra-quiet servo
(see `converter_status.ppm` vs. a few seconds of averaging), and ultra-quiet servo
operation requires fine-grained transfer. A future version may accept
per-block timestamps for sub-sample phase observation.
- One producer thread and one consumer thread; construction/destruction must
Expand Down
Loading
Loading