I went through a few iterations testing with Node, PHP, and C# but settled on this example. The bigger challenge was sorting the popping on the fast frequency transition. I tested this up to 9600 baud with my ST-8000. The bit stream is 8 bit characters  "A": "01100011" 11000 with 1 start bit and 2 stop bits. The actual bitrate at 45.45 baud is around 22ms. but since I am directly converting points to a wav file I keep track of that based on how many samples. Prints well on all my machines. Model 28 / UGC-136BX / Hal. See the working example. https://w8zjt.net/itty

73, de W8ZJT

    async generateAFSKWav(bitstream: string, baud, mark, space): Promise<Blob> {
        const sampleRate = 16000;  // Sample rate in Hz
        const fadeDuration = 50;   // Number of samples for smooth frequency transition
        const samplesPerBit = Math.round(sampleRate / baud);
        const totalSamples = bitstream.length * samplesPerBit;
        let audioSamples: Float32Array = new Float32Array(totalSamples);
        let phase = 0.0;
        let currentFreq = bitstream[0] === "1" ? mark : space;
        let currentPhaseIncrement = (2 * Math.PI * currentFreq) / sampleRate;

        for (let bitIndex = 0; bitIndex < bitstream.length; bitIndex++) {
            const newFreq = bitstream[bitIndex] === "1" ? mark : space;
            const newPhaseIncrement = (2 * Math.PI * newFreq) / sampleRate;

            for (let i = 0; i < samplesPerBit; i++) {
                if (i < fadeDuration) {
                    // Smooth frequency transition (interpolation)
                    const fadeFactor = i / fadeDuration;
                    const interpolatedFreq = currentFreq * (1 - fadeFactor) + newFreq * fadeFactor;
                    currentPhaseIncrement = (2 * Math.PI * interpolatedFreq) / sampleRate;
                } else {
                    currentPhaseIncrement = newPhaseIncrement;
                }

                // Generate waveform sample
                audioSamples[bitIndex * samplesPerBit + i] = Math.sin(phase);
                phase += currentPhaseIncrement;

                if (phase >= 2 * Math.PI) {
                    phase -= 2 * Math.PI;
                }
            }

            currentFreq = newFreq;
        }
        // Convert to WAV format
        const wavBuffer = await WavEncoder.encode({
            sampleRate: sampleRate,
            channelData: [audioSamples] // Mono channel
        });

On Wed, Nov 12, 2025 at 1:57 PM Steve <zarco@sonic.net> wrote:
Much easier to do in hardware with just a few chips.
555 timer and shift register. A couple more chips and
send R-Y test. You can use a scope or period counter to
accurately setup the 555 clock. Can be implemented in Baudot
or ASCII.
 
With a uP, use the programmable timer which in most these
days is internal. This should be largely independent of the
code if done right.
 
Projects like this are fun...enjoy!
Steve W6SSP
______________________________________________________________
GreenKeys mailing list
Home: http://mailman.qth.net/mailman/listinfo/greenkeys
Help: http://mailman.qth.net/mmfaq.htm
Post: mailto:GreenKeys@mailman.qth.net

>>> Jordan Spencer Cunningham's GreenKeys Search Tool: https://teletype.net/gksearch
>>> 2002-to-present greenkeys archive: http://mailman.qth.net/pipermail/greenkeys/
>>> 1998-to-2001 greenkeys archive: http://mailman.qth.net/archive/greenkeys/greenkeys.html
>>> Randy Guttery's 2001-to-2009 GreenKeys Search Tool: http://comcents.com/tty/greenkeyssearch.html

This list hosted by: http://www.qsl.net
Please help support this email list: http://www.qsl.net/donate.html
Message delivered to zach.tumbusch@gmail.com