diff --git a/app/instrument/[id]/components/audio-preview.tsx b/app/instrument/[id]/components/audio-preview.tsx index a611ee1..063eeeb 100644 --- a/app/instrument/[id]/components/audio-preview.tsx +++ b/app/instrument/[id]/components/audio-preview.tsx @@ -15,10 +15,11 @@ import { } from "@/components/ui/select"; import { useFmSynthesizer } from "@/contexts/synth"; import type { FmInstrument } from "@/features/instrument/types"; -import { AVAILABLE_CHIPS } from "@/features/preview/consts"; +import { AVAILABLE_CHIP_MAP } from "@/features/preview/consts"; import { clampOctave, withinOctave } from "@/features/preview/octave"; import { type Pitch, pitchToString } from "@/features/preview/pitch"; import type { AvailableChip } from "@/features/preview/types"; +import { useSynthSettings } from "@/stores/synth-settings"; import { iota } from "@/utils/range"; const DISPLAYED_OCTAVE_RANGE = 4; @@ -42,7 +43,7 @@ export function AudioPreview({ }: { instrument: FmInstrument; }): React.JSX.Element { - const [playbackChip, setPlaybackChip] = useState("OPNA"); + const { playbackChip, setPlaybackChip } = useSynthSettings(); const [octaveOffset, setOctaveOffset] = useState(3); // Key: pointerId, Value: ActiveNote @@ -66,6 +67,12 @@ export function AudioPreview({ }; }, [synthContext]); + function handlePlaybackChipValueChanged(value: string) { + const chipType = value as AvailableChip; + synthContext?.changeChip(chipType); + setPlaybackChip(chipType); + } + async function noteOn(pitch: Pitch, pointerId: number) { await synthContext?.keyOn(pitch, pointerId); } @@ -161,17 +168,19 @@ export function AudioPreview({ diff --git a/features/preview/consts.ts b/features/preview/consts.ts index dd2148a..1bb1b5e 100644 --- a/features/preview/consts.ts +++ b/features/preview/consts.ts @@ -1,3 +1,13 @@ import type { AvailableChip } from "./types"; -export const AVAILABLE_CHIPS: AvailableChip[] = ["OPNA"] as const; +/** + * Map of available sound chips and their corresponding values. + * The values must match the ones defined in WASM. + */ +export const AVAILABLE_CHIP_MAP: ReadonlyMap = new Map< + AvailableChip, + number +>([ + ["OPNA", 0], + ["OPN2", 1], +]); diff --git a/features/preview/synth.ts b/features/preview/synth.ts index 0600dec..8aa2ec4 100644 --- a/features/preview/synth.ts +++ b/features/preview/synth.ts @@ -1,6 +1,8 @@ import { useEffect, useRef } from "react"; import type { FmInstrument } from "../instrument/types"; +import { AVAILABLE_CHIP_MAP } from "./consts"; import type { Pitch } from "./pitch"; +import type { AvailableChip } from "./types"; export type Synthesizer = { audioContext: AudioContext; @@ -12,6 +14,7 @@ export type Synthesizer = { keyOff: (id: number) => void; setInstrument: (instrument: FmInstrument) => void; reset: () => void; + changeChip: (chip: AvailableChip) => void; }; const RING_BUFFER_SIZE = 8192; @@ -44,6 +47,11 @@ type ResetWorkerRequestMessage = { type: "reset"; }; +type ChangeChipWorkerRequestMessage = { + type: "changeChip"; + chipId: number; +}; + type LoadWasmWorkerRequestMessage = { type: "loadWasm"; loaderUrl: string; @@ -234,6 +242,18 @@ export function useSynthesizer() { type: "reset", } satisfies ResetWorkerRequestMessage); }, + + changeChip: (chip) => { + const chipId = AVAILABLE_CHIP_MAP.get(chip); + if (chipId === undefined) { + return; + } + + wasmWorker.postMessage({ + type: "changeChip", + chipId, + } satisfies ChangeChipWorkerRequestMessage); + }, }; })(); diff --git a/features/preview/types.ts b/features/preview/types.ts index b54f8ec..3731733 100644 --- a/features/preview/types.ts +++ b/features/preview/types.ts @@ -2,4 +2,4 @@ import type { RecommendedChip } from "../instrument/models"; type Subset = U; -export type AvailableChip = Subset; +export type AvailableChip = Subset; diff --git a/public/wasm/synth.wasm b/public/wasm/synth.wasm index 1d8a423..09bf883 100644 Binary files a/public/wasm/synth.wasm and b/public/wasm/synth.wasm differ diff --git a/public/worker/worker.js b/public/worker/worker.js index ba54c99..ff2e79d 100644 --- a/public/worker/worker.js +++ b/public/worker/worker.js @@ -2,6 +2,8 @@ importScripts("/wasm/synth.js"); let wasm; +let currentInstrument; + /** @type {Int32Array} */ let control; const SAB_CONTROL_SIZE = 2; @@ -127,6 +129,7 @@ function handleSetInstrument(instrument) { } wasm.setInstrument(instrument); + currentInstrument = instrument; } /** @@ -197,6 +200,20 @@ function handleGenerate() { Atomics.store(control, SAB_WRITE_POSITION_INDEX, pos); } +function handleChangeChip(id) { + if (!wasm) { + self.postMessage(WASM_NOT_LOADED_MESSAGE); + return; + } + + wasm.changeChip(id); + messageQueue.length = 0; + + if (currentInstrument) { + wasm.setInstrument(currentInstrument); + } +} + self.onmessage = async ({ data }) => { switch (data.type) { case "loadWasm": @@ -217,6 +234,10 @@ self.onmessage = async ({ data }) => { handleReset(); break; + case "changeChip": + handleChangeChip(data.chipId); + break; + default: break; } diff --git a/stores/search.ts b/stores/search.ts index a93f59d..3b0d8a3 100644 --- a/stores/search.ts +++ b/stores/search.ts @@ -2,10 +2,10 @@ import { create } from "zustand"; export type ViewMode = "grid" | "list"; -interface SearchStore { +type SearchStore = { viewMode: ViewMode; setViewMode: (mode: ViewMode) => void; -} +}; export const useSearchStore = create((set) => ({ viewMode: "grid", diff --git a/stores/synth-settings.ts b/stores/synth-settings.ts new file mode 100644 index 0000000..3f37375 --- /dev/null +++ b/stores/synth-settings.ts @@ -0,0 +1,12 @@ +import { create } from "zustand"; +import type { AvailableChip } from "@/features/preview/types"; + +type SynthSettings = { + playbackChip: AvailableChip; + setPlaybackChip: (chip: AvailableChip) => void; +}; + +export const useSynthSettings = create((set) => ({ + playbackChip: "OPNA", + setPlaybackChip: (chip) => set({ playbackChip: chip }), +})); diff --git a/wasm-src/src/chip/ym2612.cpp b/wasm-src/src/chip/ym2612.cpp new file mode 100644 index 0000000..f7f37cc --- /dev/null +++ b/wasm-src/src/chip/ym2612.cpp @@ -0,0 +1,235 @@ +// SPDX-FileCopyrightText: 2025 Rerrah +// SPDX-License-Identifier: MIT + +#include "ym2612.hpp" + +#include +#include + +#include "ymfm_opn.h" + +namespace { +ymfm::ymfm_interface ymfm_common_interface{}; + +constexpr std::uint32_t kClock{7'670'453}; + +std::uint16_t PitchToBlockAndFNumber(const synth::Pitch& pitch) { + static std::int16_t fnum_table[12] = { + 0x0284, 0x02AA, 0x02D3, 0x02FE, 0x032B, 0x035B, + 0x038E, 0x03C5, 0x03FE, 0x043B, 0x047B, 0x04BF, + }; + + return (pitch.octave << 11) | fnum_table[static_cast(pitch.semitone)]; +} + +constexpr std::uint8_t kSlotFlags = 0xf0; +constexpr std::uint8_t kHighChannelKeyOnFlag = 0x04; +constexpr std::uint8_t kCenterPanning = 0xc0; + +class Ym2612InstrumentView { + public: + explicit Ym2612InstrumentView(const synth::FmInstrument& instrument) noexcept + : instrument_(instrument) {} + + std::uint8_t value_fb_al() const noexcept { + return ((instrument_.fb & 0x7) << 3) | (instrument_.al & 0x7); + } + + std::uint8_t value_lfo_freq() const noexcept { + return instrument_.lfo_freq & 0xf; + } + + std::uint8_t value_ams_pms() const noexcept { + return (instrument_.ams & 0x3) << 4 | (instrument_.pms & 0x7); + } + + class OperatorView; + + OperatorView operator_view(std::size_t index) const { + return OperatorView(instrument_.op.at(index)); + } + + class OperatorView { + public: + std::uint8_t value_dt_ml() const noexcept { + return (op_.dt & 0x7) << 4 | (op_.ml & 0xF); + } + + std::uint8_t value_ks_ar() const noexcept { + return (op_.ks & 0x3) << 6 | (op_.ar & 0x1F); + } + + std::uint8_t value_am_dr() const noexcept { + return (op_.am ? 0x80 : 0) | (op_.dr & 0x1F); + } + + std::uint8_t value_sl_rr() const noexcept { + return (op_.sl & 0xF) << 4 | (op_.rr & 0xF); + } + + std::uint8_t value_sr() const noexcept { return op_.sr & 0x1F; } + + std::uint8_t value_ssg_eg() const noexcept { return op_.ssg_eg & 0xF; } + + std::uint8_t value_tl() const noexcept { return op_.tl & 0x7F; } + + private: + explicit OperatorView(const synth::FmOperator& op) noexcept : op_(op) {} + + const synth::FmOperator& op_; + + friend OperatorView Ym2612InstrumentView::operator_view( + std::size_t index) const; + }; + + private: + const synth::FmInstrument& instrument_; +}; +} // namespace + +namespace synth::chip { +Ym2612::Ym2612() + : chip_(std::make_unique(ymfm_common_interface)) { + Reset(); +} + +Ym2612::~Ym2612() = default; + +bool Ym2612::Reset() { + if (!chip_) { + return false; + } + + chip_->reset(); + + WriteLow(0x27, 0); // Disable FM 3ch extended mode + + for (std::uint8_t ch = 0; ch < 3; ++ch) { + WriteLow(0xB4 + ch, kCenterPanning); + WriteHigh(0xB4 + ch, kCenterPanning); + } + + return true; +} + +std::uint32_t Ym2612::sampling_rate() const { + return chip_ ? chip_->sample_rate(kClock) : 0; +} + +bool Ym2612::KeyOn(std::uint8_t ch, const Pitch& pitch) { + if (!chip_) { + return false; + } + + std::uint16_t block_and_fnum = PitchToBlockAndFNumber(pitch); + + if (ch < 3) { + WriteLow(0xA4 + ch, block_and_fnum >> 8); + WriteLow(0xA0 + ch, block_and_fnum & 0xFF); + WriteLow(0x28, kSlotFlags | ch); + } else if (ch < 6) { + const auto high_ch = ch - 3; + WriteHigh(0xA4 + high_ch, block_and_fnum >> 8); + WriteHigh(0xA0 + high_ch, block_and_fnum & 0xFF); + WriteLow(0x28, kSlotFlags | kHighChannelKeyOnFlag | high_ch); + } + + return true; +} + +bool Ym2612::KeyOff(std::uint8_t ch) { + if (!chip_) { + return false; + } + + if (ch < 3) { + WriteLow(0x28, ~kSlotFlags & ch); + } else if (ch < 6) { + const auto high_ch = ch - 3; + WriteLow(0x28, ~kSlotFlags & (kHighChannelKeyOnFlag | high_ch)); + } + + return true; +} + +bool Ym2612::SetInstrument(const FmInstrument& instrument) { + if (!chip_) { + return false; + } + + WriteLow(0x22, instrument.lfo_freq); + + const auto write_low_and_high = [this](std::uint8_t address, + std::uint8_t data) { + WriteLow(address, data); + WriteHigh(address, data); + }; + + Ym2612InstrumentView inst_view{instrument}; + + for (std::uint8_t ch_offset = 0; ch_offset < 3; ++ch_offset) { + write_low_and_high(0xB0 + ch_offset, inst_view.value_fb_al()); + + write_low_and_high(0xB4 + ch_offset, + kCenterPanning | inst_view.value_ams_pms()); + + static const std::array kOpOffset{0x0, 0x8, 0x4, 0xC}; + for (std::size_t i = 0; i < 4; ++i) { + std::uint8_t op_offset = kOpOffset.at(i); + auto op_view = inst_view.operator_view(i); + + const auto write_op = [write_low_and_high, ch_offset, op_offset]( + std::uint8_t address, std::uint8_t data) { + write_low_and_high(address + ch_offset + op_offset, data); + }; + + write_op(0x30, op_view.value_dt_ml()); + write_op(0x40, op_view.value_tl()); + write_op(0x50, op_view.value_ks_ar()); + write_op(0x60, op_view.value_am_dr()); + write_op(0x70, op_view.value_sr()); + write_op(0x80, op_view.value_sl_rr()); + write_op(0x90, op_view.value_ssg_eg()); + } + } + + return true; +} + +void Ym2612::WriteLow(std::uint8_t address, std::uint8_t data) { + // if (!chip_) { + // return; + // } + + chip_->write_address(address); + chip_->write_data(data); +} + +void Ym2612::WriteHigh(std::uint8_t address, std::uint8_t data) { + // if (!chip_) { + // return; + // } + + chip_->write_address_hi(address & 0xFF); + chip_->write_data_hi(data); +} + +bool Ym2612::Generate(float* left_buffer, float* right_buffer, + std::uint32_t num_samples) { + if (!chip_) { + return false; + } + + ymfm::ym2612::output_data output; + for (std::uint32_t i = 0; i < num_samples; ++i) { + chip_->generate(&output); + + static constexpr float kNormalizeFactor = + std::numeric_limits::max(); + left_buffer[i] = output.data[0] / kNormalizeFactor; + right_buffer[i] = output.data[1] / kNormalizeFactor; + } + + return true; +} +} // namespace synth::chip diff --git a/wasm-src/src/chip/ym2612.hpp b/wasm-src/src/chip/ym2612.hpp new file mode 100644 index 0000000..d871bae --- /dev/null +++ b/wasm-src/src/chip/ym2612.hpp @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2025 Rerrah +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include + +#include "../instrument.hpp" +#include "../pitch.hpp" + +namespace ymfm { +class ym2612; +} + +namespace synth::chip { +class Ym2612 { + public: + Ym2612(); + ~Ym2612(); + + bool Reset(); + + std::uint32_t sampling_rate() const; + std::uint8_t num_channels() const { return 6; } + + bool KeyOn(std::uint8_t ch, const Pitch& pitch); + bool KeyOff(std::uint8_t ch); + + bool SetInstrument(const FmInstrument& instrument); + + bool Generate(float* left_buffer, float* right_buffer, + std::uint32_t num_samples); + + private: + std::unique_ptr chip_; + + void WriteLow(std::uint8_t address, std::uint8_t data); + void WriteHigh(std::uint8_t address, std::uint8_t data); +}; +} // namespace synth::chip diff --git a/wasm-src/src/synth2.cpp b/wasm-src/src/synth.cpp similarity index 93% rename from wasm-src/src/synth2.cpp rename to wasm-src/src/synth.cpp index d73da2c..c97a04b 100644 --- a/wasm-src/src/synth2.cpp +++ b/wasm-src/src/synth.cpp @@ -14,6 +14,7 @@ #include #include "chip/ym2608.hpp" +#include "chip/ym2612.hpp" #include "instrument.hpp" #include "keyboard.hpp" #include "miniaudio.h" @@ -21,7 +22,7 @@ namespace { // Chip instance. -using VariantChip = std::variant; +using VariantChip = std::variant; VariantChip chip_instance; // Temporary buffer. @@ -90,13 +91,11 @@ std::mutex chip_input_mutex, chip_output_mutex; namespace synth { enum ChipType { - Ym2203 = 0, - Ym2608 = 1, - Ym2610 = 2, - Ym2612 = 3, + Ym2608 = 0, + Ym2612 = 1, }; -bool SetSamplingRateInternal(std::uint32_t rate) { +bool initializeResampler(std::uint32_t rate) { ma_resampler_uninit(&left_resampler, nullptr); ma_resampler_uninit(&right_resampler, nullptr); @@ -111,26 +110,36 @@ bool SetSamplingRateInternal(std::uint32_t rate) { return false; } - sampling_rate = rate; return true; } bool SetSamplingRate(std::uint32_t rate) { std::scoped_lock lock(chip_input_mutex, chip_output_mutex); - return SetSamplingRateInternal(rate); + bool result = initializeResampler(rate); + if (result) { + sampling_rate = rate; + } + + return result; } -bool SwitchChip(ChipType type, std::uint32_t rate) { +bool ChangeChip(int type) { std::scoped_lock lock(chip_input_mutex, chip_output_mutex); - switch (type) { + switch (static_cast(type)) { case ChipType::Ym2608: if (!std::holds_alternative(chip_instance)) { chip_instance.emplace(); } break; + case ChipType::Ym2612: + if (!std::holds_alternative(chip_instance)) { + chip_instance.emplace(); + } + break; + default: // Unsupported chip type. return false; @@ -141,11 +150,7 @@ bool SwitchChip(ChipType type, std::uint32_t rate) { [](const auto& chip) { return chip.num_channels(); }, chip_instance); keyboard = Keyboard(num_channels); - return SetSamplingRateInternal(rate); -} - -bool ChangeChip(int type) { - return SwitchChip(static_cast(type), sampling_rate); + return initializeResampler(sampling_rate); } bool Initialize() { return ChangeChip(ChipType::Ym2608); }