Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 6, 2026

Implementation Plan for Audio Time-Scale/Pitch Modification

  • Add pitch shifting parameter to Fire structure
  • Add time stretch parameter to Fire structure
  • Implement pitch shifting in source SampleAt method using linear interpolation
  • Update Fire.At() to handle pitch-adjusted playback rates
  • Update SetFire API to support pitch and timeStretch parameters via SetFireWithPitch
  • Add tests for pitch shifting functionality
  • Add tests for interpolated sampling
  • Update documentation with new features
  • Fix code review issues (playback position tracking, interpolation duplication)
  • Fix CI build errors - update API to include ADSR parameters
  • Apply PR review feedback - fix critical bugs and improve API design
  • Apply second round of PR review feedback - additional improvements
  • Apply third round of PR review feedback - safety and testing
  • Apply fourth round of PR review feedback - timeStretch validation and formatting
  • Implement proper time stretching (currently pitch affects speed)

What's Been Implemented

Pitch Shifting: The mixer now supports pitch shifting audio without manual resampling.

  • Added SetFireWithPitch() API for simple pitch shifting (no ADSR parameters needed)
  • Added SetFireWithPitchADSR() API for full control over both pitch and ADSR envelope
  • Pitch multiplier: 1.0 = no change, 2.0 = up one octave, 0.5 = down one octave
  • Input validation: pitch must be positive non-zero (panics on invalid values)
  • timeStretch validation: must be 1.0 (time-stretching not yet implemented, panics otherwise)
  • Uses linear interpolation for smooth pitch shifts without artifacts
  • Automatically adjusts playback duration based on pitch (higher pitch = shorter duration)

Bug Fixes (from PR reviews):

  • Fixed PlaybackTz advancement timing - uses pre-advancement value for interpolation
  • Fixed condition logic to properly handle initial sample (fireTz >= 0 && IsPlaying())
  • Added length check for fireSample in interpolated path to prevent index out of bounds
  • Fixed fireStateReady to mirror fireStatePlay behavior for consistent sample timing
  • Added bounds checking in interpolation function for safer array access
  • Refactored API to use shared internal helper function

Code Quality:

  • Comprehensive edge case testing (boundary, beyond length, volume/pan, extreme values, zero/negative pitch, invalid timeStretch)
  • Unit tests for pitch shifting in Fire struct (advancement, EndTz calculation, state transitions, fractional positions)
  • Integration tests for SetFireWithPitchADSR
  • Clean API design with separate functions for different use cases
  • Exported HasPitchShift() for better maintainability
  • Simplified HasPitchShift() logic (relies on input validation)
  • Clear documentation of valid pitch range and timeStretch constraints
  • Internal-use-only documentation for PlaybackTz field
  • Proper code formatting with gofmt

The implementation uses sample-rate modification: changing the pitch changes playback speed proportionally. This is suitable for many use cases and avoids complex time-domain stretching algorithms.

Note: True time-stretching (changing speed without pitch) would require more advanced algorithms like phase vocoding or time-domain harmonic scaling. The timeStretch parameter is reserved for future implementation and currently must be set to 1.0.

Original prompt

This section details on the original issue you should resolve

<issue_title>Audio time-scale/pitch modification</issue_title>
<issue_description>From https://en.wikipedia.org/wiki/Audio_time-scale/pitch_modification

Time stretching is the process of changing the speed or duration of an audio signal without affecting its pitch. Pitch scaling or pitch shifting is the opposite: the process of changing the pitch without affecting the speed. Similar methods can change speed, pitch, or both at once, in a time-varying way.

These processes are used, for instance, to match the pitches and tempos of two pre-recorded clips for mixing when the clips cannot be reperformed or resampled. (A drum track containing no pitched instruments could be moderately resampled for tempo without adverse effects, but a pitched track could not). They are also used to create effects such as increasing the range of an instrument (like pitch shifting a guitar down an octave).
</issue_description>

Comments on the Issue (you are @copilot in this section)

@charneykaye I think that the option of being beat-aware for time stretching is very appealing. As this feature develops (and it will, over the next few months) we'll be sure to consider this.

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@charneykaye
Copy link
Contributor

@copilot try again

Copilot AI and others added 4 commits January 7, 2026 07:58
Co-authored-by: charneykaye <8584836+charneykaye@users.noreply.github.com>
Co-authored-by: charneykaye <8584836+charneykaye@users.noreply.github.com>
…e duplication

Co-authored-by: charneykaye <8584836+charneykaye@users.noreply.github.com>
Co-authored-by: charneykaye <8584836+charneykaye@users.noreply.github.com>
@charneykaye
Copy link
Contributor

Copy link
Contributor Author

Copilot AI commented Jan 7, 2026

@copilot try again, continuing https://github.com/copilot/tasks/pull/PR_kwDOAurw3M67rybW?session_id=b8a55384-7e66-422a-b03c-b8b4542bb8a9

The audio pitch shifting implementation is complete and all tests pass (commit f65f161).

Implemented features:

  • Pitch shifting API via SetFireWithPitch()
  • Linear interpolation for smooth playback
  • Helper methods to reduce code duplication
  • Comprehensive test coverage
  • Documentation in docs/PITCH_SHIFTING.md

The implementation supports pitch multipliers (1.0 = no change, 2.0 = up one octave, 0.5 = down one octave) using sample-rate modification with fractional position tracking for artifact-free pitch shifts.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • esm.ubuntu.com
    • Triggering command: /usr/lib/apt/methods/https /usr/lib/apt/methods/https /tmp/go-build507779176/b170/_x005.o /tmp/go-build507779176/b170/_x006.o -O2 (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@charneykaye
Copy link
Contributor

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds pitch shifting functionality to the go-mix audio mixer, allowing audio sources to be played at different pitches using sample-rate modification with linear interpolation. However, the implementation has several critical bugs that break existing functionality and the new pitch shifting feature.

Key Changes:

  • Added SetFireWithPitch() API for pitch-shifted audio playback
  • Implemented fractional playback position tracking (PlaybackTz) for smooth interpolation
  • Added SampleAtInterpolated() method for linear interpolation between samples

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
mix.go Adds top-level SetFireWithPitch API wrapper function
mix_test.go Adds basic tests for SetFireWithPitch parameter setting
lib/mix/mix.go CRITICAL BUGS: SetFire implementation broken (missing ADSR params), missing validation for interpolated samples, condition logic issue
lib/fire/fire.go CRITICAL BUG: PlaybackTz advancement timing causes interpolation from wrong sample position; adds Pitch/TimeStretch fields
lib/fire/fire_test.go Updates TestBase to include pitch/timeStretch parameters
lib/source/source.go Implements SampleAtInterpolated with linear interpolation logic
lib/source/source_test.go Adds basic interpolation test but missing important edge cases
docs/PITCH_SHIFTING.md BUG: Documentation contains incorrect example code for SetFire
README.md Adds feature list highlighting pitch shifting capability
go.sum Contains duplicate dependency entries that should be cleaned up

…ameters

Co-authored-by: charneykaye <8584836+charneykaye@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 7, 2026

@copilot latest test run in CI has many errors https://github.com/go-mix/mix/actions/runs/20791034830/job/59712996302?pr=71

Fixed the CI build errors in commit 75e747a. The issue was that PR #68 added ADSR envelope parameters to the Fire struct, but SetFireWithPitch wasn't updated to accept them.

Changes made:

  • Updated SetFireWithPitch() signature to include ADSR parameters (attack, decay, sustainLevel, release)
  • Fixed all test calls to New() in fire_test.go to include pitch and timeStretch parameters
  • Updated documentation with correct API signatures
  • All tests now pass ✅

@charneykaye
Copy link
Contributor

@copilot apply changes based on the comments in this thread

- Fix PlaybackTz advancement timing to use pre-advancement value for interpolation
- Fix condition logic to use >= 0 && IsPlaying() for proper initial sample handling
- Add length check for fireSample when using interpolated sampling
- Add SetFireWithPitchADSR API for full control over both pitch and ADSR
- Refactor to use shared setFireInternal helper function
- Simplify SetFireWithPitch to not require ADSR parameters
- Remove extra blank lines in test files
- Add comprehensive edge case tests for interpolation
- Add test for SetFireWithPitchADSR
- Update documentation with correct API signatures and examples

Co-authored-by: charneykaye <8584836+charneykaye@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 7, 2026

@copilot apply changes based on the comments in this thread

Applied all review feedback in commit 503b190:

Critical Bugs Fixed:

  • ✅ PlaybackTz advancement timing corrected (uses pre-advancement value for interpolation)
  • ✅ Condition logic fixed (fireTz >= 0 && IsPlaying() for proper initial sample)
  • ✅ Length check added for fireSample in interpolated path

API Improvements:

  • ✅ Added SetFireWithPitchADSR() for full control over pitch + ADSR
  • ✅ Simplified SetFireWithPitch() to not require ADSR parameters
  • ✅ Refactored to use shared setFireInternal() helper

Testing & Documentation:

  • ✅ Removed extra blank lines
  • ✅ Added comprehensive edge case tests (boundary, beyond length, volume/pan)
  • ✅ Added integration test for SetFireWithPitchADSR
  • ✅ Updated documentation with correct API signatures and examples

All tests pass ✅

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated 7 comments.

@charneykaye
Copy link
Contributor

@copilot apply changes based on the comments in this thread

- Fix fireStateReady to return current PlaybackTz before advancing (consistent with fireStatePlay)
- Add input validation: pitch parameter must be positive non-zero value
- Export HasPitchShift() method and use it in mixer for maintainability
- Add comprehensive edge case tests for pitch values (extreme, zero, negative)
- Update documentation to clarify valid pitch range
- Clarify PlaybackTz is for internal use only
- Remove extra closing backticks in documentation

Co-authored-by: charneykaye <8584836+charneykaye@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 8, 2026

@copilot apply changes based on the comments in this thread

Applied all review feedback in commit c2d6231:

Bug Fixes:

  • ✅ Fixed fireStateReady to return current PlaybackTz before advancing (consistent with fireStatePlay)
  • ✅ Removed extra closing backticks in documentation

Input Validation:

  • ✅ Added pitch parameter validation (must be positive non-zero, panics on invalid values)
  • ✅ Documented valid range in API comments and documentation

Code Quality:

  • ✅ Exported HasPitchShift() method and used it in mixer for maintainability
  • ✅ Added comprehensive edge case tests (extreme values, zero, negative)
  • ✅ Clarified PlaybackTz is for internal use only

All tests pass ✅

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

lib/fire/fire.go:102

  • In the fireStateRelease case, the PlaybackTz field is not being advanced. This means that if pitch shifting is enabled and the fire enters the release phase, it will keep returning the same sample position (nowTz) without advancing PlaybackTz. This could cause the release phase to play the same audio sample repeatedly instead of continuing through the source audio.

Consider whether PlaybackTz should continue to advance during the release phase to maintain proper audio playback, or if this behavior is intentional.

	case fireStateRelease:
		t = f.nowTz
		f.nowTz++
		// Check if release phase is complete
		if at >= f.releaseTz+f.Release {
			f.state = fireStateDone
		}

@charneykaye
Copy link
Contributor

@copilot apply changes based on the comments in this thread

- Add bounds checking in interpolate function for safer array access
- Fix comment to match HasPitchShift function name
- Simplify HasPitchShift logic (pitch validation ensures never 0)
- Add comprehensive unit tests for pitch shifting in Fire struct:
  * PlaybackTz advancement with different pitch values
  * EndTz calculation with pitch shifting
  * HasPitchShift() validation
  * pitchAdvancement() validation
  * State transitions with pitch shifting
  * Fractional position tracking

Co-authored-by: charneykaye <8584836+charneykaye@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 8, 2026

@copilot apply changes based on the comments in this thread

Applied all review feedback in commit ba308c8:

Safety Improvements:

  • ✅ Added bounds checking in interpolate function to prevent array access panics
  • ✅ Fixed comment to match HasPitchShift function name
  • ✅ Simplified HasPitchShift logic (validation ensures pitch is never 0)

Comprehensive Testing:

  • ✅ Added 6 new unit tests for pitch shifting in Fire struct:
    • TestPitchShiftPlaybackAdvancement - validates PlaybackTz advancement
    • TestPitchShiftEndTzCalculation - validates EndTz calculation with pitch
    • TestHasPitchShift - validates HasPitchShift() method
    • TestPitchAdvancement - validates pitchAdvancement() method
    • TestPitchShiftWithStateTransitions - validates state transitions (Ready→Play→Release→Done)
    • TestPitchShiftFractionalPositions - validates fractional position tracking

All tests pass ✅

@charneykaye charneykaye marked this pull request as ready for review January 8, 2026 22:53
@charneykaye charneykaye requested a review from Copilot January 8, 2026 22:53
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated 10 comments.

lib/mix/mix.go Outdated
Comment on lines 94 to 99
// timeStretch: multiplier for duration (1.0 = no change, 2.0 = twice as slow, 0.5 = twice as fast)
// This function uses a default ADSR envelope that has no effect (attack=0, decay=0, sustainLevel=1.0, release=0).
func SetFireWithPitch(source string, begin time.Duration, sustain time.Duration, volume float64, pan float64, pitch float64, timeStretch float64) *fire.Fire {
if pitch <= 0 {
panic("SetFireWithPitch: pitch must be a positive non-zero value")
}
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeStretch parameter is accepted by the API but is not actually used in the implementation. While this is documented in PITCH_SHIFTING.md as "currently unused", having a non-functional parameter in the public API can be confusing for users. Consider either removing this parameter until time-stretching is implemented, or adding validation to ensure it's set to 1.0 with a panic message explaining it's not yet implemented. This would provide clearer feedback to users who might try to use this feature.

Suggested change
// timeStretch: multiplier for duration (1.0 = no change, 2.0 = twice as slow, 0.5 = twice as fast)
// This function uses a default ADSR envelope that has no effect (attack=0, decay=0, sustainLevel=1.0, release=0).
func SetFireWithPitch(source string, begin time.Duration, sustain time.Duration, volume float64, pan float64, pitch float64, timeStretch float64) *fire.Fire {
if pitch <= 0 {
panic("SetFireWithPitch: pitch must be a positive non-zero value")
}
// timeStretch: multiplier for duration (1.0 = no change, 2.0 = twice as slow, 0.5 = twice as fast).
// NOTE: timeStretch is not yet implemented; currently only a value of 1.0 is supported.
// This function uses a default ADSR envelope that has no effect (attack=0, decay=0, sustainLevel=1.0, release=0).
func SetFireWithPitch(source string, begin time.Duration, sustain time.Duration, volume float64, pan float64, pitch float64, timeStretch float64) *fire.Fire {
if pitch <= 0 {
panic("SetFireWithPitch: pitch must be a positive non-zero value")
}
if timeStretch != 1.0 {
panic("SetFireWithPitch: timeStretch != 1.0 is not yet implemented; only timeStretch = 1.0 is currently supported")
}

Copilot uses AI. Check for mistakes.
// pitch: multiplier for pitch (1.0 = no change, 2.0 = up one octave, 0.5 = down one octave)
// Must be a positive non-zero value. Values < 0.01 or > 100 are not recommended.
// timeStretch: multiplier for duration (1.0 = no change, 2.0 = twice as slow, 0.5 = twice as fast)
func SetFireWithPitchADSR(source string, begin time.Duration, sustain time.Duration, volume float64, pan float64, attack time.Duration, decay time.Duration, sustainLevel float64, release time.Duration, pitch float64, timeStretch float64) *fire.Fire {
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeStretch parameter is accepted by the API but is not actually used in the implementation. While this is documented in PITCH_SHIFTING.md as "currently unused", having a non-functional parameter in the public API can be confusing for users. Consider either removing this parameter until time-stretching is implemented, or adding validation to ensure it's set to 1.0 with a panic message explaining it's not yet implemented. This would provide clearer feedback to users who might try to use this feature.

Copilot uses AI. Check for mistakes.
// ADSR envelope control and pitch shifting / time stretching.
// pitch: multiplier for pitch (1.0 = no change, 2.0 = up one octave, 0.5 = down one octave)
// timeStretch: multiplier for duration (1.0 = no change, 2.0 = twice as slow, 0.5 = twice as fast)
func SetFireWithPitchADSR(source string, begin time.Duration, sustain time.Duration, volume float64, pan float64, attack time.Duration, decay time.Duration, sustainLevel float64, release time.Duration, pitch float64, timeStretch float64) *fire.Fire {
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeStretch parameter is accepted by the API but is not actually used in the implementation. While this is documented in PITCH_SHIFTING.md as "currently unused", having a non-functional parameter in the public API can be confusing for users. Consider either removing this parameter until time-stretching is implemented, or adding validation to ensure it's set to 1.0 with a panic message explaining it's not yet implemented. This would provide clearer feedback to users who might try to use this feature.

Copilot uses AI. Check for mistakes.
Comment on lines 555 to 585
fire1.At(bgnTz)
fire1.At(bgnTz + 1) // This should calculate EndTz

// EndTz should be calculated based on pitch adjustment
// With 2.0x pitch, duration should be half
assert.True(t, fire1.EndTz > 0, "EndTz should be calculated")

// Test with pitch shift down (0.5x) and no explicit endTz
fire2 := New(src, bgnTz, 0, 1.0, 0, 0, 0, 1.0, 0, 0.5, 1.0)

// Start playing
fire2.At(bgnTz)
fire2.At(bgnTz + 1)

// EndTz should be calculated with pitch adjustment
// With 0.5x pitch, duration should be double
assert.True(t, fire2.EndTz > 0, "EndTz should be calculated")
}

func TestHasPitchShift(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)

// Test with no pitch shift (1.0)
fire1 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.0, 1.0)
assert.False(t, fire1.HasPitchShift(), "Pitch 1.0 should not be considered pitch shift")

// Test with pitch shift up
fire2 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 2.0, 1.0)
assert.True(t, fire2.HasPitchShift(), "Pitch 2.0 should be considered pitch shift")
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test function body is not properly indented. All lines from 547 to 571 should be indented with tabs to follow Go formatting conventions.

Suggested change
fire1.At(bgnTz)
fire1.At(bgnTz + 1) // This should calculate EndTz
// EndTz should be calculated based on pitch adjustment
// With 2.0x pitch, duration should be half
assert.True(t, fire1.EndTz > 0, "EndTz should be calculated")
// Test with pitch shift down (0.5x) and no explicit endTz
fire2 := New(src, bgnTz, 0, 1.0, 0, 0, 0, 1.0, 0, 0.5, 1.0)
// Start playing
fire2.At(bgnTz)
fire2.At(bgnTz + 1)
// EndTz should be calculated with pitch adjustment
// With 0.5x pitch, duration should be double
assert.True(t, fire2.EndTz > 0, "EndTz should be calculated")
}
func TestHasPitchShift(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)
// Test with no pitch shift (1.0)
fire1 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.0, 1.0)
assert.False(t, fire1.HasPitchShift(), "Pitch 1.0 should not be considered pitch shift")
// Test with pitch shift up
fire2 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 2.0, 1.0)
assert.True(t, fire2.HasPitchShift(), "Pitch 2.0 should be considered pitch shift")
fire1.At(bgnTz)
fire1.At(bgnTz + 1) // This should calculate EndTz
// EndTz should be calculated based on pitch adjustment
// With 2.0x pitch, duration should be half
assert.True(t, fire1.EndTz > 0, "EndTz should be calculated")
// Test with pitch shift down (0.5x) and no explicit endTz
fire2 := New(src, bgnTz, 0, 1.0, 0, 0, 0, 1.0, 0, 0.5, 1.0)
// Start playing
fire2.At(bgnTz)
fire2.At(bgnTz + 1)
// EndTz should be calculated with pitch adjustment
// With 0.5x pitch, duration should be double
assert.True(t, fire2.EndTz > 0, "EndTz should be calculated")
}
func TestHasPitchShift(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)
// Test with no pitch shift (1.0)
fire1 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.0, 1.0)
assert.False(t, fire1.HasPitchShift(), "Pitch 1.0 should not be considered pitch shift")
// Test with pitch shift up
fire2 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 2.0, 1.0)
assert.True(t, fire2.HasPitchShift(), "Pitch 2.0 should be considered pitch shift")

Copilot uses AI. Check for mistakes.
Comment on lines 580 to 605
fire1 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.0, 1.0)
assert.False(t, fire1.HasPitchShift(), "Pitch 1.0 should not be considered pitch shift")

// Test with pitch shift up
fire2 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 2.0, 1.0)
assert.True(t, fire2.HasPitchShift(), "Pitch 2.0 should be considered pitch shift")

// Test with pitch shift down
fire3 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 0.5, 1.0)
assert.True(t, fire3.HasPitchShift(), "Pitch 0.5 should be considered pitch shift")

// Test with slight pitch shift
fire4 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.1, 1.0)
assert.True(t, fire4.HasPitchShift(), "Pitch 1.1 should be considered pitch shift")
}

func TestPitchAdvancement(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)

// Test no pitch shift returns 1.0
fire1 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.0, 1.0)
assert.Equal(t, 1.0, fire1.pitchAdvancement())

// Test pitch shift returns pitch value
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test function body is not properly indented. All lines from 575 to 593 should be indented with tabs to follow Go formatting conventions.

Suggested change
fire1 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.0, 1.0)
assert.False(t, fire1.HasPitchShift(), "Pitch 1.0 should not be considered pitch shift")
// Test with pitch shift up
fire2 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 2.0, 1.0)
assert.True(t, fire2.HasPitchShift(), "Pitch 2.0 should be considered pitch shift")
// Test with pitch shift down
fire3 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 0.5, 1.0)
assert.True(t, fire3.HasPitchShift(), "Pitch 0.5 should be considered pitch shift")
// Test with slight pitch shift
fire4 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.1, 1.0)
assert.True(t, fire4.HasPitchShift(), "Pitch 1.1 should be considered pitch shift")
}
func TestPitchAdvancement(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)
// Test no pitch shift returns 1.0
fire1 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.0, 1.0)
assert.Equal(t, 1.0, fire1.pitchAdvancement())
// Test pitch shift returns pitch value
fire1 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.0, 1.0)
assert.False(t, fire1.HasPitchShift(), "Pitch 1.0 should not be considered pitch shift")
// Test with pitch shift up
fire2 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 2.0, 1.0)
assert.True(t, fire2.HasPitchShift(), "Pitch 2.0 should be considered pitch shift")
// Test with pitch shift down
fire3 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 0.5, 1.0)
assert.True(t, fire3.HasPitchShift(), "Pitch 0.5 should be considered pitch shift")
// Test with slight pitch shift
fire4 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.1, 1.0)
assert.True(t, fire4.HasPitchShift(), "Pitch 1.1 should be considered pitch shift")
}
func TestPitchAdvancement(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)
// Test no pitch shift returns 1.0
fire1 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.0, 1.0)
assert.Equal(t, 1.0, fire1.pitchAdvancement())
// Test pitch shift returns pitch value

Copilot uses AI. Check for mistakes.
Comment on lines 597 to 675
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)

// Test no pitch shift returns 1.0
fire1 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.0, 1.0)
assert.Equal(t, 1.0, fire1.pitchAdvancement())

// Test pitch shift returns pitch value
fire2 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 2.0, 1.0)
assert.Equal(t, 2.0, fire2.pitchAdvancement())

fire3 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 0.5, 1.0)
assert.Equal(t, 0.5, fire3.pitchAdvancement())
}

func TestPitchShiftWithStateTransitions(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(110) // Short duration
release := spec.Tz(5)

// Test pitch shifting with ADSR and state transitions
fire := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, release, 2.0, 1.0)

// Before start - should be in Ready state
assert.Equal(t, fireStateReady, fire.state)
assert.Equal(t, 0.0, fire.PlaybackTz)

// Start playing - transition to Play state
result := fire.At(bgnTz)
assert.Equal(t, spec.Tz(0), result)
assert.Equal(t, fireStatePlay, fire.state)
assert.Equal(t, 2.0, fire.PlaybackTz) // Advanced by pitch

// Continue playing with pitch shift
for i := spec.Tz(1); i < 10; i++ {
fire.At(bgnTz + i)
}

// Should have advanced playback position significantly due to 2.0x pitch
assert.True(t, fire.PlaybackTz >= 20.0, "PlaybackTz should advance by pitch multiplier")

// Play until end - should transition to Release state
fire.At(endTz)
assert.Equal(t, fireStateRelease, fire.state)

// Complete release
for i := spec.Tz(1); i <= release; i++ {
fire.At(endTz + i)
}

// Should be done
assert.Equal(t, fireStateDone, fire.state)
}

func TestPitchShiftFractionalPositions(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)

// Test that fractional playback positions are maintained correctly
fire := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.5, 1.0)

// Start
fire.At(bgnTz)
assert.Equal(t, 1.5, fire.PlaybackTz)

// Second sample
fire.At(bgnTz + 1)
assert.Equal(t, 3.0, fire.PlaybackTz) // 1.5 * 2

// Third sample
fire.At(bgnTz + 2)
assert.Equal(t, 4.5, fire.PlaybackTz) // 1.5 * 3

// Fourth sample
fire.At(bgnTz + 3)
assert.Equal(t, 6.0, fire.PlaybackTz) // 1.5 * 4
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test function body is not properly indented. All lines from 597 to 610 should be indented with tabs to follow Go formatting conventions.

Suggested change
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)
// Test no pitch shift returns 1.0
fire1 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.0, 1.0)
assert.Equal(t, 1.0, fire1.pitchAdvancement())
// Test pitch shift returns pitch value
fire2 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 2.0, 1.0)
assert.Equal(t, 2.0, fire2.pitchAdvancement())
fire3 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 0.5, 1.0)
assert.Equal(t, 0.5, fire3.pitchAdvancement())
}
func TestPitchShiftWithStateTransitions(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(110) // Short duration
release := spec.Tz(5)
// Test pitch shifting with ADSR and state transitions
fire := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, release, 2.0, 1.0)
// Before start - should be in Ready state
assert.Equal(t, fireStateReady, fire.state)
assert.Equal(t, 0.0, fire.PlaybackTz)
// Start playing - transition to Play state
result := fire.At(bgnTz)
assert.Equal(t, spec.Tz(0), result)
assert.Equal(t, fireStatePlay, fire.state)
assert.Equal(t, 2.0, fire.PlaybackTz) // Advanced by pitch
// Continue playing with pitch shift
for i := spec.Tz(1); i < 10; i++ {
fire.At(bgnTz + i)
}
// Should have advanced playback position significantly due to 2.0x pitch
assert.True(t, fire.PlaybackTz >= 20.0, "PlaybackTz should advance by pitch multiplier")
// Play until end - should transition to Release state
fire.At(endTz)
assert.Equal(t, fireStateRelease, fire.state)
// Complete release
for i := spec.Tz(1); i <= release; i++ {
fire.At(endTz + i)
}
// Should be done
assert.Equal(t, fireStateDone, fire.state)
}
func TestPitchShiftFractionalPositions(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)
// Test that fractional playback positions are maintained correctly
fire := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.5, 1.0)
// Start
fire.At(bgnTz)
assert.Equal(t, 1.5, fire.PlaybackTz)
// Second sample
fire.At(bgnTz + 1)
assert.Equal(t, 3.0, fire.PlaybackTz) // 1.5 * 2
// Third sample
fire.At(bgnTz + 2)
assert.Equal(t, 4.5, fire.PlaybackTz) // 1.5 * 3
// Fourth sample
fire.At(bgnTz + 3)
assert.Equal(t, 6.0, fire.PlaybackTz) // 1.5 * 4
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)
// Test no pitch shift returns 1.0
fire1 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.0, 1.0)
assert.Equal(t, 1.0, fire1.pitchAdvancement())
// Test pitch shift returns pitch value
fire2 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 2.0, 1.0)
assert.Equal(t, 2.0, fire2.pitchAdvancement())
fire3 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 0.5, 1.0)
assert.Equal(t, 0.5, fire3.pitchAdvancement())
}
func TestPitchShiftWithStateTransitions(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(110) // Short duration
release := spec.Tz(5)
// Test pitch shifting with ADSR and state transitions
fire := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, release, 2.0, 1.0)
// Before start - should be in Ready state
assert.Equal(t, fireStateReady, fire.state)
assert.Equal(t, 0.0, fire.PlaybackTz)
// Start playing - transition to Play state
result := fire.At(bgnTz)
assert.Equal(t, spec.Tz(0), result)
assert.Equal(t, fireStatePlay, fire.state)
assert.Equal(t, 2.0, fire.PlaybackTz) // Advanced by pitch
// Continue playing with pitch shift
for i := spec.Tz(1); i < 10; i++ {
fire.At(bgnTz + i)
}
// Should have advanced playback position significantly due to 2.0x pitch
assert.True(t, fire.PlaybackTz >= 20.0, "PlaybackTz should advance by pitch multiplier")
// Play until end - should transition to Release state
fire.At(endTz)
assert.Equal(t, fireStateRelease, fire.state)
// Complete release
for i := spec.Tz(1); i <= release; i++ {
fire.At(endTz + i)
}
// Should be done
assert.Equal(t, fireStateDone, fire.state)
}
func TestPitchShiftFractionalPositions(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)
// Test that fractional playback positions are maintained correctly
fire := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.5, 1.0)
// Start
fire.At(bgnTz)
assert.Equal(t, 1.5, fire.PlaybackTz)
// Second sample
fire.At(bgnTz + 1)
assert.Equal(t, 3.0, fire.PlaybackTz) // 1.5 * 2
// Third sample
fire.At(bgnTz + 2)
assert.Equal(t, 4.5, fire.PlaybackTz) // 1.5 * 3
// Fourth sample
fire.At(bgnTz + 3)
assert.Equal(t, 6.0, fire.PlaybackTz) // 1.5 * 4

Copilot uses AI. Check for mistakes.
Comment on lines 527 to 556
result = fire1.At(bgnTz + 1)
assert.Equal(t, spec.Tz(2), result)
assert.Equal(t, 4.0, fire1.PlaybackTz)

// Test pitch shift down (0.5x)
fire2 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 0.5, 1.0)
assert.Equal(t, 0.5, fire2.Pitch)

// First call returns 0, advances to 0.5
result = fire2.At(bgnTz)
assert.Equal(t, spec.Tz(0), result)
assert.Equal(t, 0.5, fire2.PlaybackTz)

// Second call returns 0.5, advances to 1.0
result = fire2.At(bgnTz + 1)
assert.Equal(t, spec.Tz(0), result) // Truncated to 0
assert.Equal(t, 1.0, fire2.PlaybackTz)
}

func TestPitchShiftEndTzCalculation(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)

// Test with pitch shift up (2.0x) and no explicit endTz
fire1 := New(src, bgnTz, 0, 1.0, 0, 0, 0, 1.0, 0, 2.0, 1.0)
assert.Equal(t, spec.Tz(0), fire1.EndTz) // Initially 0

// Start playing
fire1.At(bgnTz)
fire1.At(bgnTz + 1) // This should calculate EndTz
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test function body is not properly indented. In Go code, function bodies should be indented with tabs. All lines from 511 to 543 need to be indented by one tab level to follow Go formatting conventions and match the style of other test functions in this file.

Suggested change
result = fire1.At(bgnTz + 1)
assert.Equal(t, spec.Tz(2), result)
assert.Equal(t, 4.0, fire1.PlaybackTz)
// Test pitch shift down (0.5x)
fire2 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 0.5, 1.0)
assert.Equal(t, 0.5, fire2.Pitch)
// First call returns 0, advances to 0.5
result = fire2.At(bgnTz)
assert.Equal(t, spec.Tz(0), result)
assert.Equal(t, 0.5, fire2.PlaybackTz)
// Second call returns 0.5, advances to 1.0
result = fire2.At(bgnTz + 1)
assert.Equal(t, spec.Tz(0), result) // Truncated to 0
assert.Equal(t, 1.0, fire2.PlaybackTz)
}
func TestPitchShiftEndTzCalculation(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)
// Test with pitch shift up (2.0x) and no explicit endTz
fire1 := New(src, bgnTz, 0, 1.0, 0, 0, 0, 1.0, 0, 2.0, 1.0)
assert.Equal(t, spec.Tz(0), fire1.EndTz) // Initially 0
// Start playing
fire1.At(bgnTz)
fire1.At(bgnTz + 1) // This should calculate EndTz
result = fire1.At(bgnTz + 1)
assert.Equal(t, spec.Tz(2), result)
assert.Equal(t, 4.0, fire1.PlaybackTz)
// Test pitch shift down (0.5x)
fire2 := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 0.5, 1.0)
assert.Equal(t, 0.5, fire2.Pitch)
// First call returns 0, advances to 0.5
result = fire2.At(bgnTz)
assert.Equal(t, spec.Tz(0), result)
assert.Equal(t, 0.5, fire2.PlaybackTz)
// Second call returns 0.5, advances to 1.0
result = fire2.At(bgnTz + 1)
assert.Equal(t, spec.Tz(0), result) // Truncated to 0
assert.Equal(t, 1.0, fire2.PlaybackTz)
}
func TestPitchShiftEndTzCalculation(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)
// Test with pitch shift up (2.0x) and no explicit endTz
fire1 := New(src, bgnTz, 0, 1.0, 0, 0, 0, 1.0, 0, 2.0, 1.0)
assert.Equal(t, spec.Tz(0), fire1.EndTz) // Initially 0
// Start playing
fire1.At(bgnTz)
fire1.At(bgnTz + 1) // This should calculate EndTz

Copilot uses AI. Check for mistakes.
Comment on lines 637 to 675
// Should have advanced playback position significantly due to 2.0x pitch
assert.True(t, fire.PlaybackTz >= 20.0, "PlaybackTz should advance by pitch multiplier")

// Play until end - should transition to Release state
fire.At(endTz)
assert.Equal(t, fireStateRelease, fire.state)

// Complete release
for i := spec.Tz(1); i <= release; i++ {
fire.At(endTz + i)
}

// Should be done
assert.Equal(t, fireStateDone, fire.state)
}

func TestPitchShiftFractionalPositions(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)

// Test that fractional playback positions are maintained correctly
fire := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.5, 1.0)

// Start
fire.At(bgnTz)
assert.Equal(t, 1.5, fire.PlaybackTz)

// Second sample
fire.At(bgnTz + 1)
assert.Equal(t, 3.0, fire.PlaybackTz) // 1.5 * 2

// Third sample
fire.At(bgnTz + 2)
assert.Equal(t, 4.5, fire.PlaybackTz) // 1.5 * 3

// Fourth sample
fire.At(bgnTz + 3)
assert.Equal(t, 6.0, fire.PlaybackTz) // 1.5 * 4
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test function body is not properly indented. All lines from 614 to 650 should be indented with tabs to follow Go formatting conventions.

Suggested change
// Should have advanced playback position significantly due to 2.0x pitch
assert.True(t, fire.PlaybackTz >= 20.0, "PlaybackTz should advance by pitch multiplier")
// Play until end - should transition to Release state
fire.At(endTz)
assert.Equal(t, fireStateRelease, fire.state)
// Complete release
for i := spec.Tz(1); i <= release; i++ {
fire.At(endTz + i)
}
// Should be done
assert.Equal(t, fireStateDone, fire.state)
}
func TestPitchShiftFractionalPositions(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)
// Test that fractional playback positions are maintained correctly
fire := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.5, 1.0)
// Start
fire.At(bgnTz)
assert.Equal(t, 1.5, fire.PlaybackTz)
// Second sample
fire.At(bgnTz + 1)
assert.Equal(t, 3.0, fire.PlaybackTz) // 1.5 * 2
// Third sample
fire.At(bgnTz + 2)
assert.Equal(t, 4.5, fire.PlaybackTz) // 1.5 * 3
// Fourth sample
fire.At(bgnTz + 3)
assert.Equal(t, 6.0, fire.PlaybackTz) // 1.5 * 4
// Should have advanced playback position significantly due to 2.0x pitch
assert.True(t, fire.PlaybackTz >= 20.0, "PlaybackTz should advance by pitch multiplier")
// Play until end - should transition to Release state
fire.At(endTz)
assert.Equal(t, fireStateRelease, fire.state)
// Complete release
for i := spec.Tz(1); i <= release; i++ {
fire.At(endTz + i)
}
// Should be done
assert.Equal(t, fireStateDone, fire.state)
}
func TestPitchShiftFractionalPositions(t *testing.T) {
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)
// Test that fractional playback positions are maintained correctly
fire := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.5, 1.0)
// Start
fire.At(bgnTz)
assert.Equal(t, 1.5, fire.PlaybackTz)
// Second sample
fire.At(bgnTz + 1)
assert.Equal(t, 3.0, fire.PlaybackTz) // 1.5 * 2
// Third sample
fire.At(bgnTz + 2)
assert.Equal(t, 4.5, fire.PlaybackTz) // 1.5 * 3
// Fourth sample
fire.At(bgnTz + 3)
assert.Equal(t, 6.0, fire.PlaybackTz) // 1.5 * 4

Copilot uses AI. Check for mistakes.
Comment on lines 654 to 675
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)

// Test that fractional playback positions are maintained correctly
fire := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.5, 1.0)

// Start
fire.At(bgnTz)
assert.Equal(t, 1.5, fire.PlaybackTz)

// Second sample
fire.At(bgnTz + 1)
assert.Equal(t, 3.0, fire.PlaybackTz) // 1.5 * 2

// Third sample
fire.At(bgnTz + 2)
assert.Equal(t, 4.5, fire.PlaybackTz) // 1.5 * 3

// Fourth sample
fire.At(bgnTz + 3)
assert.Equal(t, 6.0, fire.PlaybackTz) // 1.5 * 4
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test function body is not properly indented. All lines from 654 to 675 should be indented with tabs to follow Go formatting conventions.

Suggested change
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)
// Test that fractional playback positions are maintained correctly
fire := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.5, 1.0)
// Start
fire.At(bgnTz)
assert.Equal(t, 1.5, fire.PlaybackTz)
// Second sample
fire.At(bgnTz + 1)
assert.Equal(t, 3.0, fire.PlaybackTz) // 1.5 * 2
// Third sample
fire.At(bgnTz + 2)
assert.Equal(t, 4.5, fire.PlaybackTz) // 1.5 * 3
// Fourth sample
fire.At(bgnTz + 3)
assert.Equal(t, 6.0, fire.PlaybackTz) // 1.5 * 4
src := "test.wav"
bgnTz := spec.Tz(100)
endTz := spec.Tz(200)
// Test that fractional playback positions are maintained correctly
fire := New(src, bgnTz, endTz, 1.0, 0, 0, 0, 1.0, 0, 1.5, 1.0)
// Start
fire.At(bgnTz)
assert.Equal(t, 1.5, fire.PlaybackTz)
// Second sample
fire.At(bgnTz + 1)
assert.Equal(t, 3.0, fire.PlaybackTz) // 1.5 * 2
// Third sample
fire.At(bgnTz + 2)
assert.Equal(t, 4.5, fire.PlaybackTz) // 1.5 * 3
// Fourth sample
fire.At(bgnTz + 3)
assert.Equal(t, 6.0, fire.PlaybackTz) // 1.5 * 4

Copilot uses AI. Check for mistakes.
mix.go Outdated
Comment on lines 213 to 223
// timeStretch: multiplier for duration (1.0 = no change, 2.0 = twice as slow, 0.5 = twice as fast)
// This function uses a default ADSR envelope that has no effect (attack=0, decay=0, sustainLevel=1.0, release=0).
func SetFireWithPitch(source string, begin time.Duration, sustain time.Duration, volume float64, pan float64, pitch float64, timeStretch float64) *fire.Fire {
return mix.SetFireWithPitch(source, begin, sustain, volume, pan, pitch, timeStretch)
}

// SetFireWithPitchADSR represents a single audio source playing at a specific time with both
// ADSR envelope control and pitch shifting / time stretching.
// pitch: multiplier for pitch (1.0 = no change, 2.0 = up one octave, 0.5 = down one octave)
// timeStretch: multiplier for duration (1.0 = no change, 2.0 = twice as slow, 0.5 = twice as fast)
func SetFireWithPitchADSR(source string, begin time.Duration, sustain time.Duration, volume float64, pan float64, attack time.Duration, decay time.Duration, sustainLevel float64, release time.Duration, pitch float64, timeStretch float64) *fire.Fire {
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeStretch parameter is accepted by the API but is not actually used in the implementation. While this is documented in PITCH_SHIFTING.md as "currently unused", having a non-functional parameter in the public API can be confusing for users. Consider either removing this parameter until time-stretching is implemented, or adding validation to ensure it's set to 1.0 with a panic message explaining it's not yet implemented. This would provide clearer feedback to users who might try to use this feature.

Suggested change
// timeStretch: multiplier for duration (1.0 = no change, 2.0 = twice as slow, 0.5 = twice as fast)
// This function uses a default ADSR envelope that has no effect (attack=0, decay=0, sustainLevel=1.0, release=0).
func SetFireWithPitch(source string, begin time.Duration, sustain time.Duration, volume float64, pan float64, pitch float64, timeStretch float64) *fire.Fire {
return mix.SetFireWithPitch(source, begin, sustain, volume, pan, pitch, timeStretch)
}
// SetFireWithPitchADSR represents a single audio source playing at a specific time with both
// ADSR envelope control and pitch shifting / time stretching.
// pitch: multiplier for pitch (1.0 = no change, 2.0 = up one octave, 0.5 = down one octave)
// timeStretch: multiplier for duration (1.0 = no change, 2.0 = twice as slow, 0.5 = twice as fast)
func SetFireWithPitchADSR(source string, begin time.Duration, sustain time.Duration, volume float64, pan float64, attack time.Duration, decay time.Duration, sustainLevel float64, release time.Duration, pitch float64, timeStretch float64) *fire.Fire {
// timeStretch: multiplier for duration (currently must be 1.0; time-stretching is not yet implemented)
// This function uses a default ADSR envelope that has no effect (attack=0, decay=0, sustainLevel=1.0, release=0).
func SetFireWithPitch(source string, begin time.Duration, sustain time.Duration, volume float64, pan float64, pitch float64, timeStretch float64) *fire.Fire {
if timeStretch != 1.0 {
panic("mix: timeStretch is not yet implemented; timeStretch must be 1.0")
}
return mix.SetFireWithPitch(source, begin, sustain, volume, pan, pitch, timeStretch)
}
// SetFireWithPitchADSR represents a single audio source playing at a specific time with both
// ADSR envelope control and pitch shifting / time stretching.
// pitch: multiplier for pitch (1.0 = no change, 2.0 = up one octave, 0.5 = down one octave)
// timeStretch: multiplier for duration (currently must be 1.0; time-stretching is not yet implemented)
func SetFireWithPitchADSR(source string, begin time.Duration, sustain time.Duration, volume float64, pan float64, attack time.Duration, decay time.Duration, sustainLevel float64, release time.Duration, pitch float64, timeStretch float64) *fire.Fire {
if timeStretch != 1.0 {
panic("mix: timeStretch is not yet implemented; timeStretch must be 1.0")
}

Copilot uses AI. Check for mistakes.
@charneykaye
Copy link
Contributor

@copilot apply changes based on the comments in this thread

- Add timeStretch validation: must be 1.0 (time-stretching not yet implemented)
- Add validation in SetFireWithPitch() and SetFireWithPitchADSR() at both lib/mix and public API levels
- Fix indentation in test functions (run gofmt on fire_test.go)
- Add tests for timeStretch validation (panics when != 1.0)
- Update documentation comments to clarify timeStretch constraint

Co-authored-by: charneykaye <8584836+charneykaye@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 21, 2026

@copilot apply changes based on the comments in this thread

Applied all review feedback in commit dbf2aba:

timeStretch Validation:

  • ✅ Added validation in SetFireWithPitch() - panics if timeStretch != 1.0
  • ✅ Added validation in SetFireWithPitchADSR() - panics if timeStretch != 1.0
  • ✅ Applied at both lib/mix and public API levels
  • ✅ Updated documentation comments to clarify timeStretch must be 1.0
  • ✅ Added tests for timeStretch validation

Code Formatting:

  • ✅ Fixed indentation in all test functions (ran gofmt on fire_test.go)
  • ✅ All test functions now properly indented with tabs

All tests pass ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Audio time-scale/pitch modification

2 participants