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

Projetos elaborados, ferrovia