waveywaves
11 hours ago
Hi HN, I built Contrapunk because I wanted to play guitar and hear counterpoint harmonies generated in real-time. It takes audio from your guitar, MIDI player or your computer keyboard and generates harmony voices that follow counterpoint rules to generate harmonies. You can choose the key you would like to improvise/play in and the voice leading style and which part of the harmony you would like to play as, as well.
macOS DMG: https://github.com/contrapunk-audio/contrapunk/releases/tag/...
Source: https://github.com/contrapunk-audio/contrapunk (do open any issues if you have any)
Would love feedback on the DSP approach and the harmony algorithms. I am also looking at training a ML model for better realtime guitar to midi detection. I believe that will take some time.
Slow_Hand
7 hours ago
Cool idea!
I've got a few thoughts for features, if you're open to them:
1. Ability to specify where your "played" voice resides in the voicing: As the bass note, as an inner voice, or as the top line.
2. Options for first species, second species, third, florid, etc counterpoint for each of the generated voices. Ex: You play a single note and the upper voice plays two notes for every one of yours, etc, etc.
3. If you want to get real fancy, make the generated voices perform a canon of your played notes.
waveywaves
5 hours ago
Have you been able to try it as well would love to hear what you think! Coming back to the features, regarding 1. you can already choose between soprano, alto, tenor or bass. I have still filed an issue for this, will help me remember to take vet this feature. Sometimes it's not as strict as it should be but that's also something I need to work on. Regarding 2. it's a good idea, helps you be in control of the kind of counterpoint you are doing, filed an issue for the same. Please feel free to comment on the issue. 3. is just feels is a little goofy as well I love it. I haver filed an issue for this as well check https://github.com/contrapunk-audio/contrapunk/issues/
chrisweekly
9 hours ago
"Realtime" as in "while playing guitar" has some pretty challenging latency requirements. Even if your solution is optimal, hardware specs will play a meaningful role. I'd be really interested if you've solved for this e2e.
waveywaves
5 hours ago
Yes, latency was the main problem to solve here. Because of which I opted for Rust. The pipeline is:
- 128-sample cpal audio buffers (~2.7ms at 48kHz) - Single-cycle pitch detection - 2-frame McLeod pitch voting for confirmation - Entire DSP pipeline is Rust, pre-allocated ring buffers with minimal heap pressure
The e2e from pluck to MIDI "note-on" signal, is under 10ms on an M-series Mac. Hardware matters for sure so an audio interface with low-latency drivers (I use an Audient iD14) helped a lot. The web version (app.contrapunk.com) adds AudioWorklet latency on top, so the native Mac app is noticeably tighter. I am still working on figuring out how to have lesser noise and pitch jitter in the final output. Also this works really well for higher notes, bass not so much right now. Still need to figure out how to handle harmonics better. I have created this issue for you for now, let me know if you would like to add anything else to this as well. https://github.com/contrapunk-audio/contrapunk/issues/6.
seertaak
3 hours ago
Gradus ad Parnassum! What a cool idea, and the fact it's counterpoint gives you a nice little time buffer for any DSP. Super cool
waveywaves
3 hours ago
Thank you! The idea is not completely mine, I have to give thanks to Abhinav Arora who had this idea initially during the ADCx music hackathon. Kudos to him! Also love the phrase Gradus ad Parnassum! Maybe this should be the motto of contrapunk :)
seertaak
3 hours ago
Contrapunk is a cool name though.
How are you finding rust for audio development? I have a background in pro audio, and both for the audio and GPU render threads, I used a lot of arena allocators and ring buffers. Do you find yourself fighting with rust's strict semantics?
waveywaves
2 hours ago
This is callback heavy audio code so this was the bigger problem for me mainly and learning about lifetimes was a pain initially. cpal's stream callback wants 'static which means you can't just pass references around. You end up using channels (crossbeam / std::sync::mpsc) between the audio thread and everything else. Once I structured around that it got smoother. I also got a lot of help from AI to understand and reimplement a lot of the parts for this as you can tell from the commit messages.