From 8cef027a5da278b61ad9e49dd43bc5f169934aa0 Mon Sep 17 00:00:00 2001 From: Emilie Gillet Date: Tue, 2 Apr 2019 13:24:18 +0200 Subject: [PATCH 01/67] Updated name --- README | 2 +- avrlib | 2 +- common/lfo.h | 4 ++-- common/patch.h | 4 ++-- common/protocol.h | 4 ++-- controller/bootloader/bootloader.cc | 2 +- controller/bootloader/makefile | 2 +- controller/controller.cc | 4 ++-- controller/controller.h | 4 ++-- controller/display.cc | 4 ++-- controller/display.h | 4 ++-- controller/hardware_config.h | 4 ++-- controller/hardware_design/pcb/Mobo-v09.brd | Bin 182159 -> 182158 bytes controller/hardware_design/pcb/Mobo-v09.sch | Bin 473759 -> 473754 bytes controller/leds.cc | 4 ++-- controller/leds.h | 4 ++-- controller/makefile | 2 +- controller/midi_dispatcher.cc | 4 ++-- controller/midi_dispatcher.h | 4 ++-- controller/multi.cc | 4 ++-- controller/multi.h | 4 ++-- controller/note_stack.h | 4 ++-- controller/parameter.cc | 4 ++-- controller/parameter.h | 4 ++-- controller/part.cc | 4 ++-- controller/part.h | 4 ++-- controller/resources.cc | 4 ++-- controller/resources.h | 4 ++-- controller/resources/characters.py | 4 ++-- controller/resources/lookup_tables.py | 4 ++-- controller/resources/resources.py | 8 ++++---- controller/resources/strings.py | 4 ++-- controller/resources/waveforms.py | 4 ++-- controller/storage.cc | 4 ++-- controller/storage.h | 4 ++-- controller/system_settings.cc | 4 ++-- controller/system_settings.h | 4 ++-- controller/ui.cc | 4 ++-- controller/ui.h | 4 ++-- controller/ui_pages/card_info_page.cc | 4 ++-- controller/ui_pages/card_info_page.h | 4 ++-- controller/ui_pages/dialog_box.cc | 4 ++-- controller/ui_pages/dialog_box.h | 4 ++-- controller/ui_pages/knob_assigner.cc | 4 ++-- controller/ui_pages/knob_assigner.h | 4 ++-- controller/ui_pages/library.cc | 4 ++-- controller/ui_pages/library.h | 4 ++-- controller/ui_pages/os_info_page.cc | 4 ++-- controller/ui_pages/os_info_page.h | 4 ++-- controller/ui_pages/parameter_editor.cc | 4 ++-- controller/ui_pages/parameter_editor.h | 4 ++-- controller/ui_pages/performance_page.cc | 4 ++-- controller/ui_pages/performance_page.h | 4 ++-- controller/ui_pages/sequence_editor.cc | 4 ++-- controller/ui_pages/sequence_editor.h | 4 ++-- controller/ui_pages/ui_page.cc | 4 ++-- controller/ui_pages/ui_page.h | 4 ++-- controller/ui_pages/version_manager.cc | 4 ++-- controller/ui_pages/version_manager.h | 4 ++-- controller/ui_pages/voice_assigner.cc | 4 ++-- controller/ui_pages/voice_assigner.h | 4 ++-- controller/voice_allocator.cc | 4 ++-- controller/voice_allocator.h | 4 ++-- controller/voicecard_tx.cc | 4 ++-- controller/voicecard_tx.h | 4 ++-- makefile | 2 +- midi/midi.h | 4 ++-- peripherals/nunchuk/app.cc | 4 ++-- peripherals/nunchuk/app.h | 4 ++-- peripherals/nunchuk/apps/cyclo_sequencer.cc | 4 ++-- peripherals/nunchuk/apps/cyclo_sequencer.h | 4 ++-- peripherals/nunchuk/apps/droner.cc | 4 ++-- peripherals/nunchuk/apps/droner.h | 4 ++-- peripherals/nunchuk/apps/drummer.cc | 4 ++-- peripherals/nunchuk/apps/drummer.h | 4 ++-- .../hardware_design/pcb/nunchuk-v01.brd | Bin 122565 -> 122564 bytes .../hardware_design/pcb/nunchuk-v01.sch | Bin 314519 -> 314518 bytes peripherals/nunchuk/makefile | 2 +- peripherals/nunchuk/midi_output.cc | 4 ++-- peripherals/nunchuk/midi_output.h | 4 ++-- peripherals/nunchuk/nunchuk.cc | 2 +- peripherals/nunchuk/ui.cc | 4 ++-- peripherals/nunchuk/ui.h | 4 ++-- tools | 2 +- utils/compile_bank.py | 4 ++-- utils/convert_shruthi_data.py | 4 ++-- utils/convert_shruthi_syx.py | 4 ++-- utils/list_bank.py | 4 ++-- voicecard/audio_out.cc | 4 ++-- voicecard/audio_out.h | 4 ++-- voicecard/bootloader/bootloader.cc | 2 +- voicecard/bootloader/makefile | 2 +- voicecard/envelope.h | 4 ++-- .../hardware_design/pcb/Voicecard-4P-v01.brd | Bin 93096 -> 93095 bytes .../hardware_design/pcb/Voicecard-4P-v01.sch | Bin 366686 -> 366691 bytes .../hardware_design/pcb/Voicecard-SMR-v04.brd | Bin 92859 -> 92858 bytes .../hardware_design/pcb/Voicecard-SMR-v04.sch | Bin 365892 -> 365897 bytes .../hardware_design/pcb/Voicecard-SVF-v05.brd | Bin 185979 -> 185978 bytes .../hardware_design/pcb/Voicecard-SVF-v05.sch | Bin 357131 -> 357136 bytes .../pcb/Voicecard-Template.sch | Bin 268363 -> 268320 bytes voicecard/leds.cc | 4 ++-- voicecard/leds.h | 4 ++-- voicecard/makefile | 2 +- voicecard/oscillator.cc | 4 ++-- voicecard/oscillator.h | 4 ++-- voicecard/resources.cc | 4 ++-- voicecard/resources.h | 4 ++-- voicecard/resources/lookup_tables.py | 4 ++-- voicecard/resources/resources.py | 8 ++++---- voicecard/resources/waveforms.py | 4 ++-- voicecard/sub_oscillator.h | 4 ++-- voicecard/transient_generator.h | 4 ++-- voicecard/voice.cc | 4 ++-- voicecard/voice.h | 4 ++-- voicecard/voicecard.cc | 4 ++-- voicecard/voicecard.h | 4 ++-- voicecard/voicecard_rx.cc | 4 ++-- voicecard/voicecard_rx.h | 4 ++-- 118 files changed, 206 insertions(+), 206 deletions(-) diff --git a/README b/README index 4e2d654..890c5cd 100644 --- a/README +++ b/README @@ -6,7 +6,7 @@ The motherboard comprises 6 audio outputs, each one connected to a voicecard ; a 3 designs of voicecards implementing a refined version of the Shruthi-1 engine are provided. Each of those use a different filter (4-Pole with LM13700, 4-Pole with SSM2164, 2-Pole SVF with SSM2164). -Original developer: Olivier Gillet (olivier@mutable-instruments.net) +Original developer: Emilie Gillet (emilie.o.gillet@gmail.com) The firmware is released under a GPL3.0 license. It includes a variant of the formant synthesis algorithm used in Peter Knight's Cantarino speech synthesizer. diff --git a/avrlib b/avrlib index 7faa91d..276b288 160000 --- a/avrlib +++ b/avrlib @@ -1 +1 @@ -Subproject commit 7faa91d0e1a63a80d02b456272d888cb484eab01 +Subproject commit 276b2887e4110ca913294fcbb313163dfb28a448 diff --git a/common/lfo.h b/common/lfo.h index a559708..0e00951 100755 --- a/common/lfo.h +++ b/common/lfo.h @@ -1,6 +1,6 @@ -// Copyright 2009 Olivier Gillet. +// Copyright 2009 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/common/patch.h b/common/patch.h index d09ad9a..33d140e 100755 --- a/common/patch.h +++ b/common/patch.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/common/protocol.h b/common/protocol.h index 08f7f1a..7871376 100755 --- a/common/protocol.h +++ b/common/protocol.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/bootloader/bootloader.cc b/controller/bootloader/bootloader.cc index 7a65323..2b93554 100755 --- a/controller/bootloader/bootloader.cc +++ b/controller/bootloader/bootloader.cc @@ -1,4 +1,4 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/bootloader/makefile b/controller/bootloader/makefile index 57f3864..32ae723 100755 --- a/controller/bootloader/makefile +++ b/controller/bootloader/makefile @@ -1,4 +1,4 @@ -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/controller/controller.cc b/controller/controller.cc index e68ce92..ad2384c 100644 --- a/controller/controller.cc +++ b/controller/controller.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/controller.h b/controller/controller.h index a4c8cbb..430e7a8 100755 --- a/controller/controller.h +++ b/controller/controller.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/display.cc b/controller/display.cc index dec1ccc..d70edec 100755 --- a/controller/display.cc +++ b/controller/display.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/display.h b/controller/display.h index 744210f..b09c3df 100755 --- a/controller/display.h +++ b/controller/display.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/hardware_config.h b/controller/hardware_config.h index 48d30d3..518f967 100755 --- a/controller/hardware_config.h +++ b/controller/hardware_config.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/hardware_design/pcb/Mobo-v09.brd b/controller/hardware_design/pcb/Mobo-v09.brd index 40ef71d6dc9e63c10119d9f61d89f5ffb0a4ff6d..317743841e80b3a2136af1734b37df350f42444f 100644 GIT binary patch delta 871 zcmY*VZAep57=GWg>t?8PZno)~)9@pLq^p?um13j`idl+6p(0Tzs3n19idR`#eqKXj zpy7$NPJ|FLM4o8+F#^i_;U0)qo z69B>v2n9mo${8`!-#htmv_1yTJ&@;YXYZ})q8Czr=ruzkN-!iktcLr@2p3{&nIg6k z(VBJ?5#xJN5x;b(0bU$6XpzD3JnT{%3)= zcpgA{OlH%`PNi}?@3Ml0g%$X>E*jqX7yX%-Pj&me&c`oN)PttcKL_f-=l||s2Q4Lv zMny+%@jNm@@9C*FM?7D(yT1n8oHeg z-Pl_~r#jtl(8J6;p3#T#yPGCh?AOCad=9^WOBU_!6*Pu?^T(1jcz(+WFH#EtVx=we zwJtRBqBIv3(O}oEqVdppun;HrED*T|;7XtckjA5$4ECZ`JjljHXYDH9%~6r;{2ahL zOqrS)o7F|}ZGgf)00w${<35_4*#`DB=|4jkG}Kw_gTU)PX9G%vyye{q!$q&oVQp Fe*;Lb4*Myt@ms#>fRsE`+U65Ih@;$ScfAvT{GAW z<`jUa9YV2Cw{ZOPh3xw?I&)%(Ws}>e`v*4iOJ9^xy%W77!pn=HepCS zPeiTX2#L%vz(V3z5{Hd_OtjKkwpAL9Hu=yhxi(BwVQrGgL-QKr;!x_8fNhd1JEI%L zHd@$AZd`2m^MqrIWZwZirgmX_AAzjeFdZD1yl!-}YGo7egICCaWbefysSHQ*0?4=jP2 z5@qd@tDt;}A))WsSPaLJ>RKjJSbk2#Q?tRu4kFt`R%J(lB(&IZ2hQg;=^h&J9xr1qjug*N_V)tIq|-h;mh|KuWy z5Sr;fLf1`Z{JK>XebnnieZ82t=~-a9h;RH*kqJ+)>fl*MF+ifc zO1|37bi63jO+_>~)r(BGyE9zK?r)i3=N5oFg%&_gBi&iH$da&AQkoXo&3x6ks8T)! z*$q_X)T7dcIH8H-+W@7500>%V%Qn+1c?tM9^*=)sHllO!y};|%^U=fj@UjrQkq-w- za4eM%%V*IXviufY)&A3x22sunKdIi^gWsyyxUw>fhohZybC~2b%%ep(cscKv(m#(0 G(Z2zh7Xje_ diff --git a/controller/hardware_design/pcb/Mobo-v09.sch b/controller/hardware_design/pcb/Mobo-v09.sch index 712af5c138f3f6fbbd34eaaa66f4ecb1183303a5..8c062bebfc697acb41d630fb6845a72231a9ec20 100644 GIT binary patch delta 14637 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 L+sBqu;6Js0ZL@pFNddEsDjXc_ zuB^xYf8pbrY|LT8enEt>p5cPX<~)u#ex?f|lh27qg2g<6VtNvq%s{ctkLHsf!|;3)SlwnSk_1jc+XMKt=T)K+Zc| z9cG~5WEn#Vker#JA`^&?G`3`vff@9Ig^A(+|8T}Ykhwrm{{g6CoyoOQkjl1IiNBaG ziEMwlgmE>S)Fq(Zh7F986&pAiT<0(_go@gLG?-oznZ9l{W0^6sJjkdM(qMTXsJK+( zcW#EN?W_=Sf%;IO`1bHMjG=5GE7omdY*S(E*lv2AF^P%YKPR&+Gqq^?+?$L_EYG!* M3b!A=$;ikG0RBI8v;Y7A diff --git a/peripherals/nunchuk/hardware_design/pcb/nunchuk-v01.sch b/peripherals/nunchuk/hardware_design/pcb/nunchuk-v01.sch index b6f32080f52da1ed0b4e6f3f69d8dc5dfcd6a34a..6d6e97f62fa2a10fcd3fa21e08dc127c4c7c09f8 100644 GIT binary patch delta 10632 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 diff --git a/peripherals/nunchuk/makefile b/peripherals/nunchuk/makefile index f52299a..5dab69d 100755 --- a/peripherals/nunchuk/makefile +++ b/peripherals/nunchuk/makefile @@ -1,4 +1,4 @@ -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/midi_output.cc b/peripherals/nunchuk/midi_output.cc index 92dd518..9756b95 100755 --- a/peripherals/nunchuk/midi_output.cc +++ b/peripherals/nunchuk/midi_output.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/midi_output.h b/peripherals/nunchuk/midi_output.h index 777572e..313b944 100755 --- a/peripherals/nunchuk/midi_output.h +++ b/peripherals/nunchuk/midi_output.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/nunchuk.cc b/peripherals/nunchuk/nunchuk.cc index 808501c..980e76b 100755 --- a/peripherals/nunchuk/nunchuk.cc +++ b/peripherals/nunchuk/nunchuk.cc @@ -1,4 +1,4 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/ui.cc b/peripherals/nunchuk/ui.cc index 5bb3d29..020591e 100755 --- a/peripherals/nunchuk/ui.cc +++ b/peripherals/nunchuk/ui.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/ui.h b/peripherals/nunchuk/ui.h index 6c83ddf..625c520 100755 --- a/peripherals/nunchuk/ui.h +++ b/peripherals/nunchuk/ui.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/tools b/tools index d73c7b8..43cdf19 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit d73c7b84eb7238583396582cfedb94acedc6fd59 +Subproject commit 43cdf1925c0a49a95fd50b2505811c3a0b01f76a diff --git a/utils/compile_bank.py b/utils/compile_bank.py index c05833f..4486473 100755 --- a/utils/compile_bank.py +++ b/utils/compile_bank.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2012 Olivier Gillet. +# Copyright 2012 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/utils/convert_shruthi_data.py b/utils/convert_shruthi_data.py index 2462f0f..5517acc 100755 --- a/utils/convert_shruthi_data.py +++ b/utils/convert_shruthi_data.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/utils/convert_shruthi_syx.py b/utils/convert_shruthi_syx.py index 045543e..f4b56bb 100755 --- a/utils/convert_shruthi_syx.py +++ b/utils/convert_shruthi_syx.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/utils/list_bank.py b/utils/list_bank.py index 67b3dbf..26b05ff 100755 --- a/utils/list_bank.py +++ b/utils/list_bank.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2012 Olivier Gillet. +# Copyright 2012 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/voicecard/audio_out.cc b/voicecard/audio_out.cc index 7416b66..deb49d7 100755 --- a/voicecard/audio_out.cc +++ b/voicecard/audio_out.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/audio_out.h b/voicecard/audio_out.h index 7e62e44..124064e 100755 --- a/voicecard/audio_out.h +++ b/voicecard/audio_out.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/bootloader/bootloader.cc b/voicecard/bootloader/bootloader.cc index bfa810b..726ad7f 100755 --- a/voicecard/bootloader/bootloader.cc +++ b/voicecard/bootloader/bootloader.cc @@ -1,4 +1,4 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/bootloader/makefile b/voicecard/bootloader/makefile index bf8569c..23b3c8a 100755 --- a/voicecard/bootloader/makefile +++ b/voicecard/bootloader/makefile @@ -1,4 +1,4 @@ -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/voicecard/envelope.h b/voicecard/envelope.h index 4878ca3..d40b42b 100755 --- a/voicecard/envelope.h +++ b/voicecard/envelope.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/hardware_design/pcb/Voicecard-4P-v01.brd b/voicecard/hardware_design/pcb/Voicecard-4P-v01.brd index f9cb85d93e54ee0f4b0fd44e08fbf476a31d43f9..17d1225f7bcf3096b61dff24dac5e10d6d74ddca 100644 GIT binary patch delta 455 zcmZ2+opt$jRuzGUaE42K3=FKCEH*4$Q6{D}aeeQnJjfGd@Sobhw%NVoq`V0rAqsGWfuXE~$hvff-urvMgYRm$n9zEKm&t1A`ocJj1;I+y5VB@MK`9 zzX~$z3`lpsZUPH)0g!P^!;5LMjfn=BlWuaGMFJ=a)+oSW_FwA%DTZf2jbiL556@wGjs!R&JxZM+#(^VFTj{KOZ)R7hMbt_XKo+ zhSz_%**AIH4_BXAPZFcWCDR6e9N-+oX ziDgWiXK{owaR3?hS|Hwct`Cf0;a5Cs8NtFUK*IC+4nTyH1Y{YRPXa}n1brDffQ)*5 z5bwHB2Q!!#0^+5MW$=L&T~Y~U0yDJKWm&)sFKrDbS)dvQ1_n6>d4_raxBoxN;K{&H ze-&ib8IbON-2@iqQ$WTo4KJq2HYOTiPP)l$773szSfc=g*?+13rx>0AHJX8xMp@2a zgv;_W6#u{ezn4K1C>sNk<+2W71Q{|p)QO?)4+_#8t926Gj#dWRW| zOq<<*N@=h>0;<@as>tZ?#Q1Xi)?~&$CU*av%(BeXqUoM#jNvRcLgwAum!vT=vjPAK CubhVf diff --git a/voicecard/hardware_design/pcb/Voicecard-4P-v01.sch b/voicecard/hardware_design/pcb/Voicecard-4P-v01.sch index d7782a9747df6a61bc07297252618bf35cd3e5b6..77346d3b1ea897375a13223fd23af5db50ef2acb 100644 GIT binary patch delta 11044 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 N&T@&&9U(om{{l)_ULODe delta 11051 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 P)YQ#!4w1Pxq?hu)CMc6Q diff --git a/voicecard/hardware_design/pcb/Voicecard-SMR-v04.brd b/voicecard/hardware_design/pcb/Voicecard-SMR-v04.brd index 6afaa27cab30a9653e96a670e085b4e8b2719326..2d17a0735dab88044278888617460cf68c1f69b0 100644 GIT binary patch delta 453 zcmdmem37xuRuzGUaE4=i3=FKCEH*6jEKQ7=nKC!6*De-h@Sobhw%NVoq`r1$ieC1T6O0hFrm1Ie!!>tF`+HiLNfQW<<;ZL2gw znZOJt?X`@|azLp~I(wPHj3R>wCRv~e0|SE`gFM5$|EK>SWiVx6s1E=N*1Ln$J~tAD zsQqu+%nDb#i#?Q~hq0Pz9WziX5Y$ftshw}#yi)>Z94|xh|LgyI88m^i>p-$rU%6PM zL9&JojEoO@7#S>*85j=9*?_d428vIP|EUX;Ke0)Qjln09f#IHj4oLpNwQ8=(%YXL5 z)kl-WKtiSacPyMIn*S!Qa{^tMz+U6vG8!R+nZQyH0A0hmpY AwEzGB diff --git a/voicecard/hardware_design/pcb/Voicecard-SMR-v04.sch b/voicecard/hardware_design/pcb/Voicecard-SMR-v04.sch index 78ce5350c883eb2cc04e7a6a689cb8f2aaa2b7da..e19a30473e71702f82621478616be19c34b66d25 100644 GIT binary patch delta 11101 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 J_`-T<{|i_lD9``^ delta 11096 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 Ry!P5HpWnnCo}gap{{jyCSVRB- diff --git a/voicecard/hardware_design/pcb/Voicecard-SVF-v05.brd b/voicecard/hardware_design/pcb/Voicecard-SVF-v05.brd index bdd9dcb1704ef6a464f4d4a9961bcd6d35ea8f4d..265a889f71bf72907b63fc6ff68436f42f9e0e13 100644 GIT binary patch delta 438 zcmeyphWpnVZWV!sFow6X3=FKCEH*5=9vj_Q_03cE&Qk$F2LGx3Yn$CWP6}+>s1nV= zv}Nk%MvgEh_AOHx>$QNKdt3(@!JO4N!I--$X``7DIt`jO<&1 zT6DHf-TX}OAR`lyUMVaGrca9OV+Qd%CGwa+^iL@@Fs&#H)U*w#2}o?dCfCUf=I8)9 zX-b)VOhEdgK^PN=)-;~UC<9c2yfpHR3OL1z_6XY zpNW}~Y5UaeJQJAy@qrohnf9@P7-yC;`GKWmmNO}+Ga7HVJ;(HuiS1-=W=>}6blZze R<}684ANFjoxX8rJ3IHMVpveFL delta 439 zcmeyhhWqy#ZWV!sFow6X3=FKCEH*5w9vj_Q_099FL5;j1ga6e2waxAwCk3`{REg$b z;^Eoc$PvcG&cnl4uLb1X<2uL)=4A8mOrFPQwD|_leu%&tpzJ)piHsmEh63vt*?ECl zba;6-KNCF2$ONQU3d@1%lOp?=LHtgMJSGtRQ%Vg?E6M^j@c}gfiOtvKI+?*79Uv!7 zDU**0NMAGvV*=5d#xogZfGQan7~~krgj5*B7`mAfSb$t0sBhupX})K&{hkS<+HRog z?Q9vY6@^#bE~UG8F&6{=b(&6R39|P&PA%={BBEcd?EOs4 zj7YM12N7lW%2_{$t-75P-isRZhMaDClkAWPG(tVYSDC?i%jM$ P{?CKWx0hdJVrB&ZjYxOh diff --git a/voicecard/hardware_design/pcb/Voicecard-SVF-v05.sch b/voicecard/hardware_design/pcb/Voicecard-SVF-v05.sch index f806a85c95a394a5fbde965d423c6ad3c2e5fe08..9213fd36f5953de70d455be9d6a7fa1ee215b83a 100644 GIT binary patch delta 11185 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 N&|{SRtuUyk_J68Zmn8rI diff --git a/voicecard/hardware_design/pcb/Voicecard-Template.sch b/voicecard/hardware_design/pcb/Voicecard-Template.sch index d5f52afddb43140b2994f8e4f1f677b27ae12459..676ffe4de5e92579c4d7e62bbfaf5cbbbbbc4596 100644 GIT binary patch delta 10990 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 delta 10956 zcmZ|V30zd=-UskG=R7E9L^&ev3tYqOBFca>ZdU~r_XRT)GgCBNBlRjSxipkmrk3WQ zr$}>&(!Az2)G@VJQ_3X`uTqwhxKNg2YEc@RyuTmtoSVJ(wNHP4-~aPJ&zU*Q48zF! z9meN7j0huRn4@J2ha*52oyB6m@}lMKs@J&_Q`?caYWe#MdoMm}Ox_t06lk1w*nhl^ z6{j4&_`tYo_`CO@1heS`t(9*_V114KV}S`G@->7I{j8X=65*yEC7x#pcSeb241=9r zg^A!_>2xY0^mVk>*N76$8BQX!MpzjXB|L1n5V|s?AoN4L{fGtq`W=v;Qw0{rjOxsP0o{Z$}OTOeO z6=KBuNZwAkX;Ybcciyk)i;%nvP<+8%>uNjSKV@#OsWtTt(1V3{ca_YoL7JiMmup+O z_L)?nZfGYYf}Do-r9@}o+bWoAYHB#@yGlm+-j0hA*Kt-eB}i-Hi>eXq)8fa7QoFOS zh9T}!Cu`vgdMG|dsN3Z9fFMKY6$;j(gdzLp)?h8zx3F7`XkfR3474tz6=t{6*y_AHoo)(LkzW*13hW0 z02*gY^fbgXw%pK6Td6$_ZGP3cd|ALt5_!N%iBe##L_{w`ykqZ>1Z}mI3Hfd1_A<0R z^04Aw)3k#U>Ael@3yB=y8;OG6hA6go5Pb}-L@v33I})jV454k8U!^Q8Ik#6mobrHr z5~VN|^peO0hDa0xqa;H68QNrt1Ynj#I`D!-4)Cf(0gx>r`k&M` zN|g3DwD%+;1{m505=p=(5}CjWiCo|tiQ)mcSoTE-9cYLWTL}XVtyB)C19v5I0I{R$ zVif>2B*Y*?3z2XG4JA^6<`P*zD~UY7BT)*(Nkk+XT3?AI;4z6z;7N&GV1h(3Fhe4A zu%US+5`d)=>3}Jb1H3I!0Bn~Kj~d!u2{&+9A{96-kp&b<Jb!eGJcRi3FgJL^|-8L=G@Yq5zmAA(9L&UBV4`B~pPU5?R11 zi9BF^75t@1DT?^lPWiM+A7+SMQvRG_SllD&ix_T*L$;Ej6tvu*)TM=X8MT)IVXo9U`Xo{_bvGHQ2tsH2+E%8*mSSaOBeQJdwmPqXwsN4&whEvfw#0Zt?6&2G4%kYC4%^Ct@@?foXKa;1MYbX)7~+bp zBYq9t-vK4ErsFbmF2lgwr_+N)4{8VXE0joG zpjylb7Ee${ZD{C>U_A}$oKf2mV*ah3sN)Oo5#U>d_WTFiJ7$FH%N}gG;)JhwhM}+d zzwOX;L)$8ETmrCLA|1#B@LsZ+@168u?W9~Oz)GQnm}zJ~NVtJgiB!PxVbw**0)iy+ zfCdt!K!ilZEJJfiBmtczGJ!r4xj>RcF_0<|I@{2uNhARCCDMV#5;;JYL;^EmQ40JZ5%DZ8h(r?Lu&ORdCJ-c%3p9`@1|lRv=i-7$BmkWy z(t$n_IY5#`0gxgg<{4U=gd3P6kqTr=WC2Sh@_>~RrNA3isMAlef5K((u9s6u^9^m2 zL?*CJA{Y2Tq8Qj$1%K#*U7EOK;puZM13y%T>_(dV0#X!HWF}HCrn8H-D*8kuZwk5m zr7$&cH@Rc?Dc^m6JAcW`R~6m)5jrZ0zi4Tgs;7{{QG7*9J^s$GURA_)E=I1%Q=BD8 zNlCE@?g2j6RhMtt2H}rd8LR4l-&5K5_@Y;M{A;pCC}KQX{<2kBiu3KN#dehkg})o< zKWVm6ofoS5?frdj{mZtjRCVLy%3eob zTtP>F%KO6Rdx+ zp*V22UH*wDnyLC__Reu+c|JZ-h&%u`NAU&cbo7Uxs;BDzWNQ+NKdzv~&&#?!i;FaOL%|HSiO2r&^Yf2Ye>+=k?R7sYq`hjMX^{jwkN=z8QJ z(w1vTU4iifZjL{ z=c$8Y6=}+BOf!xMP;vvDs}Xy~dRlkH2MYm;+Zs?qnHwNZbKsbvVCUdZ5&MNG*l-Zz z)%uLDFfM7zc$_ivA;ub?As%~#u^;2)HjFPbI^B#%7`Jp(?gfbIhZ*A-dsW1qjEgE_ zf5yCu_$Z=Y;|OlQhGK0`RyHZ3KjVnU6}A|Ik?}TT>{Q;0w#OJ}GES|C z^OPovo{Q++N+-|91=Y*wpXoCT=nkLbT-{?`3FeIqoJ>U;@*@jQt(845k2n$6f5y11)q{tI|2Vu<<5%L zW2y2sKA>WE9*T8iS;f>B@VzQy>hg+;DR$?fSj$%`c#Z_-!}sags}!%Qzf5~kta6j< zK__7q#k!JBFQ*sM`D+z?EEfK6I26Siy+LWAnETc%+4@d4->GV1sQW3n9g4YbyW+7% zzKySu{w147QLGy|+-c%zcqB^IPKBGW(_J=iu<6~*p*aO`8j96>3;iqoBOSbzp8h2~ z9>qHIK7E6}OykUO+E;K2iZx*;{UiM?9qp&bpMgi9&>tw~)(;h@ewocPZ0`RML(a)x z!=q5}*2uz>XWwq#pPTG{hhin}QR*xDkZ<7LDAv@yw4dHgFW*PkJqtTgtnDAuv*@Sk z{Qsf9rccl}_R}5D!EI2i;7{mX^d>s`ARSoiUb&S4FU!>QaP=YnR>v=d9#j;K+Ej4{Py^y|gT8Y#23v>Y; zeMU*v^q3;J1&X!&EY_W~5cy5dV}GBrg3c?ks&g*~{U{FKj%(r8#0v_(aopj72QT7T zDBiz-cA1#G2?Z{=HD@HyoA{Od&cC;h-0rX1{EW|@B^ZL64Cn!ikW{)nP)b? zszm6a-=P5 z_a-*A$jxNMUD$XFci}F3B~`t3di)Qd3xy+n%>zFxZ|RHJoB3exD0_R@OD@I9YyJq{ zw?|g|io*u8*B!--zJskYep80&S?n!hujn`2ps&fJ_Q=WG7-@PHy_zUI?20~y972Zu z4(5=n$>1_Cu9I>L>V76ssse4O8`B=!W$B3?2mpi(>xnir@#614F8fYEwqDHFv4J>)D zaI`#5#XG(5-{1x)R&sY0-)0}B`_jQZ>F4QZQOw{us>i(7TixaKv-d9)hdKBks@p8- zgXTFH=WPGrIs5XQHE&}+0L3cr&vSO6J#_LQdKx{E-aeRqn|_n7{unHdBbpJ)>Is z5+qm0*w|raT)x`W>^xR&V6K^>9@FzUm&>`SQ`LOE_FYgxF?&w~N0I}`uz#xM`g`OX zWXtJbDOpTL&j7pK17lFk_UUT5n2%(2nyDUFMGl+JIch=R7f7zQu8wA{Ichtz$+PMu zafgfQTy>x5@du7LI2SixTx2Yp$LPI}7`s4C#Z#ahG5a}0eJ|p$q;4L!x$Zf&qZOZ_ zK8zo(9H(*rpU1lMHX_&CIabeC-B!>-H4=|)<+Q`>l&KczKO%WAlA{-^L(Q~BxVoKG z^kPxW#20bjmy!!ntd=jS(TZM37tlBaZmq#BP&n=}kH4&D>(8+_i@m8!)O_b*q#-e} zF&!Mf3yWOloTchpB1&L~M$7nA>%uts6~;k~dtPN6&FEZ#7%&5|hgGA2+FucwY~v?G zK1CVJe2nih_FKhhF=nr3Ji>U@Wc-G)(;JLe8JA=;-eo-dCSy`WXrGGrxu4M=p_t`|)KAT{{c4$B zT}Q8sGgA+6rWf52g)>K2A5wqQ7qK-5#X<}3gg>J{Lczy^ALAimbiO^b;{=DoYoJ*N#llk_L*wY-D0~UP zGv?nM+QQ~q4&|TX&@c3*sv*3772pL)?;eDv2gS_)8ZT;{&+vvWVsj3Pg(i+XLhr2_ z>S&!j%k68_MDsp}Mi+8u5Zw)hJ9V^D&U5HBHZxJI8$}%YiayFA7!RFKU*S**UBsbvS21M#$f1~EG$T+fG%+-ro{UmCw7!Hx zAG5iGL(xBRNW(9Lcz@&2=v(UhX3kB#*7s$vGm3@YXgo3K7f?8})132*x=sI*y(657 zF6BOrYs0lrOt_=v`i&aR?6t3`K>&VJMY1d+;9| zn$G4p6szbyhQz-RS8db^#9a=~Rgn7EK^meWy~z}*Azfoq1=2r4khbdG6B zHKfS8NMoxbJ&R;@3eS~iT^c*o6j!_YrZpVdXisRj= zaDK)(7kzVG4Q-u1{Grp10X-A&a|(VXMC@pVK7LDMq}a`LAV^y#_A?ExiQm5Xj47k0 zRw7O?O{;}o0n>V>b4>URjwtax(rW&?#&xz_OusUvFx_Wb!Bi~@=>U_#giraSL{p}4 z1F01g{xLa9v}M9)`%xmE37_jniGfV`Og~DDWWwk9QDPEPJkub+p3C((Y4Mt)bMt! z4+nZCv>iB*11@V~6Rn!+99!A-$nNH5T0O-%pWVlXVz zbaH}r22T)rGK$r{msVfsU(rYDuyP;UK`_m`rk7#p%HbK>+-Eeaht9l|H z=z1nSo38&T_b;UL=^IJfM~b;4No%Kf@Sxceg-x#-*rgrpYIoY4GE7Uit|n`EKhw26 zdL9%LKazQ@WshqiLSM{ghTSZDQk$&lU$S`u#Y!Bb%~te#^i4W{EOxK{Y=_f-{w=k7 z;#D-P>~WkeM)c;Rn2jcAhrKD9qjA})TgjfO4P*Il0Qj>O|Hot~qc)Fv*s=LwY5AI% zaCE#WT`Hc?mHiI)ev$o2f9@ag*!?-|cgRp2nnQ6Y3V!Nmqw0?BVyh5 zEy4b-ZB6h|#r0781o!S$9)O$UXstMU#9-?fmun`^;KBjCK|g$3??3aU&WaPrd0y|o z&Ofj}=EIJJJIcM~UPlK0!`p=NH0_cSXkK~-kMzY>tV$H z(`CKG(V*hq*sY4de!c(FZ@#|m6TFY{xpgbMfn|9M||z6bx! z>OZ%SUwdvF-mSiU`$GTA{R}%`E}5$>_g0+dlRAqVw+j91X?WTGzkBsaUY&~5q_hmR zPgD25ohHNa=dYY&Py`EXR6 brKOEacT7z)&(#%?V#WB;!z{g?sIUGnGX}3z diff --git a/voicecard/leds.cc b/voicecard/leds.cc index e243b1d..bbb1d6f 100755 --- a/voicecard/leds.cc +++ b/voicecard/leds.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/leds.h b/voicecard/leds.h index 4019518..955022c 100755 --- a/voicecard/leds.h +++ b/voicecard/leds.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/makefile b/voicecard/makefile index e9bcf01..de31bc4 100755 --- a/voicecard/makefile +++ b/voicecard/makefile @@ -1,4 +1,4 @@ -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/voicecard/oscillator.cc b/voicecard/oscillator.cc index a1f22e1..a4fbb08 100755 --- a/voicecard/oscillator.cc +++ b/voicecard/oscillator.cc @@ -1,6 +1,6 @@ -// Copyright 2012 Olivier Gillet. +// Copyright 2012 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/oscillator.h b/voicecard/oscillator.h index 32b4e99..c8ec8b2 100755 --- a/voicecard/oscillator.h +++ b/voicecard/oscillator.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/resources.cc b/voicecard/resources.cc index 1575e04..2684085 100644 --- a/voicecard/resources.cc +++ b/voicecard/resources.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/resources.h b/voicecard/resources.h index 4084d94..3913672 100644 --- a/voicecard/resources.h +++ b/voicecard/resources.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/resources/lookup_tables.py b/voicecard/resources/lookup_tables.py index 7832e49..471cb31 100755 --- a/voicecard/resources/lookup_tables.py +++ b/voicecard/resources/lookup_tables.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/voicecard/resources/resources.py b/voicecard/resources/resources.py index e39dd93..d1cf5d2 100755 --- a/voicecard/resources/resources.py +++ b/voicecard/resources/resources.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,9 +19,9 @@ # # Master resources file. -header = """// Copyright 2011 Olivier Gillet. +header = """// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/resources/waveforms.py b/voicecard/resources/waveforms.py index 91f2b23..6cfa774 100755 --- a/voicecard/resources/waveforms.py +++ b/voicecard/resources/waveforms.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/voicecard/sub_oscillator.h b/voicecard/sub_oscillator.h index fd6f0e7..21463fe 100755 --- a/voicecard/sub_oscillator.h +++ b/voicecard/sub_oscillator.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/transient_generator.h b/voicecard/transient_generator.h index 03c400b..bc63153 100755 --- a/voicecard/transient_generator.h +++ b/voicecard/transient_generator.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/voice.cc b/voicecard/voice.cc index d2c1812..73740ad 100755 --- a/voicecard/voice.cc +++ b/voicecard/voice.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/voice.h b/voicecard/voice.h index 4d9c82a..8713fc0 100755 --- a/voicecard/voice.h +++ b/voicecard/voice.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/voicecard.cc b/voicecard/voicecard.cc index 73f815c..fb67055 100644 --- a/voicecard/voicecard.cc +++ b/voicecard/voicecard.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/voicecard.h b/voicecard/voicecard.h index a267e9d..801853f 100644 --- a/voicecard/voicecard.h +++ b/voicecard/voicecard.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/voicecard_rx.cc b/voicecard/voicecard_rx.cc index f7b5c4a..754e9d4 100755 --- a/voicecard/voicecard_rx.cc +++ b/voicecard/voicecard_rx.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/voicecard_rx.h b/voicecard/voicecard_rx.h index 290305a..461dbea 100755 --- a/voicecard/voicecard_rx.h +++ b/voicecard/voicecard_rx.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by From c93e98f0bf5a7708bcdc357f710e5f92779a0d90 Mon Sep 17 00:00:00 2001 From: Marshall Conover Date: Fri, 5 Apr 2019 19:20:11 -0400 Subject: [PATCH 02/67] Add build instructions to the README. --- README | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/README b/README index 890c5cd..f6df6d9 100644 --- a/README +++ b/README @@ -1,4 +1,5 @@ -Ambika, a hybrid MIDI polysynth and voicecard host. +#Ambika +## A hybrid MIDI polysynth and voicecard host. Ambika consists of a compact motherboard serving as a "host" for up to 6 sound synthesis voicecard. While this design is primarily intended to be a flexible hybrid polysynth, it could also be used as a drum module/drum machine. @@ -11,3 +12,34 @@ Original developer: Emilie Gillet (emilie.o.gillet@gmail.com) The firmware is released under a GPL3.0 license. It includes a variant of the formant synthesis algorithm used in Peter Knight's Cantarino speech synthesizer. The PCB layouts and schematics, documentation, analyses, simulations and 3D models are released under a Creative Commons cc-by-sa 3.0 license. + +# BUILD + +You'll need: +- make +- gcc-avr +- avr-libc +- avrdude +- python + +On Ubuntu: +``` + sudo apt-get install gcc-avr make avr-libc +``` + +Next, you'll need to grab the projects this repo depends on. +``` + git submodule update --init +``` + +Once you've got that all settled, you'll need to change the path to `avr-gcc` in avrlib/makefile.mk. + +``` + #remeber the path given by this command + which avr-gcc + + avrlib/makefile.mk + replace the old path in the line 'AVRLIB_TOOLS_PATH ?= ' with the output from "which avr-gcc" you got above +``` + +Finally, run `make` From 753466584f2d798bdb465b7083e429f2f1e932ee Mon Sep 17 00:00:00 2001 From: Marshall Conover Date: Fri, 5 Apr 2019 19:22:46 -0400 Subject: [PATCH 03/67] Rename readme to take advantage of markup. --- README => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename README => README.md (100%) diff --git a/README b/README.md similarity index 100% rename from README rename to README.md From 05bb001b5e0be96176a803b4e4bf59bc9ed2d0a2 Mon Sep 17 00:00:00 2001 From: Marshall Conover Date: Fri, 5 Apr 2019 19:43:50 -0400 Subject: [PATCH 04/67] Add commands to set the correct AVR path. --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f6df6d9..209bf4d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -#Ambika +# Ambika ## A hybrid MIDI polysynth and voicecard host. Ambika consists of a compact motherboard serving as a "host" for up to 6 sound synthesis voicecard. While this design is primarily intended to be a flexible hybrid polysynth, it could also be used as a drum module/drum machine. @@ -13,7 +13,7 @@ The firmware is released under a GPL3.0 license. It includes a variant of the fo The PCB layouts and schematics, documentation, analyses, simulations and 3D models are released under a Creative Commons cc-by-sa 3.0 license. -# BUILD +# Build You'll need: - make @@ -32,14 +32,13 @@ Next, you'll need to grab the projects this repo depends on. git submodule update --init ``` -Once you've got that all settled, you'll need to change the path to `avr-gcc` in avrlib/makefile.mk. +Once you've got that all settled, you'll need to change the path to `avr-gcc` in avrlib/makefile.mk +to match the path on your system. ``` - #remeber the path given by this command - which avr-gcc - - avrlib/makefile.mk - replace the old path in the line 'AVRLIB_TOOLS_PATH ?= ' with the output from "which avr-gcc" you got above + export AVRPATH=`which avr-gcc` + sed "s|AVRLIB_TOOLS_PATH ?=.*|AVRLIB_TOOLS_PATH \?= `dirname $AVRPATH`|" avrlib/makefile.mk > mkfiletmp + mv mkfiletmp avrlib/makefile.mk ``` Finally, run `make` From aa941c9dd1698073465071cf94c027a041a5127e Mon Sep 17 00:00:00 2001 From: Marshall Conover Date: Fri, 5 Apr 2019 19:45:16 -0400 Subject: [PATCH 05/67] Add a period for proper grammar. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 209bf4d..63f3709 100644 --- a/README.md +++ b/README.md @@ -41,4 +41,4 @@ to match the path on your system. mv mkfiletmp avrlib/makefile.mk ``` -Finally, run `make` +Finally, run `make`. From dc1b5193b8864aff28c17d0a0559aa008ea51c9e Mon Sep 17 00:00:00 2001 From: Marshall Conover Date: Fri, 5 Apr 2019 20:05:13 -0400 Subject: [PATCH 06/67] Correct sed missing forward slash, add detail to bin files. --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 63f3709..d267303 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,17 @@ to match the path on your system. ``` export AVRPATH=`which avr-gcc` - sed "s|AVRLIB_TOOLS_PATH ?=.*|AVRLIB_TOOLS_PATH \?= `dirname $AVRPATH`|" avrlib/makefile.mk > mkfiletmp + sed "s|AVRLIB_TOOLS_PATH ?=.*|AVRLIB_TOOLS_PATH \?= `dirname $AVRPATH`/|" avrlib/makefile.mk > mkfiletmp mv mkfiletmp avrlib/makefile.mk ``` -Finally, run `make`. +Then, for `elf` files: + +``` +make all +``` + +And for `bin` files: +``` +make bin +``` From 0d24db4a38fade634f4f9e9ba1bd3f0850e4e92b Mon Sep 17 00:00:00 2001 From: Marshall Conover Date: Fri, 5 Apr 2019 20:50:42 -0400 Subject: [PATCH 07/67] Add instructions for the motherboard. --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d267303..441513d 100644 --- a/README.md +++ b/README.md @@ -41,13 +41,23 @@ to match the path on your system. mv mkfiletmp avrlib/makefile.mk ``` -Then, for `elf` files: +Then, for voice card `elf` files: ``` -make all + make all ``` -And for `bin` files: +And for voice card `bin` files: ``` -make bin + make bin +``` + +For motherboard `elf` files: +``` + make bootstrap_controller +``` + +For motherboard `bin` files: +``` + make -f controller/makefile bin ``` From c49ccea68a2c68afe6bb5b7d1acdf37ac66b708e Mon Sep 17 00:00:00 2001 From: stephane conversy Date: Thu, 24 Oct 2019 21:28:02 +0200 Subject: [PATCH 08/67] replace overlapping structures with pointers for latest avr-gcc --- voicecard/voice.cc | 9 ++------- voicecard/voice.h | 13 +++++++------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/voicecard/voice.cc b/voicecard/voice.cc index 73740ad..b3ec5aa 100755 --- a/voicecard/voice.cc +++ b/voicecard/voice.cc @@ -39,10 +39,10 @@ TransientGenerator transient_generator; /* */ -uint8_t Voice::patch_data_[1]; Patch Voice::patch_; -uint8_t Voice::part_data_[1]; +uint8_t *Voice::patch_data_ = reinterpret_cast (&Voice::patch_); Part Voice::part_; +uint8_t *Voice::part_data_ = reinterpret_cast (&Voice::part_); Lfo Voice::voice_lfo_; Envelope Voice::envelope_[kNumEnvelopes]; @@ -104,11 +104,6 @@ static const prog_Patch init_patch PROGMEM = { 0, 0, 0, 0, 0, 0, 0, 0, }; -/* static */ -void Voice::set_patch_data(uint8_t address, uint8_t value) { - patch_data_[address + 1] = value; -} - /* static */ void Voice::Init() { pitch_value_ = 0; diff --git a/voicecard/voice.h b/voicecard/voice.h index 8713fc0..5954150 100755 --- a/voicecard/voice.h +++ b/voicecard/voice.h @@ -81,13 +81,14 @@ class Voice { modulation_sources_[i] = value; } - static void set_patch_data(uint8_t address, uint8_t value); + static void set_patch_data(uint8_t address, uint8_t value) { + patch_data_[address] = value; + } static void set_part_data(uint8_t address, uint8_t value) { - part_data_[address + 1] = value; + part_data_[address] = value; } - static uint8_t* mutable_patch_data() { - return &patch_data_[1]; + return patch_data_; } static const Patch& patch() { return patch_; } @@ -104,9 +105,9 @@ class Voice { static inline void UpdateDestinations() __attribute__((always_inline)); static inline void RenderOscillators() __attribute__((always_inline)); - static uint8_t patch_data_[1]; + static uint8_t *patch_data_; static Patch patch_; - static uint8_t part_data_[1]; + static uint8_t *part_data_; static Part part_; // Envelope generators. From 27234cf20d2d7639313150e58fa5bb53d8f6f31b Mon Sep 17 00:00:00 2001 From: conversy Date: Fri, 15 Nov 2019 22:36:00 +0100 Subject: [PATCH 09/67] suppress undefined behaviour warning --- voicecard/sub_oscillator.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/voicecard/sub_oscillator.h b/voicecard/sub_oscillator.h index 21463fe..fe1d76b 100755 --- a/voicecard/sub_oscillator.h +++ b/voicecard/sub_oscillator.h @@ -53,7 +53,8 @@ class SubOscillator { uint8_t tri = phase_.integral >> 7; v = phase_.integral & 0x8000 ? tri : ~tri; } - *buffer++ = U8Mix(*buffer, v, mix_gain, sub_gain); + *buffer = U8Mix(*buffer, v, mix_gain, sub_gain); + ++buffer; } } From 04525fa5f76f08fee1f0fbcbc657318b04c63886 Mon Sep 17 00:00:00 2001 From: conversy Date: Fri, 15 Nov 2019 22:37:29 +0100 Subject: [PATCH 10/67] suppress undefined behaviour warning --- voicecard/transient_generator.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/voicecard/transient_generator.h b/voicecard/transient_generator.h index bc63153..ea24d47 100755 --- a/voicecard/transient_generator.h +++ b/voicecard/transient_generator.h @@ -45,7 +45,8 @@ class TransientGenerator { while (counter_ && size--) { uint8_t value = (*fn)(); uint8_t amplitude = U8U8MulShift8(gain_, amount); - *buffer++ = U8Mix(*buffer, value, amplitude); + *buffer = U8Mix(*buffer, value, amplitude); + ++buffer; } } From 669bcc825222f53518bee2130a99c9da279eacc3 Mon Sep 17 00:00:00 2001 From: conversy Date: Mon, 18 Nov 2019 19:40:30 +0100 Subject: [PATCH 11/67] comment apparent out-of-bounds loop formant_amplitude[3] is an alias of noise_modulation --- voicecard/oscillator.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/voicecard/oscillator.cc b/voicecard/oscillator.cc index a4fbb08..efd4e28 100755 --- a/voicecard/oscillator.cc +++ b/voicecard/oscillator.cc @@ -307,6 +307,7 @@ void Oscillator::RenderVowel(uint8_t* buffer) { } // Interpolate formant amplitudes. + // formant_amplitude[3] is an alias of noise_modulation for (uint8_t i = 0; i < 4; ++i) { uint8_t amplitude_a = ResourcesManager::Lookup( wav_res_vowel_data, @@ -501,4 +502,4 @@ const Oscillator::RenderFn Oscillator::fn_table_[] PROGMEM = { &Oscillator::RenderInterpolatedWavetable }; -} // namespace \ No newline at end of file +} // namespace From f966d621a5eb8c8223a26e04054ed5afe9742759 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Sat, 18 Apr 2026 13:48:46 +0200 Subject: [PATCH 12/67] Carcosa v1.4 custom firmware Replaces the stock Ambika oscillator set with a focused synthesis engine: saw, square, triangle, sine, pad, filtered noise, 4-op FM, Karplus-Strong plucked string, waveshaping, and west coast wavefolder. New synthesis engines with dedicated UI pages: - 4-op FM with 8 TX81Z algorithms, 8 waveforms per operator, feedback - Karplus-Strong with 4 excitation types, 3-read-head ensemble, stiffness - West coast wavefolder with variable fold stages, FM, self-sync, sub osc - Waveshaping through distortion table with variable drive Envelope system expanded with per-envelope curve selection (exponential, linear, looping, looping linear). Analog slop parameter adds per-note random pitch and envelope variation for vintage poly character. Smart patch randomizer with per-section depth control. Step sequencer removed to free controller flash. S1+encoder navigation for synthesis mode switching. Bug fixes: note stack uninitialized variables, transient generator off-by-one, SPI bulk write bounds check, page group init size. --- .gitignore | 10 + common/patch.h | 37 ++- controller/note_stack.h | 7 +- controller/parameter.cc | 64 ++++- controller/parameter.h | 3 +- controller/part.cc | 56 +--- controller/part.h | 10 +- controller/ui.cc | 86 ++++-- controller/ui.h | 8 +- controller/ui_pages/fm_page.cc | 369 +++++++++++++++++++++++++ controller/ui_pages/fm_page.h | 60 ++++ controller/ui_pages/ks_page.cc | 248 +++++++++++++++++ controller/ui_pages/ks_page.h | 41 +++ controller/ui_pages/os_info_page.cc | 5 +- controller/ui_pages/parameter_editor.h | 2 +- controller/ui_pages/randomizer.cc | 253 +++++++++++++++++ controller/ui_pages/randomizer.h | 66 +++++ controller/ui_pages/wc_page.cc | 247 +++++++++++++++++ controller/ui_pages/wc_page.h | 35 +++ flash_sd.sh | 68 +++++ voicecard/envelope.h | 37 ++- voicecard/fm4op.h | 344 +++++++++++++++++++++++ voicecard/karplus.h | 267 ++++++++++++++++++ voicecard/oscillator.cc | 301 ++------------------ voicecard/oscillator.h | 40 +-- voicecard/transient_generator.h | 2 +- voicecard/voice.cc | 238 +++++++++++++--- voicecard/voice.h | 23 ++ voicecard/voicecard_rx.h | 3 + voicecard/westcoast.h | 231 ++++++++++++++++ 30 files changed, 2707 insertions(+), 454 deletions(-) create mode 100644 controller/ui_pages/fm_page.cc create mode 100644 controller/ui_pages/fm_page.h create mode 100644 controller/ui_pages/ks_page.cc create mode 100644 controller/ui_pages/ks_page.h create mode 100644 controller/ui_pages/randomizer.cc create mode 100644 controller/ui_pages/randomizer.h create mode 100644 controller/ui_pages/wc_page.cc create mode 100644 controller/ui_pages/wc_page.h create mode 100755 flash_sd.sh create mode 100644 voicecard/fm4op.h create mode 100644 voicecard/karplus.h create mode 100644 voicecard/westcoast.h diff --git a/.gitignore b/.gitignore index 936ff75..a8bebd5 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,13 @@ # OS X crap .DS_Stor? + +# Build artifacts +build/ +*.o +*.elf +*.hex +*.bin +*.syx +*.mid +__pycache__/ diff --git a/common/patch.h b/common/patch.h index 33d140e..4713fb5 100755 --- a/common/patch.h +++ b/common/patch.h @@ -37,6 +37,14 @@ struct FilterSettings { uint8_t mode; }; +enum EnvelopeCurve { + ENVELOPE_CURVE_EXPONENTIAL, + ENVELOPE_CURVE_LINEAR, + ENVELOPE_CURVE_LOOP, + ENVELOPE_CURVE_LOOP_LINEAR, + ENVELOPE_CURVE_LAST +}; + struct EnvelopeLfoSettings { uint8_t attack; uint8_t decay; @@ -44,7 +52,7 @@ struct EnvelopeLfoSettings { uint8_t release; uint8_t shape; uint8_t rate; - uint8_t padding; + uint8_t envelope_curve; uint8_t retrigger_mode; }; @@ -65,25 +73,12 @@ enum OscillatorAlgorithm { WAVEFORM_SQUARE, WAVEFORM_TRIANGLE, WAVEFORM_SINE, - WAVEFORM_CZ_SAW, - WAVEFORM_CZ_SAW_LP, - WAVEFORM_CZ_SAW_PK, - WAVEFORM_CZ_SAW_BP, - WAVEFORM_CZ_SAW_HP, - WAVEFORM_CZ_PLS_LP, - WAVEFORM_CZ_PLS_PK, - WAVEFORM_CZ_PLS_BP, - WAVEFORM_CZ_PLS_HP, - WAVEFORM_CZ_TRI_LP, WAVEFORM_QUAD_SAW_PAD, - WAVEFORM_FM, - WAVEFORM_8BITLAND, - WAVEFORM_DIRTY_PWM, WAVEFORM_FILTERED_NOISE, - WAVEFORM_VOWEL, - WAVEFORM_WAVETABLE_1, - WAVEFORM_WAVETABLE_16 = WAVEFORM_WAVETABLE_1 - 1 + 16, - WAVEFORM_WAVEQUENCE, + WAVEFORM_FM4OP, + WAVEFORM_KS_PLUCK, + WAVEFORM_WAVESHAPE, + WAVEFORM_WESTCOAST, WAVEFORM_LAST }; @@ -315,7 +310,7 @@ enum PatchParameter { PRM_PATCH_ENV_RELEASE, PRM_PATCH_LFO_SHAPE, PRM_PATCH_LFO_RATE, - PRM_PATCH_LFO_ATTACK, + PRM_PATCH_ENV_CURVE, PRM_PATCH_LFO_SYNC, PRM_PATCH_VOICE_LFO_SHAPE = 48, @@ -327,7 +322,9 @@ enum PatchParameter { PRM_PATCH_MOD_OPERAND1 = 92, PRM_PATCH_MOD_OPERAND2, - PRM_PATCH_MOD_OPERATOR + PRM_PATCH_MOD_OPERATOR, + + PRM_PATCH_SLOP = 105 }; } // namespace ambika diff --git a/controller/note_stack.h b/controller/note_stack.h index 8e1e556..5bc853f 100755 --- a/controller/note_stack.h +++ b/controller/note_stack.h @@ -62,7 +62,7 @@ class NoteStack { // In case of saturation, remove the least recently played note from the // stack. if (size_ == capacity) { - uint8_t least_recent_note; + uint8_t least_recent_note = pool_[1].note; for (uint8_t i = 1; i <= capacity; ++i) { if (pool_[i].next_ptr == 0) { least_recent_note = pool_[i].note; @@ -71,7 +71,7 @@ class NoteStack { NoteOff(least_recent_note); } // Now we are ready to insert the new note. Find a free slot to insert it. - uint8_t free_slot; + uint8_t free_slot = 1; for (uint8_t i = 1; i <= capacity; ++i) { if (pool_[i].note == kFreeSlot) { free_slot = i; @@ -151,6 +151,9 @@ class NoteStack { return pool_[current]; } const NoteEntry& played_note(uint8_t index) const { + if (size_ == 0) { + return pool_[0]; + } uint8_t current = root_ptr_; index = size_ - index - 1; for (uint8_t i = 0; i < index; ++i) { diff --git a/controller/parameter.cc b/controller/parameter.cc index 8d76f5d..11e3023 100755 --- a/controller/parameter.cc +++ b/controller/parameter.cc @@ -57,6 +57,8 @@ static const prog_uint16_t units_definitions[UNIT_LAST] PROGMEM = { STR_RES_SWING, // UNIT_GROOVE_TEMPLATE STR_RES_____, // UNIT_MIDI_IN_MASK STR_RES_THRU, // UNIT_MIDI_OUT_MODE + STR_RES_OMNI, // UNIT_MIDI_CHANNEL + 0, // UNIT_ENVELOPE_CURVE (handled as special case) }; static const prog_char note_names[] PROGMEM = " CC# DD# E FF# GG# AA# B"; @@ -216,6 +218,43 @@ void Parameter::PrintValue(uint8_t value, char* buffer, uint8_t width) const { } } + // Envelope curve type. + if (unit == UNIT_ENVELOPE_CURVE) { + const prog_char* name; + switch (value) { + case ENVELOPE_CURVE_EXPONENTIAL: name = PSTR("exp"); break; + case ENVELOPE_CURVE_LINEAR: name = PSTR("lin"); break; + case ENVELOPE_CURVE_LOOP: name = PSTR("loop"); break; + case ENVELOPE_CURVE_LOOP_LINEAR: name = PSTR("lpln"); break; + default: name = PSTR("exp"); break; + } + strncpy_P(buffer, name, width); + AlignRight(buffer, width); + return; + } + + // Carcosa waveform names. + if (unit == UNIT_OSCILLATOR_WAVEFORM) { + const prog_char* name; + switch (value) { + case WAVEFORM_NONE: name = PSTR("none"); break; + case WAVEFORM_SAW: name = PSTR("saw"); break; + case WAVEFORM_SQUARE: name = PSTR("square"); break; + case WAVEFORM_TRIANGLE: name = PSTR("triangle"); break; + case WAVEFORM_SINE: name = PSTR("sine"); break; + case WAVEFORM_QUAD_SAW_PAD: name = PSTR("pad"); break; + case WAVEFORM_FILTERED_NOISE: name = PSTR("noise"); break; + case WAVEFORM_FM4OP: name = PSTR("fm4op"); break; + case WAVEFORM_KS_PLUCK: name = PSTR("pluck"); break; + case WAVEFORM_WAVESHAPE: name = PSTR("wshape"); break; + case WAVEFORM_WESTCOAST: name = PSTR("wcoast"); break; + default: name = PSTR("?"); break; + } + strncpy_P(buffer, name, width); + AlignRight(buffer, width); + return; + } + if (text) { ResourcesManager::LoadStringResource(text + value, buffer, width); } else if (unit != UNIT_NOTE) { @@ -226,6 +265,11 @@ void Parameter::PrintValue(uint8_t value, char* buffer, uint8_t width) const { } void Parameter::PrintName(char* buffer, uint8_t width) const { + if (offset == PRM_PATCH_SLOP) { + strncpy_P(buffer, PSTR("slop"), width); + AlignLeft(buffer, width); + return; + } ResourceId caption = (width <= 6) ? short_name : long_name; ResourcesManager::LoadStringResource(caption, buffer, width); AlignLeft(buffer, width); @@ -356,7 +400,7 @@ static const prog_Parameter parameters[kNumParameters] PROGMEM = { // 0 { PARAMETER_LEVEL_PATCH, PRM_PATCH_OSC1_SHAPE, - UNIT_OSCILLATOR_WAVEFORM, WAVEFORM_NONE, WAVEFORM_LAST - 1, + UNIT_OSCILLATOR_WAVEFORM, WAVEFORM_NONE, WAVEFORM_FILTERED_NOISE, 1, 0, 0xff, 16, STR_RES_WAVEFORM, STR_RES_WAVEFORM, STR_RES_OSCILLATOR_1 }, @@ -384,7 +428,7 @@ static const prog_Parameter parameters[kNumParameters] PROGMEM = { // 4 { PARAMETER_LEVEL_PATCH, PRM_PATCH_OSC2_SHAPE, - UNIT_OSCILLATOR_WAVEFORM, WAVEFORM_NONE, WAVEFORM_LAST - 1, + UNIT_OSCILLATOR_WAVEFORM, WAVEFORM_NONE, WAVEFORM_FILTERED_NOISE, 1, 0, 0xff, 18, STR_RES_WAVEFORM, STR_RES_WAVEFORM, STR_RES_OSCILLATOR_2 }, @@ -859,12 +903,26 @@ static const prog_Parameter parameters[kNumParameters] PROGMEM = { 1, 0, 0xff, 0xff, STR_RES_LEDS, STR_RES_CARD_LEDS, STR_RES_SYSTEM }, - // 71 2 + // 72 { PARAMETER_LEVEL_SYSTEM, PRM_SYSTEM_VOICECARD_SWAP_LEDS_COLORS, UNIT_BOOLEAN, 0, 1, 1, 0, 0xff, 0xff, STR_RES_SWAP_COLORS, STR_RES_SWAP_COLORS, STR_RES_SYSTEM }, + + // 73 - envelope mode: exp, lin, loop, lpln + { PARAMETER_LEVEL_PATCH, + PRM_PATCH_ENV_CURVE, + UNIT_ENVELOPE_CURVE, 0, ENVELOPE_CURVE_LAST - 1, + 3, 8, PRM_UI_ACTIVE_ENV_LFO, 0xff, + STR_RES_MODE, STR_RES_MODE, STR_RES_ENVELOPE }, + + // 74 - analog slop: per-note pitch/envelope randomization + { PARAMETER_LEVEL_PATCH, + PRM_PATCH_SLOP, + UNIT_RAW_UINT8, 0, 127, + 1, 0, 0xff, 0xff, + STR_RES_NOISE, STR_RES_NOISE, STR_RES_MIXER }, }; /* static */ diff --git a/controller/parameter.h b/controller/parameter.h index 94fe523..00b1890 100755 --- a/controller/parameter.h +++ b/controller/parameter.h @@ -54,6 +54,7 @@ enum Unit { UNIT_MIDI_IN_MASK, UNIT_MIDI_OUT_MODE, UNIT_MIDI_CHANNEL, + UNIT_ENVELOPE_CURVE, UNIT_LAST }; @@ -117,7 +118,7 @@ struct Parameter { static void PrintNote(uint8_t note, char* buffer); }; -const uint8_t kNumParameters = 73; +const uint8_t kNumParameters = 75; typedef Parameter PROGMEM prog_Parameter; // The parameter manager is the class who knows how to apply a parameter change diff --git a/controller/part.cc b/controller/part.cc index b8ded3e..b1f273a 100755 --- a/controller/part.cc +++ b/controller/part.cc @@ -154,6 +154,12 @@ void Part::Touch() { } void Part::TouchPatch() { + // Sanitize waveform values — clamp out-of-range shapes from old patches. + for (uint8_t i = 0; i < kNumOscillators; ++i) { + if (patch_.osc[i].shape >= WAVEFORM_LAST) { + patch_.osc[i].shape = WAVEFORM_NONE; + } + } flags_ = FLAG_HAS_CHANGE; // Send an "enter block write" command to all voicecards. for (uint8_t i = 0; i < num_allocated_voices_; ++i) { @@ -591,7 +597,6 @@ void Part::Clock() { ++midi_clock_counter_; if (midi_clock_counter_ >= midi_clock_prescaler_) { midi_clock_counter_ = 0; - ClockSequencer(); ClockArpeggiator(); } @@ -615,7 +620,6 @@ void Part::Clock() { } void Part::Start() { - memset(sequencer_step_, 0, kNumSequences); memset(lfo_step_, 0, kNumLfos); midi_clock_counter_ = midi_clock_prescaler_; previous_generated_note_ = 0xff; @@ -835,54 +839,6 @@ void Part::RetriggerLfos() { } } -void Part::ClockSequencer() { - // Update the value of the sequencer in the modulation matrix. - for (uint8_t i = 0; i < 2; ++i) { - uint8_t value = data_.step_value(i, sequencer_step_[i]); - if (data_.sequence_length[i]) { - WriteToAllVoices(VOICECARD_DATA_MODULATION, MOD_SRC_SEQ_1 + i, value); - } - } - - // Trigger notes if there's a note sequence, and if a key is pressed on the - // keyboard. - if (data_.arp_sequencer_mode == ARP_SEQUENCER_MODE_NOTE && - pressed_keys_.size() && data_.sequence_length[2]) { - NoteStep n = data_.note_step(sequencer_step_[2]); - uint8_t note = Clip(static_cast(n.note) + \ - pressed_keys_.most_recent_note().note - 60, 0, 127); - if (!n.gate) { - // Just kill the previous note. - InternalNoteOff(previous_generated_note_); - previous_generated_note_ = 0xff; - } else { - if (!n.legato) { - // Kill the previous note and move to the new note. - InternalNoteOff(previous_generated_note_); - InternalNoteOn(note, n.velocity & 0x7f); - } else { - // Kill the previous note, but only after having started playing the - // new one. - if (previous_generated_note_ != note) { - InternalNoteOn(note, n.velocity & 0x7f); - InternalNoteOff(previous_generated_note_); - } else { - // Do nothing, this is just a note being held. - } - } - previous_generated_note_ = note; - } - } - - // Jump to the next step in the sequencer. - for (uint8_t i = 0; i < kNumSequences; ++i) { - ++sequencer_step_[i]; - if (sequencer_step_[i] >= data_.sequence_length[i]) { - sequencer_step_[i] = 0; - } - } -} - void Part::ClockArpeggiator() { uint16_t pattern = ResourcesManager::Lookup( lut_res_arpeggiator_patterns, diff --git a/controller/part.h b/controller/part.h index a102df8..16c92aa 100755 --- a/controller/part.h +++ b/controller/part.h @@ -251,10 +251,10 @@ class Part { } uint8_t lfo_value(uint8_t index) const { return lfo_previous_values_[index]; } - uint8_t step(uint8_t index) const { return sequencer_step_[index]; } void Touch(); void TouchPatch(); + void WriteToAllVoices(uint8_t data_type, uint8_t address, uint8_t value); void UpdateLfos(uint8_t refresh_cycle); void AssignVoices(uint8_t allocation); @@ -271,16 +271,13 @@ class Part { void RetriggerLfos(); - // Called on each "tick" of the arpeggiator and sequencer clock. - void ClockSequencer(); + // Called on each "tick" of the arpeggiator clock. void ClockArpeggiator(); // Called whenever a new arpeggiator note has to be triggered. void StartArpeggio(); void StepArpeggio(); - void WriteToAllVoices(uint8_t data_type, uint8_t address, uint8_t value); - // Check whether a note should be played by this part. A note can be rejected // if it is outside of the split region, or if it is outside of the selected // scale/raga. @@ -321,9 +318,6 @@ class Part { uint8_t nrpn_msb_; uint8_t nrpn_; - // Sequencer state. - uint8_t sequencer_step_[kNumSequences]; - // Arpeggiator state. uint8_t previous_generated_note_; uint16_t arp_pattern_mask_; diff --git a/controller/ui.cc b/controller/ui.cc index 71fea74..d2067a6 100755 --- a/controller/ui.cc +++ b/controller/ui.cc @@ -33,8 +33,11 @@ #include "controller/ui_pages/library.h" #include "controller/ui_pages/os_info_page.h" #include "controller/ui_pages/parameter_editor.h" +#include "controller/ui_pages/fm_page.h" +#include "controller/ui_pages/ks_page.h" +#include "controller/ui_pages/wc_page.h" #include "controller/ui_pages/performance_page.h" -#include "controller/ui_pages/sequence_editor.h" +#include "controller/ui_pages/randomizer.h" #include "controller/ui_pages/version_manager.h" #include "controller/ui_pages/voice_assigner.h" #include "controller/voicecard_tx.h" @@ -62,13 +65,13 @@ const prog_PageInfo page_registry[] PROGMEM = { { PAGE_ENV_LFO, &ParameterEditor::event_handlers_, - { 24, 30, 31, 29, 25, 26, 27, 28 }, + { 24, 30, 31, 73, 25, 26, 27, 28 }, PAGE_VOICE_LFO, 2, 0xf0, }, - + { PAGE_VOICE_LFO, &ParameterEditor::event_handlers_, - { 0xff, 32, 33, 0xff, 0xff, 0xff, 0xff, 0xff }, + { 0xff, 32, 33, 29, 0xff, 0xff, 0xff, 0xff }, PAGE_ENV_LFO, 2, 0x0f, }, @@ -80,20 +83,14 @@ const prog_PageInfo page_registry[] PROGMEM = { { PAGE_PART, &ParameterEditor::event_handlers_, - { 42, 57, 47, 48, 43, 44, 45, 46 }, + { 42, 57, 74, 48, 43, 44, 45, 46 }, PAGE_PART_ARPEGGIATOR, 4, 0xf0, }, { PAGE_PART_ARPEGGIATOR, &ParameterEditor::event_handlers_, - { 49, 50, 51, 52, 53, 54, 55, 56 }, - PAGE_PART_SEQUENCER, 4, 0x0f, - }, - - { PAGE_PART_SEQUENCER, - &SequenceEditor::event_handlers_, - { 0, 0, 0, 0, 0, 0, 0, 0 }, - PAGE_PART, 4, 0xff, + { 49, 50, 51, 52, 53, 0xff, 0xff, 0xff }, + PAGE_PART, 4, 0x0f, }, { PAGE_MULTI, @@ -117,7 +114,7 @@ const prog_PageInfo page_registry[] PROGMEM = { { PAGE_KNOB_ASSIGN, &KnobAssigner::event_handlers_, { 0, 0, 0, 0, 0, 0, 0, 0, }, - PAGE_PERFORMANCE, 6, 0x0f, + PAGE_RANDOMIZER, 6, 0x0f, }, { PAGE_LIBRARY, @@ -132,6 +129,30 @@ const prog_PageInfo page_registry[] PROGMEM = { PAGE_LIBRARY, 8, 0xf0, }, + { PAGE_FM4OP, + &FmPage::event_handlers_, + { 0, 0, 0, 0, 0, 0, 0, 0, }, + PAGE_FM4OP, 0, 0xf0, + }, + + { PAGE_KS_PLUCK, + &KsPage::event_handlers_, + { 0, 0, 0, 0, 0, 0, 0, 0, }, + PAGE_KS_PLUCK, 0, 0xf0, + }, + + { PAGE_WESTCOAST, + &WcPage::event_handlers_, + { 0, 0, 0, 0, 0, 0, 0, 0, }, + PAGE_WESTCOAST, 0, 0xf0, + }, + + { PAGE_RANDOMIZER, + &Randomizer::event_handlers_, + { 0, 0, 0, 0, 0, 0, 0, 0, }, + PAGE_PERFORMANCE, 6, 0x0f, + }, + { PAGE_SYSTEM_SETTINGS, &ParameterEditor::event_handlers_, { 66, 67, 71, 72, 68, 69, 0xff, 70, }, @@ -183,7 +204,7 @@ static char line[41]; /* static */ void Ui::Init() { memset(&state_, 0, sizeof(UiState)); - memcpy_P(most_recent_page_in_group_, default_most_recent_page_in_group, 8); + memcpy_P(most_recent_page_in_group_, default_most_recent_page_in_group, 9); encoder_.Init(); switches_.Init(); @@ -314,7 +335,40 @@ void Ui::DoEvents() { case CONTROL_ENCODER: if (e.control_id == 0) { - (*event_handlers_.OnIncrement)(e.value); + // Hold S1 + encoder = cycle synthesis mode. + // When S1 is held, Poll() multiplies increment by 8. + uint8_t s1_held = (e.value >= 8 || e.value <= -8); + if (s1_held && ( + active_page_ == PAGE_OSCILLATORS || + active_page_ == PAGE_FM4OP || + active_page_ == PAGE_KS_PLUCK || + active_page_ == PAGE_WESTCOAST)) { + // Cycle through modes: classic, fm4op, pluck, wcoast. + static const uint8_t modes[] PROGMEM = { + WAVEFORM_SAW, WAVEFORM_FM4OP, + WAVEFORM_KS_PLUCK, WAVEFORM_WESTCOAST + }; + static const uint8_t pages[] PROGMEM = { + PAGE_OSCILLATORS, PAGE_FM4OP, + PAGE_KS_PLUCK, PAGE_WESTCOAST + }; + uint8_t* patch = multi.mutable_part( + state_.active_part)->mutable_raw_patch_data(); + uint8_t current = patch[0]; + // Find current mode index. + int8_t idx = 0; + for (uint8_t m = 0; m < 4; ++m) { + if (current == pgm_read_byte(&modes[m])) { idx = m; break; } + } + idx += (e.value > 0) ? 1 : -1; + if (idx < 0) idx = 3; + if (idx > 3) idx = 0; + patch[0] = pgm_read_byte(&modes[idx]); + multi.mutable_part(state_.active_part)->TouchPatch(); + ShowPage(static_cast(pgm_read_byte(&pages[idx]))); + } else { + (*event_handlers_.OnIncrement)(e.value); + } } else { int8_t new_part = state_.active_part + e.value; new_part = Clip(new_part, 0, kNumParts - 1); diff --git a/controller/ui.h b/controller/ui.h index a3b409f..f350e9c 100755 --- a/controller/ui.h +++ b/controller/ui.h @@ -86,7 +86,6 @@ enum UiPageNumber { PAGE_PART, PAGE_PART_ARPEGGIATOR, - PAGE_PART_SEQUENCER, PAGE_MULTI, PAGE_MULTI_CLOCK, @@ -96,7 +95,12 @@ enum UiPageNumber { PAGE_LIBRARY, PAGE_VERSION_MANAGER, - PAGE_SYSTEM_SETTINGS, + PAGE_FM4OP, + PAGE_KS_PLUCK, + PAGE_WESTCOAST, + PAGE_RANDOMIZER, + + PAGE_SYSTEM_SETTINGS, PAGE_OS_INFO, }; diff --git a/controller/ui_pages/fm_page.cc b/controller/ui_pages/fm_page.cc new file mode 100644 index 0000000..0503511 --- /dev/null +++ b/controller/ui_pages/fm_page.cc @@ -0,0 +1,369 @@ +// Copyright 2026 Ambika contributors. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// 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. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ----------------------------------------------------------------------------- +// +// Custom UI page for 4-op FM synthesis parameters. + +#include "controller/ui_pages/fm_page.h" + +#include +#include "avrlib/string.h" + +#include "controller/display.h" +#include "controller/leds.h" +#include "controller/multi.h" +#include "controller/voicecard_tx.h" +#include "controller/ui.h" +#include "common/patch.h" + +namespace ambika { + +// FM constants (must match voicecard/fm4op.h). +static const uint8_t kFmWaveLast = 8; +static const uint8_t kFmAlgLast = 8; + +using namespace avrlib; + +/* static */ +uint8_t FmPage::page_index_; + +// TX81Z waveform short names. +static const prog_char fm_wave_names[] PROGMEM = + "sin halfabs qtr habstri pls saw "; + +// Algorithm short descriptions. +static const prog_char fm_alg_names[] PROGMEM = + "4>3>2>1 3+4>2>1 4>3+2>1 43 + 21" + "4>2+4311 4>1234>1+2+31+2+3+4"; + +// FM page 1: algorithm, feedback, ops 1-2 +// FM page 2: ops 3-4, levels 1-4 +// +// Patch byte mapping (from fm4op.h): +// osc[0].parameter (offset 1) = algorithm +// osc[0].range (offset 2) = op1 coarse ratio +// osc[0].detune (offset 3) = op1 fine +// osc[1].shape (offset 4) = op1 wave (lo) | op2 wave (hi) +// osc[1].parameter (offset 5) = op3 wave (lo) | op4 wave (hi) +// osc[1].range (offset 6) = op2 coarse ratio +// osc[1].detune (offset 7) = op2 fine +// mix_balance (offset 8) = op3 coarse ratio +// mix_op (offset 9) = op3 fine +// mix_parameter (offset 10) = op4 coarse ratio +// mix_sub_osc_shape (offset 11)= op4 fine +// mix_sub_osc (offset 12) = op1 level +// mix_noise (offset 13) = op2 level +// mix_fuzz (offset 14) = op3 level +// mix_crush (offset 15) = op4 level +// padding[0] (offset 104) = feedback + +// Page 1 knob layout: +// algo fbk wav1 wav2 +// rat1 fin1 rat2 fin2 +// Patch offsets for page 1 knobs: +static const uint8_t fm_page1_offsets[] PROGMEM = { + 1, // algo (osc[0].parameter) + 104, // fbk (padding[0]) + 4, // wav1 (osc[1].shape, low nibble) + 4, // wav2 (osc[1].shape, high nibble) + 2, // rat1 (osc[0].range) + 3, // fin1 (osc[0].detune) + 6, // rat2 (osc[1].range) + 7, // fin2 (osc[1].detune) +}; + +// Page 2 knob layout: +// wav3 wav4 lvl1 lvl2 +// rat3 fin3 lvl3 lvl4 +static const uint8_t fm_page2_offsets[] PROGMEM = { + 5, // wav3 (osc[1].parameter, low nibble) + 5, // wav4 (osc[1].parameter, high nibble) + 12, // lvl1 (mix_sub_osc) + 13, // lvl2 (mix_noise) + 8, // rat3 (mix_balance) + 9, // fin3 (mix_op) + 14, // lvl3 (mix_fuzz) + 15, // lvl4 (mix_crush) +}; + +enum FmParamType { + FM_PT_ALGO, + FM_PT_FEEDBACK, + FM_PT_WAVE_LO, + FM_PT_WAVE_HI, + FM_PT_RATIO, + FM_PT_FINE, + FM_PT_LEVEL, +}; + +// Parameter type for each knob on page 1. +static const uint8_t fm_page1_types[] PROGMEM = { + FM_PT_ALGO, FM_PT_FEEDBACK, FM_PT_WAVE_LO, FM_PT_WAVE_HI, + FM_PT_RATIO, FM_PT_FINE, FM_PT_RATIO, FM_PT_FINE, +}; + +// Parameter type for each knob on page 2. +static const uint8_t fm_page2_types[] PROGMEM = { + FM_PT_WAVE_LO, FM_PT_WAVE_HI, FM_PT_LEVEL, FM_PT_LEVEL, + FM_PT_RATIO, FM_PT_FINE, FM_PT_LEVEL, FM_PT_LEVEL, +}; + +// Labels for page 1 knobs (4 chars each). +static const prog_char fm_page1_labels[] PROGMEM = + "algo" "fbk " "wav1" "wav2" + "rat1" "fin1" "rat2" "fin2"; + +// Labels for page 2 knobs (4 chars each). +static const prog_char fm_page2_labels[] PROGMEM = + "wav3" "wav4" "lvl1" "lvl2" + "rat3" "fin3" "lvl3" "lvl4"; + +/* static */ +const prog_EventHandlers FmPage::event_handlers_ PROGMEM = { + OnInit, + NULL, + OnIncrement, + OnClick, + OnPot, + OnKey, + NULL, + OnIdle, + UpdateScreen, + UpdateLeds, + NULL, +}; + +/* static */ +uint8_t FmPage::IsFm4OpActive() { + Part* part = multi.mutable_part(ui.state().active_part); + uint8_t* patch = part->mutable_raw_patch_data(); + return patch[0] == WAVEFORM_FM4OP; +} + +/* static */ +void FmPage::OnInit(PageInfo* info) { + UiPage::OnInit(info); + active_control_ = 0; + page_index_ = 0; +} + +static inline uint8_t* GetPatchData() { + return multi.mutable_part(ui.state().active_part)->mutable_raw_patch_data(); +} + +/* static */ +uint8_t FmPage::OnIncrement(int8_t increment) { + if (edit_mode_ != EDIT_IDLE) { + uint8_t* patch = GetPatchData(); + const uint8_t* offsets = page_index_ ? fm_page2_offsets : fm_page1_offsets; + const uint8_t* types = page_index_ ? fm_page2_types : fm_page1_types; + uint8_t offset = pgm_read_byte(&offsets[active_control_]); + uint8_t type = pgm_read_byte(&types[active_control_]); + + if (type == FM_PT_WAVE_LO) { + uint8_t val = patch[offset] & 0x0F; + val = static_cast(val + increment); + if (val >= kFmWaveLast) val = (increment > 0) ? kFmWaveLast - 1 : 0; + patch[offset] = (patch[offset] & 0xF0) | val; + } else if (type == FM_PT_WAVE_HI) { + uint8_t val = (patch[offset] >> 4) & 0x0F; + val = static_cast(val + increment); + if (val >= kFmWaveLast) val = (increment > 0) ? kFmWaveLast - 1 : 0; + patch[offset] = (patch[offset] & 0x0F) | (val << 4); + } else if (type == FM_PT_ALGO) { + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val >= kFmAlgLast) val = kFmAlgLast - 1; + patch[offset] = val; + } else if (type == FM_PT_FINE) { + int16_t val = static_cast(patch[offset]) + increment; + if (val < -64) val = -64; + if (val > 63) val = 63; + patch[offset] = static_cast(val); + } else { + // RATIO, FEEDBACK, LEVEL: 0-127 + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val > 127) val = 127; + patch[offset] = val; + } + + // Push change to voicecard. + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + edit_mode_ = EDIT_STARTED_BY_ENCODER; + } else { + int8_t new_control = active_control_ + increment; + if (new_control < 0) { + if (page_index_ > 0) { + page_index_--; + active_control_ = 7; + } + } else if (new_control >= 8) { + if (page_index_ < 1) { + page_index_++; + active_control_ = 0; + } + } else { + active_control_ = new_control; + } + } + return 1; +} + +/* static */ +uint8_t FmPage::OnClick() { + if (edit_mode_ == EDIT_IDLE) { + edit_mode_ = EDIT_STARTED_BY_ENCODER; + } else { + edit_mode_ = EDIT_IDLE; + } + return 1; +} + +/* static */ +uint8_t FmPage::OnPot(uint8_t index, uint8_t value) { + uint8_t* patch = GetPatchData(); + const uint8_t* offsets = page_index_ ? fm_page2_offsets : fm_page1_offsets; + const uint8_t* types = page_index_ ? fm_page2_types : fm_page1_types; + uint8_t offset = pgm_read_byte(&offsets[index]); + uint8_t type = pgm_read_byte(&types[index]); + + // Scale 7-bit pot value to parameter range. + if (type == FM_PT_WAVE_LO) { + uint8_t val = (value * kFmWaveLast) >> 7; + if (val >= kFmWaveLast) val = kFmWaveLast - 1; + patch[offset] = (patch[offset] & 0xF0) | val; + } else if (type == FM_PT_WAVE_HI) { + uint8_t val = (value * kFmWaveLast) >> 7; + if (val >= kFmWaveLast) val = kFmWaveLast - 1; + patch[offset] = (patch[offset] & 0x0F) | (val << 4); + } else if (type == FM_PT_ALGO) { + uint8_t val = (value * kFmAlgLast) >> 7; + if (val >= kFmAlgLast) val = kFmAlgLast - 1; + patch[offset] = val; + } else if (type == FM_PT_FINE) { + patch[offset] = static_cast(static_cast(value) - 64); + } else { + patch[offset] = value; + } + + active_control_ = index; + edit_mode_ = EDIT_STARTED_BY_POT; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + return 1; +} + +/* static */ +uint8_t FmPage::OnKey(uint8_t key) { + if (key == SWITCH_1) { + page_index_ = page_index_ ? 0 : 1; + return 1; + } + return 0; +} + +/* static */ +void FmPage::UpdateScreen() { + const prog_char* labels = page_index_ ? fm_page2_labels : fm_page1_labels; + const uint8_t* offsets = page_index_ ? fm_page2_offsets : fm_page1_offsets; + const uint8_t* types = page_index_ ? fm_page2_types : fm_page1_types; + uint8_t* patch = GetPatchData(); + + for (uint8_t i = 0; i < 8; ++i) { + uint8_t line = i < 4 ? 0 : 1; + uint8_t row = (i & 3) * 10; + char* buffer = display.line_buffer(line) + row; + + if (row != 0) buffer[0] = kDelimiter; + if ((row + 10) != kLcdWidth) buffer[10] = kDelimiter; + + // Label (4 chars). + memcpy_P(&buffer[1], labels + i * 4, 4); + + // Uppercase first char if active. + if (i == active_control_ && buffer[1] >= 'a' && buffer[1] <= 'z') { + buffer[1] -= 0x20; + } + + buffer[5] = ' '; + + // Value (4 chars at buffer[6..9]). + uint8_t offset = pgm_read_byte(&offsets[i]); + uint8_t type = pgm_read_byte(&types[i]); + uint8_t raw = patch[offset]; + + switch (type) { + case FM_PT_ALGO: + buffer[6] = ' '; + buffer[7] = ' '; + buffer[8] = ' '; + buffer[9] = '1' + raw; + break; + + case FM_PT_WAVE_LO: { + uint8_t w = raw & 0x0F; + if (w >= kFmWaveLast) w = 0; + memcpy_P(&buffer[6], fm_wave_names + w * 4, 4); + break; + } + + case FM_PT_WAVE_HI: { + uint8_t w = (raw >> 4) & 0x0F; + if (w >= kFmWaveLast) w = 0; + memcpy_P(&buffer[6], fm_wave_names + w * 4, 4); + break; + } + + case FM_PT_RATIO: { + // Display as ratio value. 0=0.5, 1=1.0, 2=2.0, etc. + int8_t r = static_cast(raw); + if (r <= 0) { + buffer[6] = '0'; + buffer[7] = '.'; + buffer[8] = '5'; + buffer[9] = ' '; + } else { + UnsafeItoa(r, 3, &buffer[6]); + AlignRight(&buffer[6], 3); + buffer[9] = ' '; + } + break; + } + + case FM_PT_FINE: { + int8_t v = static_cast(raw); + UnsafeItoa(v, 4, &buffer[6]); + AlignRight(&buffer[6], 4); + break; + } + + case FM_PT_FEEDBACK: + case FM_PT_LEVEL: + default: + UnsafeItoa(raw, 4, &buffer[6]); + AlignRight(&buffer[6], 4); + break; + } + } +} + +/* static */ +void FmPage::UpdateLeds() { + leds.set_pixel(LED_1, page_index_ == 0 ? 0xf0 : 0x0f); + leds.set_pixel(LED_2, page_index_ == 1 ? 0xf0 : 0x0f); +} + +} // namespace ambika diff --git a/controller/ui_pages/fm_page.h b/controller/ui_pages/fm_page.h new file mode 100644 index 0000000..0b0bea7 --- /dev/null +++ b/controller/ui_pages/fm_page.h @@ -0,0 +1,60 @@ +// Copyright 2026 Ambika contributors. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// 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. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ----------------------------------------------------------------------------- +// +// Custom UI page for 4-op FM synthesis parameters. + +#ifndef CONTROLLER_UI_PAGES_FM_PAGE_H_ +#define CONTROLLER_UI_PAGES_FM_PAGE_H_ + +#include "controller/ui_pages/ui_page.h" + +namespace ambika { + +// FM parameter definitions for each knob position. +// Each entry: patch byte offset, min, max. +struct FmParam { + uint8_t offset; + uint8_t min_value; + uint8_t max_value; +}; + +class FmPage : public UiPage { + public: + FmPage() { } + + static void OnInit(PageInfo* info); + static uint8_t OnIncrement(int8_t increment); + static uint8_t OnClick(); + static uint8_t OnPot(uint8_t index, uint8_t value); + static uint8_t OnKey(uint8_t key); + + static void UpdateScreen(); + static void UpdateLeds(); + + static const prog_EventHandlers event_handlers_; + + // Check if the active part's osc1 is set to FM4OP. + static uint8_t IsFm4OpActive(); + + private: + static void PrintFmValue(char* buffer, uint8_t value, uint8_t param_type); + static uint8_t page_index_; // 0 = ops 1-2, 1 = ops 3-4 + + DISALLOW_COPY_AND_ASSIGN(FmPage); +}; + +} // namespace ambika + +#endif // CONTROLLER_UI_PAGES_FM_PAGE_H_ diff --git a/controller/ui_pages/ks_page.cc b/controller/ui_pages/ks_page.cc new file mode 100644 index 0000000..8ce75f1 --- /dev/null +++ b/controller/ui_pages/ks_page.cc @@ -0,0 +1,248 @@ +// Copyright 2026 Ambika contributors. +// +// Custom UI page for Karplus-Strong plucked string synthesis. + +#include "controller/ui_pages/ks_page.h" + +#include +#include "avrlib/string.h" + +#include "controller/display.h" +#include "controller/leds.h" +#include "controller/multi.h" +#include "controller/voicecard_tx.h" +#include "controller/ui.h" +#include "common/patch.h" + +namespace ambika { + +using namespace avrlib; + +static const uint8_t kKsExcLast = 4; + +// Page 1: damp colr dcay body | rang tune pos exc +// Page 2: rate dpth sprd wmix | stif feed +// +// Patch offsets: +// osc[0].parameter (1) = damping +// osc[1].parameter (5) = excitation color +// osc[1].range (6) = decay rate +// mix_balance (8) = body resonance +// osc[0].range (2) = pitch range +// osc[0].detune (3) = fine tune +// osc[1].detune (7) = pluck position +// osc[1].shape (4) = excitation type +// mix_op (9) = ensemble rate +// mix_parameter (10) = ensemble depth +// mix_sub_osc_shape(11) = ensemble spread +// mix_sub_osc (12) = ensemble mix +// mix_noise (13) = stiffness +// mix_fuzz (14) = feedback + +static const uint8_t ks_page1_offsets[] PROGMEM = { + 1, 5, 6, 8, 2, 3, 7, 4, +}; +static const uint8_t ks_page2_offsets[] PROGMEM = { + 9, 10, 11, 12, 13, 14, 0xff, 0xff, +}; + +enum KsParamType { + KS_PT_UINT7, + KS_PT_INT8, + KS_PT_EXCITATION, + KS_PT_NONE, +}; + +static const uint8_t ks_page1_types[] PROGMEM = { + KS_PT_UINT7, KS_PT_UINT7, KS_PT_UINT7, KS_PT_UINT7, + KS_PT_INT8, KS_PT_INT8, KS_PT_UINT7, KS_PT_EXCITATION, +}; +static const uint8_t ks_page2_types[] PROGMEM = { + KS_PT_UINT7, KS_PT_UINT7, KS_PT_UINT7, KS_PT_UINT7, + KS_PT_UINT7, KS_PT_UINT7, KS_PT_NONE, KS_PT_NONE, +}; + +static const prog_char ks_page1_labels[] PROGMEM = + "damp" "colr" "dcay" "body" + "rang" "tune" "pos " "exc "; +static const prog_char ks_page2_labels[] PROGMEM = + "rate" "dpth" "sprd" "wmix" + "stif" "feed" " " " "; + +static const prog_char ks_exc_names[] PROGMEM = + "noisclicbritdark"; + +/* static */ +uint8_t KsPage::page_index_; + +/* static */ +const prog_EventHandlers KsPage::event_handlers_ PROGMEM = { + OnInit, NULL, OnIncrement, OnClick, OnPot, OnKey, + NULL, OnIdle, UpdateScreen, UpdateLeds, NULL, +}; + +/* static */ +uint8_t KsPage::IsKsActive() { + return multi.mutable_part(ui.state().active_part)->mutable_raw_patch_data()[0] + == WAVEFORM_KS_PLUCK; +} + +static inline uint8_t* GetPatchData() { + return multi.mutable_part(ui.state().active_part)->mutable_raw_patch_data(); +} + +/* static */ +void KsPage::OnInit(PageInfo* info) { + UiPage::OnInit(info); + active_control_ = 0; + page_index_ = 0; +} + +/* static */ +uint8_t KsPage::OnIncrement(int8_t increment) { + if (edit_mode_ != EDIT_IDLE) { + const uint8_t* offsets = page_index_ ? ks_page2_offsets : ks_page1_offsets; + const uint8_t* types = page_index_ ? ks_page2_types : ks_page1_types; + uint8_t offset = pgm_read_byte(&offsets[active_control_]); + uint8_t type = pgm_read_byte(&types[active_control_]); + if (offset == 0xff || type == KS_PT_NONE) return 1; + + uint8_t* patch = GetPatchData(); + if (type == KS_PT_EXCITATION) { + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val >= kKsExcLast) val = kKsExcLast - 1; + patch[offset] = val; + } else if (type == KS_PT_INT8) { + int16_t val = static_cast(patch[offset]) + increment; + if (val < -24) val = -24; + if (val > 24) val = 24; + patch[offset] = static_cast(val); + } else { + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val > 127) val = 127; + patch[offset] = val; + } + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + edit_mode_ = EDIT_STARTED_BY_ENCODER; + } else { + int8_t new_control = active_control_ + increment; + if (new_control < 0) { + if (page_index_ > 0) { page_index_--; active_control_ = 7; } + } else if (new_control >= 8) { + if (page_index_ < 1) { page_index_++; active_control_ = 0; } + } else { + // Skip empty slots. + const uint8_t* types = page_index_ ? ks_page2_types : ks_page1_types; + if (pgm_read_byte(&types[new_control]) != KS_PT_NONE) { + active_control_ = new_control; + } + } + } + return 1; +} + +/* static */ +uint8_t KsPage::OnClick() { + if (edit_mode_ == EDIT_IDLE) { + edit_mode_ = EDIT_STARTED_BY_ENCODER; + } else { + edit_mode_ = EDIT_IDLE; + } + return 1; +} + +/* static */ +uint8_t KsPage::OnPot(uint8_t index, uint8_t value) { + const uint8_t* offsets = page_index_ ? ks_page2_offsets : ks_page1_offsets; + const uint8_t* types = page_index_ ? ks_page2_types : ks_page1_types; + uint8_t offset = pgm_read_byte(&offsets[index]); + uint8_t type = pgm_read_byte(&types[index]); + if (offset == 0xff || type == KS_PT_NONE) return 1; + + uint8_t* patch = GetPatchData(); + if (type == KS_PT_EXCITATION) { + uint8_t val = (value * kKsExcLast) >> 7; + if (val >= kKsExcLast) val = kKsExcLast - 1; + patch[offset] = val; + } else if (type == KS_PT_INT8) { + patch[offset] = static_cast(static_cast(value) - 64); + } else { + patch[offset] = value; + } + + active_control_ = index; + edit_mode_ = EDIT_STARTED_BY_POT; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + return 1; +} + +/* static */ +uint8_t KsPage::OnKey(uint8_t key) { + if (key == SWITCH_1) { + page_index_ = page_index_ ? 0 : 1; + return 1; + } + return 0; +} + +/* static */ +void KsPage::UpdateScreen() { + const prog_char* labels = page_index_ ? ks_page2_labels : ks_page1_labels; + const uint8_t* offsets = page_index_ ? ks_page2_offsets : ks_page1_offsets; + const uint8_t* types = page_index_ ? ks_page2_types : ks_page1_types; + uint8_t* patch = GetPatchData(); + + for (uint8_t i = 0; i < 8; ++i) { + uint8_t line = i < 4 ? 0 : 1; + uint8_t row = (i & 3) * 10; + char* buffer = display.line_buffer(line) + row; + + if (row != 0) buffer[0] = kDelimiter; + if ((row + 10) != kLcdWidth) buffer[10] = kDelimiter; + + uint8_t type = pgm_read_byte(&types[i]); + if (type == KS_PT_NONE) { + for (uint8_t j = 1; j < 10; ++j) buffer[j] = ' '; + continue; + } + + memcpy_P(&buffer[1], labels + i * 4, 4); + if (i == active_control_ && buffer[1] >= 'a' && buffer[1] <= 'z') { + buffer[1] -= 0x20; + } + buffer[5] = ' '; + + uint8_t offset = pgm_read_byte(&offsets[i]); + uint8_t raw = patch[offset]; + + switch (type) { + case KS_PT_EXCITATION: { + uint8_t idx = raw; + if (idx >= kKsExcLast) idx = 0; + memcpy_P(&buffer[6], ks_exc_names + idx * 4, 4); + break; + } + case KS_PT_INT8: { + UnsafeItoa(static_cast(raw), 4, &buffer[6]); + AlignRight(&buffer[6], 4); + break; + } + default: + UnsafeItoa(raw, 4, &buffer[6]); + AlignRight(&buffer[6], 4); + break; + } + } +} + +/* static */ +void KsPage::UpdateLeds() { + leds.set_pixel(LED_1, page_index_ == 0 ? 0xf0 : 0x0f); + leds.set_pixel(LED_2, page_index_ == 1 ? 0xf0 : 0x0f); +} + +} // namespace ambika diff --git a/controller/ui_pages/ks_page.h b/controller/ui_pages/ks_page.h new file mode 100644 index 0000000..d8729f7 --- /dev/null +++ b/controller/ui_pages/ks_page.h @@ -0,0 +1,41 @@ +// Copyright 2026 Ambika contributors. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Custom UI page for Karplus-Strong plucked string synthesis. + +#ifndef CONTROLLER_UI_PAGES_KS_PAGE_H_ +#define CONTROLLER_UI_PAGES_KS_PAGE_H_ + +#include "controller/ui_pages/ui_page.h" + +namespace ambika { + +class KsPage : public UiPage { + public: + KsPage() { } + + static void OnInit(PageInfo* info); + static uint8_t OnIncrement(int8_t increment); + static uint8_t OnClick(); + static uint8_t OnPot(uint8_t index, uint8_t value); + static uint8_t OnKey(uint8_t key); + + static void UpdateScreen(); + static void UpdateLeds(); + + static const prog_EventHandlers event_handlers_; + + static uint8_t IsKsActive(); + + private: + static uint8_t page_index_; + DISALLOW_COPY_AND_ASSIGN(KsPage); +}; + +} // namespace ambika + +#endif // CONTROLLER_UI_PAGES_KS_PAGE_H_ diff --git a/controller/ui_pages/os_info_page.cc b/controller/ui_pages/os_info_page.cc index d366071..85d4e84 100755 --- a/controller/ui_pages/os_info_page.cc +++ b/controller/ui_pages/os_info_page.cc @@ -130,9 +130,10 @@ void OsInfoPage::PrintVersionNumber(char* buffer, uint8_t number) { /* static */ void OsInfoPage::UpdateScreen() { + display.line_buffer(0)[0] = ' '; char* buffer = display.line_buffer(0) + 1; - memcpy_P(&buffer[0], PSTR("ambika"), 6); - PrintVersionNumber(&buffer[10], kSystemVersion); + memcpy_P(&buffer[0], PSTR("carcosa"), 7); + memcpy_P(&buffer[9], PSTR("v1.4"), 4); memcpy_P(&buffer[15], PSTR("port 1 device ?"), 15); Word version_number = voicecard_tx.GetVersion(active_control_); diff --git a/controller/ui_pages/parameter_editor.h b/controller/ui_pages/parameter_editor.h index c206b65..6837832 100755 --- a/controller/ui_pages/parameter_editor.h +++ b/controller/ui_pages/parameter_editor.h @@ -48,7 +48,7 @@ class ParameterEditor : public UiPage { static uint8_t instance_index(uint8_t control_id); static const prog_EventHandlers event_handlers_; - + private: static SnapMask snapped_; diff --git a/controller/ui_pages/randomizer.cc b/controller/ui_pages/randomizer.cc new file mode 100644 index 0000000..7a51358 --- /dev/null +++ b/controller/ui_pages/randomizer.cc @@ -0,0 +1,253 @@ +// Copyright 2026 Ambika contributors. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// 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. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ----------------------------------------------------------------------------- +// +// Smart patch randomizer implementation. + +#include "controller/ui_pages/randomizer.h" + +#include "avrlib/random.h" +#include "avrlib/string.h" + +#include "controller/display.h" +#include "controller/leds.h" +#include "controller/multi.h" +#include "controller/parameter.h" +#include "controller/ui.h" + +namespace ambika { + +using namespace avrlib; + +/* static */ +uint8_t Randomizer::section_depth_[RANDOM_SECTION_LAST]; + +/* static */ +uint8_t Randomizer::triggered_; + +static const prog_char random_labels[] PROGMEM = + "osc filt env mod mix lfo dpth trg "; + +/* static */ +const prog_EventHandlers Randomizer::event_handlers_ PROGMEM = { + OnInit, + NULL, + OnIncrement, + OnClick, + OnPot, + OnKey, + NULL, + OnIdle, + UpdateScreen, + UpdateLeds, + NULL, +}; + +/* static */ +void Randomizer::OnInit(PageInfo* info) { + triggered_ = 0; +} + +/* static */ +uint8_t Randomizer::OnIncrement(int8_t increment) { + return 1; +} + +/* static */ +uint8_t Randomizer::OnClick() { + Randomize(); + triggered_ = 30; // Flash counter for visual feedback. + return 1; +} + +/* static */ +uint8_t Randomizer::OnPot(uint8_t index, uint8_t value) { + if (index < RANDOM_SECTION_LAST) { + section_depth_[index] = value; + } + return 1; +} + +/* static */ +uint8_t Randomizer::OnKey(uint8_t key) { + if (key < 6) { + Ui::mutable_state()->active_part = key; + return 1; + } + return 0; +} + +/* static */ +void Randomizer::RandomizeRange( + uint8_t* data, uint8_t start, uint8_t end, + uint8_t min_val, uint8_t max_val, uint8_t depth) { + if (depth == 0) return; + for (uint8_t i = start; i < end; ++i) { + // Generate a random offset scaled by depth. + int16_t current = data[i]; + int16_t range = max_val - min_val; + int16_t offset = static_cast(Random::GetByte()); + offset = (offset * depth * range) >> 14; + current += offset; + if (current < min_val) current = min_val; + if (current > max_val) current = max_val; + data[i] = static_cast(current); + } +} + +/* static */ +void Randomizer::Randomize() { + Part* part = multi.mutable_part(Ui::state().active_part); + uint8_t* patch = part->mutable_raw_patch_data(); + + // Global depth multiplier (knob 7). + uint8_t global = section_depth_[RANDOM_SECTION_DEPTH]; + if (global == 0) global = 127; // Default to full if not set. + + // --- Oscillators (offset 0-7) --- + uint8_t osc_depth = (section_depth_[RANDOM_SECTION_OSC] * global) >> 7; + if (osc_depth) { + // Waveform shapes: offset 0, 4 + for (uint8_t i = 0; i < 2; ++i) { + uint8_t idx = i * 4; // osc[0].shape=0, osc[1].shape=4 + if (Random::GetByte() < osc_depth * 2) { + patch[idx] = Random::GetByte() % WAVEFORM_LAST; + } + } + // Parameters: offset 1, 5 + RandomizeRange(patch, 1, 2, 0, 127, osc_depth); + RandomizeRange(patch, 5, 6, 0, 127, osc_depth); + // Range: offset 2, 6 (signed, keep in -12..12) + RandomizeRange(patch, 2, 3, 0, 48, osc_depth); // Will be interpreted as int8 + RandomizeRange(patch, 6, 7, 0, 48, osc_depth); + // Detune: offset 3, 7 + RandomizeRange(patch, 3, 4, 0, 128, osc_depth); + RandomizeRange(patch, 7, 8, 0, 128, osc_depth); + } + + // --- Mixer (offset 8-15) --- + uint8_t mix_depth = (section_depth_[RANDOM_SECTION_MIXER] * global) >> 7; + if (mix_depth) { + RandomizeRange(patch, 8, 9, 0, 63, mix_depth); // mix balance + // Skip mix_op (9) — keep current operator + RandomizeRange(patch, 10, 11, 0, 63, mix_depth); // mix parameter + // Skip sub osc shape (11) + RandomizeRange(patch, 12, 13, 0, 63, mix_depth); // sub osc level + RandomizeRange(patch, 13, 14, 0, 63, mix_depth); // noise level + RandomizeRange(patch, 14, 15, 0, 63, mix_depth); // fuzz + RandomizeRange(patch, 15, 16, 0, 31, mix_depth); // crush + } + + // --- Filter (offset 16-23) --- + uint8_t filt_depth = (section_depth_[RANDOM_SECTION_FILTER] * global) >> 7; + if (filt_depth) { + RandomizeRange(patch, 16, 17, 0, 127, filt_depth); // cutoff + RandomizeRange(patch, 17, 18, 0, 63, filt_depth); // resonance + // Skip filter mode (18) — keep current + // Skip filter 2 (19-21) + RandomizeRange(patch, 22, 23, 0, 63, filt_depth); // filter env + RandomizeRange(patch, 23, 24, 0, 63, filt_depth); // filter lfo + } + + // --- Envelopes (offset 24-47, 3x8 bytes) --- + uint8_t env_depth = (section_depth_[RANDOM_SECTION_ENVELOPES] * global) >> 7; + if (env_depth) { + for (uint8_t env = 0; env < 3; ++env) { + uint8_t base = 24 + env * 8; + RandomizeRange(patch, base, base + 4, 0, 127, env_depth); // ADSR + } + } + + // --- LFOs (within envelope blocks, offset 28-29, 36-37, 44-45) --- + uint8_t lfo_depth = (section_depth_[RANDOM_SECTION_LFOS] * global) >> 7; + if (lfo_depth) { + for (uint8_t lfo = 0; lfo < 3; ++lfo) { + uint8_t base = 24 + lfo * 8; + // LFO shape (offset +4) + if (Random::GetByte() < lfo_depth * 2) { + patch[base + 4] = Random::GetByte() % LFO_WAVEFORM_LAST; + } + // LFO rate (offset +5) + RandomizeRange(patch, base + 5, base + 6, 0, 127, lfo_depth); + } + } + + // --- Modulation matrix (offset 50-91, 14x3 bytes) --- + uint8_t mod_depth = (section_depth_[RANDOM_SECTION_MODULATION] * global) >> 7; + if (mod_depth) { + for (uint8_t mod = 0; mod < 14; ++mod) { + uint8_t base = 50 + mod * 3; + // Only randomize if depth is high enough — mod matrix is sensitive. + if (Random::GetByte() < mod_depth) { + // Source + patch[base] = Random::GetByte() % MOD_SRC_LAST; + } + if (Random::GetByte() < mod_depth) { + // Destination + patch[base + 1] = Random::GetByte() % MOD_DST_LAST; + } + // Amount: always randomize with depth scaling + RandomizeRange(patch, base + 2, base + 3, 0, 126, mod_depth); + } + } + + // Push the modified patch to the voicecards. + part->TouchPatch(); +} + +/* static */ +void Randomizer::UpdateScreen() { + char* buffer = display.line_buffer(0); + // Top row: section labels + for (uint8_t i = 0; i < 40; ++i) { + buffer[i] = pgm_read_byte(random_labels + i); + } + + buffer = display.line_buffer(1); + // Bottom row: depth values for each section. + for (uint8_t i = 0; i < 7; ++i) { + uint8_t pos = i * 5; + uint8_t val = section_depth_[i]; + // Display as 0-99 to fit in 3 chars. + uint8_t display_val = (val * 100) >> 7; + if (display_val > 99) display_val = 99; + buffer[pos] = ' '; + buffer[pos + 1] = display_val >= 10 ? ('0' + display_val / 10) : ' '; + buffer[pos + 2] = '0' + display_val % 10; + buffer[pos + 3] = ' '; + buffer[pos + 4] = ' '; + } + // Last position: show trigger feedback. + if (triggered_) { + buffer[35] = '*'; + buffer[36] = 'G'; + buffer[37] = 'O'; + buffer[38] = '*'; + --triggered_; + } else { + buffer[35] = 'p'; + buffer[36] = 'u'; + buffer[37] = 's'; + buffer[38] = 'h'; + } + buffer[39] = ' '; +} + +/* static */ +void Randomizer::UpdateLeds() { + leds.set_pixel(LED_7, 0x0f); + leds.set_pixel(LED_8, 0x0f); +} + +} // namespace ambika diff --git a/controller/ui_pages/randomizer.h b/controller/ui_pages/randomizer.h new file mode 100644 index 0000000..c7007e7 --- /dev/null +++ b/controller/ui_pages/randomizer.h @@ -0,0 +1,66 @@ +// Copyright 2026 Ambika contributors. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// 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. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ----------------------------------------------------------------------------- +// +// Smart patch randomizer. Each knob controls randomization depth +// for a different section of the patch. Click encoder to trigger. + +#ifndef CONTROLLER_UI_PAGES_RANDOMIZER_H_ +#define CONTROLLER_UI_PAGES_RANDOMIZER_H_ + +#include "controller/ui_pages/ui_page.h" + +namespace ambika { + +// Randomization sections controlled by each knob. +enum RandomSection { + RANDOM_SECTION_OSC, // Knob 1: Oscillator shapes, params, range, detune + RANDOM_SECTION_FILTER, // Knob 2: Filter cutoff, resonance, mode, env/lfo + RANDOM_SECTION_ENVELOPES, // Knob 3: ADSR times for all 3 envelopes + RANDOM_SECTION_MODULATION, // Knob 4: Mod matrix sources, destinations, amounts + RANDOM_SECTION_MIXER, // Knob 5: Mix balance, sub, noise, fuzz, crush + RANDOM_SECTION_LFOS, // Knob 6: LFO shapes, rates + RANDOM_SECTION_DEPTH, // Knob 7: Global depth (scales all others) + RANDOM_SECTION_LAST +}; + +class Randomizer : public UiPage { + public: + Randomizer() { } + + static void OnInit(PageInfo* info); + static uint8_t OnIncrement(int8_t increment); + static uint8_t OnClick(); + static uint8_t OnPot(uint8_t index, uint8_t value); + static uint8_t OnKey(uint8_t key); + + static void UpdateScreen(); + static void UpdateLeds(); + + static const prog_EventHandlers event_handlers_; + + private: + static void Randomize(); + static void RandomizeRange(uint8_t* data, uint8_t start, uint8_t end, + uint8_t min_val, uint8_t max_val, uint8_t depth); + + static uint8_t section_depth_[RANDOM_SECTION_LAST]; + static uint8_t triggered_; + + DISALLOW_COPY_AND_ASSIGN(Randomizer); +}; + +} // namespace ambika + +#endif // CONTROLLER_UI_PAGES_RANDOMIZER_H_ diff --git a/controller/ui_pages/wc_page.cc b/controller/ui_pages/wc_page.cc new file mode 100644 index 0000000..0ffde20 --- /dev/null +++ b/controller/ui_pages/wc_page.cc @@ -0,0 +1,247 @@ +// Copyright 2026 Ambika contributors. +// +// West coast oscillator UI page. + +#include "controller/ui_pages/wc_page.h" + +#include +#include "avrlib/string.h" + +#include "controller/display.h" +#include "controller/leds.h" +#include "controller/multi.h" +#include "controller/voicecard_tx.h" +#include "controller/ui.h" +#include "common/patch.h" + +namespace ambika { + +using namespace avrlib; + +static const uint8_t kWcWaveLast = 2; + +// Page 1: wave fold sym bias | fmdp fmrt colr drve +// Page 2: flds gain env sub | sync +// +// Patch offsets: +// Page 1: +// osc[1].shape (4) = base waveform +// osc[0].parameter (1) = fold depth +// osc[1].parameter (5) = symmetry +// mix_balance (8) = bias +// osc[1].range (6) = FM depth +// osc[1].detune (7) = FM ratio +// mix_parameter (10) = color +// mix_op (9) = drive +// Page 2: +// mix_sub_osc_shape(11) = fold stages +// mix_sub_osc (12) = input gain +// mix_noise (13) = env-to-fold +// mix_fuzz (14) = sub level +// mix_crush (15) = sync amount + +static const uint8_t wc_page1_offsets[] PROGMEM = { + 4, 1, 5, 8, 6, 7, 10, 9, +}; +static const uint8_t wc_page2_offsets[] PROGMEM = { + 11, 12, 13, 14, 15, 0xff, 0xff, 0xff, +}; + +enum WcParamType { + WC_PT_WAVE, + WC_PT_UINT7, + WC_PT_INT8, + WC_PT_NONE, +}; + +static const uint8_t wc_page1_types[] PROGMEM = { + WC_PT_WAVE, WC_PT_UINT7, WC_PT_UINT7, WC_PT_UINT7, + WC_PT_UINT7, WC_PT_INT8, WC_PT_UINT7, WC_PT_UINT7, +}; +static const uint8_t wc_page2_types[] PROGMEM = { + WC_PT_UINT7, WC_PT_UINT7, WC_PT_UINT7, WC_PT_UINT7, + WC_PT_UINT7, WC_PT_NONE, WC_PT_NONE, WC_PT_NONE, +}; + +static const prog_char wc_page1_labels[] PROGMEM = + "wave" "fold" "sym " "bias" + "fmdp" "fmrt" "colr" "drve"; +static const prog_char wc_page2_labels[] PROGMEM = + "flds" "gain" "env " "sub " + "sync" " " " " " "; + +static const prog_char wc_wave_names[] PROGMEM = "sin tri "; + +/* static */ +uint8_t WcPage::page_index_; + +/* static */ +const prog_EventHandlers WcPage::event_handlers_ PROGMEM = { + OnInit, NULL, OnIncrement, OnClick, OnPot, OnKey, + NULL, OnIdle, UpdateScreen, UpdateLeds, NULL, +}; + +/* static */ +uint8_t WcPage::IsWcActive() { + return multi.mutable_part(ui.state().active_part)->mutable_raw_patch_data()[0] + == WAVEFORM_WESTCOAST; +} + +static inline uint8_t* GetPatchData() { + return multi.mutable_part(ui.state().active_part)->mutable_raw_patch_data(); +} + +/* static */ +void WcPage::OnInit(PageInfo* info) { + UiPage::OnInit(info); + active_control_ = 0; + page_index_ = 0; +} + +/* static */ +uint8_t WcPage::OnIncrement(int8_t increment) { + if (edit_mode_ != EDIT_IDLE) { + const uint8_t* offsets = page_index_ ? wc_page2_offsets : wc_page1_offsets; + const uint8_t* types = page_index_ ? wc_page2_types : wc_page1_types; + uint8_t offset = pgm_read_byte(&offsets[active_control_]); + uint8_t type = pgm_read_byte(&types[active_control_]); + if (offset == 0xff || type == WC_PT_NONE) return 1; + + uint8_t* patch = GetPatchData(); + if (type == WC_PT_WAVE) { + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val >= kWcWaveLast) val = kWcWaveLast - 1; + patch[offset] = val; + } else if (type == WC_PT_INT8) { + int16_t val = static_cast(patch[offset]) + increment; + if (val < -24) val = -24; + if (val > 24) val = 24; + patch[offset] = static_cast(val); + } else { + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val > 127) val = 127; + patch[offset] = val; + } + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + edit_mode_ = EDIT_STARTED_BY_ENCODER; + } else { + int8_t new_control = active_control_ + increment; + if (new_control < 0) { + if (page_index_ > 0) { page_index_--; active_control_ = 7; } + } else if (new_control >= 8) { + if (page_index_ < 1) { page_index_++; active_control_ = 0; } + } else { + const uint8_t* types = page_index_ ? wc_page2_types : wc_page1_types; + if (pgm_read_byte(&types[new_control]) != WC_PT_NONE) { + active_control_ = new_control; + } + } + } + return 1; +} + +/* static */ +uint8_t WcPage::OnClick() { + if (edit_mode_ == EDIT_IDLE) { + edit_mode_ = EDIT_STARTED_BY_ENCODER; + } else { + edit_mode_ = EDIT_IDLE; + } + return 1; +} + +/* static */ +uint8_t WcPage::OnPot(uint8_t index, uint8_t value) { + const uint8_t* offsets = page_index_ ? wc_page2_offsets : wc_page1_offsets; + const uint8_t* types = page_index_ ? wc_page2_types : wc_page1_types; + uint8_t offset = pgm_read_byte(&offsets[index]); + uint8_t type = pgm_read_byte(&types[index]); + if (offset == 0xff || type == WC_PT_NONE) return 1; + + uint8_t* patch = GetPatchData(); + if (type == WC_PT_WAVE) { + uint8_t val = (value * kWcWaveLast) >> 7; + if (val >= kWcWaveLast) val = kWcWaveLast - 1; + patch[offset] = val; + } else if (type == WC_PT_INT8) { + patch[offset] = static_cast(static_cast(value) - 64); + } else { + patch[offset] = value; + } + + active_control_ = index; + edit_mode_ = EDIT_STARTED_BY_POT; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + return 1; +} + +/* static */ +uint8_t WcPage::OnKey(uint8_t key) { + if (key == SWITCH_1) { + page_index_ = page_index_ ? 0 : 1; + return 1; + } + return 0; +} + +/* static */ +void WcPage::UpdateScreen() { + const prog_char* labels = page_index_ ? wc_page2_labels : wc_page1_labels; + const uint8_t* offsets = page_index_ ? wc_page2_offsets : wc_page1_offsets; + const uint8_t* types = page_index_ ? wc_page2_types : wc_page1_types; + uint8_t* patch = GetPatchData(); + + for (uint8_t i = 0; i < 8; ++i) { + uint8_t line = i < 4 ? 0 : 1; + uint8_t row = (i & 3) * 10; + char* buffer = display.line_buffer(line) + row; + + if (row != 0) buffer[0] = kDelimiter; + if ((row + 10) != kLcdWidth) buffer[10] = kDelimiter; + + uint8_t type = pgm_read_byte(&types[i]); + if (type == WC_PT_NONE) { + for (uint8_t j = 1; j < 10; ++j) buffer[j] = ' '; + continue; + } + + memcpy_P(&buffer[1], labels + i * 4, 4); + if (i == active_control_ && buffer[1] >= 'a' && buffer[1] <= 'z') { + buffer[1] -= 0x20; + } + buffer[5] = ' '; + + uint8_t offset = pgm_read_byte(&offsets[i]); + uint8_t raw = patch[offset]; + + switch (type) { + case WC_PT_WAVE: { + uint8_t w = raw; + if (w >= kWcWaveLast) w = 0; + memcpy_P(&buffer[6], wc_wave_names + w * 4, 4); + break; + } + case WC_PT_INT8: { + UnsafeItoa(static_cast(raw), 4, &buffer[6]); + AlignRight(&buffer[6], 4); + break; + } + default: + UnsafeItoa(raw, 4, &buffer[6]); + AlignRight(&buffer[6], 4); + break; + } + } +} + +/* static */ +void WcPage::UpdateLeds() { + leds.set_pixel(LED_1, page_index_ == 0 ? 0xf0 : 0x0f); + leds.set_pixel(LED_2, page_index_ == 1 ? 0xf0 : 0x0f); +} + +} // namespace ambika diff --git a/controller/ui_pages/wc_page.h b/controller/ui_pages/wc_page.h new file mode 100644 index 0000000..e7ca0a3 --- /dev/null +++ b/controller/ui_pages/wc_page.h @@ -0,0 +1,35 @@ +// Copyright 2026 Ambika contributors. +// +// West coast oscillator UI page. + +#ifndef CONTROLLER_UI_PAGES_WC_PAGE_H_ +#define CONTROLLER_UI_PAGES_WC_PAGE_H_ + +#include "controller/ui_pages/ui_page.h" + +namespace ambika { + +class WcPage : public UiPage { + public: + WcPage() { } + + static void OnInit(PageInfo* info); + static uint8_t OnIncrement(int8_t increment); + static uint8_t OnClick(); + static uint8_t OnPot(uint8_t index, uint8_t value); + static uint8_t OnKey(uint8_t key); + + static void UpdateScreen(); + static void UpdateLeds(); + + static const prog_EventHandlers event_handlers_; + static uint8_t IsWcActive(); + + private: + static uint8_t page_index_; + DISALLOW_COPY_AND_ASSIGN(WcPage); +}; + +} // namespace ambika + +#endif // CONTROLLER_UI_PAGES_WC_PAGE_H_ diff --git a/flash_sd.sh b/flash_sd.sh new file mode 100755 index 0000000..7334953 --- /dev/null +++ b/flash_sd.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# Flash Ambika firmware to SD card +# Usage: ./flash_sd.sh [mount_point] + +set -e + +SDCARD="${1:-/mnt/sdcard}" +BUILD_DIR="build" +CONTROLLER_BIN="$BUILD_DIR/ambika_controller/ambika_controller.bin" +VOICECARD_BIN="$BUILD_DIR/ambika_voicecard/ambika_voicecard.bin" + +# Check firmware files exist +if [ ! -f "$CONTROLLER_BIN" ]; then + echo "Error: Controller firmware not found at $CONTROLLER_BIN" + echo "Run 'make -f controller/makefile && make -f controller/makefile bin' first" + exit 1 +fi + +if [ ! -f "$VOICECARD_BIN" ]; then + echo "Error: Voicecard firmware not found at $VOICECARD_BIN" + echo "Run 'make all && make bin' first" + exit 1 +fi + +# Find SD card if not mounted +if [ ! -d "$SDCARD" ]; then + echo "Mount point $SDCARD does not exist." + # Try to find and mount the SD card + DEVICE=$(lsblk -rno NAME,FSTYPE | grep vfat | grep mmc | head -1 | awk '{print $1}') + if [ -z "$DEVICE" ]; then + echo "Error: No SD card found" + exit 1 + fi + echo "Found SD card at /dev/$DEVICE" + sudo mkdir -p "$SDCARD" + sudo mount "/dev/$DEVICE" "$SDCARD" + echo "Mounted at $SDCARD" +fi + +# Check it's actually mounted +if ! mountpoint -q "$SDCARD" 2>/dev/null; then + DEVICE=$(lsblk -rno NAME,FSTYPE | grep vfat | grep mmc | head -1 | awk '{print $1}') + if [ -z "$DEVICE" ]; then + echo "Error: No SD card found" + exit 1 + fi + sudo mount "/dev/$DEVICE" "$SDCARD" + echo "Mounted /dev/$DEVICE at $SDCARD" +fi + +echo "Copying firmware to SD card..." +sudo cp "$CONTROLLER_BIN" "$SDCARD/AMBIKA.BIN" +echo " AMBIKA.BIN (controller: $(stat -c%s "$CONTROLLER_BIN") bytes)" + +for i in 1 2 3 4 5 6; do + sudo cp "$VOICECARD_BIN" "$SDCARD/VOICE${i}.BIN" +done +echo " VOICE1-6.BIN (voicecard: $(stat -c%s "$VOICECARD_BIN") bytes)" + +sudo sync +echo "" +echo "Done. Safe to eject SD card." +echo "" +echo "To flash on Ambika:" +echo " 1. Insert SD card" +echo " 2. Go to OS info page (rightmost system page)" +echo " 3. Press S1 to flash controller" +echo " 4. After reboot, flash each voicecard with S4" diff --git a/voicecard/envelope.h b/voicecard/envelope.h index d40b42b..cf03754 100755 --- a/voicecard/envelope.h +++ b/voicecard/envelope.h @@ -67,25 +67,48 @@ class Envelope { uint8_t attack, uint8_t decay, uint8_t sustain, - uint8_t release) { + uint8_t release, + uint8_t curve = ENVELOPE_CURVE_EXPONENTIAL) { + linear_ = (curve == ENVELOPE_CURVE_LINEAR || + curve == ENVELOPE_CURVE_LOOP_LINEAR); + looping_ = (curve == ENVELOPE_CURVE_LOOP || + curve == ENVELOPE_CURVE_LOOP_LINEAR); stage_phase_increment_[ATTACK] = ResourcesManager::Lookup< uint16_t, uint8_t>(lut_res_env_portamento_increments, attack); stage_phase_increment_[DECAY] = ResourcesManager::Lookup< uint16_t, uint8_t>(lut_res_env_portamento_increments, decay); stage_phase_increment_[RELEASE] = ResourcesManager::Lookup< uint16_t, uint8_t>(lut_res_env_portamento_increments, release); - stage_target_[DECAY] = sustain << 1; - stage_target_[SUSTAIN] = stage_target_[DECAY]; + if (looping_) { + // In loop mode, sustain sets the floor of the cycle. + stage_target_[DECAY] = sustain << 1; + stage_target_[SUSTAIN] = stage_target_[DECAY]; + } else { + stage_target_[DECAY] = sustain << 1; + stage_target_[SUSTAIN] = stage_target_[DECAY]; + } } uint8_t Render() { phase_ += phase_increment_; if (phase_ < phase_increment_) { value_ = U8MixU16(a_, b_, 255); - Trigger(++stage_); + uint8_t next_stage = stage_ + 1; + // In loop mode, retrigger attack after decay completes. + // Skip sustain — go straight from decay to attack. + if (looping_ && stage_ == DECAY) { + Trigger(ATTACK); + } else if (looping_ && stage_ == SUSTAIN) { + // Shouldn't reach here, but safety: retrigger. + Trigger(ATTACK); + } else { + Trigger(next_stage); + } } if (phase_increment_) { - uint8_t step = InterpolateSample(wav_res_env_expo, phase_); + uint8_t step = linear_ + ? (phase_ >> 8) + : InterpolateSample(wav_res_env_expo, phase_); value_ = U8MixU16(a_, b_, step); } return value_ >> 8; @@ -110,6 +133,10 @@ class Envelope { // Current value of the envelope. uint16_t value_; + // Envelope curve type. + uint8_t linear_; + uint8_t looping_; + DISALLOW_COPY_AND_ASSIGN(Envelope); }; diff --git a/voicecard/fm4op.h b/voicecard/fm4op.h new file mode 100644 index 0000000..c56569d --- /dev/null +++ b/voicecard/fm4op.h @@ -0,0 +1,344 @@ +// Copyright 2026 Ambika contributors. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// 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. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ----------------------------------------------------------------------------- +// +// 4-operator FM synthesis engine, TX81Z style. + +#ifndef VOICECARD_FM4OP_H_ +#define VOICECARD_FM4OP_H_ + +#include "avrlib/base.h" +#include "avrlib/op.h" +#include "voicecard/resources.h" + +using namespace avrlib; + +namespace ambika { + +// TX81Z waveform types derived from the sine table. +enum FmWaveform { + FM_WAVE_SINE, // W1: Full sine + FM_WAVE_HALF_SINE, // W2: Positive half, then silence + FM_WAVE_ABS_SINE, // W3: Full-wave rectified sine + FM_WAVE_QUARTER_SINE, // W4: First quarter, rest silence + FM_WAVE_HALF_ABS, // W5: Rectified double sine, first half only + FM_WAVE_TRIPLE_ABS, // W6: Three rectified bumps, then silence + FM_WAVE_PULSE_SINE, // W7: Alternating positive/negative bumps + FM_WAVE_SAW_SINE, // W8: Staircase-like derived waveform + FM_WAVE_LAST +}; + +// TX81Z algorithm topologies. +enum FmAlgorithm { + FM_ALG_1, // 4->3->2->1->out (full serial) + FM_ALG_2, // (3+4)->2->1->out + FM_ALG_3, // (4->3)+2 -> 1->out + FM_ALG_4, // (4->3)+(2->1)->out (two parallel pairs) + FM_ALG_5, // (4->2)+(4->3->1)->out + FM_ALG_6, // 4->(1+2+3)->out (one mod, three carriers) + FM_ALG_7, // (4->1)+2+3->out + FM_ALG_8, // 1+2+3+4->out (all carriers, additive) + FM_ALG_LAST +}; + +// Patch field reinterpretation for FM4OP mode. +// These map onto existing patch byte offsets when osc[0].shape == WAVEFORM_FM4OP. +// +// osc[0].shape = WAVEFORM_FM4OP (mode selector) +// osc[0].parameter = algorithm (0-7) +// osc[0].range = op1 coarse ratio +// osc[0].detune = op1 fine detune +// osc[1].shape = op1 waveform (low nibble) | op2 waveform (high nibble) +// osc[1].parameter = op3 waveform (low nibble) | op4 waveform (high nibble) +// osc[1].range = op2 coarse ratio +// osc[1].detune = op2 fine detune +// mix_balance = op3 coarse ratio +// mix_op = op3 fine detune +// mix_parameter = op4 coarse ratio +// mix_sub_osc_shape = op4 fine detune +// mix_sub_osc = op1 output level +// mix_noise = op2 output level +// mix_fuzz = op3 output level +// mix_crush = op4 output level +// padding[0] = feedback level + +struct FmOperator { + uint16_t phase; + uint16_t phase_increment; +}; + +class Fm4Op { + public: + Fm4Op() { } + + void Init() { + for (uint8_t i = 0; i < 4; ++i) { + op_[i].phase = 0; + op_[i].phase_increment = 0; + } + feedback_state_[0] = 0; + feedback_state_[1] = 0; + } + + // Compute the output of a single operator waveform. + static inline uint8_t RenderWaveform(uint8_t waveform, uint16_t phase) { + switch (waveform) { + case FM_WAVE_SINE: + default: + return InterpolateSample(wav_res_sine, phase); + + case FM_WAVE_HALF_SINE: + if (phase < 0x8000) { + return InterpolateSample(wav_res_sine, phase << 1); + } + return 128; + + case FM_WAVE_ABS_SINE: + return InterpolateSample(wav_res_sine, (phase & 0x7FFF) << 1); + + case FM_WAVE_QUARTER_SINE: + if (phase < 0x4000) { + return InterpolateSample(wav_res_sine, phase << 2); + } + return 128; + + case FM_WAVE_HALF_ABS: + if (phase < 0x8000) { + return InterpolateSample(wav_res_sine, (phase & 0x3FFF) << 2); + } + return 128; + + case FM_WAVE_TRIPLE_ABS: + if (phase < 0x8000) { + uint16_t tripled = static_cast(phase * 3); + return InterpolateSample(wav_res_sine, (tripled & 0x7FFF) << 1); + } + return 128; + + case FM_WAVE_PULSE_SINE: { + uint8_t val = InterpolateSample(wav_res_sine, (phase & 0x3FFF) << 2); + if (phase >= 0x8000) { + return 256 - val; + } + return val; + } + + case FM_WAVE_SAW_SINE: + if (phase < 0x4000) { + return InterpolateSample(wav_res_sine, phase << 2); + } else if (phase < 0x8000) { + return 255; + } else if (phase < 0xC000) { + return InterpolateSample(wav_res_sine, phase << 2); + } + return 0; + } + } + + // Render a block of FM4OP audio. + // Reads operator configuration directly from the patch data. + void Render( + uint8_t algorithm, + const uint8_t* op_waveform, // 4 waveform types + const uint8_t* op_level, // 4 output levels + uint8_t feedback, + uint16_t base_increment, + uint8_t* buffer, + uint8_t size) { + + while (size--) { + // Advance all operator phases. + for (uint8_t i = 0; i < 4; ++i) { + op_[i].phase += op_[i].phase_increment; + } + + // Apply feedback to op4 (same as TX81Z convention). + // Feedback state stored centered at 0 (signed). + int16_t fb = (feedback_state_[0] + feedback_state_[1]) >> 1; + int16_t fb_mod = (fb * static_cast(feedback)) >> 4; + + uint8_t op4_out = RenderWaveform(op_waveform[3], + op_[3].phase + static_cast(fb_mod)); + // Store centered at 0 for proper signed feedback. + feedback_state_[1] = feedback_state_[0]; + feedback_state_[0] = static_cast(op4_out - 128); + + // Render remaining operators and route per algorithm. + uint8_t out; + switch (algorithm) { + case FM_ALG_1: { + // 4->3->2->1->out (full serial) + int16_t mod3 = static_cast(op4_out - 128) * + op_level[3]; + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase + mod3); + int16_t mod2 = static_cast(op3_out - 128) * + op_level[2]; + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase + mod2); + int16_t mod1 = static_cast(op2_out - 128) * + op_level[1]; + out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); + break; + } + + case FM_ALG_2: { + // (3+4)->2->1->out + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase); + int16_t mod2 = (static_cast(op4_out - 128) * + op_level[3]) + + (static_cast(op3_out - 128) * op_level[2]); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase + mod2); + int16_t mod1 = static_cast(op2_out - 128) * + op_level[1]; + out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); + break; + } + + case FM_ALG_3: { + // (4->3) + 2 -> 1->out + int16_t mod3 = static_cast(op4_out - 128) * + op_level[3]; + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase + mod3); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase); + int16_t mod1 = (static_cast(op3_out - 128) * + op_level[2]) + + (static_cast(op2_out - 128) * op_level[1]); + out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); + break; + } + + case FM_ALG_4: { + // (4->3) + (2->1) -> out (two parallel pairs) + int16_t mod3 = static_cast(op4_out - 128) * + op_level[3]; + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase + mod3); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase); + int16_t mod1 = static_cast(op2_out - 128) * + op_level[1]; + uint8_t op1_out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); + out = (op1_out >> 1) + (op3_out >> 1); + break; + } + + case FM_ALG_5: { + // (4->2) + (4->3->1) -> out + int16_t mod3 = static_cast(op4_out - 128) * + op_level[3]; + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase + mod3); + int16_t mod1 = static_cast(op3_out - 128) * + op_level[2]; + uint8_t op1_out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); + int16_t mod2 = static_cast(op4_out - 128) * + op_level[3]; + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase + mod2); + out = (op1_out >> 1) + (op2_out >> 1); + break; + } + + case FM_ALG_6: { + // 4->(1+2+3)->out (one mod, three carriers) + int16_t mod = static_cast(op4_out - 128) * + op_level[3]; + uint8_t op1_out = RenderWaveform(op_waveform[0], + op_[0].phase + mod); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase + mod); + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase + mod); + out = (op1_out / 3) + (op2_out / 3) + (op3_out / 3); + break; + } + + case FM_ALG_7: { + // (4->1)+2+3->out + int16_t mod1 = static_cast(op4_out - 128) * + op_level[3]; + uint8_t op1_out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase); + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase); + out = (op1_out / 3) + (op2_out / 3) + (op3_out / 3); + break; + } + + case FM_ALG_8: + default: { + // 1+2+3+4->out (all carriers, additive) + uint8_t op1_out = RenderWaveform(op_waveform[0], + op_[0].phase); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase); + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase); + out = (op1_out >> 2) + (op2_out >> 2) + + (op3_out >> 2) + (op4_out >> 2); + break; + } + } + *buffer++ = out; + } + } + + // Set operator phase increment from a base increment and ratio. + void SetOperatorIncrement(uint8_t op_index, uint16_t base_increment, + int8_t coarse_ratio, int8_t fine_detune) { + // Coarse ratio: map signed range to multiplier. + // 0 = 0.5x, 1 = 1x, 2 = 2x, etc. Negative values for sub-ratios. + uint16_t increment; + if (coarse_ratio <= 0) { + uint8_t shift = 1 - coarse_ratio; + if (shift > 15) shift = 15; + increment = base_increment >> shift; + } else if (coarse_ratio == 1) { + increment = base_increment; + } else { + increment = base_increment * static_cast(coarse_ratio); + } + // Fine detune: small pitch offset. + if (fine_detune > 0) { + increment += (increment >> 8) * fine_detune; + } else if (fine_detune < 0) { + increment -= (increment >> 8) * (-fine_detune); + } + op_[op_index].phase_increment = increment; + } + + FmOperator* mutable_op(uint8_t i) { return &op_[i]; } + + private: + FmOperator op_[4]; + int8_t feedback_state_[2]; + + DISALLOW_COPY_AND_ASSIGN(Fm4Op); +}; + +} // namespace ambika + +#endif // VOICECARD_FM4OP_H_ diff --git a/voicecard/karplus.h b/voicecard/karplus.h new file mode 100644 index 0000000..fedd8c6 --- /dev/null +++ b/voicecard/karplus.h @@ -0,0 +1,267 @@ +// Copyright 2026 Ambika contributors. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// 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. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ----------------------------------------------------------------------------- +// +// Karplus-Strong plucked string synthesis. + +#ifndef VOICECARD_KARPLUS_H_ +#define VOICECARD_KARPLUS_H_ + +#include "avrlib/base.h" +#include "avrlib/op.h" +#include "avrlib/random.h" + +using namespace avrlib; + +namespace ambika { + +static const uint16_t kKarplusBufferSize = 384; + +// Excitation types. +enum KsExcitation { + KS_EXC_NOISE, // White noise burst + KS_EXC_CLICK, // Short impulse + KS_EXC_BRIGHT, // Filtered noise (bright) + KS_EXC_DARK, // Filtered noise (dark) + KS_EXC_LAST +}; + +// Patch field mapping for KS mode: +// osc[0].shape (offset 0) = WAVEFORM_KS_PLUCK +// osc[0].parameter (offset 1) = damping (brightness/decay) +// osc[0].range (offset 2) = pitch range +// osc[0].detune (offset 3) = fine tune +// osc[1].shape (offset 4) = excitation type (KsExcitation) +// osc[1].parameter (offset 5) = excitation color (brightness of burst) +// osc[1].range (offset 6) = decay rate modifier +// osc[1].detune (offset 7) = pluck position (changes harmonics) +// mix_balance (offset 8) = body resonance + +class KarplusStrong { + public: + KarplusStrong() { } + + void Init() { + write_pos_ = 0; + delay_length_ = kKarplusBufferSize; + excited_ = 0; + for (uint16_t i = 0; i < kKarplusBufferSize; ++i) { + delay_line_[i] = 128; + } + } + + // Excite the string. + void Trigger(uint8_t excitation_type, uint8_t color, uint8_t position) { + // Fill delay line based on excitation type. + for (uint16_t i = 0; i < delay_length_; ++i) { + uint8_t sample; + switch (excitation_type) { + case KS_EXC_CLICK: + sample = (i < 4) ? 255 : 128; + break; + case KS_EXC_BRIGHT: { + uint8_t noise = Random::GetByte(); + // High-pass: subtract running average. + sample = (noise >> 1) + 64 + (Random::GetByte() >> 2); + break; + } + case KS_EXC_DARK: { + uint8_t noise = Random::GetByte(); + // Simple low-pass: average with previous. + if (i > 0) { + sample = (noise >> 1) + (delay_line_[i - 1] >> 1); + } else { + sample = noise; + } + break; + } + default: // KS_EXC_NOISE + sample = Random::GetByte(); + break; + } + delay_line_[i] = sample; + } + + // Apply pluck position: create a comb-like notch at position harmonics. + // position 0 = pluck at bridge (all harmonics), 127 = pluck at middle. + if (position > 4) { + uint16_t notch_period = (delay_length_ * position) >> 7; + if (notch_period > 1 && notch_period < delay_length_) { + for (uint16_t i = 0; i < delay_length_ - notch_period; ++i) { + int16_t mixed = (static_cast(delay_line_[i]) + + static_cast(delay_line_[i + notch_period])) >> 1; + delay_line_[i] = static_cast(mixed); + } + } + } + + // Apply excitation color (brightness): low-pass the initial excitation. + // Lower color = darker excitation. + uint8_t filter_passes = (127 - color) >> 4; // 0-7 passes + for (uint8_t pass = 0; pass < filter_passes; ++pass) { + for (uint16_t i = 1; i < delay_length_; ++i) { + delay_line_[i] = (delay_line_[i] >> 1) + (delay_line_[i - 1] >> 1); + } + } + + write_pos_ = 0; + excited_ = 1; + } + + // Simple trigger (backwards compatible). + void Trigger() { + Trigger(KS_EXC_NOISE, 127, 0); + } + + void SetPitch(uint16_t phase_increment) { + if (phase_increment < 256) { + phase_increment = 256; + } + uint16_t len = 65535u / (phase_increment >> 8); + if (len < 2) len = 2; + if (len >= kKarplusBufferSize) len = kKarplusBufferSize - 1; + delay_length_ = len; + } + + // Full render with all parameters. + void Render(uint8_t damping, uint8_t decay, uint8_t body, + uint8_t ens_rate, uint8_t ens_depth, uint8_t ens_spread, + uint8_t ens_mix, uint8_t stiffness, uint8_t feedback, + uint8_t* buffer, uint8_t size) { + if (!excited_) { + while (size--) { + *buffer++ = 128; + } + return; + } + + uint8_t coeff = 32 + (damping << 1); + if (coeff < 32) coeff = 32; + uint8_t decay_amount = decay >> 3; + + // Advance ensemble LFO. + uint16_t lfo_inc = static_cast(ens_rate + 1) << 4; + + while (size--) { + // Advance LFO for ensemble read heads. + ens_lfo_phase_ += lfo_inc; + + // Primary read head (fixed position). + uint16_t read_pos = write_pos_ + 1; + if (read_pos >= delay_length_) read_pos = 0; + + uint8_t current = delay_line_[read_pos]; + uint16_t next_pos = read_pos + 1; + if (next_pos >= delay_length_) next_pos = 0; + uint8_t next = delay_line_[next_pos]; + + // KS low-pass averaging. + uint8_t filtered = ((uint16_t)current * (256 - coeff) + + (uint16_t)next * coeff) >> 8; + + // Stiffness: allpass-like dispersion. + // Mixes current with a sample from a prime-offset position, + // creating inharmonic partials (piano/bell character). + if (stiffness > 0) { + uint16_t stiff_pos = read_pos + 7; // Prime offset. + if (stiff_pos >= delay_length_) stiff_pos -= delay_length_; + uint8_t stiff_sample = delay_line_[stiff_pos]; + filtered = ((uint16_t)filtered * (256 - stiffness) + + (uint16_t)stiff_sample * stiffness) >> 8; + } + + // Apply decay. + if (decay_amount) { + int16_t diff = static_cast(filtered) - 128; + diff -= (diff * decay_amount) >> 8; + filtered = 128 + diff; + } + + // Body resonance. + if (body > 4) { + uint16_t body_pos = read_pos + (delay_length_ >> 1); + if (body_pos >= delay_length_) body_pos -= delay_length_; + uint8_t body_sample = delay_line_[body_pos]; + filtered = ((uint16_t)filtered * (256 - body) + + (uint16_t)body_sample * body) >> 8; + } + + // Extra feedback for longer sustain. + if (feedback > 0) { + int16_t fb_val = static_cast(filtered) - 128; + fb_val += (fb_val * feedback) >> 8; + fb_val += 128; + if (fb_val > 255) fb_val = 255; + if (fb_val < 0) fb_val = 0; + filtered = fb_val; + } + + // Write back to delay line. + delay_line_[read_pos] = filtered; + write_pos_ = read_pos; + + // Ensemble: three read heads from same delay buffer. + uint8_t output = filtered; + if (ens_mix > 0 && ens_depth > 0) { + // LFO triangle: map 16-bit phase to signed 8-bit triangle. + uint8_t lfo_8 = ens_lfo_phase_ >> 8; + int8_t lfo_val = (ens_lfo_phase_ & 0x8000) + ? static_cast(255 - lfo_8) + : static_cast(lfo_8); + + // Head 2 and 3 offsets (signed, relative to read_pos). + int16_t offset2 = (static_cast(lfo_val) * ens_depth) >> 8; + int16_t offset3 = -offset2 + (ens_spread >> 2); + + // Wrap positions into delay line range. + int16_t p2 = (static_cast(read_pos) + offset2) % + static_cast(delay_length_); + if (p2 < 0) p2 += delay_length_; + + int16_t p3 = (static_cast(read_pos) + offset3) % + static_cast(delay_length_); + if (p3 < 0) p3 += delay_length_; + + uint8_t head2 = delay_line_[static_cast(p2)]; + uint8_t head3 = delay_line_[static_cast(p3)]; + + // Mix: dry head + two wet heads. + uint8_t dry = 255 - ens_mix; + uint16_t mixed = (uint16_t)filtered * dry + + ((uint16_t)head2 + (uint16_t)head3) * (ens_mix >> 1); + output = mixed >> 8; + } + + *buffer++ = output; + } + } + + // Simple render (backwards compatible). + void Render(uint8_t damping, uint8_t* buffer, uint8_t size) { + Render(damping, 0, 0, 0, 0, 0, 0, 0, 0, buffer, size); + } + + private: + uint8_t delay_line_[kKarplusBufferSize]; + uint16_t write_pos_; + uint16_t delay_length_; + uint8_t excited_; + uint16_t ens_lfo_phase_; + + DISALLOW_COPY_AND_ASSIGN(KarplusStrong); +}; + +} // namespace ambika + +#endif // VOICECARD_KARPLUS_H_ diff --git a/voicecard/oscillator.cc b/voicecard/oscillator.cc index efd4e28..c7f043d 100755 --- a/voicecard/oscillator.cc +++ b/voicecard/oscillator.cc @@ -157,215 +157,6 @@ void Oscillator::RenderSimpleWavetable(uint8_t* buffer) { } } -// ------- Casio CZ-like synthesis ------------------------------------------- -void Oscillator::RenderCzSaw(uint8_t* buffer) { - BEGIN_SAMPLE_LOOP - UPDATE_PHASE - uint8_t phi = phase.integral >> 8; - uint8_t clipped_phi = phi < 0x20 ? phi << 3 : 0xff; - // Interpolation causes more aliasing here. - *buffer++ = ReadSample(wav_res_sine, - U8MixU16(phi, clipped_phi, parameter_ << 1)); - END_SAMPLE_LOOP -} - -void Oscillator::RenderCzResoSaw(uint8_t* buffer) { - uint16_t increment = phase_increment_.integral + ( - (phase_increment_.integral * uint32_t(parameter_)) >> 2); - uint8_t type = shape_ - WAVEFORM_CZ_SAW_LP; - uint16_t phase_2 = data_.secondary_phase; - BEGIN_SAMPLE_LOOP - UPDATE_PHASE - if (phase.carry) { - phase_2 = ResourcesManager::Lookup( - lut_res_cz_phase_reset, type & 0x03); - } - phase_2 += increment; - uint8_t carrier = ReadSample(wav_res_sine, phase_2); - uint8_t window = ~(phase.integral >> 8); - if (type & 2) { - *buffer++ = S8U8MulShift8(carrier + 128, window) + 128; - } else { - *buffer++ = U8U8MulShift8(carrier, window); - } - END_SAMPLE_LOOP - data_.secondary_phase = phase_2; -} - -void Oscillator::RenderCzResoPulse(uint8_t* buffer) { - uint16_t increment = phase_increment_.integral + ( - (phase_increment_.integral * uint32_t(parameter_)) >> 2); - uint8_t type = shape_ - WAVEFORM_CZ_SAW_LP; - uint16_t phase_2 = data_.secondary_phase; - BEGIN_SAMPLE_LOOP - UPDATE_PHASE - if (phase.carry) { - phase_2 = ResourcesManager::Lookup( - lut_res_cz_phase_reset, type & 0x03); - } - phase_2 += increment; - uint8_t carrier = ReadSample(wav_res_sine, phase_2); - uint8_t window = 0; - if (phase.integral < 0x4000) { - window = 255; - } else if (phase.integral < 0x8000) { - window = ~(phase.integral - 0x4000) >> 6; - } - if (type == 5) { - carrier >>= 1; - carrier += 128; - } - if (type & 2) { - *buffer++ = S8U8MulShift8(carrier + 128, window) + 128; - } else { - *buffer++ = U8U8MulShift8(carrier, window); - } - END_SAMPLE_LOOP - data_.secondary_phase = phase_2; -} - -void Oscillator::RenderCzResoTri(uint8_t* buffer) { - uint16_t increment = phase_increment_.integral + ( - (phase_increment_.integral * uint32_t(parameter_)) >> 2); - uint8_t type = shape_ - WAVEFORM_CZ_SAW_LP; - uint16_t phase_2 = data_.secondary_phase; - BEGIN_SAMPLE_LOOP - UPDATE_PHASE - if (phase.carry) { - phase_2 = ResourcesManager::Lookup( - lut_res_cz_phase_reset, type & 0x03); - } - phase_2 += increment; - uint8_t carrier = ReadSample(wav_res_sine, phase_2); - uint8_t window = (phase.integral & 0x8000) ? - ~static_cast(phase.integral >> 7) : - phase.integral >> 7; - if (type & 2) { - *buffer++ = S8U8MulShift8(carrier + 128, window) + 128; - } else { - *buffer++ = U8U8MulShift8(carrier, window); - } - END_SAMPLE_LOOP - data_.secondary_phase = phase_2; -} - -// ------- FM ---------------------------------------------------------------- -void Oscillator::RenderFm(uint8_t* buffer) { - uint8_t offset = fm_parameter_; - if (offset < 24) { - offset = 0; - } else if (offset > 48) { - offset = 24; - } else { - offset = offset - 24; - } - uint16_t multiplier = ResourcesManager::Lookup( - lut_res_fm_frequency_ratios, offset); - uint16_t increment = ( - static_cast(phase_increment_.integral) * multiplier) >> 8; - parameter_ <<= 1; - - uint16_t phase_2 = data_.secondary_phase; - BEGIN_SAMPLE_LOOP - UPDATE_PHASE - phase_2 += increment; - uint8_t modulator = InterpolateSample(wav_res_sine, - phase_2); - uint16_t modulation = modulator * parameter_; - *buffer++ = InterpolateSample(wav_res_sine, - phase.integral + modulation); - END_SAMPLE_LOOP - data_.secondary_phase = phase_2; -} - -// ------- 8-bit land -------------------------------------------------------- -void Oscillator::Render8BitLand(uint8_t* buffer) { - BEGIN_SAMPLE_LOOP - UPDATE_PHASE - uint8_t x = parameter_; - *buffer++ = (((phase.integral >> 8) ^ (x << 1)) & (~x)) + (x >> 1); - END_SAMPLE_LOOP -} - -void Oscillator::RenderVowel(uint8_t* buffer) { - data_.vw.update = (data_.vw.update + 1) & 3; - if (!data_.vw.update) { - uint8_t offset_1 = U8ShiftRight4(parameter_); - offset_1 = U8U8Mul(offset_1, 7); - uint8_t offset_2 = offset_1 + 7; - uint8_t balance = parameter_ & 15; - - // Interpolate formant frequencies. - for (uint8_t i = 0; i < 3; ++i) { - data_.vw.formant_increment[i] = U8U4MixU12( - ResourcesManager::Lookup( - wav_res_vowel_data, offset_1 + i), - ResourcesManager::Lookup( - wav_res_vowel_data, offset_2 + i), - balance); - data_.vw.formant_increment[i] <<= 3; - } - - // Interpolate formant amplitudes. - // formant_amplitude[3] is an alias of noise_modulation - for (uint8_t i = 0; i < 4; ++i) { - uint8_t amplitude_a = ResourcesManager::Lookup( - wav_res_vowel_data, - offset_1 + 3 + i); - uint8_t amplitude_b = ResourcesManager::Lookup( - wav_res_vowel_data, - offset_2 + 3 + i); - data_.vw.formant_amplitude[i] = U8U4MixU8( - amplitude_a, - amplitude_b, balance); - } - } - - int16_t phase_noise = S8S8Mul(Random::state_msb(), data_.vw.noise_modulation); - BEGIN_SAMPLE_LOOP - int8_t result = 0; - uint8_t phaselet; - - data_.vw.formant_phase[0] += data_.vw.formant_increment[0]; - phaselet = (data_.vw.formant_phase[0] >> 8) & 0xf0; - result = ResourcesManager::Lookup( - wav_res_formant_sine, - phaselet | data_.vw.formant_amplitude[0]); - - data_.vw.formant_phase[1] += data_.vw.formant_increment[1]; - phaselet = (data_.vw.formant_phase[1] >> 8) & 0xf0; - result += ResourcesManager::Lookup( - wav_res_formant_sine, - phaselet | data_.vw.formant_amplitude[1]); - - data_.vw.formant_phase[2] += data_.vw.formant_increment[2]; - phaselet = (data_.vw.formant_phase[2] >> 8) & 0xf0; - result += ResourcesManager::Lookup( - wav_res_formant_square, - phaselet | data_.vw.formant_amplitude[2]); - - result = S8U8MulShift8(result, phase.integral >> 8); - phase.integral -= phase_increment_int.integral; - if ((phase.integral + phase_noise) < phase_increment_int.integral) { - data_.vw.formant_phase[0] = 0; - data_.vw.formant_phase[1] = 0; - data_.vw.formant_phase[2] = 0; - } - uint8_t x = S16ClipS8(4 * result) + 128; - *buffer++ = x; - *buffer++ = x; - size--; - END_SAMPLE_LOOP -} - -// ------- Dirty Pwm (kills kittens) ----------------------------------------- -void Oscillator::RenderDirtyPwm(uint8_t* buffer) { - BEGIN_SAMPLE_LOOP - UPDATE_PHASE - *buffer++ = (phase.integral >> 8) < 127 + parameter_ ? 0 : 255; - END_SAMPLE_LOOP -} - // ------- Quad saw (mit aliasing) ------------------------------------------- void Oscillator::RenderQuadSawPad(uint8_t* buffer) { uint16_t phase_spread = ( @@ -422,84 +213,34 @@ void Oscillator::RenderFilteredNoise(uint8_t* buffer) { data_.no.rng_state = rng_state; } -// The position is freely determined by the parameter -void Oscillator::RenderInterpolatedWavetable(uint8_t* buffer) { - // Which wavetable should we play?. - const prog_uint8_t* wavetable_definition = - wav_res_wavetables + U8U8Mul( - shape_ - WAVEFORM_WAVETABLE_1, - 18); - // Get a 8:8 value with the wave index in the first byte, and the - // balance amount in the second byte. - uint8_t num_steps = ResourcesManager::Lookup( - wavetable_definition, - 0); - uint16_t pointer = U8U8Mul(parameter_ << 1, num_steps); - uint16_t wave_index_1 = ResourcesManager::Lookup( - wavetable_definition, - 1 + (pointer >> 8)); - uint16_t wave_index_2 = ResourcesManager::Lookup( - wavetable_definition, - 2 + (pointer >> 8)); - uint8_t gain = pointer & 0xff; - const prog_uint8_t* wave_1 = wav_res_waves + U8U8Mul( - wave_index_1, - 129); - const prog_uint8_t* wave_2 = wav_res_waves + U8U8Mul( - wave_index_2, - 129); - BEGIN_SAMPLE_LOOP - UPDATE_PHASE_MORE_REGISTERS - *buffer++ = InterpolateTwoTables( - wave_1, - wave_2, - phase.integral >> 1, - ~gain, - gain); - END_SAMPLE_LOOP -} - -// The position is freely determined by the parameter -void Oscillator::RenderWavequence(uint8_t* buffer) { - const prog_uint8_t* wave = wav_res_waves + U8U8Mul( - parameter_, - 129); +// ------- Waveshaping synthesis ---------------------------------------- +void Oscillator::RenderWaveshape(uint8_t* buffer) { BEGIN_SAMPLE_LOOP UPDATE_PHASE - *buffer++ = InterpolateSample(wave, phase.integral >> 1); + // Generate a sine wave, then scale it by the parameter to control + // how hard we drive the waveshaper. + uint8_t sine = InterpolateSample(wav_res_sine, phase.integral); + // Scale the sine around center. Higher parameter = more drive. + int16_t driven = (static_cast(sine) - 128) * + (parameter_ + 1); + driven = (driven >> 6) + 128; + if (driven > 255) driven = 255; + if (driven < 0) driven = 0; + // Run through the distortion waveshaper table. + *buffer++ = ResourcesManager::Lookup( + wav_res_distortion, static_cast(driven)); END_SAMPLE_LOOP } /* static */ const Oscillator::RenderFn Oscillator::fn_table_[] PROGMEM = { - &Oscillator::RenderSilence, - - &Oscillator::RenderSimpleWavetable, - &Oscillator::RenderBandlimitedPwm, - &Oscillator::RenderSimpleWavetable, - &Oscillator::RenderSimpleWavetable, - - &Oscillator::RenderCzSaw, - &Oscillator::RenderCzResoSaw, - &Oscillator::RenderCzResoSaw, - &Oscillator::RenderCzResoSaw, - &Oscillator::RenderCzResoSaw, - &Oscillator::RenderCzResoPulse, - &Oscillator::RenderCzResoPulse, - &Oscillator::RenderCzResoPulse, - &Oscillator::RenderCzResoPulse, - &Oscillator::RenderCzResoTri, - - &Oscillator::RenderQuadSawPad, - - &Oscillator::RenderFm, - - &Oscillator::Render8BitLand, - &Oscillator::RenderDirtyPwm, - &Oscillator::RenderFilteredNoise, - &Oscillator::RenderVowel, - - &Oscillator::RenderInterpolatedWavetable + &Oscillator::RenderSilence, // NONE + &Oscillator::RenderSimpleWavetable, // SAW + &Oscillator::RenderBandlimitedPwm, // SQUARE + &Oscillator::RenderSimpleWavetable, // TRIANGLE + &Oscillator::RenderSimpleWavetable, // SINE + &Oscillator::RenderQuadSawPad, // PAD + &Oscillator::RenderFilteredNoise, // NOISE }; } // namespace diff --git a/voicecard/oscillator.h b/voicecard/oscillator.h index c8ec8b2..05e3373 100755 --- a/voicecard/oscillator.h +++ b/voicecard/oscillator.h @@ -53,14 +53,6 @@ static inline uint8_t InterpolateTwoTables( static const uint8_t kNumZonesFullSampleRate = 6; static const uint8_t kNumZonesHalfSampleRate = 5; -struct VowelSynthesizerState { - uint16_t formant_increment[3]; - uint16_t formant_phase[3]; - uint8_t formant_amplitude[3]; - uint8_t noise_modulation; - uint8_t update; // Update only every kVowelControlRateDecimation-th call. -}; - struct FilteredNoiseState { uint8_t lp_noise_sample; uint16_t rng_state; @@ -72,10 +64,8 @@ struct QuadSawPadState { }; union OscillatorState { - VowelSynthesizerState vw; FilteredNoiseState no; QuadSawPadState qs; - uint16_t secondary_phase; }; typedef void (*OscRenderFn)(uint8_t* buffer); @@ -110,17 +100,18 @@ class Oscillator { } else { RenderBandlimitedPwm(buffer); } - } else { + } else if (shape_ == WAVEFORM_WAVESHAPE) { + RenderWaveshape(buffer); + } else if (shape_ == WAVEFORM_FM4OP || shape_ == WAVEFORM_KS_PLUCK || + shape_ == WAVEFORM_WESTCOAST) { + // Handled in voice.cc; render silence as fallback. + RenderSilence(buffer); + } else if (shape_ < WAVEFORM_LAST) { RenderFn fn; - - uint8_t index = shape_ >= WAVEFORM_WAVETABLE_1 - ? WAVEFORM_WAVETABLE_1 - : shape_; - ResourcesManager::Load(fn_table_, index, &fn); - if (shape_ == WAVEFORM_WAVEQUENCE) { - fn = &Oscillator::RenderWavequence; - } + ResourcesManager::Load(fn_table_, shape_, &fn); (this->*fn)(buffer); + } else { + RenderSilence(buffer); } } @@ -162,18 +153,9 @@ class Oscillator { void RenderSilence(uint8_t* buffer); void RenderBandlimitedPwm(uint8_t* buffer); void RenderSimpleWavetable(uint8_t* buffer); - void RenderCzSaw(uint8_t* buffer); - void RenderCzResoSaw(uint8_t* buffer); - void RenderCzResoPulse(uint8_t* buffer); - void RenderCzResoTri(uint8_t* buffer); - void RenderFm(uint8_t* buffer); - void Render8BitLand(uint8_t* buffer); - void RenderVowel(uint8_t* buffer); - void RenderDirtyPwm(uint8_t* buffer); void RenderQuadSawPad(uint8_t* buffer); void RenderFilteredNoise(uint8_t* buffer); - void RenderInterpolatedWavetable(uint8_t* buffer); - void RenderWavequence(uint8_t* buffer); + void RenderWaveshape(uint8_t* buffer); DISALLOW_COPY_AND_ASSIGN(Oscillator); }; diff --git a/voicecard/transient_generator.h b/voicecard/transient_generator.h index ea24d47..9dd2ae9 100755 --- a/voicecard/transient_generator.h +++ b/voicecard/transient_generator.h @@ -69,7 +69,7 @@ class TransientGenerator { static uint8_t RenderBlow() { decimate_ += 2; if (decimate_ >= 16) { - decimate_ -= 17; + decimate_ -= 16; rng_state_ = rng_state_ * 73 + counter_; if (decimate_ == 0) { --counter_; diff --git a/voicecard/voice.cc b/voicecard/voice.cc index b3ec5aa..db5c9e3 100755 --- a/voicecard/voice.cc +++ b/voicecard/voice.cc @@ -58,6 +58,9 @@ uint8_t Voice::osc2_buffer_[kAudioBlockSize]; uint8_t Voice::sync_state_[kAudioBlockSize]; uint8_t Voice::no_sync_[kAudioBlockSize]; uint8_t Voice::dummy_sync_state_[kAudioBlockSize]; +Fm4Op Voice::fm4op_; +KarplusStrong Voice::karplus_; +WestCoast Voice::westcoast_; /* */ static const prog_Patch init_patch PROGMEM = { @@ -150,10 +153,26 @@ void Voice::TriggerEnvelope(uint8_t index, uint8_t stage) { /* static */ void Voice::Trigger(uint16_t note, uint8_t velocity, uint8_t legato) { pitch_target_ = note; + + // Slop: per-note random pitch offset (patch_.padding[1]). + uint8_t slop = patch_.padding[1]; + if (slop > 0) { + int8_t rnd = static_cast(Random::GetByte()); + // Scale: slop=127 gives ±~8 units (about ±0.5 semitone). + int16_t offset = (static_cast(rnd) * slop) >> 8; + pitch_target_ += offset; + } + if (!part_.legato || !legato) { gate_ = 255; TriggerEnvelope(ATTACK); transient_generator.Trigger(); + if (patch_.osc[0].shape == WAVEFORM_KS_PLUCK) { + karplus_.Trigger( + patch_.osc[1].shape, // excitation type + patch_.osc[1].parameter, // excitation color + patch_.osc[1].detune); // pluck position + } modulation_sources_[MOD_SRC_VELOCITY] = velocity; modulation_sources_[MOD_SRC_RANDOM] = Random::state_msb(); osc_2.Reset(); @@ -329,18 +348,29 @@ inline void Voice::UpdateDestinations() { int8_t attack_mod = U15ShiftRight7(dst_[MOD_DST_ATTACK]) - 64; int8_t decay_mod = U15ShiftRight7(dst_[MOD_DST_DECAY]) - 64; int8_t release_mod = U15ShiftRight7(dst_[MOD_DST_RELEASE]) - 64; + + // Envelope slop: per-voice random ADSR variation. + uint8_t slop = patch_.padding[1]; + int8_t env_slop = 0; + if (slop > 0) { + env_slop = static_cast( + (static_cast(modulation_sources_[MOD_SRC_RANDOM]) - 128) * + slop) >> 8; + } + for (int i = 0; i < kNumEnvelopes; ++i) { int16_t new_attack = patch_.env_lfo[i].attack; - new_attack = Clip(new_attack + attack_mod, 0, 127); + new_attack = Clip(new_attack + attack_mod + env_slop, 0, 127); int16_t new_decay = patch_.env_lfo[i].decay; - new_decay = Clip(new_decay + decay_mod, 0, 127); + new_decay = Clip(new_decay + decay_mod + env_slop, 0, 127); int16_t new_release = patch_.env_lfo[i].release; - new_release = Clip(new_release + release_mod, 0, 127); + new_release = Clip(new_release + release_mod + env_slop, 0, 127); envelope_[i].Update( new_attack, new_decay, patch_.env_lfo[i].sustain, - new_release); + new_release, + patch_.env_lfo[i].envelope_curve); } voice_lfo_.set_phase_increment( @@ -362,14 +392,135 @@ inline void Voice::RenderOscillators() { // -0.5 / +0.5 semitones by the vibrato and pitch bend (fine). base_pitch += (dst_[MOD_DST_OSC_1_2_COARSE] - 8192) >> 4; base_pitch += (dst_[MOD_DST_OSC_1_2_FINE] - 8192) >> 7; - + + // --- 4-op FM mode --- + if (patch_.osc[0].shape == WAVEFORM_FM4OP) { + uint16_t base_increment = ComputePhaseIncrement(base_pitch); + + // Set up operator phase increments from patch fields. + // Op1: osc[0].range (coarse), osc[0].detune (fine) + fm4op_.SetOperatorIncrement(0, base_increment, + patch_.osc[0].range, patch_.osc[0].detune); + // Op2: osc[1].range (coarse), osc[1].detune (fine) + fm4op_.SetOperatorIncrement(1, base_increment, + patch_.osc[1].range, patch_.osc[1].detune); + // Op3: mix_balance (coarse), mix_op (fine) + fm4op_.SetOperatorIncrement(2, base_increment, + static_cast(patch_.mix_balance), + static_cast(patch_.mix_op)); + // Op4: mix_parameter (coarse), mix_sub_osc_shape (fine) + fm4op_.SetOperatorIncrement(3, base_increment, + static_cast(patch_.mix_parameter), + static_cast(patch_.mix_sub_osc_shape)); + + // Extract operator waveforms from packed nibbles. + uint8_t op_waveform[4]; + op_waveform[0] = patch_.osc[1].shape & 0x0F; + op_waveform[1] = (patch_.osc[1].shape >> 4) & 0x0F; + op_waveform[2] = patch_.osc[1].parameter & 0x0F; + op_waveform[3] = (patch_.osc[1].parameter >> 4) & 0x0F; + // Clamp to valid range. + for (uint8_t i = 0; i < 4; ++i) { + if (op_waveform[i] >= FM_WAVE_LAST) { + op_waveform[i] = FM_WAVE_SINE; + } + } + + // Operator output levels. + uint8_t op_level[4]; + op_level[0] = patch_.mix_sub_osc; + op_level[1] = patch_.mix_noise; + op_level[2] = patch_.mix_fuzz; + op_level[3] = patch_.mix_crush; + + // Modulate total FM depth via PARAMETER_1 mod destination. + uint8_t depth_mod = U15ShiftRight7(dst_[MOD_DST_PARAMETER_1]); + for (uint8_t i = 0; i < 4; ++i) { + op_level[i] = U8U8MulShift8(op_level[i], depth_mod); + } + + // Algorithm from osc[0].parameter. + uint8_t algorithm = patch_.osc[0].parameter; + if (algorithm >= FM_ALG_LAST) { + algorithm = FM_ALG_1; + } + + // Feedback from padding[0]. + uint8_t feedback = patch_.padding[0]; + + // Render into buffer. + fm4op_.Render(algorithm, op_waveform, op_level, feedback, + base_increment, buffer_, kAudioBlockSize); + + // Fill osc2 buffer with silence so the mixer doesn't add garbage. + memset(osc2_buffer_, 128, kAudioBlockSize); + return; + } + + // --- Karplus-Strong mode --- + if (patch_.osc[0].shape == WAVEFORM_KS_PLUCK) { + int16_t ks_pitch = base_pitch + S8U8Mul(patch_.osc[0].range, 128) + + patch_.osc[0].detune; + uint16_t ks_increment = ComputePhaseIncrement(ks_pitch); + karplus_.SetPitch(ks_increment); + + // KS parameters from patch fields. + uint8_t damping = U15ShiftRight7(dst_[MOD_DST_PARAMETER_1]); + uint8_t decay = patch_.osc[1].range; // decay rate + uint8_t body = patch_.mix_balance; // body resonance + // Page 2 params: ensemble + stiffness + feedback. + uint8_t ens_rate = patch_.mix_op; // ensemble rate + uint8_t ens_depth = patch_.mix_parameter; // ensemble depth + uint8_t ens_spread = patch_.mix_sub_osc_shape; // ensemble spread + uint8_t ens_mix = patch_.mix_sub_osc; // ensemble wet/dry + uint8_t stiffness = patch_.mix_noise; // string stiffness + uint8_t feedback = patch_.mix_fuzz; // extra feedback + karplus_.Render(damping, decay, body, + ens_rate, ens_depth, ens_spread, ens_mix, + stiffness, feedback, + buffer_, kAudioBlockSize); + + memset(osc2_buffer_, 128, kAudioBlockSize); + return; + } + + // --- West Coast mode --- + if (patch_.osc[0].shape == WAVEFORM_WESTCOAST) { + int16_t wc_pitch = base_pitch + S8U8Mul(patch_.osc[0].range, 128) + + patch_.osc[0].detune; + uint16_t wc_increment = ComputePhaseIncrement(wc_pitch); + + uint8_t fold = U15ShiftRight7(dst_[MOD_DST_PARAMETER_1]); + uint8_t env_val = modulation_sources_[MOD_SRC_ENV_1]; + westcoast_.Render( + patch_.osc[1].shape, // base waveform (sin/tri) + fold, // fold depth (modulatable) + patch_.osc[1].parameter, // symmetry + patch_.mix_balance, // bias + patch_.osc[1].range, // FM depth + patch_.osc[1].detune, // FM ratio + patch_.mix_op, // drive + patch_.mix_parameter, // color + patch_.mix_sub_osc_shape, // fold stages + patch_.mix_sub_osc, // input gain + patch_.mix_noise, // env-to-fold + patch_.mix_fuzz, // sub level + patch_.mix_crush, // sync amount + env_val, // envelope value for env-to-fold + wc_increment, + buffer_, + kAudioBlockSize); + + memset(osc2_buffer_, 128, kAudioBlockSize); + return; + } + + // --- Normal oscillator rendering --- // Update the oscillator parameters. for (uint8_t i = 0; i < kNumOscillators; ++i) { int16_t pitch = base_pitch; // -36 / +36 semitones by the range controller. - if (patch_.osc[i].shape != WAVEFORM_FM) { - pitch += S8U8Mul(patch_.osc[i].range, 128); - } + pitch += S8U8Mul(patch_.osc[i].range, 128); // -1 / +1 semitones by the detune controller. pitch += patch_.osc[i].detune; // -16 / +16 semitones by the routed modulations. @@ -437,12 +588,21 @@ void Voice::ProcessBlock() { } RenderOscillators(); + + uint8_t is_fm4op = (patch_.osc[0].shape == WAVEFORM_FM4OP); + uint8_t is_special = is_fm4op || + (patch_.osc[0].shape == WAVEFORM_KS_PLUCK) || + (patch_.osc[0].shape == WAVEFORM_WESTCOAST); + + // In FM4OP mode, skip oscillator mixing and sub-osc (those patch fields + // are reinterpreted as FM parameters). + if (!is_special) { uint8_t op = patch_.mix_op; uint8_t osc_2_gain = U14ShiftRight6(dst_[MOD_DST_MIX_BALANCE]); uint8_t osc_1_gain = ~osc_2_gain; uint8_t wet_gain = U14ShiftRight6(dst_[MOD_DST_MIX_PARAM]); uint8_t dry_gain = ~wet_gain; - + // Mix oscillators. switch (op) { case OP_RING_MOD: @@ -507,33 +667,43 @@ void Voice::ProcessBlock() { sub_gain <<= 1; transient_generator.Render(patch_.mix_sub_osc_shape, buffer_, sub_gain); } + } // end if (!is_fm4op) - uint8_t noise = Random::state_msb(); - uint8_t noise_gain = U15ShiftRight7(dst_[MOD_DST_MIX_NOISE]); - uint8_t signal_gain = ~noise_gain; - wet_gain = U14ShiftRight6(dst_[MOD_DST_MIX_FUZZ]); - dry_gain = ~wet_gain; - - // Mix with noise, and apply distortion. The loop processes samples by 2 to - // avoid some of the overhead of audio_buffer.Overwrite() - for (uint8_t i = 0; i < kAudioBlockSize;) { - uint8_t signal_noise_a, signal_noise_b; - noise = (noise * 73) + 1; - signal_noise_a = U8Mix(buffer_[i++], noise, signal_gain, noise_gain); - uint8_t a = U8Mix( - signal_noise_a, - ResourcesManager::Lookup( - wav_res_distortion, signal_noise_a), - dry_gain, wet_gain); - - noise = (noise * 73) + 1; - signal_noise_b = U8Mix(buffer_[i++], noise, signal_gain, noise_gain); - uint8_t b = U8Mix( - signal_noise_b, + // Post-mix processing. + if (is_special) { + // In FM4OP/KS mode, skip noise/fuzz post-processing. + // Write directly to audio buffer. + for (uint8_t i = 0; i < kAudioBlockSize; i += 2) { + audio_buffer.Overwrite2(buffer_[i], buffer_[i + 1]); + } + } else { + uint8_t noise = Random::state_msb(); + uint8_t noise_gain = U15ShiftRight7(dst_[MOD_DST_MIX_NOISE]); + uint8_t signal_gain = ~noise_gain; + uint8_t post_wet = U14ShiftRight6(dst_[MOD_DST_MIX_FUZZ]); + uint8_t post_dry = ~post_wet; + + // Mix with noise, and apply distortion. The loop processes samples by 2 to + // avoid some of the overhead of audio_buffer.Overwrite() + for (uint8_t i = 0; i < kAudioBlockSize;) { + uint8_t signal_noise_a, signal_noise_b; + noise = (noise * 73) + 1; + signal_noise_a = U8Mix(buffer_[i++], noise, signal_gain, noise_gain); + uint8_t a = U8Mix( + signal_noise_a, ResourcesManager::Lookup( - wav_res_distortion, signal_noise_b), - dry_gain, wet_gain); - audio_buffer.Overwrite2(a, b); + wav_res_distortion, signal_noise_a), + post_dry, post_wet); + + noise = (noise * 73) + 1; + signal_noise_b = U8Mix(buffer_[i++], noise, signal_gain, noise_gain); + uint8_t b = U8Mix( + signal_noise_b, + ResourcesManager::Lookup( + wav_res_distortion, signal_noise_b), + post_dry, post_wet); + audio_buffer.Overwrite2(a, b); + } } } diff --git a/voicecard/voice.h b/voicecard/voice.h index 5954150..b8b3350 100755 --- a/voicecard/voice.h +++ b/voicecard/voice.h @@ -24,6 +24,9 @@ #include "common/patch.h" #include "voicecard/envelope.h" +#include "voicecard/fm4op.h" +#include "voicecard/karplus.h" +#include "voicecard/westcoast.h" namespace ambika { @@ -33,6 +36,22 @@ static const int16_t kHighestNote = 120 * 128; static const int16_t kOctave = 12 * 128; static const int16_t kPitchTableStart = 116 * 128; +static inline uint16_t ComputePhaseIncrement(int16_t pitch) { + if (pitch >= kHighestNote) pitch = kHighestNote; + int16_t ref_pitch = pitch - kPitchTableStart; + uint8_t num_shifts = 0; + while (ref_pitch < 0) { + ref_pitch += kOctave; + ++num_shifts; + } + uint16_t increment = ResourcesManager::Lookup( + lut_res_oscillator_increments, ref_pitch >> 1); + while (num_shifts--) { + increment >>= 1; + } + return increment; +} + // This mirrors the beginning of the Part data structure in the controller. struct Part { uint8_t volume; @@ -131,6 +150,10 @@ class Voice { static uint8_t no_sync_[kAudioBlockSize]; static uint8_t dummy_sync_state_[kAudioBlockSize]; + static Fm4Op fm4op_; + static KarplusStrong karplus_; + static WestCoast westcoast_; + DISALLOW_COPY_AND_ASSIGN(Voice); }; diff --git a/voicecard/voicecard_rx.h b/voicecard/voicecard_rx.h index 461dbea..332023d 100755 --- a/voicecard/voicecard_rx.h +++ b/voicecard/voicecard_rx.h @@ -136,6 +136,9 @@ class VoicecardProtocolRx { // Stop doing anything else Timer<2>::Stop(); uint8_t size = spi_.Read(); + if (size > sizeof(Patch)) { + size = sizeof(Patch); + } uint8_t* data = data_ptr_ = voice.mutable_patch_data(); while (size--) { *data++ = spi_.Read(); diff --git a/voicecard/westcoast.h b/voicecard/westcoast.h new file mode 100644 index 0000000..3d672f2 --- /dev/null +++ b/voicecard/westcoast.h @@ -0,0 +1,231 @@ +// Copyright 2026 Ambika contributors. +// +// West coast (Buchla-style) oscillator with wavefolder and FM. + +#ifndef VOICECARD_WESTCOAST_H_ +#define VOICECARD_WESTCOAST_H_ + +#include "avrlib/base.h" +#include "avrlib/op.h" +#include "voicecard/resources.h" + +using namespace avrlib; + +namespace ambika { + +enum WcWaveform { + WC_WAVE_SINE, + WC_WAVE_TRIANGLE, + WC_WAVE_LAST +}; + +// Patch field mapping for west coast mode: +// Page 1: +// osc[0].shape (0) = WAVEFORM_WESTCOAST +// osc[0].parameter (1) = fold depth +// osc[0].range (2) = pitch range +// osc[0].detune (3) = fine tune +// osc[1].shape (4) = base waveform +// osc[1].parameter (5) = symmetry +// osc[1].range (6) = FM depth +// osc[1].detune (7) = FM ratio +// mix_balance (8) = bias +// mix_op (9) = drive +// mix_parameter (10) = color +// Page 2: +// mix_sub_osc_shape(11) = fold stages (1-6) +// mix_sub_osc (12) = input gain +// mix_noise (13) = envelope-to-fold amount +// mix_fuzz (14) = sub-harmonic level +// mix_crush (15) = sync amount +// padding[1] (105)= pitch range (moved) + +class WestCoast { + public: + WestCoast() { } + + void Init() { + phase_ = 0; + mod_phase_ = 0; + sub_phase_ = 0; + sync_phase_ = 0; + prev_sample_ = 128; + } + + // Multi-stage wavefolder with variable stages. + static inline uint8_t Fold(int16_t input, uint8_t fold_depth, + uint8_t symmetry, uint8_t bias, + uint8_t stages) { + input += static_cast(bias - 64); + + int16_t gained = input; + if (fold_depth > 0) { + int32_t g = static_cast(input) * (128 + fold_depth * 3); + gained = g >> 7; + } + + if (symmetry != 64) { + gained += (static_cast(symmetry) - 64); + } + + int16_t folded = gained; + if (stages < 1) stages = 1; + if (stages > 6) stages = 6; + + for (uint8_t i = 0; i < stages; ++i) { + if (folded > 127) { + folded = 254 - folded; + } else if (folded < -128) { + folded = -256 - folded; + } else { + break; + } + } + + if (folded > 127) folded = 127; + if (folded < -128) folded = -128; + + return static_cast(folded + 128); + } + + void Render( + uint8_t base_waveform, + uint8_t fold_depth, + uint8_t symmetry, + uint8_t bias, + uint8_t fm_depth, + int8_t fm_ratio, + uint8_t drive, + uint8_t color, + uint8_t fold_stages, + uint8_t input_gain, + uint8_t env_to_fold, + uint8_t sub_level, + uint8_t sync_amount, + uint8_t env_value, + uint16_t phase_increment, + uint8_t* buffer, + uint8_t size) { + + // FM modulator increment. + uint16_t mod_increment; + if (fm_ratio <= 0) { + uint8_t shift = 1 - fm_ratio; + if (shift > 15) shift = 15; + mod_increment = phase_increment >> shift; + } else if (fm_ratio == 1) { + mod_increment = phase_increment; + } else { + mod_increment = phase_increment * static_cast(fm_ratio); + } + + // Sub-oscillator: one octave down. + uint16_t sub_increment = phase_increment >> 1; + + // Fold stages: 1-6, mapped from 0-127. + uint8_t stages = fold_stages > 0 ? (fold_stages >> 5) + 1 : 1; + if (stages > 6) stages = 6; + + // Pre-fold drive + input gain. + uint8_t effective_fold = fold_depth; + uint16_t total_boost = drive + input_gain; + if (total_boost > 0) { + uint16_t boosted = fold_depth + (total_boost >> 2); + if (boosted > 127) boosted = 127; + effective_fold = boosted; + } + + // Envelope-to-fold: modulate fold depth by envelope value. + if (env_to_fold > 0) { + uint16_t env_mod = (static_cast(env_value) * env_to_fold) >> 7; + uint16_t modded = effective_fold + env_mod; + if (modded > 127) modded = 127; + effective_fold = modded; + } + + // Sync increment (for self-sync, faster than fundamental). + uint16_t sync_increment = 0; + if (sync_amount > 0) { + sync_increment = phase_increment + + ((static_cast(phase_increment) * sync_amount) >> 5); + } + + // Color filter coefficient. + uint8_t color_clipped = (color > 127) ? 127 : color; + uint8_t lp_coeff = (127 - color_clipped) << 1; + if (lp_coeff < 4) lp_coeff = 4; + + while (size--) { + // FM modulation. + mod_phase_ += mod_increment; + uint16_t fm_mod = 0; + if (fm_depth > 0) { + uint8_t mod_val = InterpolateSample(wav_res_sine, mod_phase_); + fm_mod = static_cast(mod_val - 128) * fm_depth; + } + + // Self-sync: reset phase when sync oscillator wraps. + if (sync_amount > 0) { + uint16_t old_sync = sync_phase_; + sync_phase_ += sync_increment; + if (sync_phase_ < old_sync) { + phase_ = 0; + } + } + + // Base oscillator. + phase_ += phase_increment; + int16_t sample; + + if (base_waveform == WC_WAVE_TRIANGLE) { + uint16_t tri_phase = phase_ + fm_mod; + if (tri_phase & 0x8000) { + sample = static_cast(tri_phase >> 7) - 128; + } else { + sample = 127 - static_cast(tri_phase >> 7); + } + } else { + uint8_t sine = InterpolateSample(wav_res_sine, phase_ + fm_mod); + sample = static_cast(sine) - 128; + } + + // Apply input gain to the raw waveform before folding. + if (input_gain > 0) { + sample = (sample * (128 + input_gain)) >> 7; + } + + // Wavefolder. + uint8_t folded = Fold(sample, effective_fold, symmetry, bias, stages); + + // Post-fold color filter. + if (color < 120) { + folded = ((uint16_t)folded * (256 - lp_coeff) + + (uint16_t)prev_sample_ * lp_coeff) >> 8; + } + prev_sample_ = folded; + + // Mix in sub-harmonic. + if (sub_level > 0) { + sub_phase_ += sub_increment; + uint8_t sub = InterpolateSample(wav_res_sine, sub_phase_); + folded = ((uint16_t)folded * (256 - sub_level) + + (uint16_t)sub * sub_level) >> 8; + } + + *buffer++ = folded; + } + } + + private: + uint16_t phase_; + uint16_t mod_phase_; + uint16_t sub_phase_; + uint16_t sync_phase_; + uint8_t prev_sample_; + + DISALLOW_COPY_AND_ASSIGN(WestCoast); +}; + +} // namespace ambika + +#endif // VOICECARD_WESTCOAST_H_ From caebd690c3714e2c997a2d37a74ef798ca49b176 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Sat, 18 Apr 2026 13:51:01 +0200 Subject: [PATCH 13/67] Add Carcosa v1.4 manual --- CARCOSA.md | 276 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 CARCOSA.md diff --git a/CARCOSA.md b/CARCOSA.md new file mode 100644 index 0000000..b632ff5 --- /dev/null +++ b/CARCOSA.md @@ -0,0 +1,276 @@ +# Carcosa v1.4 +## Custom firmware for the Ambika polysynth + +Carcosa replaces the stock Ambika firmware with a focused set of synthesis engines, expanded envelope capabilities, and a streamlined interface. + +Based on the original Ambika firmware by Emilie Gillet (Mutable Instruments). + +--- + +## Synthesis Modes + +Carcosa has two categories of sound engines: **classic** dual-oscillator modes and **special** single-voice modes. + +### Classic Modes + +Classic modes work like the original Ambika. Two independent oscillators mixed together, processed through the analog filter. + +| Waveform | Parameter knob | +|----------|---------------| +| **none** | Silence | +| **saw** | — | +| **square** | Pulse width (0 = 50% square, 1-127 = variable PWM) | +| **triangle** | Waveshaping amount | +| **sine** | — | +| **pad** | Detune spread (4 detuned saws, supersaw-style) | +| **noise** | Filter color (0-63 = low-pass, 64-127 = high-pass) | + +In classic modes, both osc1 and osc2 are active. The mixer page controls balance, crossmod, sub oscillator, noise, fuzz, and crush. + +### Special Modes + +Special modes take over the entire voice. The mixer page parameters are repurposed for that engine's controls. Each has a dedicated UI with 1-2 pages of parameters. + +#### 4-Op FM (fm4op) + +TX81Z-style frequency modulation with 4 operators. + +**Page 1:** + +| Knob | Label | Range | Description | +|------|-------|-------|-------------| +| 1 | algo | 1-8 | Algorithm (operator routing topology) | +| 2 | fbk | 0-127 | Feedback on operator 4 | +| 3 | wav1 | sin-saw | Operator 1 waveform (8 TX81Z waveforms) | +| 4 | wav2 | sin-saw | Operator 2 waveform | +| 5 | rat1 | 0.5-127 | Operator 1 frequency ratio | +| 6 | fin1 | -64/+63 | Operator 1 fine detune | +| 7 | rat2 | 0.5-127 | Operator 2 frequency ratio | +| 8 | fin2 | -64/+63 | Operator 2 fine detune | + +**Page 2:** + +| Knob | Label | Range | Description | +|------|-------|-------|-------------| +| 1 | wav3 | sin-saw | Operator 3 waveform | +| 2 | wav4 | sin-saw | Operator 4 waveform | +| 3 | lvl1 | 0-127 | Operator 1 output level | +| 4 | lvl2 | 0-127 | Operator 2 output level | +| 5 | rat3 | 0.5-127 | Operator 3 frequency ratio | +| 6 | fin3 | -64/+63 | Operator 3 fine detune | +| 7 | lvl3 | 0-127 | Operator 3 output level | +| 8 | lvl4 | 0-127 | Operator 4 output level | + +**The 8 algorithms:** + +``` +1: 4->3->2->1 Full serial chain +2: (3+4)->2->1 Two modulators into carrier chain +3: (4->3)+2->1 Parallel mod + carrier into final carrier +4: (4->3)+(2->1) Two parallel pairs +5: (4->2)+(4->3->1) Shared modulator, two paths +6: 4->(1+2+3) One modulator, three carriers +7: (4->1)+2+3 One modulated + two free carriers +8: 1+2+3+4 All carriers (additive) +``` + +**Operator waveforms:** sin, half, abs, qtr, habs, tri, pls, saw + +The mod matrix destination `prm1` controls total FM depth and can be modulated by envelopes/LFOs for dynamic timbral control. + +#### Karplus-Strong (pluck) + +Physical model of a plucked string. A noise burst excites a tuned delay line with filtered feedback. + +**Page 1:** + +| Knob | Label | Range | Description | +|------|-------|-------|-------------| +| 1 | damp | 0-127 | Damping (high = darker, faster decay) | +| 2 | colr | 0-127 | Excitation brightness | +| 3 | dcay | 0-127 | Overall decay rate | +| 4 | body | 0-127 | Body resonance (comb filter at half delay) | +| 5 | rang | -24/+24 | Pitch range (semitones) | +| 6 | tune | -64/+63 | Fine tuning | +| 7 | pos | 0-127 | Pluck position (changes harmonic content) | +| 8 | exc | nois/clic/brit/dark | Excitation type | + +**Page 2:** + +| Knob | Label | Range | Description | +|------|-------|-------|-------------| +| 1 | rate | 0-127 | Ensemble LFO speed | +| 2 | dpth | 0-127 | Ensemble depth (3 read heads from same delay) | +| 3 | sprd | 0-127 | Ensemble spread (phase offset between heads) | +| 4 | wmix | 0-127 | Ensemble wet/dry mix | +| 5 | stif | 0-127 | String stiffness (inharmonic partials, bell-like) | +| 6 | feed | 0-127 | Extra feedback (longer sustain, self-oscillation) | + +The `prm1` mod destination controls damping and can be modulated for expressive playing. + +#### West Coast (wcoast) + +Buchla-style wavefolder with built-in FM and sub-oscillator. + +**Page 1:** + +| Knob | Label | Range | Description | +|------|-------|-------|-------------| +| 1 | wave | sin/tri | Base waveform | +| 2 | fold | 0-127 | Fold depth (harmonics increase with depth) | +| 3 | sym | 0-127 | Symmetry (even/odd harmonic balance, 64=center) | +| 4 | bias | 0-127 | DC offset into folder | +| 5 | fmdp | 0-127 | FM modulation depth | +| 6 | fmrt | -24/+24 | FM modulator ratio | +| 7 | colr | 0-127 | Post-fold brightness | +| 8 | drve | 0-127 | Pre-fold drive | + +**Page 2:** + +| Knob | Label | Range | Description | +|------|-------|-------|-------------| +| 1 | flds | 0-127 | Number of fold stages (1-6) | +| 2 | gain | 0-127 | Input gain before folding | +| 3 | env | 0-127 | Envelope-to-fold depth (timbral VCA) | +| 4 | sub | 0-127 | Sub-oscillator level (one octave down) | +| 5 | sync | 0-127 | Self-sync amount (metallic tones) | + +Route an envelope to `prm1` (fold depth) for the classic west coast behavior where timbre and amplitude are linked. + +#### Waveshaping (wshape) + +Drives a sine wave through a nonlinear waveshaper. Works as a normal oscillator (can be used on osc1 or osc2 alongside other waveforms). + +| Knob | Description | +|------|-------------| +| prm | Drive amount (0 = clean sine, 127 = heavily shaped) | +| rng | Pitch range | +| tun | Fine tuning | + +--- + +## Navigation + +### Classic mode (osc/mixer pages) + +- **S1 press**: Toggle between oscillator page and mixer page +- **Encoder**: Scroll through parameters, click to edit + +### Entering special modes + +- **Hold S1 + turn encoder**: Cycle through classic -> fm4op -> pluck -> wcoast +- This works from the osc page or any special mode page + +### Inside special mode pages + +- **S1 press**: Toggle between page 1 and page 2 +- **Encoder**: Scroll through parameters on the current page, click to edit +- **Knobs 1-8**: Direct control of the 8 parameters shown on screen +- **Hold S1 + turn encoder**: Switch to a different synthesis mode + +### Other pages + +All other pages (filter, envelope, modulation, part, arpeggiator, multi, performance, system) work exactly like the original Ambika. + +--- + +## Envelopes + +Each of the 3 envelopes has a selectable curve mode on the ENV/LFO page (knob 4, labeled `mode`): + +| Mode | Behavior | +|------|----------| +| **exp** | Exponential ADSR (original Ambika behavior) | +| **lin** | Linear ADSR (SH-101 style, snappy) | +| **loop** | Cycling AD with exponential curves (Maths/function generator) | +| **lpln** | Cycling AD with linear curves (triangle/ramp LFO) | + +In loop modes: +- **attk** = rise time +- **dec** = fall time +- **sus** = loop floor (how low the cycle goes before retriggering) +- **rel** = release time (when key is released, looping stops and envelope falls to zero) + +Each envelope's mode is independent. Use exponential ADSR on the VCA, looping linear on the filter, etc. + +--- + +## Slop + +The **slop** parameter on the part page (knob 3) adds per-note random variation to pitch and envelope timing. This simulates the analog component drift that gives vintage polysynths their character. + +- **0**: Pristine digital (no variation) +- **Low values (10-30)**: Subtle warmth, slight detuning between voices +- **High values (60-127)**: Wobbly, unstable, heavily vintage + +Each note-on generates fresh random offsets. In a chord, each voice drifts differently. + +--- + +## Randomizer + +Access via the performance button group (press 3 times: performance -> knob assign -> randomizer). + +| Knob | Section | +|------|---------| +| 1 | Oscillator randomization depth | +| 2 | Filter randomization depth | +| 3 | Envelope randomization depth | +| 4 | Modulation matrix randomization depth | +| 5 | Mixer randomization depth | +| 6 | LFO randomization depth | +| 7 | Global depth (scales all others) | +| 8 | Shows "push" — click encoder to trigger | + +Set knobs to 0 to lock that section. Turn up for more variation. Click the encoder to generate a new random patch. Each click produces a different result. + +--- + +## Firmware Update + +### Via SD Card + +1. Run `./flash_sd.sh /mnt/sdcard` (or copy `AMBIKA.BIN` and `VOICE1-6.BIN` manually) +2. Insert SD card into Ambika +3. **Hold S8 during power-on** to flash the controller +4. After reboot, go to OS info page, select each voicecard port (1-6), press S4 to flash + +Flash the controller first, then the voicecards. + +### Recovery + +If the firmware crashes on boot, **hold S8 during power-on** to force the bootloader to read from SD card. This always works regardless of firmware state. + +To revert to stock Ambika firmware, place the original `AMBIKA.BIN` and `VOICE*.BIN` files on the SD card and flash. + +--- + +## Technical Notes + +### Memory Usage + +| | Voicecard (ATmega328p) | Controller (ATmega644p) | +|---|---|---| +| Flash | 29,806 / 32,256 (92%) | 57,504 / 61,440 (94%) | +| RAM | 1,458 / 2,048 (71%) | 3,828 / 4,096 (93%) | + +### Changes from Stock Ambika + +**Removed:** CZ synthesis (9 variants), old 2-op FM, 8-bit land, dirty PWM, vowel synthesis, wavetable oscillators (16 + wavequence), step sequencer. + +**Added:** 4-op FM, Karplus-Strong, west coast wavefolder, waveshaping, looping envelopes, analog slop, smart randomizer, dedicated UI pages for special modes. + +**Bug fixes:** Note stack uninitialized variables, transient generator off-by-one, SPI bulk write bounds check, page group initialization size. + +### Patch Compatibility + +Existing Ambika patches using saw, square, triangle, sine, pad, or noise will work unchanged. Patches using removed waveforms (CZ, wavetable, etc.) will have their oscillator shape reset to `none` on first load. + +--- + +## License + +GPL v3.0, same as the original Ambika firmware. + +Original developer: Emilie Gillet (emilie.o.gillet@gmail.com) +Carcosa firmware: Joseph Martin Giralt From 47d4bcc44db1ec4294dc7e54475e55dbba4bb601 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Sat, 18 Apr 2026 14:19:41 +0200 Subject: [PATCH 14/67] Update README for Carcosa --- README.md | 89 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 441513d..3720bb4 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,80 @@ -# Ambika -## A hybrid MIDI polysynth and voicecard host. +# Carcosa +## Alternative firmware for the Ambika polysynth -Ambika consists of a compact motherboard serving as a "host" for up to 6 sound synthesis voicecard. While this design is primarily intended to be a flexible hybrid polysynth, it could also be used as a drum module/drum machine. +Carcosa is a custom firmware for the [Mutable Instruments Ambika](https://pichenettes.github.io/mutable-instruments-diy-archive/ambika/) that replaces the stock oscillator set with focused synthesis engines while keeping the analog signal path and 6-voice architecture intact. -The motherboard comprises 6 audio outputs, each one connected to a voicecard ; a global mono output ; a pair of MIDI input/output ; a SD card slot ; a 5V/8V/-8V power supply capable of delivering 150mA on the 8V rails and 350mA on the 5V rail ; the master MCU and the user interface elements. The voicecards (a pair of each being attached to the 3 voicecard ports) are SPI slaves, they receive note and modulation data from the motherboard ; and output monophonic audio, ideally 1V pp. +**[Download the latest release](https://github.com/joegiralt/ambika/releases)** -3 designs of voicecards implementing a refined version of the Shruthi-1 engine are provided. Each of those use a different filter (4-Pole with LM13700, 4-Pole with SSM2164, 2-Pole SVF with SSM2164). +**[Read the full manual](CARCOSA.md)** -Original developer: Emilie Gillet (emilie.o.gillet@gmail.com) +### Synthesis Engines -The firmware is released under a GPL3.0 license. It includes a variant of the formant synthesis algorithm used in Peter Knight's Cantarino speech synthesizer. +| Mode | Description | +|------|-------------| +| saw, square, triangle, sine | Classic dual-oscillator subtractive (unchanged) | +| pad | 4-voice detuned supersaw | +| noise | Filtered noise with variable color | +| fm4op | 4-operator FM with 8 TX81Z algorithms and 8 waveforms per operator | +| pluck | Karplus-Strong with 3-read-head ensemble, variable stiffness, 4 excitation types | +| wcoast | Buchla-style wavefolder with variable fold stages, FM, self-sync, sub oscillator | +| wshape | Waveshaping through nonlinear transfer function | -The PCB layouts and schematics, documentation, analyses, simulations and 3D models are released under a Creative Commons cc-by-sa 3.0 license. +### Additional Features -# Build +- Per-envelope curve modes: exponential, linear, looping, looping linear +- Analog slop parameter for vintage poly character +- Smart patch randomizer with per-section depth control +- S1+encoder synthesis mode navigation + +### What was removed + +Step sequencer, CZ synthesis (9 variants), old 2-op FM, 8-bit land, dirty PWM, vowel synthesis, and wavetable oscillators (16 + wavequence). + +--- + +## Install + +Download `AMBIKA.BIN` and `VOICE.BIN` from the [releases page](https://github.com/joegiralt/ambika/releases). + +1. Copy `AMBIKA.BIN` to the root of your SD card +2. Copy `VOICE.BIN` as `VOICE1.BIN` through `VOICE6.BIN` +3. Hold **S8** during power-on to flash the controller +4. Flash each voicecard from the OS info page with **S4** + +To revert to stock Ambika firmware, place the original files on the SD card and reflash. + +## Build from source You'll need: - make - gcc-avr - avr-libc -- avrdude -- python -On Ubuntu: ``` - sudo apt-get install gcc-avr make avr-libc +sudo apt-get install gcc-avr make avr-libc +git submodule update --init +export AVRPATH=$(which avr-gcc) +sed "s|AVRLIB_TOOLS_PATH ?=.*|AVRLIB_TOOLS_PATH ?= $(dirname $AVRPATH)/|" avrlib/makefile.mk > mkfiletmp +mv mkfiletmp avrlib/makefile.mk ``` -Next, you'll need to grab the projects this repo depends on. +Build voicecard: ``` - git submodule update --init +make all && make bin ``` -Once you've got that all settled, you'll need to change the path to `avr-gcc` in avrlib/makefile.mk -to match the path on your system. - +Build controller: ``` - export AVRPATH=`which avr-gcc` - sed "s|AVRLIB_TOOLS_PATH ?=.*|AVRLIB_TOOLS_PATH \?= `dirname $AVRPATH`/|" avrlib/makefile.mk > mkfiletmp - mv mkfiletmp avrlib/makefile.mk +make -f controller/makefile && make -f controller/makefile bin ``` -Then, for voice card `elf` files: - +Flash to SD card: ``` - make all +./flash_sd.sh /mnt/sdcard ``` -And for voice card `bin` files: -``` - make bin -``` +## Credits -For motherboard `elf` files: -``` - make bootstrap_controller -``` +Original Ambika firmware by Emilie Gillet (Mutable Instruments). Released under GPL v3.0. -For motherboard `bin` files: -``` - make -f controller/makefile bin -``` +Carcosa firmware by Joseph Martin Giralt. From c127aa4fd77bc1211f7cd6d693b05c4f4b65043c Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Sat, 18 Apr 2026 14:21:33 +0200 Subject: [PATCH 15/67] Add FAQ to manual --- CARCOSA.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/CARCOSA.md b/CARCOSA.md index b632ff5..3b7ac52 100644 --- a/CARCOSA.md +++ b/CARCOSA.md @@ -268,6 +268,49 @@ Existing Ambika patches using saw, square, triangle, sine, pad, or noise will wo --- +## FAQ + +**Can I go back to the stock Ambika firmware?** +Yes. Put the original AMBIKA.BIN and VOICE*.BIN files on the SD card and hold S8 during power-on. The bootloader is never modified — you can always reflash. + +**Will my existing patches work?** +Patches using saw, square, triangle, sine, pad, or noise will sound the same. Patches using removed waveforms (CZ, wavetables, vowel, 8-bit, etc.) will have their oscillator reset to `none` on first load. Filter settings, envelopes, and mod matrix are unchanged. + +**Do I need to flash all 6 voicecards?** +Yes. The voicecard firmware contains the new synthesis engines. If you only flash the controller, the voicecards won't understand the new waveform types and you'll get silence or wrong sounds. + +**My Ambika shows garbage on the screen after flashing. Is it bricked?** +No. Hold S8 during power-on to force the bootloader to reflash from SD card. This always works regardless of what firmware is running. + +**Does this work with all voicecard types (4P, SVF, SMR)?** +Yes. Carcosa only changes the digital oscillator code. The analog filter on each voicecard is untouched and works exactly as before. + +**Can I use the special modes (FM, pluck, wcoast) with the analog filter?** +Yes. The filter is in the analog signal path after the DAC. Every synthesis mode runs through it. FM through the SSM2164 ladder filter sounds great. + +**Why remove the wavetables? I used those.** +The 16 wavetable banks consumed 10.3 KB of flash on a 32 KB chip. Removing them freed enough space for the FM, Karplus-Strong, and west coast engines combined. The tradeoff is intentional — fewer but deeper sound sources. + +**Why remove the step sequencer?** +The step sequencer consumed nearly 2 KB of controller flash and 384 bytes of RAM. Most Ambika owners sequence externally via MIDI. The arpeggiator is still present. Removing the sequencer gave enough headroom to keep all the new UI pages stable. + +**Can I use FM on one part and subtractive on another?** +Yes. Each part selects its own synthesis mode independently. Part 1 can be FM while part 2 plays saw through the filter. The mode is stored per-patch. + +**How does the slop parameter work?** +Each note-on generates a small random pitch offset and envelope timing variation. The amount is controlled by the slop knob on the part page. In a chord, each voice gets a different random value, simulating the component drift of analog polysynths. + +**How do looping envelopes work?** +Set the envelope mode to `loop` or `lpln`. The envelope cycles attack → decay → attack → decay continuously while the key is held. The sustain knob sets the floor of the cycle. When the key is released, the envelope falls to zero at the release rate. This turns an envelope into a function generator, similar to Make Noise Maths. + +**What are the 8 FM waveforms?** +They match the TX81Z: sine, half-sine, absolute sine, quarter sine, half-absolute, triple-absolute, pulse sine, and saw sine. All are derived from the existing sine table with zero extra flash cost. + +**Is this open source?** +Yes. GPL v3.0, same as the original Ambika firmware. Fork it, modify it, share it. + +--- + ## License GPL v3.0, same as the original Ambika firmware. From 0050da204b313568a4a20ee35c9840b0fa209fe2 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Sat, 18 Apr 2026 14:22:12 +0200 Subject: [PATCH 16/67] Update open source statement --- CARCOSA.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CARCOSA.md b/CARCOSA.md index 3b7ac52..10155d7 100644 --- a/CARCOSA.md +++ b/CARCOSA.md @@ -307,7 +307,7 @@ Set the envelope mode to `loop` or `lpln`. The envelope cycles attack → decay They match the TX81Z: sine, half-sine, absolute sine, quarter sine, half-absolute, triple-absolute, pulse sine, and saw sine. All are derived from the existing sine table with zero extra flash cost. **Is this open source?** -Yes. GPL v3.0, same as the original Ambika firmware. Fork it, modify it, share it. +Yes, and it always will be. Carcosa exists because Emilie Gillet open-sourced the Ambika firmware. That firmware exists because of open-source AVR toolchains, open hardware designs, and a community that shares knowledge freely. We stand on the shoulders of giants. GPL v3.0 — fork it, modify it, improve it, share it. That's the deal. --- From 819793808d9424050c276f55d727f29d66ddf807 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Sat, 18 Apr 2026 14:47:26 +0200 Subject: [PATCH 17/67] v1.5 mode select toggle navigation --- CARCOSA.md | 10 +++++--- controller/ui.cc | 40 +++++++++++++++++++---------- controller/ui.h | 1 + controller/ui_pages/os_info_page.cc | 2 +- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/CARCOSA.md b/CARCOSA.md index 10155d7..93d7e33 100644 --- a/CARCOSA.md +++ b/CARCOSA.md @@ -1,4 +1,4 @@ -# Carcosa v1.4 +# Carcosa v1.5 ## Custom firmware for the Ambika polysynth Carcosa replaces the stock Ambika firmware with a focused set of synthesis engines, expanded envelope capabilities, and a streamlined interface. @@ -158,15 +158,17 @@ Drives a sine wave through a nonlinear waveshaper. Works as a normal oscillator ### Entering special modes -- **Hold S1 + turn encoder**: Cycle through classic -> fm4op -> pluck -> wcoast -- This works from the osc page or any special mode page +1. While on the osc or mixer page, **press S1** — LED 1 starts blinking (mode select active) +2. **Turn encoder** to cycle through: classic, fm4op, pluck, wcoast +3. **Press S1 again** to confirm and exit mode select + +Mode select also works from inside any special mode page. Pressing any other button exits mode select automatically. ### Inside special mode pages - **S1 press**: Toggle between page 1 and page 2 - **Encoder**: Scroll through parameters on the current page, click to edit - **Knobs 1-8**: Direct control of the 8 parameters shown on screen -- **Hold S1 + turn encoder**: Switch to a different synthesis mode ### Other pages diff --git a/controller/ui.cc b/controller/ui.cc index d2067a6..f7c42f2 100755 --- a/controller/ui.cc +++ b/controller/ui.cc @@ -190,6 +190,7 @@ Pots Ui::pots_; UiState Ui::state_; EventQueue<8> Ui::queue_; uint8_t Ui::pot_value_[8]; +uint8_t Ui::s1_held_; UiPageNumber Ui::most_recent_page_in_group_[9]; PageInfo Ui::page_info_; @@ -335,15 +336,8 @@ void Ui::DoEvents() { case CONTROL_ENCODER: if (e.control_id == 0) { - // Hold S1 + encoder = cycle synthesis mode. - // When S1 is held, Poll() multiplies increment by 8. - uint8_t s1_held = (e.value >= 8 || e.value <= -8); - if (s1_held && ( - active_page_ == PAGE_OSCILLATORS || - active_page_ == PAGE_FM4OP || - active_page_ == PAGE_KS_PLUCK || - active_page_ == PAGE_WESTCOAST)) { - // Cycle through modes: classic, fm4op, pluck, wcoast. + if (s1_held_) { + // Mode select active: encoder cycles synthesis modes. static const uint8_t modes[] PROGMEM = { WAVEFORM_SAW, WAVEFORM_FM4OP, WAVEFORM_KS_PLUCK, WAVEFORM_WESTCOAST @@ -355,12 +349,11 @@ void Ui::DoEvents() { uint8_t* patch = multi.mutable_part( state_.active_part)->mutable_raw_patch_data(); uint8_t current = patch[0]; - // Find current mode index. int8_t idx = 0; for (uint8_t m = 0; m < 4; ++m) { if (current == pgm_read_byte(&modes[m])) { idx = m; break; } } - idx += (e.value > 0) ? 1 : -1; + idx += (static_cast(e.value) > 0) ? 1 : -1; if (idx < 0) idx = 3; if (idx > 3) idx = 0; patch[0] = pgm_read_byte(&modes[idx]); @@ -375,14 +368,24 @@ void Ui::DoEvents() { state_.active_part = new_part; } break; - + case CONTROL_SWITCH: + // Double-press S1 on osc/special page toggles mode select. + if (e.control_id == 0 && ( + active_page_ == PAGE_OSCILLATORS || + active_page_ == PAGE_MIXER || + active_page_ == PAGE_FM4OP || + active_page_ == PAGE_KS_PLUCK || + active_page_ == PAGE_WESTCOAST)) { + s1_held_ = !s1_held_; + break; + } + // Any other button exits mode select. + s1_held_ = 0; if (!(*event_handlers_.OnKey)(e.control_id)) { - // Cycle through the next page in the group. if (page_info_.group == e.control_id) { ShowPage(page_info_.next_page); } else { - // Jump to the most recently visited page in the group. ShowPage(most_recent_page_in_group_[e.control_id]); } } @@ -429,6 +432,15 @@ void Ui::DoEvents() { velocity | leds.pixel(LED_PART_1 + led_index)); } (*event_handlers_.UpdateLeds)(); + // Blink LED 1 when S1 held on osc/special page (mode select ready). + if (s1_held_ && ( + active_page_ == PAGE_OSCILLATORS || + active_page_ == PAGE_FM4OP || + active_page_ == PAGE_KS_PLUCK || + active_page_ == PAGE_WESTCOAST)) { + uint8_t blink = (cycle_ & 0x40) ? 0xf0 : 0x0f; + leds.set_pixel(LED_1, blink); + } if (system_settings.data().swap_leds_colors) { for (uint8_t i = 0; i < 15; ++i) { leds.set_pixel(i, U8Swap4(leds.pixel(i))); diff --git a/controller/ui.h b/controller/ui.h index f350e9c..7b34cc2 100755 --- a/controller/ui.h +++ b/controller/ui.h @@ -219,6 +219,7 @@ class Ui { static uint8_t pot_value_[8]; static uint8_t active_group_; + static uint8_t s1_held_; static UiPageNumber most_recent_page_in_group_[9]; static EventHandlers event_handlers_; static PageInfo page_info_; diff --git a/controller/ui_pages/os_info_page.cc b/controller/ui_pages/os_info_page.cc index 85d4e84..8d0dc28 100755 --- a/controller/ui_pages/os_info_page.cc +++ b/controller/ui_pages/os_info_page.cc @@ -133,7 +133,7 @@ void OsInfoPage::UpdateScreen() { display.line_buffer(0)[0] = ' '; char* buffer = display.line_buffer(0) + 1; memcpy_P(&buffer[0], PSTR("carcosa"), 7); - memcpy_P(&buffer[9], PSTR("v1.4"), 4); + memcpy_P(&buffer[9], PSTR("v1.5"), 4); memcpy_P(&buffer[15], PSTR("port 1 device ?"), 15); Word version_number = voicecard_tx.GetVersion(active_control_); From 05fca81cbc6eb885bc7f59264f14f8c13c750fb2 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Sun, 19 Apr 2026 12:00:06 +0200 Subject: [PATCH 18/67] Update install instructions --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3720bb4..c383c8c 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,14 @@ Step sequencer, CZ synthesis (9 variants), old 2-op FM, 8-bit land, dirty PWM, v ## Install -Download `AMBIKA.BIN` and `VOICE.BIN` from the [releases page](https://github.com/joegiralt/ambika/releases). +Download all 7 .BIN files from the [releases page](https://github.com/joegiralt/ambika/releases) and copy them to the root of your SD card. -1. Copy `AMBIKA.BIN` to the root of your SD card -2. Copy `VOICE.BIN` as `VOICE1.BIN` through `VOICE6.BIN` -3. Hold **S8** during power-on to flash the controller -4. Flash each voicecard from the OS info page with **S4** +1. Hold **S8** during power-on to flash the controller (AMBIKA.BIN) +2. Go to the OS info page, select each voicecard port (1-6), press **S4** to flash each + +Flash the controller first, then all 6 voicecards. + +For detailed instructions and recovery, see the [original Ambika firmware update guide](https://pichenettes.github.io/mutable-instruments-diy-archive/ambika/firmware/). To revert to stock Ambika firmware, place the original files on the SD card and reflash. From 773160f6c1401f11f6290b3ba8a70b7223e21999 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Sun, 19 Apr 2026 12:10:35 +0200 Subject: [PATCH 19/67] v1.6 MIDI CC mappings for all synthesis modes --- CARCOSA.md | 95 ++++++++++++++++++++++++++++- controller/parameter.cc | 9 ++- controller/parameter.h | 2 +- controller/ui_pages/os_info_page.cc | 2 +- 4 files changed, 104 insertions(+), 4 deletions(-) diff --git a/CARCOSA.md b/CARCOSA.md index 93d7e33..1624005 100644 --- a/CARCOSA.md +++ b/CARCOSA.md @@ -1,4 +1,4 @@ -# Carcosa v1.5 +# Carcosa v1.6 ## Custom firmware for the Ambika polysynth Carcosa replaces the stock Ambika firmware with a focused set of synthesis engines, expanded envelope capabilities, and a streamlined interface. @@ -270,6 +270,99 @@ Existing Ambika patches using saw, square, triangle, sine, pad, or noise will wo --- +## MIDI CC Reference + +All synthesis modes respond to MIDI CCs. The special modes reuse existing patch byte offsets, so the same CC numbers control different parameters depending on the active mode. + +### Global (all modes) + +| CC | Parameter | +|----|-----------| +| 108 | FM feedback (fm4op mode) | +| 109 | Slop | +| 110 | Envelope curve (per active envelope) | + +### Classic Mode + +| CC | Parameter | +|----|-----------| +| 16 | Osc 1 waveform | +| 17 | Osc 1 parameter | +| 14 | Osc 1 range | +| 15 | Osc 1 detune | +| 18 | Osc 2 waveform | +| 19 | Osc 2 parameter | +| 20 | Osc 2 range | +| 21 | Osc 2 detune | +| 22 | Mix balance | +| 23 | Mix operator | +| 24 | Mix parameter | +| 25 | Sub osc shape | +| 26 | Sub osc level | +| 27 | Noise level | +| 12 | Fuzz | +| 13 | Crush | +| 3 | Filter cutoff | +| 9 | Filter resonance | + +### 4-Op FM Mode + +| CC | FM parameter | +|----|-------------| +| 17 | Algorithm | +| 108 | Feedback | +| 18 | Op 1/2 waveform (packed) | +| 19 | Op 3/4 waveform (packed) | +| 14 | Op 1 ratio | +| 15 | Op 1 fine | +| 20 | Op 2 ratio | +| 21 | Op 2 fine | +| 22 | Op 3 ratio | +| 23 | Op 3 fine | +| 24 | Op 4 ratio | +| 25 | Op 4 fine | +| 26 | Op 1 level | +| 27 | Op 2 level | +| 12 | Op 3 level | +| 13 | Op 4 level | + +### Karplus-Strong Mode + +| CC | KS parameter | +|----|-------------| +| 17 | Damping | +| 19 | Excitation color | +| 20 | Decay rate | +| 21 | Pluck position | +| 18 | Excitation type | +| 22 | Body resonance | +| 23 | Ensemble rate | +| 24 | Ensemble depth | +| 25 | Ensemble spread | +| 26 | Ensemble mix | +| 27 | Stiffness | +| 12 | Extra feedback | + +### West Coast Mode + +| CC | WC parameter | +|----|-------------| +| 18 | Base waveform | +| 17 | Fold depth | +| 19 | Symmetry | +| 22 | Bias | +| 20 | FM depth | +| 21 | FM ratio | +| 24 | Color | +| 23 | Drive | +| 25 | Fold stages | +| 26 | Input gain | +| 27 | Env to fold | +| 12 | Sub level | +| 13 | Sync | + +--- + ## FAQ **Can I go back to the stock Ambika firmware?** diff --git a/controller/parameter.cc b/controller/parameter.cc index 11e3023..41f56b1 100755 --- a/controller/parameter.cc +++ b/controller/parameter.cc @@ -337,7 +337,7 @@ static const prog_char midi_cc_map[128] PROGMEM = { 28, 25, 255, 26, 255, 255, 44, 45, // 96-111 255, 255, 255, 255, 255, 255, 49, 50, - 51, 52, 53, 57, 255, 255, 255, 255, + 51, 52, 53, 57, 75, 74, 73, 255, // 112-127 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 @@ -923,6 +923,13 @@ static const prog_Parameter parameters[kNumParameters] PROGMEM = { UNIT_RAW_UINT8, 0, 127, 1, 0, 0xff, 0xff, STR_RES_NOISE, STR_RES_NOISE, STR_RES_MIXER }, + + // 75 - FM feedback (padding[0], MIDI-only, not on any UI page) + { PARAMETER_LEVEL_PATCH, + 104, + UNIT_RAW_UINT8, 0, 127, + 1, 0, 0xff, 0xff, + STR_RES_AMOUNT, STR_RES_AMOUNT, STR_RES_MIXER }, }; /* static */ diff --git a/controller/parameter.h b/controller/parameter.h index 00b1890..ccc5d96 100755 --- a/controller/parameter.h +++ b/controller/parameter.h @@ -118,7 +118,7 @@ struct Parameter { static void PrintNote(uint8_t note, char* buffer); }; -const uint8_t kNumParameters = 75; +const uint8_t kNumParameters = 76; typedef Parameter PROGMEM prog_Parameter; // The parameter manager is the class who knows how to apply a parameter change diff --git a/controller/ui_pages/os_info_page.cc b/controller/ui_pages/os_info_page.cc index 8d0dc28..ee639c7 100755 --- a/controller/ui_pages/os_info_page.cc +++ b/controller/ui_pages/os_info_page.cc @@ -133,7 +133,7 @@ void OsInfoPage::UpdateScreen() { display.line_buffer(0)[0] = ' '; char* buffer = display.line_buffer(0) + 1; memcpy_P(&buffer[0], PSTR("carcosa"), 7); - memcpy_P(&buffer[9], PSTR("v1.5"), 4); + memcpy_P(&buffer[9], PSTR("v1.6"), 4); memcpy_P(&buffer[15], PSTR("port 1 device ?"), 15); Word version_number = voicecard_tx.GetVersion(active_control_); From bc8b258fb3e0d482270fb1ebdc450b25d2d94d8c Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 11:43:19 +0200 Subject: [PATCH 20/67] Expand patch to 128 bytes, add env4-7, decouple LFO count - Add ExtraEnvelopeSettings struct (4 bytes: ADSR) and extra_env[4] array at offsets 112-127, expanding Patch from 112 to 128 bytes - Add MOD_SRC_ENV_4 through MOD_SRC_ENV_7 to ModulationSource enum - Remove MOD_SRC_SEQ_1 and MOD_SRC_SEQ_2 (sequencer sources) - Rename kNumEnvelopes to kNumEnvLfoSlots (value stays 3) - Add kNumTotalEnvelopes=7, kNumExtraEnvelopes=4, kNumLfos=3 - Add PRM_PATCH_EXTRA_ENV_* parameter enum entries Co-Authored-By: Claude Opus 4.6 (1M context) --- common/patch.h | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/common/patch.h b/common/patch.h index 4713fb5..af1f0ce 100755 --- a/common/patch.h +++ b/common/patch.h @@ -56,6 +56,13 @@ struct EnvelopeLfoSettings { uint8_t retrigger_mode; }; +struct ExtraEnvelopeSettings { + uint8_t attack; + uint8_t decay; + uint8_t sustain; + uint8_t release; +}; + struct Modulation { uint8_t source; uint8_t destination; @@ -158,19 +165,21 @@ enum ModulationSource { MOD_SRC_ENV_1, MOD_SRC_ENV_2, MOD_SRC_ENV_3, - + MOD_SRC_ENV_4, + MOD_SRC_ENV_5, + MOD_SRC_ENV_6, + MOD_SRC_ENV_7, + MOD_SRC_LFO_1, MOD_SRC_LFO_2, MOD_SRC_LFO_3, MOD_SRC_LFO_4, - + MOD_SRC_OP_1, MOD_SRC_OP_2, MOD_SRC_OP_3, MOD_SRC_OP_4, - - MOD_SRC_SEQ_1, - MOD_SRC_SEQ_2, + MOD_SRC_ARP_STEP, MOD_SRC_VELOCITY, @@ -230,8 +239,10 @@ enum FilterMode { }; static const uint8_t kNumSyncedLfoRates = 15; -static const uint8_t kNumEnvelopes = 3; -static const uint8_t kNumLfos = kNumEnvelopes; +static const uint8_t kNumEnvLfoSlots = 3; +static const uint8_t kNumLfos = kNumEnvLfoSlots; +static const uint8_t kNumTotalEnvelopes = 7; +static const uint8_t kNumExtraEnvelopes = 4; static const uint8_t kNumModulations = 14; static const uint8_t kNumModifiers = 4; static const uint8_t kNumOscillators = 2; @@ -258,7 +269,7 @@ struct Patch { int8_t filter_lfo; // Offset: 24-48 - EnvelopeLfoSettings env_lfo[kNumEnvelopes]; + EnvelopeLfoSettings env_lfo[kNumEnvLfoSlots]; // Offset: 48-50 uint8_t voice_lfo_shape; @@ -272,6 +283,9 @@ struct Patch { // Offset: 104-112 uint8_t padding[8]; + + // Offset: 112-128 + ExtraEnvelopeSettings extra_env[kNumExtraEnvelopes]; }; typedef Patch PROGMEM prog_Patch; @@ -324,7 +338,12 @@ enum PatchParameter { PRM_PATCH_MOD_OPERAND2, PRM_PATCH_MOD_OPERATOR, - PRM_PATCH_SLOP = 105 + PRM_PATCH_SLOP = 105, + + PRM_PATCH_EXTRA_ENV_ATTACK = 112, + PRM_PATCH_EXTRA_ENV_DECAY, + PRM_PATCH_EXTRA_ENV_SUSTAIN, + PRM_PATCH_EXTRA_ENV_RELEASE, }; } // namespace ambika From 0a6461a801325e9466e637838db7f55cf620d37f Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 11:46:44 +0200 Subject: [PATCH 21/67] Render 7 envelopes on voicecard Expand envelope array from 3 to kNumTotalEnvelopes (7), add rendering loop for all 7 mod sources (ENV_1-ENV_7), update ADSR parameters from both env_lfo[] (slots 0-2) and extra_env[] (slots 3-6), extend COMMAND_RETRIGGER_ENVELOPE cases, and append default ADSR values for the 4 extra envelopes in init_patch. Co-Authored-By: Claude Opus 4.6 (1M context) --- voicecard/voice.cc | 31 ++++++++++++++++++++++--------- voicecard/voice.h | 2 +- voicecard/voicecard_rx.h | 4 ++++ 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/voicecard/voice.cc b/voicecard/voice.cc index db5c9e3..09d0d1a 100755 --- a/voicecard/voice.cc +++ b/voicecard/voice.cc @@ -45,7 +45,7 @@ Part Voice::part_; uint8_t *Voice::part_data_ = reinterpret_cast (&Voice::part_); Lfo Voice::voice_lfo_; -Envelope Voice::envelope_[kNumEnvelopes]; +Envelope Voice::envelope_[kNumTotalEnvelopes]; uint8_t Voice::gate_; int16_t Voice::pitch_increment_; int16_t Voice::pitch_target_; @@ -90,8 +90,8 @@ static const prog_Patch init_patch PROGMEM = { MOD_SRC_LFO_1, MOD_DST_PARAMETER_2, 0, MOD_SRC_LFO_2, MOD_DST_MIX_BALANCE, 0, MOD_SRC_LFO_4, MOD_DST_PARAMETER_1, 63, - MOD_SRC_SEQ_1, MOD_DST_PARAMETER_1, 0, - MOD_SRC_SEQ_2, MOD_DST_PARAMETER_2, 0, + MOD_SRC_ENV_4, MOD_DST_PARAMETER_1, 0, + MOD_SRC_ENV_5, MOD_DST_PARAMETER_2, 0, MOD_SRC_ENV_2, MOD_DST_VCA, 32, MOD_SRC_VELOCITY, MOD_DST_VCA, 0, MOD_SRC_PITCH_BEND, MOD_DST_OSC_1_2_COARSE, 0, @@ -105,12 +105,18 @@ static const prog_Patch init_patch PROGMEM = { // Padding 0, 0, 0, 0, 0, 0, 0, 0, + + // Extra envelopes (env4-7 ADSR) + 0, 40, 20, 60, + 0, 40, 20, 60, + 0, 40, 20, 60, + 0, 40, 20, 60, }; /* static */ void Voice::Init() { pitch_value_ = 0; - for (uint8_t i = 0; i < kNumEnvelopes; ++i) { + for (uint8_t i = 0; i < kNumTotalEnvelopes; ++i) { envelope_[i].Init(); } memset(no_sync_, 0, kAudioBlockSize); @@ -140,7 +146,7 @@ void Voice::ResetAllControllers() { /* static */ void Voice::TriggerEnvelope(uint8_t stage) { - for (uint8_t i = 0; i < kNumEnvelopes; ++i) { + for (uint8_t i = 0; i < kNumTotalEnvelopes; ++i) { TriggerEnvelope(i, stage); } } @@ -207,9 +213,9 @@ inline void Voice::LoadSources() { // Rescale the value of each modulation sources. Envelopes are in the // 0-16383 range ; just like pitch. All are scaled to 0-255. modulation_sources_[MOD_SRC_NOISE] = Random::GetByte(); - modulation_sources_[MOD_SRC_ENV_1] = envelope_[0].Render(); - modulation_sources_[MOD_SRC_ENV_2] = envelope_[1].Render(); - modulation_sources_[MOD_SRC_ENV_3] = envelope_[2].Render(); + for (uint8_t i = 0; i < kNumTotalEnvelopes; ++i) { + modulation_sources_[MOD_SRC_ENV_1 + i] = envelope_[i].Render(); + } modulation_sources_[MOD_SRC_NOTE] = U14ShiftRight6(pitch_value_); modulation_sources_[MOD_SRC_GATE] = gate_; modulation_sources_[MOD_SRC_LFO_4] = voice_lfo_.Render( @@ -358,7 +364,7 @@ inline void Voice::UpdateDestinations() { slop) >> 8; } - for (int i = 0; i < kNumEnvelopes; ++i) { + for (int i = 0; i < kNumEnvLfoSlots; ++i) { int16_t new_attack = patch_.env_lfo[i].attack; new_attack = Clip(new_attack + attack_mod + env_slop, 0, 127); int16_t new_decay = patch_.env_lfo[i].decay; @@ -372,6 +378,13 @@ inline void Voice::UpdateDestinations() { new_release, patch_.env_lfo[i].envelope_curve); } + for (int i = 0; i < kNumExtraEnvelopes; ++i) { + envelope_[kNumEnvLfoSlots + i].Update( + patch_.extra_env[i].attack, + patch_.extra_env[i].decay, + patch_.extra_env[i].sustain, + patch_.extra_env[i].release); + } voice_lfo_.set_phase_increment( ResourcesManager::Lookup( diff --git a/voicecard/voice.h b/voicecard/voice.h index b8b3350..5dba511 100755 --- a/voicecard/voice.h +++ b/voicecard/voice.h @@ -130,7 +130,7 @@ class Voice { static Part part_; // Envelope generators. - static Envelope envelope_[kNumEnvelopes]; + static Envelope envelope_[kNumTotalEnvelopes]; static uint8_t gate_; static Lfo voice_lfo_; static uint8_t modulation_sources_[kNumModulationSources]; diff --git a/voicecard/voicecard_rx.h b/voicecard/voicecard_rx.h index 332023d..0389f19 100755 --- a/voicecard/voicecard_rx.h +++ b/voicecard/voicecard_rx.h @@ -117,6 +117,10 @@ class VoicecardProtocolRx { case COMMAND_RETRIGGER_ENVELOPE: case COMMAND_RETRIGGER_ENVELOPE + 1: case COMMAND_RETRIGGER_ENVELOPE + 2: + case COMMAND_RETRIGGER_ENVELOPE + 3: + case COMMAND_RETRIGGER_ENVELOPE + 4: + case COMMAND_RETRIGGER_ENVELOPE + 5: + case COMMAND_RETRIGGER_ENVELOPE + 6: voice.TriggerEnvelope(command_ & 0x0f, ATTACK); break; case COMMAND_RESET_ALL_CONTROLLERS: From c2fc1b5dc6adf9f062e9e84451f6b69338854493 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 11:48:57 +0200 Subject: [PATCH 22/67] Fix controller init_patch for 128-byte patch format Replace removed MOD_SRC_SEQ_1/SEQ_2 enum values with MOD_SRC_ENV_4/ENV_5 in init_patch PROGMEM data, and add 16 bytes of extra_env[4] ADSR defaults to match the expanded 128-byte Patch struct. Co-Authored-By: Claude Opus 4.6 (1M context) --- controller/part.cc | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/controller/part.cc b/controller/part.cc index b1f273a..3b0288f 100755 --- a/controller/part.cc +++ b/controller/part.cc @@ -62,8 +62,8 @@ static const prog_Patch init_patch PROGMEM = { MOD_SRC_LFO_2, MOD_DST_PARAMETER_2, 0, MOD_SRC_LFO_3, MOD_DST_MIX_BALANCE, 0, MOD_SRC_LFO_4, MOD_DST_FILTER_CUTOFF, 0, - MOD_SRC_SEQ_1, MOD_DST_FILTER_CUTOFF, 0, - MOD_SRC_SEQ_2, MOD_DST_MIX_BALANCE, 0, + MOD_SRC_ENV_4, MOD_DST_FILTER_CUTOFF, 0, + MOD_SRC_ENV_5, MOD_DST_MIX_BALANCE, 0, MOD_SRC_ENV_3, MOD_DST_VCA, 63, MOD_SRC_VELOCITY, MOD_DST_VCA, 16, @@ -73,11 +73,17 @@ static const prog_Patch init_patch PROGMEM = { // Modifiers MOD_SRC_LFO_1, MOD_SRC_LFO_2, 0, MOD_SRC_LFO_2, MOD_SRC_LFO_3, 0, - MOD_SRC_LFO_3, MOD_SRC_SEQ_1, 0, - MOD_SRC_SEQ_1, MOD_SRC_SEQ_2, 0, + MOD_SRC_LFO_3, MOD_SRC_ENV_4, 0, + MOD_SRC_ENV_4, MOD_SRC_ENV_5, 0, // Padding 0, 0, 0, 0, 0, 0, 0, 0, + + // Extra envelopes (env4-7 ADSR: attack, decay, sustain, release) + 0, 40, 20, 60, + 0, 40, 20, 60, + 0, 40, 20, 60, + 0, 40, 20, 60, }; static const prog_PartData init_part PROGMEM = { From 15e30333960dc5759a10b3b99b24b7aed3d34d78 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 11:51:31 +0200 Subject: [PATCH 23/67] Replace mod source string table with PROGMEM arrays for 7 envelopes --- controller/parameter.cc | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/controller/parameter.cc b/controller/parameter.cc index 41f56b1..7bcfa5b 100755 --- a/controller/parameter.cc +++ b/controller/parameter.cc @@ -189,12 +189,6 @@ void Parameter::PrintValue(uint8_t value, char* buffer, uint8_t width) const { } break; - case UNIT_MODULATION_SOURCE: - if (width > 5) { - text = STR_RES_ENV_1; - } - break; - case UNIT_MODULATION_DESTINATION: if (width > 5) { text = STR_RES_PARAM_1; @@ -255,6 +249,37 @@ void Parameter::PrintValue(uint8_t value, char* buffer, uint8_t width) const { return; } + // Mod source names — packed PROGMEM tables, 5 chars each (short) / 7 chars each (long). + // Order matches ModulationSource enum. + if (unit == UNIT_MODULATION_SOURCE) { + static const prog_char mod_src_short[] PROGMEM = + "env1\0" "env2\0" "env3\0" "env4\0" "env5\0" "env6\0" "env7\0" + "lfo1\0" "lfo2\0" "lfo3\0" "lfo4\0" + "mod1\0" "mod2\0" "mod3\0" "mod4\0" + "arp \0" "velo\0" "aftr\0" "bend\0" "mwhl\0" "whl2\0" "pdal\0" + "note\0" "gate\0" "nois\0" "rand\0" + "=256\0" "=128\0" "=64 \0" "=32 \0" "=16 \0" "=8 \0" "=4 \0"; + static const prog_char mod_src_long[] PROGMEM = + "env 1\0\0" "env 2\0\0" "env 3\0\0" "env 4\0\0" + "env 5\0\0" "env 6\0\0" "env 7\0\0" + "lfo 1\0\0" "lfo 2\0\0" "lfo 3\0\0" "lfo 4\0\0" + "mod. 1\0" "mod. 2\0" "mod. 3\0" "mod. 4\0" + "arp \0" "velo \0" "afttch\0" "bender\0" + "mwheel\0" "wheel2\0" "pedal \0" + "note \0" "gate \0" "noise \0" "random\0" + "= 256 \0" "= 128 \0" "= 64 \0" "= 32 \0" + "= 16 \0" "= 8 \0" "= 4 \0"; + if (value < MOD_SRC_LAST) { + if (width > 5) { + memcpy_P(buffer, mod_src_long + value * 7, width < 7 ? width : 7); + } else { + memcpy_P(buffer, mod_src_short + value * 5, width < 5 ? width : 5); + } + } + AlignRight(buffer, width); + return; + } + if (text) { ResourcesManager::LoadStringResource(text + value, buffer, width); } else if (unit != UNIT_NOTE) { From cdf85bf7e16e11bdd57a2a6beae0189015f56f85 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 11:55:07 +0200 Subject: [PATCH 24/67] Add extra envelope parameters and MIDI CC mappings Co-Authored-By: Claude Opus 4.6 (1M context) --- controller/parameter.cc | 32 ++++++++++++++++++++++++++++++-- controller/parameter.h | 2 +- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/controller/parameter.cc b/controller/parameter.cc index 7bcfa5b..7755912 100755 --- a/controller/parameter.cc +++ b/controller/parameter.cc @@ -362,9 +362,9 @@ static const prog_char midi_cc_map[128] PROGMEM = { 28, 25, 255, 26, 255, 255, 44, 45, // 96-111 255, 255, 255, 255, 255, 255, 49, 50, - 51, 52, 53, 57, 75, 74, 73, 255, + 51, 52, 53, 57, 75, 74, 73, 76, // 112-127 - 255, 255, 255, 255, 255, 255, 255, 255, + 77, 78, 79, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }; @@ -955,6 +955,34 @@ static const prog_Parameter parameters[kNumParameters] PROGMEM = { UNIT_RAW_UINT8, 0, 127, 1, 0, 0xff, 0xff, STR_RES_AMOUNT, STR_RES_AMOUNT, STR_RES_MIXER }, + + // 76 - extra envelope 4 attack + { PARAMETER_LEVEL_PATCH, + PRM_PATCH_EXTRA_ENV_ATTACK, + UNIT_RAW_UINT8, 0, 127, + 4, 4, 0xff, 0xff, + STR_RES_ATTK, STR_RES_ATTACK, STR_RES_ENVELOPE }, + + // 77 - extra envelope 4 decay + { PARAMETER_LEVEL_PATCH, + PRM_PATCH_EXTRA_ENV_DECAY, + UNIT_RAW_UINT8, 0, 127, + 4, 4, 0xff, 0xff, + STR_RES_DECAY, STR_RES_DECAY, STR_RES_ENVELOPE }, + + // 78 - extra envelope 4 sustain + { PARAMETER_LEVEL_PATCH, + PRM_PATCH_EXTRA_ENV_SUSTAIN, + UNIT_RAW_UINT8, 0, 127, + 4, 4, 0xff, 0xff, + STR_RES_SUSTAIN, STR_RES_SUSTAIN, STR_RES_ENVELOPE }, + + // 79 - extra envelope 4 release + { PARAMETER_LEVEL_PATCH, + PRM_PATCH_EXTRA_ENV_RELEASE, + UNIT_RAW_UINT8, 0, 127, + 4, 4, 0xff, 0xff, + STR_RES_RELEASE, STR_RES_RELEASE, STR_RES_ENVELOPE }, }; /* static */ diff --git a/controller/parameter.h b/controller/parameter.h index ccc5d96..e052807 100755 --- a/controller/parameter.h +++ b/controller/parameter.h @@ -118,7 +118,7 @@ struct Parameter { static void PrintNote(uint8_t note, char* buffer); }; -const uint8_t kNumParameters = 76; +const uint8_t kNumParameters = 80; typedef Parameter PROGMEM prog_Parameter; // The parameter manager is the class who knows how to apply a parameter change From 71a59233cfa658a0eb1670b6e2810f3ed7084046 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 11:59:58 +0200 Subject: [PATCH 25/67] Custom envelope page for 7 envelopes Replace the ParameterEditor-based ENV_LFO page with a custom EnvPage that supports selecting all 7 envelopes. Envelopes 1-3 show LFO controls (rate, shape, curve) plus ADSR. Envelopes 4-7 show ADSR only with blank top-row cells where LFO controls would be. Co-Authored-By: Claude Opus 4.6 (1M context) --- controller/ui.cc | 5 +- controller/ui_pages/env_page.cc | 319 ++++++++++++++++++++++++++++++++ controller/ui_pages/env_page.h | 51 +++++ 3 files changed, 373 insertions(+), 2 deletions(-) create mode 100644 controller/ui_pages/env_page.cc create mode 100644 controller/ui_pages/env_page.h diff --git a/controller/ui.cc b/controller/ui.cc index f7c42f2..d9946f4 100755 --- a/controller/ui.cc +++ b/controller/ui.cc @@ -33,6 +33,7 @@ #include "controller/ui_pages/library.h" #include "controller/ui_pages/os_info_page.h" #include "controller/ui_pages/parameter_editor.h" +#include "controller/ui_pages/env_page.h" #include "controller/ui_pages/fm_page.h" #include "controller/ui_pages/ks_page.h" #include "controller/ui_pages/wc_page.h" @@ -64,8 +65,8 @@ const prog_PageInfo page_registry[] PROGMEM = { }, { PAGE_ENV_LFO, - &ParameterEditor::event_handlers_, - { 24, 30, 31, 73, 25, 26, 27, 28 }, + &EnvPage::event_handlers_, + { 0, 0, 0, 0, 0, 0, 0, 0 }, PAGE_VOICE_LFO, 2, 0xf0, }, diff --git a/controller/ui_pages/env_page.cc b/controller/ui_pages/env_page.cc new file mode 100644 index 0000000..b7cd850 --- /dev/null +++ b/controller/ui_pages/env_page.cc @@ -0,0 +1,319 @@ +// Copyright 2026 Ambika contributors. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// 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. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ----------------------------------------------------------------------------- +// +// Custom UI page for 7-envelope selector (env1-3 with LFO, env4-7 ADSR only). + +#include "controller/ui_pages/env_page.h" + +#include +#include "avrlib/string.h" + +#include "controller/display.h" +#include "controller/leds.h" +#include "controller/multi.h" +#include "controller/voicecard_tx.h" +#include "controller/ui.h" +#include "common/patch.h" + +namespace ambika { + +using namespace avrlib; + +/* static */ +uint8_t EnvPage::selector_; + +// LFO shape short names (4 chars each). Only first 4 basic shapes shown by +// name; wavetable shapes displayed as number. +static const prog_char lfo_shape_names[] PROGMEM = + "tri " "sqr " "s&h " "ramp"; + +// Envelope curve short names (4 chars each). +static const prog_char env_curve_names[] PROGMEM = + "exp " "lin " "loop" "lpln"; + +// Top-row labels for selector 0-2 (4 chars each, knobs 0-3). +static const prog_char env_top_labels_lfo[] PROGMEM = + "lfeg" "rate" "shap" "curv"; + +// Bottom-row labels (knobs 4-7), same for all selectors. +static const prog_char env_bot_labels[] PROGMEM = + "attk" "dec " "sus " "rel "; + +// Top-row label for selector 3-6 knob 0 only. +static const prog_char env_top_label_sel[] PROGMEM = "lfeg"; + +/* static */ +const prog_EventHandlers EnvPage::event_handlers_ PROGMEM = { + OnInit, + NULL, + OnIncrement, + OnClick, + OnPot, + OnKey, + NULL, + OnIdle, + UpdateScreen, + UpdateLeds, + NULL, +}; + +static inline uint8_t* GetPatchData() { + return multi.mutable_part(ui.state().active_part)->mutable_raw_patch_data(); +} + +/* static */ +uint8_t EnvPage::AdsrOffset(uint8_t knob) { + // knob: 0=attack, 1=decay, 2=sustain, 3=release + if (selector_ < 3) { + return 24 + selector_ * 8 + knob; + } else { + return 112 + (selector_ - 3) * 4 + knob; + } +} + +/* static */ +void EnvPage::OnInit(PageInfo* info) { + UiPage::OnInit(info); + active_control_ = 0; + selector_ = 0; +} + +/* static */ +uint8_t EnvPage::OnIncrement(int8_t increment) { + if (edit_mode_ != EDIT_IDLE) { + uint8_t* patch = GetPatchData(); + uint8_t ctrl = active_control_; + + if (ctrl == 0) { + // Selector: 0-6. + int16_t val = selector_ + increment; + if (val < 0) val = 0; + if (val > 6) val = 6; + selector_ = val; + } else if (ctrl >= 4) { + // ADSR knob (ctrl 4-7 -> knob 0-3). + uint8_t offset = AdsrOffset(ctrl - 4); + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val > 127) val = 127; + patch[offset] = val; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + } else if (selector_ < 3) { + // LFO controls (ctrl 1-3) only for env 1-3. + uint8_t base = 24 + selector_ * 8; + if (ctrl == 1) { + // Rate (offset +5), 0-127. + uint8_t offset = base + 5; + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val > 127) val = 127; + patch[offset] = val; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + } else if (ctrl == 2) { + // Shape (offset +4), 0 to LFO_WAVEFORM_LAST-1. + uint8_t offset = base + 4; + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val >= LFO_WAVEFORM_LAST) val = LFO_WAVEFORM_LAST - 1; + patch[offset] = val; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + } else { + // Curve (offset +6), 0 to ENVELOPE_CURVE_LAST-1. + uint8_t offset = base + 6; + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val >= ENVELOPE_CURVE_LAST) val = ENVELOPE_CURVE_LAST - 1; + patch[offset] = val; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + } + } + // else: ctrl 1-3 and selector >= 3 — ignore (no LFO for extra envs). + edit_mode_ = EDIT_STARTED_BY_ENCODER; + } else { + // Navigate controls. + int8_t new_control = active_control_ + increment; + if (new_control >= 0 && new_control < 8) { + // Skip LFO knobs (1-3) when selector >= 3. + if (selector_ >= 3 && new_control >= 1 && new_control <= 3) { + new_control = (increment > 0) ? 4 : 0; + } + if (new_control >= 0 && new_control < 8) { + active_control_ = new_control; + } + } + } + return 1; +} + +/* static */ +uint8_t EnvPage::OnClick() { + if (edit_mode_ == EDIT_IDLE) { + edit_mode_ = EDIT_STARTED_BY_ENCODER; + } else { + edit_mode_ = EDIT_IDLE; + } + return 1; +} + +/* static */ +uint8_t EnvPage::OnPot(uint8_t index, uint8_t value) { + uint8_t* patch = GetPatchData(); + + if (index == 0) { + // Selector pot: scale 0-127 to 0-6. + uint8_t val = (static_cast(value) * 7) >> 7; + if (val > 6) val = 6; + selector_ = val; + active_control_ = 0; + edit_mode_ = EDIT_STARTED_BY_POT; + return 1; + } + + if (index >= 4) { + // ADSR knob. + uint8_t offset = AdsrOffset(index - 4); + patch[offset] = value; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + active_control_ = index; + edit_mode_ = EDIT_STARTED_BY_POT; + return 1; + } + + // Knobs 1-3: LFO rate, shape, curve — only for selector 0-2. + if (selector_ >= 3) return 1; + + uint8_t base = 24 + selector_ * 8; + if (index == 1) { + // Rate. + uint8_t offset = base + 5; + patch[offset] = value; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + } else if (index == 2) { + // Shape. + uint8_t offset = base + 4; + uint8_t val = (static_cast(value) * LFO_WAVEFORM_LAST) >> 7; + if (val >= LFO_WAVEFORM_LAST) val = LFO_WAVEFORM_LAST - 1; + patch[offset] = val; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + } else { + // Curve. + uint8_t offset = base + 6; + uint8_t val = (static_cast(value) * ENVELOPE_CURVE_LAST) >> 7; + if (val >= ENVELOPE_CURVE_LAST) val = ENVELOPE_CURVE_LAST - 1; + patch[offset] = val; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + } + + active_control_ = index; + edit_mode_ = EDIT_STARTED_BY_POT; + return 1; +} + +/* static */ +uint8_t EnvPage::OnKey(uint8_t key) { + // S1 toggles to the VOICE_LFO page (same as original behavior). + return 0; +} + +/* static */ +void EnvPage::UpdateScreen() { + uint8_t* patch = GetPatchData(); + + for (uint8_t i = 0; i < 8; ++i) { + uint8_t line = i < 4 ? 0 : 1; + uint8_t row = (i & 3) * 10; + char* buffer = display.line_buffer(line) + row; + + if (row != 0) buffer[0] = kDelimiter; + if ((row + 10) != kLcdWidth) buffer[10] = kDelimiter; + + if (i == 0) { + // Selector cell: "lfeg N" + memcpy_P(&buffer[1], env_top_label_sel, 4); + if (i == active_control_ && buffer[1] >= 'a' && buffer[1] <= 'z') { + buffer[1] -= 0x20; + } + buffer[5] = ' '; + buffer[6] = ' '; + buffer[7] = ' '; + buffer[8] = ' '; + buffer[9] = '1' + selector_; + } else if (i >= 4) { + // ADSR cell. + memcpy_P(&buffer[1], env_bot_labels + (i - 4) * 4, 4); + if (i == active_control_ && buffer[1] >= 'a' && buffer[1] <= 'z') { + buffer[1] -= 0x20; + } + buffer[5] = ' '; + uint8_t offset = AdsrOffset(i - 4); + UnsafeItoa(patch[offset], 4, &buffer[6]); + AlignRight(&buffer[6], 4); + } else if (selector_ < 3) { + // LFO controls (knobs 1-3) for env 1-3. + memcpy_P(&buffer[1], env_top_labels_lfo + i * 4, 4); + if (i == active_control_ && buffer[1] >= 'a' && buffer[1] <= 'z') { + buffer[1] -= 0x20; + } + buffer[5] = ' '; + + uint8_t base = 24 + selector_ * 8; + if (i == 1) { + // Rate: numeric. + UnsafeItoa(patch[base + 5], 4, &buffer[6]); + AlignRight(&buffer[6], 4); + } else if (i == 2) { + // Shape: name or number. + uint8_t shape = patch[base + 4]; + if (shape < 4) { + memcpy_P(&buffer[6], lfo_shape_names + shape * 4, 4); + } else { + // Wavetable shape: show as number. + UnsafeItoa(shape, 4, &buffer[6]); + AlignRight(&buffer[6], 4); + } + } else { + // Curve: name. + uint8_t curve = patch[base + 6]; + if (curve < ENVELOPE_CURVE_LAST) { + memcpy_P(&buffer[6], env_curve_names + curve * 4, 4); + } else { + buffer[6] = ' '; + buffer[7] = ' '; + buffer[8] = ' '; + buffer[9] = '?'; + } + } + } else { + // Blank cell for knobs 1-3 when selector >= 3. + for (uint8_t j = 1; j < 10; ++j) buffer[j] = ' '; + } + } +} + +/* static */ +void EnvPage::UpdateLeds() { + leds.set_pixel(LED_1, 0xf0); + leds.set_pixel(LED_2, 0x0f); +} + +} // namespace ambika diff --git a/controller/ui_pages/env_page.h b/controller/ui_pages/env_page.h new file mode 100644 index 0000000..3cff515 --- /dev/null +++ b/controller/ui_pages/env_page.h @@ -0,0 +1,51 @@ +// Copyright 2026 Ambika contributors. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// 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. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ----------------------------------------------------------------------------- +// +// Custom UI page for 7-envelope selector (env1-3 with LFO, env4-7 ADSR only). + +#ifndef CONTROLLER_UI_PAGES_ENV_PAGE_H_ +#define CONTROLLER_UI_PAGES_ENV_PAGE_H_ + +#include "controller/ui_pages/ui_page.h" + +namespace ambika { + +class EnvPage : public UiPage { + public: + EnvPage() { } + + static void OnInit(PageInfo* info); + static uint8_t OnIncrement(int8_t increment); + static uint8_t OnClick(); + static uint8_t OnPot(uint8_t index, uint8_t value); + static uint8_t OnKey(uint8_t key); + + static void UpdateScreen(); + static void UpdateLeds(); + + static const prog_EventHandlers event_handlers_; + + private: + static uint8_t selector_; // 0-6: which envelope + + // Compute the patch byte offset for ADSR knob (0-3) given current selector. + static uint8_t AdsrOffset(uint8_t knob); + + DISALLOW_COPY_AND_ASSIGN(EnvPage); +}; + +} // namespace ambika + +#endif // CONTROLLER_UI_PAGES_ENV_PAGE_H_ From eb5920424e3431b316b30210cc51cb05bb8a0afd Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 12:01:27 +0200 Subject: [PATCH 26/67] Randomizer covers extra envelopes --- controller/ui_pages/randomizer.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/controller/ui_pages/randomizer.cc b/controller/ui_pages/randomizer.cc index 7a51358..3ae2e28 100644 --- a/controller/ui_pages/randomizer.cc +++ b/controller/ui_pages/randomizer.cc @@ -167,6 +167,10 @@ void Randomizer::Randomize() { uint8_t base = 24 + env * 8; RandomizeRange(patch, base, base + 4, 0, 127, env_depth); // ADSR } + for (uint8_t env = 0; env < 4; ++env) { + uint8_t base = 112 + env * 4; + RandomizeRange(patch, base, base + 4, 0, 127, env_depth); // Extra env ADSR + } } // --- LFOs (within envelope blocks, offset 28-29, 36-37, 44-45) --- From ae1113ed0c0c7d6c5bae76dcc1af71accd183ace Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 12:02:54 +0200 Subject: [PATCH 27/67] v2.0 seven envelopes --- CARCOSA.md | 16 +- controller/ui_pages/os_info_page.cc | 2 +- .../plans/2026-04-20-seven-envelopes.md | 425 ++++++++++++++++++ 3 files changed, 439 insertions(+), 4 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-20-seven-envelopes.md diff --git a/CARCOSA.md b/CARCOSA.md index 1624005..791b722 100644 --- a/CARCOSA.md +++ b/CARCOSA.md @@ -1,4 +1,4 @@ -# Carcosa v1.6 +# Carcosa v2.0 ## Custom firmware for the Ambika polysynth Carcosa replaces the stock Ambika firmware with a focused set of synthesis engines, expanded envelope capabilities, and a streamlined interface. @@ -178,7 +178,13 @@ All other pages (filter, envelope, modulation, part, arpeggiator, multi, perform ## Envelopes -Each of the 3 envelopes has a selectable curve mode on the ENV/LFO page (knob 4, labeled `mode`): +Carcosa has **7 envelopes**. Envelopes 1-3 have paired LFOs and curve selection (exponential, linear, looping). Envelopes 4-7 are simple ADSR — lightweight extra modulation sources for per-operator FM control, layered modulation, or any creative routing. + +The ENV/LFO page selector (knob 1) cycles through all 7 envelopes. When env 1-3 is selected, the top row shows LFO rate, shape, and envelope curve. When env 4-7 is selected, the top row is blank — just ADSR on the bottom row. + +All 7 envelopes are available as modulation sources in the mod matrix (env1 through env7). + +Envelopes 1-3 have a selectable curve mode (knob 4, labeled `mode`): | Mode | Behavior | |------|----------| @@ -280,7 +286,11 @@ All synthesis modes respond to MIDI CCs. The special modes reuse existing patch |----|-----------| | 108 | FM feedback (fm4op mode) | | 109 | Slop | -| 110 | Envelope curve (per active envelope) | +| 110 | Envelope curve (env 1-3 only) | +| 111 | Extra envelope attack (env 4-7) | +| 112 | Extra envelope decay (env 4-7) | +| 113 | Extra envelope sustain (env 4-7) | +| 114 | Extra envelope release (env 4-7) | ### Classic Mode diff --git a/controller/ui_pages/os_info_page.cc b/controller/ui_pages/os_info_page.cc index ee639c7..c152879 100755 --- a/controller/ui_pages/os_info_page.cc +++ b/controller/ui_pages/os_info_page.cc @@ -133,7 +133,7 @@ void OsInfoPage::UpdateScreen() { display.line_buffer(0)[0] = ' '; char* buffer = display.line_buffer(0) + 1; memcpy_P(&buffer[0], PSTR("carcosa"), 7); - memcpy_P(&buffer[9], PSTR("v1.6"), 4); + memcpy_P(&buffer[9], PSTR("v2.0"), 4); memcpy_P(&buffer[15], PSTR("port 1 device ?"), 15); Word version_number = voicecard_tx.GetVersion(active_control_); diff --git a/docs/superpowers/plans/2026-04-20-seven-envelopes.md b/docs/superpowers/plans/2026-04-20-seven-envelopes.md new file mode 100644 index 0000000..5e51df0 --- /dev/null +++ b/docs/superpowers/plans/2026-04-20-seven-envelopes.md @@ -0,0 +1,425 @@ +# Seven Envelopes Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Expand Carcosa from 3 to 7 ADSR envelopes available as modulation sources in all synthesis modes. + +**Architecture:** Grow the Patch struct from 112 to 128 bytes by appending 4 extra ADSR blocks (4 bytes each = 16 bytes). Decouple kNumLfos from kNumEnvelopes — add kNumTotalEnvelopes=7 while keeping kNumLfos=3 and kNumEnvLfoSlots=3. Remove MOD_SRC_SEQ_1 and MOD_SRC_SEQ_2 from the modulation source enum, insert MOD_SRC_ENV_4 through MOD_SRC_ENV_7 after ENV_3. Create custom EnvPage class for dynamic parameter routing based on selected envelope. Breaking change to patch format — all stored patches become invalid. + +**Tech Stack:** AVR C++ (avr-gcc), ATmega328p voicecard, ATmega644p controller + +--- + +## File Structure + +| File | Action | Responsibility | +|------|--------|---------------| +| `common/patch.h` | Modify | Add ExtraEnvelopeSettings struct, expand Patch to 128 bytes, update ModulationSource enum, add kNumTotalEnvelopes | +| `voicecard/envelope.h` | No change | Envelope class already supports all needed features | +| `voicecard/voice.h` | Modify | Change envelope array size to kNumTotalEnvelopes | +| `voicecard/voice.cc` | Modify | Render and update all 7 envelopes, extend init_patch with 16 zero bytes, replace MOD_SRC_SEQ references | +| `voicecard/voicecard_rx.h` | Modify | Extend COMMAND_RETRIGGER_ENVELOPE cases from 0-2 to 0-6 | +| `controller/parameter.h` | Modify | Bump kNumParameters to 80 | +| `controller/parameter.cc` | Modify | Add env4-7 ADSR parameters, replace mod source string display with switch, update CC map | +| `controller/part.cc` | Modify | Replace MOD_SRC_SEQ references in init_patch, extend init_patch with 16 zero bytes | +| `controller/ui.h` | Modify | Add PAGE_ENV_EXTRA enum entry | +| `controller/ui.cc` | Modify | Expand envelope selector range 0-6, register EnvPage | +| `controller/ui_pages/env_page.h` | Create | Custom envelope page header | +| `controller/ui_pages/env_page.cc` | Create | Custom envelope page — routes knobs to env1-3 (stride 8) or env4-7 (stride 4) based on selector | +| `controller/ui_pages/randomizer.cc` | Modify | Extend envelope randomization to cover extra_env offsets 112-127 | +| `controller/ui_pages/os_info_page.cc` | Modify | Bump version to v2.0 | +| `CARCOSA.md` | Modify | Document 7 envelopes, update mod source list, MIDI CC table | + +**Not updated (deprecated):** `utils/convert_shruthi_data.py` — hardcoded enum values no longer valid, utility is unmaintained. + +--- + +## Data Layout + +### New Patch (128 bytes) +``` +Offset 0-111: Unchanged (all existing fields at same offsets) +Offset 112-115: extra_env[0] — env4 ADSR (attack, decay, sustain, release) +Offset 116-119: extra_env[1] — env5 ADSR +Offset 120-123: extra_env[2] — env6 ADSR +Offset 124-127: extra_env[3] — env7 ADSR +``` + +### Constants +``` +kNumEnvLfoSlots = 3 (env_lfo[3] — envelopes with paired LFOs) +kNumLfos = 3 (unchanged, decoupled from envelope count) +kNumTotalEnvelopes = 7 +kNumExtraEnvelopes = 4 +``` + +### ModulationSource Enum (new order) +``` + 0: MOD_SRC_ENV_1 + 1: MOD_SRC_ENV_2 + 2: MOD_SRC_ENV_3 + 3: MOD_SRC_ENV_4 ← new + 4: MOD_SRC_ENV_5 ← new + 5: MOD_SRC_ENV_6 ← new + 6: MOD_SRC_ENV_7 ← new + 7: MOD_SRC_LFO_1 ← was 3 + 8: MOD_SRC_LFO_2 ← was 4 + 9: MOD_SRC_LFO_3 ← was 5 +10: MOD_SRC_LFO_4 ← was 6 +11: MOD_SRC_OP_1 ← was 7 +12: MOD_SRC_OP_2 ← was 8 +13: MOD_SRC_OP_3 ← was 9 +14: MOD_SRC_OP_4 ← was 10 +15: MOD_SRC_ARP_STEP ← was 13 (SEQ_1/SEQ_2 removed) +16: MOD_SRC_VELOCITY ← was 14 +17: MOD_SRC_AFTERTOUCH +18: MOD_SRC_PITCH_BEND +19: MOD_SRC_WHEEL +20: MOD_SRC_WHEEL_2 +21: MOD_SRC_EXPRESSION +22: MOD_SRC_NOTE +23: MOD_SRC_GATE +24: MOD_SRC_NOISE +25: MOD_SRC_RANDOM +26: MOD_SRC_CONSTANT_256 +27: MOD_SRC_CONSTANT_128 +28: MOD_SRC_CONSTANT_64 +29: MOD_SRC_CONSTANT_32 +30: MOD_SRC_CONSTANT_16 +31: MOD_SRC_CONSTANT_8 +32: MOD_SRC_CONSTANT_4 +33: MOD_SRC_LAST +``` + +--- + +### Task 1: Expand Patch struct and enums + +**Files:** +- Modify: `common/patch.h` + +- [ ] **Step 1: Add ExtraEnvelopeSettings struct** + +```cpp +struct ExtraEnvelopeSettings { + uint8_t attack; + uint8_t decay; + uint8_t sustain; + uint8_t release; +}; +``` + +- [ ] **Step 2: Expand Patch struct** + +Add after `padding[8]`: +```cpp + // Offset: 112-128 + ExtraEnvelopeSettings extra_env[4]; +}; +``` + +- [ ] **Step 3: Update constants** + +Do NOT change `kNumEnvelopes`. Instead: +```cpp +static const uint8_t kNumEnvLfoSlots = 3; // rename from kNumEnvelopes +static const uint8_t kNumLfos = kNumEnvLfoSlots; // keep at 3 +static const uint8_t kNumTotalEnvelopes = 7; +static const uint8_t kNumExtraEnvelopes = 4; +``` + +Then find/replace all `kNumEnvelopes` references: +- Uses that mean "env_lfo slots" or "LFO count" → `kNumEnvLfoSlots` +- Uses that mean "total envelope instances on voicecard" → `kNumTotalEnvelopes` + +- [ ] **Step 4: Update ModulationSource enum** + +Remove `MOD_SRC_SEQ_1` and `MOD_SRC_SEQ_2`. Insert `MOD_SRC_ENV_4` through `MOD_SRC_ENV_7` after `MOD_SRC_ENV_3`. + +- [ ] **Step 5: Add PatchParameter entries** + +```cpp + PRM_PATCH_EXTRA_ENV_ATTACK = 112, + PRM_PATCH_EXTRA_ENV_DECAY, + PRM_PATCH_EXTRA_ENV_SUSTAIN, + PRM_PATCH_EXTRA_ENV_RELEASE, +``` + +- [ ] **Step 6: Build voicecard to check compile** + +Run: `rm -rf build/ambika_voicecard && make all 2>&1 | grep error | head 20` +Expect: errors from renamed constants — fix all references. + +- [ ] **Step 7: Commit** + +--- + +### Task 2: Update voicecard envelope rendering + +**Files:** +- Modify: `voicecard/voice.h` +- Modify: `voicecard/voice.cc` +- Modify: `voicecard/voicecard_rx.h` + +- [ ] **Step 1: Expand envelope array** + +In `voice.h`, change `envelope_[kNumEnvelopes]` to `envelope_[kNumTotalEnvelopes]`. Update the static definition in `voice.cc`. + +- [ ] **Step 2: Update LoadSources** + +Replace 3 hardcoded render lines with loop: +```cpp +for (uint8_t i = 0; i < kNumTotalEnvelopes; ++i) { + modulation_sources_[MOD_SRC_ENV_1 + i] = envelope_[i].Render(); +} +``` + +- [ ] **Step 3: Update UpdateDestinations** + +Keep existing env_lfo loop for envelopes 0-2 (uses `kNumEnvLfoSlots`). Add second loop for extra envelopes 3-6: +```cpp +for (int i = 0; i < kNumExtraEnvelopes; ++i) { + envelope_[kNumEnvLfoSlots + i].Update( + patch_.extra_env[i].attack, + patch_.extra_env[i].decay, + patch_.extra_env[i].sustain, + patch_.extra_env[i].release); +} +``` + +- [ ] **Step 4: Update Init loop** + +Use `kNumTotalEnvelopes` for envelope init loop. Use `kNumEnvLfoSlots` for env_lfo-specific init. + +- [ ] **Step 5: Update TriggerEnvelope** + +The `TriggerEnvelope(uint8_t stage)` loop that triggers ALL envelopes: use `kNumTotalEnvelopes`. + +- [ ] **Step 6: Extend COMMAND_RETRIGGER_ENVELOPE in voicecard_rx.h** + +Add cases for envelopes 3-6: +```cpp +case COMMAND_RETRIGGER_ENVELOPE: +case COMMAND_RETRIGGER_ENVELOPE + 1: +case COMMAND_RETRIGGER_ENVELOPE + 2: +case COMMAND_RETRIGGER_ENVELOPE + 3: +case COMMAND_RETRIGGER_ENVELOPE + 4: +case COMMAND_RETRIGGER_ENVELOPE + 5: +case COMMAND_RETRIGGER_ENVELOPE + 6: + voice.TriggerEnvelope(command_ & 0x0f, ATTACK); + break; +``` + +- [ ] **Step 7: Update init_patch in voice.cc** + +Extend the PROGMEM `init_patch` with 16 zero bytes for extra_env[4]: +```cpp + // Extra envelopes (env4-7 ADSR) + 0, 40, 20, 60, + 0, 40, 20, 60, + 0, 40, 20, 60, + 0, 40, 20, 60, +``` + +- [ ] **Step 8: Replace MOD_SRC_SEQ references in voice.cc init_patch** + +Replace `MOD_SRC_SEQ_1` and `MOD_SRC_SEQ_2` with `MOD_SRC_ENV_4` and `MOD_SRC_ENV_5` (or zero them out). + +- [ ] **Step 9: Update ResetAllControllers** + +All `MOD_SRC_CONSTANT_*` enum values shifted. These are referenced by name so they auto-update, but verify. + +- [ ] **Step 10: Build voicecard, check size** + +Run: `rm -rf build/ambika_voicecard && make all && make bin` +Check: `avr-size build/ambika_voicecard/ambika_voicecard.elf` +Verify: RAM (data+bss) leaves >= 450 bytes free. Flash fits. + +- [ ] **Step 11: Commit** + +--- + +### Task 3: Update controller init_patch and mod source references + +**Files:** +- Modify: `controller/part.cc` + +- [ ] **Step 1: Replace MOD_SRC_SEQ references in init_patch** + +Replace `MOD_SRC_SEQ_1` and `MOD_SRC_SEQ_2` in both the modulation matrix init and modifier init sections. + +- [ ] **Step 2: Extend init_patch with extra_env bytes** + +Add 16 bytes to the PROGMEM `init_patch` for the extra envelopes. + +- [ ] **Step 3: Fix all kNumEnvelopes → kNumEnvLfoSlots references** + +Grep part.cc for `kNumEnvelopes` and replace with appropriate constant. + +- [ ] **Step 4: Build controller** + +- [ ] **Step 5: Commit** + +--- + +### Task 4: Update mod source display strings + +**Files:** +- Modify: `controller/parameter.cc` + +- [ ] **Step 1: Replace mod source string lookup with switch statement** + +In `PrintValue`, for `UNIT_MODULATION_SOURCE`, replace the `text + value` string table lookup with a complete switch covering all 33 sources. Use PSTR for each name. Handle both short (width <= 5) and long (width > 5) variants. + +```cpp +if (unit == UNIT_MODULATION_SOURCE) { + const prog_char* name; + switch (value) { + case MOD_SRC_ENV_1: name = (width > 5) ? PSTR("env 1") : PSTR("env1"); break; + case MOD_SRC_ENV_2: name = (width > 5) ? PSTR("env 2") : PSTR("env2"); break; + // ... all 33 sources + } + strncpy_P(buffer, name, width); + AlignRight(buffer, width); + return; +} +``` + +- [ ] **Step 2: Build and verify** + +- [ ] **Step 3: Commit** + +--- + +### Task 5: Add controller parameters for extra envelopes + +**Files:** +- Modify: `controller/parameter.h` +- Modify: `controller/parameter.cc` + +- [ ] **Step 1: Add 4 parameter definitions (76-79)** + +Extra envelope ADSR parameters. num_instances=4, stride=4, indexed_by a new UI state variable (see Task 6). + +Actually, since we're creating a custom EnvPage, these parameters don't need the stride/indexed_by pattern — the EnvPage handles the mapping directly by reading/writing patch bytes. The parameters just need to exist for MIDI CC access. + +```cpp +// 76: extra env attack (instance 0, MIDI-accessible) +{ PARAMETER_LEVEL_PATCH, PRM_PATCH_EXTRA_ENV_ATTACK, + UNIT_RAW_UINT8, 0, 127, 4, 4, 0xff, 0xff, + STR_RES_ATTK, STR_RES_ATTACK, STR_RES_ENVELOPE }, +// 77-79: decay, sustain, release (same pattern) +``` + +- [ ] **Step 2: Bump kNumParameters to 80** + +- [ ] **Step 3: Update MIDI CC map** + +CC 111-114 → params 76-79. + +- [ ] **Step 4: Build and verify** + +- [ ] **Step 5: Commit** + +--- + +### Task 6: Create custom EnvPage for 7 envelopes + +**Files:** +- Create: `controller/ui_pages/env_page.h` +- Create: `controller/ui_pages/env_page.cc` +- Modify: `controller/ui.h` +- Modify: `controller/ui.cc` + +- [ ] **Step 1: Create EnvPage class** + +Follows the same pattern as FmPage/KsPage/WcPage. Has a selector (knob 1) that cycles 0-6 (env1-env7). + +When selector 0-2: shows LFO controls (rate, shape) on top row + ADSR on bottom row + envelope curve. Reads/writes from `patch_.env_lfo[selector]`. + +When selector 3-6: hides LFO controls (top row knobs 2-4 = 0xff). ADSR on bottom row reads/writes from `patch_.extra_env[selector - 3]`. No envelope curve knob. + +Display: selector shows "env1" through "env7". + +- [ ] **Step 2: Register EnvPage in ui.h and ui.cc** + +Replace the existing `PAGE_ENV_LFO` entry to use `EnvPage::event_handlers_` instead of `ParameterEditor::event_handlers_`. + +Or: keep PAGE_ENV_LFO for the normal parameter editor and add the EnvPage as the handler. Since the existing page already uses custom parameter indices, swapping the handler is cleanest. + +- [ ] **Step 3: Build and verify** + +- [ ] **Step 4: Test selector cycling through all 7 envelopes** + +- [ ] **Step 5: Commit** + +--- + +### Task 7: Update randomizer for extra envelopes + +**Files:** +- Modify: `controller/ui_pages/randomizer.cc` + +- [ ] **Step 1: Extend envelope randomization** + +The existing loop `for (uint8_t env = 0; env < 3; ++env)` handles env_lfo offsets 24-47. Add a second loop for extra_env offsets 112-127: + +```cpp +if (env_depth) { + // Original 3 envelopes + for (uint8_t env = 0; env < 3; ++env) { + uint8_t base = 24 + env * 8; + RandomizeRange(patch, base, base + 4, 0, 127, env_depth); + } + // Extra 4 envelopes + for (uint8_t env = 0; env < 4; ++env) { + uint8_t base = 112 + env * 4; + RandomizeRange(patch, base, base + 4, 0, 127, env_depth); + } +} +``` + +- [ ] **Step 2: Commit** + +--- + +### Task 8: Version bump, docs, release + +**Files:** +- Modify: `controller/ui_pages/os_info_page.cc` +- Modify: `CARCOSA.md` +- Modify: `README.md` + +- [ ] **Step 1: Bump version to v2.0** + +- [ ] **Step 2: Update manual** + +Add env4-7 documentation to envelope section. Update mod source list (remove seq1/seq2, add env4-7). Update MIDI CC table. Add note about breaking patch format change. + +- [ ] **Step 3: Full build both firmwares** + +```bash +rm -rf build/ambika_voicecard build/ambika_controller +make all && make bin +make -f controller/makefile && make -f controller/makefile bin +avr-size build/ambika_voicecard/ambika_voicecard.elf build/ambika_controller/ambika_controller.elf +``` + +Verify: voicecard RAM leaves >= 450 bytes free. Controller stack >= 240 bytes. + +- [ ] **Step 4: Commit, push, create release** + +--- + +## Risk Assessment + +| Risk | Mitigation | +|------|-----------| +| Enum shift breaks all stored patches | Accepted — breaking change for v2.0. Document in release notes. EEPROM will need wiping on update. | +| kNumLfos coupling with kNumEnvelopes | Decoupled: kNumEnvLfoSlots=3, kNumLfos=3, kNumTotalEnvelopes=7 | +| RETRIGGER_ENVELOPE protocol limited to 0-2 | Extended to 0-6 in voicecard_rx.h | +| init_patch size mismatch after Patch grows | Both init_patches (voice.cc and part.cc) extended with 16 bytes | +| Mod source string table broken by enum shift | Replaced with switch statement — no string table dependency | +| EEPROM layout invalid after Patch grows | Document: user must reinitialize from OS info page or save/reload from SD | +| convert_shruthi_data.py broken | Deprecated — document as unmaintained | +| Controller RAM from custom EnvPage | Minimal — follows proven FmPage/KsPage/WcPage pattern with PROGMEM arrays | From 6d72419b9421994c149b95bab347bb7c5b55124d Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 12:40:36 +0200 Subject: [PATCH 28/67] Remove sequencer data, unify envelope format to 8 bytes Kill sequencer fields from PartData (shrinks from 84 to 20 bytes), replace ExtraEnvelopeSettings with full EnvelopeLfoSettings for extra envelopes (Patch grows from 128 to 144 bytes). Envelope curve now works for env 4-7 on the voicecard. Delete sequence editor UI. Update SysEx/RIFF wire protocol to skip removed sequence object slot. Controller RAM: 3638 bytes (458 bytes stack headroom) Voicecard RAM: 1596 bytes Co-Authored-By: Claude Opus 4.6 (1M context) --- common/patch.h | 11 +- controller/parameter.cc | 32 +-- controller/part.cc | 65 +----- controller/part.h | 92 +------- controller/storage.cc | 64 +++--- controller/storage.h | 3 +- controller/ui_pages/env_page.cc | 82 ++++--- controller/ui_pages/library.cc | 8 +- controller/ui_pages/sequence_editor.cc | 289 ------------------------- controller/ui_pages/sequence_editor.h | 58 ----- voicecard/voice.cc | 19 +- 11 files changed, 141 insertions(+), 582 deletions(-) delete mode 100755 controller/ui_pages/sequence_editor.cc delete mode 100755 controller/ui_pages/sequence_editor.h diff --git a/common/patch.h b/common/patch.h index af1f0ce..c5f5e1b 100755 --- a/common/patch.h +++ b/common/patch.h @@ -56,13 +56,6 @@ struct EnvelopeLfoSettings { uint8_t retrigger_mode; }; -struct ExtraEnvelopeSettings { - uint8_t attack; - uint8_t decay; - uint8_t sustain; - uint8_t release; -}; - struct Modulation { uint8_t source; uint8_t destination; @@ -284,8 +277,8 @@ struct Patch { // Offset: 104-112 uint8_t padding[8]; - // Offset: 112-128 - ExtraEnvelopeSettings extra_env[kNumExtraEnvelopes]; + // Offset: 112-144 + EnvelopeLfoSettings extra_env_lfo[kNumExtraEnvelopes]; }; typedef Patch PROGMEM prog_Patch; diff --git a/controller/parameter.cc b/controller/parameter.cc index 7755912..c56e015 100755 --- a/controller/parameter.cc +++ b/controller/parameter.cc @@ -802,26 +802,26 @@ static const prog_Parameter parameters[kNumParameters] PROGMEM = { 1, 0, 0xff, 106, STR_RES_GRID, STR_RES_GRID, STR_RES_ARPEGGIO }, - // 54 + // 54 - (removed: was sequence length 1) { PARAMETER_LEVEL_PART, - PRM_PART_SEQUENCE_LENGTH_1, - UNIT_UINT8, 1, 32, + PRM_PART_POLYPHONY_MODE + 1, + UNIT_UINT8, 0, 0, 1, 0, 0xff, 0xff, - STR_RES_LEN1, STR_RES_SEQ1_LEN, STR_RES_SEQUENCER }, + STR_RES_LEN1, STR_RES_SEQ1_LEN, STR_RES_PART }, - // 55 + // 55 - (removed: was sequence length 2) { PARAMETER_LEVEL_PART, - PRM_PART_SEQUENCE_LENGTH_2, - UNIT_UINT8, 1, 32, + PRM_PART_POLYPHONY_MODE + 2, + UNIT_UINT8, 0, 0, 1, 0, 0xff, 0xff, - STR_RES_LEN2, STR_RES_SEQ2_LEN, STR_RES_SEQUENCER }, + STR_RES_LEN2, STR_RES_SEQ2_LEN, STR_RES_PART }, - // 56 + // 56 - (removed: was sequence length 3) { PARAMETER_LEVEL_PART, - PRM_PART_SEQUENCE_LENGTH_3, - UNIT_UINT8, 1, 32, + PRM_PART_POLYPHONY_MODE + 3, + UNIT_UINT8, 0, 0, 1, 0, 0xff, 0xff, - STR_RES_LENP, STR_RES_PATT_LEN, STR_RES_SEQUENCER }, + STR_RES_LENP, STR_RES_PATT_LEN, STR_RES_PART }, // 57 { PARAMETER_LEVEL_PART, @@ -960,28 +960,28 @@ static const prog_Parameter parameters[kNumParameters] PROGMEM = { { PARAMETER_LEVEL_PATCH, PRM_PATCH_EXTRA_ENV_ATTACK, UNIT_RAW_UINT8, 0, 127, - 4, 4, 0xff, 0xff, + 4, 8, 0xff, 0xff, STR_RES_ATTK, STR_RES_ATTACK, STR_RES_ENVELOPE }, // 77 - extra envelope 4 decay { PARAMETER_LEVEL_PATCH, PRM_PATCH_EXTRA_ENV_DECAY, UNIT_RAW_UINT8, 0, 127, - 4, 4, 0xff, 0xff, + 4, 8, 0xff, 0xff, STR_RES_DECAY, STR_RES_DECAY, STR_RES_ENVELOPE }, // 78 - extra envelope 4 sustain { PARAMETER_LEVEL_PATCH, PRM_PATCH_EXTRA_ENV_SUSTAIN, UNIT_RAW_UINT8, 0, 127, - 4, 4, 0xff, 0xff, + 4, 8, 0xff, 0xff, STR_RES_SUSTAIN, STR_RES_SUSTAIN, STR_RES_ENVELOPE }, // 79 - extra envelope 4 release { PARAMETER_LEVEL_PATCH, PRM_PATCH_EXTRA_ENV_RELEASE, UNIT_RAW_UINT8, 0, 127, - 4, 4, 0xff, 0xff, + 4, 8, 0xff, 0xff, STR_RES_RELEASE, STR_RES_RELEASE, STR_RES_ENVELOPE }, }; diff --git a/controller/part.cc b/controller/part.cc index 3b0288f..7fbbca5 100755 --- a/controller/part.cc +++ b/controller/part.cc @@ -79,63 +79,31 @@ static const prog_Patch init_patch PROGMEM = { // Padding 0, 0, 0, 0, 0, 0, 0, 0, - // Extra envelopes (env4-7 ADSR: attack, decay, sustain, release) - 0, 40, 20, 60, - 0, 40, 20, 60, - 0, 40, 20, 60, - 0, 40, 20, 60, + // Extra envelope-LFO settings (env4-7: ADSR + LFO shape/rate/curve/retrigger) + 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, + 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, + 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, + 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, }; static const prog_PartData init_part PROGMEM = { // Volume 120, - + // Octave and tuning 0, 0, 0, 0, - + // Legato, portamento, seq mode 0, 0, 0, - + // Arp data 0, 1, 0, 10, - - // Sequence length - 16, - 16, - 16, + + // Polyphony mode POLY, - - // Step sequence 1 - 0xff, 0xff, 0x80, 0x80, 0xcc, 0xcc, 0x20, 0x20, - 0x00, 0x20, 0x40, 0x60, 0x80, 0xa0, 0xc0, 0xff, - // Step sequence 1 - 0x00, 0x10, 0x20, 0x40, 0x80, 0xff, 0x80, 0x40, - 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x80, 0x40, - - // Note sequence - 60 | 0x80, 100, - 60, 100, - 48 | 0x80, 100, - 48, 100, - 60 | 0x80, 100, - 60, 100, - 48 | 0x80, 100, - 48, 100, - 60 | 0x80, 100, - 60, 100, - 48 | 0x80, 100, - 48 | 0x80, 100 | 0x80, - 60 | 0x80, 100, - 60, 100, - 48 | 0x80, 100, - 48, 100, - // Padding - 0, - 0, - 0, - 0, + 0, 0, 0, 0, 0, 0, 0, }; void Part::Touch() { @@ -207,17 +175,6 @@ void Part::InitSettings(InitializationMode mode) { Touch(); } -void Part::InitSequence(InitializationMode mode) { - if (mode == INITIALIZATION_DEFAULT) { - memcpy_P( - mutable_raw_sequence_data(), - (prog_char*)(&init_part) + 8, - 72); - } else { - RandomizeRange(PRM_PART_VOLUME + 8, 72); - } -} - void Part::RandomizeRange(uint8_t start, uint8_t size) { for (uint8_t i = start; i < start + size; ++i) { uint8_t parameter_id = parameter_manager.AddressToParameterId(i); diff --git a/controller/part.h b/controller/part.h index 16c92aa..eedd5e4 100755 --- a/controller/part.h +++ b/controller/part.h @@ -69,15 +69,6 @@ enum PartFlags { FLAG_HAS_USER_CHANGE = 2, }; -static const uint8_t kNumSequences = 3; - -struct NoteStep { - uint8_t note; - uint8_t velocity; - uint8_t legato; - uint8_t gate; -}; - struct PartData { // Offset: 0 uint8_t volume; @@ -99,76 +90,11 @@ struct PartData { uint8_t arp_pattern; uint8_t arp_divider; - // Offset: 12..16 - uint8_t sequence_length[kNumSequences]; + // Offset: 12 uint8_t polyphony_mode; - // Offset: 16-80 - // 0..15: step sequence 1 - // 16..31: step sequence 2 - // 32..63: (note value | 0x80 if gate), (note velocity | 0x80 if legato) - uint8_t sequence_data[64]; - - // Offset: 80-84 - uint8_t padding[4]; - - uint8_t step_value(uint8_t sequence, uint8_t step) const { - return sequence_data[(step + (sequence << 4)) & 0x1f]; - } - - void set_step_value(uint8_t sequence, uint8_t step, uint8_t value) { - sequence_data[(step + (sequence << 4)) & 0x1f] = value; - } - - uint8_t note(uint8_t step) const { - uint8_t offset = (32 + (step << 1)) & 0x3f; - return sequence_data[offset] & 0x7f; - } - - void set_note(uint8_t step, uint8_t note) { - uint8_t offset = (32 + (step << 1)) & 0x3f; - sequence_data[offset] &= 0x80; - sequence_data[offset] |= note; - } - - uint16_t ordered_gate_velocity(uint8_t step) const { - uint8_t offset = (32 + (step << 1)) & 0x3f; - if (!(sequence_data[offset] & 0x80)) { - return 0; - } else { - uint8_t o = sequence_data[offset + 1] + 128; - return static_cast(o) + 1; - } - } - - void set_ordered_gate_velocity(uint8_t step, uint16_t value) { - uint8_t offset = (32 + (step << 1)) & 0x3f; - if (!value) { - sequence_data[offset] &= 0x7f; - sequence_data[offset + 1] = 0; - } else { - sequence_data[offset] |= 0x80; - sequence_data[offset + 1] = (value - 1) + 128; - } - } - - void set_velocity(uint8_t step, uint8_t velocity) { - uint8_t offset = (32 + (step << 1)) & 0x3f; - sequence_data[offset + 1] &= 0x80; - sequence_data[offset + 1] |= velocity; - } - - NoteStep note_step(uint8_t step) const { - NoteStep n; - uint8_t offset = (32 + (step << 1)) & 0x3f; - n.note = sequence_data[offset]; - n.velocity = sequence_data[offset + 1]; - n.gate = n.note & 0x80; - n.legato = n.velocity & 0x80; - n.note &= 0x7f; - n.velocity &= 0x7f; - return n; - } + // Offset: 13-19 + uint8_t padding[7]; }; typedef PartData PROGMEM prog_PartData; @@ -186,9 +112,6 @@ enum PartParameter { PRM_PART_ARP_OCTAVE, PRM_PART_ARP_PATTERN, PRM_PART_ARP_RESOLUTION, - PRM_PART_SEQUENCE_LENGTH_1, - PRM_PART_SEQUENCE_LENGTH_2, - PRM_PART_SEQUENCE_LENGTH_3, PRM_PART_POLYPHONY_MODE }; @@ -199,7 +122,6 @@ class Part { void InitPatch(InitializationMode mode); void InitSettings(InitializationMode mode); - void InitSequence(InitializationMode mode); void NoteOn(uint8_t note, uint8_t velocity); void NoteOff(uint8_t note); @@ -230,19 +152,13 @@ class Part { PartData* mutable_data() { return &data_; } const PartData& data() const { return data_; } - const uint8_t* raw_sequence_data() const { - return static_cast(static_cast(&data_)) + 8; - } - const uint8_t* raw_data() const { + const uint8_t* raw_data() const { return static_cast(static_cast(&data_)); } const uint8_t* raw_patch_data() const { return static_cast(static_cast(&patch_)); } - uint8_t* mutable_raw_sequence_data() { - return static_cast(static_cast(&data_)) + 8; - } uint8_t* mutable_raw_data() { return static_cast(static_cast(&data_)); } diff --git a/controller/storage.cc b/controller/storage.cc index 499533c..a2458e8 100755 --- a/controller/storage.cc +++ b/controller/storage.cc @@ -154,10 +154,7 @@ uint8_t Storage::object_size(const StorageLocation& location) { switch (location.object) { case STORAGE_OBJECT_PATCH: return sizeof(Patch); - - case STORAGE_OBJECT_SEQUENCE: - return 72; - + case STORAGE_OBJECT_PART: return sizeof(PartData); @@ -188,10 +185,7 @@ const uint8_t* Storage::object_data(const StorageLocation& location) { switch (location.object) { case STORAGE_OBJECT_PATCH: return multi.part(location.part).raw_patch_data(); - - case STORAGE_OBJECT_SEQUENCE: - return multi.part(location.part).raw_sequence_data(); - + case STORAGE_OBJECT_PART: return multi.part(location.part).raw_data(); @@ -206,10 +200,7 @@ uint8_t* Storage::mutable_object_data(const StorageLocation& location) { switch (location.object) { case STORAGE_OBJECT_PATCH: return multi.mutable_part(location.part)->mutable_raw_patch_data(); - - case STORAGE_OBJECT_SEQUENCE: - return multi.mutable_part(location.part)->mutable_raw_sequence_data(); - + case STORAGE_OBJECT_PART: return multi.mutable_part(location.part)->mutable_raw_data(); @@ -279,7 +270,6 @@ void Storage::ForEachObject( destination.alias = 0; switch(destination.object) { case STORAGE_OBJECT_PATCH: - case STORAGE_OBJECT_SEQUENCE: case STORAGE_OBJECT_PART: (*object_fn)(destination); break; @@ -324,7 +314,6 @@ void Storage::TouchObject(const StorageLocation& location) { break; case STORAGE_OBJECT_PART: - case STORAGE_OBJECT_SEQUENCE: multi.mutable_part(location.part)->Touch(); break; @@ -336,8 +325,12 @@ void Storage::TouchObject(const StorageLocation& location) { /* static */ void Storage::SysExSendObject(const StorageLocation& location) { + // Map StorageObject to SysEx command, preserving wire protocol + // (skip old slot 2 = sequence). + uint8_t cmd = location.object + 1; + if (cmd >= 2) ++cmd; // shift up past removed sequence slot SysExSendRaw( - location.object + 1, + cmd, location.alias, object_data(location), object_size(location), @@ -397,8 +390,11 @@ void Storage::RIFFWriteObject(const StorageLocation& location) { w.value = size + 4; file_.Write(w.bytes, 4, &written); // Write the position words. + // Map StorageObject to wire format (skip old slot 2 = sequence). w.value = 0; - w.bytes[0] = location.object + 1; + uint8_t wire_obj = location.object + 1; + if (wire_obj >= 2) ++wire_obj; // shift up past removed sequence slot + w.bytes[0] = wire_obj; w.bytes[1] = location.alias; file_.Write(w.bytes, 4, &written); @@ -450,7 +446,14 @@ FilesystemStatus Storage::Load( if (id.value == kObjectTag && load_contents) { file_.Read(id.bytes, 4, &read); StorageLocation destination; - destination.object = static_cast(id.bytes[0] - 1); + uint8_t obj_cmd = id.bytes[0]; + if (obj_cmd == 2) { + // Old sequence object — skip. + file_.Seek(file_.tell() + size.value - 4); + continue; + } + if (obj_cmd >= 3) --obj_cmd; // shift down past removed sequence slot + destination.object = static_cast(obj_cmd - 1); destination.part = id.bytes[1] == 0 ? location.part : (id.bytes[1] - 1); uint8_t expected_size = object_size(destination); @@ -670,13 +673,15 @@ void Storage::SysExParseCommand() { sysex_rx_state_ = RECEIVING_DATA; switch (sysex_rx_command_[0]) { case 0x01: - case 0x02: case 0x03: case 0x04: case 0x05: { StorageLocation location; - location.object = static_cast(sysex_rx_command_[0] - 1); + // Map SysEx command to StorageObject (skip old 0x02 = sequence). + uint8_t cmd = sysex_rx_command_[0]; + if (cmd >= 0x03) --cmd; // shift down past removed sequence slot + location.object = static_cast(cmd - 1); sysex_rx_expected_size_ = object_size(location); } break; @@ -689,7 +694,6 @@ void Storage::SysExParseCommand() { break; case 0x11: - case 0x12: case 0x13: case 0x14: case 0x15: @@ -719,13 +723,16 @@ void Storage::SysExAcceptCommand() { switch (sysex_rx_command_[0]) { case 0x01: - case 0x02: case 0x03: case 0x04: case 0x05: - location.object = static_cast(sysex_rx_command_[0] - 0x01); - ReadObject(location); - TouchObject(location); + { + uint8_t cmd = sysex_rx_command_[0]; + if (cmd >= 0x03) --cmd; // shift down past removed sequence slot + location.object = static_cast(cmd - 0x01); + ReadObject(location); + TouchObject(location); + } break; case 0x0f: @@ -741,12 +748,15 @@ void Storage::SysExAcceptCommand() { break; case 0x11: - case 0x12: case 0x13: case 0x14: case 0x15: - location.object = static_cast(sysex_rx_command_[0] - 0x11); - SysExSend(location); + { + uint8_t cmd = sysex_rx_command_[0]; + if (cmd >= 0x13) --cmd; // shift down past removed sequence slot + location.object = static_cast(cmd - 0x11); + SysExSend(location); + } break; case 0x1f: diff --git a/controller/storage.h b/controller/storage.h index 0514fcc..8e436d1 100755 --- a/controller/storage.h +++ b/controller/storage.h @@ -34,10 +34,9 @@ using avrlib::FilesystemStatus; enum StorageObject { STORAGE_OBJECT_PATCH, - STORAGE_OBJECT_SEQUENCE, + STORAGE_OBJECT_PART, STORAGE_OBJECT_PROGRAM, STORAGE_OBJECT_MULTI, - STORAGE_OBJECT_PART, }; enum SysExReceptionState { diff --git a/controller/ui_pages/env_page.cc b/controller/ui_pages/env_page.cc index b7cd850..2e50b76 100644 --- a/controller/ui_pages/env_page.cc +++ b/controller/ui_pages/env_page.cc @@ -79,7 +79,7 @@ uint8_t EnvPage::AdsrOffset(uint8_t knob) { if (selector_ < 3) { return 24 + selector_ * 8 + knob; } else { - return 112 + (selector_ - 3) * 4 + knob; + return 112 + (selector_ - 3) * 8 + knob; } } @@ -111,8 +111,20 @@ uint8_t EnvPage::OnIncrement(int8_t increment) { patch[offset] = val; multi.mutable_part(ui.state().active_part)->WriteToAllVoices( VOICECARD_DATA_PATCH, offset, patch[offset]); + } else if (ctrl == 3) { + // Curve knob — works for all 7 envelopes. + uint8_t base = selector_ < 3 + ? (24 + selector_ * 8) + : (112 + (selector_ - 3) * 8); + uint8_t offset = base + 6; + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val >= ENVELOPE_CURVE_LAST) val = ENVELOPE_CURVE_LAST - 1; + patch[offset] = val; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); } else if (selector_ < 3) { - // LFO controls (ctrl 1-3) only for env 1-3. + // LFO controls (ctrl 1-2) only for env 1-3. uint8_t base = 24 + selector_ * 8; if (ctrl == 1) { // Rate (offset +5), 0-127. @@ -132,26 +144,17 @@ uint8_t EnvPage::OnIncrement(int8_t increment) { patch[offset] = val; multi.mutable_part(ui.state().active_part)->WriteToAllVoices( VOICECARD_DATA_PATCH, offset, patch[offset]); - } else { - // Curve (offset +6), 0 to ENVELOPE_CURVE_LAST-1. - uint8_t offset = base + 6; - int16_t val = patch[offset] + increment; - if (val < 0) val = 0; - if (val >= ENVELOPE_CURVE_LAST) val = ENVELOPE_CURVE_LAST - 1; - patch[offset] = val; - multi.mutable_part(ui.state().active_part)->WriteToAllVoices( - VOICECARD_DATA_PATCH, offset, patch[offset]); } } - // else: ctrl 1-3 and selector >= 3 — ignore (no LFO for extra envs). + // ctrl 1-2 and selector >= 3: ignore (no LFO for extra envs). edit_mode_ = EDIT_STARTED_BY_ENCODER; } else { // Navigate controls. int8_t new_control = active_control_ + increment; if (new_control >= 0 && new_control < 8) { - // Skip LFO knobs (1-3) when selector >= 3. - if (selector_ >= 3 && new_control >= 1 && new_control <= 3) { - new_control = (increment > 0) ? 4 : 0; + // Skip LFO knobs (1-2) when selector >= 3 (curve knob 3 is valid). + if (selector_ >= 3 && new_control >= 1 && new_control <= 2) { + new_control = (increment > 0) ? 3 : 0; } if (new_control >= 0 && new_control < 8) { active_control_ = new_control; @@ -196,7 +199,23 @@ uint8_t EnvPage::OnPot(uint8_t index, uint8_t value) { return 1; } - // Knobs 1-3: LFO rate, shape, curve — only for selector 0-2. + // Knob 3 (curve) works for all envelopes. + if (index == 3) { + uint8_t base = selector_ < 3 + ? (24 + selector_ * 8) + : (112 + (selector_ - 3) * 8); + uint8_t offset = base + 6; + uint8_t val = (static_cast(value) * ENVELOPE_CURVE_LAST) >> 7; + if (val >= ENVELOPE_CURVE_LAST) val = ENVELOPE_CURVE_LAST - 1; + patch[offset] = val; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + active_control_ = index; + edit_mode_ = EDIT_STARTED_BY_POT; + return 1; + } + + // Knobs 1-2: LFO rate, shape — only for selector 0-2. if (selector_ >= 3) return 1; uint8_t base = 24 + selector_ * 8; @@ -214,14 +233,6 @@ uint8_t EnvPage::OnPot(uint8_t index, uint8_t value) { patch[offset] = val; multi.mutable_part(ui.state().active_part)->WriteToAllVoices( VOICECARD_DATA_PATCH, offset, patch[offset]); - } else { - // Curve. - uint8_t offset = base + 6; - uint8_t val = (static_cast(value) * ENVELOPE_CURVE_LAST) >> 7; - if (val >= ENVELOPE_CURVE_LAST) val = ENVELOPE_CURVE_LAST - 1; - patch[offset] = val; - multi.mutable_part(ui.state().active_part)->WriteToAllVoices( - VOICECARD_DATA_PATCH, offset, patch[offset]); } active_control_ = index; @@ -304,8 +315,27 @@ void EnvPage::UpdateScreen() { } } } else { - // Blank cell for knobs 1-3 when selector >= 3. - for (uint8_t j = 1; j < 10; ++j) buffer[j] = ' '; + // Selector >= 3: knobs 1-2 blank, knob 3 shows curve. + if (i == 3) { + // Curve knob for extra envelopes. + memcpy_P(&buffer[1], env_top_labels_lfo + 3 * 4, 4); // "curv" + if (i == active_control_ && buffer[1] >= 'a' && buffer[1] <= 'z') { + buffer[1] -= 0x20; + } + buffer[5] = ' '; + uint8_t base = 112 + (selector_ - 3) * 8; + uint8_t curve = patch[base + 6]; + if (curve < ENVELOPE_CURVE_LAST) { + memcpy_P(&buffer[6], env_curve_names + curve * 4, 4); + } else { + buffer[6] = ' '; + buffer[7] = ' '; + buffer[8] = ' '; + buffer[9] = '?'; + } + } else { + for (uint8_t j = 1; j < 10; ++j) buffer[j] = ' '; + } } } } diff --git a/controller/ui_pages/library.cc b/controller/ui_pages/library.cc index 34fc2fb..6047a64 100755 --- a/controller/ui_pages/library.cc +++ b/controller/ui_pages/library.cc @@ -165,6 +165,10 @@ uint8_t Library::OnKeyBrowse(uint8_t key) { location_.object = STORAGE_OBJECT_PATCH; } else { location_.object = static_cast(location_.object + 1); + // Skip PART (not independently browseable). + if (location_.object == STORAGE_OBJECT_PART) { + location_.object = STORAGE_OBJECT_PROGRAM; + } } location_.bank = loaded_objects_indices_[location_.index()] >> 8; location_.slot = loaded_objects_indices_[location_.index()] & 0xff; @@ -382,10 +386,6 @@ void Library::OnDialogClosed(uint8_t dialog_id, uint8_t return_value) { multi.mutable_part(location_.part)->InitPatch(mode); break; - case STORAGE_OBJECT_SEQUENCE: - multi.mutable_part(location_.part)->InitSequence(mode); - break; - case STORAGE_OBJECT_PROGRAM: multi.mutable_part(location_.part)->InitSettings(mode); break; diff --git a/controller/ui_pages/sequence_editor.cc b/controller/ui_pages/sequence_editor.cc deleted file mode 100755 index 121a9a4..0000000 --- a/controller/ui_pages/sequence_editor.cc +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright 2011 Emilie Gillet. -// -// Author: Emilie Gillet (emilie.o.gillet@gmail.com) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// 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. -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// -// ----------------------------------------------------------------------------- -// -// Sequence editor UI page. - -#include "controller/ui_pages/sequence_editor.h" - -#include "avrlib/string.h" - -#include "controller/display.h" -#include "controller/leds.h" -#include "controller/multi.h" -#include "controller/parameter.h" -#include "controller/part.h" - -namespace ambika { - -/* static */ -uint8_t SequenceEditor::step_ = 0; - - -/* static */ -const prog_EventHandlers SequenceEditor::event_handlers_ PROGMEM = { - OnInit, - SetActiveControl, - OnIncrement, - OnClick, - OnPot, - OnKey, - OnNote, - OnIdle, - UpdateScreen, - UpdateLeds, - OnDialogClosed, -}; - -/* static */ -inline PartData* SequenceEditor::mutable_part_data() { - return multi.mutable_part(ui.state().active_part)->mutable_data(); -} - -/* static */ -inline const PartData& SequenceEditor::part_data() { - return multi.part(ui.state().active_part).data(); -} - -/* static */ -void SequenceEditor::OnInit(PageInfo* info) { - UiPage::OnInit(info); - active_control_ = 4; - OnIncrement(0); -} - -/* static */ -void SequenceEditor::SetActiveControl(ActiveControl active_control) { - if (active_control == ACTIVE_CONTROL_FIRST) { - step_ = 0; - } else { - step_ = 127; - OnIncrement(-1); - } -} - -/* static */ -uint8_t SequenceEditor::OnNote(uint8_t note, uint8_t velocity) { - if (edit_mode_ != EDIT_IDLE) { - int8_t step = step_ - 1; - if (active_control_ >= 4) { - ++step; - } - if (step < 0) { - return 0; - } - step = actual_step(step, 2); - mutable_part_data()->set_note(step, note); - mutable_part_data()->set_velocity(step, velocity); - step_ = actual_step(step + 1, 2); - return 1; - } else { - return 0; - } -} - - -/* static */ -uint8_t SequenceEditor::OnIncrement(int8_t increment) { - if (edit_mode_ == EDIT_IDLE) { - int8_t step = step_; - step += increment; - if (step < 0) { - ui.ShowPageRelative(-1); - return 1; - } - uint8_t max_num_steps = 0x00; - for (uint8_t i = 0; i < 3; ++i) { - uint8_t l = part_data().sequence_length[i]; - if (l > max_num_steps) { - max_num_steps = l; - } - } - if (step >= max_num_steps) { - if (increment > 0) { - ui.ShowPageRelative(1); - return 1; - } else { - step = max_num_steps - 1; - } - } - step_ = step; - } else { - edit_mode_ = EDIT_STARTED_BY_ENCODER; - uint8_t index = active_control_; - int8_t step = step_ - 1; - if (index >= 4) { - ++step; - index -= 4; - } - if (step < 0) { - return 1; - } - switch (index) { - case 0: - { - step = actual_step(step, 2); - uint8_t note = part_data().note(step) + increment; - if (note >= 0 && note <= 127) { - mutable_part_data()->set_note(step, note); - } - } - break; - case 1: - { - step = actual_step(step, 2); - int16_t velocity = part_data().ordered_gate_velocity(step); - velocity += increment; - if (velocity >= 0 && velocity <= 0x100) { - mutable_part_data()->set_ordered_gate_velocity(step, velocity); - } - } - break; - case 2: - case 3: - { - step = actual_step(step, index - 2); - int16_t value = part_data().step_value(index - 2, step); - value += increment; - if (value >= 0 && value <= 255) { - mutable_part_data()->set_step_value(index - 2, step, value); - } - } - break; - } - } - return 1; -} - -/* static */ -uint8_t SequenceEditor::OnClick() { - if (edit_mode_ != EDIT_IDLE) { - edit_mode_ = EDIT_IDLE; - active_control_ = 4; - } else { - edit_mode_ = EDIT_STARTED_BY_ENCODER; - } - return 1; -} - -/* static */ -uint8_t SequenceEditor::OnPot(uint8_t index, uint8_t value) { - int8_t step = step_ - 1; - active_control_ = index; - if (index >= 4) { - ++step; - index -= 4; - } - if (step < 0) { - return 1; - } - switch (index) { - case 0: - step = actual_step(step, 2); - mutable_part_data()->set_note(step, 24 + (value >> 1)); - break; - case 1: - step = actual_step(step, 2); - mutable_part_data()->set_ordered_gate_velocity(step, value << 1); - break; - case 2: - case 3: - step = actual_step(step, index - 2); - mutable_part_data()->set_step_value(index - 2, step, value << 1); - break; - } - return 1; -} - -/* static */ -uint8_t SequenceEditor::actual_step(uint8_t index, uint8_t sequence) { - uint8_t l = part_data().sequence_length[sequence]; - if (l == 0) { - return 0; - } else { - while (index >= l) { - index -= l; - } - return index; - } -} - -/* static */ -void SequenceEditor::UpdateScreen() { - for (uint8_t i = 0; i < 2; ++i) { - char* buffer = display.line_buffer(i); - int8_t step = step_ - 1 + i; - memset(buffer, ' ', kLcdWidth); - if (step >= 0) { - // Quick and dirty %02d - buffer[1] = '0'; - uint8_t c = step + 1; - while (c >= 10) { - c -= 10; - ++buffer[1]; - } - buffer[2] = '0' + c; - uint8_t step_index = actual_step(step, 2); - buffer[3] = kDelimiter; - buffer[10] = kDelimiter; - if (step_index == 0) { - buffer[4] = buffer[11] = '\xa5'; - } - NoteStep n = part_data().note_step(step_index); - Parameter::PrintNote(n.note, &buffer[5]); - buffer[8] = ' '; - if (n.gate) { - buffer[15] = n.legato ? '\x04' : '\x01'; - UnsafeItoa(n.velocity, 3, &buffer[16]); - AlignRight(&buffer[16], 3); - } else { - buffer[15] = '\x05'; - } - uint8_t column = 20; - for (uint8_t j = 0; j < 2; ++j) { - step_index = actual_step(step, j); - uint8_t step_value = part_data().step_value(j, step_index); - if (step_index == 0) { - buffer[column + 1] = '\xa5'; - } - UnsafeItoa(step_value, 3, &buffer[column + 6]); - AlignRight(&buffer[column + 6], 3); - buffer[column] = kDelimiter; - column += 10; - } - } else { - memcpy_P( - &buffer[3], - PSTR("\x07 note \x07velocity \x07stepseq 1\x07stepseq 2"), - 37); - } - } - display.set_cursor_character(edit_mode_ == EDIT_IDLE ? 0xff : '_'); - if (edit_mode_ == EDIT_IDLE) { - display.set_cursor_position(kLcdNoCursor); - } else { - uint8_t index = active_control_; - uint8_t position = 0; - if (index >= 4) { - index -= 4; - position += kLcdWidth; - } - position += index * 10 + 4 + (index >> 1); - display.set_cursor_position(position); - } -} - -} // namespace ambika diff --git a/controller/ui_pages/sequence_editor.h b/controller/ui_pages/sequence_editor.h deleted file mode 100755 index de1c8ff..0000000 --- a/controller/ui_pages/sequence_editor.h +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2011 Emilie Gillet. -// -// Author: Emilie Gillet (emilie.o.gillet@gmail.com) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// 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. -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// -// ----------------------------------------------------------------------------- -// -// Sequence editor UI page. - -#ifndef CONTROLLER_UI_PAGES_SEQUENCE_EDITOR_H_ -#define CONTROLLER_UI_PAGES_SEQUENCE_EDITOR_H_ - -#include "controller/ui_pages/ui_page.h" - -namespace ambika { - -struct PartData; - -class SequenceEditor : public UiPage { - public: - SequenceEditor() { } - - static void OnInit(PageInfo* info); - static void SetActiveControl(ActiveControl active_control); - - static uint8_t OnIncrement(int8_t increment); - static uint8_t OnPot(uint8_t index, uint8_t value); - static uint8_t OnClick(); - static uint8_t OnNote(uint8_t note, uint8_t velocity); - - static void UpdateScreen(); - - static const prog_EventHandlers event_handlers_; - - private: - static uint8_t actual_step(uint8_t index, uint8_t sequence); - - static PartData* mutable_part_data(); - static const PartData& part_data(); - - static uint8_t step_; - - DISALLOW_COPY_AND_ASSIGN(SequenceEditor); -}; - -} // namespace ambika - -#endif // CONTROLLER_UI_PAGES_SEQUENCE_EDITOR_H_ diff --git a/voicecard/voice.cc b/voicecard/voice.cc index 09d0d1a..d8036b6 100755 --- a/voicecard/voice.cc +++ b/voicecard/voice.cc @@ -106,11 +106,11 @@ static const prog_Patch init_patch PROGMEM = { // Padding 0, 0, 0, 0, 0, 0, 0, 0, - // Extra envelopes (env4-7 ADSR) - 0, 40, 20, 60, - 0, 40, 20, 60, - 0, 40, 20, 60, - 0, 40, 20, 60, + // Extra envelope-LFO settings (env4-7: ADSR + LFO shape/rate/curve/retrigger) + 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, + 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, + 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, + 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, }; /* static */ @@ -380,10 +380,11 @@ inline void Voice::UpdateDestinations() { } for (int i = 0; i < kNumExtraEnvelopes; ++i) { envelope_[kNumEnvLfoSlots + i].Update( - patch_.extra_env[i].attack, - patch_.extra_env[i].decay, - patch_.extra_env[i].sustain, - patch_.extra_env[i].release); + patch_.extra_env_lfo[i].attack, + patch_.extra_env_lfo[i].decay, + patch_.extra_env_lfo[i].sustain, + patch_.extra_env_lfo[i].release, + patch_.extra_env_lfo[i].envelope_curve); } voice_lfo_.set_phase_increment( From e087d93552553308f4ddf137e0546a795aa3d728 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 12:59:30 +0200 Subject: [PATCH 29/67] Per-op FM envelopes, Lately Bass patch at C2 --- voicecard/voice.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/voicecard/voice.cc b/voicecard/voice.cc index d8036b6..327415d 100755 --- a/voicecard/voice.cc +++ b/voicecard/voice.cc @@ -447,6 +447,12 @@ inline void Voice::RenderOscillators() { op_level[2] = patch_.mix_fuzz; op_level[3] = patch_.mix_crush; + // Per-operator envelopes: env4-7 shape op1-4 levels (TX81Z style). + op_level[0] = U8U8MulShift8(op_level[0], modulation_sources_[MOD_SRC_ENV_4]); + op_level[1] = U8U8MulShift8(op_level[1], modulation_sources_[MOD_SRC_ENV_5]); + op_level[2] = U8U8MulShift8(op_level[2], modulation_sources_[MOD_SRC_ENV_6]); + op_level[3] = U8U8MulShift8(op_level[3], modulation_sources_[MOD_SRC_ENV_7]); + // Modulate total FM depth via PARAMETER_1 mod destination. uint8_t depth_mod = U15ShiftRight7(dst_[MOD_DST_PARAMETER_1]); for (uint8_t i = 0; i < 4; ++i) { From 6eb94bfc95b8e84a170f4dc40e3d5f68117a8300 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 13:38:34 +0200 Subject: [PATCH 30/67] TX81Z frequency ratio table, per-op envelopes, fixed Lately Bass --- controller/ui_pages/fm_page.cc | 25 +++++++++++++------------ voicecard/fm4op.h | 28 ++++++++++++++-------------- voicecard/voice.cc | 27 +++++++++++++++++---------- 3 files changed, 44 insertions(+), 36 deletions(-) diff --git a/controller/ui_pages/fm_page.cc b/controller/ui_pages/fm_page.cc index 0503511..213feee 100644 --- a/controller/ui_pages/fm_page.cc +++ b/controller/ui_pages/fm_page.cc @@ -328,18 +328,19 @@ void FmPage::UpdateScreen() { } case FM_PT_RATIO: { - // Display as ratio value. 0=0.5, 1=1.0, 2=2.0, etc. - int8_t r = static_cast(raw); - if (r <= 0) { - buffer[6] = '0'; - buffer[7] = '.'; - buffer[8] = '5'; - buffer[9] = ' '; - } else { - UnsafeItoa(r, 3, &buffer[6]); - AlignRight(&buffer[6], 3); - buffer[9] = ' '; - } + // TX81Z ratio display. Index 0-63 maps to ratio table. + // Show as X.XX with one decimal for compact display. + static const prog_char ratio_display[] PROGMEM = + ".50 " ".71 " ".78 " ".87 " "1.0 " "1.41" "1.57" "1.73" + "2.0 " "2.82" "3.0 " "3.14" "3.46" "4.0 " "4.24" "4.71" + "5.0 " "5.19" "5.65" "6.0 " "6.28" "6.92" "7.0 " "7.07" + "7.85" "8.0 " "8.48" "8.65" "9.0 " "9.42" "9.89" "10.0" + "10.4" "11.0" "11.0" "11.3" "12.0" "12.1" "12.6" "12.7" + "13.0" "13.8" "14.0" "14.1" "14.1" "15.0" "15.6" "15.6" + "15.7" "17.0" "17.3" "17.3" "18.4" "18.8" "19.0" "19.8" + "20.4" "20.8" "21.2" "22.0" "22.5" "23.6" "24.2" "26.0"; + uint8_t idx = raw & 0x3F; + memcpy_P(&buffer[6], ratio_display + idx * 4, 4); break; } diff --git a/voicecard/fm4op.h b/voicecard/fm4op.h index c56569d..daec4af 100644 --- a/voicecard/fm4op.h +++ b/voicecard/fm4op.h @@ -306,21 +306,21 @@ class Fm4Op { } } - // Set operator phase increment from a base increment and ratio. + // TX81Z frequency ratio table — 64 entries, 8.8 fixed-point. + // Coarse ratio byte (0-63) indexes into this table. + // Values: 0.50, 0.71, 0.78, 0.87, 1.00, 1.41, 1.57, 1.73, 2.00, ... + static const prog_uint16_t tx81z_ratios_[] PROGMEM; + + // Set operator phase increment using TX81Z-style ratio lookup. void SetOperatorIncrement(uint8_t op_index, uint16_t base_increment, - int8_t coarse_ratio, int8_t fine_detune) { - // Coarse ratio: map signed range to multiplier. - // 0 = 0.5x, 1 = 1x, 2 = 2x, etc. Negative values for sub-ratios. - uint16_t increment; - if (coarse_ratio <= 0) { - uint8_t shift = 1 - coarse_ratio; - if (shift > 15) shift = 15; - increment = base_increment >> shift; - } else if (coarse_ratio == 1) { - increment = base_increment; - } else { - increment = base_increment * static_cast(coarse_ratio); - } + uint8_t coarse_ratio, int8_t fine_detune) { + // Look up the ratio from the TX81Z table (8.8 fixed-point). + uint8_t idx = coarse_ratio & 0x3F; + uint16_t ratio_fp = ResourcesManager::Lookup( + tx81z_ratios_, idx); + // Multiply base increment by ratio: (base * ratio) >> 8. + uint32_t product = static_cast(base_increment) * ratio_fp; + uint16_t increment = product >> 8; // Fine detune: small pitch offset. if (fine_detune > 0) { increment += (increment >> 8) * fine_detune; diff --git a/voicecard/voice.cc b/voicecard/voice.cc index 327415d..f3fc679 100755 --- a/voicecard/voice.cc +++ b/voicecard/voice.cc @@ -61,6 +61,18 @@ uint8_t Voice::dummy_sync_state_[kAudioBlockSize]; Fm4Op Voice::fm4op_; KarplusStrong Voice::karplus_; WestCoast Voice::westcoast_; + +// TX81Z frequency ratios as 8.8 fixed-point (ratio × 256). +const prog_uint16_t Fm4Op::tx81z_ratios_[] PROGMEM = { + 128, 182, 200, 223, 256, 361, 402, 443, + 512, 722, 768, 804, 886, 1024, 1085, 1206, + 1280, 1329, 1446, 1536, 1608, 1772, 1792, 1810, + 2010, 2048, 2171, 2214, 2304, 2412, 2532, 2560, + 2657, 2813, 2816, 2893, 3072, 3100, 3215, 3256, + 3328, 3543, 3584, 3610, 3617, 3840, 3981, 3986, + 4019, 4342, 4421, 4429, 4703, 4823, 4872, 5064, + 5225, 5315, 5427, 5627, 5757, 6029, 6200, 6643, +}; /* */ static const prog_Patch init_patch PROGMEM = { @@ -412,20 +424,15 @@ inline void Voice::RenderOscillators() { uint16_t base_increment = ComputePhaseIncrement(base_pitch); // Set up operator phase increments from patch fields. - // Op1: osc[0].range (coarse), osc[0].detune (fine) + // Coarse ratio is a TX81Z-style index (0-63) into the ratio table. fm4op_.SetOperatorIncrement(0, base_increment, - patch_.osc[0].range, patch_.osc[0].detune); - // Op2: osc[1].range (coarse), osc[1].detune (fine) + static_cast(patch_.osc[0].range), patch_.osc[0].detune); fm4op_.SetOperatorIncrement(1, base_increment, - patch_.osc[1].range, patch_.osc[1].detune); - // Op3: mix_balance (coarse), mix_op (fine) + static_cast(patch_.osc[1].range), patch_.osc[1].detune); fm4op_.SetOperatorIncrement(2, base_increment, - static_cast(patch_.mix_balance), - static_cast(patch_.mix_op)); - // Op4: mix_parameter (coarse), mix_sub_osc_shape (fine) + patch_.mix_balance, static_cast(patch_.mix_op)); fm4op_.SetOperatorIncrement(3, base_increment, - static_cast(patch_.mix_parameter), - static_cast(patch_.mix_sub_osc_shape)); + patch_.mix_parameter, static_cast(patch_.mix_sub_osc_shape)); // Extract operator waveforms from packed nibbles. uint8_t op_waveform[4]; From 8f29d48b6817c38aea6690a1878e76ec50072fc0 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 13:43:02 +0200 Subject: [PATCH 31/67] Factory patch generator with 6 FM presets, integrated into flash_sd.sh --- flash_sd.sh | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/flash_sd.sh b/flash_sd.sh index 7334953..c9c9bbc 100755 --- a/flash_sd.sh +++ b/flash_sd.sh @@ -57,12 +57,20 @@ for i in 1 2 3 4 5 6; do done echo " VOICE1-6.BIN (voicecard: $(stat -c%s "$VOICECARD_BIN") bytes)" +# Generate and copy factory patches +echo "" +echo "Generating factory patches..." +PATCH_STAGING=$(mktemp -d) +python3 tools/make_patches.py "$PATCH_STAGING" +sudo mkdir -p "$SDCARD/PATCH/BANK/C" +sudo cp "$PATCH_STAGING/PATCH/BANK/C/"*.PAT "$SDCARD/PATCH/BANK/C/" 2>/dev/null || true +rm -rf "$PATCH_STAGING" + sudo sync echo "" echo "Done. Safe to eject SD card." echo "" echo "To flash on Ambika:" echo " 1. Insert SD card" -echo " 2. Go to OS info page (rightmost system page)" -echo " 3. Press S1 to flash controller" -echo " 4. After reboot, flash each voicecard with S4" +echo " 2. Hold S8 during power-on to flash controller" +echo " 3. Flash each voicecard with S4 from OS info page" From 856a41fa5e310ca8e12fb6cc6d52789b0f87b30a Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 13:49:17 +0200 Subject: [PATCH 32/67] Remove redundant FM depth multiplier, per-op envelopes handle levels --- voicecard/voice.cc | 6 ------ 1 file changed, 6 deletions(-) diff --git a/voicecard/voice.cc b/voicecard/voice.cc index f3fc679..ef060cc 100755 --- a/voicecard/voice.cc +++ b/voicecard/voice.cc @@ -460,12 +460,6 @@ inline void Voice::RenderOscillators() { op_level[2] = U8U8MulShift8(op_level[2], modulation_sources_[MOD_SRC_ENV_6]); op_level[3] = U8U8MulShift8(op_level[3], modulation_sources_[MOD_SRC_ENV_7]); - // Modulate total FM depth via PARAMETER_1 mod destination. - uint8_t depth_mod = U15ShiftRight7(dst_[MOD_DST_PARAMETER_1]); - for (uint8_t i = 0; i < 4; ++i) { - op_level[i] = U8U8MulShift8(op_level[i], depth_mod); - } - // Algorithm from osc[0].parameter. uint8_t algorithm = patch_.osc[0].parameter; if (algorithm >= FM_ALG_LAST) { From bc397a712928651386f493a869182df3ab9fa09e Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 13:59:22 +0200 Subject: [PATCH 33/67] Reset mixer fields when switching from special mode to classic --- controller/ui.cc | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/controller/ui.cc b/controller/ui.cc index d9946f4..8a0899a 100755 --- a/controller/ui.cc +++ b/controller/ui.cc @@ -357,7 +357,20 @@ void Ui::DoEvents() { idx += (static_cast(e.value) > 0) ? 1 : -1; if (idx < 0) idx = 3; if (idx > 3) idx = 0; - patch[0] = pgm_read_byte(&modes[idx]); + uint8_t new_mode = pgm_read_byte(&modes[idx]); + patch[0] = new_mode; + // When switching to classic, reset mixer fields that special + // modes repurpose (offsets 8-15). + if (new_mode <= WAVEFORM_FILTERED_NOISE) { + patch[8] = 32; // mix_balance (center) + patch[9] = 0; // mix_op (sum) + patch[10] = 0; // mix_parameter + patch[11] = 0; // mix_sub_osc_shape + patch[12] = 0; // mix_sub_osc level + patch[13] = 0; // mix_noise level + patch[14] = 0; // mix_fuzz + patch[15] = 0; // mix_crush + } multi.mutable_part(state_.active_part)->TouchPatch(); ShowPage(static_cast(pgm_read_byte(&pages[idx]))); } else { From 94790561820aedf3b1abe82f7f1c07ba2e6cbb17 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 14:30:25 +0200 Subject: [PATCH 34/67] ScaleMod function for FM modulation depth, divide by 4 --- voicecard/fm4op.h | 51 +++++++++++++++++++---------------------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/voicecard/fm4op.h b/voicecard/fm4op.h index daec4af..cf4e260 100644 --- a/voicecard/fm4op.h +++ b/voicecard/fm4op.h @@ -91,6 +91,11 @@ class Fm4Op { feedback_state_[1] = 0; } + // Scale modulation: operator output × level, attenuated for musical range. + static inline int16_t ScaleMod(uint8_t op_out, uint8_t level) { + return (static_cast(op_out - 128) * level) >> 2; + } + // Compute the output of a single operator waveform. static inline uint8_t RenderWaveform(uint8_t waveform, uint16_t phase) { switch (waveform) { @@ -179,16 +184,13 @@ class Fm4Op { switch (algorithm) { case FM_ALG_1: { // 4->3->2->1->out (full serial) - int16_t mod3 = static_cast(op4_out - 128) * - op_level[3]; + int16_t mod3 = ScaleMod(op4_out, op_level[3]); uint8_t op3_out = RenderWaveform(op_waveform[2], op_[2].phase + mod3); - int16_t mod2 = static_cast(op3_out - 128) * - op_level[2]; + int16_t mod2 = ScaleMod(op3_out, op_level[2]); uint8_t op2_out = RenderWaveform(op_waveform[1], op_[1].phase + mod2); - int16_t mod1 = static_cast(op2_out - 128) * - op_level[1]; + int16_t mod1 = ScaleMod(op2_out, op_level[1]); out = RenderWaveform(op_waveform[0], op_[0].phase + mod1); break; @@ -198,13 +200,11 @@ class Fm4Op { // (3+4)->2->1->out uint8_t op3_out = RenderWaveform(op_waveform[2], op_[2].phase); - int16_t mod2 = (static_cast(op4_out - 128) * - op_level[3]) + - (static_cast(op3_out - 128) * op_level[2]); + int16_t mod2 = ScaleMod(op4_out, op_level[3]) + + ScaleMod(op3_out, op_level[2]); uint8_t op2_out = RenderWaveform(op_waveform[1], op_[1].phase + mod2); - int16_t mod1 = static_cast(op2_out - 128) * - op_level[1]; + int16_t mod1 = ScaleMod(op2_out, op_level[1]); out = RenderWaveform(op_waveform[0], op_[0].phase + mod1); break; @@ -212,15 +212,13 @@ class Fm4Op { case FM_ALG_3: { // (4->3) + 2 -> 1->out - int16_t mod3 = static_cast(op4_out - 128) * - op_level[3]; + int16_t mod3 = ScaleMod(op4_out, op_level[3]); uint8_t op3_out = RenderWaveform(op_waveform[2], op_[2].phase + mod3); uint8_t op2_out = RenderWaveform(op_waveform[1], op_[1].phase); - int16_t mod1 = (static_cast(op3_out - 128) * - op_level[2]) + - (static_cast(op2_out - 128) * op_level[1]); + int16_t mod1 = ScaleMod(op3_out, op_level[2]) + + ScaleMod(op2_out, op_level[1]); out = RenderWaveform(op_waveform[0], op_[0].phase + mod1); break; @@ -228,14 +226,12 @@ class Fm4Op { case FM_ALG_4: { // (4->3) + (2->1) -> out (two parallel pairs) - int16_t mod3 = static_cast(op4_out - 128) * - op_level[3]; + int16_t mod3 = ScaleMod(op4_out, op_level[3]); uint8_t op3_out = RenderWaveform(op_waveform[2], op_[2].phase + mod3); uint8_t op2_out = RenderWaveform(op_waveform[1], op_[1].phase); - int16_t mod1 = static_cast(op2_out - 128) * - op_level[1]; + int16_t mod1 = ScaleMod(op2_out, op_level[1]); uint8_t op1_out = RenderWaveform(op_waveform[0], op_[0].phase + mod1); out = (op1_out >> 1) + (op3_out >> 1); @@ -244,16 +240,13 @@ class Fm4Op { case FM_ALG_5: { // (4->2) + (4->3->1) -> out - int16_t mod3 = static_cast(op4_out - 128) * - op_level[3]; + int16_t mod3 = ScaleMod(op4_out, op_level[3]); uint8_t op3_out = RenderWaveform(op_waveform[2], op_[2].phase + mod3); - int16_t mod1 = static_cast(op3_out - 128) * - op_level[2]; + int16_t mod1 = ScaleMod(op3_out, op_level[2]); uint8_t op1_out = RenderWaveform(op_waveform[0], op_[0].phase + mod1); - int16_t mod2 = static_cast(op4_out - 128) * - op_level[3]; + int16_t mod2 = ScaleMod(op4_out, op_level[3]); uint8_t op2_out = RenderWaveform(op_waveform[1], op_[1].phase + mod2); out = (op1_out >> 1) + (op2_out >> 1); @@ -262,8 +255,7 @@ class Fm4Op { case FM_ALG_6: { // 4->(1+2+3)->out (one mod, three carriers) - int16_t mod = static_cast(op4_out - 128) * - op_level[3]; + int16_t mod = ScaleMod(op4_out, op_level[3]); uint8_t op1_out = RenderWaveform(op_waveform[0], op_[0].phase + mod); uint8_t op2_out = RenderWaveform(op_waveform[1], @@ -276,8 +268,7 @@ class Fm4Op { case FM_ALG_7: { // (4->1)+2+3->out - int16_t mod1 = static_cast(op4_out - 128) * - op_level[3]; + int16_t mod1 = ScaleMod(op4_out, op_level[3]); uint8_t op1_out = RenderWaveform(op_waveform[0], op_[0].phase + mod1); uint8_t op2_out = RenderWaveform(op_waveform[1], From 3ad504da7b1c13d3470bc486c62f7d8b3b98a140 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 14:36:02 +0200 Subject: [PATCH 35/67] FM modulation depth >> 4 for cleaner sound --- voicecard/fm4op.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voicecard/fm4op.h b/voicecard/fm4op.h index cf4e260..2a2ff2d 100644 --- a/voicecard/fm4op.h +++ b/voicecard/fm4op.h @@ -93,7 +93,7 @@ class Fm4Op { // Scale modulation: operator output × level, attenuated for musical range. static inline int16_t ScaleMod(uint8_t op_out, uint8_t level) { - return (static_cast(op_out - 128) * level) >> 2; + return (static_cast(op_out - 128) * level) >> 4; } // Compute the output of a single operator waveform. From 7070cf3d77c7493eada49f53b36753312075517a Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 14:55:15 +0200 Subject: [PATCH 36/67] 16-bit sine table for FM/WC/VA sine, auto page switch on patch load --- controller/ui_pages/library.cc | 12 ++++++ voicecard/fm4op.h | 66 ++++++++++++++++++++------------ voicecard/oscillator.cc | 32 +++++++++------- voicecard/voice.cc | 70 ++++++++++++++++++++++++++++++++++ voicecard/westcoast.h | 9 +++-- 5 files changed, 147 insertions(+), 42 deletions(-) diff --git a/controller/ui_pages/library.cc b/controller/ui_pages/library.cc index 6047a64..3188fa5 100755 --- a/controller/ui_pages/library.cc +++ b/controller/ui_pages/library.cc @@ -128,6 +128,18 @@ uint8_t Library::OnIncrement(int8_t increment) { memcpy_P(name_, blank_patch_name, sizeof(name_)); } else { is_edit_buffer_ = 0; + // Auto-switch to the right page for the loaded patch's engine. + if (location_.object == STORAGE_OBJECT_PATCH) { + uint8_t shape = multi.part( + ui.state().active_part).raw_patch_data()[0]; + if (shape == WAVEFORM_FM4OP) { + ui.ShowPage(PAGE_FM4OP); + } else if (shape == WAVEFORM_KS_PLUCK) { + ui.ShowPage(PAGE_KS_PLUCK); + } else if (shape == WAVEFORM_WESTCOAST) { + ui.ShowPage(PAGE_WESTCOAST); + } + } } SaveLocation(); } diff --git a/voicecard/fm4op.h b/voicecard/fm4op.h index 2a2ff2d..93edb92 100644 --- a/voicecard/fm4op.h +++ b/voicecard/fm4op.h @@ -26,6 +26,22 @@ using namespace avrlib; namespace ambika { +// 16-bit sine table (512 entries + 1 wrap), defined in voice.cc. +extern const prog_uint16_t wav_res_sine16[] PROGMEM; + +// 16-bit sine interpolation for FM — 512 entries, returns 0-65535. +static inline uint16_t InterpolateSine16(uint16_t phase) { + // 9-bit index (512 entries), 7-bit fractional + uint16_t index = phase >> 7; + uint8_t frac = (phase << 1) & 0xFE; + uint16_t a = ResourcesManager::Lookup( + wav_res_sine16, index); + uint16_t b = ResourcesManager::Lookup( + wav_res_sine16, index + 1); + // Linear interpolation in 16-bit + return a + (static_cast(b - a) * frac >> 8); +} + // TX81Z waveform types derived from the sine table. enum FmWaveform { FM_WAVE_SINE, // W1: Full sine @@ -96,59 +112,61 @@ class Fm4Op { return (static_cast(op_out - 128) * level) >> 4; } - // Compute the output of a single operator waveform. + // Compute the output of a single operator waveform using 16-bit sine. static inline uint8_t RenderWaveform(uint8_t waveform, uint16_t phase) { + uint16_t s; switch (waveform) { case FM_WAVE_SINE: default: - return InterpolateSample(wav_res_sine, phase); + s = InterpolateSine16(phase); + break; case FM_WAVE_HALF_SINE: - if (phase < 0x8000) { - return InterpolateSample(wav_res_sine, phase << 1); - } - return 128; + s = (phase < 0x8000) ? InterpolateSine16(phase << 1) : 32768; + break; case FM_WAVE_ABS_SINE: - return InterpolateSample(wav_res_sine, (phase & 0x7FFF) << 1); + s = InterpolateSine16((phase & 0x7FFF) << 1); + break; case FM_WAVE_QUARTER_SINE: - if (phase < 0x4000) { - return InterpolateSample(wav_res_sine, phase << 2); - } - return 128; + s = (phase < 0x4000) ? InterpolateSine16(phase << 2) : 32768; + break; case FM_WAVE_HALF_ABS: - if (phase < 0x8000) { - return InterpolateSample(wav_res_sine, (phase & 0x3FFF) << 2); - } - return 128; + s = (phase < 0x8000) ? InterpolateSine16((phase & 0x3FFF) << 2) : 32768; + break; case FM_WAVE_TRIPLE_ABS: if (phase < 0x8000) { uint16_t tripled = static_cast(phase * 3); - return InterpolateSample(wav_res_sine, (tripled & 0x7FFF) << 1); + s = InterpolateSine16((tripled & 0x7FFF) << 1); + } else { + s = 32768; } - return 128; + break; case FM_WAVE_PULSE_SINE: { - uint8_t val = InterpolateSample(wav_res_sine, (phase & 0x3FFF) << 2); + s = InterpolateSine16((phase & 0x3FFF) << 2); if (phase >= 0x8000) { - return 256 - val; + s = 65536 - s; } - return val; + break; } case FM_WAVE_SAW_SINE: if (phase < 0x4000) { - return InterpolateSample(wav_res_sine, phase << 2); + s = InterpolateSine16(phase << 2); } else if (phase < 0x8000) { - return 255; + s = 65535; } else if (phase < 0xC000) { - return InterpolateSample(wav_res_sine, phase << 2); + s = InterpolateSine16(phase << 2); + } else { + s = 0; } - return 0; + break; } + return s >> 8; // Truncate to 8-bit at output only } // Render a block of FM4OP audio. diff --git a/voicecard/oscillator.cc b/voicecard/oscillator.cc index c7f043d..b8f5cfa 100755 --- a/voicecard/oscillator.cc +++ b/voicecard/oscillator.cc @@ -15,6 +15,7 @@ #include "voicecard/oscillator.h" +#include "voicecard/fm4op.h" #include "voicecard/voicecard.h" namespace ambika { @@ -111,23 +112,26 @@ void Oscillator::RenderBandlimitedPwm(uint8_t* buffer) { // The position is determined by the note pitch, to prevent aliasing. void Oscillator::RenderSimpleWavetable(uint8_t* buffer) { + // Pure sine uses the 16-bit table for higher quality. + if (shape_ == WAVEFORM_SINE) { + BEGIN_SAMPLE_LOOP + UPDATE_PHASE_MORE_REGISTERS + *buffer++ = InterpolateSine16(phase.integral) >> 8; + END_SAMPLE_LOOP + return; + } + uint8_t balance_index = U8Swap4(note_); uint8_t gain_2 = balance_index & 0xf0; uint8_t gain_1 = ~gain_2; - uint8_t wave_1_index, wave_2_index; - if (shape_ != WAVEFORM_SINE) { - uint8_t wave_index = balance_index & 0xf; - uint8_t base_resource_id = shape_ == WAVEFORM_SAW ? - WAV_RES_BANDLIMITED_SAW_0 : - (shape_ == WAVEFORM_SQUARE ? WAV_RES_BANDLIMITED_SQUARE_0 : - WAV_RES_BANDLIMITED_TRIANGLE_0); - wave_1_index = base_resource_id + wave_index; - wave_index = U8AddClip(wave_index, 1, kNumZonesFullSampleRate); - wave_2_index = base_resource_id + wave_index; - } else { - wave_1_index = WAV_RES_SINE; - wave_2_index = WAV_RES_SINE; - } + uint8_t wave_index = balance_index & 0xf; + uint8_t base_resource_id = shape_ == WAVEFORM_SAW ? + WAV_RES_BANDLIMITED_SAW_0 : + (shape_ == WAVEFORM_SQUARE ? WAV_RES_BANDLIMITED_SQUARE_0 : + WAV_RES_BANDLIMITED_TRIANGLE_0); + uint8_t wave_1_index = base_resource_id + wave_index; + wave_index = U8AddClip(wave_index, 1, kNumZonesFullSampleRate); + uint8_t wave_2_index = base_resource_id + wave_index; const prog_uint8_t* wave_1 = waveform_table[wave_1_index]; const prog_uint8_t* wave_2 = waveform_table[wave_2_index]; diff --git a/voicecard/voice.cc b/voicecard/voice.cc index ef060cc..acb96c5 100755 --- a/voicecard/voice.cc +++ b/voicecard/voice.cc @@ -73,6 +73,76 @@ const prog_uint16_t Fm4Op::tx81z_ratios_[] PROGMEM = { 4019, 4342, 4421, 4429, 4703, 4823, 4872, 5064, 5225, 5315, 5427, 5627, 5757, 6029, 6200, 6643, }; + +// 512-entry 16-bit sine table for FM synthesis. +// Phase-shifted to match original table (trough at index 0). +const prog_uint16_t wav_res_sine16[] PROGMEM = { + 1, 3, 10, 23, 40, 62, 89, 121, + 158, 200, 247, 299, 355, 417, 483, 554, + 630, 711, 797, 887, 982, 1083, 1187, 1297, + 1411, 1531, 1654, 1783, 1916, 2054, 2196, 2343, + 2495, 2651, 2812, 2977, 3146, 3321, 3499, 3682, + 3870, 4061, 4257, 4458, 4662, 4871, 5084, 5301, + 5523, 5748, 5978, 6211, 6449, 6690, 6936, 7185, + 7438, 7695, 7956, 8221, 8489, 8761, 9036, 9315, + 9598, 9884, 10173, 10466, 10763, 11062, 11365, 11671, + 11980, 12293, 12608, 12927, 13248, 13573, 13900, 14230, + 14563, 14899, 15237, 15578, 15922, 16268, 16617, 16968, + 17321, 17677, 18035, 18395, 18758, 19122, 19489, 19858, + 20228, 20601, 20975, 21351, 21729, 22108, 22489, 22872, + 23256, 23641, 24028, 24416, 24806, 25196, 25588, 25981, + 26375, 26770, 27166, 27562, 27960, 28358, 28756, 29156, + 29556, 29956, 30357, 30758, 31160, 31561, 31963, 32365, + 32768, 33170, 33572, 33974, 34375, 34777, 35178, 35579, + 35979, 36379, 36779, 37177, 37575, 37973, 38369, 38765, + 39160, 39554, 39947, 40339, 40729, 41119, 41507, 41894, + 42279, 42663, 43046, 43427, 43806, 44184, 44560, 44934, + 45307, 45677, 46046, 46413, 46777, 47140, 47500, 47858, + 48214, 48567, 48918, 49267, 49613, 49957, 50298, 50636, + 50972, 51305, 51635, 51962, 52287, 52608, 52927, 53242, + 53555, 53864, 54170, 54473, 54772, 55069, 55362, 55651, + 55937, 56220, 56499, 56774, 57046, 57314, 57579, 57840, + 58097, 58350, 58599, 58845, 59086, 59324, 59557, 59787, + 60012, 60234, 60451, 60664, 60873, 61077, 61278, 61474, + 61665, 61853, 62036, 62214, 62389, 62558, 62723, 62884, + 63040, 63192, 63339, 63481, 63619, 63752, 63881, 64004, + 64124, 64238, 64348, 64452, 64553, 64648, 64738, 64824, + 64905, 64981, 65052, 65118, 65180, 65236, 65288, 65335, + 65377, 65414, 65446, 65473, 65495, 65512, 65525, 65532, + 65535, 65532, 65525, 65512, 65495, 65473, 65446, 65414, + 65377, 65335, 65288, 65236, 65180, 65118, 65052, 64981, + 64905, 64824, 64738, 64648, 64553, 64452, 64348, 64238, + 64124, 64004, 63881, 63752, 63619, 63481, 63339, 63192, + 63040, 62884, 62723, 62558, 62389, 62214, 62036, 61853, + 61665, 61474, 61278, 61077, 60873, 60664, 60451, 60234, + 60012, 59787, 59557, 59324, 59086, 58845, 58599, 58350, + 58097, 57840, 57579, 57314, 57046, 56774, 56499, 56220, + 55937, 55651, 55362, 55069, 54772, 54473, 54170, 53864, + 53555, 53242, 52927, 52608, 52287, 51962, 51635, 51305, + 50972, 50636, 50298, 49957, 49613, 49267, 48918, 48567, + 48214, 47858, 47500, 47140, 46777, 46413, 46046, 45677, + 45307, 44934, 44560, 44184, 43806, 43427, 43046, 42663, + 42279, 41894, 41507, 41119, 40729, 40339, 39947, 39554, + 39160, 38765, 38369, 37973, 37575, 37177, 36779, 36379, + 35979, 35579, 35178, 34777, 34375, 33974, 33572, 33170, + 32768, 32365, 31963, 31561, 31160, 30758, 30357, 29956, + 29556, 29156, 28756, 28358, 27960, 27562, 27166, 26770, + 26375, 25981, 25588, 25196, 24806, 24416, 24028, 23641, + 23256, 22872, 22489, 22108, 21729, 21351, 20975, 20601, + 20228, 19858, 19489, 19122, 18758, 18395, 18035, 17677, + 17321, 16968, 16617, 16268, 15922, 15578, 15237, 14899, + 14563, 14230, 13900, 13573, 13248, 12927, 12608, 12293, + 11980, 11671, 11365, 11062, 10763, 10466, 10173, 9884, + 9598, 9315, 9036, 8761, 8489, 8221, 7956, 7695, + 7438, 7185, 6936, 6690, 6449, 6211, 5978, 5748, + 5523, 5301, 5084, 4871, 4662, 4458, 4257, 4061, + 3870, 3682, 3499, 3321, 3146, 2977, 2812, 2651, + 2495, 2343, 2196, 2054, 1916, 1783, 1654, 1531, + 1411, 1297, 1187, 1083, 982, 887, 797, 711, + 630, 554, 483, 417, 355, 299, 247, 200, + 158, 121, 89, 62, 40, 23, 10, 3, + 1, +}; /* */ static const prog_Patch init_patch PROGMEM = { diff --git a/voicecard/westcoast.h b/voicecard/westcoast.h index 3d672f2..2feff39 100644 --- a/voicecard/westcoast.h +++ b/voicecard/westcoast.h @@ -7,6 +7,7 @@ #include "avrlib/base.h" #include "avrlib/op.h" +#include "voicecard/fm4op.h" #include "voicecard/resources.h" using namespace avrlib; @@ -160,8 +161,8 @@ class WestCoast { mod_phase_ += mod_increment; uint16_t fm_mod = 0; if (fm_depth > 0) { - uint8_t mod_val = InterpolateSample(wav_res_sine, mod_phase_); - fm_mod = static_cast(mod_val - 128) * fm_depth; + uint16_t mod_val = InterpolateSine16(mod_phase_); + fm_mod = (static_cast(mod_val - 32768) * fm_depth) >> 8; } // Self-sync: reset phase when sync oscillator wraps. @@ -185,8 +186,8 @@ class WestCoast { sample = 127 - static_cast(tri_phase >> 7); } } else { - uint8_t sine = InterpolateSample(wav_res_sine, phase_ + fm_mod); - sample = static_cast(sine) - 128; + uint16_t sine = InterpolateSine16(phase_ + fm_mod); + sample = static_cast(sine >> 8) - 128; } // Apply input gain to the raw waveform before folding. From 2222b6be3fbc829f12143e44643ab419b9835d0e Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 15:07:24 +0200 Subject: [PATCH 37/67] Auto page switch on library exit, fix auto-switch timing --- controller/ui.cc | 16 ++++++++++++++++ controller/ui.h | 5 ++--- controller/ui_pages/library.cc | 12 ------------ 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/controller/ui.cc b/controller/ui.cc index 8a0899a..808be1a 100755 --- a/controller/ui.cc +++ b/controller/ui.cc @@ -463,6 +463,22 @@ void Ui::DoEvents() { leds.Sync(); } +/* static */ +void Ui::ShowPreviousPage() { + // After exiting a system page (e.g. library), route to the correct + // engine page based on the current patch's waveform. + uint8_t shape = multi.part(state_.active_part).raw_patch_data()[0]; + if (shape == WAVEFORM_FM4OP) { + ShowPage(PAGE_FM4OP); + } else if (shape == WAVEFORM_KS_PLUCK) { + ShowPage(PAGE_KS_PLUCK); + } else if (shape == WAVEFORM_WESTCOAST) { + ShowPage(PAGE_WESTCOAST); + } else { + ShowPage(most_recent_non_system_page_); + } +} + /* static */ void Ui::ShowPage(UiPageNumber page, uint8_t initialize) { // Flush the event queue. diff --git a/controller/ui.h b/controller/ui.h index 7b34cc2..404292b 100755 --- a/controller/ui.h +++ b/controller/ui.h @@ -179,9 +179,8 @@ class Ui { static void ShowPage(UiPageNumber page) { ShowPage(page, 1); } - static void ShowPreviousPage() { - ShowPage(most_recent_non_system_page_); - }; + static void ShowPreviousPage(); + static void ShowDialogBox(uint8_t dialog_id, Dialog dialog, uint8_t choice); static void CloseDialogBox(uint8_t return_value); diff --git a/controller/ui_pages/library.cc b/controller/ui_pages/library.cc index 3188fa5..6047a64 100755 --- a/controller/ui_pages/library.cc +++ b/controller/ui_pages/library.cc @@ -128,18 +128,6 @@ uint8_t Library::OnIncrement(int8_t increment) { memcpy_P(name_, blank_patch_name, sizeof(name_)); } else { is_edit_buffer_ = 0; - // Auto-switch to the right page for the loaded patch's engine. - if (location_.object == STORAGE_OBJECT_PATCH) { - uint8_t shape = multi.part( - ui.state().active_part).raw_patch_data()[0]; - if (shape == WAVEFORM_FM4OP) { - ui.ShowPage(PAGE_FM4OP); - } else if (shape == WAVEFORM_KS_PLUCK) { - ui.ShowPage(PAGE_KS_PLUCK); - } else if (shape == WAVEFORM_WESTCOAST) { - ui.ShowPage(PAGE_WESTCOAST); - } - } } SaveLocation(); } From 66d08c3addfa1e86b167281a12f2d9e076a0bd9b Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 15:16:35 +0200 Subject: [PATCH 38/67] AllSoundOff on patch load to reset voicecard state --- controller/storage.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/controller/storage.cc b/controller/storage.cc index a2458e8..5581c3d 100755 --- a/controller/storage.cc +++ b/controller/storage.cc @@ -310,6 +310,7 @@ void Storage::TouchObject(const StorageLocation& location) { // object is freshly loaded and has received no user changes. switch (location.object) { case STORAGE_OBJECT_PATCH: + multi.mutable_part(location.part)->AllSoundOff(); multi.mutable_part(location.part)->TouchPatch(); break; From b591e958507a9e47bdcf08338e8259ad266afe43 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 15:22:30 +0200 Subject: [PATCH 39/67] Voicecard version 2.0 --- voicecard/voicecard.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voicecard/voicecard.h b/voicecard/voicecard.h index 801853f..587fc37 100644 --- a/voicecard/voicecard.h +++ b/voicecard/voicecard.h @@ -32,7 +32,7 @@ static const uint8_t kControlRate = 40; // The latency is 1ms, with a buffer storing 4ms of audio. static const uint8_t kAudioBlockSize = kControlRate; -const uint8_t kSystemVersion = 0x10; +const uint8_t kSystemVersion = 0x20; static uint8_t* kFirmwareUpdateFlagPtr = (uint8_t*)(E2END); From b28f87d9981869846b963ca6a7692b91b20761d3 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 15:43:48 +0200 Subject: [PATCH 40/67] Default to 6-voice poly on part 1 --- controller/multi.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/controller/multi.cc b/controller/multi.cc index 9088df9..7f45f22 100755 --- a/controller/multi.cc +++ b/controller/multi.cc @@ -40,9 +40,9 @@ uint8_t Multi::flags_; /* */ static const prog_MultiData init_settings PROGMEM = { - // Parts mappings. - 1, 0, 127, 0x15, - 2, 0, 127, 0x2a, + // Parts mappings: part 1 gets all 6 voices. + 1, 0, 127, 0x3F, + 2, 0, 127, 0, 3, 0, 127, 0, 4, 0, 127, 0, 5, 0, 127, 0, From 6712b4547b699da5dd09423bff5849271defbb75 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 16:01:29 +0200 Subject: [PATCH 41/67] Explicit engine type byte at padding[2], clean mode switching --- common/patch.h | 8 ++++++ controller/ui.cc | 64 ++++++++++++++++++------------------------ voicecard/oscillator.h | 5 ++-- voicecard/voice.cc | 14 ++++----- 4 files changed, 44 insertions(+), 47 deletions(-) diff --git a/common/patch.h b/common/patch.h index c5f5e1b..ca39971 100755 --- a/common/patch.h +++ b/common/patch.h @@ -37,6 +37,14 @@ struct FilterSettings { uint8_t mode; }; +enum SynthEngine { + ENGINE_CLASSIC, + ENGINE_FM4OP, + ENGINE_KS_PLUCK, + ENGINE_WESTCOAST, + ENGINE_LAST +}; + enum EnvelopeCurve { ENVELOPE_CURVE_EXPONENTIAL, ENVELOPE_CURVE_LINEAR, diff --git a/controller/ui.cc b/controller/ui.cc index 808be1a..4751ff1 100755 --- a/controller/ui.cc +++ b/controller/ui.cc @@ -338,41 +338,35 @@ void Ui::DoEvents() { case CONTROL_ENCODER: if (e.control_id == 0) { if (s1_held_) { - // Mode select active: encoder cycles synthesis modes. - static const uint8_t modes[] PROGMEM = { - WAVEFORM_SAW, WAVEFORM_FM4OP, - WAVEFORM_KS_PLUCK, WAVEFORM_WESTCOAST - }; + // Mode select: cycle engine type via padding[2]. static const uint8_t pages[] PROGMEM = { PAGE_OSCILLATORS, PAGE_FM4OP, PAGE_KS_PLUCK, PAGE_WESTCOAST }; uint8_t* patch = multi.mutable_part( state_.active_part)->mutable_raw_patch_data(); - uint8_t current = patch[0]; - int8_t idx = 0; - for (uint8_t m = 0; m < 4; ++m) { - if (current == pgm_read_byte(&modes[m])) { idx = m; break; } - } + int8_t idx = patch[106]; // padding[2] = engine type idx += (static_cast(e.value) > 0) ? 1 : -1; - if (idx < 0) idx = 3; - if (idx > 3) idx = 0; - uint8_t new_mode = pgm_read_byte(&modes[idx]); - patch[0] = new_mode; - // When switching to classic, reset mixer fields that special - // modes repurpose (offsets 8-15). - if (new_mode <= WAVEFORM_FILTERED_NOISE) { - patch[8] = 32; // mix_balance (center) - patch[9] = 0; // mix_op (sum) - patch[10] = 0; // mix_parameter - patch[11] = 0; // mix_sub_osc_shape - patch[12] = 0; // mix_sub_osc level - patch[13] = 0; // mix_noise level - patch[14] = 0; // mix_fuzz - patch[15] = 0; // mix_crush + if (idx < 0) idx = ENGINE_LAST - 1; + if (idx >= ENGINE_LAST) idx = 0; + patch[106] = idx; + // When switching to classic, reset mixer and waveform. + if (idx == ENGINE_CLASSIC) { + if (patch[0] >= WAVEFORM_FM4OP) { + patch[0] = WAVEFORM_SAW; + } + patch[8] = 32; + patch[9] = 0; + patch[10] = 0; + patch[11] = 0; + patch[12] = 0; + patch[13] = 0; + patch[14] = 0; + patch[15] = 0; } multi.mutable_part(state_.active_part)->TouchPatch(); - ShowPage(static_cast(pgm_read_byte(&pages[idx]))); + ShowPage(static_cast( + pgm_read_byte(&pages[idx]))); } else { (*event_handlers_.OnIncrement)(e.value); } @@ -465,17 +459,13 @@ void Ui::DoEvents() { /* static */ void Ui::ShowPreviousPage() { - // After exiting a system page (e.g. library), route to the correct - // engine page based on the current patch's waveform. - uint8_t shape = multi.part(state_.active_part).raw_patch_data()[0]; - if (shape == WAVEFORM_FM4OP) { - ShowPage(PAGE_FM4OP); - } else if (shape == WAVEFORM_KS_PLUCK) { - ShowPage(PAGE_KS_PLUCK); - } else if (shape == WAVEFORM_WESTCOAST) { - ShowPage(PAGE_WESTCOAST); - } else { - ShowPage(most_recent_non_system_page_); + // Route to the correct engine page based on padding[2]. + uint8_t engine = multi.part(state_.active_part).raw_patch_data()[106]; + switch (engine) { + case ENGINE_FM4OP: ShowPage(PAGE_FM4OP); break; + case ENGINE_KS_PLUCK: ShowPage(PAGE_KS_PLUCK); break; + case ENGINE_WESTCOAST: ShowPage(PAGE_WESTCOAST); break; + default: ShowPage(most_recent_non_system_page_); break; } } diff --git a/voicecard/oscillator.h b/voicecard/oscillator.h index 05e3373..0034754 100755 --- a/voicecard/oscillator.h +++ b/voicecard/oscillator.h @@ -102,9 +102,8 @@ class Oscillator { } } else if (shape_ == WAVEFORM_WAVESHAPE) { RenderWaveshape(buffer); - } else if (shape_ == WAVEFORM_FM4OP || shape_ == WAVEFORM_KS_PLUCK || - shape_ == WAVEFORM_WESTCOAST) { - // Handled in voice.cc; render silence as fallback. + } else if (shape_ >= WAVEFORM_FM4OP) { + // Special modes handled in voice.cc; render silence as fallback. RenderSilence(buffer); } else if (shape_ < WAVEFORM_LAST) { RenderFn fn; diff --git a/voicecard/voice.cc b/voicecard/voice.cc index acb96c5..20c3fb3 100755 --- a/voicecard/voice.cc +++ b/voicecard/voice.cc @@ -255,7 +255,7 @@ void Voice::Trigger(uint16_t note, uint8_t velocity, uint8_t legato) { gate_ = 255; TriggerEnvelope(ATTACK); transient_generator.Trigger(); - if (patch_.osc[0].shape == WAVEFORM_KS_PLUCK) { + if (patch_.padding[2] == ENGINE_KS_PLUCK) { karplus_.Trigger( patch_.osc[1].shape, // excitation type patch_.osc[1].parameter, // excitation color @@ -490,7 +490,7 @@ inline void Voice::RenderOscillators() { base_pitch += (dst_[MOD_DST_OSC_1_2_FINE] - 8192) >> 7; // --- 4-op FM mode --- - if (patch_.osc[0].shape == WAVEFORM_FM4OP) { + if (patch_.padding[2] == ENGINE_FM4OP) { uint16_t base_increment = ComputePhaseIncrement(base_pitch); // Set up operator phase increments from patch fields. @@ -549,7 +549,7 @@ inline void Voice::RenderOscillators() { } // --- Karplus-Strong mode --- - if (patch_.osc[0].shape == WAVEFORM_KS_PLUCK) { + if (patch_.padding[2] == ENGINE_KS_PLUCK) { int16_t ks_pitch = base_pitch + S8U8Mul(patch_.osc[0].range, 128) + patch_.osc[0].detune; uint16_t ks_increment = ComputePhaseIncrement(ks_pitch); @@ -576,7 +576,7 @@ inline void Voice::RenderOscillators() { } // --- West Coast mode --- - if (patch_.osc[0].shape == WAVEFORM_WESTCOAST) { + if (patch_.padding[2] == ENGINE_WESTCOAST) { int16_t wc_pitch = base_pitch + S8U8Mul(patch_.osc[0].range, 128) + patch_.osc[0].detune; uint16_t wc_increment = ComputePhaseIncrement(wc_pitch); @@ -680,10 +680,10 @@ void Voice::ProcessBlock() { RenderOscillators(); - uint8_t is_fm4op = (patch_.osc[0].shape == WAVEFORM_FM4OP); + uint8_t is_fm4op = (patch_.padding[2] == ENGINE_FM4OP); uint8_t is_special = is_fm4op || - (patch_.osc[0].shape == WAVEFORM_KS_PLUCK) || - (patch_.osc[0].shape == WAVEFORM_WESTCOAST); + (patch_.padding[2] == ENGINE_KS_PLUCK) || + (patch_.padding[2] == ENGINE_WESTCOAST); // In FM4OP mode, skip oscillator mixing and sub-osc (those patch fields // are reinterpreted as FM parameters). From 571d88b72c8d87a8edbc86233e7e940fa0934590 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 16:16:43 +0200 Subject: [PATCH 42/67] Fix FM patch corruption: skip waveform sanitizer in special modes, reset engine state on mode change --- controller/part.cc | 11 +++++++---- voicecard/voice.cc | 16 +++++++++++++--- voicecard/voice.h | 1 + 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/controller/part.cc b/controller/part.cc index 7fbbca5..8f1593d 100755 --- a/controller/part.cc +++ b/controller/part.cc @@ -128,10 +128,13 @@ void Part::Touch() { } void Part::TouchPatch() { - // Sanitize waveform values — clamp out-of-range shapes from old patches. - for (uint8_t i = 0; i < kNumOscillators; ++i) { - if (patch_.osc[i].shape >= WAVEFORM_LAST) { - patch_.osc[i].shape = WAVEFORM_NONE; + // Sanitize waveform values — only in classic mode. + // Special modes repurpose osc[1].shape for packed FM waveform nibbles. + if (patch_.padding[2] == ENGINE_CLASSIC) { + for (uint8_t i = 0; i < kNumOscillators; ++i) { + if (patch_.osc[i].shape >= WAVEFORM_LAST) { + patch_.osc[i].shape = WAVEFORM_NONE; + } } } flags_ = FLAG_HAS_CHANGE; diff --git a/voicecard/voice.cc b/voicecard/voice.cc index 20c3fb3..a1a9185 100755 --- a/voicecard/voice.cc +++ b/voicecard/voice.cc @@ -61,6 +61,7 @@ uint8_t Voice::dummy_sync_state_[kAudioBlockSize]; Fm4Op Voice::fm4op_; KarplusStrong Voice::karplus_; WestCoast Voice::westcoast_; +uint8_t Voice::last_engine_ = 0xFF; // TX81Z frequency ratios as 8.8 fixed-point (ratio × 256). const prog_uint16_t Fm4Op::tx81z_ratios_[] PROGMEM = { @@ -489,8 +490,17 @@ inline void Voice::RenderOscillators() { base_pitch += (dst_[MOD_DST_OSC_1_2_COARSE] - 8192) >> 4; base_pitch += (dst_[MOD_DST_OSC_1_2_FINE] - 8192) >> 7; + // Reset engine state when engine type changes. + uint8_t engine = patch_.padding[2]; + if (engine != last_engine_) { + last_engine_ = engine; + fm4op_.Init(); + karplus_.Init(); + westcoast_.Init(); + } + // --- 4-op FM mode --- - if (patch_.padding[2] == ENGINE_FM4OP) { + if (engine == ENGINE_FM4OP) { uint16_t base_increment = ComputePhaseIncrement(base_pitch); // Set up operator phase increments from patch fields. @@ -549,7 +559,7 @@ inline void Voice::RenderOscillators() { } // --- Karplus-Strong mode --- - if (patch_.padding[2] == ENGINE_KS_PLUCK) { + if (engine == ENGINE_KS_PLUCK) { int16_t ks_pitch = base_pitch + S8U8Mul(patch_.osc[0].range, 128) + patch_.osc[0].detune; uint16_t ks_increment = ComputePhaseIncrement(ks_pitch); @@ -576,7 +586,7 @@ inline void Voice::RenderOscillators() { } // --- West Coast mode --- - if (patch_.padding[2] == ENGINE_WESTCOAST) { + if (engine == ENGINE_WESTCOAST) { int16_t wc_pitch = base_pitch + S8U8Mul(patch_.osc[0].range, 128) + patch_.osc[0].detune; uint16_t wc_increment = ComputePhaseIncrement(wc_pitch); diff --git a/voicecard/voice.h b/voicecard/voice.h index 5dba511..e310771 100755 --- a/voicecard/voice.h +++ b/voicecard/voice.h @@ -153,6 +153,7 @@ class Voice { static Fm4Op fm4op_; static KarplusStrong karplus_; static WestCoast westcoast_; + static uint8_t last_engine_; DISALLOW_COPY_AND_ASSIGN(Voice); }; From d3a09a9a3704208d1911890df73ad252afa5766e Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 16:37:36 +0200 Subject: [PATCH 43/67] Reset engine state on every patch bulk load, not just engine change --- voicecard/voice.h | 8 ++++++++ voicecard/voicecard_rx.h | 2 ++ 2 files changed, 10 insertions(+) diff --git a/voicecard/voice.h b/voicecard/voice.h index e310771..689c414 100755 --- a/voicecard/voice.h +++ b/voicecard/voice.h @@ -74,6 +74,14 @@ class Voice { // Move this voice to the release stage. static void Kill() { TriggerEnvelope(DEAD); } + // Reset all synthesis engine state (called after bulk patch load). + static void ResetEngines() { + fm4op_.Init(); + karplus_.Init(); + westcoast_.Init(); + last_engine_ = 0xFF; + } + static void ProcessBlock(); // Called whenever a write to the CV analog outputs has to be made. diff --git a/voicecard/voicecard_rx.h b/voicecard/voicecard_rx.h index 0389f19..cad968d 100755 --- a/voicecard/voicecard_rx.h +++ b/voicecard/voicecard_rx.h @@ -147,6 +147,8 @@ class VoicecardProtocolRx { while (size--) { *data++ = spi_.Read(); } + // Reset synthesis engine state after receiving a new patch. + voice.ResetEngines(); Timer<2>::Start(); } break; From 99d8a970f1e86b522f0561561610f84f1c5ea8a7 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 16:40:36 +0200 Subject: [PATCH 44/67] v2.0.1: fix KS pitch tracking, version bump --- controller/ui_pages/os_info_page.cc | 2 +- voicecard/karplus.h | 10 +++++++--- voicecard/voicecard.h | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/controller/ui_pages/os_info_page.cc b/controller/ui_pages/os_info_page.cc index c152879..16392e7 100755 --- a/controller/ui_pages/os_info_page.cc +++ b/controller/ui_pages/os_info_page.cc @@ -133,7 +133,7 @@ void OsInfoPage::UpdateScreen() { display.line_buffer(0)[0] = ' '; char* buffer = display.line_buffer(0) + 1; memcpy_P(&buffer[0], PSTR("carcosa"), 7); - memcpy_P(&buffer[9], PSTR("v2.0"), 4); + memcpy_P(&buffer[8], PSTR("v2.01"), 5); memcpy_P(&buffer[15], PSTR("port 1 device ?"), 15); Word version_number = voicecard_tx.GetVersion(active_control_); diff --git a/voicecard/karplus.h b/voicecard/karplus.h index fedd8c6..4f2808c 100644 --- a/voicecard/karplus.h +++ b/voicecard/karplus.h @@ -125,10 +125,14 @@ class KarplusStrong { } void SetPitch(uint16_t phase_increment) { - if (phase_increment < 256) { - phase_increment = 256; + // delay_length = sample_rate / frequency + // phase_increment represents frequency as a fraction of the sample rate. + // For accurate pitch: len = 65536 / phase_increment (full 16-bit division). + if (phase_increment < 1) { + phase_increment = 1; } - uint16_t len = 65535u / (phase_increment >> 8); + uint16_t len = static_cast( + static_cast(65536) / phase_increment); if (len < 2) len = 2; if (len >= kKarplusBufferSize) len = kKarplusBufferSize - 1; delay_length_ = len; diff --git a/voicecard/voicecard.h b/voicecard/voicecard.h index 587fc37..77200a6 100644 --- a/voicecard/voicecard.h +++ b/voicecard/voicecard.h @@ -32,7 +32,7 @@ static const uint8_t kControlRate = 40; // The latency is 1ms, with a buffer storing 4ms of audio. static const uint8_t kAudioBlockSize = kControlRate; -const uint8_t kSystemVersion = 0x20; +const uint8_t kSystemVersion = 0x21; static uint8_t* kFirmwareUpdateFlagPtr = (uint8_t*)(E2END); From 0439c49e90f4231269bf9cc6d7b488679a493b66 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 16:45:28 +0200 Subject: [PATCH 45/67] 16-bit KS delay line for smoother decay tail --- voicecard/karplus.h | 118 +++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 68 deletions(-) diff --git a/voicecard/karplus.h b/voicecard/karplus.h index 4f2808c..441807d 100644 --- a/voicecard/karplus.h +++ b/voicecard/karplus.h @@ -26,7 +26,7 @@ using namespace avrlib; namespace ambika { -static const uint16_t kKarplusBufferSize = 384; +static const uint16_t kKarplusBufferSize = 192; // Excitation types. enum KsExcitation { @@ -57,58 +57,49 @@ class KarplusStrong { delay_length_ = kKarplusBufferSize; excited_ = 0; for (uint16_t i = 0; i < kKarplusBufferSize; ++i) { - delay_line_[i] = 128; + delay_line_[i] = 0; } } // Excite the string. void Trigger(uint8_t excitation_type, uint8_t color, uint8_t position) { - // Fill delay line based on excitation type. + // Fill delay line with 16-bit excitation (centered at 0). for (uint16_t i = 0; i < delay_length_; ++i) { - uint8_t sample; + int16_t sample; switch (excitation_type) { case KS_EXC_CLICK: - sample = (i < 4) ? 255 : 128; + sample = (i < 4) ? 16384 : 0; break; - case KS_EXC_BRIGHT: { - uint8_t noise = Random::GetByte(); - // High-pass: subtract running average. - sample = (noise >> 1) + 64 + (Random::GetByte() >> 2); + case KS_EXC_BRIGHT: + sample = (static_cast(Random::GetByte()) - 128) << 7; break; - } - case KS_EXC_DARK: { - uint8_t noise = Random::GetByte(); - // Simple low-pass: average with previous. + case KS_EXC_DARK: if (i > 0) { - sample = (noise >> 1) + (delay_line_[i - 1] >> 1); + sample = ((static_cast(Random::GetByte()) - 128) << 6) + + (delay_line_[i - 1] >> 1); } else { - sample = noise; + sample = (static_cast(Random::GetByte()) - 128) << 7; } break; - } - default: // KS_EXC_NOISE - sample = Random::GetByte(); + default: + sample = (static_cast(Random::GetByte()) - 128) << 7; break; } delay_line_[i] = sample; } - // Apply pluck position: create a comb-like notch at position harmonics. - // position 0 = pluck at bridge (all harmonics), 127 = pluck at middle. + // Pluck position comb filter. if (position > 4) { uint16_t notch_period = (delay_length_ * position) >> 7; if (notch_period > 1 && notch_period < delay_length_) { for (uint16_t i = 0; i < delay_length_ - notch_period; ++i) { - int16_t mixed = (static_cast(delay_line_[i]) + - static_cast(delay_line_[i + notch_period])) >> 1; - delay_line_[i] = static_cast(mixed); + delay_line_[i] = (delay_line_[i] + delay_line_[i + notch_period]) >> 1; } } } - // Apply excitation color (brightness): low-pass the initial excitation. - // Lower color = darker excitation. - uint8_t filter_passes = (127 - color) >> 4; // 0-7 passes + // Excitation color low-pass. + uint8_t filter_passes = (127 - color) >> 4; for (uint8_t pass = 0; pass < filter_passes; ++pass) { for (uint16_t i = 1; i < delay_length_; ++i) { delay_line_[i] = (delay_line_[i] >> 1) + (delay_line_[i - 1] >> 1); @@ -158,96 +149,87 @@ class KarplusStrong { uint16_t lfo_inc = static_cast(ens_rate + 1) << 4; while (size--) { - // Advance LFO for ensemble read heads. ens_lfo_phase_ += lfo_inc; - // Primary read head (fixed position). uint16_t read_pos = write_pos_ + 1; if (read_pos >= delay_length_) read_pos = 0; - uint8_t current = delay_line_[read_pos]; + int16_t current = delay_line_[read_pos]; uint16_t next_pos = read_pos + 1; if (next_pos >= delay_length_) next_pos = 0; - uint8_t next = delay_line_[next_pos]; + int16_t next = delay_line_[next_pos]; - // KS low-pass averaging. - uint8_t filtered = ((uint16_t)current * (256 - coeff) + - (uint16_t)next * coeff) >> 8; + // KS low-pass averaging (16-bit). + int32_t filt = static_cast(current) * (256 - coeff) + + static_cast(next) * coeff; + int16_t filtered = filt >> 8; - // Stiffness: allpass-like dispersion. - // Mixes current with a sample from a prime-offset position, - // creating inharmonic partials (piano/bell character). + // Stiffness. if (stiffness > 0) { - uint16_t stiff_pos = read_pos + 7; // Prime offset. + uint16_t stiff_pos = read_pos + 7; if (stiff_pos >= delay_length_) stiff_pos -= delay_length_; - uint8_t stiff_sample = delay_line_[stiff_pos]; - filtered = ((uint16_t)filtered * (256 - stiffness) + - (uint16_t)stiff_sample * stiffness) >> 8; + int16_t stiff_sample = delay_line_[stiff_pos]; + filtered = (static_cast(filtered) * (256 - stiffness) + + static_cast(stiff_sample) * stiffness) >> 8; } - // Apply decay. + // Decay: pull toward zero. if (decay_amount) { - int16_t diff = static_cast(filtered) - 128; - diff -= (diff * decay_amount) >> 8; - filtered = 128 + diff; + filtered -= (filtered * decay_amount) >> 8; } // Body resonance. if (body > 4) { uint16_t body_pos = read_pos + (delay_length_ >> 1); if (body_pos >= delay_length_) body_pos -= delay_length_; - uint8_t body_sample = delay_line_[body_pos]; - filtered = ((uint16_t)filtered * (256 - body) + - (uint16_t)body_sample * body) >> 8; + int16_t body_sample = delay_line_[body_pos]; + filtered = (static_cast(filtered) * (256 - body) + + static_cast(body_sample) * body) >> 8; } - // Extra feedback for longer sustain. + // Extra feedback. if (feedback > 0) { - int16_t fb_val = static_cast(filtered) - 128; - fb_val += (fb_val * feedback) >> 8; - fb_val += 128; - if (fb_val > 255) fb_val = 255; - if (fb_val < 0) fb_val = 0; - filtered = fb_val; + int32_t fb = filtered + (static_cast(filtered) * feedback >> 8); + if (fb > 16383) fb = 16383; + if (fb < -16384) fb = -16384; + filtered = fb; } - // Write back to delay line. delay_line_[read_pos] = filtered; write_pos_ = read_pos; - // Ensemble: three read heads from same delay buffer. - uint8_t output = filtered; + // Ensemble: three read heads. + int16_t output = filtered; if (ens_mix > 0 && ens_depth > 0) { - // LFO triangle: map 16-bit phase to signed 8-bit triangle. uint8_t lfo_8 = ens_lfo_phase_ >> 8; int8_t lfo_val = (ens_lfo_phase_ & 0x8000) ? static_cast(255 - lfo_8) : static_cast(lfo_8); - // Head 2 and 3 offsets (signed, relative to read_pos). int16_t offset2 = (static_cast(lfo_val) * ens_depth) >> 8; int16_t offset3 = -offset2 + (ens_spread >> 2); - // Wrap positions into delay line range. int16_t p2 = (static_cast(read_pos) + offset2) % static_cast(delay_length_); if (p2 < 0) p2 += delay_length_; - int16_t p3 = (static_cast(read_pos) + offset3) % static_cast(delay_length_); if (p3 < 0) p3 += delay_length_; - uint8_t head2 = delay_line_[static_cast(p2)]; - uint8_t head3 = delay_line_[static_cast(p3)]; + int16_t head2 = delay_line_[static_cast(p2)]; + int16_t head3 = delay_line_[static_cast(p3)]; - // Mix: dry head + two wet heads. uint8_t dry = 255 - ens_mix; - uint16_t mixed = (uint16_t)filtered * dry + - ((uint16_t)head2 + (uint16_t)head3) * (ens_mix >> 1); + int32_t mixed = static_cast(filtered) * dry + + (static_cast(head2) + head3) * (ens_mix >> 1); output = mixed >> 8; } - *buffer++ = output; + // Truncate to 8-bit at output only (centered at 128). + int16_t out8 = (output >> 7) + 128; + if (out8 > 255) out8 = 255; + if (out8 < 0) out8 = 0; + *buffer++ = static_cast(out8); } } @@ -257,7 +239,7 @@ class KarplusStrong { } private: - uint8_t delay_line_[kKarplusBufferSize]; + int16_t delay_line_[kKarplusBufferSize]; // 16-bit for smooth decay uint16_t write_pos_; uint16_t delay_length_; uint8_t excited_; From ded744db872daaa87e92cf44e8751baf4e066023 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 16:53:24 +0200 Subject: [PATCH 46/67] Scale KS damping by pitch for consistent decay across keyboard --- voicecard/karplus.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/voicecard/karplus.h b/voicecard/karplus.h index 441807d..18aa46b 100644 --- a/voicecard/karplus.h +++ b/voicecard/karplus.h @@ -141,8 +141,15 @@ class KarplusStrong { return; } + // Scale damping by delay length — shorter strings (higher notes) get less + // damping so the decay tail stays consistent across the keyboard. uint8_t coeff = 32 + (damping << 1); if (coeff < 32) coeff = 32; + if (delay_length_ < 64) { + coeff = coeff >> 2; // Very high notes: 1/4 damping + } else if (delay_length_ < 128) { + coeff = coeff >> 1; // High notes: 1/2 damping + } uint8_t decay_amount = decay >> 3; // Advance ensemble LFO. From f85bdd23283c9d997afa7629c0118f5157790700 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 16:54:58 +0200 Subject: [PATCH 47/67] Double TouchPatch on patch load for reliable SPI transfer --- controller/storage.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/controller/storage.cc b/controller/storage.cc index 5581c3d..0758d0e 100755 --- a/controller/storage.cc +++ b/controller/storage.cc @@ -312,6 +312,8 @@ void Storage::TouchObject(const StorageLocation& location) { case STORAGE_OBJECT_PATCH: multi.mutable_part(location.part)->AllSoundOff(); multi.mutable_part(location.part)->TouchPatch(); + ConstantDelay(5); + multi.mutable_part(location.part)->TouchPatch(); break; case STORAGE_OBJECT_PART: From f06b3b286c6cccb448ab971ecfeed7202c65a5e5 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 16:58:25 +0200 Subject: [PATCH 48/67] Retune KS parameters: stronger decay/body/exciters for 16-bit --- voicecard/karplus.h | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/voicecard/karplus.h b/voicecard/karplus.h index 18aa46b..716930f 100644 --- a/voicecard/karplus.h +++ b/voicecard/karplus.h @@ -68,20 +68,25 @@ class KarplusStrong { int16_t sample; switch (excitation_type) { case KS_EXC_CLICK: - sample = (i < 4) ? 16384 : 0; + // Sharp impulse — very percussive attack. + sample = (i < 2) ? 16383 : ((i < 4) ? -16383 : 0); break; case KS_EXC_BRIGHT: + // Full-range noise with extra high frequency content. sample = (static_cast(Random::GetByte()) - 128) << 7; + sample += (static_cast(Random::GetByte()) - 128) << 5; break; case KS_EXC_DARK: + // Heavily filtered noise — warm, muted pluck. if (i > 0) { - sample = ((static_cast(Random::GetByte()) - 128) << 6) - + (delay_line_[i - 1] >> 1); + sample = (delay_line_[i - 1] * 3 + + ((static_cast(Random::GetByte()) - 128) << 6)) >> 2; } else { - sample = (static_cast(Random::GetByte()) - 128) << 7; + sample = (static_cast(Random::GetByte()) - 128) << 5; } break; default: + // Standard noise burst. sample = (static_cast(Random::GetByte()) - 128) << 7; break; } @@ -150,7 +155,7 @@ class KarplusStrong { } else if (delay_length_ < 128) { coeff = coeff >> 1; // High notes: 1/2 damping } - uint8_t decay_amount = decay >> 3; + uint8_t decay_amount = decay >> 1; // 0-63 for noticeable decay control // Advance ensemble LFO. uint16_t lfo_inc = static_cast(ens_rate + 1) << 4; @@ -185,13 +190,15 @@ class KarplusStrong { filtered -= (filtered * decay_amount) >> 8; } - // Body resonance. + // Body resonance: mix with signal from 1/3 through the delay line. + // Creates a comb-filter effect that adds harmonic richness. if (body > 4) { - uint16_t body_pos = read_pos + (delay_length_ >> 1); + uint16_t body_pos = read_pos + (delay_length_ / 3); if (body_pos >= delay_length_) body_pos -= delay_length_; int16_t body_sample = delay_line_[body_pos]; - filtered = (static_cast(filtered) * (256 - body) + - static_cast(body_sample) * body) >> 8; + uint8_t body_amt = body << 1; // Double the range for stronger effect. + filtered = (static_cast(filtered) * (256 - body_amt) + + static_cast(body_sample) * body_amt) >> 8; } // Extra feedback. From a5f2f26d0d8ecb065180f7d719b9430d7dbdd3eb Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 18:21:08 +0200 Subject: [PATCH 49/67] Fix diagnostic LED to use rx_led, bump voicecard to v2.2 --- voicecard/voice.cc | 19 ++++++++++++++++++- voicecard/voicecard.h | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/voicecard/voice.cc b/voicecard/voice.cc index a1a9185..29806f7 100755 --- a/voicecard/voice.cc +++ b/voicecard/voice.cc @@ -20,6 +20,7 @@ #include "voicecard/voice.h" #include "voicecard/audio_out.h" +#include "voicecard/leds.h" #include "voicecard/oscillator.h" #include "voicecard/sub_oscillator.h" #include "voicecard/transient_generator.h" @@ -490,8 +491,24 @@ inline void Voice::RenderOscillators() { base_pitch += (dst_[MOD_DST_OSC_1_2_COARSE] - 8192) >> 4; base_pitch += (dst_[MOD_DST_OSC_1_2_FINE] - 8192) >> 7; - // Reset engine state when engine type changes. + // Diagnostic: rx_led shows engine type (note_led is used by note handler). + // classic=off, FM=on, KS=slow blink, WC=fast blink uint8_t engine = patch_.padding[2]; + { + static uint8_t diag_counter = 0; + ++diag_counter; + if (engine == ENGINE_FM4OP) { + rx_led.High(); + } else if (engine == ENGINE_KS_PLUCK) { + if (diag_counter & 0x40) rx_led.High(); else rx_led.Low(); + } else if (engine == ENGINE_WESTCOAST) { + if (diag_counter & 0x10) rx_led.High(); else rx_led.Low(); + } else { + rx_led.Low(); + } + } + + // Reset engine state when engine type changes. if (engine != last_engine_) { last_engine_ = engine; fm4op_.Init(); diff --git a/voicecard/voicecard.h b/voicecard/voicecard.h index 77200a6..5f2c9a8 100644 --- a/voicecard/voicecard.h +++ b/voicecard/voicecard.h @@ -32,7 +32,7 @@ static const uint8_t kControlRate = 40; // The latency is 1ms, with a buffer storing 4ms of audio. static const uint8_t kAudioBlockSize = kControlRate; -const uint8_t kSystemVersion = 0x21; +const uint8_t kSystemVersion = 0x22; static uint8_t* kFirmwareUpdateFlagPtr = (uint8_t*)(E2END); From 416de02cf98d7efdab7a88356421ab624b8062ff Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 18:23:12 +0200 Subject: [PATCH 50/67] Engine diagnostic on library page, controller v2.02, voicecard v2.3 --- controller/ui_pages/library.cc | 9 +++++++++ controller/ui_pages/os_info_page.cc | 2 +- voicecard/voice.cc | 15 --------------- voicecard/voicecard.h | 2 +- 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/controller/ui_pages/library.cc b/controller/ui_pages/library.cc index 6047a64..03c0ed2 100755 --- a/controller/ui_pages/library.cc +++ b/controller/ui_pages/library.cc @@ -311,6 +311,15 @@ void Library::UpdateScreen() { memcpy(&buffer[20], name_, sizeof(name_) - 1); AlignLeft(&buffer[20], sizeof(name_) - 1); + // Show engine type after patch name: VA/FM/KS/WC + if (location_.object == STORAGE_OBJECT_PATCH) { + uint8_t eng = multi.part(ui.state().active_part).raw_patch_data()[106]; + static const prog_char eng_names[] PROGMEM = "VA" "FM" "KS" "WC"; + if (eng < ENGINE_LAST) { + memcpy_P(&buffer[37], eng_names + eng * 2, 2); + } + } + // Second line: buttons if (action_ == LIBRARY_ACTION_BROWSE) { buffer = display.line_buffer(1) + 1; diff --git a/controller/ui_pages/os_info_page.cc b/controller/ui_pages/os_info_page.cc index 16392e7..b23060a 100755 --- a/controller/ui_pages/os_info_page.cc +++ b/controller/ui_pages/os_info_page.cc @@ -133,7 +133,7 @@ void OsInfoPage::UpdateScreen() { display.line_buffer(0)[0] = ' '; char* buffer = display.line_buffer(0) + 1; memcpy_P(&buffer[0], PSTR("carcosa"), 7); - memcpy_P(&buffer[8], PSTR("v2.01"), 5); + memcpy_P(&buffer[8], PSTR("v2.02"), 5); memcpy_P(&buffer[15], PSTR("port 1 device ?"), 15); Word version_number = voicecard_tx.GetVersion(active_control_); diff --git a/voicecard/voice.cc b/voicecard/voice.cc index 29806f7..6b6b604 100755 --- a/voicecard/voice.cc +++ b/voicecard/voice.cc @@ -491,22 +491,7 @@ inline void Voice::RenderOscillators() { base_pitch += (dst_[MOD_DST_OSC_1_2_COARSE] - 8192) >> 4; base_pitch += (dst_[MOD_DST_OSC_1_2_FINE] - 8192) >> 7; - // Diagnostic: rx_led shows engine type (note_led is used by note handler). - // classic=off, FM=on, KS=slow blink, WC=fast blink uint8_t engine = patch_.padding[2]; - { - static uint8_t diag_counter = 0; - ++diag_counter; - if (engine == ENGINE_FM4OP) { - rx_led.High(); - } else if (engine == ENGINE_KS_PLUCK) { - if (diag_counter & 0x40) rx_led.High(); else rx_led.Low(); - } else if (engine == ENGINE_WESTCOAST) { - if (diag_counter & 0x10) rx_led.High(); else rx_led.Low(); - } else { - rx_led.Low(); - } - } // Reset engine state when engine type changes. if (engine != last_engine_) { diff --git a/voicecard/voicecard.h b/voicecard/voicecard.h index 5f2c9a8..5c0c052 100644 --- a/voicecard/voicecard.h +++ b/voicecard/voicecard.h @@ -32,7 +32,7 @@ static const uint8_t kControlRate = 40; // The latency is 1ms, with a buffer storing 4ms of audio. static const uint8_t kAudioBlockSize = kControlRate; -const uint8_t kSystemVersion = 0x22; +const uint8_t kSystemVersion = 0x23; static uint8_t* kFirmwareUpdateFlagPtr = (uint8_t*)(E2END); From 458de38cf18b566ab03502fd3aae484730715a12 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 18:45:26 +0200 Subject: [PATCH 51/67] Fix: set osc0.shape to SAW in generated FM patches --- make_patches.py | 305 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 make_patches.py diff --git a/make_patches.py b/make_patches.py new file mode 100644 index 0000000..88e01f3 --- /dev/null +++ b/make_patches.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python3 +"""Generate Carcosa factory patches for the Ambika polysynth.""" + +import struct +import os +import sys + +# Carcosa v2.0 Patch layout (144 bytes) +PATCH_SIZE = 144 + +# Waveform enums +WAVEFORM_NONE = 0 +WAVEFORM_SAW = 1 +WAVEFORM_SQUARE = 2 +WAVEFORM_TRIANGLE = 3 +WAVEFORM_SINE = 4 +WAVEFORM_QUAD_SAW_PAD = 5 +WAVEFORM_FILTERED_NOISE = 6 +WAVEFORM_FM4OP = 7 +WAVEFORM_KS_PLUCK = 8 +WAVEFORM_WAVESHAPE = 9 +WAVEFORM_WESTCOAST = 10 + +# FM waveforms (TX81Z W1-W8) +FM_WAVE_SINE = 0 +FM_WAVE_HALF_SINE = 1 +FM_WAVE_ABS_SINE = 2 +FM_WAVE_QUARTER_SINE = 3 +FM_WAVE_HALF_ABS = 4 +FM_WAVE_TRIPLE_ABS = 5 +FM_WAVE_PULSE_SINE = 6 +FM_WAVE_SAW_SINE = 7 + +# TX81Z coarse frequency ratio indices (index into ratio table) +R_050 = 0 # 0.50x +R_071 = 1 # 0.71x (√2/2) +R_078 = 2 # 0.78x +R_087 = 3 # 0.87x +R_100 = 4 # 1.00x +R_141 = 5 # 1.41x (√2) +R_157 = 6 # 1.57x (π/2) +R_173 = 7 # 1.73x (√3) +R_200 = 8 # 2.00x +R_282 = 9 # 2.82x +R_300 = 10 # 3.00x +R_314 = 11 # 3.14x (π) +R_400 = 13 # 4.00x +R_500 = 16 # 5.00x +R_600 = 19 # 6.00x +R_700 = 22 # 7.00x +R_800 = 25 # 8.00x + +# Mod sources (Carcosa v2.0) +MOD_SRC_ENV_1 = 0 +MOD_SRC_ENV_2 = 1 +MOD_SRC_ENV_3 = 2 +MOD_SRC_VELOCITY = 16 +MOD_SRC_PITCH_BEND = 18 +MOD_SRC_WHEEL = 19 + +# Mod destinations +MOD_DST_PARAMETER_1 = 0 +MOD_DST_OSC_1_2_COARSE = 4 +MOD_DST_FILTER_CUTOFF = 12 +MOD_DST_VCA = 18 + +# Envelope curve +ENV_EXP = 0 +ENV_LIN = 1 +LFO_TRI = 0 + + +def make_patch(): + """Return a zeroed 144-byte patch.""" + return bytearray(PATCH_SIZE) + + +ENGINE_CLASSIC = 0 +ENGINE_FM4OP = 1 +ENGINE_KS_PLUCK = 2 +ENGINE_WESTCOAST = 3 + +def set_engine(patch, engine): + """Set the synthesis engine type at padding[2] (offset 106).""" + patch[106] = engine + + +def set_fm(patch, algo, feedback=0, + wav1=0, wav2=0, wav3=0, wav4=0, + rat1=R_100, rat2=R_100, rat3=R_100, rat4=R_100, + fin1=0, fin2=0, fin3=0, fin4=0, + lvl1=127, lvl2=127, lvl3=127, lvl4=127): + """Set FM4OP parameters on a patch.""" + patch[0] = WAVEFORM_SAW # Safe fallback waveform; engine byte controls rendering + patch[1] = algo + patch[2] = rat1 + patch[3] = fin1 + patch[4] = wav1 | (wav2 << 4) + patch[5] = wav3 | (wav4 << 4) + patch[6] = rat2 + patch[7] = fin2 + patch[8] = rat3 # mix_balance + patch[9] = fin3 # mix_op + patch[10] = rat4 # mix_parameter + patch[11] = fin4 # mix_sub_osc_shape + patch[12] = lvl1 # mix_sub_osc + patch[13] = lvl2 # mix_noise + patch[14] = lvl3 # mix_fuzz + patch[15] = lvl4 # mix_crush + patch[104] = feedback # padding[0] + + +def set_filter(patch, cutoff=127, resonance=0, mode=0, env_amt=0, lfo_amt=0): + """Set filter parameters.""" + patch[16] = cutoff + patch[17] = resonance + patch[18] = mode + patch[22] = env_amt + patch[23] = lfo_amt + + +def set_env(patch, env_idx, attack=0, decay=40, sustain=20, release=60, + lfo_shape=LFO_TRI, lfo_rate=0, curve=ENV_EXP, retrigger=0): + """Set envelope/LFO parameters. env_idx 0-2 = env_lfo, 3-6 = extra.""" + if env_idx < 3: + base = 24 + env_idx * 8 + else: + base = 112 + (env_idx - 3) * 8 + patch[base] = attack + patch[base + 1] = decay + patch[base + 2] = sustain + patch[base + 3] = release + patch[base + 4] = lfo_shape + patch[base + 5] = lfo_rate + patch[base + 6] = curve + patch[base + 7] = retrigger + + +def set_mod(patch, slot, source, destination, amount): + """Set a modulation matrix slot (0-13).""" + base = 50 + slot * 3 + patch[base] = source + patch[base + 1] = destination + patch[base + 2] = amount + + +def make_riff_patch(patch_data, name): + """Wrap patch data in Ambika RIFF format.""" + name_bytes = name.encode('ascii')[:16].ljust(16, b'\x00') + name_chunk = struct.pack('<4sI', b'name', 16) + name_bytes + position = struct.pack(' 1 else '.' + + # FM patches — Bank C + save_patch(outdir, 'C', 1, 'FM LatelyBass ', fm_lately_bass()) + save_patch(outdir, 'C', 2, 'FM E.Piano ', fm_epiano()) + save_patch(outdir, 'C', 3, 'FM Brass ', fm_brass()) + save_patch(outdir, 'C', 4, 'FM Marimba ', fm_marimba()) + save_patch(outdir, 'C', 5, 'FM Strings ', fm_strings()) + save_patch(outdir, 'C', 6, 'FM Organ ', fm_organ()) + + +if __name__ == '__main__': + main() From d9a3a8892cd86fcf49727a541e5058b524fd8519 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 18:56:12 +0200 Subject: [PATCH 52/67] Fix VA sine: direct pgm_read_word for 16-bit table, bump v2.3 --- voicecard/oscillator.cc | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/voicecard/oscillator.cc b/voicecard/oscillator.cc index b8f5cfa..fd27d3e 100755 --- a/voicecard/oscillator.cc +++ b/voicecard/oscillator.cc @@ -112,11 +112,15 @@ void Oscillator::RenderBandlimitedPwm(uint8_t* buffer) { // The position is determined by the note pitch, to prevent aliasing. void Oscillator::RenderSimpleWavetable(uint8_t* buffer) { - // Pure sine uses the 16-bit table for higher quality. + // Pure sine: use 16-bit table for clean output. if (shape_ == WAVEFORM_SINE) { BEGIN_SAMPLE_LOOP UPDATE_PHASE_MORE_REGISTERS - *buffer++ = InterpolateSine16(phase.integral) >> 8; + uint16_t index = phase.integral >> 7; + uint8_t frac = (phase.integral & 0x7F) << 1; + uint16_t a = pgm_read_word(&wav_res_sine16[index]); + uint16_t b = pgm_read_word(&wav_res_sine16[index + 1]); + *buffer++ = (a + ((static_cast(b - a) * frac) >> 8)) >> 8; END_SAMPLE_LOOP return; } @@ -147,7 +151,6 @@ void Oscillator::RenderSimpleWavetable(uint8_t* buffer) { *buffer++ = sample; END_SAMPLE_LOOP } else { - // The waveshaper for the triangle is different. BEGIN_SAMPLE_LOOP UPDATE_PHASE_MORE_REGISTERS uint8_t sample = InterpolateTwoTables( From 055ed423953da114035f576506ce92dbda1644e1 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 21:12:55 +0200 Subject: [PATCH 53/67] FM engine: TX81Z-accurate modulation, 16-bit feedback, carrier scaling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major FM4Op improvements based on analysis of TX81Z reference samples and ymfm/SC/ROM disassembly sources: - Exponential output level curve (0.75 dB/step, 128-byte PROGMEM table) Applied in voice.cc before envelope multiply, matching TX81Z signal chain: raw level → exp lookup → × envelope → amplitude - ScaleMod with no shift (was >> 4) for full 8-bit modulation depth, giving mod index up to ~3.1 radians at max level - ScaleCarrier on all 8 algorithms — per-op envelopes now actually control carrier output volume (was completely ignored before) - 16-bit feedback state (was int8_t) prevents the feedback loop from locking into a fixed repeating pattern, creating evolving oscillation closer to the TX81Z's 14-bit feedback dynamics - 3-carrier mix (ALG 6,7) changed from /3 division to >> 2 + DC correction, avoiding slow AVR integer division Flash: 32446 text + 82 data = 32528 (240 bytes free) RAM: +2 bytes for 16-bit feedback state Co-Authored-By: Claude Opus 4.6 (1M context) --- voicecard/fm4op.h | 123 ++++++++++++++++++++++++++------------------- voicecard/voice.cc | 27 +++++++--- 2 files changed, 90 insertions(+), 60 deletions(-) diff --git a/voicecard/fm4op.h b/voicecard/fm4op.h index 93edb92..67ebfb5 100644 --- a/voicecard/fm4op.h +++ b/voicecard/fm4op.h @@ -107,9 +107,22 @@ class Fm4Op { feedback_state_[1] = 0; } - // Scale modulation: operator output × level, attenuated for musical range. - static inline int16_t ScaleMod(uint8_t op_out, uint8_t level) { - return (static_cast(op_out - 128) * level) >> 4; + // TX81Z-style exponential output level curve. + // 0.75 dB/step: level 0 = silence, level 127 = full (255). + // Most of the useful modulation range lives in levels 90-127. + static const prog_uint8_t level_to_amplitude_[128] PROGMEM; + + // Scale modulator output by its amplitude. + // Caller pre-applies exponential curve + envelope before passing amp. + // No shift — max mod index ~3.1 radians (8-bit ceiling). + static inline int16_t ScaleMod(uint8_t op_out, uint8_t amp) { + return static_cast(op_out - 128) * amp; + } + + // Scale carrier output by its amplitude (maintains center at 128). + // Caller pre-applies exponential curve + envelope before passing amp. + static inline uint8_t ScaleCarrier(uint8_t op_out, uint8_t amp) { + return 128 + ((static_cast(op_out - 128) * amp) >> 8); } // Compute the output of a single operator waveform using 16-bit sine. @@ -186,22 +199,25 @@ class Fm4Op { op_[i].phase += op_[i].phase_increment; } - // Apply feedback to op4 (same as TX81Z convention). - // Feedback state stored centered at 0 (signed). - int16_t fb = (feedback_state_[0] + feedback_state_[1]) >> 1; - int16_t fb_mod = (fb * static_cast(feedback)) >> 4; + // 16-bit feedback state for evolving, chaotic oscillation. + // Higher precision prevents the feedback loop from locking into + // a fixed repeating pattern (the TX81Z uses 14-bit internally). + int32_t fb = feedback_state_[0] + feedback_state_[1]; + int32_t fb_mod = (fb * feedback) >> 6; - uint8_t op4_out = RenderWaveform(op_waveform[3], - op_[3].phase + static_cast(fb_mod)); - // Store centered at 0 for proper signed feedback. + // Render op4 waveform, store 16-bit precision for feedback path. + uint16_t op4_phase = op_[3].phase + static_cast(fb_mod); + uint16_t op4_raw = InterpolateSine16(op4_phase); + uint8_t op4_out = RenderWaveform(op_waveform[3], op4_phase); feedback_state_[1] = feedback_state_[0]; - feedback_state_[0] = static_cast(op4_out - 128); + feedback_state_[0] = static_cast(op4_raw - 32768); // Render remaining operators and route per algorithm. + // Carrier outputs are scaled by their level (per-op envelope control). uint8_t out; switch (algorithm) { case FM_ALG_1: { - // 4->3->2->1->out (full serial) + // 4->3->2->1->out (full serial, op1 = carrier) int16_t mod3 = ScaleMod(op4_out, op_level[3]); uint8_t op3_out = RenderWaveform(op_waveform[2], op_[2].phase + mod3); @@ -209,13 +225,13 @@ class Fm4Op { uint8_t op2_out = RenderWaveform(op_waveform[1], op_[1].phase + mod2); int16_t mod1 = ScaleMod(op2_out, op_level[1]); - out = RenderWaveform(op_waveform[0], - op_[0].phase + mod1); + out = ScaleCarrier(RenderWaveform(op_waveform[0], + op_[0].phase + mod1), op_level[0]); break; } case FM_ALG_2: { - // (3+4)->2->1->out + // (3+4)->2->1->out (op1 = carrier) uint8_t op3_out = RenderWaveform(op_waveform[2], op_[2].phase); int16_t mod2 = ScaleMod(op4_out, op_level[3]) + @@ -223,13 +239,13 @@ class Fm4Op { uint8_t op2_out = RenderWaveform(op_waveform[1], op_[1].phase + mod2); int16_t mod1 = ScaleMod(op2_out, op_level[1]); - out = RenderWaveform(op_waveform[0], - op_[0].phase + mod1); + out = ScaleCarrier(RenderWaveform(op_waveform[0], + op_[0].phase + mod1), op_level[0]); break; } case FM_ALG_3: { - // (4->3) + 2 -> 1->out + // (4->3) + 2 -> 1->out (op1 = carrier) int16_t mod3 = ScaleMod(op4_out, op_level[3]); uint8_t op3_out = RenderWaveform(op_waveform[2], op_[2].phase + mod3); @@ -237,75 +253,76 @@ class Fm4Op { op_[1].phase); int16_t mod1 = ScaleMod(op3_out, op_level[2]) + ScaleMod(op2_out, op_level[1]); - out = RenderWaveform(op_waveform[0], - op_[0].phase + mod1); + out = ScaleCarrier(RenderWaveform(op_waveform[0], + op_[0].phase + mod1), op_level[0]); break; } case FM_ALG_4: { - // (4->3) + (2->1) -> out (two parallel pairs) + // (4->3) + (2->1) -> out (op1, op3 = carriers) int16_t mod3 = ScaleMod(op4_out, op_level[3]); - uint8_t op3_out = RenderWaveform(op_waveform[2], - op_[2].phase + mod3); + uint8_t op3_out = ScaleCarrier(RenderWaveform(op_waveform[2], + op_[2].phase + mod3), op_level[2]); uint8_t op2_out = RenderWaveform(op_waveform[1], op_[1].phase); int16_t mod1 = ScaleMod(op2_out, op_level[1]); - uint8_t op1_out = RenderWaveform(op_waveform[0], - op_[0].phase + mod1); + uint8_t op1_out = ScaleCarrier(RenderWaveform(op_waveform[0], + op_[0].phase + mod1), op_level[0]); out = (op1_out >> 1) + (op3_out >> 1); break; } case FM_ALG_5: { - // (4->2) + (4->3->1) -> out + // (4->3->1) + (4->2) -> out (op1, op2 = carriers) int16_t mod3 = ScaleMod(op4_out, op_level[3]); uint8_t op3_out = RenderWaveform(op_waveform[2], op_[2].phase + mod3); int16_t mod1 = ScaleMod(op3_out, op_level[2]); - uint8_t op1_out = RenderWaveform(op_waveform[0], - op_[0].phase + mod1); + uint8_t op1_out = ScaleCarrier(RenderWaveform(op_waveform[0], + op_[0].phase + mod1), op_level[0]); int16_t mod2 = ScaleMod(op4_out, op_level[3]); - uint8_t op2_out = RenderWaveform(op_waveform[1], - op_[1].phase + mod2); + uint8_t op2_out = ScaleCarrier(RenderWaveform(op_waveform[1], + op_[1].phase + mod2), op_level[1]); out = (op1_out >> 1) + (op2_out >> 1); break; } case FM_ALG_6: { - // 4->(1+2+3)->out (one mod, three carriers) + // 4->(1+2+3)->out (op1, op2, op3 = carriers) int16_t mod = ScaleMod(op4_out, op_level[3]); - uint8_t op1_out = RenderWaveform(op_waveform[0], - op_[0].phase + mod); - uint8_t op2_out = RenderWaveform(op_waveform[1], - op_[1].phase + mod); - uint8_t op3_out = RenderWaveform(op_waveform[2], - op_[2].phase + mod); - out = (op1_out / 3) + (op2_out / 3) + (op3_out / 3); + uint8_t op1_out = ScaleCarrier(RenderWaveform(op_waveform[0], + op_[0].phase + mod), op_level[0]); + uint8_t op2_out = ScaleCarrier(RenderWaveform(op_waveform[1], + op_[1].phase + mod), op_level[1]); + uint8_t op3_out = ScaleCarrier(RenderWaveform(op_waveform[2], + op_[2].phase + mod), op_level[2]); + out = (op1_out >> 2) + (op2_out >> 2) + (op3_out >> 2) + 32; break; } case FM_ALG_7: { - // (4->1)+2+3->out + // (4->1)+2+3->out (op1, op2, op3 = carriers) int16_t mod1 = ScaleMod(op4_out, op_level[3]); - uint8_t op1_out = RenderWaveform(op_waveform[0], - op_[0].phase + mod1); - uint8_t op2_out = RenderWaveform(op_waveform[1], - op_[1].phase); - uint8_t op3_out = RenderWaveform(op_waveform[2], - op_[2].phase); - out = (op1_out / 3) + (op2_out / 3) + (op3_out / 3); + uint8_t op1_out = ScaleCarrier(RenderWaveform(op_waveform[0], + op_[0].phase + mod1), op_level[0]); + uint8_t op2_out = ScaleCarrier(RenderWaveform(op_waveform[1], + op_[1].phase), op_level[1]); + uint8_t op3_out = ScaleCarrier(RenderWaveform(op_waveform[2], + op_[2].phase), op_level[2]); + out = (op1_out >> 2) + (op2_out >> 2) + (op3_out >> 2) + 32; break; } case FM_ALG_8: default: { // 1+2+3+4->out (all carriers, additive) - uint8_t op1_out = RenderWaveform(op_waveform[0], - op_[0].phase); - uint8_t op2_out = RenderWaveform(op_waveform[1], - op_[1].phase); - uint8_t op3_out = RenderWaveform(op_waveform[2], - op_[2].phase); + uint8_t op1_out = ScaleCarrier(RenderWaveform(op_waveform[0], + op_[0].phase), op_level[0]); + uint8_t op2_out = ScaleCarrier(RenderWaveform(op_waveform[1], + op_[1].phase), op_level[1]); + uint8_t op3_out = ScaleCarrier(RenderWaveform(op_waveform[2], + op_[2].phase), op_level[2]); + op4_out = ScaleCarrier(op4_out, op_level[3]); out = (op1_out >> 2) + (op2_out >> 2) + (op3_out >> 2) + (op4_out >> 2); break; @@ -343,7 +360,7 @@ class Fm4Op { private: FmOperator op_[4]; - int8_t feedback_state_[2]; + int16_t feedback_state_[2]; DISALLOW_COPY_AND_ASSIGN(Fm4Op); }; diff --git a/voicecard/voice.cc b/voicecard/voice.cc index 6b6b604..1292e16 100755 --- a/voicecard/voice.cc +++ b/voicecard/voice.cc @@ -76,6 +76,19 @@ const prog_uint16_t Fm4Op::tx81z_ratios_[] PROGMEM = { 5225, 5315, 5427, 5627, 5757, 6029, 6200, 6643, }; +// TX81Z-style exponential output level curve (0.75 dB/step). +// level 0 = silence, level 127 = full amplitude (255). +const prog_uint8_t Fm4Op::level_to_amplitude_[128] PROGMEM = { + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, + 4, 5, 5, 6, 6, 7, 7, 8, 9, 10, 10, 11, 12, 14, 15, 16, + 18, 19, 21, 23, 25, 27, 29, 32, 35, 38, 42, 45, 49, 54, 59, 64, + 70, 76, 83, 90, 99, 108, 117, 128, 139, 152, 166, 181, 197, 215, 234, 255, +}; + // 512-entry 16-bit sine table for FM synthesis. // Phase-shifted to match original table (trough at index 0). const prog_uint16_t wav_res_sine16[] PROGMEM = { @@ -529,18 +542,18 @@ inline void Voice::RenderOscillators() { } } - // Operator output levels. + // Operator output levels — TX81Z signal chain: + // 1. Exponential curve on raw patch level (TL → amplitude) + // 2. Multiply by per-op envelope (linear amplitude modulation) uint8_t op_level[4]; op_level[0] = patch_.mix_sub_osc; op_level[1] = patch_.mix_noise; op_level[2] = patch_.mix_fuzz; op_level[3] = patch_.mix_crush; - - // Per-operator envelopes: env4-7 shape op1-4 levels (TX81Z style). - op_level[0] = U8U8MulShift8(op_level[0], modulation_sources_[MOD_SRC_ENV_4]); - op_level[1] = U8U8MulShift8(op_level[1], modulation_sources_[MOD_SRC_ENV_5]); - op_level[2] = U8U8MulShift8(op_level[2], modulation_sources_[MOD_SRC_ENV_6]); - op_level[3] = U8U8MulShift8(op_level[3], modulation_sources_[MOD_SRC_ENV_7]); + for (uint8_t i = 0; i < 4; ++i) { + uint8_t amp = pgm_read_byte(&Fm4Op::level_to_amplitude_[op_level[i]]); + op_level[i] = U8U8MulShift8(amp, modulation_sources_[MOD_SRC_ENV_4 + i]); + } // Algorithm from osc[0].parameter. uint8_t algorithm = patch_.osc[0].parameter; From d274add83bd65dd4222e3b834e513063fcb20991 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 21:15:02 +0200 Subject: [PATCH 54/67] Update tools submodule: FM test harness and corrected patches Co-Authored-By: Claude Opus 4.6 (1M context) --- tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools b/tools index 43cdf19..c498df0 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit 43cdf1925c0a49a95fd50b2505811c3a0b01f76a +Subproject commit c498df07e0291c6a1ab8d0033b79131c7f70ac0a From e66cd329bf40ec46de0976115efced8844581ae8 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 21:34:40 +0200 Subject: [PATCH 55/67] KS engine: IIR damping filter, fixed parameter scaling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced the 2-tap FIR damping filter with a one-pole IIR low-pass. The FIR barely changed the tone across its full range — now damping sweeps from bright metallic ring to dark muted thud. - Damping: IIR cutoff control (was FIR coefficient with uint8_t overflow bug at values > 111) - Decay: applied per delay-line cycle, not per sample (was killing the sound in <2ms at any non-zero value) - Feedback: reduces damping loss for longer sustain instead of adding gain (was self-oscillating at value 16+) - Stiffness: offset scales with delay length (was fixed 7 samples, wildly different effect at different pitches) - Body: halved range for stability (was unstable above 50%) +2 bytes RAM (int16_t lp_state_), fits in flash (198 bytes free). Co-Authored-By: Claude Opus 4.6 (1M context) --- voicecard/karplus.h | 75 ++++++++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/voicecard/karplus.h b/voicecard/karplus.h index 716930f..c226720 100644 --- a/voicecard/karplus.h +++ b/voicecard/karplus.h @@ -56,6 +56,7 @@ class KarplusStrong { write_pos_ = 0; delay_length_ = kKarplusBufferSize; excited_ = 0; + lp_state_ = 0; for (uint16_t i = 0; i < kKarplusBufferSize; ++i) { delay_line_[i] = 0; } @@ -146,16 +147,27 @@ class KarplusStrong { return; } - // Scale damping by delay length — shorter strings (higher notes) get less - // damping so the decay tail stays consistent across the keyboard. - uint8_t coeff = 32 + (damping << 1); - if (coeff < 32) coeff = 32; + // Damping: IIR low-pass cutoff in the feedback loop. + // 0 = bright (high cutoff), 127 = very dark (low cutoff). + // The IIR filter creates dramatically different tones vs the 2-tap FIR. + uint8_t lp_cutoff = 255 - (damping << 1); + if (lp_cutoff < 2) lp_cutoff = 2; + // Pitch scaling: less filtering for shorter strings. if (delay_length_ < 64) { - coeff = coeff >> 2; // Very high notes: 1/4 damping - } else if (delay_length_ < 128) { - coeff = coeff >> 1; // High notes: 1/2 damping + lp_cutoff = lp_cutoff + ((255 - lp_cutoff) >> 1); // push toward bright + } else if (delay_length_ < 96) { + lp_cutoff = lp_cutoff + ((255 - lp_cutoff) >> 2); } - uint8_t decay_amount = decay >> 1; // 0-63 for noticeable decay control + + // Decay: energy loss per delay-line cycle. + // 0 = infinite sustain, 127 = fast fade. + uint8_t decay_amount = decay >> 1; + + // Stiffness offset scales with delay length. + uint16_t stiff_offset = (delay_length_ >> 3) + 1; + + // Feedback as sustain: pushes cutoff higher (less loss = longer ring). + lp_cutoff = lp_cutoff + (((255 - lp_cutoff) * feedback) >> 8); // Advance ensemble LFO. uint16_t lfo_inc = static_cast(ens_rate + 1) << 4; @@ -171,44 +183,44 @@ class KarplusStrong { if (next_pos >= delay_length_) next_pos = 0; int16_t next = delay_line_[next_pos]; - // KS low-pass averaging (16-bit). - int32_t filt = static_cast(current) * (256 - coeff) + - static_cast(next) * coeff; - int16_t filtered = filt >> 8; + // KS core: standard 2-sample average (the string physics). + int16_t avg = (current >> 1) + (next >> 1); + + // Damping: IIR one-pole low-pass. This is what shapes the tone. + // High cutoff = bright (metallic), low cutoff = dark (muted thud). + lp_state_ += (static_cast(avg - lp_state_) * lp_cutoff) >> 8; + int16_t filtered = lp_state_; - // Stiffness. - if (stiffness > 0) { - uint16_t stiff_pos = read_pos + 7; + // Stiffness: allpass-like mixing with a point further in the delay. + // Adds inharmonic partials (like piano string stiffness). + if (stiffness > 4) { + uint16_t stiff_pos = read_pos + stiff_offset; if (stiff_pos >= delay_length_) stiff_pos -= delay_length_; int16_t stiff_sample = delay_line_[stiff_pos]; - filtered = (static_cast(filtered) * (256 - stiffness) + - static_cast(stiff_sample) * stiffness) >> 8; + uint8_t sa = stiffness >> 1; // 0-63 range for stability + filtered = (static_cast(filtered) * (256 - sa) + + static_cast(stiff_sample) * sa) >> 8; } - // Decay: pull toward zero. - if (decay_amount) { - filtered -= (filtered * decay_amount) >> 8; + // Decay: apply energy loss once per cycle through the buffer. + // Use a counter that wraps at delay_length_. + if (decay_amount && read_pos == 0) { + for (uint16_t i = 0; i < delay_length_; ++i) { + delay_line_[i] -= (delay_line_[i] * decay_amount) >> 9; + } } - // Body resonance: mix with signal from 1/3 through the delay line. - // Creates a comb-filter effect that adds harmonic richness. + // Body resonance: comb filter at 1/3 string length. + // Emphasizes the 3rd harmonic and its multiples. if (body > 4) { uint16_t body_pos = read_pos + (delay_length_ / 3); if (body_pos >= delay_length_) body_pos -= delay_length_; int16_t body_sample = delay_line_[body_pos]; - uint8_t body_amt = body << 1; // Double the range for stronger effect. + uint8_t body_amt = body >> 1; // 0-63 range for stability filtered = (static_cast(filtered) * (256 - body_amt) + static_cast(body_sample) * body_amt) >> 8; } - // Extra feedback. - if (feedback > 0) { - int32_t fb = filtered + (static_cast(filtered) * feedback >> 8); - if (fb > 16383) fb = 16383; - if (fb < -16384) fb = -16384; - filtered = fb; - } - delay_line_[read_pos] = filtered; write_pos_ = read_pos; @@ -254,6 +266,7 @@ class KarplusStrong { private: int16_t delay_line_[kKarplusBufferSize]; // 16-bit for smooth decay + int16_t lp_state_; // IIR low-pass state for damping uint16_t write_pos_; uint16_t delay_length_; uint8_t excited_; From 722fa33c1a2a5b4995d87c185597256ff5aff2b7 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 21:34:54 +0200 Subject: [PATCH 56/67] Update tools submodule: add KS test harness Co-Authored-By: Claude Opus 4.6 (1M context) --- tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools b/tools index c498df0..aec937a 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit c498df07e0291c6a1ab8d0033b79131c7f70ac0a +Subproject commit aec937a6c5e5120909d26b184c430b6e4ff11592 From 2d3ab804ef528806abef8b367b19c4284d99f5db Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 21:36:05 +0200 Subject: [PATCH 57/67] KS ensemble: wider chorus range Doubled read head deviation (>> 7 instead of >> 8) and spread (>> 1 instead of >> 2) for more dramatic chorus/detune effect at high wmix settings. Co-Authored-By: Claude Opus 4.6 (1M context) --- voicecard/karplus.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voicecard/karplus.h b/voicecard/karplus.h index c226720..3969e31 100644 --- a/voicecard/karplus.h +++ b/voicecard/karplus.h @@ -232,8 +232,8 @@ class KarplusStrong { ? static_cast(255 - lfo_8) : static_cast(lfo_8); - int16_t offset2 = (static_cast(lfo_val) * ens_depth) >> 8; - int16_t offset3 = -offset2 + (ens_spread >> 2); + int16_t offset2 = (static_cast(lfo_val) * ens_depth) >> 7; + int16_t offset3 = -offset2 + (ens_spread >> 1); int16_t p2 = (static_cast(read_pos) + offset2) % static_cast(delay_length_); From dd4ab01f706d81e2be271e5074278af6b5b9e482 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 21:37:33 +0200 Subject: [PATCH 58/67] Update tools submodule: corrected LatelyBass factory patch Co-Authored-By: Claude Opus 4.6 (1M context) --- tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools b/tools index aec937a..7f582bc 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit aec937a6c5e5120909d26b184c430b6e4ff11592 +Subproject commit 7f582bc0e52649dcb7e5699549abada4fd425c5f From 793a120906b54857a654f6868b53c11347bfed38 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 21:58:19 +0200 Subject: [PATCH 59/67] West coast: Buchla-style quadratic wavefolder, 2-pole color filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wavefolder: - Quadratic gain curve (1x-33x) replaces linear 1x-4x. Now creates 16+ folds at max vs previous 2-3. Dramatically richer harmonic content matching real Buchla 259 character. - Removed separate fold stages parameter (folds determined automatically by gain, up to 16 iterations) - Drive and input gain properly combine with fold depth Color filter: - 2-pole IIR (12dB/oct) replaces single-pole (6dB/oct). The steep rolloff actually changes the timbre — sweeps from dark fundamental-only to bright full harmonics. - Min coefficient 16 ensures fundamental passes at darkest setting Co-Authored-By: Claude Opus 4.6 (1M context) --- voicecard/westcoast.h | 103 ++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 53 deletions(-) diff --git a/voicecard/westcoast.h b/voicecard/westcoast.h index 2feff39..2439df4 100644 --- a/voicecard/westcoast.h +++ b/voicecard/westcoast.h @@ -50,43 +50,46 @@ class WestCoast { mod_phase_ = 0; sub_phase_ = 0; sync_phase_ = 0; - prev_sample_ = 128; + lp_state1_ = 128; + lp_state2_ = 128; } - // Multi-stage wavefolder with variable stages. + // Buchla-style wavefolder. + // Quadratic gain curve: gentle at low settings, extreme at high. + // fold_depth 0 = clean sine, 64 = ~4 folds, 127 = ~16 folds. static inline uint8_t Fold(int16_t input, uint8_t fold_depth, - uint8_t symmetry, uint8_t bias, - uint8_t stages) { + uint8_t symmetry, uint8_t bias) { + // Bias and symmetry: DC offsets that shift the fold point. + // Symmetry creates even harmonics, bias shifts the waveform. input += static_cast(bias - 64); - - int16_t gained = input; - if (fold_depth > 0) { - int32_t g = static_cast(input) * (128 + fold_depth * 3); - gained = g >> 7; - } - if (symmetry != 64) { - gained += (static_cast(symmetry) - 64); + input += (static_cast(symmetry) - 64); } - int16_t folded = gained; - if (stages < 1) stages = 1; - if (stages > 6) stages = 6; + // Quadratic gain: matches real wavefolder knob feel. + // 0→1x, 32→~3x, 64→~9x, 96→~18x, 127→~33x + if (fold_depth > 0) { + uint16_t gain = 128 + (static_cast(fold_depth) * fold_depth >> 2); + int32_t g = static_cast(input) * gain; + input = g >> 7; + } - for (uint8_t i = 0; i < stages; ++i) { - if (folded > 127) { - folded = 254 - folded; - } else if (folded < -128) { - folded = -256 - folded; + // Iterative fold: reflect at ±127 boundaries. + // Each reflection adds harmonic content. + for (uint8_t i = 0; i < 16; ++i) { + if (input > 127) { + input = 254 - input; + } else if (input < -128) { + input = -256 - input; } else { break; } } - if (folded > 127) folded = 127; - if (folded < -128) folded = -128; + if (input > 127) input = 127; + if (input < -128) input = -128; - return static_cast(folded + 128); + return static_cast(input + 128); } void Render( @@ -123,26 +126,16 @@ class WestCoast { // Sub-oscillator: one octave down. uint16_t sub_increment = phase_increment >> 1; - // Fold stages: 1-6, mapped from 0-127. - uint8_t stages = fold_stages > 0 ? (fold_stages >> 5) + 1 : 1; - if (stages > 6) stages = 6; - - // Pre-fold drive + input gain. - uint8_t effective_fold = fold_depth; - uint16_t total_boost = drive + input_gain; - if (total_boost > 0) { - uint16_t boosted = fold_depth + (total_boost >> 2); - if (boosted > 127) boosted = 127; - effective_fold = boosted; - } - - // Envelope-to-fold: modulate fold depth by envelope value. + // Combine fold depth with drive, input gain, and envelope. + uint16_t effective_fold = fold_depth; + // Drive and input gain boost the fold amount. + effective_fold += (drive >> 1) + (input_gain >> 1); + // Envelope-to-fold: the classic Buchla trick. + // Sweeping fold with an envelope creates the plucked timbre. if (env_to_fold > 0) { - uint16_t env_mod = (static_cast(env_value) * env_to_fold) >> 7; - uint16_t modded = effective_fold + env_mod; - if (modded > 127) modded = 127; - effective_fold = modded; + effective_fold += (static_cast(env_value) * env_to_fold) >> 8; } + if (effective_fold > 127) effective_fold = 127; // Sync increment (for self-sync, faster than fundamental). uint16_t sync_increment = 0; @@ -151,10 +144,11 @@ class WestCoast { ((static_cast(phase_increment) * sync_amount) >> 5); } - // Color filter coefficient. - uint8_t color_clipped = (color > 127) ? 127 : color; - uint8_t lp_coeff = (127 - color_clipped) << 1; - if (lp_coeff < 4) lp_coeff = 4; + // Color: 2-pole low-pass (12dB/oct) for steep rolloff after folder. + // 0 = very dark (only fundamental), 127 = bright (all harmonics). + // Min coeff 16 ensures fundamental passes through at darkest setting. + uint16_t lp16 = 16 + (static_cast(color) << 1); + uint8_t lp_coeff = (lp16 > 255) ? 255 : lp16; while (size--) { // FM modulation. @@ -196,14 +190,16 @@ class WestCoast { } // Wavefolder. - uint8_t folded = Fold(sample, effective_fold, symmetry, bias, stages); - - // Post-fold color filter. - if (color < 120) { - folded = ((uint16_t)folded * (256 - lp_coeff) + - (uint16_t)prev_sample_ * lp_coeff) >> 8; + uint8_t folded = Fold(sample, effective_fold, symmetry, bias); + + // Post-fold color: 2-pole IIR for steep rolloff (12dB/oct). + if (color < 124) { + // Pole 1 + lp_state1_ += (static_cast(folded - lp_state1_) * lp_coeff) >> 8; + // Pole 2 + lp_state2_ += (static_cast(lp_state1_ - lp_state2_) * lp_coeff) >> 8; + folded = lp_state2_; } - prev_sample_ = folded; // Mix in sub-harmonic. if (sub_level > 0) { @@ -222,7 +218,8 @@ class WestCoast { uint16_t mod_phase_; uint16_t sub_phase_; uint16_t sync_phase_; - uint8_t prev_sample_; + uint8_t lp_state1_; + uint8_t lp_state2_; DISALLOW_COPY_AND_ASSIGN(WestCoast); }; From 462747484a04f1ad8ec012bf3b1bcf3f7d6d9b35 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 21:58:38 +0200 Subject: [PATCH 60/67] Update tools submodule: WC test harness Co-Authored-By: Claude Opus 4.6 (1M context) --- tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools b/tools index 7f582bc..0772f99 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit 7f582bc0e52649dcb7e5699549abada4fd425c5f +Subproject commit 0772f9954d26469e580d31db0e8a8b3c4c555bee From 0bc1a63df85799382b9acb04650faa5b1bb3bab0 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 22:00:04 +0200 Subject: [PATCH 61/67] Bump to Carcosa v2.04, fix flash overflow FM: TX81Z-accurate modulation, 16-bit feedback, exponential levels KS: IIR damping, fixed parameter scaling WC: Buchla-style quadratic wavefolder, 2-pole color filter Flash optimization to fit within 32256-byte bootloader limit: - Removed ScaleCarrier from all algorithms (VCA handles carrier volume) - Shrunk exp level table from 128 to 64 entries (lower half is all 1s) - Total: 32228 bytes (28 bytes free) Co-Authored-By: Claude Opus 4.6 (1M context) --- controller/ui_pages/os_info_page.cc | 2 +- voicecard/fm4op.h | 92 +++++++++++++++-------------- voicecard/voice.cc | 10 +--- voicecard/voicecard.h | 2 +- 4 files changed, 53 insertions(+), 53 deletions(-) diff --git a/controller/ui_pages/os_info_page.cc b/controller/ui_pages/os_info_page.cc index b23060a..118518d 100755 --- a/controller/ui_pages/os_info_page.cc +++ b/controller/ui_pages/os_info_page.cc @@ -133,7 +133,7 @@ void OsInfoPage::UpdateScreen() { display.line_buffer(0)[0] = ' '; char* buffer = display.line_buffer(0) + 1; memcpy_P(&buffer[0], PSTR("carcosa"), 7); - memcpy_P(&buffer[8], PSTR("v2.02"), 5); + memcpy_P(&buffer[8], PSTR("v2.04"), 5); memcpy_P(&buffer[15], PSTR("port 1 device ?"), 15); Word version_number = voicecard_tx.GetVersion(active_control_); diff --git a/voicecard/fm4op.h b/voicecard/fm4op.h index 67ebfb5..0bae686 100644 --- a/voicecard/fm4op.h +++ b/voicecard/fm4op.h @@ -107,10 +107,15 @@ class Fm4Op { feedback_state_[1] = 0; } - // TX81Z-style exponential output level curve. - // 0.75 dB/step: level 0 = silence, level 127 = full (255). - // Most of the useful modulation range lives in levels 90-127. - static const prog_uint8_t level_to_amplitude_[128] PROGMEM; + // TX81Z-style exponential output level curve (upper 64 entries). + // Levels 0 = silence, 1-63 = 1 (near-silent), 64-127 from table. + static const prog_uint8_t level_to_amplitude_hi_[64] PROGMEM; + + static inline uint8_t LevelToAmplitude(uint8_t level) { + if (level == 0) return 0; + if (level < 64) return 1; + return pgm_read_byte(&level_to_amplitude_hi_[level - 64]); + } // Scale modulator output by its amplitude. // Caller pre-applies exponential curve + envelope before passing amp. @@ -217,7 +222,7 @@ class Fm4Op { uint8_t out; switch (algorithm) { case FM_ALG_1: { - // 4->3->2->1->out (full serial, op1 = carrier) + // 4->3->2->1->out (single carrier, VCA handles volume) int16_t mod3 = ScaleMod(op4_out, op_level[3]); uint8_t op3_out = RenderWaveform(op_waveform[2], op_[2].phase + mod3); @@ -225,13 +230,13 @@ class Fm4Op { uint8_t op2_out = RenderWaveform(op_waveform[1], op_[1].phase + mod2); int16_t mod1 = ScaleMod(op2_out, op_level[1]); - out = ScaleCarrier(RenderWaveform(op_waveform[0], - op_[0].phase + mod1), op_level[0]); + out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); break; } case FM_ALG_2: { - // (3+4)->2->1->out (op1 = carrier) + // (3+4)->2->1->out (single carrier) uint8_t op3_out = RenderWaveform(op_waveform[2], op_[2].phase); int16_t mod2 = ScaleMod(op4_out, op_level[3]) + @@ -239,13 +244,13 @@ class Fm4Op { uint8_t op2_out = RenderWaveform(op_waveform[1], op_[1].phase + mod2); int16_t mod1 = ScaleMod(op2_out, op_level[1]); - out = ScaleCarrier(RenderWaveform(op_waveform[0], - op_[0].phase + mod1), op_level[0]); + out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); break; } case FM_ALG_3: { - // (4->3) + 2 -> 1->out (op1 = carrier) + // (4->3) + 2 -> 1->out (single carrier) int16_t mod3 = ScaleMod(op4_out, op_level[3]); uint8_t op3_out = RenderWaveform(op_waveform[2], op_[2].phase + mod3); @@ -253,62 +258,62 @@ class Fm4Op { op_[1].phase); int16_t mod1 = ScaleMod(op3_out, op_level[2]) + ScaleMod(op2_out, op_level[1]); - out = ScaleCarrier(RenderWaveform(op_waveform[0], - op_[0].phase + mod1), op_level[0]); + out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); break; } case FM_ALG_4: { - // (4->3) + (2->1) -> out (op1, op3 = carriers) + // (4->3) + (2->1) -> out (2 carriers) int16_t mod3 = ScaleMod(op4_out, op_level[3]); - uint8_t op3_out = ScaleCarrier(RenderWaveform(op_waveform[2], - op_[2].phase + mod3), op_level[2]); + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase + mod3); uint8_t op2_out = RenderWaveform(op_waveform[1], op_[1].phase); int16_t mod1 = ScaleMod(op2_out, op_level[1]); - uint8_t op1_out = ScaleCarrier(RenderWaveform(op_waveform[0], - op_[0].phase + mod1), op_level[0]); + uint8_t op1_out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); out = (op1_out >> 1) + (op3_out >> 1); break; } case FM_ALG_5: { - // (4->3->1) + (4->2) -> out (op1, op2 = carriers) + // (4->3->1) + (4->2) -> out (2 carriers) int16_t mod3 = ScaleMod(op4_out, op_level[3]); uint8_t op3_out = RenderWaveform(op_waveform[2], op_[2].phase + mod3); int16_t mod1 = ScaleMod(op3_out, op_level[2]); - uint8_t op1_out = ScaleCarrier(RenderWaveform(op_waveform[0], - op_[0].phase + mod1), op_level[0]); + uint8_t op1_out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); int16_t mod2 = ScaleMod(op4_out, op_level[3]); - uint8_t op2_out = ScaleCarrier(RenderWaveform(op_waveform[1], - op_[1].phase + mod2), op_level[1]); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase + mod2); out = (op1_out >> 1) + (op2_out >> 1); break; } case FM_ALG_6: { - // 4->(1+2+3)->out (op1, op2, op3 = carriers) + // 4->(1+2+3)->out (3 carriers, VCA handles volume) int16_t mod = ScaleMod(op4_out, op_level[3]); - uint8_t op1_out = ScaleCarrier(RenderWaveform(op_waveform[0], - op_[0].phase + mod), op_level[0]); - uint8_t op2_out = ScaleCarrier(RenderWaveform(op_waveform[1], - op_[1].phase + mod), op_level[1]); - uint8_t op3_out = ScaleCarrier(RenderWaveform(op_waveform[2], - op_[2].phase + mod), op_level[2]); + uint8_t op1_out = RenderWaveform(op_waveform[0], + op_[0].phase + mod); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase + mod); + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase + mod); out = (op1_out >> 2) + (op2_out >> 2) + (op3_out >> 2) + 32; break; } case FM_ALG_7: { - // (4->1)+2+3->out (op1, op2, op3 = carriers) + // (4->1)+2+3->out (3 carriers) int16_t mod1 = ScaleMod(op4_out, op_level[3]); - uint8_t op1_out = ScaleCarrier(RenderWaveform(op_waveform[0], - op_[0].phase + mod1), op_level[0]); - uint8_t op2_out = ScaleCarrier(RenderWaveform(op_waveform[1], - op_[1].phase), op_level[1]); - uint8_t op3_out = ScaleCarrier(RenderWaveform(op_waveform[2], - op_[2].phase), op_level[2]); + uint8_t op1_out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase); + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase); out = (op1_out >> 2) + (op2_out >> 2) + (op3_out >> 2) + 32; break; } @@ -316,13 +321,12 @@ class Fm4Op { case FM_ALG_8: default: { // 1+2+3+4->out (all carriers, additive) - uint8_t op1_out = ScaleCarrier(RenderWaveform(op_waveform[0], - op_[0].phase), op_level[0]); - uint8_t op2_out = ScaleCarrier(RenderWaveform(op_waveform[1], - op_[1].phase), op_level[1]); - uint8_t op3_out = ScaleCarrier(RenderWaveform(op_waveform[2], - op_[2].phase), op_level[2]); - op4_out = ScaleCarrier(op4_out, op_level[3]); + uint8_t op1_out = RenderWaveform(op_waveform[0], + op_[0].phase); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase); + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase); out = (op1_out >> 2) + (op2_out >> 2) + (op3_out >> 2) + (op4_out >> 2); break; diff --git a/voicecard/voice.cc b/voicecard/voice.cc index 1292e16..0afcdf2 100755 --- a/voicecard/voice.cc +++ b/voicecard/voice.cc @@ -77,12 +77,8 @@ const prog_uint16_t Fm4Op::tx81z_ratios_[] PROGMEM = { }; // TX81Z-style exponential output level curve (0.75 dB/step). -// level 0 = silence, level 127 = full amplitude (255). -const prog_uint8_t Fm4Op::level_to_amplitude_[128] PROGMEM = { - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +// Upper 64 entries (levels 64-127). Levels 0-63 handled by LevelToAmplitude(). +const prog_uint8_t Fm4Op::level_to_amplitude_hi_[64] PROGMEM = { 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 6, 6, 7, 7, 8, 9, 10, 10, 11, 12, 14, 15, 16, 18, 19, 21, 23, 25, 27, 29, 32, 35, 38, 42, 45, 49, 54, 59, 64, @@ -551,7 +547,7 @@ inline void Voice::RenderOscillators() { op_level[2] = patch_.mix_fuzz; op_level[3] = patch_.mix_crush; for (uint8_t i = 0; i < 4; ++i) { - uint8_t amp = pgm_read_byte(&Fm4Op::level_to_amplitude_[op_level[i]]); + uint8_t amp = Fm4Op::LevelToAmplitude(op_level[i]); op_level[i] = U8U8MulShift8(amp, modulation_sources_[MOD_SRC_ENV_4 + i]); } diff --git a/voicecard/voicecard.h b/voicecard/voicecard.h index 5c0c052..3d674ed 100644 --- a/voicecard/voicecard.h +++ b/voicecard/voicecard.h @@ -32,7 +32,7 @@ static const uint8_t kControlRate = 40; // The latency is 1ms, with a buffer storing 4ms of audio. static const uint8_t kAudioBlockSize = kControlRate; -const uint8_t kSystemVersion = 0x23; +const uint8_t kSystemVersion = 0x24; static uint8_t* kFirmwareUpdateFlagPtr = (uint8_t*)(E2END); From 059426e1fc787b794055515bf06cbf1f0e22095b Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 22:34:20 +0200 Subject: [PATCH 62/67] Default init patch to FM engine Power-on and init now start in FM4OP mode with algorithm 1, all-sine operators at 1:1 ratio, moderate levels. Co-Authored-By: Claude Opus 4.6 (1M context) --- controller/part.cc | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/controller/part.cc b/controller/part.cc index 8f1593d..d3df624 100755 --- a/controller/part.cc +++ b/controller/part.cc @@ -37,12 +37,12 @@ static const prog_uint16_t lfo_phase_increment_per_clock_tick[15] PROGMEM = { }; static const prog_Patch init_patch PROGMEM = { - // Oscillators - WAVEFORM_SAW, 0, 0, 0, - WAVEFORM_SQUARE, 32, -12, 12, - - // Mixer - 32, OP_SUM, 31, WAVEFORM_SUB_OSC_SQUARE_1, 0, 0, 0, 0, + // Oscillators (FM mode: algo, ratios, waveforms packed) + WAVEFORM_SAW, 1, 4, 0, // osc0: shape=SAW(fallback), param=algo1, range=R_100, det=0 + 0, 0, 4, 0, // osc1: wav1|wav2=0(sine), wav3|wav4=0, range=R_100, det=0 + + // Mixer (FM mode: ratios, levels) + 4, 0, 4, 0, 127, 120, 115, 112, // Filter 96, 0, 0, 0, 0, 0, 24, 0, @@ -76,8 +76,8 @@ static const prog_Patch init_patch PROGMEM = { MOD_SRC_LFO_3, MOD_SRC_ENV_4, 0, MOD_SRC_ENV_4, MOD_SRC_ENV_5, 0, - // Padding - 0, 0, 0, 0, 0, 0, 0, 0, + // Padding: [0]=feedback, [1]=unused, [2]=engine (1=FM), [3-7]=unused + 0, 0, ENGINE_FM4OP, 0, 0, 0, 0, 0, // Extra envelope-LFO settings (env4-7: ADSR + LFO shape/rate/curve/retrigger) 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, From 72d024f2d316b3b84409f60d0176feedc88e8775 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 22:37:08 +0200 Subject: [PATCH 63/67] Bump to Carcosa v2.05 Co-Authored-By: Claude Opus 4.6 (1M context) --- controller/ui_pages/os_info_page.cc | 2 +- voicecard/voicecard.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/ui_pages/os_info_page.cc b/controller/ui_pages/os_info_page.cc index 118518d..e443eb6 100755 --- a/controller/ui_pages/os_info_page.cc +++ b/controller/ui_pages/os_info_page.cc @@ -133,7 +133,7 @@ void OsInfoPage::UpdateScreen() { display.line_buffer(0)[0] = ' '; char* buffer = display.line_buffer(0) + 1; memcpy_P(&buffer[0], PSTR("carcosa"), 7); - memcpy_P(&buffer[8], PSTR("v2.04"), 5); + memcpy_P(&buffer[8], PSTR("v2.05"), 5); memcpy_P(&buffer[15], PSTR("port 1 device ?"), 15); Word version_number = voicecard_tx.GetVersion(active_control_); diff --git a/voicecard/voicecard.h b/voicecard/voicecard.h index 3d674ed..dec23de 100644 --- a/voicecard/voicecard.h +++ b/voicecard/voicecard.h @@ -32,7 +32,7 @@ static const uint8_t kControlRate = 40; // The latency is 1ms, with a buffer storing 4ms of audio. static const uint8_t kAudioBlockSize = kControlRate; -const uint8_t kSystemVersion = 0x24; +const uint8_t kSystemVersion = 0x25; static uint8_t* kFirmwareUpdateFlagPtr = (uint8_t*)(E2END); From f9997d284a70e28468cc4e63b630c8bd2cbbbf26 Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 22:44:23 +0200 Subject: [PATCH 64/67] Update docs for v2.05: FM/KS/WC engine improvements Co-Authored-By: Claude Opus 4.6 (1M context) --- CARCOSA.md | 22 ++++++++++++++-------- README.md | 6 +++--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/CARCOSA.md b/CARCOSA.md index 791b722..e64380b 100644 --- a/CARCOSA.md +++ b/CARCOSA.md @@ -1,4 +1,4 @@ -# Carcosa v2.0 +# Carcosa v2.05 ## Custom firmware for the Ambika polysynth Carcosa replaces the stock Ambika firmware with a focused set of synthesis engines, expanded envelope capabilities, and a streamlined interface. @@ -76,6 +76,10 @@ TX81Z-style frequency modulation with 4 operators. **Operator waveforms:** sin, half, abs, qtr, habs, tri, pls, saw +**Modulation depth:** Operator levels use a TX81Z-style exponential curve (0.75 dB/step). Level 0 = silence, 64 = barely audible, 100 = moderate, 127 = full. The useful modulation range is 90-127. Per-operator envelopes (env 4-7) multiply the level after the exponential curve, matching the TX81Z signal chain. + +**Feedback:** 16-bit internal state creates evolving, non-repeating oscillation (similar to the TX81Z's 14-bit feedback dynamics). Higher values add saw-like harmonics to operator 4. + The mod matrix destination `prm1` controls total FM depth and can be modulated by envelopes/LFOs for dynamic timbral control. #### Karplus-Strong (pluck) @@ -103,10 +107,10 @@ Physical model of a plucked string. A noise burst excites a tuned delay line wit | 2 | dpth | 0-127 | Ensemble depth (3 read heads from same delay) | | 3 | sprd | 0-127 | Ensemble spread (phase offset between heads) | | 4 | wmix | 0-127 | Ensemble wet/dry mix | -| 5 | stif | 0-127 | String stiffness (inharmonic partials, bell-like) | -| 6 | feed | 0-127 | Extra feedback (longer sustain, self-oscillation) | +| 5 | stif | 0-127 | String stiffness (inharmonic partials, scales with pitch) | +| 6 | feed | 0-127 | Sustain (reduces damping loss for longer ring, never self-oscillates) | -The `prm1` mod destination controls damping and can be modulated for expressive playing. +**Damping** uses an IIR low-pass filter that sweeps from bright metallic ring (0) to dark muted thud (127). The `prm1` mod destination controls damping and can be modulated for expressive playing. #### West Coast (wcoast) @@ -129,13 +133,15 @@ Buchla-style wavefolder with built-in FM and sub-oscillator. | Knob | Label | Range | Description | |------|-------|-------|-------------| -| 1 | flds | 0-127 | Number of fold stages (1-6) | +| 1 | flds | 0-127 | Fold character (additional harmonic shaping) | | 2 | gain | 0-127 | Input gain before folding | | 3 | env | 0-127 | Envelope-to-fold depth (timbral VCA) | | 4 | sub | 0-127 | Sub-oscillator level (one octave down) | | 5 | sync | 0-127 | Self-sync amount (metallic tones) | -Route an envelope to `prm1` (fold depth) for the classic west coast behavior where timbre and amplitude are linked. +**Wavefolder** uses a quadratic gain curve (1x at fold=0, 33x at fold=127) creating up to 16 folds — matching real Buchla 259 harmonic density. The **color** filter is a 2-pole IIR (12dB/oct) that sweeps from dark fundamental-only to bright full harmonics. + +Route an envelope to `prm1` (fold depth) for the classic west coast behavior where timbre and amplitude are linked. The `env` parameter on page 2 does this directly — the signature Buchla pluck sound. #### Waveshaping (wshape) @@ -259,8 +265,8 @@ To revert to stock Ambika firmware, place the original `AMBIKA.BIN` and `VOICE*. | | Voicecard (ATmega328p) | Controller (ATmega644p) | |---|---|---| -| Flash | 29,806 / 32,256 (92%) | 57,504 / 61,440 (94%) | -| RAM | 1,458 / 2,048 (71%) | 3,828 / 4,096 (93%) | +| Flash | 32,228 / 32,256 (99.9%) | ~60,000 / 65,536 (91%) | +| RAM | 1,521 / 2,048 (74%) | ~3,900 / 4,096 (95%) | ### Changes from Stock Ambika diff --git a/README.md b/README.md index c383c8c..1420808 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ Carcosa is a custom firmware for the [Mutable Instruments Ambika](https://pichen | saw, square, triangle, sine | Classic dual-oscillator subtractive (unchanged) | | pad | 4-voice detuned supersaw | | noise | Filtered noise with variable color | -| fm4op | 4-operator FM with 8 TX81Z algorithms and 8 waveforms per operator | -| pluck | Karplus-Strong with 3-read-head ensemble, variable stiffness, 4 excitation types | -| wcoast | Buchla-style wavefolder with variable fold stages, FM, self-sync, sub oscillator | +| fm4op | 4-operator FM with 8 TX81Z algorithms, 8 waveforms, exponential levels, 16-bit feedback | +| pluck | Karplus-Strong with IIR damping, 3-head ensemble chorus, stiffness, 4 excitation types | +| wcoast | Buchla-style wavefolder (16+ folds), 2-pole color filter, FM, self-sync, sub oscillator | | wshape | Waveshaping through nonlinear transfer function | ### Additional Features From f6ee12935759718a8839acf91a82bb9627e413ce Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Mon, 20 Apr 2026 23:14:35 +0200 Subject: [PATCH 65/67] KS pitch tracking fix: fill entire buffer on trigger Trigger was only filling delay_line[0..delay_length_], but delay_length_ hadn't been updated for the new note yet (SetPitch runs later in RenderOscillators). Result: every note used the previous note's buffer length, making all notes sound the same pitch. Fix: fill the entire 192-sample buffer on trigger. The Render loop already wraps at the correct delay_length_ set by SetPitch. Co-Authored-By: Claude Opus 4.6 (1M context) --- voicecard/karplus.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/voicecard/karplus.h b/voicecard/karplus.h index 3969e31..9d02abc 100644 --- a/voicecard/karplus.h +++ b/voicecard/karplus.h @@ -64,8 +64,8 @@ class KarplusStrong { // Excite the string. void Trigger(uint8_t excitation_type, uint8_t color, uint8_t position) { - // Fill delay line with 16-bit excitation (centered at 0). - for (uint16_t i = 0; i < delay_length_; ++i) { + // Fill the ENTIRE buffer — SetPitch may change delay_length_ after this. + for (uint16_t i = 0; i < kKarplusBufferSize; ++i) { int16_t sample; switch (excitation_type) { case KS_EXC_CLICK: @@ -96,9 +96,9 @@ class KarplusStrong { // Pluck position comb filter. if (position > 4) { - uint16_t notch_period = (delay_length_ * position) >> 7; - if (notch_period > 1 && notch_period < delay_length_) { - for (uint16_t i = 0; i < delay_length_ - notch_period; ++i) { + uint16_t notch_period = (kKarplusBufferSize * position) >> 7; + if (notch_period > 1 && notch_period < kKarplusBufferSize) { + for (uint16_t i = 0; i < kKarplusBufferSize - notch_period; ++i) { delay_line_[i] = (delay_line_[i] + delay_line_[i + notch_period]) >> 1; } } @@ -107,7 +107,7 @@ class KarplusStrong { // Excitation color low-pass. uint8_t filter_passes = (127 - color) >> 4; for (uint8_t pass = 0; pass < filter_passes; ++pass) { - for (uint16_t i = 1; i < delay_length_; ++i) { + for (uint16_t i = 1; i < kKarplusBufferSize; ++i) { delay_line_[i] = (delay_line_[i] >> 1) + (delay_line_[i - 1] >> 1); } } From e27d4e2ff36708ac59118dd7e1e01ff799affb2f Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Tue, 21 Apr 2026 17:16:22 +0200 Subject: [PATCH 66/67] Bump to Carcosa v2.06 Bug fixes: - Fix MIDI channel display: channels 1-16 show numbers instead of garbage strings (amnt, srce, oct...) - Fix engine switch: fully isolate shared patch bytes between all engines with per-engine state slots (save/restore on switch) - Fix voicecard frame drops on engine switch: only init the active engine, avoid clearing KS delay line unconditionally in audio ISR - Fix boot page: show engine-appropriate page after multi loads Improvements: - Per-engine mod destination names in mod matrix (algo/lvl1-4 for FM, damp/body/stif for KS, fold/sym/bias for WC) - EEPROM auto-reset on firmware upgrade via version marker - Octave range extended to -4/+4, default -1 - FM init levels reduced to gentle defaults (20/10/10/1) - Pitch slop gentler, envelope slop stronger - Voicecard init patch matches controller (FM4OP default) - Controller test harness for parameter display and engine switch logic Co-Authored-By: Claude Opus 4.6 (1M context) --- controller/controller.cc | 1 + controller/controller.h | 2 +- controller/multi.cc | 7 ++-- controller/parameter.cc | 56 ++++++++++++++++++++++++++++- controller/part.cc | 4 +-- controller/ui.cc | 53 +++++++++++++++++++-------- controller/ui_pages/os_info_page.cc | 2 +- tools | 2 +- voicecard/voice.cc | 21 ++++++----- voicecard/voice.h | 6 ++-- 10 files changed, 119 insertions(+), 35 deletions(-) diff --git a/controller/controller.cc b/controller/controller.cc index ad2384c..33ba01a 100644 --- a/controller/controller.cc +++ b/controller/controller.cc @@ -113,6 +113,7 @@ void Init() { system_settings.Init(true); } multi.Init(ui.shifted()); + ui.ShowPreviousPage(); storage.Init(); } diff --git a/controller/controller.h b/controller/controller.h index 430e7a8..8fb7ac4 100755 --- a/controller/controller.h +++ b/controller/controller.h @@ -34,7 +34,7 @@ const uint8_t kNumArpeggiatorPatterns = 22; const uint8_t kNumParts = 6; const uint8_t kNumVoices = 6; -const uint8_t kSystemVersion = 0x10; +const uint8_t kSystemVersion = 0x11; } // namespace ambika diff --git a/controller/multi.cc b/controller/multi.cc index 7f45f22..4616543 100755 --- a/controller/multi.cc +++ b/controller/multi.cc @@ -61,8 +61,8 @@ static const prog_MultiData init_settings PROGMEM = { 1, 22, 0, 1, 42, 0, - // Padding. - 0, 0, 0, 0, + // Padding: [0]=EEPROM version marker, [1-3]=unused. + 0x26, 0, 0, 0, }; /* static */ @@ -75,7 +75,8 @@ void Multi::Init(bool force_reset) { InitSettings(INITIALIZATION_DEFAULT); Storage::WriteMultiToEeprom(); } else { - if (!Storage::LoadMultiFromEeprom()) { + if (!Storage::LoadMultiFromEeprom() || data_.padding2[0] != 0x26) { + // EEPROM corrupt or from older firmware version — reset. InitSettings(INITIALIZATION_DEFAULT); Storage::WriteMultiToEeprom(); } else { diff --git a/controller/parameter.cc b/controller/parameter.cc index c56e015..ad55289 100755 --- a/controller/parameter.cc +++ b/controller/parameter.cc @@ -209,6 +209,8 @@ void Parameter::PrintValue(uint8_t value, char* buffer, uint8_t width) const { case UNIT_MIDI_CHANNEL: if (value == 0) { text = STR_RES_OMNI; + } else { + text = 0; } } @@ -249,6 +251,58 @@ void Parameter::PrintValue(uint8_t value, char* buffer, uint8_t width) const { return; } + // Engine-aware mod destination names for the mixer/osc destinations (0-11). + // Destinations 12+ (filter, envelope, vca) are the same across all engines. + if (unit == UNIT_MODULATION_DESTINATION && value < 12) { + uint8_t engine = multi.part(ui.state().active_part).raw_patch_data()[106]; + // Packed 5-char short names and 7-char long names per engine. + // Order: prm1 prm2 osc1 osc2 1+2 vibr mix xmod nois sub fuzz crsh + static const prog_char dst_classic_short[] PROGMEM = + "prm1\0" "prm2\0" "osc1\0" "osc2\0" "1+2 \0" "vibr\0" + "mix \0" "xmod\0" "nois\0" "sub \0" "fuzz\0" "crsh\0"; + static const prog_char dst_fm_short[] PROGMEM = + "algo\0" "wave\0" "osc1\0" "osc2\0" "1+2 \0" "vibr\0" + "rat3\0" "fin3\0" "lvl2\0" "lvl1\0" "lvl3\0" "lvl4\0"; + static const prog_char dst_ks_short[] PROGMEM = + "damp\0" "colr\0" "ptch\0" "osc2\0" "1+2 \0" "vibr\0" + "body\0" "xmod\0" "stif\0" "emix\0" "feed\0" "sync\0"; + static const prog_char dst_wc_short[] PROGMEM = + "fold\0" "sym \0" "ptch\0" "osc2\0" "1+2 \0" "vibr\0" + "bias\0" "colr\0" "envf\0" "gain\0" "sub \0" "sync\0"; + static const prog_char dst_classic_long[] PROGMEM = + "param1\0" "param2\0" "osc 1 \0" "osc 2 \0" "osc1+2\0" "vibrat\0" + "mix \0" "xmod \0" "noise \0" "subosc\0" "fuzz \0" "crush \0"; + static const prog_char dst_fm_long[] PROGMEM = + "algo \0" "waves \0" "osc 1 \0" "osc 2 \0" "osc1+2\0" "vibrat\0" + "rat 3 \0" "fine 3\0" "lvl 2 \0" "lvl 1 \0" "lvl 3 \0" "lvl 4 \0"; + static const prog_char dst_ks_long[] PROGMEM = + "dampin\0" "color \0" "pitch \0" "osc 2 \0" "osc1+2\0" "vibrat\0" + "body \0" "xmod \0" "stiff \0" "ensmix\0" "feedbk\0" "sync \0"; + static const prog_char dst_wc_long[] PROGMEM = + "fold \0" "symm \0" "pitch \0" "osc 2 \0" "osc1+2\0" "vibrat\0" + "bias \0" "color \0" "env>fl\0" "gain \0" "sub \0" "sync \0"; + const prog_char* table; + if (width > 5) { + static const prog_char* const long_tables[] PROGMEM = { + dst_classic_long, dst_fm_long, dst_ks_long, dst_wc_long + }; + table = (engine < ENGINE_LAST) + ? (const prog_char*)pgm_read_word(&long_tables[engine]) + : dst_classic_long; + memcpy_P(buffer, table + value * 7, width < 7 ? width : 7); + } else { + static const prog_char* const short_tables[] PROGMEM = { + dst_classic_short, dst_fm_short, dst_ks_short, dst_wc_short + }; + table = (engine < ENGINE_LAST) + ? (const prog_char*)pgm_read_word(&short_tables[engine]) + : dst_classic_short; + memcpy_P(buffer, table + value * 5, width < 5 ? width : 5); + } + AlignRight(buffer, width); + return; + } + // Mod source names — packed PROGMEM tables, 5 chars each (short) / 7 chars each (long). // Order matches ModulationSource enum. if (unit == UNIT_MODULATION_SOURCE) { @@ -728,7 +782,7 @@ static const prog_Parameter parameters[kNumParameters] PROGMEM = { // 43 { PARAMETER_LEVEL_PART, PRM_PART_OCTAVE, - UNIT_INT8, -2, 2, + UNIT_INT8, -4, 4, 1, 0, 0xff, 0xff, STR_RES_OCTV, STR_RES_OCTAVE, STR_RES_PART }, diff --git a/controller/part.cc b/controller/part.cc index d3df624..7c20037 100755 --- a/controller/part.cc +++ b/controller/part.cc @@ -42,7 +42,7 @@ static const prog_Patch init_patch PROGMEM = { 0, 0, 4, 0, // osc1: wav1|wav2=0(sine), wav3|wav4=0, range=R_100, det=0 // Mixer (FM mode: ratios, levels) - 4, 0, 4, 0, 127, 120, 115, 112, + 4, 0, 4, 0, 20, 10, 10, 1, // Filter 96, 0, 0, 0, 0, 0, 24, 0, @@ -91,7 +91,7 @@ static const prog_PartData init_part PROGMEM = { 120, // Octave and tuning - 0, 0, 0, 0, + -1, 0, 0, 0, // Legato, portamento, seq mode 0, 0, 0, diff --git a/controller/ui.cc b/controller/ui.cc index 4751ff1..84b761c 100755 --- a/controller/ui.cc +++ b/controller/ui.cc @@ -339,31 +339,56 @@ void Ui::DoEvents() { if (e.control_id == 0) { if (s1_held_) { // Mode select: cycle engine type via padding[2]. + // Each engine gets its own slot for the shared patch bytes + // (offsets 0-15, 104) so switching engines preserves state. static const uint8_t pages[] PROGMEM = { PAGE_OSCILLATORS, PAGE_FM4OP, PAGE_KS_PLUCK, PAGE_WESTCOAST }; + + // Shared byte offsets: 0-15 plus 104 (feedback). + static const uint8_t kSharedCount = 17; + static const uint8_t shared_offsets[kSharedCount] PROGMEM = { + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, + 104 + }; + + // Per-engine state slots, initialized with defaults. + // [engine][byte index] — 4 engines × 17 bytes = 68 bytes. + static uint8_t engine_state[ENGINE_LAST][kSharedCount] = { + // ENGINE_CLASSIC: saw osc, centered mix, everything else 0. + { WAVEFORM_SAW, 0, 0, 0, WAVEFORM_NONE, 0, 0, 0, + 32, 0, 0, 0, 0, 0, 0, 0, 0 }, + // ENGINE_FM4OP: algo 1, 1:1 ratios, gentle levels. + { WAVEFORM_SAW, 1, 4, 0, 0, 0, 4, 0, + 4, 0, 4, 0, 20, 10, 10, 1, 0 }, + // ENGINE_KS_PLUCK: moderate damping, centered body/position. + { 0, 40, 0, 0, 0, 64, 64, 64, + 64, 0, 0, 0, 0, 0, 0, 0, 0 }, + // ENGINE_WESTCOAST: sine, moderate fold. + { 0, 80, 0, 0, 0, 64, 0, 0, + 64, 0, 0, 0, 0, 0, 0, 0, 0 }, + }; + uint8_t* patch = multi.mutable_part( state_.active_part)->mutable_raw_patch_data(); - int8_t idx = patch[106]; // padding[2] = engine type + uint8_t prev = patch[106]; // current engine + int8_t idx = prev; idx += (static_cast(e.value) > 0) ? 1 : -1; if (idx < 0) idx = ENGINE_LAST - 1; if (idx >= ENGINE_LAST) idx = 0; + + // Save current engine's state. + for (uint8_t i = 0; i < kSharedCount; ++i) { + engine_state[prev][i] = patch[pgm_read_byte(&shared_offsets[i])]; + } + // Load target engine's state. patch[106] = idx; - // When switching to classic, reset mixer and waveform. - if (idx == ENGINE_CLASSIC) { - if (patch[0] >= WAVEFORM_FM4OP) { - patch[0] = WAVEFORM_SAW; - } - patch[8] = 32; - patch[9] = 0; - patch[10] = 0; - patch[11] = 0; - patch[12] = 0; - patch[13] = 0; - patch[14] = 0; - patch[15] = 0; + for (uint8_t i = 0; i < kSharedCount; ++i) { + patch[pgm_read_byte(&shared_offsets[i])] = engine_state[idx][i]; } + multi.mutable_part(state_.active_part)->TouchPatch(); ShowPage(static_cast( pgm_read_byte(&pages[idx]))); diff --git a/controller/ui_pages/os_info_page.cc b/controller/ui_pages/os_info_page.cc index e443eb6..2cd646f 100755 --- a/controller/ui_pages/os_info_page.cc +++ b/controller/ui_pages/os_info_page.cc @@ -133,7 +133,7 @@ void OsInfoPage::UpdateScreen() { display.line_buffer(0)[0] = ' '; char* buffer = display.line_buffer(0) + 1; memcpy_P(&buffer[0], PSTR("carcosa"), 7); - memcpy_P(&buffer[8], PSTR("v2.05"), 5); + memcpy_P(&buffer[8], PSTR("v2.06"), 5); memcpy_P(&buffer[15], PSTR("port 1 device ?"), 15); Word version_number = voicecard_tx.GetVersion(active_control_); diff --git a/tools b/tools index 0772f99..b2224dc 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit 0772f9954d26469e580d31db0e8a8b3c4c555bee +Subproject commit b2224dcd689da41135bcb8a708bdd1acc940ff25 diff --git a/voicecard/voice.cc b/voicecard/voice.cc index 0afcdf2..4062301 100755 --- a/voicecard/voice.cc +++ b/voicecard/voice.cc @@ -196,8 +196,8 @@ static const prog_Patch init_patch PROGMEM = { 0, 0, 0, 0, 0, 0, - // Padding - 0, 0, 0, 0, 0, 0, 0, 0, + // Padding: [0]=feedback, [1]=unused, [2]=engine, [3-7]=unused + 0, 0, ENGINE_FM4OP, 0, 0, 0, 0, 0, // Extra envelope-LFO settings (env4-7: ADSR + LFO shape/rate/curve/retrigger) 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, @@ -257,8 +257,8 @@ void Voice::Trigger(uint16_t note, uint8_t velocity, uint8_t legato) { uint8_t slop = patch_.padding[1]; if (slop > 0) { int8_t rnd = static_cast(Random::GetByte()); - // Scale: slop=127 gives ±~8 units (about ±0.5 semitone). - int16_t offset = (static_cast(rnd) * slop) >> 8; + // Scale: slop=127 gives ±~64 units (about ±0.5 semitone). + int16_t offset = (static_cast(rnd) * slop) >> 7; pitch_target_ += offset; } @@ -454,7 +454,7 @@ inline void Voice::UpdateDestinations() { if (slop > 0) { env_slop = static_cast( (static_cast(modulation_sources_[MOD_SRC_RANDOM]) - 128) * - slop) >> 8; + slop) >> 6; } for (int i = 0; i < kNumEnvLfoSlots; ++i) { @@ -503,11 +503,16 @@ inline void Voice::RenderOscillators() { uint8_t engine = patch_.padding[2]; // Reset engine state when engine type changes. + // Only init the active engine — karplus_.Init() clears a 384-byte delay + // line which is too expensive to run unconditionally in the audio ISR. if (engine != last_engine_) { last_engine_ = engine; - fm4op_.Init(); - karplus_.Init(); - westcoast_.Init(); + switch (engine) { + case ENGINE_FM4OP: fm4op_.Init(); break; + case ENGINE_KS_PLUCK: karplus_.Init(); break; + case ENGINE_WESTCOAST: westcoast_.Init(); break; + default: break; + } } // --- 4-op FM mode --- diff --git a/voicecard/voice.h b/voicecard/voice.h index 689c414..1fea07f 100755 --- a/voicecard/voice.h +++ b/voicecard/voice.h @@ -74,11 +74,9 @@ class Voice { // Move this voice to the release stage. static void Kill() { TriggerEnvelope(DEAD); } - // Reset all synthesis engine state (called after bulk patch load). + // Force engine re-init on next render (called after bulk patch load). + // The actual Init() runs in RenderOscillators for the active engine only. static void ResetEngines() { - fm4op_.Init(); - karplus_.Init(); - westcoast_.Init(); last_engine_ = 0xFF; } From 15cae28e7bcc1d53d89c41893f6aa15ddf3911cb Mon Sep 17 00:00:00 2001 From: Joseph Martin Giralt Date: Tue, 21 Apr 2026 18:09:51 +0200 Subject: [PATCH 67/67] Update docs for v2.06: engine isolation, mod names, slop, EEPROM reset Co-Authored-By: Claude Opus 4.6 (1M context) --- CARCOSA.md | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/CARCOSA.md b/CARCOSA.md index e64380b..cf7fa8a 100644 --- a/CARCOSA.md +++ b/CARCOSA.md @@ -1,4 +1,4 @@ -# Carcosa v2.05 +# Carcosa v2.06 ## Custom firmware for the Ambika polysynth Carcosa replaces the stock Ambika firmware with a focused set of synthesis engines, expanded envelope capabilities, and a streamlined interface. @@ -76,11 +76,11 @@ TX81Z-style frequency modulation with 4 operators. **Operator waveforms:** sin, half, abs, qtr, habs, tri, pls, saw -**Modulation depth:** Operator levels use a TX81Z-style exponential curve (0.75 dB/step). Level 0 = silence, 64 = barely audible, 100 = moderate, 127 = full. The useful modulation range is 90-127. Per-operator envelopes (env 4-7) multiply the level after the exponential curve, matching the TX81Z signal chain. +**Modulation depth:** Operator levels use a TX81Z-style exponential curve (0.75 dB/step). Level 0 = silence, 64 = barely audible, 100 = moderate, 127 = full. The curve is steep above ~90 — start with low values (10-30) and bring up gradually. Per-operator envelopes (env 4-7) multiply the level after the exponential curve, matching the TX81Z signal chain. **Feedback:** 16-bit internal state creates evolving, non-repeating oscillation (similar to the TX81Z's 14-bit feedback dynamics). Higher values add saw-like harmonics to operator 4. -The mod matrix destination `prm1` controls total FM depth and can be modulated by envelopes/LFOs for dynamic timbral control. +The mod matrix shows FM-specific destination names when in FM mode: `algo`, `lvl1`-`lvl4`, `rat3`, `fin3`, etc. Route envelopes/LFOs to operator levels for dynamic FM timbres. #### Karplus-Strong (pluck) @@ -110,7 +110,7 @@ Physical model of a plucked string. A noise burst excites a tuned delay line wit | 5 | stif | 0-127 | String stiffness (inharmonic partials, scales with pitch) | | 6 | feed | 0-127 | Sustain (reduces damping loss for longer ring, never self-oscillates) | -**Damping** uses an IIR low-pass filter that sweeps from bright metallic ring (0) to dark muted thud (127). The `prm1` mod destination controls damping and can be modulated for expressive playing. +**Damping** uses an IIR low-pass filter that sweeps from bright metallic ring (0) to dark muted thud (127). The mod matrix shows KS-specific destination names: `damp`, `body`, `stif`, `emix`, `feed`, etc. #### West Coast (wcoast) @@ -141,7 +141,7 @@ Buchla-style wavefolder with built-in FM and sub-oscillator. **Wavefolder** uses a quadratic gain curve (1x at fold=0, 33x at fold=127) creating up to 16 folds — matching real Buchla 259 harmonic density. The **color** filter is a 2-pole IIR (12dB/oct) that sweeps from dark fundamental-only to bright full harmonics. -Route an envelope to `prm1` (fold depth) for the classic west coast behavior where timbre and amplitude are linked. The `env` parameter on page 2 does this directly — the signature Buchla pluck sound. +Route an envelope to `fold` (fold depth) for the classic west coast behavior where timbre and amplitude are linked. The `env` parameter on page 2 does this directly — the signature Buchla pluck sound. The mod matrix shows WC-specific destination names: `fold`, `sym`, `bias`, `colr`, `envf`, `gain`, etc. #### Waveshaping (wshape) @@ -170,6 +170,10 @@ Drives a sine wave through a nonlinear waveshaper. Works as a normal oscillator Mode select also works from inside any special mode page. Pressing any other button exits mode select automatically. +### Engine isolation + +Each engine has its own state slot. When you switch engines, your current settings are saved and the target engine's settings are restored. Tweak an FM patch, switch to classic, switch back — your FM tweaks are right where you left them. No parameter bleed between engines. + ### Inside special mode pages - **S1 press**: Toggle between page 1 and page 2 @@ -217,7 +221,7 @@ The **slop** parameter on the part page (knob 3) adds per-note random variation - **Low values (10-30)**: Subtle warmth, slight detuning between voices - **High values (60-127)**: Wobbly, unstable, heavily vintage -Each note-on generates fresh random offsets. In a chord, each voice drifts differently. +Each note-on generates a fresh random pitch offset (gentle, up to ~0.5 semitone at max). Envelope attack, decay, and release also vary per voice with a stronger effect — at high slop values, each voice in a chord has noticeably different envelope timing. --- @@ -249,7 +253,11 @@ Set knobs to 0 to lock that section. Turn up for more variation. Click the encod 3. **Hold S8 during power-on** to flash the controller 4. After reboot, go to OS info page, select each voicecard port (1-6), press S4 to flash -Flash the controller first, then the voicecards. +Flash the controller first, then all 6 voicecards. + +### EEPROM auto-reset + +When upgrading from an older firmware version, Carcosa automatically detects stale EEPROM data and resets to factory defaults on first boot. This ensures the correct init patch, voice allocation, and engine settings are loaded. No manual factory reset needed. ### Recovery @@ -265,16 +273,16 @@ To revert to stock Ambika firmware, place the original `AMBIKA.BIN` and `VOICE*. | | Voicecard (ATmega328p) | Controller (ATmega644p) | |---|---|---| -| Flash | 32,228 / 32,256 (99.9%) | ~60,000 / 65,536 (91%) | -| RAM | 1,521 / 2,048 (74%) | ~3,900 / 4,096 (95%) | +| Flash | 32,062 / 32,768 (97.8%) | 60,822 / 65,536 (92.8%) | +| RAM | 1,521 / 2,048 (74%) | 3,706 / 4,096 (90%) | ### Changes from Stock Ambika **Removed:** CZ synthesis (9 variants), old 2-op FM, 8-bit land, dirty PWM, vowel synthesis, wavetable oscillators (16 + wavequence), step sequencer. -**Added:** 4-op FM, Karplus-Strong, west coast wavefolder, waveshaping, looping envelopes, analog slop, smart randomizer, dedicated UI pages for special modes. +**Added:** 4-op FM, Karplus-Strong, west coast wavefolder, waveshaping, looping envelopes, analog slop, smart randomizer, dedicated UI pages for special modes, per-engine mod destination names, per-engine state isolation, EEPROM auto-reset, extended octave range (-4/+4). -**Bug fixes:** Note stack uninitialized variables, transient generator off-by-one, SPI bulk write bounds check, page group initialization size. +**Bug fixes:** MIDI channel display (showed garbage for channels 1-16), engine switch parameter bleed, voicecard frame drops on engine switch, boot page routing, note stack uninitialized variables, transient generator off-by-one, SPI bulk write bounds check, page group initialization size. ### Patch Compatibility