0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C言語 FSK方式で文字列をWAV波形にして音で通信してみる

Posted at

目的

本記事では、FSK方式を使ってASCIIコードの文字列を波形に変換し、その波形をWAV形式で保存します。その後、WAVファイルを読み込んで元の文字列を復元する方法を紹介します。
本来は電波を使って実験を行いたいところですが、任意の電波を生成する装置がなく、又電波が飛んでいても肉眼で確認できないため、WAV形式を採用しました。
音声形式ではありますが、いずれも波形である点では同じように扱えます。

FM(周波数変調)​ とFSK(周波数偏移変調)の違い

紛らわしいので以下2つの違いを説明しておきます。
どちらも周波数の変化で情報を表すという点では同じです。

FM(周波数変調) - アナログ方式

入力:連続的なアナログ信号(音声など)
出力:連続的に変化する周波数

周波数
  ↑
  |    ___     ___     _      滑らかに変化
  |   /   \   /   \   / \    
  |  /     \_/     \_/   \   
  |_/________________________→ 時間

FSK(周波数偏移変調) - デジタル方式

入力:離散的なデジタル信号(0と1)
出力:2つ(または数個)の決まった周波数だけを使う

周波数
  ↑
  |     2000Hz       2000Hz 2000Hz       段階的変化
  |     _______      _______ _______
  |    |       |    |       |       |
  |    |       |    |       |       |
  |____|       |____|       |_______|_____→ 時間
     1000Hz       1000Hz

変調方式
├── アナログ変調(連続信号伝送)
│ ├── AM(振幅変調)
│ ├── FM(周波数変調)← アナログ版
│ └── PM(位相変調)

└── デジタル変調(離散信号伝送)
├── ASK(振幅偏移変調)
├── FSK(周波数偏移変調)← デジタル版
└── PSK(位相偏移変調)

FSKで情報を伝達する大まかな流れ

情報源 → [エンコーダ] → 通信路 → [デコーダ] → 情報復元
   ↓          ↓          ↓         ↓
  "A"    周波数パターン   波      "A"に復元

FSKの具体例

データ: 10110
ビット時間: 0.1秒
周波数: 0=1000Hz, 1=2000Hz

時間       0.0-0.1s  0.1-0.2s  0.2-0.3s  0.3-0.4s  0.4-0.5s
ビット        1         0         1         1         0
周波数     2000Hz    1000Hz    2000Hz    2000Hz    1000Hz
状態      高い音を   低い音を  高い音を  高い音を  低い音を
          0.1秒間    0.1秒間   0.1秒間   0.1秒間   0.1秒間
          維持       維持      維持      維持      維持
周波数
  ↑
  |   2000Hz   1000Hz   2000Hz   2000Hz   1000Hz
  |   _______         _______   _______        
  |  |       |       |       | |       |       
  |  |   1   |   0   |   1   | |   1   |   0   
  |__|       |_______|       |_|       |_______
  |
  +-------------------------------------------→ 時間
     0.1s    0.2s    0.3s    0.4s    0.5s

ビット列をASCIIコードにすることで文字列を波として表現できます。

C言語で検証

構成

[文字列] → [ASCII変換] → [FSK変調] → [WAV出力]
                                 ↓
[WAV入力] → [FSK復調] → [ビット判定] → [文字列復元]

本来は、WAV音声を再生し、それを録音して文字列を復元したいところですが、録音すると雑音が入る可能性があります。これにより、波形が崩れて誤り検出やビットの開始位置の判定が必要になり、処理が複雑になります。そのため、今回は出力したWAVファイルをそのまま読み込んで復元する方法を選びました。

WAVを生成

以下はAudacityというソフトを用いて生成したWAVファイルを解析し、横軸を時間、縦軸を周波数として図にしたものです。
0の部分は低く、1の部分は高くなっています。
图片.png

test@test-fujitsu:~/kaihatsu/butsuri$ gcc -o fsk_encoder fsk_encoder.c -lm
test@test-fujitsu:~/kaihatsu/butsuri$ ./fsk_encoder
=== FSKエンコーダ ===
変換する文字列: "HELLO"
パラメータ:
  サンプリングレート: 44100 Hz
  ビット時間: 0.1秒
  周波数(0): 1000 Hz
  周波数(1): 2000 Hz
  サンプル数/ビット: 4410

文字 'H' (ASCII 72) -> ビット列: 01001000
文字 'E' (ASCII 69) -> ビット列: 01000101
文字 'L' (ASCII 76) -> ビット列: 01001100
文字 'L' (ASCII 76) -> ビット列: 01001100
文字 'O' (ASCII 79) -> ビット列: 01001111

WAVファイル生成完了: hello_fsk.wav
文字数: 5
総ビット数: 40
総サンプル数: 176400
ファイルサイズ: 352844 bytes
再生時間: 4.00秒

WAVファイル 'hello_fsk.wav' が生成されました。
このファイルをオーディオプレーヤーで再生すると、FSK変調された音が聞けます。
fsk_encoder.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <stdint.h>

// WAVヘッダーの構造体
typedef struct {
    char chunkID[4];        // "RIFF"
    uint32_t chunkSize;     // ファイルサイズ - 8
    char format[4];         // "WAVE"
    char subchunk1ID[4];    // "fmt "
    uint32_t subchunk1Size; // 16 (PCMの場合)
    uint16_t audioFormat;   // 1 (PCM)
    uint16_t numChannels;   // 1 (モノラル)
    uint32_t sampleRate;    // 44100
    uint32_t byteRate;      // sampleRate * numChannels * bitsPerSample/8
    uint16_t blockAlign;    // numChannels * bitsPerSample/8
    uint16_t bitsPerSample; // 16
    char subchunk2ID[4];    // "data"
    uint32_t subchunk2Size; // データサイズ
} WavHeader;

// パラメータ設定
#define SAMPLE_RATE 44100
#define BIT_DURATION 0.1    // 1ビットあたりの時間(秒)
#define SAMPLES_PER_BIT (int)(SAMPLE_RATE * BIT_DURATION)
#define FREQ_0 1000         // ビット0の周波数
#define FREQ_1 2000         // ビット1の周波数
#define AMPLITUDE 16000     // 振幅(16ビットなので最大32767)

// 文字を8ビットのバイナリに変換
void char_to_binary(char c, int* bits) {
    for (int i = 0; i < 8; i++) {
        bits[7 - i] = (c >> i) & 1;
    }
}

// サイン波を生成(指定した周波数で)
double generate_sine_wave(double frequency, int sample_index) {
    double angle = 2.0 * M_PI * frequency * sample_index / SAMPLE_RATE;
    return sin(angle);
}

// FSK信号を生成してWAVファイルに保存
void generate_fsk_signal(const char* text, const char* filename) {
    int text_len = strlen(text);
    int total_bits = text_len * 8;
    int total_samples = total_bits * SAMPLES_PER_BIT;
    
    // サンプルデータを格納するバッファ
    int16_t* samples = (int16_t*)malloc(total_samples * sizeof(int16_t));
    if (!samples) {
        printf("メモリ割り当てエラー\n");
        return;
    }
    
    int sample_index = 0;
    
    // 各文字を処理
    for (int char_index = 0; char_index < text_len; char_index++) {
        char current_char = text[char_index];
        int bits[8];
        
        // 文字を8ビットに変換
        char_to_binary(current_char, bits);
        
        printf("文字 '%c' (ASCII %d) -> ビット列: ", current_char, current_char);
        for (int i = 0; i < 8; i++) {
            printf("%d", bits[i]);
        }
        printf("\n");
        
        // 各ビットに対してサイン波を生成
        for (int bit_index = 0; bit_index < 8; bit_index++) {
            int bit = bits[bit_index];
            double frequency = (bit == 1) ? FREQ_1 : FREQ_0;
            
            // 1ビット分のサンプルを生成
            for (int i = 0; i < SAMPLES_PER_BIT; i++) {
                double sample_value = generate_sine_wave(frequency, sample_index);
                samples[sample_index] = (int16_t)(sample_value * AMPLITUDE);
                sample_index++;
            }
        }
    }
    
    // WAVヘッダーを作成
    WavHeader header;
    memcpy(header.chunkID, "RIFF", 4);
    memcpy(header.format, "WAVE", 4);
    memcpy(header.subchunk1ID, "fmt ", 4);
    memcpy(header.subchunk2ID, "data", 4);
    
    header.subchunk1Size = 16;
    header.audioFormat = 1; // PCM
    header.numChannels = 1; // モノラル
    header.sampleRate = SAMPLE_RATE;
    header.bitsPerSample = 16;
    header.blockAlign = header.numChannels * header.bitsPerSample / 8;
    header.byteRate = header.sampleRate * header.blockAlign;
    header.subchunk2Size = total_samples * sizeof(int16_t);
    header.chunkSize = 36 + header.subchunk2Size;
    
    // WAVファイルを書き出し
    FILE* file = fopen(filename, "wb");
    if (!file) {
        printf("ファイルオープンエラー: %s\n", filename);
        free(samples);
        return;
    }
    
    // ヘッダー書き込み
    fwrite(&header, sizeof(WavHeader), 1, file);
    
    // サンプルデータ書き込み
    fwrite(samples, sizeof(int16_t), total_samples, file);
    
    fclose(file);
    free(samples);
    
    printf("\nWAVファイル生成完了: %s\n", filename);
    printf("文字数: %d\n", text_len);
    printf("総ビット数: %d\n", total_bits);
    printf("総サンプル数: %d\n", total_samples);
    printf("ファイルサイズ: %ld bytes\n", sizeof(WavHeader) + total_samples * sizeof(int16_t));
    printf("再生時間: %.2f秒\n", (double)total_samples / SAMPLE_RATE);
}

// テスト用メイン関数
int main() {
    const char* text = "HELLO";
    
    printf("=== FSKエンコーダ ===\n");
    printf("変換する文字列: \"%s\"\n", text);
    printf("パラメータ:\n");
    printf("  サンプリングレート: %d Hz\n", SAMPLE_RATE);
    printf("  ビット時間: %.1f秒\n", BIT_DURATION);
    printf("  周波数(0): %d Hz\n", FREQ_0);
    printf("  周波数(1): %d Hz\n", FREQ_1);
    printf("  サンプル数/ビット: %d\n", SAMPLES_PER_BIT);
    printf("\n");
    
    generate_fsk_signal(text, "hello_fsk.wav");
    
    printf("\nWAVファイル 'hello_fsk.wav' が生成されました。\n");
    printf("このファイルをオーディオプレーヤーで再生すると、FSK変調された音が聞けます。\n");
    
    return 0;
}

生成したWAVから文字列を復元

test@test-fujitsu:~/kaihatsu/butsuri$ gcc -o fsk_decoder fsk_decoder.c -lm
test@test-fujitsu:~/kaihatsu/butsuri$ ./fsk_decoder
復元結果: HELLO
fsk_decoder.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <math.h>

// WAVヘッダー構造体
typedef struct {
    char chunkID[4];
    uint32_t chunkSize;
    char format[4];
    char subchunk1ID[4];
    uint32_t subchunk1Size;
    uint16_t audioFormat;
    uint16_t numChannels;
    uint32_t sampleRate;
    uint32_t byteRate;
    uint16_t blockAlign;
    uint16_t bitsPerSample;
    char subchunk2ID[4];
    uint32_t subchunk2Size;
} WavHeader;

// パラメータ
#define SAMPLE_RATE 44100
#define BIT_DURATION 0.1
#define SAMPLES_PER_BIT (int)(SAMPLE_RATE * BIT_DURATION)
#define FREQ_THRESHOLD 1500

// ゼロクロス検出で周波数推定
double estimate_frequency_zero_crossing(const int16_t* samples, int num_samples) {
    int zero_crossings = 0;
    for (int i = 1; i < num_samples; i++) {
        if ((samples[i-1] <= 0 && samples[i] > 0) ||
            (samples[i-1] >= 0 && samples[i] < 0)) {
            zero_crossings++;
        }
    }
    return (zero_crossings * SAMPLE_RATE) / (2.0 * num_samples);
}

// 閾値を元に周波数からビット判定
int detect_bit(double frequency) {
    return (frequency > FREQ_THRESHOLD) ? 1 : 0;
}

// 8ビットから文字に変換
char binary_to_char(const int* bits) {
    char result = 0;
    for (int i = 0; i < 8; i++) {
        result = (result << 1) | bits[i];
    }
    return result;
}

// WAVをデコードして文字列を復元
char* decode_fsk_signal(const char* filename) {
    FILE* file = fopen(filename, "rb");
    if (!file) {
        printf("ファイルを開くのに失敗: %s\n", filename);
        return NULL;
    }

    WavHeader header;
    if (fread(&header, sizeof(WavHeader), 1, file) != 1) {
        printf("ヘッダー読み込みエラー\n");
        fclose(file);
        return NULL;
    }

    int total_samples = header.subchunk2Size / sizeof(int16_t); // WAVの標本総数
    int total_bits = total_samples / SAMPLES_PER_BIT; // FSK でエンコードされたビットの総数
    int text_len = total_bits / 8; //  伝送する文字列の長さ

    int16_t* samples = (int16_t*)malloc(total_samples * sizeof(int16_t));
    if (!samples) { fclose(file); return NULL; }
    if (fread(samples, sizeof(int16_t), total_samples, file) != total_samples) {
        printf("サンプルデータ読み込みエラー\n");
        free(samples);
        fclose(file);
        return NULL;
    }
    fclose(file);

    char* decoded_text = (char*)malloc(text_len + 1);
    if (!decoded_text) { free(samples); return NULL; }

    //復元する文字の数だけループ  HELLO→5回
    for (int char_index = 0; char_index < text_len; char_index++) {
        int bits[8];
        for (int i = 0; i < 8; i++) {
            int start = (char_index * 8 + i) * SAMPLES_PER_BIT; // 今解析する「ビット」の開始位置を計算
            double freq = estimate_frequency_zero_crossing(&samples[start], SAMPLES_PER_BIT);
            bits[i] = detect_bit(freq);
        }
        decoded_text[char_index] = binary_to_char(bits);
    }

    decoded_text[text_len] = '\0';
    free(samples);
    return decoded_text;
}

int main() {
    const char* filename = "hello_fsk.wav";

    char* decoded_text = decode_fsk_signal(filename);
    if (decoded_text) {
        printf("復元結果: %s\n", decoded_text);
        free(decoded_text);
    }

    return 0;
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?