2024年4月7日 星期日

linux 使用 ALSA lib 播放 aac 檔

先安裝開發程式庫

        sudo   apt   install   libfdk-acc-dev   libasound-dev

下載一些測試 aac 檔案: https://espressif-docs.readthedocs-hosted.com/projects/esp-adf/en/latest/design-guide/audio-samples.html

// 主程式: aacplay.cpp
#include <sys/mman.h>
#include <fdk-aac/aacdecoder_lib.h>
#include <alsa/asoundlib.h>
struct PCM16 {
    int16_t *data;
    int length;
    int channel;
    int fs;
};
const PCM16 zeroPCM16 = {.data = (int16_t *)0, .length = 0, .channel = 0, .fs = 0};
const char *path_to_close = "./close!";
auto stepDecoder = [](const void *path = nullptr, int repeate = 0) {
    static void *mmapFile = MAP_FAILED; // to management mmap file
    static HANDLE_AACDECODER decoder = nullptr;
    static int32_t length = 0;// mmap file length
    static int32_t position = 0; // trace file position
    static int32_t remainRepeate = 0;
    if (path == path_to_close) { // to close decoder
        if (decoder) {
            aacDecoder_Close(decoder);
            decoder = nullptr;
            length = 0;
            position = 0;
            remainRepeate = 0;
        }
    } else {
        if (path) {// use non-null path to initialize mmap
            const char *filename = (const char *)path;
            int fd = open(filename, O_RDONLY);
            if (mmapFile != MAP_FAILED) munmap(mmapFile, length);
            if (fd < 0) {
                printf("%s => not found.\n", filename);
            } else {
                int fileLength = lseek(fd, 0l, SEEK_END);
                if (fileLength >= 512) {
                    mmapFile = mmap(0, fileLength, PROT_READ, MAP_PRIVATE, fd, 0);
                    if (mmapFile == MAP_FAILED)  {
                        printf("%s mmap fail.\n", filename);
                    } else {
                        if (decoder) aacDecoder_Close(decoder);
                        decoder = aacDecoder_Open(TT_MP4_ADTS, 1);
                        printf("%s file length = %d\n", filename, fileLength);
                        length = fileLength;// mmap sucess
                        position = 0;
                        remainRepeate = repeate;
                    }
                }
                close(fd);// After the mmap() call, fd can be closed immediately.
            }
        }
        if (remainRepeate > 0 && (position >= length)) { // when repeate enable
            printf("End of file, position = %d, wrap arond to repeate again. remainRepeate = %d\n", position, remainRepeate);
            remainRepeate --;
            position = 0;
        }
        if (position < length) {
            unsigned char *src[] = { (unsigned char *)mmapFile + position };
            static int16_t pcmdist[1152 * 5];// distinct PCM16 buffer for 5 channels
            const uint32_t maxSteps = length - position;
            uint32_t tempSteps = maxSteps;// tempSteps will be updated by decoder
            aacDecoder_Fill(decoder, src, &maxSteps, &tempSteps);
            int result = (int)aacDecoder_DecodeFrame(decoder, pcmdist, sizeof(pcmdist), 0);
            position += maxSteps - tempSteps; // go ahead, and back off by tempSteps
            printf("maxSteps = %8d, tempSteps = %8d, position = %8d, steps = %8d, err = %4x:\t\t\n",
                maxSteps,
                tempSteps,
                position,
                maxSteps - tempSteps,
                (unsigned)result
            );          
            if (result == AAC_DEC_OK) {               
                CStreamInfo *info = aacDecoder_GetStreamInfo(decoder);                         
                return PCM16 {
                    .data = pcmdist,
                    .length = info->frameSize,
                    .channel = info->numChannels,
                    .fs = info->sampleRate
                };
            }
        }
    }
    return zeroPCM16;
};

int main(int argc, char const *argv[]) {    
    PCM16 frame = stepDecoder((argc > 1) ? argv[1] : "test.aac");
     if (frame.length==0) return 0;
    snd_pcm_t *handle;
    if (snd_pcm_open(&handle, "default", SND_PCM_STREAM_PLAYBACK, 0) == 0) {
        if (snd_pcm_set_params(handle, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED, frame.channel, frame.fs, 1, frame.fs / 4) == 0) {    
            do {
                 int err = snd_pcm_writei(handle, frame.data, frame.length);
                 if (err < 0) {// try to recover
                     if (snd_pcm_recover(handle, err, 0) < 0) {
                        printf("alsa can't recover\n");
                        break;
                    }
                 }
                frame = stepDecoder();
            } while (frame.length);
        }
        snd_pcm_close(handle);
    }   
    stepDecoder(path_to_close);
    return 0;
}

編譯並執行:

    g++   aacplay.cpp   -lfdk-aac   -lasound   &&  ./a.out

2024年4月6日 星期六

linux 使用 ALSA lib 播放 mp3 檔

先上官網下載 minimp3 原始檔: https://github.com/lieff/minimp3

只需將檔案 minimp3.h 複製到專案目錄.  再編輯 mp3 播放主程式, 同樣透過 ALSA library 來播放:
// mp3play.cpp
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include "alsa/asoundlib.h"
#define MINIMP3_IMPLEMENTATION
#include "minimp3.h"

int main(int argc, char *argv[]) {
    const char *filename = (argc > 1) ? argv[1] : "test.mp3";
    int fd = open(filename, O_RDONLY);
    if (fd < 0) {
        printf("%s => not found\n", filename);
        return -1;
    }
    int fileLength = lseek(fd, 0, SEEK_END);
    unsigned char *fileMMAP = (unsigned char *) mmap(0, fileLength, PROT_READ, MAP_PRIVATE, fd, 0);
    int16_t speaker[MINIMP3_MAX_SAMPLES_PER_FRAME];

    mp3dec_frame_info_t info;
    mp3dec_t mp3decoder;
    mp3dec_init(&mp3decoder);
    int uframes = mp3dec_decode_frame(&mp3decoder, fileMMAP, fileLength, speaker, &info);// 1st frame to get channels && sampleRate
    if (uframes > 0) {
        printf("play uframes = %d, offset =%d, channels=%d, sampleRate=%d, frame_bytes=%d\n", uframes, info.frame_offset, info.channels, info.hz, info.frame_bytes);
        unsigned int channels = info.channels;
        unsigned int sampleRate = info.hz;
        snd_pcm_t *handle;
        if (snd_pcm_open(&handle, "default", SND_PCM_STREAM_PLAYBACK, 0) == 0) { // hw:0,0 , default
            if (snd_pcm_set_params(handle, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED, channels, sampleRate, 1, sampleRate / 4) == 0) {
                do {
                     const int err = snd_pcm_writei(handle, speaker, uframes);
                     if (err < 0) {
                         if (snd_pcm_recover(handle, err, 0) < 0) break;// alsa try to recover
                     }
                    fileMMAP += info.frame_bytes;// mp3 file position goes ahead
                    uframes = mp3dec_decode_frame(&mp3decoder, fileMMAP, uframes, speaker, &info);// is 1152 enough?
                } while (uframes > 0);
            }
            snd_pcm_close(handle);
        }
    }
    close(fd);
    return 0;
}
編譯並執行

      g++    mp3play.cpp    -lasound   &&   ./a.out

2024年4月5日 星期五

linux 使用 ALSA lib 播放 wav 檔

簡單的播放器
// File: play.cpp
#include "alsa/asoundlib.h"
struct ChunkHEAD {
    char id[4];
    int32_t length;
    char *stringID() {
        static char str[5];
        memcpy(str, id, 4);
        return str;
    }
    bool id_is(const char *name) {
        if (*name == 0) return false;
        for (int i = 0; i < 4; i ++) {
            if (name[i] == 0) break; // end of String
            if (name[i] != id[i]) return false;
        }
        return true;
    }
};
struct PCMtype {
    int16_t  pcmType, channels;
    int32_t  sampleRate, byteRate;
    int16_t  frameBytes, bitsData;
};
struct WAVstruct {
    ChunkHEAD title;
    char wave[4];
    ChunkHEAD fmt;
    PCMtype pcm;
    bool is_wav() {
        if (! title.id_is("RIFF")) return false;
        static const char waveID[5] = "WAVE";
        for (int i = 0; i < 4; i ++) { if (wave[i] != waveID[i]) return false; }
        return fmt.id_is("fmt");
    }
};
struct PCM_2CH {// interleave L/R
  int16_t l;
  int16_t r;
};
 
int main(void) {     
    FILE *fin= fopen("sample.wav", "rb");
    if (fin == nullptr) return -1;
    unsigned int sampleRate = 8000;
    unsigned int channels = 2;
      int frameBytes = sizeof(PCM_2CH);
    int frameSamples = 0;
    int bufSize = 1024;
    WAVstruct header;
    const int n = fread(&header, 1, sizeof(WAVstruct), fin);
    if (n > 0 && header.is_wav()) {
        sampleRate = header.pcm.sampleRate;
        channels   = header.pcm.channels;
        frameBytes = header.pcm.frameBytes;
        ChunkHEAD chunkList;
        do {
            if (fread(&chunkList, 1, sizeof(ChunkHEAD), fin) <= 0) break;
            if (chunkList.id_is("data")) break;
            fseek(fin, chunkList.length, SEEK_CUR);// ignore unknown id
        } while (! feof(fin));
        frameSamples = chunkList.length / frameBytes;
    }
    snd_pcm_t *handle;
      char speaker[bufSize * frameBytes];
    if (snd_pcm_open(&handle, "default", SND_PCM_STREAM_PLAYBACK, 0) == 0) {
         if (snd_pcm_set_params(handle, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED, channels, sampleRate, 1, sampleRate / 4) == 0) {
            while(! feof(fin)) {                   
                const int uframes = fread(speaker, frameBytes, bufSize, fin);
                if (uframes <= 0) break;
                const int err = snd_pcm_writei(handle, speaker, uframes);
                if (err < 0) snd_pcm_recover(handle, err, 0);// try to recover
            }
        }
        snd_pcm_close(handle);
    }
    fclose(fin);
    return 0;
}
編譯並執行:

     g++  play.cpp  -lasound  &&  ./a.out

後記: 改用 mmap 方式播放 wav 檔, 修改主程式
int main(int argc, char **argv) {     
    FILE *fin= fopen(argc > 1 ? argv[1] : "sample.wav", "rb");
    if (fin == nullptr) return -1;
    unsigned int sampleRate = 44100;
    unsigned int channels = 2;  
    WAVstruct header;
    const int n = fread(&header, 1, sizeof(WAVstruct), fin);
    if (n > 0 && header.is_wav()) {
        sampleRate = header.pcm.sampleRate;
        channels   = header.pcm.channels;  
        ChunkHEAD chunkList;
        do {
            if (fread(&chunkList, 1, sizeof(ChunkHEAD), fin) <= 0) break;
            if (chunkList.is_id("data")) break;
            chunkList.length);
            fseek(fin, chunkList.length, SEEK_CUR);// ignore unknown id
        } while (! feof(fin));
    }
    snd_pcm_t *handle;
    if (snd_pcm_open(&handle, "hw:1,0", SND_PCM_STREAM_PLAYBACK, 0) == 0) { // hw:1,0 , default
        if (snd_pcm_set_params(handle, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_MMAP_INTERLEAVED, channels, sampleRate, 1, sampleRate / 4) == 0) {
            const snd_pcm_channel_area_t* areas;
            snd_pcm_uframes_t offset;
            snd_pcm_uframes_t uframes = 1024;// size to request, it will be updated by DMA
            snd_pcm_mmap_begin(handle, &areas, &offset, &uframes);// DMA 需要的 addr, offset, uframes
            const int first = areas->first / 8;
            const int uBytes = areas->step / 8;
            int8_t *data = (int8_t *)areas->addr + first;
            printf("begin offset = %ld, uframes =%ld, first = %d, uBytes = %d bytes\n", offset,  uframes, first , uBytes);
            memset(data + offset * uBytes, 0, uBytes * uframes);// fill zero data
            snd_pcm_mmap_commit(handle , offset , uframes);
            snd_pcm_start(handle); // 開始播放
            int count = 0;
            while (!feof(fin))  {
                snd_pcm_wait(handle, -1);
                uframes = 1024;
                snd_pcm_mmap_begin(handle, &areas, &offset, &uframes);
                uframes = fread(data + offset * uBytes, uBytes, uframes, fin);
                if (uframes <= 0) break;
                snd_pcm_mmap_commit(handle, offset, uframes);
            }
        }        
        snd_pcm_close(handle);
    }
    fclose(fin);
    return 0;
}

2024年3月12日 星期二

簡單的 flutter app, 透過 ffi 呼叫 oboe 播放 dtmf 聲音

參考文章:     

    1. https://github.com/google/oboe/blob/main/docs/FullGuide.md 

    2. https://chromium.googlesource.com/external/github.com/google/oboe/+/io18codelab/GettingStarted.md

用  flutter   create   projectDir 產生新專案, 並切換到該專案目錄

   cd  projectDir

擬使用 oboe 的原始程式碼, oboe 是用 c 語言寫的, 用 git 把整個目錄下載回來, 放到專案的 lib/cpp 目錄下面

   cd  lib   &&   mkdir  cpp   &&   cd  cpp   &&   git  clone  https://github.com/google/oboe

針對 Android 系統, 要建好程式庫目錄及做好目錄連結:

   cd  android   &&  mkdir  lib  &&   cd  lib   &&  ln   -sf   ../../lib/cpp  .

接著編輯 android/app/build.gradle , 加入 cmake 的支援, 添加下面綠色部份就可:

    android  {

        ...

        externalNativeBuild {
            cmake {
                path "../lib/CMakeLists.txt"
            }
        

        ...    

  }

再來編輯 android/lib/CMakeLists.txt, 加入要編譯的項目:

cmake_minimum_required(VERSION 3.16.3)
    project("oboedtmf")   
    set (OBOE_DIR  cpp/oboe)
    include_directories(${OBOE_DIR}/include)
    add_subdirectory (${OBOE_DIR}  cpp/oboe)   
    add_library(native SHARED  cpp/native.cpp)
    target_link_libraries(native android  oboe  jnigraphics)

編輯程式  lib/cpp/native.cpp, 讓 flutter 應用程式, 可以透過 dart: ffi 去呼叫它

include <math.h>
#include <oboe/Oboe.h>
// DTMF   1209     1336     1477     1633
// 697       1 ->1     2 ->2     3 ->3      A ->4
// 770       4 ->5     5 ->6     6 ->7      B ->8
// 852       7 ->9     8 ->10   9 ->11    C ->12
// 941       * ->13    0 ->14   # ->15    D ->16
int dtmfTone[16][2]= {
  {1209, 697}, {1336, 697}, {1477, 697}, {1633, 697},
  {1209, 770}, {1336, 770}, {1477, 770}, {1633, 770},
  {1209, 852}, {1336, 852}, {1477, 852}, {1633, 852},
  {1209, 941}, {1336, 941}, {1477, 941}, {1633, 941}
};
const double pi2 = 2 * M_PI;

class DTMFplayer : public oboe::AudioStreamCallback {
  private:
    double radian[16][2];//  θ: phase in radians
    double dT[16][2];    // dθ: delta θ
  public:
    int channels = 0;
    int repeat = 0;
    int tone = 0;
    int fs = 0;
    oboe::DataCallbackResult onAudioReady(oboe::AudioStream *sink, void *data, int32_t num) override {
      if (tone > 16 || tone <= 0) return oboe::DataCallbackResult::Stop;// do NOT call read() or write() on the stream in this callback !!!
      float *yt = static_cast<float *>(data);// time dependent data y(t) = Σ sin(2π*fk*t/fs)
      int i = tone - 1;// vallid tone index: 1 ~ 16
      for (int t = 0, offset = 0; t < num; t ++, offset += channels) {
        float amplitude = 0.0;
        for (int k = 0; k < 2; k ++) { // superposition: Σ sin(2π*fk*t/fs)
          amplitude += sinf(radian[i][k]);
          radian[i][k] += dT[i][k];
          if (radian[i][k] >= pi2) radian[i][k] -= pi2;
        }
        for (int ch = 0; ch < channels; ch ++) { // normalize amplitude for all channels: -1 ~ 1
          yt[offset + ch] = amplitude / 2; // todo: speed up divide by 2
        }
      }
      return (++ repeat <= 32) ?
            oboe::DataCallbackResult::Continue : oboe::DataCallbackResult::Stop; // auto stop
    }
    void update(int index) {
      if (dtmfOut == nullptr) return;
      tone = (1 <= index && index <= 16) ? index : 0;
      if (tone == 0) {
        dtmfOut->pause(0);
      } else {
        repeat = 0;
        for (int j = 0; j < 16; j ++)  {
          for (int k = 0; k < 2; k ++) { radian[j][k] = 0.0; }// phase reset
        }
        dtmfOut->start(0);
      }
    }
    std::shared_ptr<oboe::AudioStream> dtmfOut;// oboe::ManagedStream dtmfOut;// output stream
    DTMFplayer() {
      oboe::AudioStreamBuilder builder;
      builder.setFormat(oboe::AudioFormat::Float); // float data
      builder.setDirection(oboe::Direction::Output);
      builder.setSharingMode(oboe::SharingMode::Shared);// Shared, Exclusive
      builder.setPerformanceMode(oboe::PerformanceMode::LowLatency);
      builder.setCallback(this);    
      if (builder.openStream(dtmfOut) == oboe::Result::OK) {// if (builder.openManagedStream(dtmfOut) == oboe::Result::OK) {
        channels = dtmfOut->getChannelCount();
        fs = dtmfOut->getSampleRate(); // sample rate
        if (fs > 0) {
          for (int j = 0; j < 16; j ++) {
            for (int k = 0; k < 2; k++) {
              dT[j][k] = pi2 * dtmfTone[j][k] / fs; // dθ = 2π*fk/fs
            }
          }
        }
      }
    }
};
static DTMFplayer *player;
extern "C" { // export C function
  int create( ) {
    player = new DTMFplayer();
    return player ? 1 : 0;
  }
  void destroy( ) {
    if (player) {
      delete player;
      player = nullptr;
    }
  }
  void sound(int tone) {// 0: turn off sound, otherwise turn dtmf on when tone > 0
    if (player) player->update(tone);
  }
}


備註:   若編譯有問題, 像是 error:   assert  .... 什麼的, 若 assert 不影響程式的運作, 就把該行用 // 註解掉, 存檔再重新編譯.

2024年2月27日 星期二

用 dart 語法, 針對 Uint8List 資料流(stream): 內容是 vt100 字串, 僅把 ESC[ ... m 移除並轉換成 utf8 字串

Uint8List remain = Uint8List(0);
String utf8Log = "";
void stream2utf8(Uint8List data) {
  final findLF = data.lastIndexWhere((e) => e == 0xa);// find position of the last \n
  final endLF = findLF + 1; // to include LF
  final len = remain.length + (findLF < 0 ? data.length : endLF);
  final resemble = Uint8List(len);
  if (remain.isNotEmpty) resemble.setAll(0, remain);// copy remain to front of resemble
  resemble.setRange(remain.length, len, data); // append sublist of data to resemble
  if (findLF < 0) { // keep in memory
    remain = resemble;
  } else { // line feed found, time to flush out
    remain = Uint8List.sublistView(data, endLF);
    Uint8List afterESC = Uint8List.sublistView(resemble);
    while (afterESC.isNotEmpty) { // until empty
      final findESC = afterESC.indexOf(0x1b); // find ESC position
      utf8Log += utf8.decode(Uint8List.sublistView(afterESC, 0,
        findESC < 0 ? afterESC.length : findESC
      ));
      if (findESC < 0) break;
      var lenESC = 1; // sequence ^[ ... m seek
      if (afterESC[findESC + lenESC] == 0x5b) { // found sequence start '['
        do {
          lenESC ++;  // one byte advance
          if (afterESC[findESC + lenESC] == 0x6d) { // found end of character 'm'
            lenESC ++;// one byte advance
            break;
          }
        } while (findESC + lenESC < afterESC.length);
        if (findESC + lenESC >= afterESC.length) { // todo: other ^[ sequence not yet implement!
          afterESC[findESC] = 0x5E; // change ESC from 0x1b to '^' to prevent infinit loop
          continue;// backoff to show this sequence
        }
      } else { // todo: other ^ sequence not yet implement!
        afterESC[findESC] = 0x5E; // change ESC from 0x1b to '^' to prevent infinit loop
        continue;// backoff to show this sequence
      }
      afterESC = Uint8List.sublistView(afterESC, findESC + lenESC);// ok,  go ahead
    }
    setState((){}); // update screen if necessary
}

2024年2月19日 星期一

簡單的 flutter app 用來測試 usb serial for android

在 Android 系統上要使用 usb serial port 可以透過 usb-serial-for-android , 它是用 JAVA 語言寫的, 上官網打包先將它下載回來:  https://github.com/mik3y/usb-serial-for-android, 將它解壓縮

    unzip  usb-serial-for-android-master.zip

1. 打開終端機用 flutter 命令產生一個新專案(例如 usbadc):  

    flutter create usbadc

2. 將上述 usb-serial-for-android 解開的檔案, 只要把目錄 usbSerialForAndroid/src/main/java/com 整個驅動程式的目錄複製到新專案:


2024年2月8日 星期四

linux 上解決核心模組 ch34x.ko 掛載 /dev/ttyUSB0 時被強制斷線的問題

 參考討論文章: https://unix.stackexchange.com/questions/670636/unable-to-use-usb-dongle-based-on-usb-serial-converter-chip

只要有 root 權限執行以下腳本就能解決, 一勞永逸辦法是將它放在 /etc/rc.local 裡面, 一開機就執行

        # ...

    for f in /usr/lib/udev/rules.d/*brltty*.rules; do
        sudo ln -s /dev/null "/etc/udev/rules.d/$(basename "$f")"
    done
    sudo udevadm control --reload-rules

     

2024年1月27日 星期六

量測電感與電容

 參考文章:  https://www-schiessle-de.translate.goog/emt1/MessKleinKap/MessKleinKap1.htm?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=nui

討論文章: https://www.edaboard.com/threads/why-not-the-lm339-instead-of-lm311-in-aade-lc-meter-clones.183392/ 

開源碼: https://github.com/coreWeaver/LC-Meter

 

備註: 透過比較器 LM311 及 L1, C1 產生最高工作頻率 f1, 並利用準確 C0 值去校準 L1 及 C1. 透過串聯 L1 與待測電感或是並聯 C1 與待測電容, 量測出諧振頻率而計算出待測數值.  LM311 比較器可以產生 1MHz 頻率, 用 68uH/1000pF 時, 最高工作頻率約 750kHz, 若要改用 LM393 可能要修改 L1/C1 為 100uH/2nF 讓工作頻率低於 500kHz.

2024年1月16日 星期二

使用低功耗低壓差 3.3V LDO 搭配可調 Emitter follower (2N2222 npn 電晶體 + 可變電阻) 調整鋰電池電壓(3.6 ~ 4.2V) 成 1.5V

 

市面上便宜的時鐘, 內部機芯大多使用晶體振盪器產生固定頻率的脈沖去驅動 Lavet-type 步進馬達, 它只要一個 1.5V 電池就足以推動, 而馬達扭力主要是靠線圈內的電流產生磁力所致, 因此電流才是動力來源. 電壓太高會造成線圈電流飽和導致發熱,可能影響震盪頻率甚至燒毀, 使用鋰電池最怕的是電路短路產生爆炸, 下圖電路中分壓電阻 Ru 用 2k 歐姆用來限制 emiiter follower 的基極(base)電流, 可變電阻用來調整射極端輸出電壓(調整到要能同時推動時/分/秒針), 可變電阻 Rv 用 0 ~10k 歐姆, 計算出主動區輸出大約是 3.3*Rv/(Rv + Ru) - 0.6, 也就是說可調整輸出至 0 ~ 2.15V, 若無法成功運轉, 只要降低 Ru 值再調整可變電阻就可以了.




2024年1月13日 星期六

簡單電路用來測量鋰電池內電阻

 


Linux mint 的狀態欄不小心刪掉了

 參考論壇的討論文章: https://forums.linuxmint.com/viewtopic.php?p=2004336#p2004336

開啟終端機 (按快速鍵: Ctrl + alt + T), 再執行以下命令就能恢復:

    dconf reset -f /org/cinnamon/

用 python 解簡單的常微分方程式

# sudo apt install python3-pip # python3 -m venv venv # cd venv # . bin/activate # pip3 install numpy matplotlib import numpy as np import m...