[C#/COMMON/NAUDIO/.NET8] FSK (Frequency Shift Keying) 변조 사용하기
■ FSK (Frequency Shift Keying) 변조를 사용하는 방법을 보여준다. [Encoder 프로젝트] ▶ Program.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
using System; using System.IO; using System.Linq; using System.Numerics; using System.Text; using NAudio.Wave; namespace DECODER; /// <summary> /// 디코더 /// </summary> class Decoder { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Static //////////////////////////////////////////////////////////////////////////////// Private #region 다음 2의 거듭 제곱 구하기 - NextPowerOfTwo(value) /// <summary> /// 다음 2의 거듭 제곱 구하기 /// </summary> /// <param name="value">값</param> /// <returns></returns> /// <remarks>입력값보다 큰 가장 작은 2의 거듭 제곱을 계산한다.</remarks> private static int NextPowerOfTwo(int value) { int power = 1; while(power < value) { power <<= 1; } return power; } #endregion #region 비트 반전하기 - BitReverse(bitValue, bitCount) /// <summary> /// 비트 반전하기 /// </summary> /// <param name="bitValue">비트 값</param> /// <param name="bitCount">비트 수</param> /// <returns>반전 비트 값</returns> private static int BitReverse(int bitValue, int bitCount) { int reversed = 0; for(int i = 0; i < bitCount; i++) { int nextBit = (bitValue >> i) & 1; reversed |= (nextBit << (bitCount - i - 1)); } return reversed; } #endregion #region FFT 처리하기 - FFT(complexArray) /// <summary> /// FFT 처리하기 /// </summary> /// <param name="complexArray">복소수 배열</param> private static void FFT(Complex[] complexArray) { int complexArrayLength = complexArray.Length; if(complexArrayLength <= 1) { return; } // Bit reversal permutation int m1 = (int)Math.Log(complexArrayLength, 2); for(int i = 0; i < complexArrayLength; i++) { int j = BitReverse(i, m1); if(j > i) { Complex t = complexArray[i]; complexArray[i] = complexArray[j]; complexArray[j] = t; } } // Cooley-Tukey FFT for(int s = 1; s <= m1; s++) { int m2 = 1 << s; Complex wm = Complex.Exp(-2.0 * Math.PI * Complex.ImaginaryOne / m2); for(int k = 0; k < complexArrayLength; k += m2) { Complex w = 1; for(int j = 0; j < m2 / 2; j++) { int index1 = k + j; int index2 = k + j + m2 / 2; Complex t = w * complexArray[index2]; Complex u = complexArray[index1]; complexArray[index1] = u + t; complexArray[index2] = u - t; w *= wm; } } } } #endregion #region 프로그램 시작하기 - Main(argumentArray) /// <summary> /// 프로그램 시작하기 /// </summary> /// <param name="argumentArray">인자 배열</param> private static void Main(string[] argumentArray) { if(argumentArray.Length != 2) { Console.WriteLine("사용법 : decoder.exe source.wav target.zip"); return; } string sourceFilePath = argumentArray[0]; string targetFilePath = argumentArray[1]; int samplingFrequency = 44100; // 샘플링 주파수 double durationPerBit = 0.01; // 각 비트의 지속 시간 (초) int sampleCountPerBit = (int)(samplingFrequency * durationPerBit); int frequency0 = 1000; // '0'에 대한 주파수 int frequency1 = 2000; // '1'에 대한 주파수 // 시작 및 종료 마커를 설정한다. string startMarkerBitString = "1111000011110000"; string endMarkerBitString = "0000111100001111"; // WAV 파일을 읽는다. float[] signalArray; using(AudioFileReader audioFileReader = new(sourceFilePath)) { int totalSampleCount = (int)audioFileReader.Length / (audioFileReader.WaveFormat.BitsPerSample / 8); signalArray = new float[totalSampleCount]; int readSampleCount = audioFileReader.Read(signalArray, 0, totalSampleCount); } int bitCount = signalArray.Length / sampleCountPerBit; StringBuilder bitStringBuilder = new StringBuilder(); for(int i = 0; i < bitCount; i++) { int start = i * sampleCountPerBit; int end = start + sampleCountPerBit; if(end > signalArray.Length) { break; } float[] segmentArray = new float[sampleCountPerBit]; Array.Copy(signalArray, start, segmentArray, 0, sampleCountPerBit); // 입력 배열 길이를 가장 가까운 2의 거듭제곱으로 패딩한다. int paddedLength = NextPowerOfTwo(segmentArray.Length); Complex[] fftComplexArray = new Complex[paddedLength]; for(int j = 0; j < segmentArray.Length; j++) { fftComplexArray[j] = new Complex(segmentArray[j], 0); } for(int j = segmentArray.Length; j < paddedLength; j++) { fftComplexArray[j] = Complex.Zero; } // 주파수 분석을 위한 FFT를 수행한다. FFT(fftComplexArray); double[] magnitudeArray = fftComplexArray.Take(paddedLength / 2).Select(c => c.Magnitude).ToArray(); int peakIndex = Array.IndexOf(magnitudeArray, magnitudeArray.Max()); double peakFrequency = (double)peakIndex * samplingFrequency / paddedLength; // 주파수에 따라 비트를 결정한다. if(Math.Abs(peakFrequency - frequency0) < 100) { bitStringBuilder.Append('0'); } else if(Math.Abs(peakFrequency - frequency1) < 100) { bitStringBuilder.Append('1'); } else { bitStringBuilder.Append('?'); } } string bitStream = bitStringBuilder.ToString(); // 비트 스트림에서 시작 및 종료 마커를 찾는다. int startMarkerIndex = bitStream.IndexOf(startMarkerBitString); int endMarkerIndex = bitStream.IndexOf(endMarkerBitString); if(startMarkerIndex == -1 || endMarkerIndex == -1) { Console.WriteLine("시작 또는 종료 마커를 찾을 수 없습니다."); return; } int dataStart = startMarkerIndex + startMarkerBitString.Length; int dataEnd = endMarkerIndex; string dataBitStream = bitStream.Substring(dataStart, dataEnd - dataStart); // 파일 크기를 추출한다. (첫 32비트) if(dataBitStream.Length < 32) { Console.WriteLine("파일 크기 정보를 읽을 수 없습니다."); return; } string fileSizeString = dataBitStream.Substring(0, 32); dataBitStream = dataBitStream.Substring(32); // 파일 크기를 정수로 변환한다. int fileSize = Convert.ToInt32(fileSizeString, 2); // 결정할 수 없는 비트를 제거한다. dataBitStream = dataBitStream.Replace("?", ""); // 비트를 바이트로 변환한다. int byteCount = dataBitStream.Length / 8; byte[] dataByteArray = new byte[byteCount]; for(int i = 0; i < byteCount; i++) { string byteBitString = dataBitStream.Substring(i * 8, 8); dataByteArray[i] = Convert.ToByte(byteBitString, 2); } // 파일 크기에 따라 데이터를 자른다. if(dataByteArray.Length > fileSize) { byte[] tempByteArray = new byte[fileSize]; Array.Copy(dataByteArray, tempByteArray, fileSize); dataByteArray = tempByteArray; } // 복원된 파일을 저장한다. File.WriteAllBytes(targetFilePath, dataByteArray); Console.WriteLine("디코딩 완료 : " + targetFilePath); } #endregion } |
[Decoder 프로젝트] ▶ requirements.txt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
using System; using System.IO; using System.Linq; using NAudio.Wave; namespace ENCODER; /// <summary> /// 인코더 /// </summary> class Encoder { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Static //////////////////////////////////////////////////////////////////////////////// Private #region 프로그램 시작하기 - Main(argumentArray) /// <summary> /// 프로그램 시작하기 /// </summary> /// <param name="argumentArray">인자 배열</param> private static void Main(string[] argumentArray) { if(argumentArray.Length != 2) { Console.WriteLine("사용법 : encoder.exe source.zip target.wav"); return; } string sourceFilePath = argumentArray[0]; string targetFilePath = argumentArray[1]; // 소스 파일을 읽는다. byte[] sourceFileByteArray = File.ReadAllBytes(sourceFilePath); int sourceFileSize = sourceFileByteArray.Length; // 바이트를 비트로 변환한다. string dataBitString = string.Join(string.Empty, sourceFileByteArray.Select(sourceByte => Convert.ToString(sourceByte, 2).PadLeft(8, '0'))); // 파일 크기를 헤더에 추가한다. (32비트로 표현) string fileSizeBitString = Convert.ToString(sourceFileSize, 2).PadLeft(32, '0'); // 시작 및 종료 마커 문자열을 설정한다. string startMarkerString = "1111000011110000"; string endMarkerString = "0000111100001111"; // 전체 비트 스트림을 생성한다. string bitStreamString = startMarkerString + fileSizeBitString + dataBitString + endMarkerString; // 파라미터를 설정한다. int samplingFrequency = 44100; // 샘플링 주파수 double durationPerBit = 0.01; // 각 비트의 지속 시간 (초) int sampleCountPerBit = (int)(samplingFrequency * durationPerBit); int frequency0 = 1000; // '0'에 대한 주파수 int frequency1 = 2000; // '1'에 대한 주파수 // 신호를 생성한다. float[] signalArray = new float[bitStreamString.Length * sampleCountPerBit]; int bitStreamStringLength = bitStreamString.Length; for(int i = 0; i < bitStreamStringLength; i++) { char bitCharacter = bitStreamString[i]; int frequency = bitCharacter == '1' ? frequency1 : frequency0; for(int j = 0; j < sampleCountPerBit; j++) { double t = (double)j / samplingFrequency; signalArray[i * sampleCountPerBit + j] = (float)Math.Sin(2 * Math.PI * frequency * t); } } // 신호를 정규화한다. float maximumAmplitude = signalArray.Max(x => Math.Abs(x)); for(int i = 0; i < signalArray.Length; i++) { signalArray[i] = signalArray[i] / maximumAmplitude; } // WAV 파일로 저장한다. using(WaveFileWriter waveFileWriter = new WaveFileWriter(targetFilePath, new WaveFormat(samplingFrequency, 16, 1))) { waveFileWriter.WriteSamples(signalArray, 0, signalArray.Length); } Console.WriteLine("인코딩 완료 : " + targetFilePath); } #endregion } |
TestSolution.zip