An object-oriented programming language developed by Microsoft that can be used in .NET.
Hi @Marc Menzel ,
Thank you for your patience while the code was reviewed.
I have included below a revised sample that improves the chime sequence, overlapping bell decay, resonant partials, strike transient, and room ambience. The WAV output remains mono, 16-bit PCM at 44.1 kHz.
Please note that this is a sample synthesizer intended to produce a Westminster-style chime. It may not exactly reproduce the sound of the physical Westminster bells, since an exact match would require measured acoustic data or appropriately licensed recordings of the bells and tower.
Imports System
Imports System.Collections.Generic
Imports System.IO
Imports System.Linq
Imports System.Text
Imports BellStyle = BellSynth.BellStyle
Module Program
Sub Main(args As String())
Console.WriteLine("Generating all Westminster chime styles...")
Dim gen As New WestminsterChimeGenerator()
gen.GenerateAllStyles("Chimes")
Console.WriteLine("Done!")
End Sub
End Module
Public Class WestminsterChimeGenerator
Private Shared ReadOnly Changes As String()() = {
New String() {"G#4", "F#4", "E4", "B3"},
New String() {"E4", "G#4", "F#4", "B3"},
New String() {"E4", "F#4", "G#4", "E4"},
New String() {"G#4", "E4", "F#4", "B3"},
New String() {"B3", "F#4", "G#4", "E4"}
}
Private ReadOnly synth As New BellSynth()
Private ReadOnly notes As New Dictionary(Of String, Single())
Public Sub New()
synth.SampleRate = 44100
synth.DurationSeconds = 6.0
End Sub
Private Function NoteFreq(note As String) As Double
Select Case note
Case "E3" : Return 164.81
Case "B3" : Return 246.94
Case "E4" : Return 329.63
Case "F#4" : Return 369.99
Case "G#4" : Return 415.3
Case Else : Throw New ArgumentException("Unknown note: " & note)
End Select
End Function
Private Function GenerateBellNote(note As String) As Single()
If Not notes.ContainsKey(note) Then notes(note) = synth.GenerateBell(NoteFreq(note))
Return notes(note)
End Function
Private Sub AddNote(output As Single(), note As Single(), startTime As Double)
Dim startSample = CInt(startTime * synth.SampleRate)
For sampleIndex = 0 To note.Length - 1
output(startSample + sampleIndex) += note(sampleIndex)
Next
End Sub
Private Function MixChimes(changeIndexes As Integer()) As Single()
Dim beatSeconds = 0.667
Dim phraseSeconds = 4.0
Dim lengthSeconds = 0.1 + (changeIndexes.Length - 1) * phraseSeconds + 3 * beatSeconds + synth.DurationSeconds + 0.15
Dim output(CInt(Math.Ceiling(lengthSeconds * synth.SampleRate)) - 1) As Single
For changePosition = 0 To changeIndexes.Length - 1
Dim phrase = Changes(changeIndexes(changePosition))
For notePosition = 0 To phrase.Length - 1
AddNote(output, GenerateBellNote(phrase(notePosition)), 0.1 + changePosition * phraseSeconds + notePosition * beatSeconds)
Next
Next
Return output
End Function
Private Function MixStrikes(count As Integer) As Single()
synth.DurationSeconds = 11.0
Dim strike = GenerateBellNote("E3")
Dim strikeSpacing = 4.2
Dim output(CInt(Math.Ceiling(((count - 1) * strikeSpacing + synth.DurationSeconds + 2.0) * synth.SampleRate)) - 1) As Single
For strikeIndex = 0 To count - 1
AddNote(output, strike, strikeIndex * strikeSpacing)
Next
Return output
End Function
Private Sub AddRoomAmbience(samples As Single())
Dim delays = {1499, 1627, 1789, 2017, 2237, 2551}
Dim buffers = delays.Select(Function(delay) New Single(delay - 1) {}).ToArray()
Dim positions(delays.Length - 1) As Integer
Dim filtered(delays.Length - 1) As Single
For sampleIndex = 0 To samples.Length - 1
Dim dry = samples(sampleIndex)
Dim wet = 0.0
For delayIndex = 0 To delays.Length - 1
Dim position = positions(delayIndex)
Dim reflection = buffers(delayIndex)(position)
filtered(delayIndex) = CSng(filtered(delayIndex) * 0.45 + reflection * 0.55)
buffers(delayIndex)(position) = CSng(dry + filtered(delayIndex) * 0.84)
wet += reflection
positions(delayIndex) = If(position + 1 = delays(delayIndex), 0, position + 1)
Next
samples(sampleIndex) = CSng(dry * 0.86 + wet * 0.22 / delays.Length)
Next
End Sub
Private Sub Finish(samples As Single())
AddRoomAmbience(samples)
Normalize(samples)
End Sub
Private Sub Normalize(samples As Single())
Dim peak As Single = 0
For Each sample In samples
peak = Math.Max(peak, Math.Abs(sample))
Next
If peak > 0.88F Then
Dim gain = 0.88F / peak
For sampleIndex = 0 To samples.Length - 1
samples(sampleIndex) *= gain
Next
End If
End Sub
Public Sub GenerateAllStyles(rootFolder As String)
For Each style As BellStyle In [Enum].GetValues(GetType(BellStyle))
Dim folder = Path.Combine(rootFolder, style.ToString())
Directory.CreateDirectory(folder)
synth.Style = style
synth.DurationSeconds = 6.0
notes.Clear()
Console.WriteLine("Generating " & style.ToString() & " chime pack...")
GeneratePack(folder)
Next
End Sub
Private Sub GeneratePack(outputFolder As String)
Dim quarter = MixChimes({0})
Finish(quarter)
WavWriter.WriteWav(Path.Combine(outputFolder, "quarter.wav"), quarter, synth.SampleRate)
Dim half = MixChimes({1, 2})
Finish(half)
WavWriter.WriteWav(Path.Combine(outputFolder, "half.wav"), half, synth.SampleRate)
Dim threeQuarter = MixChimes({3, 4, 0})
Finish(threeQuarter)
WavWriter.WriteWav(Path.Combine(outputFolder, "threequarter.wav"), threeQuarter, synth.SampleRate)
Dim hour = MixChimes({1, 2, 3, 4})
Finish(hour)
WavWriter.WriteWav(Path.Combine(outputFolder, "hour.wav"), hour, synth.SampleRate)
Dim strikes = MixStrikes(12)
Finish(strikes)
Dim strike = GenerateBellNote("E3")
Dim singleStrike(strike.Length + 2 * synth.SampleRate - 1) As Single
Array.Copy(strike, singleStrike, strike.Length)
Finish(singleStrike)
WavWriter.WriteWav(Path.Combine(outputFolder, "strike.wav"), singleStrike, synth.SampleRate)
Dim pauseSamples = CInt(2.0 * synth.SampleRate)
Dim fullHour(hour.Length + pauseSamples + strikes.Length - 1) As Single
Array.Copy(hour, fullHour, hour.Length)
Array.Copy(strikes, 0, fullHour, hour.Length + pauseSamples, strikes.Length)
WavWriter.WriteWav(Path.Combine(outputFolder, "full_hour_12.wav"), fullHour, synth.SampleRate)
End Sub
End Class
Public Class WavWriter
Public Shared Sub WriteWav(path As String, samples As Single(), sampleRate As Integer)
Using fs As New FileStream(path, FileMode.Create, FileAccess.Write)
Using bw As New BinaryWriter(fs, Encoding.ASCII)
Dim byteRate = sampleRate * 2
Dim blockAlign = 2
Dim dataSize = samples.Length * 2
' RIFF header
bw.Write(Encoding.ASCII.GetBytes("RIFF"))
bw.Write(36 + dataSize)
bw.Write(Encoding.ASCII.GetBytes("WAVE"))
' fmt chunk
bw.Write(Encoding.ASCII.GetBytes("fmt "))
bw.Write(16)
bw.Write(CShort(1)) ' PCM
bw.Write(CShort(1)) ' mono
bw.Write(sampleRate)
bw.Write(byteRate)
bw.Write(CShort(blockAlign))
bw.Write(CShort(16)) ' bits per sample
' data chunk
bw.Write(Encoding.ASCII.GetBytes("data"))
bw.Write(dataSize)
For Each s In samples
Dim v = CInt(Math.Round(s * 32767.0))
If v > Short.MaxValue Then v = Short.MaxValue
If v < Short.MinValue Then v = Short.MinValue
bw.Write(CShort(v))
Next
End Using
End Using
End Sub
End Class
Public Class BellSynth
Public Enum BellStyle
Cathedral
Brass
Grandfather
Digital
End Enum
Public Property SampleRate As Integer = 44100
Public Property DurationSeconds As Double = 5.0
Public Property Style As BellStyle = BellStyle.Cathedral
Public Function GenerateBell(baseFreq As Double) As Single()
If Style = BellStyle.Cathedral AndAlso baseFreq >= 200.0 Then
Return GenerateQuarterBell(baseFreq)
End If
Dim totalSamples = CInt(SampleRate * DurationSeconds)
Dim samples(totalSamples - 1) As Single
Dim bell = GetStyleParameters(Style)
Dim isHourBell = baseFreq < 200.0
Dim sustain = If(isHourBell, 1.7, 1.0)
Dim random As New Random(CInt(baseFreq * 1000) + CInt(Style))
Dim lowPassNoise = 0.0
For sampleIndex = 0 To totalSamples - 1
Dim time = sampleIndex / CDbl(SampleRate)
Dim attack = 1.0 - Math.Exp(-time * 250.0)
Dim phase = 2.0 * Math.PI * baseFreq * time
Dim hum = bell.humAmp * 0.42 * Math.Exp(-time / (5.5 * sustain)) * Math.Sin(phase * bell.humFreqRatio)
Dim prime = bell.primeAmp * 0.95 * Math.Exp(-time / (4.5 * sustain)) * Math.Sin(phase * bell.primeFreqRatio + 0.3)
Dim tierce = bell.tierceAmp * 0.58 * Math.Exp(-time / (2.8 * sustain)) * Math.Sin(phase * bell.tierceFreqRatio + 1.2)
Dim quint = bell.quintAmp * 0.48 * Math.Exp(-time / (2.0 * sustain)) * Math.Sin(phase * bell.quintFreqRatio + 2.1)
Dim nominal = bell.nominalAmp * 0.76 * Math.Exp(-time / (3.2 * sustain)) * Math.Sin(phase * bell.nominalFreqRatio + 0.7)
Dim beating = bell.primeAmp * 0.17 * Math.Exp(-time / (4.0 * sustain)) * Math.Sin(phase * bell.primeFreqRatio * 1.0017 + 2.4)
Dim upperModes = 0.20 * Math.Exp(-time / 0.9) * Math.Sin(phase * 2.45 + 0.5) +
0.12 * Math.Exp(-time / 0.55) * Math.Sin(phase * 3.03 + 1.8) +
0.09 * Math.Exp(-time / 0.35) * Math.Sin(phase * 4.17 + 2.6)
Dim whiteNoise = random.NextDouble() * 2.0 - 1.0
lowPassNoise += 0.16 * (whiteNoise - lowPassNoise)
Dim impact = bell.transientAmount * 0.20 * Math.Exp(-time * 43.0) * (whiteNoise - lowPassNoise)
samples(sampleIndex) = CSng(0.24 * (attack * (hum + prime + tierce + quint + nominal + beating + upperModes) + impact))
Next
Return samples
End Function
Private Function GenerateQuarterBell(baseFreq As Double) As Single()
Dim ratios = {1.0, 2.0, 2.37, 4.0, 5.41, 5.92}
Dim amplitudes = {0.61, 1.0, 0.44, 0.72, 0.43, 0.65}
Dim decaySeconds = {1.4, 0.78, 0.76, 0.52, 0.4, 0.42}
Dim phases = {0.0, 0.3, 1.2, 2.1, 0.7, 1.7}
Dim samples(CInt(SampleRate * DurationSeconds) - 1) As Single
Dim random As New Random(CInt(baseFreq * 1000))
Dim filteredNoise = 0.0
For sampleIndex = 0 To samples.Length - 1
Dim time = sampleIndex / CDbl(SampleRate)
Dim phase = 2.0 * Math.PI * baseFreq * time
Dim ringing = 0.0
For partialIndex = 0 To ratios.Length - 1
ringing += amplitudes(partialIndex) * Math.Exp(-time / decaySeconds(partialIndex)) *
Math.Sin(phase * ratios(partialIndex) + phases(partialIndex))
Next
ringing += 0.09 * Math.Exp(-time / 0.78) * Math.Sin(phase * 2.002 + 2.4)
Dim noise = random.NextDouble() * 2.0 - 1.0
filteredNoise += 0.18 * (noise - filteredNoise)
Dim impact = 0.18 * Math.Exp(-time * 40.0) * (noise - filteredNoise)
Dim attack = 1.0 - Math.Exp(-time * 350.0)
samples(sampleIndex) = CSng(0.23 * (attack * ringing + impact))
Next
Return samples
End Function
Private Function GetStyleParameters(style As BellStyle) As (
humFreqRatio As Double, primeFreqRatio As Double, tierceFreqRatio As Double, quintFreqRatio As Double, nominalFreqRatio As Double,
humAmp As Double, primeAmp As Double, tierceAmp As Double, quintAmp As Double, nominalAmp As Double,
humDecay As Double, primeDecay As Double, tierceDecay As Double, quintDecay As Double, nominalDecay As Double,
transientAmount As Double, bloomSpeed As Double)
Select Case style
Case BellStyle.Cathedral
Return (
humFreqRatio:=0.5 * 0.997, primeFreqRatio:=1.0, tierceFreqRatio:=1.23 * 1.003, quintFreqRatio:=1.5 * 1.002, nominalFreqRatio:=2.0 * 1.001,
humAmp:=1.0, primeAmp:=0.85, tierceAmp:=0.65, quintAmp:=0.55, nominalAmp:=0.45,
humDecay:=1.2, primeDecay:=2.0, tierceDecay:=3.0, quintDecay:=4.0, nominalDecay:=5.0,
transientAmount:=1.0, bloomSpeed:=10.0)
Case BellStyle.Brass
Return (
humFreqRatio:=0.5 * 0.999, primeFreqRatio:=1.0, tierceFreqRatio:=1.25 * 1.002, quintFreqRatio:=1.52 * 1.001, nominalFreqRatio:=2.02,
humAmp:=0.8, primeAmp:=0.9, tierceAmp:=0.75, quintAmp:=0.65, nominalAmp:=0.55,
humDecay:=1.0, primeDecay:=1.8, tierceDecay:=2.5, quintDecay:=3.5, nominalDecay:=4.5,
transientAmount:=0.6, bloomSpeed:=8.0)
Case BellStyle.Grandfather
Return (
humFreqRatio:=0.5, primeFreqRatio:=1.0, tierceFreqRatio:=1.2, quintFreqRatio:=1.48, nominalFreqRatio:=2.0,
humAmp:=0.6, primeAmp:=0.8, tierceAmp:=0.5, quintAmp:=0.4, nominalAmp:=0.3,
humDecay:=1.5, primeDecay:=2.5, tierceDecay:=3.5, quintDecay:=4.5, nominalDecay:=5.5,
transientAmount:=0.4, bloomSpeed:=6.0)
Case BellStyle.Digital
Return (
humFreqRatio:=0.5, primeFreqRatio:=1.0, tierceFreqRatio:=1.25, quintFreqRatio:=1.5, nominalFreqRatio:=2.0,
humAmp:=0.3, primeAmp:=1.0, tierceAmp:=0.2, quintAmp:=0.4, nominalAmp:=0.6,
humDecay:=3.0, primeDecay:=4.0, tierceDecay:=5.0, quintDecay:=6.0, nominalDecay:=7.0,
transientAmount:=0.1, bloomSpeed:=3.0)
Case Else
Return (
humFreqRatio:=0.5, primeFreqRatio:=1.0, tierceFreqRatio:=1.23, quintFreqRatio:=1.5, nominalFreqRatio:=2.0,
humAmp:=1.0, primeAmp:=0.85, tierceAmp:=0.65, quintAmp:=0.55, nominalAmp:=0.45,
humDecay:=1.2, primeDecay:=2.0, tierceDecay:=3.0, quintDecay:=4.0, nominalDecay:=5.0,
transientAmount:=1.0, bloomSpeed:=10.0)
End Select
End Function
End Class
Please test the generated Cathedral WAV files first and let me know whether the pitch, timing, and overall tone are closer to the sound you expect. If not, the bell partial frequencies, amplitudes, and decay times can be adjusted further based on the desired reference sound.
If you found my response helpful or informative, I would greatly appreciate it if you could follow this guide for your confirmation. Thank you.