Skip to content

Commit 0d116d7

Browse files
Merge branch 'main' into test-recorder-coverage
2 parents 854038f + c3894ec commit 0d116d7

6 files changed

Lines changed: 1842 additions & 89 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
name: Label PR from Linked Issues
3+
4+
# Trigger whenever a PR is opened, reopened, or its body is edited.
5+
on:
6+
pull_request:
7+
types: [opened, reopened, edited]
8+
9+
# Minimal permissions: read issues to get their labels, write PR labels.
10+
permissions:
11+
contents: read
12+
issues: read
13+
pull-requests: write
14+
15+
jobs:
16+
label-pr:
17+
name: Copy labels from linked issues to PR
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Apply issue labels to PR
21+
uses: actions/github-script@v9
22+
with:
23+
script: |
24+
const body = context.payload.pull_request.body || '';
25+
const prNumber = context.payload.pull_request.number;
26+
const { owner, repo } = context.repo;
27+
28+
// Collect issue numbers from
29+
// "Fixes/Closes/Resolves #NNN" (case-insensitive).
30+
const pattern = /(?:fixes|closes|resolves)\s+#(\d+)/gi;
31+
const issueNumbers = [];
32+
let match;
33+
while ((match = pattern.exec(body)) !== null) {
34+
issueNumbers.push(parseInt(match[1], 10));
35+
}
36+
37+
if (issueNumbers.length === 0) {
38+
core.info('No linked issues found in PR body — nothing to do.');
39+
return;
40+
}
41+
42+
core.info(`Linked issues: ${issueNumbers.join(', ')}`);
43+
44+
// Fetch labels from every referenced issue;
45+
// ignore missing/invalid ones.
46+
const labelSet = new Set();
47+
for (const issueNumber of issueNumbers) {
48+
try {
49+
const { data: issue } = await github.rest.issues.get({
50+
owner,
51+
repo,
52+
issue_number: issueNumber,
53+
});
54+
for (const label of issue.labels) {
55+
labelSet.add(typeof label === 'string' ? label : label.name);
56+
}
57+
} catch (err) {
58+
// Non-existent or inaccessible issue — skip gracefully.
59+
core.warning(
60+
`Could not fetch issue #${issueNumber}: ${err.message}`
61+
);
62+
}
63+
}
64+
65+
if (labelSet.size === 0) {
66+
core.info(
67+
'Referenced issues carry no labels — nothing to apply.'
68+
);
69+
return;
70+
}
71+
72+
// Determine which labels the PR does not already have.
73+
const { data: prData } = await github.rest.pulls.get({
74+
owner,
75+
repo,
76+
pull_number: prNumber,
77+
});
78+
const existingLabels = new Set(
79+
prData.labels.map((l) => (typeof l === 'string' ? l : l.name))
80+
);
81+
const toAdd = [...labelSet].filter((l) => !existingLabels.has(l));
82+
83+
if (toAdd.length === 0) {
84+
core.info('PR already has all issue labels — nothing to add.');
85+
return;
86+
}
87+
88+
core.info(
89+
`Adding labels to PR #${prNumber}: ${toAdd.join(', ')}`
90+
);
91+
await github.rest.issues.addLabels({
92+
owner,
93+
repo,
94+
issue_number: prNumber,
95+
labels: toAdd,
96+
});

src/audio/effects/pitch_shifter.cpp

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ static constexpr int P_MIX = 2;
1414
// Grain window size in seconds (~23 ms at 48kHz = 1024 samples)
1515
static constexpr float GRAIN_WINDOW_SEC = 0.023f;
1616

17+
static float wrap_phase(float phase, int buf_size) {
18+
phase = std::fmod(phase, static_cast<float>(buf_size));
19+
if (phase < 0.0f) phase += static_cast<float>(buf_size);
20+
return phase;
21+
}
22+
1723
PitchShifter::PitchShifter() {
1824
params_ = {
1925
{"Shift", 0.0f, -12.0f, 12.0f, 0.0f, "st",
@@ -35,9 +41,7 @@ void PitchShifter::set_sample_rate(int sample_rate) {
3541
}
3642

3743
float PitchShifter::read_linear(float phase) const {
38-
// Wrap phase into [0, buf_size_)
39-
phase = std::fmod(phase, static_cast<float>(buf_size_));
40-
if (phase < 0.0f) phase += buf_size_;
44+
phase = wrap_phase(phase, buf_size_);
4145

4246
int pos0 = static_cast<int>(phase);
4347
int pos1 = (pos0 + 1) % buf_size_;
@@ -77,11 +81,9 @@ void PitchShifter::process(float* buffer, int num_samples) {
7781
read_phase_a_ += drift;
7882
read_phase_b_ += drift;
7983

80-
// Wrap phases into [0, buf_size_)
81-
while (read_phase_a_ < 0.0f) read_phase_a_ += buf_size_;
82-
while (read_phase_a_ >= buf_size_) read_phase_a_ -= buf_size_;
83-
while (read_phase_b_ < 0.0f) read_phase_b_ += buf_size_;
84-
while (read_phase_b_ >= buf_size_) read_phase_b_ -= buf_size_;
84+
// Wrap phases into [0, buf_size_) in constant time.
85+
read_phase_a_ = wrap_phase(read_phase_a_, buf_size_);
86+
read_phase_b_ = wrap_phase(read_phase_b_, buf_size_);
8587

8688
// Compute absolute read positions in the buffer
8789
float pos_a = static_cast<float>(write_pos_) - read_phase_a_;

tests/test_audio_engine.cpp

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,3 +367,132 @@ TEST(AudioEngineProcess_FullCoverageSweep) {
367367
engine.toggle_metronome();
368368
engine.process_audio(in.data(), out.data(), 64);
369369
}
370+
TEST(audio_engine_commit_graph_changes_stability) {
371+
AudioEngine engine;
372+
373+
for (int i = 0; i < 50; ++i) {
374+
engine.commit_graph_changes();
375+
}
376+
377+
ASSERT_TRUE(true);
378+
}
379+
TEST(audio_engine_multiple_commit_graph_changes) {
380+
AudioEngine engine;
381+
382+
engine.commit_graph_changes();
383+
engine.commit_graph_changes();
384+
engine.commit_graph_changes();
385+
386+
ASSERT_TRUE(true);
387+
}
388+
TEST(audio_engine_set_buffer_size_clamps_values) {
389+
AudioEngine engine;
390+
391+
engine.set_buffer_size(-1);
392+
ASSERT_TRUE(engine.get_buffer_size() > 0);
393+
394+
engine.set_buffer_size(999999);
395+
ASSERT_TRUE(engine.get_buffer_size() <= 8192);
396+
}
397+
TEST(audio_engine_set_sample_rate_updates_state) {
398+
AudioEngine engine;
399+
400+
engine.set_sample_rate(44100);
401+
ASSERT_TRUE(engine.get_sample_rate() == 44100);
402+
403+
engine.set_sample_rate(48000);
404+
ASSERT_TRUE(engine.get_sample_rate() == 48000);
405+
406+
engine.set_sample_rate(96000);
407+
ASSERT_TRUE(engine.get_sample_rate() == 96000);
408+
}
409+
TEST(audio_engine_commit_graph_changes_with_empty_graph) {
410+
AudioEngine engine;
411+
412+
engine.commit_graph_changes();
413+
414+
ASSERT_TRUE(true);
415+
}
416+
TEST(audio_engine_repeated_graph_commits) {
417+
AudioEngine engine;
418+
419+
for (int i = 0; i < 100; ++i) {
420+
engine.commit_graph_changes();
421+
}
422+
423+
ASSERT_TRUE(true);
424+
}
425+
TEST(audio_engine_graph_commit_after_node_removal) {
426+
AudioEngine engine;
427+
428+
auto& graph = engine.graph();
429+
430+
int n1 =
431+
graph.add_node("A", NodeRoutingType::StandardEffect);
432+
433+
int n2 =
434+
graph.add_node("B", NodeRoutingType::StandardEffect);
435+
436+
auto nodes = graph.get_nodes();
437+
438+
graph.add_link(nodes[0].output_pin_ids[0],
439+
nodes[1].input_pin_ids[0]);
440+
441+
ASSERT_TRUE(graph.remove_node(n2));
442+
443+
engine.commit_graph_changes();
444+
445+
ASSERT_TRUE(true);
446+
}
447+
TEST(audio_engine_serialize_deserialize_roundtrip) {
448+
AudioEngine engine;
449+
450+
auto serialized = engine.serialize();
451+
452+
AudioEngine loaded;
453+
454+
loaded.deserialize(serialized);
455+
456+
auto reserialized = loaded.serialize();
457+
458+
ASSERT_FALSE(serialized.empty());
459+
ASSERT_FALSE(reserialized.empty());
460+
461+
ASSERT_TRUE(reserialized.contains("effects"));
462+
}
463+
TEST(audio_engine_multiple_sample_rate_changes) {
464+
AudioEngine engine;
465+
466+
std::vector<int> rates = {
467+
22050,
468+
44100,
469+
48000,
470+
88200,
471+
96000
472+
};
473+
474+
for (int rate : rates) {
475+
engine.set_sample_rate(rate);
476+
}
477+
478+
ASSERT_TRUE(true);
479+
}
480+
TEST(audio_engine_multiple_buffer_size_changes) {
481+
AudioEngine engine;
482+
483+
std::vector<int> sizes = {
484+
16,
485+
32,
486+
64,
487+
128,
488+
256,
489+
512,
490+
1024
491+
};
492+
493+
for (int size : sizes) {
494+
engine.set_buffer_size(size);
495+
}
496+
497+
ASSERT_TRUE(true);
498+
}

0 commit comments

Comments
 (0)