audio_effects.f90 Source File


This file depends on

sourcefile~~audio_effects.f90~~EfferentGraph sourcefile~audio_effects.f90 audio_effects.f90 sourcefile~acoustics.f90 acoustics.f90 sourcefile~audio_effects.f90->sourcefile~acoustics.f90 sourcefile~forsynth.f90 forsynth.f90 sourcefile~audio_effects.f90->sourcefile~forsynth.f90 sourcefile~tape_recorder_class.f90 tape_recorder_class.f90 sourcefile~audio_effects.f90->sourcefile~tape_recorder_class.f90 sourcefile~acoustics.f90->sourcefile~forsynth.f90 sourcefile~tape_recorder_class.f90->sourcefile~forsynth.f90

Files dependent on this one

sourcefile~~audio_effects.f90~~AfferentGraph sourcefile~audio_effects.f90 audio_effects.f90 sourcefile~blues.f90 blues.f90 sourcefile~blues.f90->sourcefile~audio_effects.f90 sourcefile~chords_and_melody.f90 chords_and_melody.f90 sourcefile~chords_and_melody.f90->sourcefile~audio_effects.f90 sourcefile~demo_effects.f90 demo_effects.f90 sourcefile~demo_effects.f90->sourcefile~audio_effects.f90 sourcefile~shepard_scale.f90 shepard_scale.f90 sourcefile~shepard_scale.f90->sourcefile~audio_effects.f90

Source Code

! Forsynth: a multitracks stereo sound synthesis project
! License GPL-3.0-or-later
! Vincent Magnin
! Last modifications: 2025-03-19

!> Various audio effects
module audio_effects
    use forsynth, only: wp, RATE, PI, dt
    use tape_recorder_class
    use acoustics, only: dB_to_linear, linear_to_db

    implicit none

    private

    public :: apply_delay_effect, apply_fuzz_effect, apply_tremolo_effect, &
            & apply_autopan_effect, apply_reverse_effect, apply_dynamic_effect

contains

    !> Add the sound from "delay" seconds before,
    !> and multiply by Amp<1 for dampening.
    subroutine apply_delay_effect(tape, track, t1, t2, delay, Amp)
        type(tape_recorder), intent(inout) :: tape
        integer, intent(in)  :: track
        real(wp), intent(in) :: t1, t2, delay, Amp
        integer              :: i, j
        integer              :: id

        ! Delay as an integer:
        id = nint(delay / dt)

        ! Can not be parallelized:
        do i = nint(t1*RATE), min(nint(t2*RATE), tape%last)
            j = i - id
            if (j > 0) then
                tape%left(track,  i) = tape%left(track,  i) + Amp * tape%left(track,  j)
                tape%right(track, i) = tape%right(track, i) + Amp * tape%right(track, j)
            end if
        end do
    end subroutine

    !> Apply distorsion with hard clipping
    !> https://en.wikipedia.org/wiki/Distortion_(music)
    subroutine apply_fuzz_effect(tape, track, t1, t2, level)
        type(tape_recorder), intent(inout) :: tape
        integer, intent(in)  :: track
        real(wp), intent(in) :: t1, t2, level
        integer              :: i

        do concurrent(i = nint(t1*RATE) : min(nint(t2*RATE), tape%last))
            if (abs(tape%left(track,  i)) > level) then
                tape%left(track,  i) = sign(level, tape%left(track,  i))
            end if
            if (abs(tape%right(track, i)) > level) then
                tape%right(track, i) = sign(level, tape%right(track, i))
            end if
        end do
    end subroutine

    !> A sinusoidal modulation of the amplitude of a signal (tremolo) :
    !> f : tremolo frequency (typically a few Hz)
    !> AmpLFO : tremolo amplitude in [0 ; 1]
    !> https://en.wikipedia.org/wiki/Vibrato#Vibrato_and_tremolo/
    subroutine apply_tremolo_effect(tape, track, t1, t2, f, AmpLFO)
        type(tape_recorder), intent(inout) :: tape
        integer, intent(in)  :: track
        real(wp), intent(in) :: t1, t2, f, AmpLFO
        integer  :: i
        real(wp) :: omegaLFO
        real(wp) :: t

        omegaLFO = 2 * PI * f

        do concurrent(i = nint(t1*RATE) : min(nint(t2*RATE), tape%last))
            t = (i - nint(t1*RATE)) * dt
            tape%left(track,  i) = tape%left(track,  i) * (1.0_wp - AmpLFO*sin(omegaLFO*t))
            tape%right(track, i) = tape%right(track, i) * (1.0_wp - AmpLFO*sin(omegaLFO*t))
        end do
    end subroutine

    !> Make the sound move from one channel to the other one at a frequency f
    !> and with an amplitude AmpLFO in [0 ; 1].
    subroutine apply_autopan_effect(tape, track, t1, t2, f, AmpLFO)
        type(tape_recorder), intent(inout) :: tape
        integer, intent(in)  :: track
        real(wp), intent(in) :: t1, t2, f, AmpLFO
        real(wp), parameter  :: phi = 0.0_wp
        integer  :: i
        real(wp) :: omegaLFO
        real(wp) :: t

        omegaLFO = 2 * PI * f

        do concurrent(i = nint(t1*RATE) : min(nint(t2*RATE), tape%last))
            t = (i - nint(t1*RATE)) * dt
            tape%left(track,  i) = tape%left(track,  i) * (1.0_wp - AmpLFO * sin(omegaLFO*t + phi))
            tape%right(track, i) = tape%right(track, i) * (1.0_wp - AmpLFO * cos(omegaLFO*t + phi))
        end do
    end subroutine

    !> Copy the samples at the same t1 but in reverse order:
    subroutine apply_reverse_effect(tape, track, t1, t2)
        type(tape_recorder), intent(inout) :: tape
        integer, intent(in)  :: track
        real(wp), intent(in) :: t1, t2
        integer              :: i, i1, i2

        i1 = nint(t1*RATE)
        i2 = min(nint(t2*RATE), tape%last)

        ! Track 0 is used as an auxiliary track:
        do concurrent(i = i1:i2)
            tape%left(0,  i) = tape%left(track,  i1-i+i2)
            tape%right(0, i) = tape%right(track, i1-i+i2)
        end do
        ! Transfer on the good track:
        tape%left(track,  i1:i2) = tape%left(0,  i1:i2)
        tape%right(track, i1:i2) = tape%right(0, i1:i2)
    end subroutine

    !> A basic dynamic effect with hard knee, and only two parameters :
    !> the threshold > 0 expressed linearly (not in dB) and the ratio.
    !> It is a compressor if the ratio is > 1.
    !> It can also be used as a limiter with a ratio >= 10.
    !> Or an upward expander with a ratio < 1.
    !> By default, the ratio is applied above the threshold, but the "below"
    !> optional parameter can be used to reverse it and obtain:
    !> - an upward compressor with ratio < 1
    !> - a (downward) expander with ratio > 1.
    !> There are no attack and release parameters at this time.
    !> https://en.wikipedia.org/wiki/Dynamic_range_compression
    subroutine apply_dynamic_effect(tape, track, t1, t2, threshold, ratio, below)
        type(tape_recorder), intent(inout) :: tape
        integer, intent(in)  :: track
        real(wp), intent(in) :: t1, t2, threshold, ratio
        logical, intent(in), optional :: below
        real(wp)             :: signal, thr_db
        integer              :: i

        thr_db = linear_to_db(threshold)

        do concurrent(i = nint(t1*RATE) : min(nint(t2*RATE), tape%last))
            associate(left => tape%left(track,  i), right => tape%right(track,  i))

            if (present(below)) then
                if (below) then  ! upward compression (ratio < 1) or (downward) expansion (ratio > 1)
                    signal = linear_to_db(left)
                    if (signal < thr_db) then
                        left = sign(dB_to_linear(thr_db - (thr_db - signal) * ratio), left)
                    end if

                    signal = linear_to_db(right)
                    if (signal < thr_db) then
                        right = sign(dB_to_linear(thr_db - (thr_db - signal) * ratio), right)
                    end if

                    cycle
                end if
            end if

            ! Else it is downward compression (ratio>1) or upward expansion (ratio<1)
            signal = linear_to_db(left)
            if (signal > thr_db) then
                left = sign(dB_to_linear(thr_db + (signal - thr_db) / ratio), left)
            end if

            signal = linear_to_db(right)
            if (signal > thr_db) then
                right = sign(dB_to_linear(thr_db + (signal - thr_db) / ratio), right)
            end if

            end associate
        end do
    end subroutine apply_dynamic_effect

end module audio_effects