]> git.the-white-hart.net Git - vhdl/commitdiff
Add things for mp3 cassette project
authorrs <>
Tue, 20 Jan 2026 03:50:56 +0000 (21:50 -0600)
committerrs <>
Tue, 20 Jan 2026 03:50:56 +0000 (21:50 -0600)
.gitignore
libraries/dsp/pcm16_2ch_sum.vhd
libraries/dsp/pcm16_2ch_tapeeffect.vhd [new file with mode: 0644]
libraries/dsp/pcm16_2ch_vardelay.vhd [new file with mode: 0644]
libraries/dsp/pcm16_2ch_windowsum.vhd [new file with mode: 0644]
libraries/dsp/table_sine_1k_16.vhd [new file with mode: 0644]
projects/mp3tape/nexys2.vhd [new file with mode: 0644]
projects/mp3tape/test_tapeeffect.vhd [new file with mode: 0644]
projects/mp3tape/tests/test_nexys2.vhd [new file with mode: 0644]
projects/mp3tape/tools/fft.py [new file with mode: 0755]
projects/mp3tape/tools/jankman.py [new file with mode: 0755]

index e01d0b5addab8b1276e0869bccb48f44d85902f6..09b942d464a57c8ef0010cca771705a75270db1e 100644 (file)
@@ -21,7 +21,10 @@ projects/nexys2_host_controller/host/jtag
 projects/nexys2_host_controller/host/stm
 projects/nexys2_host_controller/host/stm_test
 
-# Test audio files
+# Test audio files, because copyright law is important to me
 *.wav
+*.mp3
+*.jnk
 
 .idea/
+__pycache__/
index 369f26ac9d64597995e058bda327ad660304e2dd..2f4c89d82d63b0072a351d5d84a4b522b081f12b 100644 (file)
@@ -53,8 +53,8 @@ begin
        samp_b_l <= signed(b_dat_i(15 downto  0));
        samp_b_r <= signed(b_dat_i(31 downto 16));
 
-       result_l <= samp_a_l + samp_b_l;
-       result_r <= samp_a_r + samp_b_r;
+       result_l <= (samp_a_l(15) & samp_a_l) + ((samp_b_l(15)) & samp_b_l);
+       result_r <= (samp_a_r(15) & samp_a_r) + ((samp_b_r(15)) & samp_b_r);
 
        e_sat_l: entity work.saturate
                generic map (WIDTH_IN => 17, WIDTH_OUT => 16)
diff --git a/libraries/dsp/pcm16_2ch_tapeeffect.vhd b/libraries/dsp/pcm16_2ch_tapeeffect.vhd
new file mode 100644 (file)
index 0000000..1769458
--- /dev/null
@@ -0,0 +1,228 @@
+library ieee;
+use ieee.std_logic_1164.all;
+use ieee.numeric_std.all;
+
+library work;
+
+
+entity pcm16_2ch_tapeeffect is
+       port (
+               rst_i:      in  std_logic;
+               clk_i:      in  std_logic;
+
+               en_rolloff: in  std_logic;
+               en_flutter: in  std_logic;
+               en_wow:     in  std_logic;
+               en_noise:   in  std_logic;
+
+               stb_i:      in  std_logic;
+               rdy_o:      out std_logic;
+               dat_i:      in  std_logic_vector(31 downto 0);
+
+               stb_o:      out std_logic;
+               rdy_i:      in  std_logic;
+               dat_o:      out std_logic_vector(31 downto 0)
+       );
+end pcm16_2ch_tapeeffect;
+
+
+architecture behavioral of pcm16_2ch_tapeeffect is
+
+       -- Stage 1 - High frequency rolloff
+
+       signal split_a_stb: std_logic;
+       signal split_a_rdy: std_logic;
+       signal split_b_stb: std_logic;
+       signal split_b_rdy: std_logic;
+       signal split_dat:   std_logic_vector(31 downto 0);
+
+       signal filter_stb:  std_logic;
+       signal filter_rdy:  std_logic;
+       signal filter_dat:  std_logic_vector(31 downto 0);
+
+       signal bypass_stb:  std_logic;
+       signal bypass_rdy:  std_logic;
+       signal bypass_dat:  std_logic_vector(31 downto 0);
+
+       signal merge_stb:   std_logic;
+       signal merge_rdy:   std_logic;
+       signal merge_dat:   std_logic_vector(31 downto 0);
+
+       signal stage1_stb:  std_logic;
+       signal stage1_rdy:  std_logic;
+       signal stage1_dat:  std_logic_vector(31 downto 0);
+
+       -- Stage 2 - Wow and flutter
+
+       constant FLUTTER_WIDTH: integer := 9;
+       constant FLUTTER_INC:   std_logic_vector(15 downto 0) := "00000000"&"00100000";
+       signal flutter_stb: std_logic;
+       signal flutter_rdy: std_logic;
+       signal flutter_dat: std_logic_vector(FLUTTER_WIDTH-1 downto 0);
+
+       signal delay_dat:   std_logic_vector(9 downto 0);
+
+       signal stage2_stb:  std_logic;
+       signal stage2_rdy:  std_logic;
+       signal stage2_dat:  std_logic_vector(31 downto 0);
+
+       -- Stage 3 - Hiss
+
+       signal noise_stb:   std_logic;
+       signal noise_rdy:   std_logic;
+       signal noise_raw:   std_logic_vector( 9 downto 0);
+       signal noise_dat:   std_logic_vector(31 downto 0);
+
+begin
+
+       ----------------------------------------------------------------------------
+       -- Low pass filter the audio stream
+
+       e_split: entity work.pipectrl_split
+               generic map (WIDTH => 32)
+               port map (
+                       rst_i   => rst_i,
+                       clk_i   => clk_i,
+
+                       stb_i   => stb_i,
+                       rdy_o   => rdy_o,
+                       dat_i   => dat_i,
+
+                       a_stb_o => split_a_stb,
+                       a_rdy_i => split_a_rdy,
+
+                       b_stb_o => split_b_stb,
+                       b_rdy_i => split_b_rdy,
+
+                       dat_o   => split_dat
+               );
+
+       e_filter: entity work.pcm16_2ch_windowsum
+               generic map (WINDOW => 16)
+               port map (
+                       rst_i => rst_i,
+                       clk_i => clk_i,
+
+                       stb_i => split_a_stb,
+                       rdy_o => split_a_rdy,
+                       dat_i => split_dat,
+
+                       stb_o => filter_stb,
+                       rdy_i => filter_rdy,
+                       dat_o => filter_dat
+               );
+
+       e_bypass: entity work.pipectrl
+               generic map (WIDTH => 32)
+               port map (
+                       rst_i => rst_i,
+                       clk_i => clk_i,
+
+                       stb_i => split_b_stb,
+                       rdy_o => split_b_rdy,
+                       dat_i => split_dat,
+
+                       stb_o => bypass_stb,
+                       rdy_i => bypass_rdy,
+                       dat_o => bypass_dat
+               );
+
+       e_filter_merge: entity work.merge
+               port map (
+                       a_stb_i => filter_stb,
+                       a_rdy_o => filter_rdy,
+
+                       b_stb_i => bypass_stb,
+                       b_rdy_o => bypass_rdy,
+
+                       stb_o   => merge_stb,
+                       rdy_i   => merge_rdy
+               );
+       merge_dat <= filter_dat when en_rolloff = '1' else bypass_dat;
+
+       e_filter_ctrl: entity work.pipectrl
+               generic map (WIDTH => 32)
+               port map (
+                       rst_i => rst_i,
+                       clk_i => clk_i,
+
+                       stb_i => merge_stb,
+                       rdy_o => merge_rdy,
+                       dat_i => merge_dat,
+
+                       stb_o => stage1_stb,
+                       rdy_i => stage1_rdy,
+                       dat_o => stage1_dat
+               );
+
+       ----------------------------------------------------------------------------
+       -- Variable delay for wow and flutter
+
+       e_flutter: entity work.src_fracstep
+               generic map (WIDTH_OUT => FLUTTER_WIDTH)
+               port map (
+                       rst_i => rst_i,
+                       clk_i => clk_i,
+
+                       inc_i => std_logic_vector(unsigned(FLUTTER_INC)),
+
+                       stb_o => flutter_stb,
+                       rdy_i => flutter_rdy,
+                       dat_o => flutter_dat
+               );
+
+       delay_dat <= std_logic_vector(resize(unsigned(flutter_dat), 10)) when en_flutter = '1' else (others => '0');
+
+       e_vardelay: entity work.pcm16_2ch_vardelay
+               port map (
+                       rst_i       => rst_i,
+                       clk_i       => clk_i,
+
+                       delay_stb_i => flutter_stb,
+                       delay_rdy_o => flutter_rdy,
+                       delay_dat_i => delay_dat,
+
+                       audio_stb_i => stage1_stb,
+                       audio_rdy_o => stage1_rdy,
+                       audio_dat_i => stage1_dat,
+
+                       stb_o       => stage2_stb,
+                       rdy_i       => stage2_rdy,
+                       dat_o       => stage2_dat
+               );
+
+       ----------------------------------------------------------------------------
+       -- Add noise stream to audio stream
+
+       e_noise: entity work.src_noise
+               generic map (WIDTH => 10)
+               port map (
+                       clk_i => clk_i,
+
+                       stb_o => noise_stb,
+                       rdy_i => noise_rdy,
+                       dat_o => noise_raw
+               );
+
+       noise_dat <= std_logic_vector(resize(signed(noise_raw), 16)) &
+                    std_logic_vector(resize(signed(noise_raw), 16)) when en_noise = '1' else (others => '0');
+
+       e_sum: entity work.pcm16_2ch_sum
+               port map (
+                       rst_i   => rst_i,
+                       clk_i   => clk_i,
+
+                       a_stb_i => stage2_stb,
+                       a_rdy_o => stage2_rdy,
+                       a_dat_i => stage2_dat,
+
+                       b_stb_i => noise_stb,
+                       b_rdy_o => noise_rdy,
+                       b_dat_i => noise_dat,
+
+                       stb_o   => stb_o,
+                       rdy_i   => rdy_i,
+                       dat_o   => dat_o
+               );
+
+end behavioral;
diff --git a/libraries/dsp/pcm16_2ch_vardelay.vhd b/libraries/dsp/pcm16_2ch_vardelay.vhd
new file mode 100644 (file)
index 0000000..d79d347
--- /dev/null
@@ -0,0 +1,146 @@
+library ieee;
+use ieee.std_logic_1164.all;
+use ieee.numeric_std.all;
+
+library unisim;
+use unisim.vcomponents.all;
+
+library work;
+
+
+entity pcm16_2ch_vardelay is
+       port (
+               rst_i:       in  std_logic;
+               clk_i:       in  std_logic;
+
+               delay_stb_i: in  std_logic;
+               delay_rdy_o: out std_logic;
+               delay_dat_i: in  std_logic_vector(9 downto 0);
+
+               audio_stb_i: in  std_logic;
+               audio_rdy_o: out std_logic;
+               audio_dat_i: in  std_logic_vector(31 downto 0);
+
+               stb_o:       out std_logic;
+               rdy_i:       in  std_logic;
+               dat_o:       out std_logic_vector(31 downto 0)
+       );
+end pcm16_2ch_vardelay;
+
+
+architecture behavioral of pcm16_2ch_vardelay is
+
+       constant COUNT_WIDTH: integer := 10;
+
+       signal en:           std_logic;
+       signal stb:          std_logic;
+       signal rdy:          std_logic;
+
+       signal samp_l:       std_logic_vector(15 downto 0);
+       signal samp_r:       std_logic_vector(15 downto 0);
+
+       signal result_l:     std_logic_vector(15 downto 0);
+       signal result_r:     std_logic_vector(15 downto 0);
+
+       signal ptr_head_reg: unsigned(COUNT_WIDTH-1 downto 0) := (others => '0');
+       signal ptr_tail:     unsigned(COUNT_WIDTH-1 downto 0);
+
+begin
+
+       e_merge: entity work.merge
+               port map (
+                       a_stb_i => delay_stb_i,
+                       a_rdy_o => delay_rdy_o,
+                       b_stb_i => audio_stb_i,
+                       b_rdy_o => audio_rdy_o,
+                       stb_o   => stb,
+                       rdy_i   => rdy
+               );
+
+       e_ctrl: entity work.pipectrl
+               generic map (WIDTH => 0)
+               port map (
+                       rst_i => rst_i,
+                       clk_i => clk_i,
+                       en_o  => en,
+                       stb_i => stb,
+                       rdy_o => rdy,
+                       dat_i => open,
+                       stb_o => stb_o,
+                       rdy_i => rdy_i,
+                       dat_o => open
+               );
+
+       process (rst_i, clk_i, en, ptr_head_reg)
+       begin
+               if rising_edge(clk_i) then
+                       if rst_i = '1' then
+                               -- Probably don't need reset logic
+                       elsif en = '1' then
+                               ptr_head_reg <= ptr_head_reg + 1;
+                       end if;
+               end if;
+       end process;
+
+       samp_l <= audio_dat_i(15 downto  0);
+       samp_r <= audio_dat_i(31 downto 16);
+
+       ptr_tail <= (ptr_head_reg - unsigned(delay_dat_i)) - 1;
+
+       e_fifo_l: ramb16_s18_s18
+               port map (
+                       -- Head port (insertion)
+                       WEA   => '1',
+                       ENA   => en,
+                       SSRA  => '0',
+                       CLKA  => clk_i,
+                       ADDRA => std_logic_vector(ptr_head_reg),
+                       DIA   => samp_l,
+                       DIPA  => (others => '0'),
+
+                       DOA   => open,
+                       DOPA  => open,
+
+                       -- Tail port (removal)
+                       WEB   => '0',
+                       ENB   => en,
+                       SSRB  => '0',
+                       CLKB  => clk_i,
+                       ADDRB => std_logic_vector(ptr_tail),
+                       DIB   => (others => '0'),
+                       DIPB  => (others => '0'),
+
+                       DOB   => result_l,
+                       DOPB  => open
+               );
+
+       e_fifo_r: ramb16_s18_s18
+               port map (
+                       -- Head port (insertion)
+                       WEA   => '1',
+                       ENA   => en,
+                       SSRA  => '0',
+                       CLKA  => clk_i,
+                       ADDRA => std_logic_vector(ptr_head_reg),
+                       DIA   => samp_r,
+                       DIPA  => (others => '0'),
+
+                       DOA   => open,
+                       DOPA  => open,
+
+                       -- Tail port (removal)
+                       WEB   => '0',
+                       ENB   => en,
+                       SSRB  => '0',
+                       CLKB  => clk_i,
+                       ADDRB => std_logic_vector(ptr_tail),
+                       DIB   => (others => '0'),
+                       DIPB  => (others => '0'),
+
+                       DOB   => result_r,
+                       DOPB  => open
+               );
+
+       dat_o <= result_r & result_l;
+
+end behavioral;
diff --git a/libraries/dsp/pcm16_2ch_windowsum.vhd b/libraries/dsp/pcm16_2ch_windowsum.vhd
new file mode 100644 (file)
index 0000000..83bff1c
--- /dev/null
@@ -0,0 +1,96 @@
+library ieee;
+use ieee.std_logic_1164.all;
+use ieee.math_real.all;
+
+library work;
+
+
+entity pcm16_2ch_windowsum is
+       generic (
+               WINDOW: positive := 16
+       );
+       port (
+               rst_i: in  std_logic;
+               clk_i: in  std_logic;
+
+               stb_i: in  std_logic;
+               rdy_o: out std_logic;
+               dat_i: in  std_logic_vector(31 downto 0);
+
+               stb_o: out std_logic;
+               rdy_i: in  std_logic;
+               dat_o: out std_logic_vector(31 downto 0)
+       );
+end pcm16_2ch_windowsum;
+
+
+architecture behavioral of pcm16_2ch_windowsum is
+
+       constant FILT_WIDTH: positive := 16+integer(ceil(log2(real(WINDOW))));
+
+       signal en:       std_logic;
+
+       signal samp_l:   std_logic_vector(15 downto 0);
+       signal samp_r:   std_logic_vector(15 downto 0);
+
+       signal filt_l:   std_logic_vector(FILT_WIDTH-1 downto 0);
+       signal filt_r:   std_logic_vector(FILT_WIDTH-1 downto 0);
+
+       signal result_l: std_logic_vector(15 downto 0);
+       signal result_r: std_logic_vector(15 downto 0);
+
+begin
+
+       e_ctrl: entity work.pipectrl
+               generic map (WIDTH => 0)
+               port map (
+                       rst_i => rst_i,
+                       clk_i => clk_i,
+                       en_o  => en,
+                       stb_i => stb_i,
+                       rdy_o => rdy_o,
+                       dat_i => open,
+                       stb_o => stb_o,
+                       rdy_i => rdy_i,
+                       dat_o => open
+               );
+
+       samp_l <= dat_i(15 downto  0);
+       samp_r <= dat_i(31 downto 16);
+
+       e_filter_l: entity work.filter_windowsum
+               generic map (
+                       WIDTH  => 16,
+                       WINDOW => WINDOW
+               )
+               port map (
+                       rst_i => rst_i,
+                       clk_i => clk_i,
+
+                       en_i  => en,
+
+                       dat_i => samp_l,
+                       dat_o => filt_l
+               );
+
+       e_filter_r: entity work.filter_windowsum
+               generic map (
+                       WIDTH  => 16,
+                       WINDOW => WINDOW
+               )
+               port map (
+                       rst_i => rst_i,
+                       clk_i => clk_i,
+
+                       en_i  => en,
+
+                       dat_i => samp_r,
+                       dat_o => filt_r
+               );
+
+       result_l <= filt_l(FILT_WIDTH-1 downto FILT_WIDTH-16);
+       result_r <= filt_r(FILT_WIDTH-1 downto FILT_WIDTH-16);
+
+       dat_o <= result_r & result_l;
+
+end behavioral;
diff --git a/libraries/dsp/table_sine_1k_16.vhd b/libraries/dsp/table_sine_1k_16.vhd
new file mode 100644 (file)
index 0000000..7d2fef1
--- /dev/null
@@ -0,0 +1,105 @@
+library ieee;
+use ieee.std_logic_1164.all;
+use ieee.numeric_std.all;
+use ieee.math_real.all;
+
+library unisim;
+use unisim.vcomponents.all;
+
+
+entity table_sine_1k_16 is
+       port (
+               rst_i: in  std_logic;
+               clk_i: in  std_logic;
+
+               en:    in  std_logic;
+
+               adr_i: in  std_logic_vector(9 downto 0);
+               dat_o: out std_logic_vector(15 downto 0)
+       );
+end table_sine_1k_16;
+
+
+architecture behavioral of table_sine_1k_16 is
+begin
+
+       e_table: ramb16_s18
+               generic map (
+                       INIT_00 = x"0bc30afb0a32096a08a107d907100647057e04b603ed0324025b019200c90000",
+                       INIT_01 = x"1833176d16a715e1151b1455138e12c71200113910720fab0ee30e1b0d530c8b",
+                       INIT_02 = x"246723a622e422232161209f1fdc1f191e561d931ccf1c0b1b461a8219bd18f8",
+                       INIT_03 = x"30412f862ecc2e102d542c982bdb2b1e2a6129a328e52826276726a725e72527",
+                       INIT_04 = x"3ba43af23a3f398c38d83824376f36b93603354d349633de3326326d31b430fb",
+                       INIT_05 = x"467445cc4523447a43d04325427941cd412040733fc53f163e673db73d073c56",
+                       INIT_06 = x"50974ffa4f5d4ebf4e204d804ce04c3f4b9d4afa4a5749b3490e486947c3471c",
+                       INIT_07 = x"59f3596358d3584257b0571d568955f4555f54c95432539a5301526851ce5133",
+                       INIT_08 = x"627161f0616e60eb60675fe25f5d5ed65e4f5dc65d3d5cb35c285b9c5b0f5a81",
+                       INIT_09 = x"69fc698b691968a5683167bc674566ce665665dd656264e7646b63ee637062f1",
+                       INIT_0a = x"708270226fc06f5e6efa6e956e306dc96d616cf86c8e6c236bb76b4a6adb6a6c",
+                       INIT_0b = x"75f375a47554750374b1745e740a73b5735e730672ae725471f9719d714070e1",
+                       INIT_0c = x"7a417a0479c779897949790878c67883783f77f977b3776b772276d8768d7640",
+                       INIT_0d = x"7d617d387d0e7ce27cb67c887c597c297bf77bc47b917b5c7b257aee7ab57a7c",
+                       INIT_0e = x"7f4c7f377f207f087eef7ed47eb97e9c7e7e7e5e7e3e7e1c7df97dd57db07d89",
+                       INIT_0f = x"7ffe7ffc7ff97ff57fef7fe87fe07fd77fcd7fc17fb47fa67f967f867f747f61",
+                       INIT_10 = x"7f747f867f967fa67fb47fc17fcd7fd77fe07fe87fef7ff57ff97ffc7ffe7fff",
+                       INIT_11 = x"7db07dd57df97e1c7e3e7e5e7e7e7e9c7eb97ed47eef7f087f207f377f4c7f61",
+                       INIT_12 = x"7ab57aee7b257b5c7b917bc47bf77c297c597c887cb67ce27d0e7d387d617d89",
+                       INIT_13 = x"768d76d87722776b77b377f9783f788378c679087949798979c77a047a417a7c",
+                       INIT_14 = x"7140719d71f9725472ae7306735e73b5740a745e74b17503755475a475f37640",
+                       INIT_15 = x"6adb6b4a6bb76c236c8e6cf86d616dc96e306e956efa6f5e6fc07022708270e1",
+                       INIT_16 = x"637063ee646b64e7656265dd665666ce674567bc683168a56919698b69fc6a6c",
+                       INIT_17 = x"5b0f5b9c5c285cb35d3d5dc65e4f5ed65f5d5fe2606760eb616e61f0627162f1",
+                       INIT_18 = x"51ce52685301539a543254c9555f55f45689571d57b0584258d3596359f35a81",
+                       INIT_19 = x"47c34869490e49b34a574afa4b9d4c3f4ce04d804e204ebf4f5d4ffa50975133",
+                       INIT_1a = x"3d073db73e673f163fc54073412041cd4279432543d0447a452345cc4674471c",
+                       INIT_1b = x"31b4326d332633de3496354d360336b9376f382438d8398c3a3f3af23ba43c56",
+                       INIT_1c = x"25e726a72767282628e529a32a612b1e2bdb2c982d542e102ecc2f86304130fb",
+                       INIT_1d = x"19bd1a821b461c0b1ccf1d931e561f191fdc209f2161222322e423a624672527",
+                       INIT_1e = x"0d530e1b0ee30fab10721139120012c7138e1455151b15e116a7176d183318f8",
+                       INIT_1f = x"00c90192025b032403ed04b6057e0647071007d908a1096a0a320afb0bc30c8b",
+                       INIT_20 = x"f43df505f5cef696f75ff827f8f0f9b9fa82fb4afc13fcdcfda5fe6eff370000",
+                       INIT_21 = x"e7cde893e959ea1feae5ebabec72ed39ee00eec7ef8ef055f11df1e5f2adf375",
+                       INIT_22 = x"db99dc5add1cddddde9fdf61e024e0e7e1aae26de331e3f5e4bae57ee643e708",
+                       INIT_23 = x"cfbfd07ad134d1f0d2acd368d425d4e2d59fd65dd71bd7dad899d959da19dad9",
+                       INIT_24 = x"c45cc50ec5c1c674c728c7dcc891c947c9fdcab3cb6acc22ccdacd93ce4ccf05",
+                       INIT_25 = x"b98cba34baddbb86bc30bcdbbd87be33bee0bf8dc03bc0eac199c249c2f9c3aa",
+                       INIT_26 = x"af69b006b0a3b141b1e0b280b320b3c1b463b506b5a9b64db6f2b797b83db8e4",
+                       INIT_27 = x"a60da69da72da7bea850a8e3a977aa0caaa1ab37abceac66acffad98ae32aecd",
+                       INIT_28 = x"9d8f9e109e929f159f99a01ea0a3a12aa1b1a23aa2c3a34da3d8a464a4f1a57f",
+                       INIT_29 = x"9604967596e7975b97cf984498bb993299aa9a239a9e9b199b959c129c909d0f",
+                       INIT_2a = x"8f7e8fde904090a29106916b91d09237929f9308937293dd944994b695259594",
+                       INIT_2b = x"8a0d8a5c8aac8afd8b4f8ba28bf68c4b8ca28cfa8d528dac8e078e638ec08f1f",
+                       INIT_2c = x"85bf85fc8639867786b786f8873a877d87c18807884d889588de8928897389c0",
+                       INIT_2d = x"829f82c882f2831e834a837883a783d78409843c846f84a484db8512854b8584",
+                       INIT_2e = x"80b480c980e080f88111812c81478164818281a281c281e48207822b82508277",
+                       INIT_2f = x"800280048007800b80118018802080298033803f804c805a806a807a808c809f",
+                       INIT_30 = x"808c807a806a805a804c803f80338029802080188011800b8007800480028001",
+                       INIT_31 = x"8250822b820781e481c281a2818281648147812c811180f880e080c980b4809f",
+                       INIT_32 = x"854b851284db84a4846f843c840983d783a78378834a831e82f282c8829f8277",
+                       INIT_33 = x"8973892888de8895884d880787c1877d873a86f886b78677863985fc85bf8584",
+                       INIT_34 = x"8ec08e638e078dac8d528cfa8ca28c4b8bf68ba28b4f8afd8aac8a5c8a0d89c0",
+                       INIT_35 = x"952594b6944993dd93729308929f923791d0916b910690a290408fde8f7e8f1f",
+                       INIT_36 = x"9c909c129b959b199a9e9a2399aa993298bb984497cf975b96e7967596049594",
+                       INIT_37 = x"a4f1a464a3d8a34da2c3a23aa1b1a12aa0a3a01e9f999f159e929e109d8f9d0f",
+                       INIT_38 = x"ae32ad98acffac66abceab37aaa1aa0ca977a8e3a850a7bea72da69da60da57f",
+                       INIT_39 = x"b83db797b6f2b64db5a9b506b463b3c1b320b280b1e0b141b0a3b006af69aecd",
+                       INIT_3a = x"c2f9c249c199c0eac03bbf8dbee0be33bd87bcdbbc30bb86baddba34b98cb8e4",
+                       INIT_3b = x"ce4ccd93ccdacc22cb6acab3c9fdc947c891c7dcc728c674c5c1c50ec45cc3aa",
+                       INIT_3c = x"da19d959d899d7dad71bd65dd59fd4e2d425d368d2acd1f0d134d07acfbfcf05",
+                       INIT_3d = x"e643e57ee4bae3f5e331e26de1aae0e7e024df61de9fdddddd1cdc5adb99dad9",
+                       INIT_3e = x"f2adf1e5f11df055ef8eeec7ee00ed39ec72ebabeae5ea1fe959e893e7cde708",
+                       INIT_3f = x"ff37fe6efda5fcdcfc13fb4afa82f9b9f8f0f827f75ff696f5cef505f43df375"
+               )
+               port map (
+                       WE   => '0',
+                       EN   => en,
+                       SSR  => rst_i,
+                       CLK  => clk_i,
+                       ADDR => adr_i,
+                       DI   => (others => '0'),
+                       DIP  => (others => '0'),
+                       DOP  => open,
+                       DO   => dat_o
+               );
+
+end behavioral;
diff --git a/projects/mp3tape/nexys2.vhd b/projects/mp3tape/nexys2.vhd
new file mode 100644 (file)
index 0000000..09eb77f
--- /dev/null
@@ -0,0 +1,237 @@
+library ieee;
+use ieee.std_logic_1164.all;
+
+library utility;
+library dsp;
+library nexys2_lib;
+
+
+entity nexys2 is
+       port (
+               clk_50:            in    std_logic;
+
+               EppDB_DstmDB:      inout std_logic_vector(7 downto 0);
+               EppWRITE:          in    std_logic;
+               EppASTB_DstmFLAGA: in    std_logic;
+               EppDSTB_DstmFLAGB: in    std_logic;
+               EppWAIT_DstmSLRD:  out   std_logic;
+               DstmIFCLK:         in    std_logic;
+               DstmSLCS:          in    std_logic;
+               DstmADR:           out   std_logic_vector(1 downto 0);
+               DstmSLWR:          out   std_logic;
+               DstmSLOE:          out   std_logic;
+               DstmPKTEND:        out   std_logic;
+               UsbMode:           in    std_logic;
+               UsbRdy:            in    std_logic;
+
+               seg:               out   std_logic_vector(6 downto 0);
+               dp:                out   std_logic;
+               an:                out   std_logic_vector(3 downto 0);
+               Led:               out   std_logic_vector(7 downto 0);
+               sw:                in    std_logic_vector(7 downto 0);
+
+               JA:                inout std_logic_vector(7 downto 0)
+       );
+end nexys2;
+
+
+architecture behavioral of nexys2 is
+
+       signal rst_50:          std_logic;  -- Reset within clk_50 domain
+
+       -- USB interfaces
+       signal wb_cyc:          std_logic;
+       signal wb_stb:          std_logic;
+       signal wb_we:           std_logic;
+       signal wb_ack:          std_logic;
+       signal wb_adr:          std_logic_vector(7 downto 0);
+       signal wb_miso:         std_logic_vector(7 downto 0);
+       signal wb_mosi:         std_logic_vector(7 downto 0);
+
+       signal dl_stb:          std_logic;
+       signal dl_rdy:          std_logic;
+       signal dl_dat:          std_logic_vector(7 downto 0);
+       signal ul_stb:          std_logic;
+       signal ul_rdy:          std_logic;
+       signal ul_dat:          std_logic_vector(7 downto 0);
+
+       -- I2S interface
+       signal i2s_rx_l_dat:    std_logic_vector(23 downto 0);
+       signal i2s_rx_r_dat:    std_logic_vector(23 downto 0);
+       signal i2s_tx_l_dat:    std_logic_vector(23 downto 0);
+       signal i2s_tx_r_dat:    std_logic_vector(23 downto 0);
+
+       -- Control signals
+       signal debug_to_host:   std_logic_vector(63 downto 0);
+       signal debug_from_host: std_logic_vector(63 downto 0);
+
+       -- Audio pipelines
+       signal audio_dl_stb:    std_logic;
+       signal audio_dl_rdy:    std_logic;
+       signal audio_dl_dat:    std_logic_vector(31 downto 0);
+
+       signal audio_ul_stb:    std_logic;
+       signal audio_ul_rdy:    std_logic;
+       signal audio_ul_dat:    std_logic_vector(31 downto 0);
+
+       signal audio_i_stb:     std_logic;
+       signal audio_i_rdy:     std_logic;
+       signal audio_i_dat:     std_logic_vector(31 downto 0);
+
+       signal audio_o_stb:     std_logic;
+       signal audio_o_rdy:     std_logic;
+       signal audio_o_dat:     std_logic_vector(31 downto 0);
+
+begin
+
+       seg <= (others => '1');
+       dp  <= '1';
+       an  <= (others => '1');
+       Led <= (others => '0');
+
+       ----------------------------------------------------------------------------
+       -- Processing pipeline
+
+       e_effect: entity dsp.pcm16_2ch_tapeeffect
+               port map (
+                       rst_i      => rst_50,
+                       clk_i      => clk_50,
+
+                       en_rolloff => sw(0),
+                       en_flutter => sw(1),
+                       en_wow     => sw(2),
+                       en_noise   => sw(3),
+
+                       stb_i      => audio_dl_stb,
+                       rdy_o      => audio_dl_rdy,
+                       dat_i      => audio_dl_dat,
+
+                       stb_o      => audio_o_stb,
+                       rdy_i      => audio_o_rdy,
+                       dat_o      => audio_o_dat
+               );
+
+       audio_ul_stb <= audio_i_stb;
+       audio_ul_rdy <= audio_i_rdy;
+       audio_ul_dat <= audio_i_dat;
+
+
+       ----------------------------------------------------------------------------
+       -- USB interfaces
+
+       e_por_clk50: entity utility.power_on_reset_opt
+               port map (
+                       rst_i => '0',
+                       clk_i => clk_50,
+                       rst_o => rst_50
+               );
+
+       e_usb: entity nexys2_lib.usb
+               port map (
+                       rst_i             => rst_50,
+
+                       EppDB_DstmDB      => EppDB_DstmDB,
+                       EppWRITE          => EppWRITE,
+                       EppASTB_DstmFLAGA => EppASTB_DstmFLAGA,
+                       EppDSTB_DstmFLAGB => EppDSTB_DstmFLAGB,
+                       EppWAIT_DstmSLRD  => EppWAIT_DstmSLRD,
+                       DstmIFCLK         => DstmIFCLK,
+                       DstmSLCS          => DstmSLCS,
+                       DstmADR           => DstmADR,
+                       DstmSLWR          => DstmSLWR,
+                       DstmSLOE          => DstmSLOE,
+                       DstmPKTEND        => DstmPKTEND,
+                       UsbMode           => UsbMode,
+                       UsbRdy            => UsbRdy,
+
+                       epp_clk_i         => clk_50,
+                       epp_cyc_o         => wb_cyc,
+                       epp_stb_o         => wb_stb,
+                       epp_we_o          => wb_we,
+                       epp_ack_i         => wb_ack,
+                       epp_adr_o         => wb_adr,
+                       epp_dat_i         => wb_miso,
+                       epp_dat_o         => wb_mosi,
+
+                       stm_clk_i         => clk_50,
+                       stm_dl_stb_o      => dl_stb,
+                       stm_dl_ack_i      => dl_rdy,
+                       stm_dl_dat_o      => dl_dat,
+                       stm_ul_stb_i      => ul_stb,
+                       stm_ul_ack_o      => ul_rdy,
+                       stm_ul_dat_i      => ul_dat
+               );
+
+       e_debug: entity utility.wb_debug
+               port map (
+                       rst_i   => rst_50,
+                       clk_i   => clk_50,
+
+                       cyc_i   => wb_cyc,
+                       stb_i   => wb_stb,
+                       we_i    => wb_we,
+                       ack_o   => wb_ack,
+                       adr_i   => wb_adr(2 downto 0),
+                       dat_i   => wb_mosi,
+                       dat_o   => wb_miso,
+
+                       debug_i => debug_to_host,
+                       debug_o => debug_from_host
+               );
+
+       e_deser: entity dsp.deserialize
+               generic map (WIDTH => 8, N => 4)
+               port map (
+                       rst_i => rst_50,
+                       clk_i => clk_50,
+
+                       stb_i => dl_stb,
+                       rdy_o => dl_rdy,
+                       dat_i => dl_dat,
+
+                       stb_o => audio_dl_stb,
+                       rdy_i => audio_dl_rdy,
+                       dat_o => audio_dl_dat
+               );
+
+       e_ser: entity dsp.serialize
+               generic map (WIDTH => 8, N => 4)
+               port map (
+                       rst_i => rst_50,
+                       clk_i => clk_50,
+
+                       stb_i => audio_ul_stb,
+                       rdy_o => audio_ul_rdy,
+                       dat_i => audio_ul_dat,
+
+                       stb_o => ul_stb,
+                       rdy_i => ul_rdy,
+                       dat_o => ul_dat
+               );
+
+
+       ----------------------------------------------------------------------------
+       -- I2S interface
+
+       e_i2s_pmod: entity dsp.i2s_pmod
+               port map (
+                       rst_50  => rst_50,
+                       clk_50  => clk_50,
+
+                       stb_i   => audio_o_stb,
+                       rdy_o   => audio_o_rdy,
+                       l_dat_i => i2s_tx_l_dat,
+                       r_dat_i => i2s_tx_r_dat,
+
+                       stb_o   => audio_i_stb,
+                       rdy_i   => audio_i_rdy,
+                       l_dat_o => i2s_rx_l_dat,
+                       r_dat_o => i2s_rx_r_dat,
+
+                       pmod    => JA
+               );
+       i2s_tx_r_dat <= audio_o_dat(31 downto 16) & x"00";
+       i2s_tx_l_dat <= audio_o_dat(15 downto  0) & x"00";
+       audio_i_dat <= i2s_rx_r_dat(23 downto  8) & i2s_rx_l_dat(23 downto 8);
+
+end behavioral;
diff --git a/projects/mp3tape/test_tapeeffect.vhd b/projects/mp3tape/test_tapeeffect.vhd
new file mode 100644 (file)
index 0000000..072d662
--- /dev/null
@@ -0,0 +1,72 @@
+library ieee;
+use ieee.std_logic_1164.all;
+
+library dsp;
+
+
+entity test_tapeeffect is
+end test_tapeeffect;
+
+
+architecture behavior of test_tapeeffect is
+
+       constant CLK_I_PERIOD: time := 20 ns;
+
+       signal rst_i: std_logic;
+       signal clk_i: std_logic;
+
+       signal stb_i: std_logic;
+       signal rdy_o: std_logic;
+       signal dat_i: std_logic_vector(31 downto 0);
+
+       signal stb_o: std_logic;
+       signal rdy_i: std_logic;
+       signal dat_o: std_logic_vector(31 downto 0);
+
+begin
+
+       p_test: process
+       begin
+               -- Initial values
+               stb_i <= '0';
+               rdy_i <= '1';
+
+               -- Reset
+               rst_i <= '1';
+               wait for CLK_I_PERIOD*17;
+               rst_i <= '0';
+
+               -- Test
+
+               -- Done
+               wait;
+       end process;
+
+       e_uut: entity dsp.pcm16_2ch_tapeeffect
+               port map (
+                       rst_i      => rst_i,
+                       clk_i      => clk_i,
+
+                       en_rolloff => '0',
+                       en_flutter => '0',
+                       en_wow     => '0',
+                       en_noise   => '1',
+
+                       stb_i      => stb_i,
+                       rdy_o      => rdy_o,
+                       dat_i      => dat_i,
+
+                       stb_o      => stb_o,
+                       rdy_i      => rdy_i,
+                       dat_o      => dat_o
+               );
+
+       p_clk: process
+       begin
+               clk_i <= '0';
+               wait for CLK_I_PERIOD/2;
+               clk_i <= '1';
+               wait for CLK_I_PERIOD/2;
+       end process;
+
+end;
diff --git a/projects/mp3tape/tests/test_nexys2.vhd b/projects/mp3tape/tests/test_nexys2.vhd
new file mode 100644 (file)
index 0000000..6e28142
--- /dev/null
@@ -0,0 +1,104 @@
+library ieee;
+use ieee.std_logic_1164.all;
+
+library simulated;
+library work;
+
+
+entity test_nexys2 is
+end test_nexys2;
+
+
+architecture behavior of test_nexys2 is
+
+       constant CLK_50_PERIOD: time := 20 ns;
+
+       signal clk_50:        std_logic;
+
+       signal ifclk:         std_logic;
+       signal slcs:          std_logic;
+       signal flaga:         std_logic;
+       signal flagb:         std_logic;
+       signal slrd:          std_logic;
+       signal slwr:          std_logic;
+       signal sloe:          std_logic;
+       signal pktend:        std_logic;
+       signal fifoadr:       std_logic_vector(1 downto 0);
+       signal db:            std_logic_vector(7 downto 0);
+
+       signal host_dl_data:  std_logic_vector(7 downto 0);
+       signal host_ul_count: integer;
+
+       signal sw:            std_logic_vector(7 downto 0);
+       signal JA:            std_logic_vector(7 downto 0);
+
+begin
+
+       p_test: process
+       begin
+               sw(7 downto 0) <= (others => '0');
+               sw(3) <= '1';  -- Enable noise
+               wait;
+       end process;
+
+
+       e_uut: entity work.nexys2
+               port map (
+                       clk_50            => clk_50,
+                       EppDB_DstmDB      => db,
+                       EppWRITE          => '1',
+                       EppASTB_DstmFLAGA => flaga,
+                       EppDSTB_DstmFLAGB => flagb,
+                       EppWAIT_DstmSLRD  => slrd,
+                       DstmIFCLK         => ifclk,
+                       DstmSLCS          => slcs,
+                       DstmADR           => fifoadr,
+                       DstmSLWR          => slwr,
+                       DstmSLOE          => sloe,
+                       DstmPKTEND        => pktend,
+                       UsbMode           => '0',
+                       UsbRdy            => '1',
+                       seg               => open,
+                       dp                => open,
+                       an                => open,
+                       Led               => open,
+                       sw                => sw,
+                       JA                => JA
+               );
+
+
+       e_stm: entity simulated.stmhost
+               port map (
+                       ifclk         => ifclk,
+                       slcs          => slcs,
+                       flaga         => flaga,
+                       flagb         => flagb,
+                       slrd          => slrd,
+                       slwr          => slwr,
+                       sloe          => sloe,
+                       pktend        => pktend,
+                       fifoadr       => fifoadr,
+                       db            => db,
+
+                       host_dl_data  => host_dl_data,
+                       host_ul_count => host_ul_count
+               );
+
+
+       e_i2s: entity simulated.proto_i2s_tx
+               port map (
+                       sck => JA(6),
+                       ws  => JA(5),
+                       sd  => JA(7)
+               );
+
+
+       p_clk_50: process
+       begin
+               clk_50 <= '0';
+               wait for CLK_50_PERIOD/2;
+               clk_50 <= '1';
+               wait for CLK_50_PERIOD/2;
+       end process;
+
+end;
diff --git a/projects/mp3tape/tools/fft.py b/projects/mp3tape/tools/fft.py
new file mode 100755 (executable)
index 0000000..495d867
--- /dev/null
@@ -0,0 +1,253 @@
+#!/usr/bin/env python3
+
+from __future__ import annotations
+from typing import *
+
+import math
+from numpy.fft import fft, ifft
+
+# ------------------------------------------------------------------------------
+
+
+class C(object):
+    def __init__(self, r: float, i: float):
+        self.r, self.i = r, i
+
+    def __add__(self, other: C):
+        if isinstance(other, int) or isinstance(other, float):
+            other = C(other, 0)
+        return C(self.r+other.r, self.i+other.i)
+
+    def __neg__(self):
+        return C(-self.r, -self.i)
+
+    def __sub__(self, other):
+        if isinstance(other, int) or isinstance(other, float):
+            other = C(other, 0)
+        return C(self.r-other.r, self.i-other.i)
+
+    def __mul__(self, other):
+        if isinstance(other, int) or isinstance(other, float):
+            other = C(other, 0)
+        return C(self.r*other.r - self.i*other.i, self.r*other.i + self.i*other.r)
+
+    def magnitude(self):
+        return math.sqrt(self.r*self.r + self.i*self.i)
+
+    def __str__(self):
+        return f'{self.r:7.3f} + {self.i:7.3f}i'
+
+    def __repr__(self):
+        return str(self)
+
+    @staticmethod
+    def nth_root_of_unity(n: float):
+        # Will have one period within N samples
+        phi = 2*math.pi/n
+        return C(math.cos(phi), math.sin(phi))
+
+
+# ------------------------------------------------------------------------------
+# Examples from:
+# https://www.youtube.com/watch?v=h7apO7q16V0
+
+
+def mul_poly(poly_coef_a: List[int], poly_coef_b: List[int]) -> List[int]:
+    """O(N^2) algorithm to multiply polynomials in coefficient form (functionally, a convolution)"""
+    poly_coef_c = [0] * (len(poly_coef_a)+len(poly_coef_b)-1)
+    for i, coef_a in enumerate(poly_coef_a):
+        for j, coef_b in enumerate(poly_coef_b):
+            poly_coef_c[i+j] += coef_a*coef_b
+    return poly_coef_c
+
+
+def eval_poly(coef: List[C], x: C) -> C:
+    xn = C(1, 0)
+    p = C(0, 0)
+    for c in coef:
+        p += c * xn
+        xn *= x
+    return p
+
+
+def dft_naive(coef: List[C]) -> List[C]:
+    out = list()
+    n = len(coef)
+    n2 = int(n/2)
+    for k in range(n):  # Number of periods
+        e = C(0, 0)  # Evaluated polynomial at x = nru
+        for i in range(n):  # Sample index
+            phi = -2*math.pi*k*i/n
+
+            # principal-nth-root-of-unity-with-k-periods-within-n-samples to the power of i
+            # k/n-th root of unity?
+            im = math.sin(phi)
+            re = math.cos(phi)
+            nru = C(re, im)  # principal-nth-root-of-unity to the power of i
+            e += coef[i] * nru
+        out.append(e)
+    return out
+
+
+def fft_recursive(coef: List[C]) -> List[C]:
+    n = len(coef)
+    if n <= 1:
+        return coef
+    n2 = int(n/2)
+
+    coef_even = coef[0::2]
+    coef_odd  = coef[1::2]
+
+    even = fft_recursive(coef_even)
+    odd  = fft_recursive(coef_odd)
+
+    out = [C(0,0)]*n
+    for k in range(n2):
+        phi = -2*math.pi*k/n
+        re = math.cos(phi)
+        im = math.sin(phi)
+        w = C(re, im)
+
+        out[k]    = even[k] + w*odd[k]
+        out[k+n2] = even[k] - w*odd[k]
+
+    return out
+
+
+def ifft_recursive(coef: List[C]) -> List[C]:
+    n = len(coef)
+    if n <= 1:
+        return coef
+    n2 = int(n/2)
+
+    coef_even = coef[0::2]
+    coef_odd  = coef[1::2]
+
+    even = ifft_recursive(coef_even)
+    odd  = ifft_recursive(coef_odd)
+
+    out = [C(0,0)]*n
+    for k in range(n2):
+        phi = 2*math.pi*k/n
+        re = math.cos(phi)
+        im = math.sin(phi)
+        w = C(re, im)
+
+        out[k]    = (even[k] + w*odd[k]) * 0.5
+        out[k+n2] = (even[k] - w*odd[k]) * 0.5
+
+    return out
+
+
+def reorder(c: List[Any]) -> List[Any]:
+    if len(c) <= 1:
+        return c
+    return reorder(c[0::2]) + reorder(c[1::2])
+
+
+def fft_iter(coef: List[C]) -> List[Any]:
+    coef = reorder(coef)  # Pretend this isn't recursive, or that we did it ahead of time
+    # On an FPGA, the reordering can be done by reversing the bits of the index while storing into a buffer
+    n = len(coef)
+
+    phis = [-2*math.pi*k/n for k in range(n)]
+    res = [math.cos(phi) for phi in phis]
+    ims = [math.sin(phi) for phi in phis]
+    ws = [C(re, im) for re, im in zip(res, ims)]
+
+    size = 2
+    while size <= n:  # Each stage in the butterfly
+        s2 = int(size/2)
+        num_groups = n / size
+        for i in range(0, n, size):  # Each group in the butterfly
+            for k in range(s2):  # Samples within each group of the butterfly
+                a = coef[i+k]
+                b = coef[i+k+s2] * ws[int(k*num_groups)]
+                coef[i+k]    = a + b
+                coef[i+k+s2] = a - b
+        size *= 2
+    return coef
+
+
+def ifft_iter(coef: List[C]) -> List[Any]:
+    coef = reorder(coef)  # Pretend this isn't recursive, or that we did it ahead of time
+    # On an FPGA, the reordering can be done by reversing the bits of the index while storing into a buffer
+    n = len(coef)
+
+    phis = [2*math.pi*k/n for k in range(n)]
+    res = [math.cos(phi) for phi in phis]
+    ims = [math.sin(phi) for phi in phis]
+    ws = [C(re, im) for re, im in zip(res, ims)]
+
+    size = 2
+    while size <= n:  # Each stage in the butterfly
+        s2 = int(size/2)
+        num_groups = n / size
+        for i in range(0, n, size):  # Each group in the butterfly
+            for k in range(s2):  # Samples within each group of the butterfly
+                a = coef[i+k]
+                b = coef[i+k+s2] * ws[int(k*num_groups)]
+                coef[i+k]    = (a + b) * 0.5  # Multiplying by 1/2 a total of log2(N) times turns into multiplying by 1/N when N = 2^x
+                coef[i+k+s2] = (a - b) * 0.5
+        size *= 2
+    return coef
+
+
+# ------------------------------------------------------------------------------
+
+
+def main() -> int:
+    # A(x) = x^2 + 3x + 2, B(x) = 2x^2 + 1
+    # print(mul_poly([2, 3, 1], [1, 0, 2]))
+
+    n = 8
+
+    a = [math.cos(i*2*math.pi/n) for i in range(n)]
+    b = fft(a)
+    b = [C(i.real, i.imag) for i in b]
+    # # c = ifft(b)
+    #
+    #a2 = [C(a[i],0) for i in range(n)]
+    #a2 = [C(i,0) for i in range(n)]
+    a2 = [C(i,0) for i in [6, 2, 8, 4, 2, 8, 3, 6]]
+    b1 = dft_naive(a2)
+    b0 = fft_recursive(a2)
+    c0 = ifft_recursive(b0)
+
+    fft_iter(a2)
+
+    for i in a2:
+        print(i)
+    print()
+    print('My FFT:')
+    for i in b0:
+        print(i)
+    print()
+    print('My naive DFT:')
+    for i in b1:
+        print(i)
+    print()
+    print('"real" FFT:')
+    for i in b:
+        print(i)
+    print()
+    print('My iFFT:')
+    for i in c0:
+        print(i)
+
+    print()
+    print('Iterative FFT:')
+    b2 = fft_iter(a2)
+    for i in b2:
+        print(i)
+    print()
+    print('Iterative iFFT:')
+    c2 = ifft_iter(b2)
+    for i in c2:
+        print(i)
+
+    return 0
+
+
+if __name__ == '__main__':
+    exit(main())
diff --git a/projects/mp3tape/tools/jankman.py b/projects/mp3tape/tools/jankman.py
new file mode 100755 (executable)
index 0000000..c1129cc
--- /dev/null
@@ -0,0 +1,190 @@
+#!/usr/bin/env python3
+
+from __future__ import annotations
+from typing import *
+
+import argparse
+import struct
+
+import fft
+
+# ------------------------------------------------------------------------------
+
+
+def f_expect(f: BinaryIO, s: bytes):
+    d = f.read(len(s))
+    assert d == s
+
+
+def f_skipto(f: BinaryIO, s: bytes):
+    d = f.read(len(s))
+    while d != s:
+        d = d[1:] + f.read(1)
+
+
+def f_chomp_u32_le(f: BinaryIO):
+    return struct.unpack('<I', f.read(4))[0]
+
+
+def f_chomp_u16_le(f: BinaryIO):
+    return struct.unpack('<H', f.read(2))[0]
+
+
+def f_chomp_s16_le(f: BinaryIO):
+    return struct.unpack('<h', f.read(2))[0]
+
+
+def f_write_u32_le(f: BinaryIO, v: int):
+    f.write(struct.pack('<I', v))
+
+
+def f_write_s16_le(f: BinaryIO, v: int):
+    # Saturate, just in case
+    if v < -32768:
+        v = -32768
+    if v > 32767:
+        v = 32767
+    f.write(struct.pack('<h', v))
+
+
+def f_write_u16_le(f: BinaryIO, v: int):
+    f.write(struct.pack('<H', v&0xffff))
+
+
+# ------------------------------------------------------------------------------
+
+
+def load_wav(f: BinaryIO):
+    samples = list()
+
+    f_expect(f, b'RIFF')
+    file_len = f_chomp_u32_le(f)
+    f_expect(f, b'WAVE')
+
+    f_expect(f, b'fmt ')
+    fmt_len = f_chomp_u32_le(f)
+    audio_format = f_chomp_u16_le(f)
+    num_chans = f_chomp_u16_le(f)
+    rate = f_chomp_u32_le(f)
+    bytes_per_sec = f_chomp_u32_le(f)
+    bytes_per_block = f_chomp_u16_le(f)
+    bits_per_sample = f_chomp_u16_le(f)
+
+    f_skipto(f, b'data')  # Some WAV files have more sections, so skip over those
+    data_len = f_chomp_u32_le(f)
+
+    assert fmt_len == 16
+    assert audio_format == 1
+    assert num_chans == 2
+    assert rate == 44100
+    assert bits_per_sample == 16
+    assert bytes_per_block == bits_per_sample * num_chans / 8
+    assert bytes_per_sec == bytes_per_block * rate
+
+    for i in range(int(data_len / bytes_per_block)):
+        samp_l = f_chomp_s16_le(f)
+        samp_r = f_chomp_s16_le(f)
+        samp = (samp_l + samp_r) / 2
+        samples.append(samp)
+
+    return samples
+
+
+def compress_chunk(c: List[fft.C], size: int, scale: float = 200.0):
+    n = len(c)
+    n2 = int(n/2)
+    x0 = fft.fft_iter(c)
+    x1 = [int(x.magnitude()/scale) for x in x0[0:n2+1]]
+    x2 = [(i, p) for i, p in enumerate(x1)]
+    x2.sort(key=lambda x: x[1], reverse=True)
+    x3 = x2[:20]  # How bad will it sound with only 20 bins?  Let's find out!
+    return x3
+
+
+def do_jankify(f: BinaryIO, g: BinaryIO):
+    print('Loading...')
+    samples = load_wav(f)
+    print('Jankifying...')
+    f_write_u32_le(g, int(len(samples)/1024))
+    for i in range(0, len(samples), 1024):
+        print(i, len(samples))  # Obnoxious, but less obnoxious than not knowing
+        block = [fft.C(s,0) for s in samples[i:i+1024]]
+        if len(block) < 1024:
+            block += [0.0]*(1024-len(block))
+        x = compress_chunk(block, 0)
+        for i, p in x:
+            f_write_u16_le(g, i)
+            f_write_u16_le(g, p)
+
+
+def do_unjank(f: BinaryIO, g: BinaryIO):
+    scale = 200
+    samples = list()
+    try:
+        num_blocks = f_chomp_u32_le(f)
+        for i in range(num_blocks):
+            print(i, num_blocks)  # Obnoxious, but less obnoxious than not knowing
+            block = [fft.C(0,0)]*1024
+            for i in range(20):
+                i = f_chomp_u16_le(f)
+                p = f_chomp_u16_le(f)
+                block[i] = fft.C(p*scale,0)
+            c0 = fft.ifft_iter(block)
+            c1 = [int(c.r) for c in c0]
+            samples.extend(c1)
+    except Exception:  # This is fine.  Listen, don't worry about it, okay?
+        pass
+
+    len_data = len(samples) * 2
+    len_file = len_data + 44
+
+    g.write(b'RIFF')
+    f_write_u32_le(g, len_file)
+    g.write(b'WAVE')
+
+    g.write(b'fmt ')
+    f_write_u32_le(g, 16)
+    f_write_u16_le(g, 1)
+    f_write_u16_le(g, 1)
+    f_write_u32_le(g, 44100)
+    f_write_u32_le(g, 44100*2)
+    f_write_u16_le(g, 2)
+    f_write_u16_le(g, 16)
+
+    g.write(b'data')
+    f_write_u32_le(g, len_data)
+    for sample in samples:
+        f_write_s16_le(g, sample)
+
+
+# ------------------------------------------------------------------------------
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser(description='Jankman pseudo-mp3 encoder/decoder')
+    subparsers = parser.add_subparsers(help='Operation', required=True, dest='cmd')
+    jankify = subparsers.add_parser('jankify', help='Convert WAV file to pseudo-mp3')
+    unjank = subparsers.add_parser('unjank', help='Convert pseudo-mp3 file to WAV')
+    jankify.add_argument('--input_file', '-i', required=True, help='Input filename')
+    jankify.add_argument('--output_file', '-o', required=True, help='Output filename')
+    unjank.add_argument('--input_file', '-i', required=True, help='Input filename')
+    unjank.add_argument('--output_file', '-o', required=True, help='Output filename')
+
+    args = parser.parse_args()
+    if args.cmd == 'jankify':
+        with open(args.input_file, 'rb') as f:
+            with open(args.output_file, 'wb') as g:
+                do_jankify(f, g)
+    elif args.cmd == 'unjank':
+        with open(args.input_file, 'rb') as f:
+            with open(args.output_file, 'wb') as g:
+                do_unjank(f, g)
+    else:
+        parser.print_usage()
+        return 1
+
+    return 0
+
+
+if __name__ == '__main__':
+    exit(main())