Experiências de software em RTTY 







Link do vídeo....

https://youtu.be/cGU99-iFAto?si=Pa35OpVo4roKsBXT


                                                         Foi utilizado o VSCODE.


Base do projeto....


Forms1.cs --------------------------------------------------------------------------------------

using System;

using System.Collections.Generic;

using System.Drawing;

using System.IO;

using System.Numerics;

using System.Windows.Forms;

using NAudio.Wave;


namespace RTTY_Waterfall;


public class Form1 : Form

{

    // ==============================

    // AUDIO & DADOS

    // ==============================

    WaveOutEvent output;

    BufferedWaveProvider provider;


    System.Windows.Forms.Timer audioTimer = new();

    System.Windows.Forms.Timer waterfallTimer = new();


    float[] samples;

    int sampleIndex = 0;

    List<(int startSample, char character)> charMap = new();


    // ==============================

    // UI

    // ==============================

    Button btnOpen, btnPlay, btnStop, btnSave;

    TrackBar zoomBar;

    PictureBox waterfallBox;

    Label lblCurrentChar, lblFullText;


    string currentText = "";

    Bitmap waterfallBmp;


    public Form1()

    {

        Text = "RTTY Generator + Waterfall - Loop Mode";

        Width = 1000;

        Height = 650;

        InitUI();

        InitAudio();

    }


    void InitUI()

    {

        btnOpen = new Button() { Text = "Abrir TXT", Left = 10, Top = 10, Width = 100 };

        btnPlay = new Button() { Text = "Play Loop", Left = 120, Top = 10, Width = 80 };

        btnStop = new Button() { Text = "Stop", Left = 210, Top = 10, Width = 80 };

        btnSave = new Button() { Text = "Salvar WAV", Left = 300, Top = 10, Width = 100 };

        zoomBar = new TrackBar() { Left = 420, Top = 5, Width = 200, Minimum = 1, Maximum = 8, Value = 2 };


        lblCurrentChar = new Label()

        {

            Left = 650,

            Top = 5,

            Width = 50,

            Height = 40,

            Font = new Font("Consolas", 24, FontStyle.Bold),

            ForeColor = Color.Red,

            TextAlign = ContentAlignment.MiddleCenter

        };


        lblFullText = new Label()

        {

            Left = 10,

            Top = 535,

            Width = 960,

            Height = 60,

            Font = new Font("Consolas", 12),

            BackColor = Color.Black,

            ForeColor = Color.Lime,

            BorderStyle = BorderStyle.Fixed3D

        };


        waterfallBox = new PictureBox() { Left = 10, Top = 50, Width = 960, Height = 480, BorderStyle = BorderStyle.FixedSingle };

        Controls.AddRange(new Control[] { btnOpen, btnPlay, btnStop, btnSave, zoomBar, waterfallBox, lblCurrentChar, lblFullText });


        btnOpen.Click += BtnOpen_Click;

        btnPlay.Click += BtnPlay_Click;

        btnStop.Click += BtnStop_Click;

        btnSave.Click += BtnSave_Click;


        waterfallBmp = new Bitmap(waterfallBox.Width, waterfallBox.Height);

    }


    void InitAudio()

    {

        provider = new BufferedWaveProvider(new WaveFormat(RTTY.SampleRate, 16, 1));

        provider.BufferDuration = TimeSpan.FromSeconds(20);

        provider.DiscardOnBufferOverflow = true;


        output = new WaveOutEvent();

        output.Init(provider);


        audioTimer.Interval = 20;

        audioTimer.Tick += AudioTimer_Tick;


        waterfallTimer.Interval = 30;

        waterfallTimer.Tick += WaterfallTimer_Tick;

    }


    void BtnPlay_Click(object sender, EventArgs e)

    {

        if (string.IsNullOrEmpty(currentText)) return;


        charMap.Clear();

        lblFullText.Text = "";


        // Geração inicial

        samples = RTTY.GenerateSamples(currentText);


        // Mapeamento de tempo por caractere para o display

        double samplesPerChar = 8 * (RTTY.SampleRate / RTTY.Baud);

        for (int i = 0; i < currentText.Length; i++)

            charMap.Add(((int)(i * samplesPerChar), currentText[i]));


        provider.ClearBuffer();

        sampleIndex = 0;


        output.Play();

        audioTimer.Start();

        waterfallTimer.Start();

    }


    void AudioTimer_Tick(object sender, EventArgs e)

    {

        if (samples == null) return;


        // --- LÓGICA DE LOOP ---

        if (sampleIndex >= samples.Length)

        {

            sampleIndex = 0; // Volta para o início do array de áudio

        }


        int targetBufferedBytes = provider.WaveFormat.AverageBytesPerSecond / 2;

        int bytesToFill = targetBufferedBytes - provider.BufferedBytes;


        if (bytesToFill > 0)

        {

            int samplesToCopy = bytesToFill / 2;


            // Se o que falta para acabar o array for menor que o necessário, 

            // pegamos apenas até o fim (o próximo Tick cuidará do reinício)

            samplesToCopy = Math.Min(samplesToCopy, samples.Length - sampleIndex);


            if (samplesToCopy <= 0) return;


            byte[] buffer = new byte[samplesToCopy * 2];

            for (int i = 0; i < samplesToCopy; i++)

            {

                short val = (short)(samples[sampleIndex++] * short.MaxValue);

                buffer[i * 2] = (byte)(val & 0xff);

                buffer[i * 2 + 1] = (byte)(val >> 8);

            }

            provider.AddSamples(buffer, 0, buffer.Length);

        }


        UpdateTextDisplay();

    }


    void UpdateTextDisplay()

    {

        char found = ' ';

        string history = "";


        foreach (var item in charMap)

        {

            if (sampleIndex >= item.startSample)

            {

                found = item.character;

                history += item.character;

            }

            else break;

        }


        lblCurrentChar.Text = found.ToString();

        if (history.Length > 80) history = history.Substring(history.Length - 80);

        lblFullText.Text = history;

    }


    void WaterfallTimer_Tick(object sender, EventArgs e)

    {

        if (samples == null) return;


        int fftSize = 1024;

        int zoom = zoomBar.Value;

        Complex[] fft = new Complex[fftSize];

        int start = Math.Max(0, sampleIndex - fftSize);


        for (int i = 0; i < fftSize; i++)

        {

            float s = (start + i < samples.Length) ? samples[start + i] : 0;

            fft[i] = new Complex(s, 0);

        }


        FFT(fft);

        ScrollBitmap(waterfallBmp);


        for (int x = 0; x < waterfallBmp.Width; x++)

        {

            int bin = x * zoom;

            if (bin >= fftSize / 2) break;

            double mag = fft[bin].Magnitude;

            int c = Math.Clamp((int)(Math.Log10(mag + 1) * 150), 0, 255);

            waterfallBmp.SetPixel(x, waterfallBmp.Height - 1, ColorFromValue(c));

        }

        waterfallBox.Image = waterfallBmp;

    }


    static void FFT(Complex[] buffer)

    {

        int n = buffer.Length;

        for (int j = 1, i = 0; j < n; j++)

        {

            int bit = n >> 1;

            for (; (i & bit) != 0; bit >>= 1) i &= ~bit;

            i |= bit;

            if (j < i) (buffer[j], buffer[i]) = (buffer[i], buffer[j]);

        }

        for (int len = 2; len <= n; len <<= 1)

        {

            double ang = -2 * Math.PI / len;

            Complex wlen = new(Math.Cos(ang), Math.Sin(ang));

            for (int i = 0; i < n; i += len)

            {

                Complex w = Complex.One;

                for (int j = 0; j < len / 2; j++)

                {

                    var u = buffer[i + j];

                    var v = buffer[i + j + len / 2] * w;

                    buffer[i + j] = u + v;

                    buffer[i + j + len / 2] = u - v;

                    w *= wlen;

                }

            }

        }

    }


    static void ScrollBitmap(Bitmap bmp)

    {

        using Graphics g = Graphics.FromImage(bmp);

        g.DrawImage(bmp, new Rectangle(0, 0, bmp.Width, bmp.Height - 1), new Rectangle(0, 1, bmp.Width, bmp.Height - 1), GraphicsUnit.Pixel);

    }


    static Color ColorFromValue(int v) => Color.FromArgb(v, v / 2, 255 - v);


    void BtnOpen_Click(object sender, EventArgs e)

    {

        using OpenFileDialog ofd = new();

        ofd.Filter = "Text|*.txt";

        if (ofd.ShowDialog() == DialogResult.OK) { currentText = File.ReadAllText(ofd.FileName); MessageBox.Show("Texto carregado!"); }

    }


    void BtnStop_Click(object sender, EventArgs e)

    {

        audioTimer.Stop();

        waterfallTimer.Stop();

        output.Stop();

        provider.ClearBuffer();

        sampleIndex = 0;

        lblCurrentChar.Text = "";

    }


    void BtnSave_Click(object sender, EventArgs e)

    {

        if (samples == null) return;

        using SaveFileDialog sfd = new();

        sfd.Filter = "WAV|*.wav";

        if (sfd.ShowDialog() == DialogResult.OK) RTTY.SaveWav16(sfd.FileName, samples);

    }

}

----------------------------------------------------------------------------------------------------------------


Program.cs -------------------------------------------------------------------------------------------------


using System;

using System.Windows.Forms;


namespace RTTY_Waterfall;


internal static class Program

{

    [STAThread]

    static void Main()

    {

        Application.EnableVisualStyles();

        Application.SetCompatibleTextRenderingDefault(false);

        Application.Run(new Form1());

    }

}

--------------------------------------------------------------------------------------------------------------------

RTTY.cs -------------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using NAudio.Wave;

namespace RTTY_Waterfall;

public static class RTTY
{
    // ======================================================
    // CONFIGURAÇÃO PADRÃO HAM
    // ======================================================

    public static int SampleRate = 11025;
    public static double Baud = 45.45;
    public static double Mark = 2125;
    public static double Shift = 170;
    public static double Volume = 0.9;

    static double Space => Mark + Shift;

    const int LTRS = 31;
    const int FIGS = 27;

    // ======================================================
    // TABELA ITA-2 LETTERS
    // ======================================================

    static readonly Dictionary<char, int> letters = new()
    {
        ['A'] = 3,
        ['B'] = 25,
        ['C'] = 14,
        ['D'] = 9,
        ['E'] = 1,
        ['F'] = 13,
        ['G'] = 26,
        ['H'] = 20,
        ['I'] = 6,
        ['J'] = 11,
        ['K'] = 15,
        ['L'] = 18,
        ['M'] = 28,
        ['N'] = 12,
        ['O'] = 24,
        ['P'] = 22,
        ['Q'] = 23,
        ['R'] = 10,
        ['S'] = 5,
        ['T'] = 16,
        ['U'] = 7,
        ['V'] = 30,
        ['W'] = 19,
        ['X'] = 29,
        ['Y'] = 21,
        ['Z'] = 17,
        [' '] = 4,
        ['\r'] = 8,
        ['\n'] = 2
    };

    // ======================================================
    // TABELA ITA-2 FIGURES
    // ======================================================

    static readonly Dictionary<char, int> figures = new()
    {
        ['1'] = 23,
        ['2'] = 19,
        ['3'] = 1,
        ['4'] = 10,
        ['5'] = 16,
        ['6'] = 21,
        ['7'] = 7,
        ['8'] = 6,
        ['9'] = 24,
        ['0'] = 22,
        ['-'] = 3,
        ['?'] = 25,
        [':'] = 14,
        ['$'] = 9,
        ['!'] = 13,
        ['&'] = 26,
        ['#'] = 20,
        ['('] = 15,
        [')'] = 18,
        ['.'] = 28,
        [','] = 12,
        [';'] = 30,
        ['/'] = 29,
        ['+'] = 17,
        ['='] = 11,
        [' '] = 4
    };

    // ======================================================
    // GERAR SINAIS RTTY (CPFSK PROFISSIONAL)
    // ======================================================

    public static float[] GenerateSamples(string text)
    {
        List<float> samples = new();

        double dt = 1.0 / SampleRate;
        double bitTime = 1.0 / Baud;

        double phase = 0;
        double lastFreq = Mark;

        bool figMode = false;

        foreach (char raw in text.ToUpper())
        {
            int code;

            if (letters.TryGetValue(raw, out code))
            {
                if (figMode)
                {
                    AddChar(LTRS);
                    figMode = false;
                }
            }
            else if (figures.TryGetValue(raw, out code))
            {
                if (!figMode)
                {
                    AddChar(FIGS);
                    figMode = true;
                }
            }
            else
                continue;

            AddChar(code);
        }

        return samples.ToArray();


        // ======================
        // adiciona caractere
        // ======================
        void AddChar(int code)
        {
            int[] bits = new int[8];

            bits[0] = 0; // start

            for (int i = 0; i < 5; i++)
                bits[i + 1] = (code >> i) & 1;

            bits[6] = 1;
            bits[7] = 1;

            foreach (var b in bits)
                AddBit(b);
        }

        // ======================
        // CPFSK fase contínua
        // ======================
        void AddBit(int bit)
        {
            double targetFreq = bit == 1 ? Mark : Space;

            double elapsed = 0;

            while (elapsed < bitTime)
            {
                double k = elapsed / bitTime;

                // raised cosine (transição suave)
                double smooth = 0.5 - 0.5 * Math.Cos(Math.PI * k);

                double freq = lastFreq + (targetFreq - lastFreq) * smooth;

                phase += 2 * Math.PI * freq * dt;

                samples.Add((float)(Math.Sin(phase) * Volume));

                elapsed += dt;
            }

            lastFreq = targetFreq;
        }
    }

    // ======================================================
    // SALVAR WAV 16-bit PCM (MÉTODO PRINCIPAL)
    // ======================================================

    public static void SaveWav16(string path, float[] samples)
    {
        using var writer = new WaveFileWriter(
            path,
            new WaveFormat(SampleRate, 16, 1));

        writer.WriteSamples(samples, 0, samples.Length);
    }

    // ======================================================
    // Alias opcional (compatibilidade)
    // ======================================================

    public static void SaveWav(string path, float[] samples)
        => SaveWav16(path, samples);
}

-------------------------------------------------------------------------------------------------------------------

Waterfalls.cs -------------------------------------------------------------------------------------------------


using System;
using System.Drawing;
using NAudio.Dsp;

namespace RTTY_Waterfall_VSCode
{
    public class Waterfall
    {
        int fftSize = 1024;
        int zoom = 1;

        Bitmap bmp;
        float[] window;

        public Bitmap Bitmap => bmp;

        public int Zoom
        {
            get => zoom;
            set => zoom = Math.Max(1, Math.Min(8, value));
        }

        public Waterfall(int w, int h)
        {
            bmp = new Bitmap(w, h);
            window = new float[fftSize];

            for (int i = 0; i < fftSize; i++)
                window[i] = (float)(0.5 * (1 - Math.Cos(2 * Math.PI * i / (fftSize - 1))));
        }

        Color Map(double v)
        {
            if (v < 0.2) return Color.FromArgb(0, 0, (int)(255 * v * 5));
            if (v < 0.4) return Color.FromArgb(0, (int)(255 * (v - 0.2) * 5), 255);
            if (v < 0.6) return Color.FromArgb((int)(255 * (v - 0.4) * 5), 255, 0);
            if (v < 0.8) return Color.Yellow;
            return Color.Red;
        }

        public void AddSamples(float[] s)
        {
            Complex[] fft = new Complex[fftSize];

            for (int i = 0; i < fftSize; i++)
            {
                fft[i].X = s[i] * window[i];
                fft[i].Y = 0;
            }

            FastFourierTransform.FFT(true, 10, fft);

            using var g = Graphics.FromImage(bmp);
            g.DrawImage(bmp, 0, 1);

            int bins = fftSize / 2 / zoom;

            for (int x = 0; x < bmp.Width; x++)
            {
                int bin = x * bins / bmp.Width;

                double mag = Math.Sqrt(fft[bin].X * fft[bin].X + fft[bin].Y * fft[bin].Y);
                double db = 20 * Math.Log10(mag + 1e-9);
                double norm = Math.Clamp((db + 60) / 60, 0, 1);

                bmp.SetPixel(x, 0, Map(norm));
            }
        }
    }
}

-----------------------------------------------------------------------------------------------------------------

RTTY_Waterfall_VSCode     ----------------------------------------------------------------------------


<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="NAudio" Version="2.2.1" />
  </ItemGroup>
</Project>




Utilizar o pacote NAudio (2.2.1)


Compile por conta e risco, isento de responsabilidade.
Funcionamento desta base , verificar o link do vídeo!!!

Comentários

Postagens mais visitadas deste blog

JVC Compulink, COMPU LINK, JVC, Tape Deck, Arduino, DCS Codes

JVC Compu Link Final

RTTY, PSK31, FT8, JS8CALL, NBTV, RDS FM, OLIVIA, SSTV, SSTV AUTO,CW, ASCII Encoder / Decoder