From 9255d8d7e18c5d6dae75972fa0f2ad9d7fb54650 Mon Sep 17 00:00:00 2001 From: Kizarm Date: Sat, 9 Mar 2024 11:10:52 +0100 Subject: [PATCH] add midi --- .gitignore | 5 + README.md | 8 + ch32v003/pcmdma.cpp | 117 +++++ common/pcmdma.h | 28 ++ midi/Makefile | 66 +++ midi/audio.h | 16 + midi/ch32v003 | 1 + midi/common | 1 + midi/main.cpp | 17 + midi/mid/joy.mid | Bin 0 -> 4810 bytes midi/mid/ukoleb.mid | Bin 0 -> 1860 bytes midi/midiplayer.cpp | 108 +++++ midi/midiplayer.h | 31 ++ midi/pwmconfig.h | 7 + midi/stm32f051 | 1 + midi/ton/gen.cpp | 76 +++ midi/ton/miditonesV1.6.c | 999 +++++++++++++++++++++++++++++++++++++++ midi/tone.cpp | 70 +++ midi/tone.h | 26 + stm32f051/pcmdma.cpp | 88 ++++ 20 files changed, 1665 insertions(+) create mode 100644 ch32v003/pcmdma.cpp create mode 100644 common/pcmdma.h create mode 100644 midi/Makefile create mode 100644 midi/audio.h create mode 120000 midi/ch32v003 create mode 120000 midi/common create mode 100644 midi/main.cpp create mode 100644 midi/mid/joy.mid create mode 100644 midi/mid/ukoleb.mid create mode 100644 midi/midiplayer.cpp create mode 100644 midi/midiplayer.h create mode 100644 midi/pwmconfig.h create mode 120000 midi/stm32f051 create mode 100644 midi/ton/gen.cpp create mode 100644 midi/ton/miditonesV1.6.c create mode 100644 midi/tone.cpp create mode 100644 midi/tone.h create mode 100644 stm32f051/pcmdma.cpp diff --git a/.gitignore b/.gitignore index c84eda2..9e20b96 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,8 @@ *.bin *.map *.elf +midi/melody.c +midi/miditone.c +midi/ton/gen +midi/ton/miditones + diff --git a/README.md b/README.md index fde73bb..238c43d 100644 --- a/README.md +++ b/README.md @@ -36,3 +36,11 @@ Tvar výpisů je tento:
A--3-B--34---- DP: --VV Z-V- -V-V ----
A1---B-------8 DP: VVVZ VZVV ZZZV -VZV
A1---B---45678 DP: ---Z -Z-V Z-Z- -VZV
+ +## midi +Funguje to podobně jako na STM32F051, jen to má omezení dané asi především +tím, že tohle nemá hardwarovou násobičku. Tedy jen 4 generátory. Na hračky +typu melodický zvonek, piánko atd. to stačí. Výhoda je, že piny vydrží 20mA, +je možné připojit 30 Ohm sluchátka (do série) plus sériový odpor 100 Ohm mezi +piny PD0 a PD2 a hraje to s dostatečnou hlasitostí a docela čistě. + diff --git a/ch32v003/pcmdma.cpp b/ch32v003/pcmdma.cpp new file mode 100644 index 0000000..982df9e --- /dev/null +++ b/ch32v003/pcmdma.cpp @@ -0,0 +1,117 @@ +#include "system.h" +#include "pcmdma.h" +#include "gpio.h" + +static PcmDma * pInstance = nullptr; +extern "C" void DMA1_Channel5_IRQHandler( void ) __attribute__((interrupt)); +void DMA1_Channel5_IRQHandler( void ) { + DMA1_Type::INTFR_DEF state (DMA1.INTFR); + DMA1.INTFCR.R = state.R; // clear all + if (!pInstance) return; + if (state.B.HTIF5 != RESET) pInstance->send(false); + else if (state.B.TCIF5 != RESET) pInstance->send(true); +} + +/* + * initialize TIM1 for PWM + */ +static inline void tim1pwm_init () noexcept { + // Enable GPIOD and TIM1 + RCC.APB2PCENR.modify([] (RCC_Type::APB2PCENR_DEF & r) -> auto { + r.B.IOPDEN = SET; + //r.B.IOPCEN = SET; + r.B.TIM1EN = SET; + return r.R; + }); + + // PD0 is T1CH1N, PD2 is T1CH1, 10MHz Output alt func, push-pull + GPIOD.CFGLR.modify([](GPIOA_Type::CFGLR_DEF & r) -> auto { + r.B.CNF0 = 2u; + r.B.MODE0 = 1u; + r.B.CNF2 = 2u; + r.B.MODE2 = 1u; + return r.R; + }); + /* Alternative + AFIO.PCFR.B.TIM1RM = 1u; + // PC3 is T1CH1N, PC6 is T1CH1, 10MHz Output alt func, push-pull + GPIOC.CFGLR.modify([](GPIOA_Type::CFGLR_DEF & r) -> auto { + r.B.CNF3 = 2u; + r.B.MODE3 = 1u; + r.B.CNF6 = 2u; + r.B.MODE6 = 1u; + return r.R; + }); + */ + // Reset TIM1 to init all regs + RCC.APB2PRSTR.B.TIM1RST = SET; + RCC.APB2PRSTR.B.TIM1RST = RESET; + // CTLR1: default is up, events generated, edge align + // SMCFGR: default clk input is CK_INT + // Prescaler + TIM1.PSC.R = 0u; + // Auto Reload - sets period + TIM1.ATRLR.R = MAXPWM - 1; + + TIM1.CCER.modify([](TIM1_Type::CCER_DEF & r) -> auto { + // Enable CH1N, CH1 output, positive pol + r.B.CC1NE = SET; + r.B.CC1E = SET; + /* + r.B.CC1NP = SET; // active Low + r.B.CC1P = SET; + */ + return r.R; + }); + // CH1 Mode is output, PWM1 (CC1S = 00, OC1M = 110) + TIM1.CHCTLR1_Output.modify([](TIM1_Type::CHCTLR1_Output_DEF & r) -> auto { + r.B.OC1M = 0x6u; + return r.R; + }); + // Enable TIM1 outputs + TIM1.BDTR.modify([](TIM1_Type::BDTR_DEF & r) -> auto { + r.B.MOE = SET; + //r.B.DTG = 48u; // Dead time 1us + return r.R; + }); + + // Reload immediately + Trigger DMA + TIM1.SWEVGR.B.UG = SET; + TIM1.DMAINTENR.B.UDE = SET; + // Enable TIM1 + TIM1.CTLR1.B.CEN = SET; +} +typedef __SIZE_TYPE__ size_t; +static inline void dma1ch5_init (void * ptr) noexcept { + // Enable DMA + RCC.AHBPCENR.modify([](RCC_Type::AHBPCENR_DEF & r) -> auto { + r.B.SRAMEN = SET; + r.B.DMA1EN = SET; + return r.R; + }); + // DMA5 can be configured to attach to T1UP + // The system can only DMA out at ~2.2MSPS. 2MHz is stable. + DMA1.CNTR5 .R = FULL_LEN; + DMA1.MADDR5.R = reinterpret_cast(ptr); + DMA1.PADDR5.R = reinterpret_cast(& TIM1.CH1CVR); + NVIC.EnableIRQ (DMA1_Channel5_IRQn); + DMA1.CFGR5.modify([](DMA1_Type::CFGR5_DEF & r) -> auto { + r.B.DIR = SET; // MEM2PERIPHERAL + r.B.PL = 3u; // Highest priority. + r.B.PSIZE = 1u; // 16-bit peripheral + r.B.MSIZE = 1u; // 16-bit memory + r.B.MINC = SET; // Increase memory. + r.B.CIRC = SET; // Circular mode. + r.B.HTIE = SET; // Half-trigger + r.B.TCIE = SET; // Whole-trigger + // Enable DMA1 ch5 + r.B.EN = SET; + return r.R; + }); +} + +PcmDma::PcmDma() noexcept : pL(buffer), pH(buffer + HALF_LEN), src(nullptr) { + pInstance = this; + tim1pwm_init (); + dma1ch5_init (buffer); +} diff --git a/common/pcmdma.h b/common/pcmdma.h new file mode 100644 index 0000000..9bbc3d4 --- /dev/null +++ b/common/pcmdma.h @@ -0,0 +1,28 @@ +#ifndef PCMDMA_H +#define PCMDMA_H +#include "oneway.h" +#ifdef HAVE_CONFIG +/* Umožní použít externí parametry. */ +#include "pwmconfig.h" +#else +static constexpr unsigned HALF_LEN = 0x40u; +static constexpr unsigned MAXPWM = 2000u; +#endif +static constexpr unsigned FULL_LEN = 2u * HALF_LEN; +/* Používá TIM1, PWM kanál 1, DMA1 kanál 5, přerušení DMA1_Channel5_IRQHandler */ +class PcmDma { + uint16_t * const pL; + uint16_t * const pH; + uint16_t buffer [FULL_LEN]; + OneWay * src; + public: + explicit PcmDma () noexcept; + void attach (OneWay & s) { src = & s; } + void send (const bool b) { + if (!src) return; + if (b) src->Send (pH, HALF_LEN); + else src->Send (pL, HALF_LEN); + } +}; + +#endif // PCMDMA_H diff --git a/midi/Makefile b/midi/Makefile new file mode 100644 index 0000000..f7d0916 --- /dev/null +++ b/midi/Makefile @@ -0,0 +1,66 @@ +# ch32v003 +TARGET?= ch32v003 +#TARGET?= stm32f051 +TOOL ?= gcc +#TOOL ?= clang + +PRJ = example + +VPATH = . ./$(TARGET) +BLD = ./build/ +DFLAGS = -d +LFLAGS = -g +LDLIBS = +BFLAGS = --strip-unneeded + +CFLAGS = -MMD -Wall -ggdb -fno-exceptions -ffunction-sections -fdata-sections +CFLAGS+= -I. -I./common -I./$(TARGET) -I/usr/include/newlib -DHAVE_CONFIG=1 +DEL = rm -f + +# zdrojaky +OBJS = main.o pcmdma.o +OBJS += tone.o midiplayer.o miditone.o melody.o + +include $(TARGET)/$(TOOL).mk +BOBJS = $(addprefix $(BLD),$(OBJS)) + +all: $(BLD) $(PRJ).elf +# ... atd. +-include $(BLD)*.d +# linker +$(PRJ).elf: $(BOBJS) + -@echo [LD $(TOOL),$(TARGET)] $@ + @$(LD) $(LFLAGS) -o $(PRJ).elf $(BOBJS) $(LDLIBS) + -@echo "size:" + @$(SIZE) $(PRJ).elf + -@echo "listing:" + $(DUMP) $(DFLAGS) $(PRJ).elf > $(PRJ).lst + -@echo "OK." + $(COPY) $(BFLAGS) -O binary $(PRJ).elf $(PRJ).bin +# preloz co je potreba +$(BLD)%.o: %.c + -@echo [CC $(TOOL),$(TARGET)] $@ + @$(CC) -c $(CFLAGS) $< -o $@ +$(BLD)%.o: %.cpp + -@echo [CX $(TOOL),$(TARGET)] $@ + @$(CXX) -std=c++17 -fno-rtti -c $(CFLAGS) $< -o $@ +$(BLD): + mkdir $(BLD) +flash: $(PRJ).elf + minichlink -w $(PRJ).bin flash -b + +miditone.c: ton/gen + ton/gen +ton/gen: ton/gen.cpp + g++ -Os ton/gen.cpp -o ton/gen +melody.c: ton/miditones + ton/miditones -d -s2 -t4 mid/ +ton/miditones: ton/miditonesV1.6.c + gcc -Os -Wno-pointer-sign -Wno-return-type ton/miditonesV1.6.c -o ton/miditones + +# vycisti +clean: + $(DEL) $(BLD)* *.lst *.bin *.elf *.map *~ miditone.c melody.c +distclean: clean + $(DEL) ton/gen ton/miditones +.PHONY: all clean distclean flash diff --git a/midi/audio.h b/midi/audio.h new file mode 100644 index 0000000..370c524 --- /dev/null +++ b/midi/audio.h @@ -0,0 +1,16 @@ +#ifndef AUDIO_H +#define AUDIO_H + +#include + +static constexpr int AudioSampleRate = 24000; +/// Počet generátorů. +static constexpr unsigned int maxGens = 4; +/// Kladné maximum vzorku. +static constexpr int maxValue = 30000; +/// Záporné maximum vzorku. +static constexpr int minValue = -maxValue; +/// +static constexpr unsigned int maxAmplt = (1U<<27); + +#endif // AUDIO_H diff --git a/midi/ch32v003 b/midi/ch32v003 new file mode 120000 index 0000000..0bcf9a1 --- /dev/null +++ b/midi/ch32v003 @@ -0,0 +1 @@ +../ch32v003/ \ No newline at end of file diff --git a/midi/common b/midi/common new file mode 120000 index 0000000..8332399 --- /dev/null +++ b/midi/common @@ -0,0 +1 @@ +../common/ \ No newline at end of file diff --git a/midi/main.cpp b/midi/main.cpp new file mode 100644 index 0000000..0f68daf --- /dev/null +++ b/midi/main.cpp @@ -0,0 +1,17 @@ +#include "midiplayer.h" +#include "pcmdma.h" + +static MidiPlayer player; +static PcmDma pcm; + +int main (void) { + pcm.attach (player); + for (;;) { + /* BUG: + * Do smyčky nejde přidat jakýkoli další kód. + * Proč, neumím vysvětlit, ale lze to izolovat. + * Je zajímavé, že ADC postavené podobně to nedělá. + * */ + } + return 0; +} diff --git a/midi/mid/joy.mid b/midi/mid/joy.mid new file mode 100644 index 0000000000000000000000000000000000000000..10d72aa7f6c1c62c3b5e3a92fe2b730df5eabff0 GIT binary patch literal 4810 zcmZuzO>1N46@DkVorofeC@N?nQU$coLd}Svg>tb$btLNpjV)JV&|(&HBNLfwW?YYk zalmkO@3oCU8)YZZpYS3JgA>TI&_9uV7ySWUxX*LWkvvnnvX9>Po^zh(e7w)|{&?h^ zd+MGz@5!&v`{#dy^>_F1$>)E5_uad<)7)2I7js{gN~L1%#pHN&`s(yN_u}~Ut$X<_u{_gq7ebySzN?d2K(+%-Vr>7=*RuQO_OtQDt@%xC35? z-ST5cyA@1v9cI*=9nuW*@aDnJ(;Mu{vSW5D@H#=5Tef?y?q_cMb+_E}y;(hovhBd_ z1z{0K-ku-z5t(+0pVs}zn{@(L55g*4BeOI!LndVnb(NjLwb&A>!|RA##^=cFrmOZu zVMbMhol$RbncfJZ?I&;)l@xZxU{?)x7B+B!B?LPPiUxZ^^Dwy3RHBZd_B1x2XfBS> z>V6Vvogv5Fy}$$i9vRv5V}qSE)RHz(Tb}B=9bBsCYYD7eCJ|&%7he)*&%Y@ug+FHv zbj?5))sD^1Sgp$}9#Y8=Bx>C>&`xdXl-vG+TRzr(z)z*lLO|Fw28vdUf-x}okO368 zrkRB)MID_P3y$|u>@x2Iw)7E-y~#YS{;mXf2W=;9G^V$@T5x`$&PQ6iIH@o?it;+I zD09rT?VsT;yC$J#G*ODi%NL{o)i?;e7eRX2?|Fy*)#a=oxWk~d?DgECKb`exUBbd) zJfUL-Y4{nOgo6f5z-jmeoYWf9_$)J8AZ(%sQ1;xiMZDn!)XpAnfA9-5`MrUeyl=GoH2>rGwM4INo7cLRG%QG5B_bOsbY` zL`2>DxBWJoKcOV!5{jxRG*OSxi7U=08+FJ}_k#ZnaGNc#X>yi`_IO)zSvVh*P``Kj z{ZNC>yDAkEAfJ5}Rb=|1MGr)3R5RdYV9D7@!t2k8ELu1wusihab0+he6|dVb?}4 zHK#Kohv@{M1fiEOD@`n;w$Jw8m)4M;gFU6)3C6_V@S_~a<>1PKsTtl{fR?WJ+^&C9 zWzbz*SNQBChwCM^H?x-@MxfYX^Wb>1a0?^dGx^9lTCLN?6B8>V#H6; zFeHa|hNyrhugoHE*qxxOb;C;5y;`w4)|4-q)Vd#cge#ei0@#YGo998QdYw`ROC-DQ zPv{F{DqW3*lU84kkI`?wz+?1#d5lg^@EUzHIX*vqeKK|bXt+B*NZs&9x6+1rIkznd zn{Ua$?Bq%rY|b^6!b%z_>dX~LdV61RPI;XjG>5s8;@0qGSQM5rIF;AKoK?-)(3~+D z3}osKT{A}kS4bpBPw0P11fEBao|#ja!|`E|Wn8pLj-K~}utQI<&*s`T2;}Yi-d%s+ zts1_EU!xQNaDH(UOCvGI!no@<+zQ=PnAap`d8OS{W-`0MrEx(-Y?q?ivDq2mCA#ul zyD77>O+MFG+o_%A*LT?DKo>inwSoaZ1=u{n;lXc`DA+~X09n|wys46oTg`||8Ifmm zeb0hG?dm5Zh^SVAeg6&A+BxvQu}Ub2ostlvH)V< zt@~9Z6cw?o(JkJ`Iji0BuTn;6ja6^?9mT(ic4J*Hh+BU04qI;3M!U3vsH0C+S{=tE z$IxepKB+aYSj}q+e@Zq`<{{&t(h57eV28A{pMbe(NlDdkJE53IR)>n#AwlwpNlV3!VQoZO%MxsCHcErfn{y8Nxig$qt}5Oh9F=n>oJ_V#3^7 zhBR{z)xL&mUwok2aU`l8k)aC05bseQl~8 zd;x*LhiZoqnDb~8Ii+u z0#Jg`%SN^Hl&XDUs+}29wbO=bCw{7SkSnU4$5id~P1TOdh-zn0sCK4|H2Vm^;VP-U znY{!t@-ed6t7y|k1zeZMNbMBn&Z7s;o@_d(cJ>-jjQA-UCTXZLLHjxU48K}`Y*A%5EK9a literal 0 HcmV?d00001 diff --git a/midi/mid/ukoleb.mid b/midi/mid/ukoleb.mid new file mode 100644 index 0000000000000000000000000000000000000000..98d079bddce0e0055d5fa01a2f93f09957bcca7f GIT binary patch literal 1860 zcmb7EO=}ZT6g|_{CZ(YYA`_HC5=h!gqh>l|m|=L$G~}bPgIEG8h`6X=v7i3S%a&b8Y9WGNmN5nwwr)E zhRYD9ihy7udbCRPd=t{uRm;kFgNz;a_~}UZ_~|I!^gy+PMbx5z=zNrjs} zN~RCmGnzEHwNXQT$dRlVRyKe-(VLiIxN%!gyOq*zDph-x{A;$D%H^Pl!VCp74lM`P z?T#rzdrweAed0`vA{s5^_-5L(MaWPBC0ol3xrl zZQ7hm$aH9W992YU)R$6(_fPdo5gsy}XK-_?3fj0LtbHM~S8qIksS35pppQIdYx$h#^5$*^2?iBH%Y=ql;XZDCTf^_<;J0 zC>>#U8PXMY$DsI*uD|MhH;Az`3 zZ7qXBkL9KhS6H4wsSBecU+HK*#HACCd6`2ox>-(W8@c>LBlig59E&yGVnM#62CP?! zKv#4dF%MJ%~vf{?-qSlAk>Hq8c@UE|Fl3OF!S}hfq8)aK> zj=qAHrQ7=UZ0pUTRzl2$bTfY&A8S;h$EhzOUV^uXaLF=*ddMU9V_`GlM2wFa^TI(m xm89Gs(C@ maxValue) res = maxValue; + if (res < minValue) res = minValue; + return (res); +} +/// Počítá další vzorek +short MidiPlayer::nextSample (void) { + if (pause) pause -= 1; // Časování tónu + else ToneChange(); // Nový tón - MidiPlayer::ToneChange + return genSample (); +} +static constexpr unsigned AudioMidiDelay = 24; + +static constexpr int INPUT_BIT_RANGE = 16; +static constexpr unsigned SIGMA_MASK = (1u << (INPUT_BIT_RANGE + 0)) - 1u; +static constexpr unsigned SIGNED_OFFEST = (1u << (INPUT_BIT_RANGE - 1)); + // Předpokládá se na vstupu signed int o šířce INPUT_BIT_RANGE + // přičemž 0 na vstupu odpovídá MAXPWM / 2 na výstupu. Vypadá to divně, ale funguje. +static unsigned pwm_sd (const int input) { + static unsigned sigma = 0; // podstatné je, že proměnná je statická + const unsigned sample = (input + SIGNED_OFFEST) * MAXPWM; + sigma &= SIGMA_MASK; // v podstatě se odečte hodnota PWM + sigma += sample; // integrace prostým součtem + return sigma >> INPUT_BIT_RANGE; +} + +/******************************************************************/ +/// Konstruktor +MidiPlayer::MidiPlayer() noexcept : OneWay() /*, but(GpioPortA, 2, GPIO_Mode_IN)*/ { + //but.setPuPd(GPIO_PuPd_UP); + index = 0; + pause = 0; + melody = scores[index++]; + running = true; +} +unsigned MidiPlayer::Send (uint16_t * const ptr, const unsigned len) { + // if (!but.get()) running = true; + + if (!running) { + for (unsigned n=0; n>= 4; + switch (cmd) { + case 0x8: // off + gens[geno].setMidiOff(); + break; + case 0x9: // on + midt = * melody++; + gens[geno].setMidiOn (midt); + break; + default: + stop(); + return; // melodie končí eventem 0xf0 + } + } else { // pause + midt = * melody++; + // Když to trochu uteče, zase se z toho nestřílí, tak to nechme být. + pause = ((unsigned int) cmd << 8) + midt; // v ms + pause *= AudioMidiDelay; // ale máme vzorkování cca 24 kHz + return; + } + } +} diff --git a/midi/midiplayer.h b/midi/midiplayer.h new file mode 100644 index 0000000..60a1efc --- /dev/null +++ b/midi/midiplayer.h @@ -0,0 +1,31 @@ +#ifndef DACPLAYER_H +#define DACPLAYER_H +#include "oneway.h" +#include "gpio.h" + +/// Třída, která hraje čistě na pozadí. +class MidiPlayer : public OneWay { + // Veřejné metody + public: + /// Konstruktor + explicit MidiPlayer () noexcept; + unsigned Send (uint16_t * const ptr, const unsigned len) override; + //bool send (uint16_t * ptr, const int len) override; + void stop (); + protected: + // Chráněné metody + /// Obsluha tónu + void ToneChange (void); + /// Obsluha vzorku + short nextSample (void); + private: + //GpioClass but; + volatile bool running; + unsigned char const * melody; + unsigned index; + volatile int pause; + +}; +extern "C" const unsigned char * const scores[]; + +#endif // DACPLAYER_H diff --git a/midi/pwmconfig.h b/midi/pwmconfig.h new file mode 100644 index 0000000..2ef2bb7 --- /dev/null +++ b/midi/pwmconfig.h @@ -0,0 +1,7 @@ +#ifndef CONFIG_H +#define CONFIG_H + +static constexpr unsigned HALF_LEN = 0x80u; +static constexpr unsigned MAXPWM = 2000u; + +#endif // CONFIG_H diff --git a/midi/stm32f051 b/midi/stm32f051 new file mode 120000 index 0000000..1a8b53f --- /dev/null +++ b/midi/stm32f051 @@ -0,0 +1 @@ +../stm32f051/ \ No newline at end of file diff --git a/midi/ton/gen.cpp b/midi/ton/gen.cpp new file mode 100644 index 0000000..0cc1961 --- /dev/null +++ b/midi/ton/gen.cpp @@ -0,0 +1,76 @@ +#include +#include +#include "../audio.h" + +static constexpr int maxTone = (1L<<23) - 1; + +int limit (double tone) { + int k = (int) round (tone); + if (k > maxTone) k = 0; + return k; +} +int normalize (double val, double scale) { + return (int) round (val * scale); +} +int main (void) { + double base, dint; + int i,j; + + base = 8.1757989156; // C5 v Hz (http://www.tonalsoft.com/pub/news/pitch-bend.aspx) + base *= (double)(1UL << 24) / double (AudioSampleRate); + dint = pow(2.0, 1.0 / 12.0); + + FILE* out = fopen ("miditone.c","w"); + // Tabulka inkrementů pro midi tóny + fprintf (out, "const unsigned int midiTones[] = {\n"); + for (i=0,j=0; i<127; i++) { + fprintf (out, "%8d, ", limit (base)); + if (++j >= 12) { + j = 0; + fprintf (out, "\n"); + } + base *= dint; + } + fprintf (out, "%8d };\n\n", limit (base)); + // Vzorky pro jednu periodu tónu včetně barvy + double samples [256], max = 0.0, val; + base = M_PI / 128.0; + for (i=0; i<256; i++) { + val = 0.0; + val += 1.0 * sin (1.0 * base * (double) i); + // Je dobré přidat nějaké harmonické, jinak je tón chudý + val += 0.3 * sin (2.0 * base * (double) i); + // 7. harmonická je nepříjemná, zkuste si to. + // val += 0.5 * sin (7.0 * base * (double) i); + if (val > +max) max = +val; + if (val < -max) max = -val; + samples [i] = val; + } + max = (double)(0x1FFF) / max; // normála do 14. bitů + // mormalizace a výpis + fprintf (out, "const short onePeriod[] = {\n"); + for (i=0,j=0; i<255; i++) { + fprintf (out, "%6d, ", normalize (samples[i], max)); + if (++j >= 8) { + j = 0; + fprintf (out, "\n"); + } + base *= dint; + } + fprintf (out, "%6d };\n\n", normalize (samples[i], max)); + + unsigned Attack = maxAmplt; + fprintf (out, "const unsigned attackTable[] = {\n"); + for (i=0,j=0; i<127; i++) { + fprintf (out, "0x%08X, ", Attack); + if (++j >= 8) { + j = 0; + fprintf (out, "\n"); + } + Attack -= Attack / 20; + } + fprintf (out, "0x%08X };\n\n", Attack); + + + fclose (out); +} diff --git a/midi/ton/miditonesV1.6.c b/midi/ton/miditonesV1.6.c new file mode 100644 index 0000000..ec09709 --- /dev/null +++ b/midi/ton/miditonesV1.6.c @@ -0,0 +1,999 @@ +/********************************************************************************* +* +* MIDITONES +* +* Convert a MIDI file into a bytestream of notes +* +* +* (C) Copyright 2011, Len Shustek +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of version 3 of the GNU General Public License as +* published by the Free Software Foundation at http://www.gnu.org/licenses, +* with Additional Permissions under term 7(b) that the original copyright +* notice and author attibution must be preserved and under term 7(c) that +* modified versions be marked as different from the original. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +***********************************************************************************/ +/* +* Change log +* 19 January 2011, L.Shustek, V1.0 +* -Initial release. +* 26 February 2011, L. Shustek, V1.1 +* -Expand the documentation generated in the output file. +* -End the binary output file with an "end of score" command. +* -Fix bug: Some "stop note" commands were generated too early. +* 04 March 2011, L. Shustek, V1.2 +* -Minor error message rewording. +* 13 June 2011, L. Shustek, V1.3 +* -Add -s2 strategy to try to keep each track on its own tone generator +* for when there are separate speakers. This obviously works only when +* each track is monophonic. (Suggested by Michal Pustejovsky) +* 20 November 2011, L. Shustek, V1.4 +* -Add -cn option to mask which channels (tracks) to process +* -Add -kn option to change key +* Both of these are in support of music-playing on my Tesla Coil. +* 05 December 2011, L. Shustek, V1.5 +* -Fix command line parsing error for option -s1 +* -Display the commandline in the C file output +* -Change to decimal instead of hex for note numbers in the C file output +* 06 August 2013, L. Shustek, V1.6 +* -Changed to allow compilation and execution in 64-bit environments +* by using C99 standard intN_t and uintN_t types for MIDI structures, +* and formatting specifications like "PRId32" instead of "ld". +*/ + +#define VERSION "1.6" + + +/*-------------------------------------------------------------------------------- +* +* +* About MIDITONES +* +* +* MIDITONES converts a MIDI music file into a much simplified stream of commands, +* so that a version of the music can be played on a synthesizer having only +* tone generators without any volume or tone controls. +* +* Volume ("velocity") and instrument specifications in the MIDI files are discarded. +* All the tracks are prcoessed and merged into a single time-ordered stream of +* "note on", "note off", and "delay" commands. +* +* This was written for the "Playtune" Arduino library, which plays polyphonic music +* using up to 6 tone generators run by the timers on the processor. See the separate +* documentation for Playtune. But MIDITONES may prove useful for other tone +* generating systems. +* +* The output can be either a C-language source code fragment that initializes an +* array with the command bytestream, or a binary file with the bytestream itself. +* +* MIDITONES is written in standard ANSI C (plus strlcpy and strlcat functions), and +* is meant to be executed from the command line. There is no GUI interface. +* +* The MIDI file format is complicated, and this has not been tested on a very +* wide variety of file types. In particular, we have tested only format type "1", +* which seems to be what most of them are. Let me know if you find MIDI files +* that it won't digest and I'll see if I can fix it. + +* This has been tested only on a little-endian PC, but I think it should work on +* big-endian processors too. Note that the MIDI file format is inherently +* big-endian. +* +* +* ***** The command line ***** +* +* To convert a MIDI file called "chopin.mid" into a command bytestream, execute +* +* miditones chopin +* +* It will create a file in the same directory called "chopin.c" which contains +* the C-language statement to intiialize an array called "score" with the bytestream. +* +* +* The general form for command line execution is this: +* +* miditones [-p] [-lg] [-lp] [-s1] [-tn] [-b] [-cn] [-kn] +* +* The is the base name, without an extension, for the input and +* output files. It can contain directory path information, or not. +* +* The input file is the base name with the extension ".mid". The output filename(s) +* are the base name with ".c", ".bin", and/or ".log" extensions. +* +* +* The following command-line options can be specified: +* +* -p Only parse the MIDI file; don't generate an output file. +* Tracks are processed sequentially instead of being merged into chronological order. +* This is mostly useful when generating a log to debug MIDI file parsing problems. +* +* -lp Log input file parsing information to the .log file +* +* -lg Log output bytestream generation information to the .log file +* +* -sn Use bytestream generation strategy "n". +* Two strategies are currently implemented: +* 1: favor track 1 notes instead of all tracks equally +* 2: try to keep each track to its own tone generator +* +* -tn Generate the bytestream so that at most n tone generators are used. +* The default is 6 tone generators, and the maximum is 16. +* The program will report how many notes had to be discarded because there +* weren't enough tone generators. Note that for the Arduino Playtunes +* library, it's ok to have the bytestream use more tone genreators than +* exist on your processor because any extra notes will be ignored, although +* it does make the file bigger than necessary . Of course, too many ignored +* notes will make the music sound really strange! +* +* -b Generate a binary file with the name .bin, instead of a +* C-language source file with the name .c. +* +* -cn Only process the channel numbers whose bits are on in the number "n". +* For example, -c3 means "only process channels 0 and 1" +* +* -kn Change the musical key of the output by n chromatic notes. +* -k-12 goes one octave down, -k12 goes one octave up, etc. +* +* +* ***** The score bytestream ***** +* +* The generated bytestream is a series of commands that turn notes on and off, and +* start delays until the next note change. Here are the details, with numbers +* shown in hexadecimal. +* +* If the high-order bit of the byte is 1, then it is one of the following commands: +* +* 9t nn Start playing note nn on tone generator t. Generators are numbered +* starting with 0. The notes numbers are the MIDI numbers for the chromatic +* scale, with decimal 60 being Middle C, and decimal 69 being Middle A (440 Hz). +* +* 8t Stop playing the note on tone generator t. +* +* F0 End of score: stop playing. +* +* E0 End of score: start playing again from the beginning. +* (Shown for completeness; MIDITONES won't generate this.) +* +* If the high-order bit of the byte is 0, it is a command to delay for a while until +* the next note change.. The other 7 bits and the 8 bits of the following byte are +* interpreted as a 15-bit big-endian integer that is the number of milliseconds to +* wait before processing the next command. For example, +* +* 07 D0 +* +* would cause a delay of 0x07d0 = 2000 decimal millisconds, or 2 seconds. Any tones +* that were playing before the delay command will continue to play. +* +* +* Len Shustek, 4 Feb 2011 +* +*----------------------------------------------------------------------------------*/ + + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/*********** MIDI file header formats *****************/ + +struct midi_header { + int8_t MThd[4]; + uint32_t header_size; + uint16_t format_type; + uint16_t number_of_tracks; + uint16_t time_division; +}; + +struct track_header { + int8_t MTrk[4]; + uint32_t track_size; +}; + + +/*********** Global variables ******************/ + +#define MAX_TONEGENS 16 /* max tone generators: tones we can play simultaneously */ +#define DEFAULT_TONEGENS 6 /* default number of tone generators */ +#define MAX_TRACKS 24 /* max number of MIDI tracks we will process */ + +bool loggen, logparse, parseonly, strategy1, strategy2, binaryoutput; +FILE *infile, *outfile, *logfile; +uint8_t *buffer, *hdrptr; +unsigned long buflen; +int num_tracks; +int tracks_done = 0; +int outfile_itemcount = 0; +int num_tonegens = DEFAULT_TONEGENS; +int num_tonegens_used = 0; +unsigned channel_mask = 0xffff; // bit mask of channels to process +int keyshift = 0; // optional chromatic note shift for output file +long int outfile_bytecount = 0; +unsigned int ticks_per_beat = 240; +unsigned long timenow = 0; +unsigned long tempo; /* current tempo in usec/qnote */ + +unsigned int directory = 0; +unsigned int file_count = 0; + +struct tonegen_status { /* current status of a tone generator */ + bool playing; /* is it playing? */ + int track; /* if so, which track is the note from? */ + int note; /* what note is playing? */ +} +tonegen [MAX_TONEGENS] = { + 0}; + +struct track_status { /* current processing point of a MIDI track */ + uint8_t *trkptr; /* ptr to the next note change */ + uint8_t *trkend; /* ptr past the end of the track */ + unsigned long time; /* what time we're at in the score */ + unsigned long tempo; /* the tempo last set, in usec/qnote */ + unsigned int preferred_tonegen; /* for strategy2: try to use this generator */ + unsigned char cmd; /* CMD_xxxx next to do */ + unsigned char note; /* for which note */ + unsigned char last_event; /* the last event, for MIDI's "running status" */ + bool tonegens[MAX_TONEGENS];/* which tone generators our notes are playing on */ +} +track[MAX_TRACKS] = { + 0}; + + +/* output bytestream commands, which are also stored in track_status.cmd */ + +#define CMD_PLAYNOTE 0x90 /* play a note: low nibble is generator #, note is next byte */ +#define CMD_STOPNOTE 0x80 /* stop a note: low nibble is generator # */ +#define CMD_RESTART 0xe0 /* restart the score from the beginning */ +#define CMD_STOP 0xf0 /* stop playing */ +/* if CMD < 0x80, then the other 7 bits and the next byte are a 15-bit number of msec to delay */ + +/* these other commands stored in the track_status.com */ +#define CMD_TEMPO 0xFE /* tempo in usec per quarter note ("beat") */ +#define CMD_TRACKDONE 0xFF /* no more data left in this track */ + + + +/************** command-line processing *******************/ + +void SayUsage(char *programName){ + static char *usage[] = { + "Convert MIDI files to an Arduino PLAYTUNE bytestream", + "miditones [-p] [-lg] [-lp] [-s1] [-tn] ", + " -p parse only, don't generate bytestream", + " -lp log input parsing", + " -lg log output generation", + " -s1 strategy 1: favor track 1", + " -s2 strategy 2: try to assign tracks to specific tone generators", + " -tn use at most n tone generators (default is 6, max is 16)", + " -b binary file output instead of C source text", + " -cn mask for which tracks to process, e.g. -c3 for only 0 and 1", + " -kn key shift in chromatic notes, positive or negative", + "input file: .mid", + "output file: .bin or .c", + "log file: .log", + "" }; + int i=0; + while (usage[i][0] != '\0') fprintf(stderr, "%s\n", usage[i++]); +} + +int HandleOptions(int argc,char *argv[]) { + /* returns the index of the first argument that is not an option; i.e. + does not start with a dash or a slash*/ + + int i,firstnonoption=0; + + /* --- The following skeleton comes from C:\lcc\lib\wizard\textmode.tpl. */ + for (i=1; i< argc;i++) { + if (argv[i][0] == '/' || argv[i][0] == '-') { + switch (toupper(argv[i][1])) { + case 'H': + case '?': + SayUsage(argv[0]); + exit(1); + case 'L': + if (toupper(argv[i][2]) == 'G') loggen = true; + else if (toupper(argv[i][2]) == 'P') logparse = true; + else goto opterror; + break; + case 'P': + parseonly = true; + break; + case 'B': + binaryoutput = true; + break; + case 'D': + directory = true; + break; + case 'S': + if (argv[i][2] == '1') strategy1 = true; + else if (argv[i][2] == '2') strategy2 = true; + else goto opterror; + break; + case 'T': + if (sscanf(&argv[i][2],"%d",&num_tonegens) != 1 || num_tonegens <1 || num_tonegens > MAX_TONEGENS) goto opterror; + printf("Using %d tone generators.\n", num_tonegens); + break; + case 'C': + if (sscanf(&argv[i][2],"%d",&channel_mask) != 1 || channel_mask > 0xffff) goto opterror; + printf("Channel (track) mask is %04X.\n", channel_mask); + break; + case 'K': + if (sscanf(&argv[i][2],"%d",&keyshift) != 1 || keyshift < -100 || keyshift > 100) goto opterror; + printf("Using keyshift %d.\n", keyshift); + break; + + /* add more option switches here */ +opterror: + default: + fprintf(stderr,"unknown option: %s\n",argv[i]); + SayUsage(argv[0]); + exit(4); + } + } + else { + firstnonoption = i; + break; + } + } + return firstnonoption; +} + +void print_command_line (int argc,char *argv[]) { + int i; + fprintf(outfile, "// command line: "); + for (i=0; i< argc; i++) fprintf(outfile,"%s ",argv[i]); + fprintf(outfile, "\n"); +} + + + +/**************** utility routines **********************/ + +/* match a constant character sequence */ + +int charcmp (char *buf, char *match) { + int len, i; + len = strlen (match); + for (i=0; i MIDI file error at position %04X (%d): %s\n", (uint16_t)(bufptr-buffer), (uint16_t)(bufptr-buffer), msg); + /* print some bytes surrounding the error */ + ptr = bufptr - 16; + if (ptr < buffer) ptr = buffer; + for (; ptr <= bufptr+16 && ptr < buffer+buflen; ++ptr) fprintf (stderr, ptr==bufptr ? " [%02X] ":"%02X ", *ptr); + fprintf(stderr, "\n"); + exit(8); +} + +/* check that we have a specified number of bytes left in the buffer */ + +void chk_bufdata(unsigned char *ptr, int len) { + if (ptr + len - buffer > buflen) midi_error("data missing", ptr); +} + + +/* fetch big-endian numbers */ + +uint16_t rev_short (uint16_t val) { + return ((val&0xff)<<8) | ((val>>8)&0xff); +} + +uint32_t rev_long (uint32_t val){ + return (((rev_short((uint16_t)val) & 0xffff) << 16) | + (rev_short((uint16_t)(val >> 16)) & 0xffff)); +} + +/* account for new items in the non-binary output file +and generate a newline every so often. */ + +void outfile_items (int n) { + outfile_bytecount += n; + outfile_itemcount += n; + if (!binaryoutput && outfile_itemcount > 20) { + fprintf (outfile, "\n"); + outfile_itemcount = 0; + } +} + +/************** process the MIDI file header *****************/ + +void process_header (void) { + struct midi_header *hdr; + unsigned int time_division; + + chk_bufdata(hdrptr, sizeof(struct midi_header)); + hdr = (struct midi_header *) hdrptr; + if (!charcmp(hdr->MThd,"MThd")) midi_error("Missing 'MThd'", hdrptr); + + num_tracks = rev_short(hdr->number_of_tracks); + + time_division = rev_short(hdr->time_division); + if (time_division < 0x8000) ticks_per_beat = time_division; + else ticks_per_beat = ((time_division >> 8) & 0x7f) /* SMTE frames/sec */ * (time_division & 0xff); /* ticks/SMTE frame */ + + if (logparse) { + fprintf (logfile, "Header size %" PRId32 "\n", rev_long(hdr->header_size)); + fprintf (logfile, "Format type %d\n", rev_short(hdr->format_type)); + fprintf (logfile, "Number of tracks %d\n", num_tracks); + fprintf (logfile, "Time division %04X\n", time_division); + fprintf (logfile, "Ticks/beat = %d\n", ticks_per_beat); + + } + hdrptr += rev_long(hdr->header_size) + 8; /* point past header to track header, presumably. */ + return; +} + + +/**************** Process a MIDI track header *******************/ + +void start_track (int tracknum) { + struct track_header *hdr; + unsigned long tracklen; + + chk_bufdata(hdrptr, sizeof(struct track_header)); + hdr = (struct track_header *) hdrptr; + if (!charcmp(hdr->MTrk,"MTrk")) midi_error("Missing 'MTrk'", hdrptr); + + tracklen = rev_long(hdr->track_size); + if (logparse) fprintf (logfile, "\nTrack %d length %ld\n", tracknum, tracklen); + hdrptr += sizeof (struct track_header); /* point past header */ + chk_bufdata(hdrptr, tracklen); + track[tracknum].trkptr = hdrptr; + hdrptr += tracklen; /* point to the start of the next track */ + track[tracknum].trkend = hdrptr; /* the point past the end of the track */ +} + + +/* Get a MIDI-style variable-length integer */ + +unsigned long get_varlen (uint8_t **ptr) { + /* Get a 1-4 byte variable-length value and adjust the pointer past it. + These are a succession of 7-bit values with a MSB bit of zero marking the end */ + + unsigned long val; + int i, byte; + + val = 0; + for (i=0; i<4; ++i){ + byte = *(*ptr)++; + val = (val<<7) | (byte&0x7f); + if (!(byte&0x80)) return val; + } + return val; +} + + +/*************** Process the MIDI track data ***************************/ + +/* Skip in the track for the next "note on", "note off" or "set tempo" command, +then record that information in the track status block and return. */ + +void find_note (int tracknum) { + unsigned long int delta_time; + int event, chan; + int i; + int note, velocity, parm; + int meta_cmd, meta_length; + unsigned long int sysex_length; + struct track_status *t; + + /* process events */ + + t = &track[tracknum]; /* our track status structure */ + while (t->trkptr < t->trkend) { + + delta_time = get_varlen(&t->trkptr); + if (logparse) { + fprintf (logfile, "trk %d ", tracknum); + fprintf (logfile, delta_time ? "delta time %4ld, " : " ", delta_time); + } + t->time += delta_time; + + if (*t->trkptr < 0x80) /* "running status" */ event = t->last_event;/* means same event as before */ + else { /* new "status" (event type) */ + event = *t->trkptr++; + t->last_event = event; + } + + if (event == 0xff) { /* meta-event */ + meta_cmd = *t->trkptr++; + meta_length = *t->trkptr++; + switch (meta_cmd) { + case 0x2f: + if (logparse) fprintf(logfile, "end of track\n"); + break; + case 0x00: + if (logparse) fprintf(logfile, "sequence number %d\n", rev_short(*(unsigned short *)t->trkptr)); + break; + case 0x20: + if (logparse) fprintf(logfile, "channel prefix %d\n", *t->trkptr); + break; + case 0x51: /* tempo: 3 byte big-endian integer! */ + t->cmd = CMD_TEMPO; + t->tempo = rev_long(*(unsigned long *)(t->trkptr-1)) & 0xffffffL; + if (logparse) fprintf(logfile, "set tempo %ld usec/qnote\n", t->tempo); + t->trkptr += meta_length; + return; + case 0x54: + if (logparse) fprintf(logfile, "SMPTE offset %08" PRIx32 "\n", rev_long(*(unsigned long *)t->trkptr)); + break; + case 0x58: + if (logparse) fprintf(logfile, "time signature %08" PRIx32 "\n", rev_long(*(unsigned long *)t->trkptr)); + break; + case 0x59: + if (logparse) fprintf(logfile, "key signature %04X\n", rev_short(*(unsigned short *)t->trkptr)); + break; + default: /* assume it is a string */ + if (logparse) { + fprintf(logfile, "meta cmd %02X, length %d, \"", meta_cmd, meta_length); + for (i=0; itrkptr[i]; + fprintf(logfile, "%c", isprint(ch) ? ch : '?'); + } + fprintf(logfile, "\"\n"); + } + if (tracknum==0 && meta_cmd==0x03 && !parseonly && !binaryoutput) { + /* Incredibly, MIDI has no standard for recording the name of the piece! + Track 0's "trackname" (meta 0x03) is sometimes used for that, so + we output it to the C file as documentation. */ + fprintf(outfile, "// "); + for (i=0; itrkptr[i]; + fprintf(outfile, "%c", isprint(ch) ? ch : '?'); + } + fprintf(outfile, "\n"); + } + break; + } + t->trkptr += meta_length; + } + + else if (event <0x80) midi_error("Unknown MIDI event type", t->trkptr); + + else { + chan = event & 0xf; + switch (event>>4) { + case 0x8: + t->note = *t->trkptr++; + velocity = *t->trkptr++; +note_off: + if (logparse) fprintf (logfile, "note %02X off, chan %d, velocity %d\n", t->note, chan, velocity); + if ((1<cmd = CMD_STOPNOTE; + return; /* stop processing and return */ + } + break; // else keep looking + case 0x9: + t->note = *t->trkptr++; + velocity = *t->trkptr++; + if (velocity == 0) /* some scores use note-on with zero velocity for off! */ goto note_off; + if (logparse) fprintf (logfile, "note %02X on, chan %d, velocity %d\n", t->note, chan, velocity); + if ((1<cmd = CMD_PLAYNOTE; + return; /* stop processing and return */ + } + break; // else keep looking + case 0xa: + note = *t->trkptr++; + velocity = *t->trkptr++; + if (logparse) fprintf (logfile, "after-touch %02X, %02X\n", note, velocity); + break; + case 0xb: + note = *t->trkptr++; + velocity = *t->trkptr++; + if (logparse) fprintf (logfile, "control change %02X, %02X\n", note, velocity); + break; + case 0xc: + note = *t->trkptr++; + if (logparse) fprintf(logfile, "program patch %02X\n", note); + break; + case 0xd: + chan = *t->trkptr++; + if (logparse) fprintf(logfile, "channel after-touch %02X\n", chan); + break; + case 0xe: + note = *t->trkptr++; + velocity = *t->trkptr++; + if (logparse) fprintf(logfile, "pitch wheel change %02X, %02X\n", note, velocity); + break; + case 0xf: + sysex_length = get_varlen(&t->trkptr); + if (logparse) fprintf(logfile, "SysEx event %02X, %ld bytes\n", event, sysex_length); + t->trkptr += sysex_length; + break; + default: + midi_error("Unknown MIDI command", t->trkptr); + } + } + } + t->cmd = CMD_TRACKDONE; /* no more notes to process */ + ++tracks_done; +} +#define MAXPATH 120 + +int ProcessMidiFile (char* filebasename) { + int i; + int tracknum = 0; + int earliest_tracknum = 0; + unsigned long earliest_time = 0; + int notes_skipped = 0; + + char filename[MAXPATH]; + // set static variables to default + outfile_bytecount = 0; + outfile_itemcount = 0; + tracks_done = 0; + num_tonegens_used = 0; + timenow = 0; + tempo = 0; + ticks_per_beat = 240; + channel_mask = 0xffff; + memset (track, 0, sizeof (track)); + memset (tonegen, 0, sizeof (tonegen)); + /* Open the input file */ + + strncpy(filename, filebasename, MAXPATH); + strncat(filename, ".mid", MAXPATH); + infile = fopen(filename, "rb"); + if (!infile) { + fprintf(stderr, "Unable to open input file %s", filename); + return 1; + } + + /* Read the whole input file into memory */ + + fseek(infile, 0, SEEK_END); /* find file size */ + buflen = ftell(infile); + fseek(infile, 0, SEEK_SET); + + buffer = (unsigned char *) malloc (buflen+1); + if (!buffer) { + fprintf(stderr, "Unable to allocate %ld bytes for the file", buflen); + return 1; + } + + i = fread(buffer, buflen, 1, infile); + fclose(infile); + if (logparse) fprintf(logfile, "Processing %s, %ld bytes\n", filename, buflen); + + /* Create the output file */ + + if (!parseonly) { + /* + strncpy(filename, filebasename, MAXPATH); + if (binaryoutput) { + strncat(filename, ".bin", MAXPATH); + outfile = fopen(filename, "wb"); + } + else { + strncat(filename, ".c", MAXPATH); + outfile = fopen(filename, "w"); + } + if (!outfile) { + fprintf(stderr, "Unable to open output file %s", filename); + return 1; + } + */ + if (!binaryoutput) { /* create header of C file that initializes score data */ + time_t rawtime; + struct tm *ptime; + time (&rawtime); + fprintf(outfile, "// Playtune bytestream for file \"%s.mid\" ", filebasename); + fprintf(outfile, "created by MIDITONES V%s on %s", VERSION, asctime(localtime(&rawtime))); +// print_command_line(argc,argv); + if (channel_mask != 0xffff) + fprintf(outfile, "// Only the masked channels were processed: %04X\n", channel_mask); + if (keyshift != 0) + fprintf(outfile, "// Keyshift was %d chromatic notes\n", keyshift); +// if (directory) { + fprintf(outfile, "const unsigned char score%d [] = {\n", file_count++); +// } else +// fprintf(outfile, "const unsigned char score [] = {\n"); + } + } + + /* process the MIDI file header */ + + hdrptr = buffer; /* pointer to file and track headers */ + process_header (); + printf (" Processing %d tracks.\n", num_tracks); + if (num_tracks > MAX_TRACKS) midi_error ("Too many tracks", buffer); + + /* initialize processing of all the tracks */ + + for (tracknum=0; tracknum < num_tracks; ++tracknum) { + start_track (tracknum); /* process the track header */ + find_note (tracknum); /* position to the first note on/off */ + /* if we are in "parse only" mode, do the whole track, + so we do them one at a time instead of time-synchronized. */ + if (parseonly) while (track[tracknum].cmd != CMD_TRACKDONE) find_note(tracknum); + } + + /* Continue processing all tracks, in an order based on the simulated time. + This is not unlike multiway merging used for tape sorting algoritms in the 50's! */ + + tracknum = 0; + if (!parseonly) do { /* while there are still track notes to process */ + struct track_status *trk; + struct tonegen_status *tg; + int tgnum; + int count_tracks; + unsigned long delta_time, delta_msec; + + /* Find the track with the earliest event time, + and output a delay command if time has advanced. + + A potential improvement: If there are multiple tracks with the same time, + first do the ones with STOPNOTE as the next command, if any. That would + help avoid running out of tone generators. In practice, though, most MIDI + files do all the STOPNOTEs first anyway, so it won't have much effect. + */ + + earliest_time = 0x7fffffff; + + /* Usually we start with the track after the one we did last time (tracknum), + so that if we run out of tone generators, we have been fair to all the tracks. + The alternate "strategy1" says we always start with track 0, which means + that we favor early tracks over later ones when there aren't enough tone generators. + */ + + count_tracks = num_tracks; + if (strategy1) tracknum = num_tracks; /* beyond the end, so we start with track 0 */ + do { + if (++tracknum >= num_tracks) tracknum=0; + trk = &track[tracknum]; + if (trk->cmd != CMD_TRACKDONE && trk->time < earliest_time) { + earliest_time = trk->time; + earliest_tracknum = tracknum; + } + } + while (--count_tracks); + + tracknum = earliest_tracknum; /* the track we picked */ + trk = &track[tracknum]; + + if (loggen) fprintf (logfile, "Earliest time is trk %d, time %ld\n", tracknum, earliest_time); + if (earliest_time < timenow) midi_error ("INTERNAL: time went backwards", trk->trkptr); + + /* If time has advanced, output a "delay" command */ + + delta_time = earliest_time - timenow; + if (delta_time) { + /* Convert ticks to milliseconds based on the current tempo */ + delta_msec = ((unsigned long) delta_time * tempo) / ticks_per_beat / 1000; + if (loggen) fprintf (logfile, "->Delay %ld msec (%ld ticks)\n", delta_msec, delta_time); + if (delta_msec > 0x7fff) { + //delta_msec = 0x3FF; + midi_error ("INTERNAL: time delta too big", trk->trkptr); + } + /* output a 15-bit delay in big-endian format */ + if (binaryoutput) { + putc ((unsigned char) (delta_msec >> 8), outfile); + putc ((unsigned char) (delta_msec & 0xff), outfile); + outfile_bytecount += 2; + } + else { + fprintf (outfile, "%ld,%ld, ", delta_msec >> 8, delta_msec & 0xff); + outfile_items(2); + } + } + timenow = earliest_time; + + /* If this track event is "set tempo", just change the global tempo. + That affects how we generate "delay" commands. */ + + if (trk->cmd == CMD_TEMPO) { + tempo = trk->tempo; + if (loggen) fprintf (logfile, "Tempo changed to %ld usec/qnote\n", tempo); + find_note (tracknum); + } + + /* If this track event is "stop note", process it and all subsequent "stop notes" for this track + that are happening at the same time. Doing so frees up as many tone generators as possible. */ + + else if (trk->cmd == CMD_STOPNOTE) do { + + // stop a note + for (tgnum=0; tgnum < num_tonegens; ++tgnum) { /* find which generator is playing it */ + tg = &tonegen[tgnum]; + if (tg->playing && tg->track == tracknum && tg->note == trk->note) { + if (loggen) fprintf (logfile, "->Stop note %02X, generator %d, track %d\n", tg->note, tgnum, tracknum); + if (binaryoutput) { + putc (CMD_STOPNOTE | tgnum, outfile); + outfile_bytecount += 1; + } + else { + fprintf (outfile, "0x%02X, ", CMD_STOPNOTE | tgnum); + outfile_items (1); + } + tg->playing = false; + trk->tonegens[tgnum] = false; + } + } + find_note (tracknum); // use up the note + } + while (trk->cmd == CMD_STOPNOTE && trk->time == timenow); + + /* If this track event is "start note", process only it. + Don't do more than one, so we allow other tracks their chance at grabbing tone generators. */ + + else if (trk->cmd == CMD_PLAYNOTE) { + bool foundgen = false; + + if (strategy2) { /* try to use the same tone generator this track used last time */ + tg = &tonegen [trk->preferred_tonegen]; + if (!tg->playing) { + tgnum = trk->preferred_tonegen; + foundgen = true; + } + } + if (!foundgen) for (tgnum=0; tgnum < num_tonegens; ++tgnum) { /* search for a free tone generator */ + tg = &tonegen[tgnum]; + if (!tg->playing) { + foundgen = true; + break; + } + } + if (foundgen) { + int shifted_note; + if (tgnum+1 > num_tonegens_used) num_tonegens_used = tgnum+1; + tg->playing = true; + tg->track = tracknum; + tg->note = trk->note; + trk->tonegens[tgnum] = true; + trk->preferred_tonegen = tgnum; + if (loggen) fprintf (logfile, "->Start note %02X, generator %d, track %d\n", trk->note, tgnum, tracknum); + shifted_note = trk->note + keyshift; + if (shifted_note < 0) shifted_note = 0; + if (shifted_note > 127) shifted_note = 127; + if (binaryoutput) { + putc (CMD_PLAYNOTE | tgnum, outfile); + putc (shifted_note, outfile); + outfile_bytecount += 2; + } + else { + fprintf (outfile, "0x%02X,%d, ", CMD_PLAYNOTE | tgnum, shifted_note); + outfile_items(2); + } + } + else { + if (loggen) fprintf (logfile, "----> No free generator, skipping note %02X, track %d\n", trk->note, tracknum); + ++notes_skipped; + } + find_note (tracknum); // use up the note + } + + } /* !parseonly do */ + while (tracks_done < num_tracks); + + if (!parseonly) { + // generate the end-of-score command and some commentary + if(binaryoutput) putc(CMD_STOP, outfile); + else { + fprintf(outfile, "0x%02x};\n// This score contains %ld bytes, and %d tone generator%s used.\n", CMD_STOP, outfile_bytecount, num_tonegens_used, num_tonegens_used == 1 ? " is" : "s are"); + if (notes_skipped) fprintf(outfile, "// %d notes had to be skipped.\n", notes_skipped); + } + printf (" %s %d tone generators were used.\n", num_tonegens_used < num_tonegens ? "Only":"All", num_tonegens_used); + if (notes_skipped) printf(" %d notes were skipped because there weren't enough tone generators.\n", notes_skipped); + printf (" %ld bytes of score data were generated.\n", outfile_bytecount); + } +} +int CreateFilename (const char * filebasename, char* filename, const char * name) { + char buf [MAXPATH]; + strncpy (buf, name, MAXPATH); + char * tmp = strstr (buf, ".mid"); + if (!tmp) return 0; + * tmp = '\0'; + strncpy (filename, filebasename, MAXPATH); + strncat (filename, buf, MAXPATH); + return 1; +} +void AppendScores (FILE * outfile, int file_count) { + fprintf (outfile, "\nconst unsigned char * const scores[] = {\n"); + int i; + for (i=0; id_name); + if (!result) continue; + printf ("Process: \"%s\"\n", filename); + ProcessMidiFile (filename); + } +/* + fprintf (outfile, "\nconst unsigned char * scores[] = {\n"); + int i; + for (i=0; i> 16) & 0xFF; + y = onePeriod [x]; // vzorek vezmeme z tabulky + + // k je horní půlka amplitudy + k = ampl >> 16; + y *= k; // vzorek násobíme amplitudou (tedy tím vrškem) + y >>= 12; // a vezmeme jen to, co potřebuje DAC + k *= fall; // Konstanta fall určuje rychlost poklesu amplitudy, + // čím více, tím je rychlejší. Pokud by bylo 1, pokles je 2^16 vzorků, což už je pomalé. + base += freq; // časová základna pro další vzorek + + if (atck) { // přidán attack = náběh amplitudy + t = attackTable [atck]; // z tabulky + if (t > ampl) ampl = t; // prevence lupání - nemí být skok amplitudy + atck -= 1; // dočasovat k nule + } else + ampl -= k; // exponenciální pokles amplitudy + // a je to + + return y; +} + diff --git a/midi/tone.h b/midi/tone.h new file mode 100644 index 0000000..cd0545d --- /dev/null +++ b/midi/tone.h @@ -0,0 +1,26 @@ +#ifndef TONE_H +#define TONE_H + +class Tone { + public: + explicit Tone () noexcept; + void setMidiOn (unsigned int m); + void setMidiOff (void); + void setFreq (unsigned int f); + void setAmpl (unsigned int a); + void setFall (unsigned int f); + int step (void); + private: + /// Amplituda tónu, interní proměnná + unsigned int ampl; + /// Exponenciální doběh - čím víc, tím rychlejší (0 = stálý) + unsigned int fall; + /// Frekvence (normalizovaná) + unsigned int freq; + /// Přetékající index do tabulky vzorků + unsigned int base; + /// Attack = index do tabulky attackTable + unsigned int atck; +}; + +#endif // TONE_H diff --git a/stm32f051/pcmdma.cpp b/stm32f051/pcmdma.cpp new file mode 100644 index 0000000..9e6e38a --- /dev/null +++ b/stm32f051/pcmdma.cpp @@ -0,0 +1,88 @@ +#include "STM32F0x1.h" +#include "CortexM0.h" +#include "gpio.h" +#include "pcmdma.h" + +typedef __SIZE_TYPE__ size_t; +/* TIMER: + * fs = 8000Hz, ft = 3 * fs = 24000Hz + * reload = SystemCoreClock / ft = 48000000Hz / 24000Hz = 2000 + * PINY : +PA9[AF2], -PB0[AF2] => TIM1:CH2 + * DMA : TIM1_UP = 5 + */ +static void Dma1Ch5Init (void * addr) { + // Configure the peripheral data register address etc + DMA1. CPAR5.R = reinterpret_cast (& (TIM1.CCR2)); + DMA1. CMAR5.R = reinterpret_cast (addr); + DMA1.CNDTR5.R = FULL_LEN; + // Configure increment, size, interrupts and circular mode + DMA1.CCR5.modify([](auto & ccr) -> auto { + ccr.B.MINC = SET; + ccr.B.MSIZE = 1u; + ccr.B.PSIZE = 1u; + ccr.B.DIR = SET; + ccr.B.HTIE = SET; // Po půlce přerušit. + ccr.B.TCIE = SET; // Po dokončení přerušit. + ccr.B.CIRC = SET; + ccr.B.EN = RESET; + return ccr.R; + }); +} +static PcmDma * PcmDmaInstance = nullptr; +PcmDma::PcmDma() noexcept : pL(buffer), pH(buffer + HALF_LEN) { + PcmDmaInstance = this; + for (unsigned n=0; n> 1; + GpioClass pin1p (GpioPortA, 9, GPIO_Mode_AF); + GpioClass pin1n (GpioPortB, 0, GPIO_Mode_AF); + pin1p.setAF (2); + pin1n.setAF (2); + // 1. Enable clock peripheral + RCC.APB2ENR.B.TIM1EN = SET; + RCC.AHBENR. B.DMA1EN = SET; + // 2. Timer + TIM1.PSC.R = 0u; + TIM1.ARR.R = MAXPWM - 1; + TIM1.RCR.R = 0u; + // OC preload, CC output, Mode 6 = PWM1 + TIM1.CCMR1_Output.modify([](TIM1_Type::CCMR1_Output_DEF & r) -> auto { + r.B.OC2PE = SET; + r.B.OC2M = 6u; + return r.R; + }); + // povol pin + negaci + TIM1.CCER.modify([](TIM1_Type::CCER_DEF & r) -> auto { + r.B.CC2E = SET; + r.B.CC2NE = SET; + return r.R; + }); + // Set Output, dead time + TIM1.BDTR.modify([](TIM1_Type::BDTR_DEF & r) -> auto { + r.B.DTG = 48u; // dead: 1 us + r.B.MOE = SET; // Main output enable + //r.B.OSSR = 1u; // Off-state selection for Run mode - TODO + return r.R; + }); + // Preload + TIM1.CR1.modify([](TIM1_Type::CR1_DEF & r) -> auto { + r.B.ARPE = SET; // TIM1_ARR register is buffered + r.B.URS = SET; // Only counter overflow/underflow generates an update DMA request + return r.R; + }); + /* Update DMA request enable + * Spustíme DMA - sice budou dlouhé buffery, ale přerušení jen po 20ms */ + TIM1.EGR.B.UG = SET; // Reinitialize the counter and generates an update of the registers + TIM1.DIER.B.UDE = SET; // Update DMA request enabled + Dma1Ch5Init (buffer); + // 3. NVIC + NVIC_EnableIRQ (DMA1_CH4_5_6_7_DMA2_CH3_4_5_IRQn); + TIM1.CR1.R |= 1u; // enable TIM1 (překladač bohužel bere poslední bit jako half, registr to neunese) + DMA1.CCR5.R |= 1u; // enable DMA (dtto) +} +// Přerušení od DMA +extern "C" void DMA1_CH4_5_6_7_DMA2_CH3_4_5_IRQHandler (void) { + DMA1_Type::ISR_DEF status (DMA1.ISR); + DMA1.IFCR.R = status.R; // clear flags + if (!PcmDmaInstance) return; + if (status.B.HTIF5 != RESET) PcmDmaInstance->send(false); + else if (status.B.TCIF5 != RESET) PcmDmaInstance->send(true); +}